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