Updated mcollective.init according to OSCI-658
[packages/precise/mcollective.git] / lib / mcollective / rpc / helpers.rb
diff --git a/lib/mcollective/rpc/helpers.rb b/lib/mcollective/rpc/helpers.rb
new file mode 100644 (file)
index 0000000..792736f
--- /dev/null
@@ -0,0 +1,306 @@
+module MCollective
+  module RPC
+    # Various utilities for the RPC system
+    class Helpers
+      # Parse JSON output as produced by printrpc and extract
+      # the "sender" of each rpc response
+      #
+      # The simplist valid JSON based data would be:
+      #
+      # [
+      #  {"sender" => "example.com"},
+      #  {"sender" => "another.com"}
+      # ]
+      def self.extract_hosts_from_json(json)
+        hosts = JSON.parse(json)
+
+        raise "JSON hosts list is not an array" unless hosts.is_a?(Array)
+
+        hosts.map do |host|
+          raise "JSON host list is not an array of Hashes" unless host.is_a?(Hash)
+          raise "JSON host list does not have senders in it" unless host.include?("sender")
+
+          host["sender"]
+        end.uniq
+      end
+
+      # Given an array of something, make sure each is a string
+      # chomp off any new lines and return just the array of hosts
+      def self.extract_hosts_from_array(hosts)
+        [hosts].flatten.map do |host|
+          raise "#{host} should be a string" unless host.is_a?(String)
+          host.chomp
+        end
+      end
+
+      # Returns a blob of text representing the results in a standard way
+      #
+      # It tries hard to do sane things so you often
+      # should not need to write your own display functions
+      #
+      # If the agent you are getting results for has a DDL
+      # it will use the hints in there to do the right thing specifically
+      # it will look at the values of display in the DDL to choose
+      # when to show results
+      #
+      # If you do not have a DDL you can pass these flags:
+      #
+      #    printrpc exim.mailq, :flatten => true
+      #    printrpc exim.mailq, :verbose => true
+      #
+      # If you've asked it to flatten the result it will not print sender
+      # hostnames, it will just print the result as if it's one huge result,
+      # handy for things like showing a combined mailq.
+      def self.rpcresults(result, flags = {})
+        flags = {:verbose => false, :flatten => false, :format => :console, :force_display_mode => false}.merge(flags)
+
+        result_text = ""
+        ddl = nil
+
+        # if running in verbose mode, just use the old style print
+        # no need for all the DDL helpers obfuscating the result
+        if flags[:format] == :json
+          if STDOUT.tty?
+            result_text = JSON.pretty_generate(result)
+          else
+            result_text = result.to_json
+          end
+        else
+          if flags[:verbose]
+            result_text = old_rpcresults(result, flags)
+          else
+            [result].flatten.each do |r|
+              begin
+                ddl ||= DDL.new(r.agent).action_interface(r.action.to_s)
+
+                sender = r[:sender]
+                status = r[:statuscode]
+                message = r[:statusmsg]
+                result = r[:data]
+
+                if flags[:force_display_mode]
+                  display = flags[:force_display_mode]
+                else
+                  display = ddl[:display]
+                end
+
+                # appand the results only according to what the DDL says
+                case display
+                  when :ok
+                    if status == 0
+                      result_text << text_for_result(sender, status, message, result, ddl)
+                    end
+
+                  when :failed
+                    if status > 0
+                      result_text << text_for_result(sender, status, message, result, ddl)
+                    end
+
+                  when :always
+                    result_text << text_for_result(sender, status, message, result, ddl)
+
+                  when :flatten
+                    result_text << text_for_flattened_result(status, result)
+
+                end
+              rescue Exception => e
+                # no DDL so just do the old style print unchanged for
+                # backward compat
+                result_text = old_rpcresults(result, flags)
+              end
+            end
+          end
+        end
+
+        result_text
+      end
+
+      # Return text representing a result
+      def self.text_for_result(sender, status, msg, result, ddl)
+        statusses = ["",
+                     Util.colorize(:red, "Request Aborted"),
+                     Util.colorize(:yellow, "Unknown Action"),
+                     Util.colorize(:yellow, "Missing Request Data"),
+                     Util.colorize(:yellow, "Invalid Request Data"),
+                     Util.colorize(:red, "Unknown Request Status")]
+
+        result_text = "%-40s %s\n" % [sender, statusses[status]]
+        result_text << "   %s\n" % [Util.colorize(:yellow, msg)] unless msg == "OK"
+
+        # only print good data, ignore data that results from failure
+        if status == 0
+          if result.is_a?(Hash)
+            # figure out the lengths of the display as strings, we'll use
+            # it later to correctly justify the output
+            lengths = result.keys.map do |k|
+              begin
+                ddl[:output][k][:display_as].size
+              rescue
+                k.to_s.size
+              end
+            end
+
+            result.keys.sort_by{|k| k}.each do |k|
+              # get all the output fields nicely lined up with a
+              # 3 space front padding
+              begin
+                display_as = ddl[:output][k][:display_as]
+              rescue
+                display_as = k.to_s
+              end
+
+              display_length = display_as.size
+              padding = lengths.max - display_length + 3
+              result_text << " " * padding
+
+              result_text << "#{display_as}:"
+
+              if [String, Numeric].include?(result[k].class)
+                lines = result[k].to_s.split("\n")
+
+                if lines.empty?
+                  result_text << "\n"
+                else
+                  lines.each_with_index do |line, i|
+                    i == 0 ? padtxt = " " : padtxt = " " * (padding + display_length + 2)
+
+                    result_text << "#{padtxt}#{line}\n"
+                  end
+                end
+              else
+                padding = " " * (lengths.max + 5)
+                result_text << " " << result[k].pretty_inspect.split("\n").join("\n" << padding) << "\n"
+              end
+            end
+          elsif status == 1
+            # for status 1 we dont want to show half baked
+            # data by default since the DDL will supply all the defaults
+            # it just doesnt look right
+          else
+            result_text << "\n\t" + result.pretty_inspect.split("\n").join("\n\t")
+          end
+        end
+
+        result_text << "\n"
+        result_text
+      end
+
+      # Returns text representing a flattened result of only good data
+      def self.text_for_flattened_result(status, result)
+        result_text = ""
+
+        if status <= 1
+          unless result.is_a?(String)
+            result_text << result.pretty_inspect
+          else
+            result_text << result
+          end
+        end
+      end
+
+      # Backward compatible display block for results without a DDL
+      def self.old_rpcresults(result, flags = {})
+        result_text = ""
+
+        if flags[:flatten]
+          result.each do |r|
+            if r[:statuscode] <= 1
+              data = r[:data]
+
+              unless data.is_a?(String)
+                result_text << data.pretty_inspect
+              else
+                result_text << data
+              end
+            else
+              result_text << r.pretty_inspect
+            end
+          end
+
+          result_text << ""
+        else
+          [result].flatten.each do |r|
+
+            if flags[:verbose]
+              result_text << "%-40s: %s\n" % [r[:sender], r[:statusmsg]]
+
+              if r[:statuscode] <= 1
+                r[:data].pretty_inspect.split("\n").each {|m| result_text += "    #{m}"}
+                result_text << "\n\n"
+              elsif r[:statuscode] == 2
+                # dont print anything, no useful data to display
+                # past what was already shown
+              elsif r[:statuscode] == 3
+                # dont print anything, no useful data to display
+                # past what was already shown
+              elsif r[:statuscode] == 4
+                # dont print anything, no useful data to display
+                # past what was already shown
+              else
+                result_text << "    #{r[:statusmsg]}"
+              end
+            else
+              unless r[:statuscode] == 0
+                result_text << "%-40s %s\n" % [r[:sender], Util.colorize(:red, r[:statusmsg])]
+              end
+            end
+          end
+        end
+
+        result_text << ""
+      end
+
+      # Add SimpleRPC common options
+      def self.add_simplerpc_options(parser, options)
+        parser.separator ""
+        parser.separator "RPC Options"
+
+        # add SimpleRPC specific options to all clients that use our library
+        parser.on('--np', '--no-progress', 'Do not show the progress bar') do |v|
+          options[:progress_bar] = false
+        end
+
+        parser.on('--one', '-1', 'Send request to only one discovered nodes') do |v|
+          options[:mcollective_limit_targets] = 1
+        end
+
+        parser.on('--batch SIZE', Integer, 'Do requests in batches') do |v|
+          options[:batch_size] = v
+        end
+
+        parser.on('--batch-sleep SECONDS', Float, 'Sleep time between batches') do |v|
+          options[:batch_sleep_time] = v
+        end
+
+        parser.on('--limit-seed NUMBER', Integer, 'Seed value for deterministic random batching') do |v|
+          options[:limit_seed] = v
+        end
+
+        parser.on('--limit-nodes COUNT', '--ln', '--limit', 'Send request to only a subset of nodes, can be a percentage') do |v|
+          raise "Invalid limit specified: #{v} valid limits are /^\d+%*$/" unless v =~ /^\d+%*$/
+
+          if v =~ /^\d+$/
+            options[:mcollective_limit_targets] = v.to_i
+          else
+            options[:mcollective_limit_targets] = v
+          end
+        end
+
+        parser.on('--json', '-j', 'Produce JSON output') do |v|
+          options[:progress_bar] = false
+          options[:output_format] = :json
+        end
+
+        parser.on('--display MODE', 'Influence how results are displayed. One of ok, all or failed') do |v|
+          if v == "all"
+            options[:force_display_mode] = :always
+          else
+            options[:force_display_mode] = v.intern
+          end
+
+          raise "--display has to be one of 'ok', 'all' or 'failed'" unless [:ok, :failed, :always].include?(options[:force_display_mode])
+        end
+      end
+    end
+  end
+end