Updated mcollective.init according to OSCI-658
[packages/precise/mcollective.git] / lib / mcollective / application.rb
diff --git a/lib/mcollective/application.rb b/lib/mcollective/application.rb
new file mode 100644 (file)
index 0000000..66751e0
--- /dev/null
@@ -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