Updated mcollective.init according to OSCI-658
[packages/precise/mcollective.git] / plugins / mcollective / application / inventory.rb
diff --git a/plugins/mcollective/application/inventory.rb b/plugins/mcollective/application/inventory.rb
new file mode 100644 (file)
index 0000000..010bc68
--- /dev/null
@@ -0,0 +1,340 @@
+class MCollective::Application::Inventory<MCollective::Application
+  description "General reporting tool for nodes, collectives and subcollectives"
+
+  option :script,
+         :description    => "Script to run",
+         :arguments      => ["--script SCRIPT"]
+
+  option :collectives,
+         :description    => "List all known collectives",
+         :arguments      => ["--list-collectives", "--lc"],
+         :default        => false,
+         :type           => :bool
+
+  option :collectivemap,
+         :description    => "Create a DOT graph of all collectives",
+         :arguments      => ["--collective-graph MAP", "--cg MAP", "--map MAP"]
+
+  def post_option_parser(configuration)
+    configuration[:node] = ARGV.shift if ARGV.size > 0
+  end
+
+  def validate_configuration(configuration)
+    unless configuration[:node] || configuration[:script] || configuration[:collectives] || configuration[:collectivemap]
+      raise "Need to specify either a node name, script to run or other options"
+    end
+  end
+
+  # Get all the known collectives and nodes that belong to them
+  def get_collectives
+    util = rpcclient("rpcutil")
+    util.progress = false
+
+    collectives = {}
+    nodes = 0
+    total = 0
+
+    util.collective_info do |r, cinfo|
+      begin
+        if cinfo[:data] && cinfo[:data][:collectives]
+          cinfo[:data][:collectives].each do |collective|
+            collectives[collective] ||= []
+            collectives[collective]  << cinfo[:sender]
+          end
+
+          nodes += 1
+          total += 1
+        end
+      end
+    end
+
+    {:collectives => collectives, :nodes => nodes, :total_nodes => total}
+  end
+
+  # Writes a crude DOT graph to a file
+  def collectives_map(file)
+    File.open(file, "w") do |graph|
+      puts "Retrieving collective info...."
+      collectives = get_collectives
+
+      graph.puts 'graph {'
+
+      collectives[:collectives].keys.sort.each do |collective|
+        graph.puts '   subgraph "%s" {' % [ collective ]
+
+        collectives[:collectives][collective].each do |member|
+          graph.puts '      "%s" -- "%s"' % [ member, collective ]
+        end
+
+        graph.puts '   }'
+      end
+
+      graph.puts '}'
+
+      puts "Graph of #{collectives[:total_nodes]} nodes has been written to #{file}"
+    end
+  end
+
+  # Prints a report of all known sub collectives
+  def collectives_report
+    collectives = get_collectives
+
+    puts "   %-30s %s" % [ "Collective", "Nodes" ]
+    puts "   %-30s %s" % [ "==========", "=====" ]
+
+    collectives[:collectives].sort_by {|key,count| count.size}.each do |collective|
+      puts "   %-30s %d" % [ collective[0], collective[1].size ]
+    end
+
+    puts
+    puts "   %30s %d" % [ "Total nodes:", collectives[:nodes] ]
+    puts
+  end
+
+  def node_inventory
+    node = configuration[:node]
+
+    util = rpcclient("rpcutil")
+    util.identity_filter node
+    util.progress = false
+
+    nodestats = util.custom_request("daemon_stats", {}, node, {"identity" => node}).first
+
+    unless nodestats
+      STDERR.puts "Did not receive any results from node #{node}"
+      exit 1
+    end
+
+    unless nodestats[:statuscode] == 0
+      STDERR.puts "Failed to retrieve daemon_stats from #{node}: #{nodestats[:statusmsg]}"
+    else
+      util.custom_request("inventory", {}, node, {"identity" => node}).each do |resp|
+        unless resp[:statuscode] == 0
+          STDERR.puts "Failed to retrieve inventory for #{node}: #{resp[:statusmsg]}"
+          next
+        end
+
+        data = resp[:data]
+
+        begin
+          puts "Inventory for #{resp[:sender]}:"
+          puts
+
+          nodestats = nodestats[:data]
+
+          puts "   Server Statistics:"
+          puts "                      Version: #{nodestats[:version]}"
+          puts "                   Start Time: #{Time.at(nodestats[:starttime])}"
+          puts "                  Config File: #{nodestats[:configfile]}"
+          puts "                  Collectives: #{data[:collectives].join(', ')}" if data.include?(:collectives)
+          puts "              Main Collective: #{data[:main_collective]}" if data.include?(:main_collective)
+          puts "                   Process ID: #{nodestats[:pid]}"
+          puts "               Total Messages: #{nodestats[:total]}"
+          puts "      Messages Passed Filters: #{nodestats[:passed]}"
+          puts "            Messages Filtered: #{nodestats[:filtered]}"
+          puts "             Expired Messages: #{nodestats[:ttlexpired]}"
+          puts "                 Replies Sent: #{nodestats[:replies]}"
+          puts "         Total Processor Time: #{nodestats[:times][:utime]} seconds"
+          puts "                  System Time: #{nodestats[:times][:stime]} seconds"
+
+          puts
+
+          puts "   Agents:"
+          if data[:agents].size > 0
+            data[:agents].sort.in_groups_of(3, "") do |agents|
+              puts "      %-15s %-15s %-15s" % agents
+            end
+          else
+            puts "      No agents installed"
+          end
+
+          puts
+
+          puts "   Data Plugins:"
+          if data[:data_plugins].size > 0
+            data[:data_plugins].sort.in_groups_of(3, "") do |plugins|
+              puts "      %-15s %-15s %-15s" % plugins.map{|p| p.gsub("_data", "")}
+            end
+          else
+            puts "      No data plugins installed"
+          end
+
+          puts
+
+          puts "   Configuration Management Classes:"
+          if data[:classes].size > 0
+            data[:classes].sort.in_groups_of(2, "") do |klasses|
+              puts "      %-30s %-30s" % klasses
+            end
+          else
+            puts "      No classes applied"
+          end
+
+          puts
+
+          puts "   Facts:"
+          if data[:facts].size > 0
+            data[:facts].sort_by{|f| f[0]}.each do |f|
+              puts "      #{f[0]} => #{f[1]}"
+            end
+          else
+            puts "      No facts known"
+          end
+
+          break
+        rescue Exception => e
+          STDERR.puts "Failed to display node inventory: #{e.class}: #{e}"
+        end
+      end
+    end
+
+    halt util.stats
+  end
+
+  # Helpers to create a simple DSL for scriptlets
+  def format(fmt)
+    @fmt = fmt
+  end
+
+  def fields(&blk)
+    @flds = blk
+  end
+
+  def identity
+    @node[:identity]
+  end
+
+  def facts
+    @node[:facts]
+  end
+
+  def classes
+    @node[:classes]
+  end
+
+  def agents
+    @node[:agents]
+  end
+
+  def page_length(len)
+    @page_length = len
+  end
+
+  def page_heading(fmt)
+    @page_heading = fmt
+  end
+
+  def page_body(fmt)
+    @page_body = fmt
+  end
+
+  # Expects a simple printf style format and apply it to
+  # each node:
+  #
+  #    inventory do
+  #        format "%s:\t\t%s\t\t%s"
+  #
+  #        fields { [ identity, facts["serialnumber"], facts["productname"] ] }
+  #    end
+  def inventory(&blk)
+    raise "Need to give a block to inventory" unless block_given?
+
+    blk.call if block_given?
+
+    raise "Need to define a format" if @fmt.nil?
+    raise "Need to define inventory fields" if @flds.nil?
+
+    util = rpcclient("rpcutil")
+    util.progress = false
+
+    util.inventory do |t, resp|
+      @node = {:identity => resp[:sender],
+        :facts    => resp[:data][:facts],
+        :classes  => resp[:data][:classes],
+        :agents   => resp[:data][:agents]}
+
+      puts @fmt % @flds.call
+    end
+  end
+
+  # Use the ruby formatr gem to build reports using Perls formats
+  #
+  # It is kind of ugly but brings a lot of flexibility in report
+  # writing without building an entire reporting language.
+  #
+  # You need to have formatr installed to enable reports like:
+  #
+  #    formatted_inventory do
+  #        page_length 20
+  #
+  #        page_heading <<TOP
+  #
+  #                Node Report @<<<<<<<<<<<<<<<<<<<<<<<<<
+  #                            time
+  #
+  #    Hostname:         Customer:     Distribution:
+  #    -------------------------------------------------------------------------
+  #    TOP
+  #
+  #        page_body <<BODY
+  #
+  #    @<<<<<<<<<<<<<<<< @<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+  #    identity,    facts["customer"], facts["lsbdistdescription"]
+  #                                    @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
+  #                                    facts["processor0"]
+  #    BODY
+  #    end
+  def formatted_inventory(&blk)
+    require 'formatr'
+
+    raise "Need to give a block to formatted_inventory" unless block_given?
+
+    blk.call if block_given?
+
+    raise "Need to define page body format" if @page_body.nil?
+
+    body_fmt = FormatR::Format.new(@page_heading, @page_body)
+    body_fmt.setPageLength(@page_length)
+    time = Time.now
+
+    util = rpcclient("rpcutil")
+    util.progress = false
+
+    util.inventory do |t, resp|
+      @node = {:identity => resp[:sender],
+        :facts    => resp[:data][:facts],
+        :classes  => resp[:data][:classes],
+        :agents   => resp[:data][:agents]}
+
+      body_fmt.printFormat(binding)
+    end
+  rescue Exception => e
+    STDERR.puts "Could not create report: #{e.class}: #{e}"
+    exit 1
+  end
+
+  @fmt = nil
+  @flds = nil
+  @page_heading = nil
+  @page_body = nil
+  @page_length = 40
+
+  def main
+    if configuration[:script]
+      if File.exist?(configuration[:script])
+        eval(File.read(configuration[:script]))
+      else
+        raise "Could not find script to run: #{configuration[:script]}"
+      end
+
+    elsif configuration[:collectivemap]
+      collectives_map(configuration[:collectivemap])
+
+    elsif configuration[:collectives]
+      collectives_report
+
+    else
+      node_inventory
+    end
+  end
+end