792736f957891cdcaad14fc34b5746bd597e6b02
[packages/precise/mcollective.git] / lib / mcollective / rpc / helpers.rb
1 module MCollective
2   module RPC
3     # Various utilities for the RPC system
4     class Helpers
5       # Parse JSON output as produced by printrpc and extract
6       # the "sender" of each rpc response
7       #
8       # The simplist valid JSON based data would be:
9       #
10       # [
11       #  {"sender" => "example.com"},
12       #  {"sender" => "another.com"}
13       # ]
14       def self.extract_hosts_from_json(json)
15         hosts = JSON.parse(json)
16
17         raise "JSON hosts list is not an array" unless hosts.is_a?(Array)
18
19         hosts.map do |host|
20           raise "JSON host list is not an array of Hashes" unless host.is_a?(Hash)
21           raise "JSON host list does not have senders in it" unless host.include?("sender")
22
23           host["sender"]
24         end.uniq
25       end
26
27       # Given an array of something, make sure each is a string
28       # chomp off any new lines and return just the array of hosts
29       def self.extract_hosts_from_array(hosts)
30         [hosts].flatten.map do |host|
31           raise "#{host} should be a string" unless host.is_a?(String)
32           host.chomp
33         end
34       end
35
36       # Returns a blob of text representing the results in a standard way
37       #
38       # It tries hard to do sane things so you often
39       # should not need to write your own display functions
40       #
41       # If the agent you are getting results for has a DDL
42       # it will use the hints in there to do the right thing specifically
43       # it will look at the values of display in the DDL to choose
44       # when to show results
45       #
46       # If you do not have a DDL you can pass these flags:
47       #
48       #    printrpc exim.mailq, :flatten => true
49       #    printrpc exim.mailq, :verbose => true
50       #
51       # If you've asked it to flatten the result it will not print sender
52       # hostnames, it will just print the result as if it's one huge result,
53       # handy for things like showing a combined mailq.
54       def self.rpcresults(result, flags = {})
55         flags = {:verbose => false, :flatten => false, :format => :console, :force_display_mode => false}.merge(flags)
56
57         result_text = ""
58         ddl = nil
59
60         # if running in verbose mode, just use the old style print
61         # no need for all the DDL helpers obfuscating the result
62         if flags[:format] == :json
63           if STDOUT.tty?
64             result_text = JSON.pretty_generate(result)
65           else
66             result_text = result.to_json
67           end
68         else
69           if flags[:verbose]
70             result_text = old_rpcresults(result, flags)
71           else
72             [result].flatten.each do |r|
73               begin
74                 ddl ||= DDL.new(r.agent).action_interface(r.action.to_s)
75
76                 sender = r[:sender]
77                 status = r[:statuscode]
78                 message = r[:statusmsg]
79                 result = r[:data]
80
81                 if flags[:force_display_mode]
82                   display = flags[:force_display_mode]
83                 else
84                   display = ddl[:display]
85                 end
86
87                 # appand the results only according to what the DDL says
88                 case display
89                   when :ok
90                     if status == 0
91                       result_text << text_for_result(sender, status, message, result, ddl)
92                     end
93
94                   when :failed
95                     if status > 0
96                       result_text << text_for_result(sender, status, message, result, ddl)
97                     end
98
99                   when :always
100                     result_text << text_for_result(sender, status, message, result, ddl)
101
102                   when :flatten
103                     result_text << text_for_flattened_result(status, result)
104
105                 end
106               rescue Exception => e
107                 # no DDL so just do the old style print unchanged for
108                 # backward compat
109                 result_text = old_rpcresults(result, flags)
110               end
111             end
112           end
113         end
114
115         result_text
116       end
117
118       # Return text representing a result
119       def self.text_for_result(sender, status, msg, result, ddl)
120         statusses = ["",
121                      Util.colorize(:red, "Request Aborted"),
122                      Util.colorize(:yellow, "Unknown Action"),
123                      Util.colorize(:yellow, "Missing Request Data"),
124                      Util.colorize(:yellow, "Invalid Request Data"),
125                      Util.colorize(:red, "Unknown Request Status")]
126
127         result_text = "%-40s %s\n" % [sender, statusses[status]]
128         result_text << "   %s\n" % [Util.colorize(:yellow, msg)] unless msg == "OK"
129
130         # only print good data, ignore data that results from failure
131         if status == 0
132           if result.is_a?(Hash)
133             # figure out the lengths of the display as strings, we'll use
134             # it later to correctly justify the output
135             lengths = result.keys.map do |k|
136               begin
137                 ddl[:output][k][:display_as].size
138               rescue
139                 k.to_s.size
140               end
141             end
142
143             result.keys.sort_by{|k| k}.each do |k|
144               # get all the output fields nicely lined up with a
145               # 3 space front padding
146               begin
147                 display_as = ddl[:output][k][:display_as]
148               rescue
149                 display_as = k.to_s
150               end
151
152               display_length = display_as.size
153               padding = lengths.max - display_length + 3
154               result_text << " " * padding
155
156               result_text << "#{display_as}:"
157
158               if [String, Numeric].include?(result[k].class)
159                 lines = result[k].to_s.split("\n")
160
161                 if lines.empty?
162                   result_text << "\n"
163                 else
164                   lines.each_with_index do |line, i|
165                     i == 0 ? padtxt = " " : padtxt = " " * (padding + display_length + 2)
166
167                     result_text << "#{padtxt}#{line}\n"
168                   end
169                 end
170               else
171                 padding = " " * (lengths.max + 5)
172                 result_text << " " << result[k].pretty_inspect.split("\n").join("\n" << padding) << "\n"
173               end
174             end
175           elsif status == 1
176             # for status 1 we dont want to show half baked
177             # data by default since the DDL will supply all the defaults
178             # it just doesnt look right
179           else
180             result_text << "\n\t" + result.pretty_inspect.split("\n").join("\n\t")
181           end
182         end
183
184         result_text << "\n"
185         result_text
186       end
187
188       # Returns text representing a flattened result of only good data
189       def self.text_for_flattened_result(status, result)
190         result_text = ""
191
192         if status <= 1
193           unless result.is_a?(String)
194             result_text << result.pretty_inspect
195           else
196             result_text << result
197           end
198         end
199       end
200
201       # Backward compatible display block for results without a DDL
202       def self.old_rpcresults(result, flags = {})
203         result_text = ""
204
205         if flags[:flatten]
206           result.each do |r|
207             if r[:statuscode] <= 1
208               data = r[:data]
209
210               unless data.is_a?(String)
211                 result_text << data.pretty_inspect
212               else
213                 result_text << data
214               end
215             else
216               result_text << r.pretty_inspect
217             end
218           end
219
220           result_text << ""
221         else
222           [result].flatten.each do |r|
223
224             if flags[:verbose]
225               result_text << "%-40s: %s\n" % [r[:sender], r[:statusmsg]]
226
227               if r[:statuscode] <= 1
228                 r[:data].pretty_inspect.split("\n").each {|m| result_text += "    #{m}"}
229                 result_text << "\n\n"
230               elsif r[:statuscode] == 2
231                 # dont print anything, no useful data to display
232                 # past what was already shown
233               elsif r[:statuscode] == 3
234                 # dont print anything, no useful data to display
235                 # past what was already shown
236               elsif r[:statuscode] == 4
237                 # dont print anything, no useful data to display
238                 # past what was already shown
239               else
240                 result_text << "    #{r[:statusmsg]}"
241               end
242             else
243               unless r[:statuscode] == 0
244                 result_text << "%-40s %s\n" % [r[:sender], Util.colorize(:red, r[:statusmsg])]
245               end
246             end
247           end
248         end
249
250         result_text << ""
251       end
252
253       # Add SimpleRPC common options
254       def self.add_simplerpc_options(parser, options)
255         parser.separator ""
256         parser.separator "RPC Options"
257
258         # add SimpleRPC specific options to all clients that use our library
259         parser.on('--np', '--no-progress', 'Do not show the progress bar') do |v|
260           options[:progress_bar] = false
261         end
262
263         parser.on('--one', '-1', 'Send request to only one discovered nodes') do |v|
264           options[:mcollective_limit_targets] = 1
265         end
266
267         parser.on('--batch SIZE', Integer, 'Do requests in batches') do |v|
268           options[:batch_size] = v
269         end
270
271         parser.on('--batch-sleep SECONDS', Float, 'Sleep time between batches') do |v|
272           options[:batch_sleep_time] = v
273         end
274
275         parser.on('--limit-seed NUMBER', Integer, 'Seed value for deterministic random batching') do |v|
276           options[:limit_seed] = v
277         end
278
279         parser.on('--limit-nodes COUNT', '--ln', '--limit', 'Send request to only a subset of nodes, can be a percentage') do |v|
280           raise "Invalid limit specified: #{v} valid limits are /^\d+%*$/" unless v =~ /^\d+%*$/
281
282           if v =~ /^\d+$/
283             options[:mcollective_limit_targets] = v.to_i
284           else
285             options[:mcollective_limit_targets] = v
286           end
287         end
288
289         parser.on('--json', '-j', 'Produce JSON output') do |v|
290           options[:progress_bar] = false
291           options[:output_format] = :json
292         end
293
294         parser.on('--display MODE', 'Influence how results are displayed. One of ok, all or failed') do |v|
295           if v == "all"
296             options[:force_display_mode] = :always
297           else
298             options[:force_display_mode] = v.intern
299           end
300
301           raise "--display has to be one of 'ok', 'all' or 'failed'" unless [:ok, :failed, :always].include?(options[:force_display_mode])
302         end
303       end
304     end
305   end
306 end