summaryrefslogtreecommitdiff
path: root/lib/chef/shell
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/shell')
-rw-r--r--lib/chef/shell/ext.rb593
-rw-r--r--lib/chef/shell/model_wrapper.rb120
-rw-r--r--lib/chef/shell/shell_rest.rb28
-rw-r--r--lib/chef/shell/shell_session.rb298
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