module MCollective class Application include RPC class << self # Intialize a blank set of options if its the first time used # else returns active options def application_options intialize_application_options unless @application_options @application_options end # set an option in the options hash def []=(option, value) intialize_application_options unless @application_options @application_options[option] = value end # retrieves a specific option def [](option) intialize_application_options unless @application_options @application_options[option] end # Sets the application description, there can be only one # description per application so multiple calls will just # change the description def description(descr) self[:description] = descr end # Supplies usage information, calling multiple times will # create multiple usage lines in --help output def usage(usage) self[:usage] << usage end def exclude_argument_sections(*sections) sections = [sections].flatten sections.each do |s| raise "Unknown CLI argument section #{s}" unless ["rpc", "common", "filter"].include?(s) end intialize_application_options unless @application_options self[:exclude_arg_sections] = sections end # Wrapper to create command line options # # - name: varaible name that will be used to access the option value # - description: textual info shown in --help # - arguments: a list of possible arguments that can be used # to activate this option # - type: a data type that ObjectParser understand of :bool or :array # - required: true or false if this option has to be supplied # - validate: a proc that will be called with the value used to validate # the supplied value # # option :foo, # :description => "The foo option" # :arguments => ["--foo ARG"] # # after this the value supplied will be in configuration[:foo] def option(name, arguments) opt = {:name => name, :description => nil, :arguments => [], :type => String, :required => false, :validate => Proc.new { true }} arguments.each_pair{|k,v| opt[k] = v} self[:cli_arguments] << opt end # Creates an empty set of options def intialize_application_options @application_options = {:description => nil, :usage => [], :cli_arguments => [], :exclude_arg_sections => []} end end # The application configuration built from CLI arguments def configuration @application_configuration ||= {} @application_configuration end # The active options hash used for MC::Client and other configuration def options @options end # Calls the supplied block in an option for validation, an error raised # will log to STDERR and exit the application def validate_option(blk, name, value) validation_result = blk.call(value) unless validation_result == true STDERR.puts "Validation of #{name} failed: #{validation_result}" exit 1 end end # Creates a standard options hash, pass in a block to add extra headings etc # see Optionparser def clioptions(help) oparser = Optionparser.new({:verbose => false, :progress_bar => true}, "filter", application_options[:exclude_arg_sections]) options = oparser.parse do |parser, options| if block_given? yield(parser, options) end RPC::Helpers.add_simplerpc_options(parser, options) unless application_options[:exclude_arg_sections].include?("rpc") end return oparser.parser.help if help validate_cli_options post_option_parser(configuration) if respond_to?(:post_option_parser) return options rescue Exception => e application_failure(e) end # Builds an ObjectParser config, parse the CLI options and validates based # on the option config def application_parse_options(help=false) @options ||= {:verbose => false} @options = clioptions(help) do |parser, options| parser.define_head application_description if application_description parser.banner = "" if application_usage parser.separator "" application_usage.each do |u| parser.separator "Usage: #{u}" end parser.separator "" end parser.separator "Application Options" unless application_cli_arguments.empty? parser.define_tail "" parser.define_tail "The Marionette Collective #{MCollective.version}" application_cli_arguments.each do |carg| opts_array = [] opts_array << :on # if a default is set from the application set it up front if carg.include?(:default) configuration[carg[:name]] = carg[:default] end # :arguments are multiple possible ones if carg[:arguments].is_a?(Array) carg[:arguments].each {|a| opts_array << a} else opts_array << carg[:arguments] end # type was given and its not one of our special types, just pass it onto optparse opts_array << carg[:type] if carg[:type] && ![:boolean, :bool, :array].include?(carg[:type]) opts_array << carg[:description] # Handle our special types else just rely on the optparser to handle the types if [:bool, :boolean].include?(carg[:type]) parser.send(*opts_array) do |v| validate_option(carg[:validate], carg[:name], v) configuration[carg[:name]] = v end elsif carg[:type] == :array parser.send(*opts_array) do |v| validate_option(carg[:validate], carg[:name], v) configuration[carg[:name]] = [] unless configuration.include?(carg[:name]) configuration[carg[:name]] << v end else parser.send(*opts_array) do |v| validate_option(carg[:validate], carg[:name], v) configuration[carg[:name]] = v end end end end end def validate_cli_options # Check all required parameters were set validation_passed = true application_cli_arguments.each do |carg| # Check for required arguments if carg[:required] unless configuration[ carg[:name] ] validation_passed = false STDERR.puts "The #{carg[:name]} option is mandatory" end end end unless validation_passed STDERR.puts "\nPlease run with --help for detailed help" exit 1 end end # Retrieves the full hash of application options def application_options self.class.application_options end # Retrieve the current application description def application_description application_options[:description] end # Return the current usage text false if nothing is set def application_usage usage = application_options[:usage] usage.empty? ? false : usage end # Returns an array of all the arguments built using # calls to optin def application_cli_arguments application_options[:cli_arguments] end # Handles failure, if we're far enough in the initialization # phase it will log backtraces if its in verbose mode only def application_failure(e, err_dest=STDERR) # peole can use exit() anywhere and not get nasty backtraces as a result if e.is_a?(SystemExit) disconnect raise(e) end if e.is_a?(CodedError) err_dest.puts "\nThe %s application failed to run: %s: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:bold, e.code), Util.colorize(:red, e.to_s)] err_dest.puts if options[:verbose] err_dest.puts "Use the 'mco doc %s' command for details about this error" % e.code else err_dest.puts "Use the 'mco doc %s' command for details about this error, use -v for full error backtrace details" % e.code end else if options[:verbose] err_dest.puts "\nThe %s application failed to run: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)] else err_dest.puts "\nThe %s application failed to run, use -v for full error backtrace details: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)] end end if options.nil? || options[:verbose] e.backtrace.first << Util.colorize(:red, " <----") err_dest.puts "\n%s %s" % [ Util.colorize(:red, e.to_s), Util.colorize(:bold, "(#{e.class.to_s})")] e.backtrace.each{|l| err_dest.puts "\tfrom #{l}"} end disconnect exit 1 end def help application_parse_options(true) end # The main logic loop, builds up the options, validate configuration and calls # the main as supplied by the user. Disconnects when done and pass any exception # onto the application_failure helper def run application_parse_options validate_configuration(configuration) if respond_to?(:validate_configuration) Util.setup_windows_sleeper if Util.windows? main disconnect rescue Exception => e application_failure(e) end def disconnect MCollective::PluginManager["connector_plugin"].disconnect rescue end # Fake abstract class that logs if the user tries to use an application without # supplying a main override method. def main STDERR.puts "Applications need to supply a 'main' method" exit 1 end # A helper that creates a consistent exit code for applications by looking at an # instance of MCollective::RPC::Stats # # Exit with 0 if nodes were discovered and all passed # Exit with 0 if no discovery were done and > 0 responses were received # Exit with 1 if no nodes were discovered # Exit with 2 if nodes were discovered but some RPC requests failed # Exit with 3 if nodes were discovered, but not responses received # Exit with 4 if no discovery were done and no responses were received def halt(stats) request_stats = {:discoverytime => 0, :discovered => 0, :failcount => 0}.merge(stats.to_hash) # was discovery done? if request_stats[:discoverytime] != 0 # was any nodes discovered if request_stats[:discovered] == 0 exit 1 # nodes were discovered, did we get responses elsif request_stats[:responses] == 0 exit 3 else # we got responses and discovery was done, no failures if request_stats[:failcount] == 0 exit 0 else exit 2 end end else # discovery wasnt done and we got no responses if request_stats[:responses] == 0 exit 4 else exit 0 end end end # Wrapper around MC::RPC#rpcclient that forcably supplies our options hash # if someone forgets to pass in options in an application the filters and other # cli options wouldnt take effect which could have a disasterous outcome def rpcclient(agent, flags = {}) flags[:options] = options unless flags.include?(:options) flags[:exit_on_failure] = false super end end end