1# encoding: utf-8
2class MCollective::Application::Puppet<MCollective::Application
3  description "Schedule runs, enable, disable and interrogate the Puppet Agent"
4
5  usage <<-END_OF_USAGE
6mco puppet [OPTIONS] [FILTERS] <ACTION> [CONCURRENCY|MESSAGE]
7Usage: mco puppet <count|enable|status|summary>
8Usage: mco puppet disable [message]
9Usage: mco puppet runonce [PUPPET OPTIONS]
10Usage: mco puppet resource type name property1=value property2=value
11Usage: mco puppet runall [--rerun SECONDS] [PUPPET OPTIONS]
12
13The ACTION can be one of the following:
14
15    count    - return a total count of running, enabled, and disabled nodes
16    enable   - enable the Puppet Agent if it was previously disabled
17    disable  - disable the Puppet Agent preventing catalog from being applied
18    resource - manage individual resources using the Puppet Type (RAL) system
19    runall   - invoke a puppet run on matching nodes, making sure to only run
20               CONCURRENCY nodes at a time. NOTE that any compound filters (-S)
21               used with runall will be wrapped in parentheses and and'ed
22               with "puppet().enabled=true".
23    runonce  - invoke a Puppet run on matching nodes
24    status   - shows a short summary about each Puppet Agent status
25    summary  - shows resource and run time summaries
26END_OF_USAGE
27
28  option :force,
29         :arguments   => ["--force"],
30         :description => "Bypass splay options when running",
31         :type        => :bool
32
33  option :server,
34         :arguments   => ["--server SERVER"],
35         :description => "Connect to a specific server or port",
36         :type        => String
37
38  option :tag,
39         :arguments   => ["--tags TAG", "--tag"],
40         :description => "Restrict the run to specific tags",
41         :type        => :array
42
43  option :noop,
44         :arguments   => ["--noop"],
45         :description => "Do a noop run",
46         :type        => :bool
47
48  option :no_noop,
49         :arguments   => ["--no-noop"],
50         :description => "Do a run with noop disabled",
51         :type        => :bool
52
53  option :environment,
54         :arguments   => ["--environment ENVIRONMENT"],
55         :description => "Place the node in a specific environment for this run",
56         :type        => String
57
58  option :splay,
59         :arguments   => ["--splay"],
60         :description => "Splay the run by up to splaylimit seconds",
61         :type        => :bool
62
63  option :no_splay,
64         :arguments   => ["--no-splay"],
65         :description => "Do a run with splay disabled",
66         :type        => :bool
67
68  option :splaylimit,
69         :arguments   => ["--splaylimit SECONDS"],
70         :description => "Maximum splay time for this run if splay is set",
71         :type        => Integer
72
73  option :use_cached_catalog,
74         :arguments   => ["--use_cached_catalog"],
75         :description => "Use cached catalog for this run",
76         :type        => :bool
77
78  option :no_use_cached_catalog,
79         :arguments   => ["--no-use_cached_catalog"],
80         :description => "Do not use cached catalog for this run",
81         :type        => :bool
82
83  option :ignoreschedules,
84         :arguments   => ["--ignoreschedules"],
85         :description => "Disable schedule processing",
86         :type        => :bool
87
88  option :rerun,
89         :arguments   => ["--rerun SECONDS"],
90         :description => "When performing runall do so repeatedly with a minimum run time of SECONDS",
91         :type        => Integer
92
93  def post_option_parser(configuration)
94    if ARGV.length >= 1
95      configuration[:command] = ARGV.shift
96
97      if arg = ARGV.shift
98        if configuration[:command] == "runall"
99          configuration[:concurrency] = Integer(arg)
100
101        elsif configuration[:command] == "disable"
102          configuration[:message] = arg
103
104        elsif configuration[:command] == "resource"
105          configuration[:type] = arg
106          configuration[:name] = ARGV.shift
107          configuration[:properties] = ARGV[0..-1]
108        end
109      end
110
111      unless ["resource", "count", "runonce", "enable", "disable", "runall", "status", "summary"].include?(configuration[:command])
112        raise_message(1)
113      end
114    else
115      raise_message(2)
116    end
117  end
118
119  def validate_configuration(configuration)
120    if configuration[:force]
121      raise_message(3) if configuration.include?(:splay)
122      raise_message(4) if configuration.include?(:splaylimit)
123    end
124
125    if configuration[:command] == "runall"
126      if configuration[:concurrency]
127        raise_message(7) unless configuration[:concurrency] > 0
128      else
129        raise_message(5)
130      end
131    elsif configuration[:command] == "resource"
132      raise_message(9) unless configuration[:type]
133      raise_message(10) unless configuration[:name]
134    end
135
136    configuration[:noop] = false if configuration.include?(:no_noop)
137    configuration[:splay] = false if configuration.include?(:no_splay)
138    configuration[:use_cached_catalog] = false if configuration.include?(:no_use_cached_catalog)
139  end
140
141  def raise_message(message, *args)
142    messages = {1 => "Action must be count, enable, disable, resource, runall, runonce, status or summary",
143                2 => "Please specify a command.",
144                3 => "Cannot set splay when forcing runs",
145                4 => "Cannot set splaylimit when forcing runs",
146                5 => "The runall command needs a concurrency limit",
147                6 => "Do not know how to handle the '%s' command",
148                7 => "The concurrency for the runall command has to be greater than 0",
149                9 => "The resource command needs a type to operate on",
150                10 => "The resource command needs a name to operate on"}
151
152    raise messages[message] % args
153  end
154
155  def spark(histo, ticks=%w[▁ ▂ ▃ ▄ ▅ ▆ ▇])
156    range = histo.max - histo.min
157
158    if range == 0
159      return ticks.first * histo.size
160    end
161
162    scale = ticks.size - 1
163    distance = histo.max.to_f / scale
164
165    histo.map do |val|
166      tick = (val / distance).round
167      tick = 0 if tick < 0
168      tick = 1 if val > 0 && tick == 0 # show at least something for very small values
169
170      ticks[tick]
171    end.join
172  end
173
174  def shorten_number(number)
175    number = Float(number)
176    return "%0.1fm" % (number / 1000000) if number >= 1000000
177    return "%0.1fk" % (number / 1000) if number >= 1000
178    return "%0.1f" % number
179  rescue
180    "NaN"
181  end
182
183  def sparkline_for_field(results, field, bucket_count=20)
184    buckets = Array.new(bucket_count) { 0 }
185    values = []
186
187    results.each do |result|
188      if result[:statuscode] == 0
189        values << result[:data][field]
190      end
191    end
192
193    values.compact!
194
195    return '' if values.empty?
196
197    min = values.min
198    max = values.max
199    total = values.inject(:+)
200    len = values.length
201    avg = total.to_f / len
202
203    bucket_size = ((max - min) / Float(bucket_count)) + 1
204
205    unless max == min
206      values.each do |value|
207        bucket = Integer(((value - min) / bucket_size))
208        buckets[bucket] += 1
209      end
210    end
211
212    "%s  min: %-6s avg: %-6s max: %-6s" % [spark(buckets), shorten_number(min), shorten_number(avg), shorten_number(max)]
213  end
214
215  def client
216    @client ||= rpcclient("puppet")
217  end
218
219  def extract_values_from_aggregates(aggregate_summary)
220    counts = {}
221
222    client.stats.aggregate_summary.each do |aggr|
223      counts[aggr.result[:output]] = aggr.result[:value]
224    end
225
226    counts
227  end
228
229  def calculate_longest_hostname(results)
230    results.map{|s| s[:sender]}.map{|s| s.length}.max
231  end
232
233  def display_results_single_field(results, field)
234    return false if results.empty?
235
236    sender_width = calculate_longest_hostname(results) + 3
237    pattern = "%%%ds: %%s" % sender_width
238
239    Array(results).each do |result|
240      if result[:statuscode] == 0
241        puts pattern % [result[:sender], result[:data][field]]
242      else
243        puts pattern % [result[:sender], MCollective::Util.colorize(:red, result[:statusmsg])]
244      end
245    end
246  end
247
248  def runonce_arguments
249    arguments = {}
250
251    [:use_cached_catalog, :force, :server, :noop, :environment, :splay, :splaylimit, :ignoreschedules].each do |arg|
252      arguments[arg] = configuration[arg] if configuration.include?(arg)
253    end
254
255    arguments[:tags] = Array(configuration[:tag]).join(",") if configuration.include?(:tag)
256
257    arguments
258  end
259
260  def resource_command
261    arguments = {:name => configuration[:name], :type => configuration[:type]}
262
263    configuration[:properties].each do |v|
264      if v =~ /^(.+?)=(.+)$/
265        arguments[$1] = $2
266      else
267        raise("Could not parse argument '%s'" % v)
268      end
269    end
270
271    printrpc client.resource(arguments)
272
273    printrpcstats :summarize => true
274
275    halt client.stats
276  end
277
278  def runall_command(runner=nil)
279    unless runner
280      require 'mcollective/util/puppetrunner.rb'
281
282      runner = MCollective::Util::Puppetrunner.new(client, configuration)
283    end
284
285    runner.logger do |msg|
286      puts "%s: %s" % [Time.now.strftime("%F %T"), msg]
287      ::MCollective::Log.debug(msg)
288    end
289
290    runner.runall(!!configuration[:rerun], configuration[:rerun])
291  end
292
293  def summary_command
294    client.progress = false
295    results = client.last_run_summary
296
297    puts "Summary statistics for %d nodes:" % results.size
298    puts
299    puts "                  Total resources: %s" % sparkline_for_field(results, :total_resources)
300    puts "            Out Of Sync resources: %s" % sparkline_for_field(results, :out_of_sync_resources)
301    puts "                 Failed resources: %s" % sparkline_for_field(results, :failed_resources)
302    puts "                Changed resources: %s" % sparkline_for_field(results, :changed_resources)
303    puts "              Corrected resources: %s" % sparkline_for_field(results, :corrected_resources)
304    puts "  Config Retrieval time (seconds): %s" % sparkline_for_field(results, :config_retrieval_time)
305    puts "         Total run-time (seconds): %s" % sparkline_for_field(results, :total_time)
306    puts "    Time since last run (seconds): %s" % sparkline_for_field(results, :since_lastrun)
307    puts
308
309    halt client.stats
310  end
311
312  def status_command
313    display_results_single_field(client.status, :message)
314
315    puts
316
317    printrpcstats :summarize => true
318
319    halt client.stats
320  end
321
322  def enable_command
323    printrpc client.enable
324    printrpcstats :summarize => true
325    halt client.stats
326  end
327
328  def disable_command
329    args = {}
330    args[:message] = configuration[:message] if configuration[:message]
331
332    printrpc client.disable(args)
333
334    printrpcstats :summarize => true
335    halt client.stats
336  end
337
338  def runonce_command
339    printrpc client.runonce(runonce_arguments)
340
341    printrpcstats
342
343    halt client.stats
344  end
345
346  def count_command
347    client.progress = false
348    client.status
349
350    counts = extract_values_from_aggregates(client.stats.aggregate_summary)
351
352    puts "Total Puppet nodes: %d" % client.stats.okcount
353    puts
354    puts "          Nodes currently enabled: %d" % counts[:enabled].fetch("enabled", 0)
355    puts "         Nodes currently disabled: %d" % counts[:enabled].fetch("disabled", 0)
356    puts
357    puts "Nodes currently doing puppet runs: %d" % counts[:applying].fetch(true, 0)
358    puts "          Nodes currently stopped: %d" % counts[:applying].fetch(false, 0)
359    puts
360    puts "       Nodes with daemons started: %d" % counts[:daemon_present].fetch("running", 0)
361    puts "    Nodes without daemons started: %d" % counts[:daemon_present].fetch("stopped", 0)
362    puts "       Daemons started but idling: %d" % counts[:idling].fetch(true, 0)
363    puts
364
365    if client.stats.failcount > 0
366      puts MCollective::Util.colorize(:red, "Failed to retrieve status of %d %s" % [client.stats.failcount, client.stats.failcount == 1 ? "node" : "nodes"])
367    end
368
369    halt client.stats
370  end
371
372  def main
373    impl_method = "%s_command" % configuration[:command]
374
375    if respond_to?(impl_method)
376      send(impl_method)
377    else
378      raise_message(6, configuration[:command])
379    end
380  end
381end
382