diff options
| author | Daniel DeLeo <dan@opscode.com> | 2010-07-24 11:56:03 -0700 |
|---|---|---|
| committer | Daniel DeLeo <dan@opscode.com> | 2010-07-24 11:56:03 -0700 |
| commit | 070d3ece70812317fd20c272c5d7d7d50d3f20ab (patch) | |
| tree | dbe19b4fb137563b5bc014481e3e8375a8e5d853 | |
| parent | 4b4e0382c61e9b86a035861211747544bf112a3d (diff) | |
| download | chef-070d3ece70812317fd20c272c5d7d7d50d3f20ab.tar.gz | |
[CHEF-1497] rm monkey patches and add config file for shef
* Apply UX methods to irb context objects via instance_eval()ing blocks
* Modify shef config loading:
* default to ~/.chef/shef.rb
* support multi-config via CLI argument
* -l LOG_LEVEL so debug ouput can be gathered during load.
| -rwxr-xr-x | chef/bin/shef | 23 | ||||
| -rw-r--r-- | chef/lib/chef/shef.rb | 163 | ||||
| -rw-r--r-- | chef/lib/chef/shef/ext.rb | 346 | ||||
| -rw-r--r-- | chef/lib/chef/shef/shef_session.rb | 4 | ||||
| -rw-r--r-- | chef/spec/unit/shef_spec.rb | 53 |
5 files changed, 330 insertions, 259 deletions
diff --git a/chef/bin/shef b/chef/bin/shef index ea9896b45e..9f42aa25eb 100755 --- a/chef/bin/shef +++ b/chef/bin/shef @@ -18,30 +18,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -# TODO::EVIL -require "rubygems" -require "mixlib/cli" +begin + require "rubygems" +rescue LoadError +end require "irb" require "irb/completion" require 'irb/ext/save-history' -# TODO::EVIL $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) require "chef/shef" -# dirty hack to make IRB initialize shef -module IRB - class << self - alias :run_original_config :run_config - - def run_config - run_original_config - Shef.init - end - end -end - -Shef.parse_opts -IRB.start +Shef.start diff --git a/chef/lib/chef/shef.rb b/chef/lib/chef/shef.rb index 70c292dd61..0656211525 100644 --- a/chef/lib/chef/shef.rb +++ b/chef/lib/chef/shef.rb @@ -5,9 +5,9 @@ # 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. @@ -20,6 +20,7 @@ require "pp" require "etc" require "mixlib/cli" +require 'chef/version' require "chef/client" require "chef/config" @@ -31,38 +32,71 @@ module Shef LEADERS = Hash.new("") LEADERS[Chef::Recipe] = ":recipe" LEADERS[Chef::Node] = ":attributes" - + class << self attr_accessor :client_type, :options end - + + # Start the irb REPL with shef's customizations + def self.start + setup_logger + # FUGLY HACK: irb gives us no other choice. + irb_help = [:help, :irb_help, IRB::ExtendCommandBundle::NO_OVERRIDE] + IRB::ExtendCommandBundle.instance_variable_get(:@ALIASES).delete(irb_help) + + parse_opts + + # HACK: this duplicates the functions of IRB.start, but we have to do it + # to get access to the main object before irb starts. + ::IRB.setup(nil) + + irb = IRB::Irb.new + + init(irb.context.main) + + + irb_conf[:IRB_RC].call(irb.context) if irb_conf[:IRB_RC] + irb_conf[:MAIN_CONTEXT] = irb.context + + trap("SIGINT") do + irb.signal_handle + end + + catch(:IRB_EXIT) do + irb.eval_input + end + end + + def self.setup_logger + Chef::Config[:log_level] ||= :warn + Chef::Log.init(STDERR) + Mixlib::Authentication::Log.logger = Ohai::Log.logger = Chef::Log.logger + Chef::Log.level = Chef::Config[:log_level] || :warn + end + # Shef assumes it's running whenever it is defined def self.running? true end - + # Set the irb_conf object to something other than IRB.conf # usful for testing. def self.irb_conf=(conf_hash) @irb_conf = conf_hash end - + def self.irb_conf @irb_conf || IRB.conf end - + def self.configure_irb irb_conf[:HISTORY_FILE] = "~/.shef_history" irb_conf[:SAVE_HISTORY] = 1000 - + irb_conf[:IRB_RC] = lambda do |conf| m = conf.main leader = LEADERS[m.class] - def m.help - shef_help - end - conf.prompt_c = "chef#{leader} > " conf.return_format = " => %s \n" conf.prompt_i = "chef#{leader} > " @@ -70,35 +104,39 @@ module Shef conf.prompt_s = "chef#{leader}%l> " end end - + def self.session client_type.instance.reset! unless client_type.instance.node_built? client_type.instance end - - def self.init + + def self.init(main) parse_json configure_irb session # trigger ohai run + session load - + session.node.consume_attributes(@json_attribs) - greeting = begin - " #{Etc.getlogin}@#{Shef.session.node.name}" - rescue NameError - "" - end + Extensions.extend_context_object(main) - version + main.version puts puts "run `help' for help, `exit' or ^D to quit." puts puts "Ohai2u#{greeting}!" end - + + def self.greeting + " #{Etc.getlogin}@#{Shef.session.node.name}" + rescue NameError + "" + end + def self.parse_json + # HACK: copied verbatim from chef/application/client, because it's not + # reusable as written there :( if Chef::Config[:json_attribs] begin json_io = open(Chef::Config[:json_attribs]) @@ -119,28 +157,43 @@ module Shef end end end - + def self.fatal!(message, exit_status) Chef::Log.fatal(message) exit exit_status end - + def self.client_type type = Shef::StandAloneSession type = Shef::SoloSession if Chef::Config[:shef_solo] type = Shef::ClientSession if Chef::Config[:client] type end - + def self.parse_opts @options = Options.new @options.parse_opts end - + class Options include Mixlib::CLI - option :config_file, + def self.footer(text=nil) + @footer = text if text + @footer + end + + banner("shef #{Chef::VERSION}\n\nUsage: shef [NAMED_CONF] (OPTIONS)") + + footer(<<-FOOTER) +When no CONFIG is specified, shef attempts to load a default configuration file: +* If a NAMED_CONF is given, shef will load ~/.chef/NAMED_CONF/shef.rb +* If no NAMED_CONF is given shef will load ~/.chef/shef.rb if it exists +* Shef falls back to loading /etc/chef/client.rb or /etc/chef/solo.rb if -z or + -s options are given and no shef.rb can be found. +FOOTER + + option :config_file, :short => "-c CONFIG", :long => "--config CONFIG", :description => "The configuration file to use" @@ -151,8 +204,13 @@ module Shef :description => "Show this message", :on => :tail, :boolean => true, - :show_options => true, - :exit => 0 + :proc => proc { print_help } + + option :log_level, + :short => "-l LOG_LEVEL", + :long => '--log-level LOG_LEVEL', + :description => "Set the logging level", + :proc => proc { |level| Chef::Log.level = level.to_sym } option :standalone, :short => "-a", @@ -194,31 +252,56 @@ module Shef :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"}, :exit => 0 + def self.print_help + instance = new + instance.parse_options([]) + puts instance.opt_parser + puts + puts footer + puts + exit 1 + end + def self.setup! self.new.parse_opts end def parse_opts - parse_options + remainder = parse_options + environment = remainder.first # We have to nuke ARGV to make sure irb's option parser never sees it. # otherwise, IRB complains about command line switches it doesn't recognize. ARGV.clear - config[:config_file] = config_file_for_shef_mode + config[:config_file] = config_file_for_shef_mode(environment) config_msg = config[:config_file] || "none (standalone shef session)" puts "loading configuration: #{config_msg}" Chef::Config.from_file(config[:config_file]) if !config[:config_file].nil? && File.exists?(config[:config_file]) && File.readable?(config[:config_file]) Chef::Config.merge!(config) end - + private - - def config_file_for_shef_mode - return config[:config_file] if config[:config_file] - return "/etc/chef/solo.rb" if config[:solo] - return "/etc/chef/client.rb" if config[:client] - nil + + def config_file_for_shef_mode(environment) + if config[:config_file] + config[:config_file] + elsif environment + config_file_to_try = ::File.join(ENV['HOME'], '.chef', environment, 'shef.rb') + unless ::File.exist?(config_file_to_try) + puts "could not find shef config for environment #{environment} at #{config_file_to_try}" + exit 1 + end + config_file_to_try + elsif ENV['HOME'] && ::File.exist?(File.join(ENV['HOME'], '.chef', 'shef.rb')) + File.join(ENV['HOME'], '.chef', 'shef.rb') + elsif config[:solo] + "/etc/chef/solo.rb" + elsif config[:client] + "/etc/chef/client.rb" + else + nil + end end end - + end
\ No newline at end of file diff --git a/chef/lib/chef/shef/ext.rb b/chef/lib/chef/shef/ext.rb index 1147243d60..31ba587f97 100644 --- a/chef/lib/chef/shef/ext.rb +++ b/chef/lib/chef/shef/ext.rb @@ -5,9 +5,9 @@ # 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. @@ -21,13 +21,11 @@ require 'chef/shef/shef_session' module Shef module Extensions - - # Extensions to be included in object. These are methods that have to be - # defined on object but are not part of the user interface. Methods that - # are part of the user interface should have help text defined with the - # +desc+ macro, and need to be defined directly on Object in ext.rb - module Object - + + # Extensions to be included in every 'main' object in shef. 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. @@ -57,7 +55,7 @@ module Shef irb(context_obj) end end - + def help_banner(title=nil) banner = [] banner << "" @@ -66,63 +64,59 @@ module Shef banner << "| " + "Command".ljust(25) + "| " + "Description" banner << "".ljust(80, "=") - self.class.all_help_descriptions.each do |cmd, description| + self.all_help_descriptions.each do |cmd, description| banner << "| " + cmd.ljust(25) + "| " + description end banner << "".ljust(80, "=") banner << "\n" banner.join("\n") 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 - - module ClassMethods - def help_descriptions - @help_descriptions ||= [] - end - - def all_help_descriptions - if (sc = superclass) && superclass.respond_to?(:help_descriptions) - help_descriptions + sc.help_descriptions - else - help_descriptions - end - end + def help_descriptions + @help_descriptions ||= [] + end - def desc(help_text) - @desc = help_text - end - - def subcommands(subcommand_help={}) - @subcommand_help = subcommand_help - end + def all_help_descriptions + #if (sc = superclass) && superclass.respond_to?(:help_descriptions) + # help_descriptions + sc.help_descriptions + #else + help_descriptions + #end + end - def method_added(mname) - if @desc - help_descriptions << [mname.to_s, @desc.to_s] - @desc = nil - end - if @subcommand_help - @subcommand_help.each do |subcommand, text| - help_descriptions << ["#{mname}.#{subcommand}", text.to_s] - end + def desc(help_text) + @desc = help_text + end + + def subcommands(subcommand_help={}) + @subcommand_help = subcommand_help + end + + def singleton_method_added(mname) + if @desc + help_descriptions << [mname.to_s, @desc.to_s] + @desc = nil + end + if @subcommand_help + @subcommand_help.each do |subcommand, text| + help_descriptions << ["#{mname}.#{subcommand}", text.to_s] end - @subcommand_help = {} end - + @subcommand_help = {} end - + end - + module String def on_off_to_bool case self @@ -135,33 +129,155 @@ module Shef 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 Shef::Extensions::ObjectCoreExtensions + + desc "prints this help message" + def help(title="Help: Shef") + puts help_banner(title) + :ucanhaz_halp + end + alias :halp :help + + desc "prints information about chef" + def version + puts "This is shef, 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 :shef :version + + desc "switch to recipe mode" + def recipe + find_or_create_session_for Shef.session.recipe + :recipe + end + + desc "switch to attributes mode" + def attributes + find_or_create_session_for Shef.session.node + :attributes + end + + desc "returns the current node (i.e., this host)" + def node + Shef.session.node + end + + desc "pretty print the node's attributes" + def ohai(key=nil) + pp(key ? node.attribute[key] : node.attribute) + end + + desc "run chef using the current recipe" + def run_chef + Chef::Log.level = :debug + session = Shef.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 + Shef.session.resource_collection.iterator + end + + desc "resets the current recipe" + def reset + Shef.session.reset! + end + + 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 + + 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.extend(FileUtils) + end + + def self.extend_context_recipe(recipe_obj) + recipe_obj.instance_eval(&ObjectUIExtensions) + recipe_obj.instance_eval(&RecipeUIExtensions) + end + end end @@ -181,129 +297,3 @@ class FalseClass include Shef::Extensions::FalseClass end -class Object - extend Shef::Extensions::Object::ClassMethods - include Shef::Extensions::Object - include FileUtils - - desc "prints this help message" - def shef_help(title="Help: Shef") - #puts Shef::Extensions::Object.help_banner("Shef Help") - puts help_banner(title) - :ucanhaz_halp - end - alias :halp :shef_help - - desc "prints information about chef" - def version - puts "This is shef, 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 :shef :version - - desc "switch to recipe mode" - def recipe - find_or_create_session_for Shef.session.recipe - :recipe - end - - desc "switch to attributes mode" - def attributes - find_or_create_session_for Shef.session.node - :attributes - end - - desc "returns the current node (i.e., this host)" - def node - Shef.session.node - end - - desc "pretty print the node's attributes" - def ohai(key=nil) - pp(key ? node.attribute[key] : node.attribute) - end - - desc "run chef using the current recipe" - def run_chef - Chef::Log.level = :debug - session = Shef.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 - Shef.session.resource_collection.iterator - end - - desc "resets the current recipe" - def reset - Shef.session.reset! - end - - 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 - -class String - undef_method :version -end - -class NilClass - undef_method :version -end - -class Chef - class Recipe - - def shef_help - super("Help: Shef/Recipe") - end - - 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 -end diff --git a/chef/lib/chef/shef/shef_session.rb b/chef/lib/chef/shef/shef_session.rb index 91779cd5c1..a62323c6d3 100644 --- a/chef/lib/chef/shef/shef_session.rb +++ b/chef/lib/chef/shef/shef_session.rb @@ -40,10 +40,12 @@ module Shef loading do rebuild_node @node = client.node + shorten_node_inspect + Shef::Extensions.extend_context_object(@node) rebuild_context node.consume_attributes(node_attributes) if node_attributes - shorten_node_inspect @recipe = Chef::Recipe.new(nil, nil, run_context) + Shef::Extensions.extend_context_recipe(@recipe) @node_built = true end end diff --git a/chef/spec/unit/shef_spec.rb b/chef/spec/unit/shef_spec.rb index 49d8701d29..c2712c83f5 100644 --- a/chef/spec/unit/shef_spec.rb +++ b/chef/spec/unit/shef_spec.rb @@ -18,8 +18,16 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) require "ostruct" -class ObjectTestHarness - attr_accessor :conf +ObjectTestHarness = Proc.new do + extend Shef::Extensions::ObjectCoreExtensions + + def conf=(new_conf) + @conf = new_conf + end + + def conf + @conf + end desc "rspecin'" def rspec_method @@ -86,7 +94,8 @@ describe Shef do Shef.configure_irb conf = OpenStruct.new - conf.main = ObjectTestHarness.new + conf.main = Object.new + conf.main.instance_eval(&ObjectTestHarness) Shef.irb_conf[:IRB_RC].call(conf) conf.prompt_c.should == "chef > " conf.return_format.should == " => %s \n" @@ -125,11 +134,12 @@ describe Shef do describe "convenience macros for creating the chef object" do before do - @chef_object = ObjectTestHarness.new + @chef_object = Object.new + @chef_object.instance_eval(&ObjectTestHarness) end it "creates help text for methods with descriptions" do - ObjectTestHarness.help_descriptions.should == [["rspec_method", "rspecin'"]] + @chef_object.help_descriptions.should == [["rspec_method", "rspecin'"]] end it "adds help text when a new method is described then defined" do @@ -138,8 +148,8 @@ describe Shef do def baz end EVAL - ObjectTestHarness.class_eval describe_define - ObjectTestHarness.help_descriptions.should == [["rspec_method", "rspecin'"],["baz", "foo2the Bar"]] + @chef_object.instance_eval describe_define + @chef_object.help_descriptions.should == [["rspec_method", "rspecin'"],["baz", "foo2the Bar"]] end it "adds help text for subcommands" do @@ -148,10 +158,10 @@ describe Shef do def baz end EVAL - ObjectTestHarness.class_eval describe_define - expected_help_text_fragments = [["rspec_method", "rspecin'"],["baz", "foo2the Bar"]] + @chef_object.instance_eval describe_define + expected_help_text_fragments = [["rspec_method", "rspecin'"]] expected_help_text_fragments << ["baz.baz_obj_command", "something you can do with baz.baz_obj_command"] - ObjectTestHarness.help_descriptions.should == expected_help_text_fragments + @chef_object.help_descriptions.should == expected_help_text_fragments end it "doesn't add previous subcommand help to commands defined afterward" do @@ -160,8 +170,8 @@ describe Shef do def monkey_time end EVAL - ObjectTestHarness.class_eval describe_define - ObjectTestHarness.help_descriptions.should_not include(["monkey_time.baz_obj_command", "something you can do with baz.baz_obj_command"]) + @chef_object.instance_eval describe_define + @chef_object.help_descriptions.should_not include(["monkey_time.baz_obj_command", "something you can do with baz.baz_obj_command"]) end it "creates a help banner with the command descriptions" do @@ -176,7 +186,9 @@ describe Shef do @shef_client = TestableShefSession.instance Shef.stub!(:session).and_return(@shef_client) @job_manager = TestJobManager.new - @root_context = ObjectTestHarness.new + @root_context = Object.new + @root_context.instance_eval(&ObjectTestHarness) + Shef::Extensions.extend_context_object(@root_context) @root_context.conf = mock("irbconf") end @@ -224,10 +236,7 @@ describe Shef do end it "has a help command" do - # note: irb whines like a 5yo with a broken toy if you define a help - # method on Object. have to override it in a sneaky way. - @root_context.stub!(:puts) - @root_context.shef_help + @root_context.should respond_to(:help) end it "turns irb tracing on and off" do @@ -290,6 +299,7 @@ describe Shef do before do @run_context = Chef::RunContext.new(Chef::Node.new, {}) @recipe_object = Chef::Recipe.new(nil, nil, @run_context) + Shef::Extensions.extend_context_recipe(@recipe_object) end it "gives a list of the resources" do @@ -306,6 +316,7 @@ describe Shef do @node = @session.node = Chef::Node.new @run_context = @session.run_context = Chef::RunContext.new(@node, {}) @recipe = @session.recipe = Chef::Recipe.new(nil, nil, @run_context) + Shef::Extensions.extend_context_recipe(@recipe) end it "has a run_context" do @@ -332,7 +343,7 @@ describe Shef do @session.resource_collection Chef::Runner.should_receive(:new).with(@session.recipe.run_context).and_return(chef_runner) - @root_context.run_chef.should == :converged + @recipe.run_chef.should == :converged end end @@ -344,10 +355,8 @@ describe Shef do @node = Chef::Node.new @run_context = @session.run_context = Chef::RunContext.new(@node, {}) @session.node = @node - #@compile = @session.compile = Chef::Compile.new(@node) - # prevent dynamic re-compilation from raining on the parade - #Chef::Compile.stub!(:new).and_return(@compile) @recipe = @session.recipe = Chef::Recipe.new(nil, nil, @run_context) + Shef::Extensions.extend_context_recipe(@recipe) end after do @@ -387,7 +396,7 @@ describe Shef do chef_runner = mock("Chef::Runner.new", :converge => :converged) Chef::Runner.should_receive(:new).with(an_instance_of(Chef::RunContext)).and_return(chef_runner) - @root_context.run_chef.should == :converged + @recipe.run_chef.should == :converged end end |
