X-Git-Url: https://review.fuel-infra.org/gitweb?a=blobdiff_plain;f=lib%2Fmcollective%2Fapplication.rb;fp=lib%2Fmcollective%2Fapplication.rb;h=66751e03e3db6a0c53bc99b4b67ab4e5a1266ff6;hb=b87d2f4e68281062df1913440ca5753ae63314a9;hp=0000000000000000000000000000000000000000;hpb=ab0ea530b8ac956091f17b104ab2311336cfc250;p=packages%2Fprecise%2Fmcollective.git diff --git a/lib/mcollective/application.rb b/lib/mcollective/application.rb new file mode 100644 index 0000000..66751e0 --- /dev/null +++ b/lib/mcollective/application.rb @@ -0,0 +1,374 @@ +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