diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index ea5ba4aaf..a111518f1 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,246 +1,247 @@ require 'puppet/application' require 'puppet/face' require 'optparse' require 'pp' class Puppet::Application::FaceBase < Puppet::Application should_parse_config 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 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) set_run_mode self.class.run_mode 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) # Invoke the rendering hook supplied by the user, if appropriate. if hook = action.when_rendering(render_as.name) result = hook.call(result) 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 @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 = @face.get_action(action_name) + @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 # REVISIT: ...and this horror thanks to our log setup, which doesn't # initialize destinations until the setup method, which we will never # reach. We could also just print here, but that is actually a little # uglier and nastier in the long term, in which we should do log setup # earlier if at all possible. --daniel 2011-05-31 Puppet::Util::Log.newdestination(:console) face = @face.name action = action_name.nil? ? 'default' : "'#{action_name}'" msg = "'#{face}' has no #{action} action. See `puppet help #{face}`." Puppet.err(msg) 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) unless result.nil? status = true rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail.to_s Puppet.err "Try 'puppet help #{@face.name} #{@action.name}' for usage" ensure exit status end end diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb index 6c288f3c0..eba99d6be 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -1,157 +1,161 @@ require 'puppet' require 'puppet/util/autoload' require 'puppet/interface/documentation' require 'prettyprint' require 'semver' class Puppet::Interface include FullDocs require 'puppet/interface/face_collection' require 'puppet/interface/action_manager' include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager require 'puppet/interface/option_manager' include Puppet::Interface::OptionManager extend Puppet::Interface::OptionManager include Puppet::Util class << self # This is just so we can search for actions. We only use its # list of directories to search. # Can't we utilize an external autoloader, or simply use the $LOAD_PATH? -pvb def autoloader @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/face") end def faces Puppet::Interface::FaceCollection.faces end def register(instance) Puppet::Interface::FaceCollection.register(instance) end def define(name, version, &block) face = Puppet::Interface::FaceCollection[name, version] if face.nil? then face = self.new(name, version) Puppet::Interface::FaceCollection.register(face) # REVISIT: Shouldn't this be delayed until *after* we evaluate the # current block, not done before? --daniel 2011-04-07 face.load_actions end face.instance_eval(&block) if block_given? return face end def face?(name, version) Puppet::Interface::FaceCollection[name, version] end def [](name, version) unless face = Puppet::Interface::FaceCollection[name, version] if current = Puppet::Interface::FaceCollection[name, :current] raise Puppet::Error, "Could not find version #{version} of #{name}" else raise Puppet::Error, "Could not find Puppet Face #{name.inspect}" end end face end + + def find_action(name, action, version = :current) + Puppet::Interface::FaceCollection.get_action_for_face(name, action, version) + end end def set_default_format(format) Puppet.warning("set_default_format is deprecated (and ineffective); use render_as on your actions instead.") end ######################################################################## # Documentation. We currently have to rewrite both getters because we share # the same instance between build-time and the runtime instance. When that # splits out this should merge into a module that both the action and face # include. --daniel 2011-04-17 def synopsis build_synopsis self.name, '' end ######################################################################## attr_reader :name, :version def initialize(name, version, &block) unless SemVer.valid?(version) raise ArgumentError, "Cannot create face #{name.inspect} with invalid version number '#{version}'!" end @name = Puppet::Interface::FaceCollection.underscorize(name) @version = SemVer.new(version) # The few bits of documentation we actually demand. The default license # is a favour to our end users; if you happen to get that in a core face # report it as a bug, please. --daniel 2011-04-26 @authors = [] @license = 'All Rights Reserved' instance_eval(&block) if block_given? end # Try to find actions defined in other files. def load_actions Puppet::Interface.autoloader.search_directories.each do |dir| Dir.glob(File.join(dir, "puppet/face/#{name}", "*.rb")).each do |file| action = file.sub(dir, '').sub(/^[\\\/]/, '').sub(/\.rb/, '') Puppet.debug "Loading action '#{action}' for '#{name}' from '#{dir}/#{action}.rb'" require(action) end end end def to_s "Puppet::Face[#{name.inspect}, #{version.inspect}]" end ######################################################################## # Action decoration, whee! You are not expected to care about this code, # which exists to support face building and construction. I marked these # private because the implementation is crude and ugly, and I don't yet know # enough to work out how to make it clean. # # Once we have established that these methods will likely change radically, # to be unrecognizable in the final outcome. At which point we will throw # all this away, replace it with something nice, and work out if we should # be making this visible to the outside world... --daniel 2011-04-14 private def __invoke_decorations(type, action, passed_args = [], passed_options = {}) [:before, :after].member?(type) or fail "unknown decoration type #{type}" # Collect the decoration methods matching our pass. methods = action.options.select do |name| passed_options.has_key? name end.map do |name| action.get_option(name).__decoration_name(type) end methods.reverse! if type == :after # Exceptions here should propagate up; this implements a hook we can use # reasonably for option validation. methods.each do |hook| respond_to? hook and self.__send__(hook, action, passed_args, passed_options) end end def __add_method(name, proc) meta_def(name, &proc) method(name).unbind end def self.__add_method(name, proc) define_method(name, proc) instance_method(name) end end diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb index fc1121eb6..ce9c60b49 100644 --- a/lib/puppet/interface/action.rb +++ b/lib/puppet/interface/action.rb @@ -1,294 +1,295 @@ require 'puppet/interface' require 'puppet/interface/documentation' require 'prettyprint' class Puppet::Interface::Action extend Puppet::Interface::DocGen include Puppet::Interface::FullDocs def initialize(face, name, attrs = {}) raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ @face = face @name = name.to_sym # The few bits of documentation we actually demand. The default license # is a favour to our end users; if you happen to get that in a core face # report it as a bug, please. --daniel 2011-04-26 @authors = [] @license = 'All Rights Reserved' attrs.each do |k, v| send("#{k}=", v) end # @options collects the added options in the order they're declared. # @options_hash collects the options keyed by alias for quick lookups. @options = [] @options_hash = {} @when_rendering = {} end # This is not nice, but it is the easiest way to make us behave like the # Ruby Method object rather than UnboundMethod. Duplication is vaguely # annoying, but at least we are a shallow clone. --daniel 2011-04-12 def __dup_and_rebind_to(to) bound_version = self.dup bound_version.instance_variable_set(:@face, to) return bound_version end def to_s() "#{@face}##{@name}" end attr_reader :name + attr_reader :face attr_accessor :default def default? !!@default end ######################################################################## # Documentation... attr_doc :returns attr_doc :arguments def synopsis build_synopsis(@face.name, default? ? nil : name, arguments) end ######################################################################## # Support for rendering formats and all. def when_rendering(type) unless type.is_a? Symbol raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" end # Do we have a rendering hook for this name? return @when_rendering[type].bind(@face) if @when_rendering.has_key? type # How about by another name? alt = type.to_s.sub(/^to_/, '').to_sym return @when_rendering[alt].bind(@face) if @when_rendering.has_key? alt # Guess not, nothing to run. return nil end def set_rendering_method_for(type, proc) unless proc.is_a? Proc msg = "The second argument to set_rendering_method_for must be a Proc" msg += ", not #{proc.class.name}" unless proc.nil? raise ArgumentError, msg end if proc.arity != 1 then msg = "when_rendering methods take one argument, the result, not " if proc.arity < 0 then msg += "a variable number" else msg += proc.arity.to_s end raise ArgumentError, msg end unless type.is_a? Symbol raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" end if @when_rendering.has_key? type then raise ArgumentError, "You can't define a rendering method for #{type} twice" end # Now, the ugly bit. We add the method to our interface object, and # retrieve it, to rotate through the dance of getting a suitable method # object out of the whole process. --daniel 2011-04-18 @when_rendering[type] = @face.__send__( :__add_method, __render_method_name_for(type), proc) end def __render_method_name_for(type) :"#{name}_when_rendering_#{type}" end private :__render_method_name_for attr_accessor :render_as def render_as=(value) @render_as = value.to_sym end ######################################################################## # Initially, this was defined to allow the @action.invoke pattern, which is # a very natural way to invoke behaviour given our introspection # capabilities. Heck, our initial plan was to have the faces delegate to # the action object for invocation and all. # # It turns out that we have a binding problem to solve: @face was bound to # the parent class, not the subclass instance, and we don't pass the # appropriate context or change the binding enough to make this work. # # We could hack around it, by either mandating that you pass the context in # to invoke, or try to get the binding right, but that has probably got # subtleties that we don't instantly think of – especially around threads. # # So, we are pulling this method for now, and will return it to life when we # have the time to resolve the problem. For now, you should replace... # # @action = @face.get_action(name) # @action.invoke(arg1, arg2, arg3) # # ...with... # # @action = @face.get_action(name) # @face.send(@action.name, arg1, arg2, arg3) # # I understand that is somewhat cumbersome, but it functions as desired. # --daniel 2011-03-31 # # PS: This code is left present, but commented, to support this chunk of # documentation, for the benefit of the reader. # # def invoke(*args, &block) # @face.send(name, *args, &block) # end # We need to build an instance method as a wrapper, using normal code, to be # able to expose argument defaulting between the caller and definer in the # Ruby API. An extra method is, sadly, required for Ruby 1.8 to work since # it doesn't expose bind on a block. # # Hopefully we can improve this when we finally shuffle off the last of Ruby # 1.8 support, but that looks to be a few "enterprise" release eras away, so # we are pretty stuck with this for now. # # Patches to make this work more nicely with Ruby 1.9 using runtime version # checking and all are welcome, provided that they don't change anything # outside this little ol' bit of code and all. # # Incidentally, we though about vendoring evil-ruby and actually adjusting # the internal C structure implementation details under the hood to make # this stuff work, because it would have been cleaner. Which gives you an # idea how motivated we were to make this cleaner. Sorry. # --daniel 2011-03-31 attr_reader :positional_arg_count attr_accessor :when_invoked def when_invoked=(block) internal_name = "#{@name} implementation, required on Ruby 1.8".to_sym arity = @positional_arg_count = block.arity if arity == 0 then # This will never fire on 1.8.7, which treats no arguments as "*args", # but will on 1.9.2, which treats it as "no arguments". Which bites, # because this just begs for us to wind up in the horrible situation # where a 1.8 vs 1.9 error bites our end users. --daniel 2011-04-19 raise ArgumentError, "when_invoked requires at least one argument (options) for action #{@name}" elsif arity > 0 then range = Range.new(1, arity - 1) decl = range.map { |x| "arg#{x}" } << "options = {}" optn = "" args = "[" + (range.map { |x| "arg#{x}" } << "options").join(", ") + "]" else range = Range.new(1, arity.abs - 1) decl = range.map { |x| "arg#{x}" } << "*rest" optn = "rest << {} unless rest.last.is_a?(Hash)" if arity == -1 then args = "rest" else args = "[" + range.map { |x| "arg#{x}" }.join(", ") + "] + rest" end end file = __FILE__ + "+eval[wrapper]" line = __LINE__ + 2 # <== points to the same line as 'def' in the wrapper. wrapper = <