diff options
Diffstat (limited to 'lib/chef/shell')
-rw-r--r-- | lib/chef/shell/ext.rb | 593 | ||||
-rw-r--r-- | lib/chef/shell/model_wrapper.rb | 120 | ||||
-rw-r--r-- | lib/chef/shell/shell_rest.rb | 28 | ||||
-rw-r--r-- | lib/chef/shell/shell_session.rb | 298 |
4 files changed, 1039 insertions, 0 deletions
diff --git a/lib/chef/shell/ext.rb b/lib/chef/shell/ext.rb new file mode 100644 index 0000000000..30d841e4cd --- /dev/null +++ b/lib/chef/shell/ext.rb @@ -0,0 +1,593 @@ +#-- +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Copyright:: Copyright (c) 2009 Daniel DeLeo +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'tempfile' +require 'chef/recipe' +require 'fileutils' +require 'chef/dsl/platform_introspection' +require 'chef/version' +require 'chef/shell/shell_session' +require 'chef/shell/model_wrapper' +require 'chef/shell/shell_rest' +require 'chef/json_compat' + +module Shell + module Extensions + + Help = Struct.new(:cmd, :desc, :explanation) + + # Extensions to be included in every 'main' object in chef-shell. + # These objects are extended with this module. + module ObjectCoreExtensions + + def ensure_session_select_defined + # irb breaks if you prematurely define IRB::JobMangager + # so these methods need to be defined at the latest possible time. + unless jobs.respond_to?(:select_session_by_context) + def jobs.select_session_by_context(&block) + @jobs.select { |job| block.call(job[1].context.main)} + end + end + + unless jobs.respond_to?(:session_select) + def jobs.select_shell_session(target_context) + session = if target_context.kind_of?(Class) + select_session_by_context { |main| main.kind_of?(target_context) } + else + select_session_by_context { |main| main.equal?(target_context) } + end + Array(session.first)[1] + end + end + end + + def find_or_create_session_for(context_obj) + ensure_session_select_defined + if subsession = jobs.select_shell_session(context_obj) + jobs.switch(subsession) + else + irb(context_obj) + end + end + + def help_banner + banner = [] + banner << "" + banner << "chef-shell Help" + banner << "".ljust(80, "=") + banner << "| " + "Command".ljust(25) + "| " + "Description" + banner << "".ljust(80, "=") + + self.all_help_descriptions.each do |help_text| + banner << "| " + help_text.cmd.ljust(25) + "| " + help_text.desc + end + banner << "".ljust(80, "=") + banner << "\n" + banner << "Use help(:command) to get detailed help with individual commands" + banner << "\n" + banner.join("\n") + end + + def explain_command(method_name) + help = self.all_help_descriptions.find { |h| h.cmd.to_s == method_name.to_s } + if help + puts "" + puts "Command: #{method_name}" + puts "".ljust(80, "=") + puts help.explanation || help.desc + puts "".ljust(80, "=") + puts "" + else + puts "" + puts "command #{method_name} not found or no help available" + puts "" + end + end + + # helpfully returns +:on+ so we can have sugary syntax like `tracing on' + def on + :on + end + + # returns +:off+ so you can just do `tracing off' + def off + :off + end + + def help_descriptions + @help_descriptions ||= [] + end + + def all_help_descriptions + help_descriptions + end + + def desc(help_text) + @desc = help_text + end + + def explain(explain_text) + @explain = explain_text + end + + def subcommands(subcommand_help={}) + @subcommand_help = subcommand_help + end + + def singleton_method_added(mname) + if @desc + help_descriptions << Help.new(mname.to_s, @desc.to_s, @explain) + @desc, @explain = nil, nil + end + if @subcommand_help + @subcommand_help.each do |subcommand, text| + help_descriptions << Help.new("#{mname}.#{subcommand}", text.to_s, nil) + end + end + @subcommand_help = {} + end + + end + + module String + def on_off_to_bool + case self + when "on" + true + when "off" + false + else + self + end + end + end + + module Symbol + def on_off_to_bool + self.to_s.on_off_to_bool + end + end + + module TrueClass + def to_on_off_str + "on" + end + + def on_off_to_bool + self + end + end + + module FalseClass + def to_on_off_str + "off" + end + + def on_off_to_bool + self + end + end + + # Methods that have associated help text need to be dynamically added + # to the main irb objects, so we define them in a proc and later + # instance_eval the proc in the object. + ObjectUIExtensions = Proc.new do + extend Shell::Extensions::ObjectCoreExtensions + + desc "prints this help message" + explain(<<-E) +## SUMMARY ## + When called with no argument, +help+ prints a table of all + chef-shell commands. When called with an argument COMMAND, +help+ + prints a detailed explanation of the command if available, or the + description if no explanation is available. +E + def help(commmand=nil) + if commmand + explain_command(commmand) + else + puts help_banner + end + :ucanhaz_halp + end + alias :halp :help + + desc "prints information about chef" + def version + puts "This is the chef-shell.\n" + + " Chef Version: #{::Chef::VERSION}\n" + + " http://www.opscode.com/chef\n" + + " http://wiki.opscode.com/display/chef/Home" + :ucanhaz_automation + end + alias :shell :version + + desc "switch to recipe mode" + def recipe_mode + find_or_create_session_for Shell.session.recipe + :recipe + end + + desc "switch to attributes mode" + def attributes_mode + find_or_create_session_for Shell.session.node + :attributes + end + + desc "run chef using the current recipe" + def run_chef + Chef::Log.level = :debug + session = Shell.session + runrun = Chef::Runner.new(session.run_context).converge + Chef::Log.level = :info + runrun + end + + desc "returns an object to control a paused chef run" + subcommands :resume => "resume the chef run", + :step => "run only the next resource", + :skip_back => "move back in the run list", + :skip_forward => "move forward in the run list" + def chef_run + Shell.session.resource_collection.iterator + end + + desc "resets the current recipe" + def reset + Shell.session.reset! + end + + desc "assume the identity of another node." + def become_node(node_name) + Shell::DoppelGangerSession.instance.assume_identity(node_name) + :doppelganger + end + alias :doppelganger :become_node + + desc "turns printout of return values on or off" + def echo(on_or_off) + conf.echo = on_or_off.on_off_to_bool + end + + desc "says if echo is on or off" + def echo? + puts "echo is #{conf.echo.to_on_off_str}" + end + + desc "turns on or off tracing of execution. *verbose*" + def tracing(on_or_off) + conf.use_tracer = on_or_off.on_off_to_bool + tracing? + end + alias :trace :tracing + + desc "says if tracing is on or off" + def tracing? + puts "tracing is #{conf.use_tracer.to_on_off_str}" + end + alias :trace? :tracing? + + desc "simple ls style command" + def ls(directory) + Dir.entries(directory) + end + end + + MainContextExtensions = Proc.new do + desc "returns the current node (i.e., this host)" + def node + Shell.session.node + end + + desc "pretty print the node's attributes" + def ohai(key=nil) + pp(key ? node.attribute[key] : node.attribute) + end + end + + RESTApiExtensions = Proc.new do + desc "edit an object in your EDITOR" + explain(<<-E) +## SUMMARY ## + +edit(object)+ allows you to edit any object that can be converted to JSON. + When finished editing, this method will return the edited object: + + new_node = edit(existing_node) + +## EDITOR SELECTION ## + chef-shell looks for an editor using the following logic + 1. Looks for an EDITOR set by Shell.editor = "EDITOR" + 2. Looks for an EDITOR configured in your chef-shell config file + 3. Uses the value of the EDITOR environment variable +E + def edit(object) + unless Shell.editor + puts "Please set your editor with Shell.editor = \"vim|emacs|mate|ed\"" + return :failburger + end + + filename = "chef-shell-edit-#{object.class.name}-" + if object.respond_to?(:name) + filename += object.name + elsif object.respond_to?(:id) + filename += object.id + end + + edited_data = Tempfile.open([filename, ".js"]) do |tempfile| + tempfile.sync = true + tempfile.puts Chef::JSONCompat.to_json(object) + system("#{Shell.editor.to_s} #{tempfile.path}") + tempfile.rewind + tempfile.read + end + + Chef::JSONCompat.from_json(edited_data) + end + + desc "Find and edit API clients" + explain(<<-E) +## SUMMARY ## + +clients+ allows you to query you chef server for information about your api + clients. + +## LIST ALL CLIENTS ## + To see all clients on the system, use + + clients.all #=> [<Chef::ApiClient...>, ...] + + If the output from all is too verbose, or you're only interested in a specific + value from each of the objects, you can give a code block to +all+: + + clients.all { |client| client.name } #=> [CLIENT1_NAME, CLIENT2_NAME, ...] + +## SHOW ONE CLIENT ## + To see a specific client, use + + clients.show(CLIENT_NAME) + +## SEARCH FOR CLIENTS ## + You can also search for clients using +find+ or +search+. You can use the + familiar string search syntax: + + clients.search("KEY:VALUE") + + Just as the +all+ subcommand, the +search+ subcommand can use a code block to + filter or transform the information returned from the search: + + clients.search("KEY:VALUE") { |c| c.name } + + You can also use a Hash based syntax, multiple search conditions will be + joined with AND. + + clients.find :KEY => :VALUE, :KEY2 => :VALUE2, ... + +## BULK-EDIT CLIENTS ## + **BE CAREFUL, THIS IS DESTRUCTIVE** + You can bulk edit API Clients using the +transform+ subcommand, which requires + a code block. Each client will be saved after the code block is run. If the + code block returns +nil+ or +false+, that client will be skipped: + + clients.transform("*:*") do |client| + if client.name =~ /borat/i + client.admin(false) + true + else + nil + end + end + + This will strip the admin privileges from any client named after borat. +E + subcommands :all => "list all api clients", + :show => "load an api client by name", + :search => "search for API clients", + :transform => "edit all api clients via a code block and save them" + def clients + @clients ||= Shell::ModelWrapper.new(Chef::ApiClient, :client) + end + + desc "Find and edit cookbooks" + subcommands :all => "list all cookbooks", + :show => "load a cookbook by name", + :transform => "edit all cookbooks via a code block and save them" + def cookbooks + @cookbooks ||= Shell::ModelWrapper.new(Chef::CookbookVersion) + end + + desc "Find and edit nodes via the API" + explain(<<-E) +## SUMMARY ## + +nodes+ Allows you to query your chef server for information about your nodes. + +## LIST ALL NODES ## + You can list all nodes using +all+ or +list+ + + nodes.all #=> [<Chef::Node...>, <Chef::Node...>, ...] + + To limit the information returned for each node, pass a code block to the +all+ + subcommand: + + nodes.all { |node| node.name } #=> [NODE1_NAME, NODE2_NAME, ...] + +## SHOW ONE NODE ## + You can show the data for a single node using the +show+ subcommand: + + nodes.show("NODE_NAME") => <Chef::Node @name="NODE_NAME" ...> + +## SEARCH FOR NODES ## + You can search for nodes using the +search+ or +find+ subcommands: + + nodes.find(:name => "app*") #=> [<Chef::Node @name="app1.example.com" ...>, ...] + + Similarly to +all+, you can pass a code block to limit or transform the + information returned: + + nodes.find(:name => "app#") { |node| node.ec2 } + +## BULK EDIT NODES ## + **BE CAREFUL, THIS OPERATION IS DESTRUCTIVE** + + Bulk edit nodes by passing a code block to the +transform+ or +bulk_edit+ + subcommand. The block will be applied to each matching node, and then the node + will be saved. If the block returns +nil+ or +false+, that node will be + skipped. + + nodes.transform do |node| + if node.fqdn =~ /.*\\.preprod\\.example\\.com/ + node.set[:environment] = "preprod" + end + end + + This will assign the attribute to every node with a FQDN matching the regex. +E + subcommands :all => "list all nodes", + :show => "load a node by name", + :search => "search for nodes", + :transform => "edit all nodes via a code block and save them" + def nodes + @nodes ||= Shell::ModelWrapper.new(Chef::Node) + end + + desc "Find and edit roles via the API" + explain(<<-E) +## SUMMARY ## + +roles+ allows you to query and edit roles on your Chef server. + +## SUBCOMMANDS ## + * all (list) + * show (load) + * search (find) + * transform (bulk_edit) + +## SEE ALSO ## + See the help for +nodes+ for more information about the subcommands. +E + subcommands :all => "list all roles", + :show => "load a role by name", + :search => "search for roles", + :transform => "edit all roles via a code block and save them" + def roles + @roles ||= Shell::ModelWrapper.new(Chef::Role) + end + + desc "Find and edit +databag_name+ via the api" + explain(<<-E) +## SUMMARY ## + +databags(DATABAG_NAME)+ allows you to query and edit data bag items on your + Chef server. Unlike other commands for working with data on the server, + +databags+ requires the databag name as an argument, for example: + databags(:users).all + +## SUBCOMMANDS ## + * all (list) + * show (load) + * search (find) + * transform (bulk_edit) + +## SEE ALSO ## + See the help for +nodes+ for more information about the subcommands. + +E + subcommands :all => "list all items in the data bag", + :show => "load a data bag item by id", + :search => "search for items in the data bag", + :transform => "edit all items via a code block and save them" + def databags(databag_name) + @named_databags_wrappers ||= {} + @named_databags_wrappers[databag_name] ||= Shell::NamedDataBagWrapper.new(databag_name) + end + + desc "Find and edit environments via the API" + explain(<<-E) +## SUMMARY ## + +environments+ allows you to query and edit environments on your Chef server. + +## SUBCOMMANDS ## + * all (list) + * show (load) + * search (find) + * transform (bulk_edit) + +## SEE ALSO ## + See the help for +nodes+ for more information about the subcommands. +E + subcommands :all => "list all environments", + :show => "load an environment by name", + :search => "search for environments", + :transform => "edit all environments via a code block and save them" + def environments + @environments ||= Shell::ModelWrapper.new(Chef::Environment) + end + + desc "A REST Client configured to authenticate with the API" + def api + @rest = Shell::ShellREST.new(Chef::Config[:chef_server_url]) + end + + end + + RecipeUIExtensions = Proc.new do + alias :original_resources :resources + + desc "list all the resources on the current recipe" + def resources(*args) + if args.empty? + pp run_context.resource_collection.instance_variable_get(:@resources_by_name).keys + else + pp resources = original_resources(*args) + resources + end + end + end + + def self.extend_context_object(obj) + obj.instance_eval(&ObjectUIExtensions) + obj.instance_eval(&MainContextExtensions) + obj.instance_eval(&RESTApiExtensions) + obj.extend(FileUtils) + obj.extend(Chef::DSL::PlatformIntrospection) + obj.extend(Chef::DSL::DataQuery) + end + + def self.extend_context_node(node_obj) + node_obj.instance_eval(&ObjectUIExtensions) + end + + def self.extend_context_recipe(recipe_obj) + recipe_obj.instance_eval(&ObjectUIExtensions) + recipe_obj.instance_eval(&RecipeUIExtensions) + end + + end +end + +class String + include Shell::Extensions::String +end + +class Symbol + include Shell::Extensions::Symbol +end + +class TrueClass + include Shell::Extensions::TrueClass +end + +class FalseClass + include Shell::Extensions::FalseClass +end diff --git a/lib/chef/shell/model_wrapper.rb b/lib/chef/shell/model_wrapper.rb new file mode 100644 index 0000000000..7ee39de7eb --- /dev/null +++ b/lib/chef/shell/model_wrapper.rb @@ -0,0 +1,120 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/convert_to_class_name' +require 'chef/mixin/language' + +module Shell + class ModelWrapper + + include Chef::Mixin::ConvertToClassName + + attr_reader :model_symbol + + def initialize(model_class, symbol=nil) + @model_class = model_class + @model_symbol = symbol || convert_to_snake_case(model_class.name, "Chef").to_sym + end + + def search(query) + return all if query.to_s == "all" + results = [] + Chef::Search::Query.new.search(@model_symbol, format_query(query)) do |obj| + if block_given? + results << yield(obj) + else + results << obj + end + end + results + end + + alias :find :search + + def all(&block) + all_objects = list_objects + block_given? ? all_objects.map(&block) : all_objects + end + + alias :list :all + + def show(obj_id) + @model_class.load(obj_id) + end + + alias :load :show + + def transform(what_to_transform, &block) + if what_to_transform == :all + objects_to_transform = list_objects + else + objects_to_transform = search(what_to_transform) + end + objects_to_transform.each do |obj| + if result = yield(obj) + obj.save + end + end + end + + alias :bulk_edit :transform + + private + + # paper over inconsistencies in the model classes APIs, and return the objects + # the user wanted instead of the URI=>object stuff + def list_objects + objects = @model_class.method(:list).arity == 0? @model_class.list : @model_class.list(true) + objects.map { |obj| Array(obj).find {|o| o.kind_of?(@model_class)} } + end + + def format_query(query) + if query.respond_to?(:keys) + query.map { |key, value| "#{key}:#{value}" }.join(" AND ") + else + query + end + end + end + + class NamedDataBagWrapper < ModelWrapper + + def initialize(databag_name) + @model_symbol = @databag_name = databag_name + end + + + alias :list :all + + def show(item) + Chef::DataBagItem.load(@databag_name, item) + end + + private + + def list_objects + all_items = [] + Chef::Search::Query.new.search(@databag_name) do |item| + all_items << item + end + all_items + end + + end + +end diff --git a/lib/chef/shell/shell_rest.rb b/lib/chef/shell/shell_rest.rb new file mode 100644 index 0000000000..a485a0a1a8 --- /dev/null +++ b/lib/chef/shell/shell_rest.rb @@ -0,0 +1,28 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module Shell + class ShellREST < Chef::REST + + alias :get :get_rest + alias :put :put_rest + alias :post :post_rest + alias :delete :delete_rest + + end +end diff --git a/lib/chef/shell/shell_session.rb b/lib/chef/shell/shell_session.rb new file mode 100644 index 0000000000..287cf0c166 --- /dev/null +++ b/lib/chef/shell/shell_session.rb @@ -0,0 +1,298 @@ +#-- +# Author:: Daniel DeLeo (<dan@kallistec.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Copyright:: Copyright (c) 2009 Daniel DeLeo +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/recipe' +require 'chef/run_context' +require 'chef/config' +require 'chef/client' +require 'chef/cookbook/cookbook_collection' +require 'chef/cookbook_loader' +require 'chef/run_list/run_list_expansion' +require 'chef/formatters/base' +require 'chef/formatters/doc' +require 'chef/formatters/minimal' + +module Shell + class ShellSession + include Singleton + + def self.session_type(type=nil) + @session_type = type if type + @session_type + end + + attr_accessor :node, :compile, :recipe, :run_context + attr_reader :node_attributes, :client + def initialize + @node_built = false + formatter = Chef::Formatters.new(Chef::Config.formatter, STDOUT, STDERR) + @events = Chef::EventDispatch::Dispatcher.new(formatter) + end + + def node_built? + !!@node_built + end + + def reset! + loading do + rebuild_node + @node = client.node + shorten_node_inspect + Shell::Extensions.extend_context_node(@node) + rebuild_context + node.consume_attributes(node_attributes) if node_attributes + @recipe = Chef::Recipe.new(nil, nil, run_context) + Shell::Extensions.extend_context_recipe(@recipe) + @node_built = true + end + end + + def node_attributes=(attrs) + @node_attributes = attrs + @node.consume_attributes(@node_attributes) + end + + def resource_collection + run_context.resource_collection + end + + def run_context + @run_context ||= rebuild_context + end + + def definitions + nil + end + + def cookbook_loader + nil + end + + def save_node + raise "Not Supported! #{self.class.name} doesn't support #save_node, maybe you need to run chef-shell in client mode?" + end + + def rebuild_context + raise "Not Implemented! :rebuild_collection should be implemented by subclasses" + end + + private + + def loading + show_loading_progress + begin + yield + rescue => e + loading_complete(false) + raise e + else + loading_complete(true) + end + end + + def show_loading_progress + print "Loading" + @loading = true + @dot_printer = Thread.new do + while @loading + print "." + sleep 0.5 + end + end + end + + def loading_complete(success) + @loading = false + @dot_printer.join + msg = success ? "done.\n\n" : "epic fail!\n\n" + print msg + end + + def shorten_node_inspect + def @node.inspect + "<Chef::Node:0x#{self.object_id.to_s(16)} @name=\"#{self.name}\">" + end + end + + def rebuild_node + raise "Not Implemented! :rebuild_node should be implemented by subclasses" + end + + end + + class StandAloneSession < ShellSession + + session_type :standalone + + def rebuild_context + cookbook_collection = Chef::CookbookCollection.new({}) + @run_context = Chef::RunContext.new(@node, cookbook_collection, @events) # no recipes + @run_context.load(Chef::RunList::RunListExpansionFromDisk.new("_default", [])) # empty recipe list + end + + private + + def rebuild_node + Chef::Config[:solo] = true + @client = Chef::Client.new + @client.run_ohai + @client.load_node + @client.build_node + end + + end + + class SoloSession < ShellSession + + session_type :solo + + def definitions + @run_context.definitions + end + + def rebuild_context + @run_status = Chef::RunStatus.new(@node, @events) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest, Chef::Config[:cookbook_path]) } + cl = Chef::CookbookLoader.new(Chef::Config[:cookbook_path]) + cl.load_cookbooks + cookbook_collection = Chef::CookbookCollection.new(cl) + @run_context = Chef::RunContext.new(node, cookbook_collection, @events) + @run_context.load(Chef::RunList::RunListExpansionFromDisk.new("_default", [])) + @run_status.run_context = run_context + end + + private + + def rebuild_node + # Tell the client we're chef solo so it won't try to contact the server + Chef::Config[:solo] = true + @client = Chef::Client.new + @client.run_ohai + @client.load_node + @client.build_node + end + + end + + class ClientSession < SoloSession + + session_type :client + + def save_node + @client.save_node + end + + def rebuild_context + @run_status = Chef::RunStatus.new(@node, @events) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, Chef::REST.new(Chef::Config[:server_url])) } + cookbook_hash = @client.sync_cookbooks + cookbook_collection = Chef::CookbookCollection.new(cookbook_hash) + @run_context = Chef::RunContext.new(node, cookbook_collection, @events) + @run_context.load(Chef::RunList::RunListExpansionFromAPI.new("_default", [])) + @run_status.run_context = run_context + end + + private + + def rebuild_node + # Make sure the client knows this is not chef solo + Chef::Config[:solo] = false + @client = Chef::Client.new + @client.run_ohai + @client.register + @client.load_node + @client.build_node + end + + end + + class DoppelGangerClient < Chef::Client + + attr_reader :node_name + + def initialize(node_name) + @node_name = node_name + @ohai = Ohai::System.new + end + + # Run the very smallest amount of ohai we can get away with and still + # hope to have things work. Otherwise we're not very good doppelgangers + def run_ohai + @ohai.require_plugin('os') + end + + # DoppelGanger implementation of build_node. preserves as many of the node's + # attributes, and does not save updates to the server + def build_node + Chef::Log.debug("Building node object for #{@node_name}") + @node = Chef::Node.find_or_create(node_name) + ohai_data = @ohai.data.merge(@node.automatic_attrs) + @node.consume_external_attrs(ohai_data,nil) + @run_list_expansion = @node.expand!('server') + @expanded_run_list_with_versions = @run_list_expansion.recipes.with_version_constraints_strings + Chef::Log.info("Run List is [#{@node.run_list}]") + Chef::Log.info("Run List expands to [#{@expanded_run_list_with_versions.join(', ')}]") + @node + end + + def register + @rest = Chef::REST.new(Chef::Config[:chef_server_url], Chef::Config[:node_name], Chef::Config[:client_key]) + end + + end + + class DoppelGangerSession < ClientSession + + session_type "doppelganger client" + + def save_node + puts "A doppelganger should think twice before saving the node" + end + + def assume_identity(node_name) + Chef::Config[:doppelganger] = @node_name = node_name + reset! + rescue Exception => e + puts "#{e.class.name}: #{e.message}" + puts Array(e.backtrace).join("\n") + puts + puts "* " * 40 + puts "failed to assume the identity of node '#{node_name}', resetting" + puts "* " * 40 + puts + Chef::Config[:doppelganger] = false + @node_built = false + Shell.session + end + + def rebuild_node + # Make sure the client knows this is not chef solo + Chef::Config[:solo] = false + @client = DoppelGangerClient.new(@node_name) + @client.run_ohai + @client.register + @client.load_node + @client.build_node + @client.sync_cookbooks + end + + end + +end |