010bc68f4a5ff67d56e77e2b788538f5c6cb615a
[packages/precise/mcollective.git] / plugins / mcollective / application / inventory.rb
1 class MCollective::Application::Inventory<MCollective::Application
2   description "General reporting tool for nodes, collectives and subcollectives"
3
4   option :script,
5          :description    => "Script to run",
6          :arguments      => ["--script SCRIPT"]
7
8   option :collectives,
9          :description    => "List all known collectives",
10          :arguments      => ["--list-collectives", "--lc"],
11          :default        => false,
12          :type           => :bool
13
14   option :collectivemap,
15          :description    => "Create a DOT graph of all collectives",
16          :arguments      => ["--collective-graph MAP", "--cg MAP", "--map MAP"]
17
18   def post_option_parser(configuration)
19     configuration[:node] = ARGV.shift if ARGV.size > 0
20   end
21
22   def validate_configuration(configuration)
23     unless configuration[:node] || configuration[:script] || configuration[:collectives] || configuration[:collectivemap]
24       raise "Need to specify either a node name, script to run or other options"
25     end
26   end
27
28   # Get all the known collectives and nodes that belong to them
29   def get_collectives
30     util = rpcclient("rpcutil")
31     util.progress = false
32
33     collectives = {}
34     nodes = 0
35     total = 0
36
37     util.collective_info do |r, cinfo|
38       begin
39         if cinfo[:data] && cinfo[:data][:collectives]
40           cinfo[:data][:collectives].each do |collective|
41             collectives[collective] ||= []
42             collectives[collective]  << cinfo[:sender]
43           end
44
45           nodes += 1
46           total += 1
47         end
48       end
49     end
50
51     {:collectives => collectives, :nodes => nodes, :total_nodes => total}
52   end
53
54   # Writes a crude DOT graph to a file
55   def collectives_map(file)
56     File.open(file, "w") do |graph|
57       puts "Retrieving collective info...."
58       collectives = get_collectives
59
60       graph.puts 'graph {'
61
62       collectives[:collectives].keys.sort.each do |collective|
63         graph.puts '   subgraph "%s" {' % [ collective ]
64
65         collectives[:collectives][collective].each do |member|
66           graph.puts '      "%s" -- "%s"' % [ member, collective ]
67         end
68
69         graph.puts '   }'
70       end
71
72       graph.puts '}'
73
74       puts "Graph of #{collectives[:total_nodes]} nodes has been written to #{file}"
75     end
76   end
77
78   # Prints a report of all known sub collectives
79   def collectives_report
80     collectives = get_collectives
81
82     puts "   %-30s %s" % [ "Collective", "Nodes" ]
83     puts "   %-30s %s" % [ "==========", "=====" ]
84
85     collectives[:collectives].sort_by {|key,count| count.size}.each do |collective|
86       puts "   %-30s %d" % [ collective[0], collective[1].size ]
87     end
88
89     puts
90     puts "   %30s %d" % [ "Total nodes:", collectives[:nodes] ]
91     puts
92   end
93
94   def node_inventory
95     node = configuration[:node]
96
97     util = rpcclient("rpcutil")
98     util.identity_filter node
99     util.progress = false
100
101     nodestats = util.custom_request("daemon_stats", {}, node, {"identity" => node}).first
102
103     unless nodestats
104       STDERR.puts "Did not receive any results from node #{node}"
105       exit 1
106     end
107
108     unless nodestats[:statuscode] == 0
109       STDERR.puts "Failed to retrieve daemon_stats from #{node}: #{nodestats[:statusmsg]}"
110     else
111       util.custom_request("inventory", {}, node, {"identity" => node}).each do |resp|
112         unless resp[:statuscode] == 0
113           STDERR.puts "Failed to retrieve inventory for #{node}: #{resp[:statusmsg]}"
114           next
115         end
116
117         data = resp[:data]
118
119         begin
120           puts "Inventory for #{resp[:sender]}:"
121           puts
122
123           nodestats = nodestats[:data]
124
125           puts "   Server Statistics:"
126           puts "                      Version: #{nodestats[:version]}"
127           puts "                   Start Time: #{Time.at(nodestats[:starttime])}"
128           puts "                  Config File: #{nodestats[:configfile]}"
129           puts "                  Collectives: #{data[:collectives].join(', ')}" if data.include?(:collectives)
130           puts "              Main Collective: #{data[:main_collective]}" if data.include?(:main_collective)
131           puts "                   Process ID: #{nodestats[:pid]}"
132           puts "               Total Messages: #{nodestats[:total]}"
133           puts "      Messages Passed Filters: #{nodestats[:passed]}"
134           puts "            Messages Filtered: #{nodestats[:filtered]}"
135           puts "             Expired Messages: #{nodestats[:ttlexpired]}"
136           puts "                 Replies Sent: #{nodestats[:replies]}"
137           puts "         Total Processor Time: #{nodestats[:times][:utime]} seconds"
138           puts "                  System Time: #{nodestats[:times][:stime]} seconds"
139
140           puts
141
142           puts "   Agents:"
143           if data[:agents].size > 0
144             data[:agents].sort.in_groups_of(3, "") do |agents|
145               puts "      %-15s %-15s %-15s" % agents
146             end
147           else
148             puts "      No agents installed"
149           end
150
151           puts
152
153           puts "   Data Plugins:"
154           if data[:data_plugins].size > 0
155             data[:data_plugins].sort.in_groups_of(3, "") do |plugins|
156               puts "      %-15s %-15s %-15s" % plugins.map{|p| p.gsub("_data", "")}
157             end
158           else
159             puts "      No data plugins installed"
160           end
161
162           puts
163
164           puts "   Configuration Management Classes:"
165           if data[:classes].size > 0
166             data[:classes].sort.in_groups_of(2, "") do |klasses|
167               puts "      %-30s %-30s" % klasses
168             end
169           else
170             puts "      No classes applied"
171           end
172
173           puts
174
175           puts "   Facts:"
176           if data[:facts].size > 0
177             data[:facts].sort_by{|f| f[0]}.each do |f|
178               puts "      #{f[0]} => #{f[1]}"
179             end
180           else
181             puts "      No facts known"
182           end
183
184           break
185         rescue Exception => e
186           STDERR.puts "Failed to display node inventory: #{e.class}: #{e}"
187         end
188       end
189     end
190
191     halt util.stats
192   end
193
194   # Helpers to create a simple DSL for scriptlets
195   def format(fmt)
196     @fmt = fmt
197   end
198
199   def fields(&blk)
200     @flds = blk
201   end
202
203   def identity
204     @node[:identity]
205   end
206
207   def facts
208     @node[:facts]
209   end
210
211   def classes
212     @node[:classes]
213   end
214
215   def agents
216     @node[:agents]
217   end
218
219   def page_length(len)
220     @page_length = len
221   end
222
223   def page_heading(fmt)
224     @page_heading = fmt
225   end
226
227   def page_body(fmt)
228     @page_body = fmt
229   end
230
231   # Expects a simple printf style format and apply it to
232   # each node:
233   #
234   #    inventory do
235   #        format "%s:\t\t%s\t\t%s"
236   #
237   #        fields { [ identity, facts["serialnumber"], facts["productname"] ] }
238   #    end
239   def inventory(&blk)
240     raise "Need to give a block to inventory" unless block_given?
241
242     blk.call if block_given?
243
244     raise "Need to define a format" if @fmt.nil?
245     raise "Need to define inventory fields" if @flds.nil?
246
247     util = rpcclient("rpcutil")
248     util.progress = false
249
250     util.inventory do |t, resp|
251       @node = {:identity => resp[:sender],
252         :facts    => resp[:data][:facts],
253         :classes  => resp[:data][:classes],
254         :agents   => resp[:data][:agents]}
255
256       puts @fmt % @flds.call
257     end
258   end
259
260   # Use the ruby formatr gem to build reports using Perls formats
261   #
262   # It is kind of ugly but brings a lot of flexibility in report
263   # writing without building an entire reporting language.
264   #
265   # You need to have formatr installed to enable reports like:
266   #
267   #    formatted_inventory do
268   #        page_length 20
269   #
270   #        page_heading <<TOP
271   #
272   #                Node Report @<<<<<<<<<<<<<<<<<<<<<<<<<
273   #                            time
274   #
275   #    Hostname:         Customer:     Distribution:
276   #    -------------------------------------------------------------------------
277   #    TOP
278   #
279   #        page_body <<BODY
280   #
281   #    @<<<<<<<<<<<<<<<< @<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
282   #    identity,    facts["customer"], facts["lsbdistdescription"]
283   #                                    @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
284   #                                    facts["processor0"]
285   #    BODY
286   #    end
287   def formatted_inventory(&blk)
288     require 'formatr'
289
290     raise "Need to give a block to formatted_inventory" unless block_given?
291
292     blk.call if block_given?
293
294     raise "Need to define page body format" if @page_body.nil?
295
296     body_fmt = FormatR::Format.new(@page_heading, @page_body)
297     body_fmt.setPageLength(@page_length)
298     time = Time.now
299
300     util = rpcclient("rpcutil")
301     util.progress = false
302
303     util.inventory do |t, resp|
304       @node = {:identity => resp[:sender],
305         :facts    => resp[:data][:facts],
306         :classes  => resp[:data][:classes],
307         :agents   => resp[:data][:agents]}
308
309       body_fmt.printFormat(binding)
310     end
311   rescue Exception => e
312     STDERR.puts "Could not create report: #{e.class}: #{e}"
313     exit 1
314   end
315
316   @fmt = nil
317   @flds = nil
318   @page_heading = nil
319   @page_body = nil
320   @page_length = 40
321
322   def main
323     if configuration[:script]
324       if File.exist?(configuration[:script])
325         eval(File.read(configuration[:script]))
326       else
327         raise "Could not find script to run: #{configuration[:script]}"
328       end
329
330     elsif configuration[:collectivemap]
331       collectives_map(configuration[:collectivemap])
332
333     elsif configuration[:collectives]
334       collectives_report
335
336     else
337       node_inventory
338     end
339   end
340 end