diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index f2d792ee1..e2ae01e4d 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -1,410 +1,411 @@ require 'optparse' require 'puppet/util/plugins' +require 'puppet/util/constant_inflector' require 'puppet/error' # This class handles all the aspects of a Puppet application/executable # * setting up options # * setting up logs # * choosing what to run # * representing execution status # # === Usage # An application is a subclass of Puppet::Application. # # For legacy compatibility, # Puppet::Application[:example].run # is equivalent to # Puppet::Application::Example.new.run # # # class Puppet::Application::Example << Puppet::Application # # def preinit # # perform some pre initialization # @all = false # end # # # run_command is called to actually run the specified command # def run_command # send Puppet::Util::CommandLine.new.args.shift # end # # # option uses metaprogramming to create a method # # and also tells the option parser how to invoke that method # option("--arg ARGUMENT") do |v| # @args << v # end # # option("--debug", "-d") do |v| # @debug = v # end # # option("--all", "-a:) do |v| # @all = v # end # # def handle_unknown(opt,arg) # # last chance to manage an option # ... # # let's say to the framework we finally handle this option # true # end # # def read # # read action # end # # def write # # writeaction # end # # end # # === Preinit # The preinit block is the first code to be called in your application, before option parsing, # setup or command execution. # # === Options # Puppet::Application uses +OptionParser+ to manage the application options. # Options are defined with the +option+ method to which are passed various # arguments, including the long option, the short option, a description... # Refer to +OptionParser+ documentation for the exact format. # * If the option method is given a block, this one will be called whenever # the option is encountered in the command-line argument. # * If the option method has no block, a default functionnality will be used, that # stores the argument (or true/false if the option doesn't require an argument) in # the global (to the application) options array. # * If a given option was not defined by a the +option+ method, but it exists as a Puppet settings: # * if +unknown+ was used with a block, it will be called with the option name and argument # * if +unknown+ wasn't used, then the option/argument is handed to Puppet.settings.handlearg for # a default behavior # # --help is managed directly by the Puppet::Application class, but can be overriden. # # === Setup # Applications can use the setup block to perform any initialization. # The defaul +setup+ behaviour is to: read Puppet configuration and manage log level and destination # # === What and how to run # If the +dispatch+ block is defined it is called. This block should return the name of the registered command # to be run. # If it doesn't exist, it defaults to execute the +main+ command if defined. # # === Execution state # The class attributes/methods of Puppet::Application serve as a global place to set and query the execution # status of the application: stopping, restarting, etc. The setting of the application status does not directly # aftect its running status; it's assumed that the various components within the application will consult these # settings appropriately and affect their own processing accordingly. Control operations (signal handlers and # the like) should set the status appropriately to indicate to the overall system that it's the process of # stopping or restarting (or just running as usual). # # So, if something in your application needs to stop the process, for some reason, you might consider: # # def stop_me! # # indicate that we're stopping # Puppet::Application.stop! # # ...do stuff... # end # # And, if you have some component that involves a long-running process, you might want to consider: # # def my_long_process(giant_list_to_munge) # giant_list_to_munge.collect do |member| # # bail if we're stopping # return if Puppet::Application.stop_requested? # process_member(member) # end # end module Puppet class Application require 'puppet/util' include Puppet::Util DOCPATTERN = ::File.expand_path(::File.dirname(__FILE__) + "/util/command_line/*" ) class << self include Puppet::Util attr_accessor :run_status def clear! self.run_status = nil end def stop! self.run_status = :stop_requested end def restart! self.run_status = :restart_requested end # Indicates that Puppet::Application.restart! has been invoked and components should # do what is necessary to facilitate a restart. def restart_requested? :restart_requested == run_status end # Indicates that Puppet::Application.stop! has been invoked and components should do what is necessary # for a clean stop. def stop_requested? :stop_requested == run_status end # Indicates that one of stop! or start! was invoked on Puppet::Application, and some kind of process # shutdown/short-circuit may be necessary. def interrupted? [:restart_requested, :stop_requested].include? run_status end # Indicates that Puppet::Application believes that it's in usual running run_mode (no stop/restart request # currently active). def clear? run_status.nil? end # Only executes the given block if the run status of Puppet::Application is clear (no restarts, stops, # etc. requested). # Upon block execution, checks the run status again; if a restart has been requested during the block's # execution, then controlled_run will send a new HUP signal to the current process. # Thus, long-running background processes can potentially finish their work before a restart. def controlled_run(&block) return unless clear? result = block.call Process.kill(:HUP, $PID) if restart_requested? result end SHOULD_PARSE_CONFIG_DEPRECATION_MSG = "is no longer supported; config file parsing " + "is now controlled by the puppet engine, rather than by individual applications. This " + "method will be removed in a future version of puppet." def should_parse_config Puppet.deprecation_warning("should_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) end def should_not_parse_config Puppet.deprecation_warning("should_not_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) end def should_parse_config? Puppet.deprecation_warning("should_parse_config? " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) true end # used to declare code that handle an option def option(*options, &block) long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_') fname = symbolize("handle_#{long}") if (block_given?) define_method(fname, &block) else define_method(fname) do |value| self.options["#{long}".to_sym] = value end end self.option_parser_commands << [options, fname] end def banner(banner = nil) @banner ||= banner end def option_parser_commands @option_parser_commands ||= ( superclass.respond_to?(:option_parser_commands) ? superclass.option_parser_commands.dup : [] ) @option_parser_commands end def find(name) - klass = name.to_s.capitalize + klass = Puppet::Util::ConstantInflector.file2constant(name.to_s) begin require ::File.join('puppet', 'application', name.to_s.downcase) rescue LoadError => e puts "Unable to find application '#{name}'. #{e}" Kernel::exit(1) end self.const_get(klass) end def [](name) find(name).new end # # I think that it would be nice to look into changing this into two methods (getter/setter); however, # it sounds like this is a desirable feature of our ruby DSL. --cprice 2012-03-06 # # Sets or gets the run_mode name. Sets the run_mode name if a mode_name is # passed. Otherwise, gets the run_mode or a default run_mode # def run_mode( mode_name = nil) return @run_mode if @run_mode and not mode_name require 'puppet/util/run_mode' @run_mode = Puppet::Util::RunMode[ mode_name || :user ] end end attr_reader :options, :command_line # Every app responds to --version option("--version", "-V") do |arg| puts "#{Puppet.version}" exit end # Every app responds to --help option("--help", "-h") do |v| puts help exit end def app_defaults() Puppet::Util::Settings.app_defaults_for_run_mode(self.class.run_mode).merge( :name => name ) end def initialize_app_defaults() Puppet.settings.initialize_app_defaults(app_defaults) end # override to execute code before running anything else def preinit end def initialize(command_line = nil) require 'puppet/util/command_line' @command_line = command_line || Puppet::Util::CommandLine.new @options = {} end # This is the main application entry point def run # I don't really like the names of these lifecycle phases. It would be nice to change them to some more meaningful # names, and make deprecated aliases. Also, Daniel suggests that we can probably get rid of this "plugin_hook" # pattern, but we need to check with PE and the community first. --cprice 2012-03-16 # exit_on_fail("get application-specific default settings") do plugin_hook('initialize_app_defaults') { initialize_app_defaults } end require 'puppet' require 'puppet/util/instrumentation' Puppet::Util::Instrumentation.init exit_on_fail("initialize") { plugin_hook('preinit') { preinit } } exit_on_fail("parse application options") { plugin_hook('parse_options') { parse_options } } exit_on_fail("prepare for execution") { plugin_hook('setup') { setup } } exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes } exit_on_fail("run") { plugin_hook('run_command') { run_command } } end def main raise NotImplementedError, "No valid command or main" end def run_command main end def setup setup_logs end def setup_logs if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end Puppet::Util::Log.setup_default unless options[:setdest] end def configure_indirector_routes route_file = Puppet[:route_file] if ::File.exists?(route_file) routes = YAML.load_file(route_file) application_routes = routes[name.to_s] Puppet::Indirector.configure_routes(application_routes) if application_routes end end def parse_options # Create an option parser option_parser = OptionParser.new(self.class.banner) # He're we're building up all of the options that the application may need to handle. The main # puppet settings defined in "defaults.rb" have already been parsed once (in command_line.rb) by # the time we get here; however, our app may wish to handle some of them specially, so we need to # make the parser aware of them again. We might be able to make this a bit more efficient by # re-using the parser object that gets built up in command_line.rb. --cprice 2012-03-16 # Add all global options to it. Puppet.settings.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| handlearg(option[0], arg) end end # Add options that are local to this application, which were # created using the "option()" metaprogramming method. If there # are any conflicts, this application's options will be favored. self.class.option_parser_commands.each do |options, fname| option_parser.on(*options) do |value| # Call the method that "option()" created. self.send(fname, value) end end # Scan command line. We just hand any exceptions to our upper levels, # rather than printing help and exiting, so that we can meaningfully # respond with context-sensitive help if we want to. --daniel 2011-04-12 option_parser.parse!(self.command_line.args) end def handlearg(opt, val) opt, val = Puppet::Util::Settings.clean_opt(opt, val) send(:handle_unknown, opt, val) if respond_to?(:handle_unknown) end # this is used for testing def self.exit(code) exit(code) end def name self.class.to_s.sub(/.*::/,"").downcase.to_sym end def help "No help available for puppet #{name}" end def plugin_hook(step,&block) Puppet::Plugins.send("before_application_#{step}",:application_object => self) x = yield Puppet::Plugins.send("after_application_#{step}",:application_object => self, :return_value => x) x end private :plugin_hook end end diff --git a/lib/puppet/application/certificate_request.rb b/lib/puppet/application/certificate_request.rb index 1b1b0830c..8a2670378 100644 --- a/lib/puppet/application/certificate_request.rb +++ b/lib/puppet/application/certificate_request.rb @@ -1,4 +1,4 @@ require 'puppet/application/indirection_base' -class Puppet::Application::Certificate_request < Puppet::Application::IndirectionBase +class Puppet::Application::CertificateRequest < Puppet::Application::IndirectionBase end diff --git a/lib/puppet/application/certificate_revocation_list.rb b/lib/puppet/application/certificate_revocation_list.rb index 60b9d97d6..ee1077522 100644 --- a/lib/puppet/application/certificate_revocation_list.rb +++ b/lib/puppet/application/certificate_revocation_list.rb @@ -1,4 +1,4 @@ require 'puppet/application/indirection_base' -class Puppet::Application::Certificate_revocation_list < Puppet::Application::IndirectionBase +class Puppet::Application::CertificateRevocationList < Puppet::Application::IndirectionBase end diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index 9615f120e..06299e5a6 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,261 +1,260 @@ require 'puppet/application' require 'puppet/face' require 'optparse' require 'pp' class Puppet::Application::FaceBase < Puppet::Application run_mode :agent option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end option("--verbose", "-v") do |_| Puppet::Util::Log.level = :info end option("--render-as FORMAT") do |format| self.render_as = format.to_sym end # This seems like a bad thing; it seems like--in an ideal world--a given app/face should have one constant run mode. # This isn't currently possible because of issues relating to the certificate authority, but I've left some notes # about "run_mode" in settings.rb and defaults.rb, and if we are able to tighten up the behavior / implementation # of that setting, we might want to revisit this. --cprice 2012-03-16 option("--mode RUNMODE", "-r") do |arg| raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg) self.class.run_mode(arg.to_sym) end attr_accessor :face, :action, :type, :arguments, :render_as def render_as=(format) if format == :json then @render_as = Puppet::Network::FormatHandler.format(:pson) else @render_as = Puppet::Network::FormatHandler.format(format) end @render_as or raise ArgumentError, "I don't know how to render '#{format}'" end def render(result, args_and_options) hook = action.when_rendering(render_as.name) if hook # when defining when_rendering on your action you can optionally # include arguments and options if hook.arity > 1 result = hook.call(result, *args_and_options) else result = hook.call(result) end end render_as.render(result) end def preinit super Signal.trap(:INT) do $stderr.puts "Cancelling Face" exit(0) end end def parse_options # We need to parse enough of the command line out early, to identify what # the action is, so that we can obtain the full set of options to parse. # REVISIT: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 - @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym - + @type = Puppet::Util::ConstantInflector.constant2file(self.class.name.to_s.sub(/.+:/, '')).to_sym @face = Puppet::Face[@type, :current] # Now, walk the command line and identify the action. We skip over # arguments based on introspecting the action and all, and find the first # non-option word to use as the action. action_name = nil index = -1 until action_name or (index += 1) >= command_line.args.length do item = command_line.args[index] if item =~ /^-/ then option = @face.options.find do |name| item =~ /^-+#{name.to_s.gsub(/[-_]/, '[-_]')}(?:[ =].*)?$/ end if option then option = @face.get_option(option) # If we have an inline argument, just carry on. We don't need to # care about optional vs mandatory in that case because we do a real # parse later, and that will totally take care of raising the error # when we get there. --daniel 2011-04-04 if option.takes_argument? and !item.index('=') then index += 1 unless (option.optional_argument? and command_line.args[index + 1] =~ /^-/) end elsif option = find_global_settings_argument(item) then unless Puppet.settings.boolean? option.name then # As far as I can tell, we treat non-bool options as always having # a mandatory argument. --daniel 2011-04-05 index += 1 # ...so skip the argument. end elsif option = find_application_argument(item) then index += 1 if (option[:argument] and not option[:optional]) else raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else # Stash away the requested action name for later, and try to fetch the # action object it represents; if this is an invalid action name that # will be nil, and handled later. action_name = item.to_sym @action = Puppet::Face.find_action(@face.name, action_name) @face = @action.face if @action end end if @action.nil? if @action = @face.get_default_action() then @is_default_action = true else face = @face.name action = action_name.nil? ? 'default' : "'#{action_name}'" msg = "'#{face}' has no #{action} action. See `puppet help #{face}`." Puppet.err(msg) Puppet::Util::Log.force_flushqueue() exit false end end # Now we can interact with the default option code to build behaviour # around the full set of options we now know we support. @action.options.each do |option| option = @action.get_option(option) # make it the object. self.class.option(*option.optparse) # ...and make the CLI parse it. end # ...and invoke our parent to parse all the command line options. super end def find_global_settings_argument(item) Puppet.settings.each do |name, object| object.optparse_args.each do |arg| next unless arg =~ /^-/ # sadly, we have to emulate some of optparse here... pattern = /^#{arg.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ pattern.match item and return object end end return nil # nothing found. end def find_application_argument(item) self.class.option_parser_commands.each do |options, function| options.each do |option| next unless option =~ /^-/ pattern = /^#{option.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ next unless pattern.match(item) return { :argument => option =~ /[ =]/, :optional => option =~ /[ =]\[/ } end end return nil # not found end def setup Puppet::Util::Log.newdestination :console @arguments = command_line.args # Note: because of our definition of where the action is set, we end up # with it *always* being the first word of the remaining set of command # line arguments. So, strip that off when we construct the arguments to # pass down to the face action. --daniel 2011-04-04 # Of course, now that we have default actions, we should leave the # "action" name on if we didn't actually consume it when we found our # action. @arguments.delete_at(0) unless @is_default_action # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we # would invoke the action with options set as global state in the # interface object. --daniel 2011-03-28 @arguments << options # If we don't have a rendering format, set one early. self.render_as ||= (@action.render_as || :console) end def main status = false # Call the method associated with the provided action (e.g., 'find'). unless @action puts Puppet::Face[:help, :current].help(@face.name) raise "#{face} does not respond to action #{arguments.first}" end # We need to do arity checking here because this is generic code # calling generic methods – that have argument defaulting. We need to # make sure we don't accidentally pass the options as the first # argument to a method that takes one argument. eg: # # puppet facts find # => options => {} # @arguments => [{}] # => @face.send :bar, {} # # def face.bar(argument, options = {}) # => bar({}, {}) # oops! we thought the options were the # # positional argument!! # # We could also fix this by making it mandatory to pass the options on # every call, but that would make the Ruby API much more annoying to # work with; having the defaulting is a much nicer convention to have. # # We could also pass the arguments implicitly, by having a magic # 'options' method that was visible in the scope of the action, which # returned the right stuff. # # That sounds attractive, but adds complications to all sorts of # things, especially when you think about how to pass options when you # are writing Ruby code that calls multiple faces. Especially if # faces are involved in that. ;) # # --daniel 2011-04-27 if (arity = @action.positional_arg_count) > 0 unless (count = arguments.length) == arity then s = arity == 2 ? '' : 's' raise ArgumentError, "puppet #{@face.name} #{@action.name} takes #{arity-1} argument#{s}, but you gave #{count-1}" end end result = @face.send(@action.name, *arguments) puts render(result, arguments) unless result.nil? status = true # We need an easy way for the action to set a specific exit code, so we # rescue SystemExit here; This allows each action to set the desired exit # code by simply calling Kernel::exit. eg: # # exit(2) # # --kelsey 2012-02-14 rescue SystemExit => detail status = detail.status rescue Exception => detail Puppet.log_exception(detail) Puppet.err "Try 'puppet help #{@face.name} #{@action.name}' for usage" ensure exit status end end diff --git a/lib/puppet/application/instrumentation_data.rb b/lib/puppet/application/instrumentation_data.rb index e4f86f196..647fc36bc 100644 --- a/lib/puppet/application/instrumentation_data.rb +++ b/lib/puppet/application/instrumentation_data.rb @@ -1,4 +1,4 @@ require 'puppet/application/indirection_base' -class Puppet::Application::Instrumentation_data < Puppet::Application::IndirectionBase +class Puppet::Application::InstrumentationData < Puppet::Application::IndirectionBase end diff --git a/lib/puppet/application/instrumentation_listener.rb b/lib/puppet/application/instrumentation_listener.rb index 64029b5c9..216e952b8 100644 --- a/lib/puppet/application/instrumentation_listener.rb +++ b/lib/puppet/application/instrumentation_listener.rb @@ -1,4 +1,4 @@ require 'puppet/application/indirection_base' -class Puppet::Application::Instrumentation_listener < Puppet::Application::IndirectionBase +class Puppet::Application::InstrumentationListener < Puppet::Application::IndirectionBase end diff --git a/lib/puppet/application/instrumentation_probe.rb b/lib/puppet/application/instrumentation_probe.rb index b31f95c45..8f808c6cc 100644 --- a/lib/puppet/application/instrumentation_probe.rb +++ b/lib/puppet/application/instrumentation_probe.rb @@ -1,4 +1,4 @@ require 'puppet/application/indirection_base' -class Puppet::Application::Instrumentation_probe < Puppet::Application::IndirectionBase +class Puppet::Application::InstrumentationProbe < Puppet::Application::IndirectionBase end diff --git a/lib/puppet/application/resource_type.rb b/lib/puppet/application/resource_type.rb index 59594262c..784225731 100644 --- a/lib/puppet/application/resource_type.rb +++ b/lib/puppet/application/resource_type.rb @@ -1,4 +1,4 @@ require 'puppet/application/indirection_base' -class Puppet::Application::Resource_type < Puppet::Application::IndirectionBase +class Puppet::Application::ResourceType < Puppet::Application::IndirectionBase end diff --git a/lib/puppet/application/secret_agent.rb b/lib/puppet/application/secret_agent.rb index 6bdbc9232..6d11be491 100644 --- a/lib/puppet/application/secret_agent.rb +++ b/lib/puppet/application/secret_agent.rb @@ -1,6 +1,6 @@ require 'puppet/application/face_base' require 'puppet/face' -class Puppet::Application::Secret_agent < Puppet::Application::FaceBase +class Puppet::Application::SecretAgent < Puppet::Application::FaceBase run_mode :agent end diff --git a/lib/puppet/face/help.rb b/lib/puppet/face/help.rb index 4a16a0d51..2055f8377 100644 --- a/lib/puppet/face/help.rb +++ b/lib/puppet/face/help.rb @@ -1,131 +1,184 @@ require 'puppet/face' +require 'puppet/application/face_base' require 'puppet/util/command_line' +require 'puppet/util/constant_inflector' require 'pathname' require 'erb' Puppet::Face.define(:help, '0.0.1') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "Display Puppet help." action(:help) do summary "Display help about Puppet subcommands and their actions." arguments "[] []" returns "Short help text for the specified subcommand or action." examples <<-'EOT' Get help for an action: $ puppet help EOT option "--version VERSION" do summary "The version of the subcommand for which to show help." end default when_invoked do |*args| # Check our invocation, because we want varargs and can't do defaults # yet. REVISIT: when we do option defaults, and positional options, we # should rewrite this to use those. --daniel 2011-04-04 options = args.pop if options.nil? or args.length > 2 then if args.select { |x| x == 'help' }.length > 2 then c = "\n %'(),-./=ADEFHILORSTUXY\\_`gnv|".split('') i = <<-'EOT'.gsub(/\s*/, '').to_i(36) 3he6737w1aghshs6nwrivl8mz5mu9nywg9tbtlt081uv6fq5kvxse1td3tj1wvccmte806nb cy6de2ogw0fqjymbfwi6a304vd56vlq71atwmqsvz3gpu0hj42200otlycweufh0hylu79t3 gmrijm6pgn26ic575qkexyuoncbujv0vcscgzh5us2swklsp5cqnuanlrbnget7rt3956kam j8adhdrzqqt9bor0cv2fqgkloref0ygk3dekiwfj1zxrt13moyhn217yy6w4shwyywik7w0l xtuevmh0m7xp6eoswin70khm5nrggkui6z8vdjnrgdqeojq40fya5qexk97g4d8qgw0hvokr pli1biaz503grqf2ycy0ppkhz1hwhl6ifbpet7xd6jjepq4oe0ofl575lxdzjeg25217zyl4 nokn6tj5pq7gcdsjre75rqylydh7iia7s3yrko4f5ud9v8hdtqhu60stcitirvfj6zphppmx 7wfm7i9641d00bhs44n6vh6qvx39pg3urifgr6ihx3e0j1ychzypunyou7iplevitkyg6gbg wm08oy1rvogcjakkqc1f7y1awdfvlb4ego8wrtgu9vzw4vmj59utwifn2ejcs569dh1oaavi sc581n7jjg1dugzdu094fdobtx6rsvk3sfctvqnr36xctold EOT 353.times{i,x=i.divmod(1184);a,b=x.divmod(37);print(c[a]*b)} end raise ArgumentError, "Puppet help only takes two (optional) arguments: a subcommand and an action" end version = :current if options.has_key? :version then if options[:version].to_s !~ /^current$/i then version = options[:version] else if args.length == 0 then raise ArgumentError, "Version only makes sense when a Faces subcommand is given" end end end # Name those parameters... facename, actionname = args if facename then if legacy_applications.include? facename then actionname and raise ArgumentError, "Legacy subcommands don't take actions" return Puppet::Application[facename].help else face = Puppet::Face[facename.to_sym, version] actionname and action = face.get_action(actionname.to_sym) end end case args.length when 0 then template = erb 'global.erb' when 1 then face or fail ArgumentError, "Unable to load face #{facename}" template = erb 'face.erb' when 2 then face or fail ArgumentError, "Unable to load face #{facename}" action or fail ArgumentError, "Unable to load action #{actionname} from #{face}" template = erb 'action.erb' else fail ArgumentError, "Too many arguments to help action" end # Run the ERB template in our current binding, including all the local # variables we established just above. --daniel 2011-04-11 return template.result(binding) end end def erb(name) template = (Pathname(__FILE__).dirname + "help" + name) erb = ERB.new(template.read, nil, '-') erb.filename = template.to_s return erb end + # Return a list of applications that are not simply just stubs for Faces. def legacy_applications - # The list of applications, less those that are duplicated as a face. Puppet::Util::CommandLine.available_subcommands.reject do |appname| - Puppet::Face.face? appname.to_sym, :current or - # ...this is a nasty way to exclude non-applications. :( - %w{face_base indirection_base}.include? appname + (is_face_app?(appname)) or (exclude_from_docs?(appname)) end.sort end + # Return a list of all applications (both legacy and Face applications), along with a summary + # of their functionality. + # @returns [Array] An Array of Arrays. The outer array contains one entry per application; each + # element in the outer array is a pair whose first element is a String containing the application + # name, and whose second element is a String containing the summary for that application. + def all_application_summaries() + Puppet::Util::CommandLine.available_subcommands.sort.inject([]) do |result, appname| + next result if exclude_from_docs?(appname) + + if (is_face_app?(appname)) + face = Puppet::Face[appname, :current] + result << [appname, face.summary] + else + result << [appname, horribly_extract_summary_from(appname)] + end + end + end + def horribly_extract_summary_from(appname) begin + # it sucks that this 'require' is necessary, and it sucks even more that we are + # doing it in two different places in this class (#horribly_extract_summary_from, + # #is_face_app?). However, we can take some solace in the fact that ruby will + # at least recognize that it's already done a 'require' for any individual app + # and basically treat it as a no-op if we try to 'require' it twice. require "puppet/application/#{appname}" help = Puppet::Application[appname].help.split("\n") # Now we find the line with our summary, extract it, and return it. This # depends on the implementation coincidence of how our pages are # formatted. If we can't match the pattern we expect we return the empty # string to ensure we don't blow up in the summary. --daniel 2011-04-11 while line = help.shift do if md = /^puppet-#{appname}\([^\)]+\) -- (.*)$/.match(line) then return md[1] end end rescue Exception # Damn, but I hate this: we just ignore errors here, no matter what # class they are. Meh. end return '' end + # This should absolutely be a private method, but for some reason it appears + # that you can't use the 'private' keyword inside of a Face definition. + # See #14205. + #private :horribly_extract_summary_from + + def exclude_from_docs?(appname) + %w{face_base indirection_base}.include? appname + end + # This should absolutely be a private method, but for some reason it appears + # that you can't use the 'private' keyword inside of a Face definition. + # See #14205. + #private :exclude_from_docs? + + def is_face_app?(appname) + # it sucks that this 'require' is necessary, and it sucks even more that we are + # doing it in two different places in this class (#horribly_extract_summary_from, + # #is_face_app?). However, we can take some solace in the fact that ruby will + # at least recognize that it's already done a 'require' for any individual app + # and basically treat it as a no-op if we try to 'require' it twice. + require "puppet/application/#{appname}" + # Would much rather use "const_get" than "eval" here, but for some reason it is not available. + # See #14205. + clazz = eval("Puppet::Application::#{Puppet::Util::ConstantInflector.file2constant(appname)}") + clazz.ancestors.include?(Puppet::Application::FaceBase) + end + # This should probably be a private method, but for some reason it appears + # that you can't use the 'private' keyword inside of a Face definition. + # See #14205. + #private :is_face_app? + end diff --git a/lib/puppet/face/help/global.erb b/lib/puppet/face/help/global.erb index c5a9ec9e0..800afff99 100644 --- a/lib/puppet/face/help/global.erb +++ b/lib/puppet/face/help/global.erb @@ -1,19 +1,15 @@ Usage: puppet [options] [options] -Available subcommands, from Puppet Faces: -<% Puppet::Face.faces.sort.each do |name| - face = Puppet::Face[name, :current] -%> - <%= face.name.to_s.ljust(16) %> <%= face.summary %> -<% end -%> - -<% unless legacy_applications.empty? then # great victory when this is true! -%> -Available applications, soon to be ported to Faces: -<% legacy_applications.each do |appname| - summary = horribly_extract_summary_from appname -%> +Available subcommands: + <%# NOTE: this is probably not a good long-term solution for this. We're only iterating over + applications to find the list of things we need to show help for... this works for now + because faces can't be run without an application stub. However, when #6753 is resolved, + all of the application stubs for faces will go away, and this will need to be updated + to reflect that. --cprice 2012-04-26 %> +<% all_application_summaries.each do |appname, summary| -%> <%= appname.to_s.ljust(16) %> <%= summary %> -<% end - end -%> +<% end -%> See 'puppet help ' for help on a specific subcommand action. See 'puppet help ' for help on a specific subcommand. Puppet v<%= Puppet::PUPPETVERSION %> diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index b4606f22f..115ed39fa 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -1,116 +1,116 @@ require 'puppet' require "puppet/util/plugins" module Puppet module Util class CommandLine def initialize(zero = $0, argv = ARGV, stdin = STDIN) @zero = zero @argv = argv.dup @stdin = stdin @subcommand_name, @args = subcommand_and_args(@zero, @argv, @stdin) Puppet::Plugins.on_commandline_initialization(:command_line_object => self) end attr :subcommand_name attr :args def appdir File.join('puppet', 'application') end def self.available_subcommands # Eventually we probably want to replace this with a call to the autoloader. however, at the moment # the autoloader considers the module path when loading, and we don't want to allow apps / faces to load # from there. Once that is resolved, this should be replaced. --cprice 2012-03-06 absolute_appdirs = $LOAD_PATH.collect do |x| File.join(x,'puppet','application') end.select{ |x| File.directory?(x) } absolute_appdirs.inject([]) do |commands, dir| commands + Dir[File.join(dir, '*.rb')].map{|fn| File.basename(fn, '.rb')} end.uniq end # available_subcommands was previously an instance method, not a class # method, and we have an unknown number of user-implemented applications # that depend on that behaviour. Forwarding allows us to preserve a # backward compatible API. --daniel 2011-04-11 def available_subcommands self.class.available_subcommands end def require_application(application) require File.join(appdir, application) end # This is the main entry point for all puppet applications / faces; it is basically where the bootstrapping # process / lifecycle of an app begins. def execute Puppet::Util.exit_on_fail("intialize global default settings") do Puppet.settings.initialize_global_settings(args) end # OK, now that we've processed the command line options and the config files, we should be able to say that # we definitively know where the libdir is... which means that we can now look for our available # applications / subcommands / faces. if subcommand_name and available_subcommands.include?(subcommand_name) then require_application subcommand_name # This will need to be cleaned up to do something that is not so application-specific # (i.e.. so that we can load faces). Longer-term, use the autoloader. See comments in # #available_subcommands method above. --cprice 2012-03-06 app = Puppet::Application.find(subcommand_name).new(self) - Puppet::Plugins.on_application_initialization(:appliation_object => self) + Puppet::Plugins.on_application_initialization(:application_object => self) app.run elsif ! execute_external_subcommand then unless subcommand_name.nil? then puts "Error: Unknown Puppet subcommand '#{subcommand_name}'" end puts "See 'puppet help' for help on available puppet subcommands" end end def execute_external_subcommand external_command = "puppet-#{subcommand_name}" require 'puppet/util' path_to_subcommand = Puppet::Util.which(external_command) return false unless path_to_subcommand exec(path_to_subcommand, *args) end def legacy_executable_name name = CommandLine::LegacyCommandLine::LEGACY_NAMES[ subcommand_name.intern ] return name unless name.nil? return subcommand_name.intern end private def subcommand_and_args(zero, argv, stdin) zero = File.basename(zero, '.rb') if zero == 'puppet' case argv.first # if they didn't pass a command, or passed a help flag, we will fall back to showing a usage message. # we no longer default to 'apply' when nil, "--help", "-h", /^-|\.pp$|\.rb$/ [nil, argv] else [argv.first, argv[1..-1]] end else [zero, argv] end end end end end diff --git a/lib/puppet/util/constant_inflector.rb b/lib/puppet/util/constant_inflector.rb index 8f93e3255..128bbc9c6 100644 --- a/lib/puppet/util/constant_inflector.rb +++ b/lib/puppet/util/constant_inflector.rb @@ -1,15 +1,19 @@ # Created on 2008-02-12 # Copyright Luke Kanies +# NOTE: I think it might be worth considering moving these methods directly into Puppet::Util. + # A common module for converting between constants and # file names. module Puppet::Util::ConstantInflector def file2constant(file) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = file.split("/").collect { |name| name.capitalize }.join("::").gsub(/_+(.)/) { |term| $1.capitalize } end + module_function :file2constant def constant2file(constant) constant.to_s.gsub(/([a-z])([A-Z])/) { |term| $1 + "_#{$2}" }.gsub("::", "/").downcase end + module_function :constant2file end diff --git a/spec/unit/application/indirection_base_spec.rb b/spec/unit/application/indirection_base_spec.rb index 8a5eee2c6..5d5660334 100755 --- a/spec/unit/application/indirection_base_spec.rb +++ b/spec/unit/application/indirection_base_spec.rb @@ -1,42 +1,48 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/application/indirection_base' require 'puppet/indirector/face' ######################################################################## # Stub for testing; the names are critical, sadly. --daniel 2011-03-30 class Puppet::Application::TestIndirection < Puppet::Application::IndirectionBase end -face = Puppet::Indirector::Face.define(:testindirection, '0.0.1') do +face = Puppet::Indirector::Face.define(:test_indirection, '0.0.1') do summary "fake summary" copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" end # REVISIT: This horror is required because we don't allow anything to be # :current except for if it lives on, and is loaded from, disk. --daniel 2011-03-29 face.instance_variable_set('@version', :current) Puppet::Face.register(face) ######################################################################## describe Puppet::Application::IndirectionBase do subject { Puppet::Application::TestIndirection.new } it "should accept a terminus command line option" do # It would be nice not to have to stub this, but whatever... writing an # entire indirection stack would cause us more grief. --daniel 2011-03-31 terminus = stub_everything("test indirection terminus") - terminus.stubs(:name).returns(:testindirection) + terminus.stubs(:name).returns(:test_indirection) + + # This is necessary because Instrumentation tickles indirection, which + # messes up our expectations. + Puppet::Util::Instrumentation.stubs(:init) Puppet::Indirector::Indirection.expects(:instance). - with(:testindirection).returns(terminus) + with(:test_indirection).returns(terminus) subject.command_line.instance_variable_set('@args', %w{--terminus foo save bar}) # Not a very nice thing. :( $stderr.stubs(:puts) Puppet.stubs(:err) - expect { subject.run }.to exit_with 0 + expect { + subject.run + }.to exit_with 0 end end diff --git a/spec/unit/face/help_spec.rb b/spec/unit/face/help_spec.rb index e5b694283..2c6582d72 100755 --- a/spec/unit/face/help_spec.rb +++ b/spec/unit/face/help_spec.rb @@ -1,135 +1,145 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/face' describe Puppet::Face[:help, '0.0.1'] do it "should have a help action" do subject.should be_action :help end it "should have a default action of help" do subject.get_action('help').should be_default end it "should accept a call with no arguments" do - expect { subject.help() }.should_not raise_error + expect { + subject.help() + }.should_not raise_error end it "should accept a face name" do expect { subject.help(:help) }.should_not raise_error end it "should accept a face and action name" do expect { subject.help(:help, :help) }.should_not raise_error end it "should fail if more than a face and action are given" do expect { subject.help(:help, :help, :for_the_love_of_god) }. should raise_error ArgumentError end it "should treat :current and 'current' identically" do subject.help(:help, :version => :current).should == subject.help(:help, :version => 'current') end it "should complain when the request version of a face is missing" do expect { subject.help(:huzzah, :bar, :version => '17.0.0') }. should raise_error Puppet::Error end it "should find a face by version" do face = Puppet::Face[:huzzah, :current] subject.help(:huzzah, :version => face.version). should == subject.help(:huzzah, :version => :current) end context "when listing subcommands" do subject { Puppet::Face[:help, :current].help } RSpec::Matchers.define :have_a_summary do match do |instance| instance.summary.is_a?(String) end end # Check a precondition for the next block; if this fails you have # something odd in your set of face, and we skip testing things that # matter. --daniel 2011-04-10 it "should have at least one face with a summary" do Puppet::Face.faces.should be_any do |name| Puppet::Face[name, :current].summary end end - it "should list all faces" do - Puppet::Face.faces.each do |name| + it "should list all faces which are runnable from the command line" do + help_face = Puppet::Face[:help, :current] + # The main purpose of the help face is to provide documentation for + # command line users. It shouldn't show documentation for faces + # that can't be run from the command line, so, rather than iterating + # over all available faces, we need to iterate over the subcommands + # that are available from the command line. + Puppet::Util::CommandLine.available_subcommands.each do |name| + next unless help_face.is_face_app?(name) + next if help_face.exclude_from_docs?(name) face = Puppet::Face[name, :current] summary = face.summary subject.should =~ %r{ #{name} } summary and subject.should =~ %r{ #{name} +#{summary}} end end context "face summaries" do # we need to set a bunk module path here, because without doing so, # the autoloader will try to use it before it is initialized. Puppet[:modulepath] = "/dev/null" Puppet::Face.faces.each do |name| it "should have a summary for #{name}" do Puppet::Face[name, :current].should have_a_summary end end end it "should list all legacy applications" do Puppet::Face[:help, :current].legacy_applications.each do |appname| subject.should =~ %r{ #{appname} } summary = Puppet::Face[:help, :current].horribly_extract_summary_from(appname) summary and subject.should =~ %r{ #{summary}\b} end end end context "#legacy_applications" do subject { Puppet::Face[:help, :current].legacy_applications } # If we don't, these tests are ... less than useful, because they assume # it. When this breaks you should consider ditching the entire feature # and tests, but if not work out how to fake one. --daniel 2011-04-11 it { should have_at_least(1).item } # Meh. This is nasty, but we can't control the other list; the specific # bug that caused these to be listed is annoyingly subtle and has a nasty # fix, so better to have a "fail if you do something daft" trigger in # place here, I think. --daniel 2011-04-11 %w{face_base indirection_base}.each do |name| it { should_not include name } end end context "help for legacy applications" do subject { Puppet::Face[:help, :current] } let :appname do subject.legacy_applications.first end # This test is purposely generic, so that as we eliminate legacy commands # we don't get into a loop where we either test a face-based replacement # and fail to notice breakage, or where we have to constantly rewrite this # test and all. --daniel 2011-04-11 it "should return the legacy help when given the subcommand" do help = subject.help(appname) help.should =~ /puppet-#{appname}/ %w{SYNOPSIS USAGE DESCRIPTION OPTIONS COPYRIGHT}.each do |heading| help.should =~ /^#{heading}$/ end end it "should fail when asked for an action on a legacy command" do expect { subject.help(appname, :whatever) }. to raise_error ArgumentError, /Legacy subcommands don't take actions/ end end end diff --git a/spec/unit/util/constant_inflector_spec.rb b/spec/unit/util/constant_inflector_spec.rb index 88c1d9aa3..de0ba7b7d 100755 --- a/spec/unit/util/constant_inflector_spec.rb +++ b/spec/unit/util/constant_inflector_spec.rb @@ -1,66 +1,56 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/constant_inflector' describe Puppet::Util::ConstantInflector, "when converting file names to constants" do - before do - @inflector = Object.new - @inflector.extend(Puppet::Util::ConstantInflector) - end - it "should capitalize terms" do - @inflector.file2constant("file").should == "File" + subject.file2constant("file").should == "File" end it "should switch all '/' characters to double colons" do - @inflector.file2constant("file/other").should == "File::Other" + subject.file2constant("file/other").should == "File::Other" end it "should remove underscores and capitalize the proceeding letter" do - @inflector.file2constant("file_other").should == "FileOther" + subject.file2constant("file_other").should == "FileOther" end it "should correctly replace as many underscores as exist in the file name" do - @inflector.file2constant("two_under_scores/with_some_more_underscores").should == "TwoUnderScores::WithSomeMoreUnderscores" + subject.file2constant("two_under_scores/with_some_more_underscores").should == "TwoUnderScores::WithSomeMoreUnderscores" end it "should collapse multiple underscores" do - @inflector.file2constant("many___scores").should == "ManyScores" + subject.file2constant("many___scores").should == "ManyScores" end it "should correctly handle file names deeper than two directories" do - @inflector.file2constant("one_two/three_four/five_six").should == "OneTwo::ThreeFour::FiveSix" + subject.file2constant("one_two/three_four/five_six").should == "OneTwo::ThreeFour::FiveSix" end end describe Puppet::Util::ConstantInflector, "when converting constnats to file names" do - before do - @inflector = Object.new - @inflector.extend(Puppet::Util::ConstantInflector) - end - it "should convert them to a string if necessary" do - @inflector.constant2file(Puppet::Util::ConstantInflector).should be_instance_of(String) + subject.constant2file(Puppet::Util::ConstantInflector).should be_instance_of(String) end it "should accept string inputs" do - @inflector.constant2file("Puppet::Util::ConstantInflector").should be_instance_of(String) + subject.constant2file("Puppet::Util::ConstantInflector").should be_instance_of(String) end it "should downcase all terms" do - @inflector.constant2file("Puppet").should == "puppet" + subject.constant2file("Puppet").should == "puppet" end it "should convert '::' to '/'" do - @inflector.constant2file("Puppet::Util::Constant").should == "puppet/util/constant" + subject.constant2file("Puppet::Util::Constant").should == "puppet/util/constant" end it "should convert mid-word capitalization to an underscore" do - @inflector.constant2file("OneTwo::ThreeFour").should == "one_two/three_four" + subject.constant2file("OneTwo::ThreeFour").should == "one_two/three_four" end it "should correctly handle constants with more than two parts" do - @inflector.constant2file("OneTwoThree::FourFiveSixSeven").should == "one_two_three/four_five_six_seven" + subject.constant2file("OneTwoThree::FourFiveSixSeven").should == "one_two_three/four_five_six_seven" end end