right_chimp 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1181 @@
1
+ #
2
+ # The Chimp class encapsulates the command-line program logic
3
+ #
4
+
5
+ module Chimp
6
+ class Chimp
7
+ attr_accessor :concurrency, :delay, :retry_count, :progress, :prompt,
8
+ :quiet, :use_chimpd, :chimpd_host, :chimpd_port, :tags, :array_names,
9
+ :deployment_names, :script, :servers, :ssh, :report, :interactive, :action,
10
+ :limit_start, :limit_end, :dry_run, :group, :job_id, :verify
11
+
12
+ #
13
+ # These class variables control verbosity
14
+ #
15
+ @@verbose = false
16
+ @@quiet = false
17
+
18
+ #
19
+ # Set up reasonable defaults
20
+ #
21
+ def initialize
22
+ #
23
+ # General configuration options
24
+ #
25
+ @progress = false
26
+ @prompt = true
27
+ @verify = true
28
+ @dry_run = false
29
+ @interactive = true
30
+
31
+ #
32
+ # Job control options
33
+ #
34
+ @concurrency = 1
35
+ @delay = 0
36
+ @retry_count = 0
37
+ @timeout = 900
38
+
39
+ @limit_start = 0
40
+ @limit_end = 0
41
+
42
+ #
43
+ # Action configuration
44
+ #
45
+ @action = :action_none
46
+ @group = :default
47
+ @group_type = :parallel
48
+ @group_concurrency = 1
49
+
50
+ #
51
+ # Options for selecting objects to work on
52
+ #
53
+ @current = true
54
+ @match_all = true
55
+ @servers = []
56
+ @arrays = []
57
+ @tags = []
58
+ @array_names = []
59
+ @deployment_names = []
60
+ @template = nil
61
+ @script = nil
62
+ @ssh = nil
63
+ @ssh_user = "rightscale"
64
+ @report = nil
65
+ @inputs = {}
66
+ @set_tags = []
67
+ @ignore_errors = false
68
+
69
+ @break_array_into_instances = false
70
+ @dont_check_templates_for_script = false
71
+
72
+ #
73
+ # chimpd configuration
74
+ #
75
+ @use_chimpd = false
76
+ @chimpd_host = 'localhost'
77
+ @chimpd_port = 9055
78
+ @chimpd_wait_until_done = false
79
+
80
+ RestClient.log = nil
81
+ end
82
+
83
+ #
84
+ # Entry point for the chimp command line application
85
+ #
86
+ def run
87
+ queue = ChimpQueue.instance
88
+
89
+ parse_command_line if @interactive
90
+ check_option_validity if @interactive
91
+ disable_logging unless @@verbose
92
+
93
+ puts "chimp #{VERSION} executing..." if (@interactive and not @use_chimpd) and not @@quiet
94
+
95
+ #
96
+ # Wait for chimpd to complete tasks
97
+ #
98
+ if @chimpd_wait_until_done
99
+ chimpd_wait_until_done
100
+ exit
101
+ end
102
+
103
+ #
104
+ # Send the command to chimpd for execution
105
+ #
106
+ if @use_chimpd
107
+ ChimpDaemonClient.submit(@chimpd_host, @chimpd_port, self)
108
+ exit
109
+ end
110
+
111
+ #
112
+ # If we're processing the command ourselves, then go
113
+ # ahead and start making API calls to select the objects
114
+ # to operate upon
115
+ #
116
+ get_array_info
117
+ get_server_info
118
+ get_template_info
119
+ get_executable_info
120
+
121
+ #
122
+ # Optionally display the list of objects to operate on
123
+ # and prompt the user
124
+ #
125
+ if @prompt and @interactive
126
+ list_of_objects = make_human_readable_list_of_objects
127
+ confirm = (list_of_objects.size > 0 and @action != :action_none) or @action == :action_none
128
+
129
+ verify("Your command will be executed on the following:", list_of_objects, confirm)
130
+
131
+ if @servers.length >= 2 and @server_template and @executable and not @dont_check_templates_for_script
132
+ warn_if_rightscript_not_in_all_servers @servers, @server_template, @executable
133
+ end
134
+ end
135
+
136
+ #
137
+ # Load the queue with work
138
+ #
139
+ jobs = generate_jobs(@servers, @arrays, @server_template, @executable)
140
+ add_to_queue(jobs)
141
+
142
+ #
143
+ # Exit early if there is nothing to do
144
+ #
145
+ if @action == :action_none or queue.group[@group].size == 0
146
+ puts "No actions to perform." unless @@quiet
147
+ else
148
+ do_work
149
+ end
150
+ end
151
+
152
+ #
153
+ # Process a non-interactive chimp object command
154
+ # Used by chimpd
155
+ #
156
+ def process
157
+ get_array_info
158
+ get_server_info
159
+ get_template_info
160
+ get_executable_info
161
+ jobs = generate_jobs(@servers, @arrays, @server_template, @executable)
162
+ return(jobs)
163
+ end
164
+
165
+ #
166
+ # Get the ServerTemplate info from the API
167
+ #
168
+ def get_template_info
169
+ if not (@servers.empty? and @array_names.empty?)
170
+ @server_template = detect_server_template(@template, @script, @servers, @array_names)
171
+ end
172
+ end
173
+
174
+ #
175
+ # Get the Executable (RightScript) info from the API
176
+ #
177
+ def get_executable_info
178
+ if not (@servers.empty? and @array_names.empty?)
179
+ @executable = detect_right_script(@server_template, @script)
180
+ puts "Using SSH command: \"#{@ssh}\"" if @action == :action_ssh
181
+ end
182
+ end
183
+
184
+ #
185
+ # Parse command line options
186
+ #
187
+ def parse_command_line
188
+ begin
189
+ opts = GetoptLong.new(
190
+ [ '--tag', '-t', GetoptLong::REQUIRED_ARGUMENT ],
191
+ [ '--tag-use-and', '-a', GetoptLong::NO_ARGUMENT ],
192
+ [ '--tag-use-or', '-o', GetoptLong::NO_ARGUMENT ],
193
+ [ '--array', '-r', GetoptLong::REQUIRED_ARGUMENT ],
194
+ [ '--deployment', '-e', GetoptLong::REQUIRED_ARGUMENT ],
195
+ [ '--script', '-s', GetoptLong::OPTIONAL_ARGUMENT ],
196
+ [ '--ssh', '-x', GetoptLong::OPTIONAL_ARGUMENT ],
197
+ [ '--input', '-i', GetoptLong::REQUIRED_ARGUMENT ],
198
+ [ '--set-template', '-m', GetoptLong::REQUIRED_ARGUMENT ],
199
+ [ '--set-tag', '-w', GetoptLong::REQUIRED_ARGUMENT ],
200
+ [ '--report', '-b', GetoptLong::REQUIRED_ARGUMENT ],
201
+ [ '--progress', '-p', GetoptLong::NO_ARGUMENT ],
202
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
203
+ [ '--quiet', '-q', GetoptLong::NO_ARGUMENT ],
204
+ [ '--noprompt', '-z', GetoptLong::NO_ARGUMENT ],
205
+ [ '--concurrency', '-c', GetoptLong::REQUIRED_ARGUMENT ],
206
+ [ '--delay', '-d', GetoptLong::REQUIRED_ARGUMENT ],
207
+ [ '--retry', '-y', GetoptLong::REQUIRED_ARGUMENT ],
208
+ [ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ],
209
+ [ '--limit', '-l', GetoptLong::REQUIRED_ARGUMENT ],
210
+ [ '--version', '-1', GetoptLong::NO_ARGUMENT ],
211
+ [ '--chimpd', '-f', GetoptLong::NO_ARGUMENT ],
212
+ [ '--chimpd-wait-until-done', '-j', GetoptLong::NO_ARGUMENT ],
213
+ [ '--dont-check-templates', '-0', GetoptLong::NO_ARGUMENT ],
214
+ [ '--ignore-errors', '-9', GetoptLong::NO_ARGUMENT ],
215
+ [ '--ssh-user', '-u', GetoptLong::REQUIRED_ARGUMENT ],
216
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
217
+ [ '--group', '-g', GetoptLong::REQUIRED_ARGUMENT ],
218
+ [ '--group-type', '-2', GetoptLong::REQUIRED_ARGUMENT ],
219
+ [ '--group-concurrency', '-3', GetoptLong::REQUIRED_ARGUMENT ],
220
+ [ '--timing-log', '-4', GetoptLong::REQUIRED_ARGUMENT ],
221
+ [ '--timeout', '-5', GetoptLong::REQUIRED_ARGUMENT ],
222
+ [ '--noverify', '-6', GetoptLong::NO_ARGUMENT ]
223
+ )
224
+
225
+ opts.each do |opt, arg|
226
+ case opt
227
+ when '--help', '-h'
228
+ help
229
+ exit 0
230
+ when '--tag', '-t'
231
+ @tags << arg
232
+ when '--tag-use-and', '-a'
233
+ @match_all = true
234
+ when '--tag-use-or', '-o'
235
+ @match_all = false
236
+ when '--array', '-a'
237
+ @array_names << arg
238
+ when '--deployment', '-e'
239
+ @deployment_names << arg
240
+ when '--template', '-m'
241
+ @template = arg
242
+ when '--script', '-s'
243
+ set_action(:action_rightscript)
244
+ if arg == ""
245
+ # Empty but not nil means show list of operational scripts to choose from
246
+ @script = ""
247
+ else
248
+ @script = arg
249
+ end
250
+ when '--ssh', '-x'
251
+ set_action(:action_ssh)
252
+ @break_array_into_instances = true
253
+ if arg == ""
254
+ print "Enter SSH command line to execute: "
255
+ @ssh = gets.chomp
256
+ else
257
+ @ssh = arg
258
+ end
259
+ when '--ssh-user', '-u'
260
+ @ssh_user = arg
261
+ when '--input', '-i'
262
+ arg =~ /(.+)=(.+)/
263
+ @inputs[$1]=$2
264
+ when '--set-template', '-m'
265
+ set_action(:action_set)
266
+ @template = arg
267
+ when '--set-tag', '-w'
268
+ set_action(:action_set)
269
+ @set_tags << arg
270
+ when '--report', '-b'
271
+ set_action(:action_report)
272
+ @report = arg
273
+ @@verbose = false
274
+ @@quiet = true
275
+ @break_array_into_instances = true
276
+ @concurrency = 5 if @concurrency == 1
277
+ when '--progress', '-p'
278
+ @progress = @progress ? false : true
279
+ when '--noprompt', '-z'
280
+ @prompt = false
281
+ when '--concurrency', '-c'
282
+ @concurrency = arg.to_i
283
+ when '--delay', '-d'
284
+ @delay = arg.to_i
285
+ when '--retry', '-y'
286
+ @retry_count = arg.to_i
287
+ when '--limit', '-l'
288
+ @limit_start, @limit_end = arg.split(',')
289
+ when '--verbose', '-v'
290
+ @@verbose = true
291
+ when '--quiet', '-q'
292
+ @@quiet = true
293
+ when '--dont-check-templates', '-0'
294
+ @dont_check_templates_for_script = true
295
+ when '--version'
296
+ puts VERSION
297
+ exit 0
298
+ when '--chimpd'
299
+ @use_chimpd = true
300
+ when '--chimpd-wait-until-done'
301
+ @use_chimpd = true
302
+ @chimpd_wait_until_done = true
303
+ when '--dry-run', '-n'
304
+ @dry_run = true
305
+ when '--ignore-errors', '-9'
306
+ @ignore_errors = true
307
+ when '--group', '-g'
308
+ @group = arg.to_sym
309
+ when '--group-type'
310
+ @group_type = arg.to_sym
311
+ when '--group-concurrency'
312
+ @group_concurrency = arg.to_i
313
+ when '--timing-log'
314
+ @timing_log = arg
315
+ when '--timeout'
316
+ @timeout = arg
317
+ when '--noverify'
318
+ @verify = false
319
+ end
320
+ end
321
+ rescue GetoptLong::InvalidOption => ex
322
+ help
323
+ exit 1
324
+ end
325
+
326
+ #
327
+ # Before we're totally done parsing command line options,
328
+ # let's make sure that a few things make sense
329
+ #
330
+ if @group_concurrency > @concurrency
331
+ @concurrency = @group_concurrency
332
+ end
333
+
334
+ end
335
+
336
+ #
337
+ # Check for any invalid combinations of command line options
338
+ #
339
+ def check_option_validity
340
+ if @tags.empty? and @array_names.empty? and @deployment_names.empty? and not @chimpd_wait_until_done
341
+ puts "ERROR: Please select the objects to operate upon."
342
+ help
343
+ exit 1
344
+ end
345
+
346
+ if not @array_names.empty? and ( not @tags.empty? or not @deployment_names.empty? )
347
+ puts "ERROR: You cannot mix ServerArray queries with other types of queries."
348
+ help
349
+ exit 1
350
+ end
351
+ end
352
+
353
+ #
354
+ # Go through each of the various ways to specify servers via
355
+ # the command line (tags, deployments, etc.) and get all the info
356
+ # needed from the RightScale API.
357
+ #
358
+ def get_server_info
359
+ @servers += get_servers_by_tag(@tags)
360
+ @servers += get_servers_by_deployment(@deployment_names)
361
+ @servers = filter_out_non_operational_servers(@servers)
362
+ end
363
+
364
+ #
365
+ # Load up @array with server arrays to operate on
366
+ #
367
+ def get_array_info
368
+ return if @array_names.empty?
369
+
370
+ #
371
+ # Some operations (e.g. ExecSSH) require individual server information.
372
+ # Check for @break_array_into_instances and break up the ServerArray
373
+ # into Servers as necessary.
374
+ #
375
+ if @break_array_into_instances
376
+ Log.debug "Breaking array into instances..."
377
+ @servers += get_servers_by_array(@array_names)
378
+ @array_names = []
379
+ end
380
+
381
+ @array_names.each do |array_name|
382
+ Log.debug "Querying API for ServerArray \'#{array_name}\'..."
383
+ a = Ec2ServerArray.find_by(:nickname) { |n| n =~ /^#{array_name}/i }.first
384
+ if not a.nil?
385
+ @arrays << a
386
+ else
387
+ if @ignore_errors
388
+ Log.warn "cannot find ServerArray #{array_name}"
389
+ else
390
+ raise "cannot find ServerArray #{array_name}"
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ #
397
+ # Get servers to operate on via a tag query
398
+ #
399
+ # Returns: array of RestConnection::Server objects
400
+ #
401
+ def get_servers_by_tag(tags)
402
+ return([]) unless tags.size > 0
403
+ servers = ::Tag.search("ec2_instance", tags, :match_all => @match_all)
404
+
405
+ if tags.size > 0 and servers.nil? or servers.empty?
406
+ if @ignore_errors
407
+ Log.warn "Tag query returned no results: #{tags.join(" ")}"
408
+ else
409
+ raise "Tag query returned no results: #{tags.join(" ")}"
410
+ end
411
+ end
412
+
413
+ return(servers)
414
+ end
415
+
416
+ #
417
+ # Parse deployment names and get Server objects
418
+ #
419
+ # Returns: array of RestConnection::Server objects
420
+ #
421
+ def get_servers_by_deployment(names)
422
+ servers = []
423
+
424
+ if names.size > 0
425
+ names.each do |deployment|
426
+ d = ::Deployment.find_by_nickname(deployment).first
427
+
428
+ if d == nil
429
+ if @ignore_errors
430
+ Log.warn "cannot find deployment #{deployment}"
431
+ else
432
+ raise "cannot find deployment #{deployment}"
433
+ end
434
+ else
435
+ d.servers_no_reload.each do |s|
436
+ servers << s
437
+ end
438
+ end
439
+ end
440
+ end
441
+
442
+ return(servers)
443
+ end
444
+
445
+ #
446
+ # Parse array names
447
+ #
448
+ # Returns: array of RestConnection::Server objects
449
+ #
450
+ def get_servers_by_array(names)
451
+ array_servers = []
452
+ if names.size > 0
453
+ names.each do |array_name|
454
+ all_arrays = ::Ec2ServerArray.find_by(:nickname) { |n| n =~ /^#{array_name}/i }
455
+
456
+ if all_arrays != nil and all_arrays.first != nil
457
+ all_arrays.first.instances.each do |s|
458
+ array_servers << s
459
+ end
460
+ end
461
+ end
462
+ end
463
+
464
+ return(array_servers)
465
+ end
466
+
467
+ #
468
+ # ServerTemplate auto-detection
469
+ #
470
+ # Returns: RestConnection::ServerTemplate
471
+ #
472
+ def detect_server_template(template, script, servers, array_names_to_detect)
473
+ st = nil
474
+
475
+ #
476
+ # If we have a script name but no template, check
477
+ # each server for the script until we locate it.
478
+ #
479
+ if script and template == nil
480
+ Log.debug "getting template URI..."
481
+
482
+ if not servers.empty?
483
+ for i in (0..servers.size - 1)
484
+
485
+ template = servers[i]['server_template_href'] if not servers[i].empty?
486
+ break if template
487
+ end
488
+
489
+ elsif not array_names_to_detect.empty?
490
+ array_names_to_detect.each do |array_name|
491
+ a = Ec2ServerArray.find_by(:nickname) { |n| n =~ /^#{array_name}/i }.first
492
+ next unless a
493
+ template = a['server_template_href']
494
+ break if template
495
+ end
496
+ end
497
+
498
+ raise "Unable to locate ServerTemplate!" unless template
499
+ Log.debug "Template: #{template}"
500
+ end
501
+
502
+ #
503
+ # Now look up the ServerTemplate via the RightScale API
504
+ #
505
+ if template
506
+ Log.debug "Looking up template..."
507
+
508
+ if template =~ /^http/
509
+ st = ::ServerTemplate.find(template)
510
+ else
511
+ st = ::ServerTemplate.find_by_nickname(template).first
512
+ end
513
+
514
+ if st == nil
515
+ raise "No matching ServerTemplate found!"
516
+ else
517
+ Log.debug "ServerTemplate: \"#{st['nickname']}\""
518
+ end
519
+ end
520
+
521
+ return(st)
522
+ end
523
+
524
+ #
525
+ # Look up the RightScript
526
+ #
527
+ # Returns: RestConnection::Executable
528
+ #
529
+ def detect_right_script(st, script)
530
+ executable = nil
531
+
532
+ if script == ""
533
+ if not @interactive
534
+ puts "Error: empty --script= option is supported only in interactive mode. Exiting."
535
+ exit 1
536
+ end
537
+ # Find operational scripts that exist in this server template
538
+ op_script_names = ['dummy name'] # Placeholder for #0 since we want to offer choices 1..n
539
+ op_script_hrefs = [ 'dummy href' ]
540
+ st.executables.each do |ex|
541
+ if ex.apply == "operational"
542
+ op_script_names.push( ex.name )
543
+ op_script_hrefs.push( ex.href )
544
+ end
545
+ end
546
+ if op_script_names.length <= 1
547
+ puts "Warning: No operational scripts found on the server(s). "
548
+ puts " (Search performed on server template '#{st.nickname}')"
549
+ else
550
+ puts "List of available operational scripts in the server template: ('#{st.nickname}')"
551
+ puts "------------------------------------------------------------"
552
+ for i in 1..op_script_names.length - 1
553
+ puts " %3d. #{op_script_names[i]}" % i
554
+ end
555
+ puts "------------------------------------------------------------"
556
+ while true
557
+ printf "Type the number of the script to run and press Enter (Ctrl-C to quit): "
558
+ op_script_id = Integer(gets.chomp) rescue -1
559
+ if op_script_id > 0 && op_script_id < op_script_names.length
560
+ puts "Script choice: #{op_script_id}. #{op_script_names[ op_script_id ]}"
561
+ break
562
+ else
563
+ puts "#{op_script_id < 0 ? 'Invalid input' : 'Input out of range'}."
564
+ end
565
+ end
566
+ # Provide the href as the input for the block that will do the lookup
567
+ script = op_script_hrefs[ op_script_id ]
568
+ end
569
+ end
570
+
571
+ if script
572
+ if script =~ /^http/ or script =~ /^\d+$/
573
+ if script =~ /^\d+$/
574
+ url_prefix = st.params['href'].match( /^.*\/acct\/\d+/)[0] # extract the 'https://my.rightscale.com/api/acct/<account_id>' part from the template's href
575
+ script = url_prefix + "/right_scripts/#{script}"
576
+ end
577
+ script_URI = script
578
+ Log.debug "Looking for script href \"#{script_URI}\""
579
+ puts
580
+ # First look up the script URI in the template.
581
+ # It *will* be found if we came here from the 'if script = ""' block
582
+ script = st.executables.detect { |ex| ex.href == script }
583
+ if not script
584
+ script_obj = ::RightScript.find(script_URI)
585
+ script_data = {}
586
+ script_data[ 'name' ] = script_obj.params['name']
587
+ script = ::RightScript.new({ :href => script_URI, :right_script => script_data })
588
+ end
589
+ else
590
+ Log.debug "looking for script \"#{script}\""
591
+ script = st.executables.detect { |ex| ex.name =~ /#{script}/ }
592
+ end
593
+
594
+ if script != nil and script['right_script'] != nil
595
+ puts "RightScript: \"#{script['right_script']['name']}\"" if @interactive
596
+ else
597
+ puts "No matching RightScript found!"
598
+ raise "No matching RightScript found!"
599
+ end
600
+
601
+ executable = script
602
+ end
603
+
604
+ return(executable)
605
+ end
606
+
607
+ #
608
+ # Load up the queue with work
609
+ #
610
+ # FIXME this needs to be refactored
611
+ #
612
+ def generate_jobs(queue_servers, queue_arrays, queue_template, queue_executable)
613
+ counter = 0
614
+ tasks = []
615
+ Log.debug "Loading queue..."
616
+
617
+ #
618
+ # Configure group
619
+ #
620
+ if not ChimpQueue[@group]
621
+ ChimpQueue.instance.create_group(@group, @group_type, @group_concurrency)
622
+ end
623
+
624
+ #
625
+ # Process ServerArray selection
626
+ #
627
+ Log.debug("processing queue selection")
628
+ if not queue_arrays.empty?
629
+ queue_arrays.each do |array|
630
+ instances = filter_out_non_operational_servers(array.instances)
631
+
632
+ if not instances
633
+ Log.error("no instances in array!")
634
+ break
635
+ end
636
+
637
+ instances.each do |array_instance|
638
+ #
639
+ # Handle limiting options
640
+ #
641
+ counter += 1
642
+ next if @limit_start.to_i > 0 and counter < @limit_start.to_i
643
+ break if @limit_end.to_i > 0 and counter > @limit_end.to_i
644
+ a = ExecArray.new(:array => array, :server => array_instance, :exec => queue_executable, :template => queue_template, :verbose => @@verbose, :quiet => @@quiet)
645
+ a.dry_run = @dry_run
646
+ ChimpQueue.instance.push(@group, a)
647
+ end
648
+ end
649
+ end
650
+
651
+ #
652
+ # Process Server selection
653
+ #
654
+ Log.debug("Processing server selection")
655
+
656
+ queue_servers.sort! { |a,b| a['nickname'] <=> b['nickname'] }
657
+ queue_servers.each do |server|
658
+
659
+ #
660
+ # Handle limiting options
661
+ #
662
+ counter += 1
663
+ next if @limit_start.to_i > 0 and counter < @limit_start.to_i
664
+ break if @limit_end.to_i > 0 and counter > @limit_end.to_i
665
+
666
+ #
667
+ # Construct the Server object
668
+ #
669
+ s = ::Server.new
670
+ s.href = server['href']
671
+ s.current_instance_href = server['current_instance_href']
672
+ s.name = server['nickname'] || server['name']
673
+ s.nickname = s.name
674
+ s.ip_address = server['ip-address'] || server['ip_address']
675
+ e = nil
676
+
677
+ if queue_executable
678
+ e = ExecRightScript.new(
679
+ :server => s,
680
+ :exec => queue_executable,
681
+ :inputs => @inputs,
682
+ :timeout => @timeout,
683
+ :verbose => @@verbose,
684
+ :quiet => @@quiet
685
+ )
686
+ elsif @ssh
687
+ e = ExecSSH.new(
688
+ :server => s,
689
+ :ssh_user => @ssh_user,
690
+ :exec => @ssh,
691
+ :verbose => @@verbose,
692
+ :quiet => @@quiet
693
+ )
694
+ elsif queue_template and not clone
695
+ e = ExecSetTemplate.new(
696
+ :server => s,
697
+ :template => queue_template,
698
+ :verbose => @@verbose,
699
+ :quiet => @@quiet
700
+ )
701
+ elsif @report
702
+ if s.href
703
+ s.href = s.href.sub("/current","")
704
+ e = ExecReport.new(:server => s, :verbose => @@verbose, :quiet => @@quiet)
705
+ e.fields = @report
706
+ end
707
+ elsif @set_tags.size > 0
708
+ e = ExecSetTags.new(:server => s, :verbose => @@verbose, :quiet => @@quiet)
709
+ e.tags = set_tags
710
+ end
711
+
712
+ if e != nil
713
+ e.dry_run = @dry_run
714
+ e.quiet = @@quiet
715
+ tasks.push(e)
716
+ end
717
+
718
+ end
719
+
720
+ return(tasks)
721
+ end
722
+
723
+ def add_to_queue(a)
724
+ a.each { |task| ChimpQueue.instance.push(@group, task) }
725
+ end
726
+
727
+ #
728
+ # Execute the user's command and provide for retrys etc.
729
+ #
730
+ def queue_runner(concurrency, delay, retry_count, progress)
731
+ queue = ChimpQueue.instance
732
+ queue.max_threads = concurrency
733
+ queue.delay = delay
734
+ queue.retry_count = retry_count
735
+ total_queue_size = queue.size
736
+
737
+ puts "Executing..." unless progress or not quiet
738
+ pbar = ProgressBar.new("Executing", 100) if progress
739
+ queue.start
740
+
741
+ queue.wait_until_done(@group) do
742
+ pbar.set(((total_queue_size.to_f - queue.size.to_f)/total_queue_size.to_f*100).to_i) if progress
743
+ end
744
+
745
+ pbar.finish if progress
746
+ end
747
+
748
+ #
749
+ # Set the action
750
+ #
751
+ def set_action(a)
752
+ raise ArgumentError.new "Cannot reset action" unless @action == :action_none
753
+ @action = a
754
+ end
755
+
756
+ #
757
+ # Allow user to verify results and retry if necessary
758
+ #
759
+ def verify_results(group = :default)
760
+ failed_workers, results_display = get_results(group)
761
+
762
+ #
763
+ # If no workers failed, then we're done.
764
+ #
765
+ return true if failed_workers.empty?
766
+
767
+ #
768
+ # Some workers failed; offer the user a chance to retry them
769
+ #
770
+ verify("The following objects failed:", results_display, false)
771
+
772
+ while true
773
+ puts "(R)etry failed jobs"
774
+ puts "(A)bort chimp run"
775
+ puts "(I)gnore errors and continue"
776
+ command = gets()
777
+
778
+ if command =~ /^a/i
779
+ puts "Aborting!"
780
+ exit 1
781
+ elsif command =~ /^i/i
782
+ puts "Ignoring errors and continuing"
783
+ exit 0
784
+ elsif command =~ /^r/i
785
+ puts "Retrying..."
786
+ ChimpQueue.instance.group[group].requeue_failed_jobs!
787
+ return false
788
+ end
789
+ end
790
+ end
791
+
792
+ #
793
+ # Get the results from the QueueRunner and format them
794
+ # in a way that's easy to display to the user
795
+ #
796
+ def get_results(group_name)
797
+ queue = ChimpQueue.instance
798
+ Log.debug("getting results for group #{group_name}")
799
+ results = queue.group[@group].results()
800
+ failed_workers = []
801
+ results_display = []
802
+
803
+ results.each do |result|
804
+ next if result == nil
805
+
806
+ if result[:status] == :error
807
+ name = result[:host] || "unknown"
808
+ message = result[:error].to_s || "unknown"
809
+ message.sub!("\n", "")
810
+ failed_workers << result[:worker]
811
+ results_display << "#{name.ljust(40)} #{message}"
812
+ end
813
+ end
814
+
815
+ return [failed_workers, results_display]
816
+ end
817
+
818
+ def print_timings
819
+ ChimpQueue.instance.group[@group].results.each do |task|
820
+ puts "Host: #{task[:host]} Type: #{task[:name]} Time: #{task[:total]} seconds"
821
+ end
822
+ end
823
+
824
+ def get_failures
825
+ return get_results(@group)
826
+ end
827
+
828
+ #
829
+ # Filter out non-operational servers
830
+ # Then add operational servers to the list of objects to display
831
+ #
832
+ def filter_out_non_operational_servers(servers)
833
+ Log.debug "Filtering out non-operational servers..."
834
+ servers.reject! { |s| s == nil || s['state'] != "operational" }
835
+ return(servers)
836
+ end
837
+
838
+ #
839
+ # Do work: either by submitting to chimpd
840
+ # or running it ourselves.
841
+ #
842
+ def do_work
843
+ done = false
844
+
845
+ while not done
846
+ queue_runner(@concurrency, @delay, @retry_count, @progress)
847
+
848
+ if @interactive and @verify
849
+ done = verify_results(@group)
850
+ else
851
+ done = true
852
+ end
853
+ end
854
+
855
+ if not @verify
856
+ failed_workers, results_display = get_results(group)
857
+ exit 1 if failed_workers.size > 0
858
+ end
859
+
860
+ puts "chimp run complete"
861
+ end
862
+
863
+ #
864
+ # Completely process a non-interactive chimp object command
865
+ #
866
+ def process
867
+ get_array_info
868
+ get_server_info
869
+ get_template_info
870
+ get_executable_info
871
+ return generate_jobs(@servers, @arrays, @server_template, @executable)
872
+ end
873
+
874
+ #
875
+ # Always returns 0. Used for chimpd compatibility.
876
+ #
877
+ def job_id
878
+ return 0
879
+ end
880
+
881
+ #
882
+ # Connect to chimpd and wait for the work queue to empty, and
883
+ # prompt the user if there are any errors.
884
+ #
885
+ def chimpd_wait_until_done
886
+ local_queue = ChimpQueue.instance
887
+
888
+ begin
889
+ while true
890
+ local_queue = ChimpQueue.instance
891
+
892
+ #
893
+ # load up remote chimpd jobs into the local queue
894
+ # this makes all the standard queue control methods available to us
895
+ #
896
+ retry_count = 1
897
+ while true
898
+ local_queue.reset!
899
+
900
+ begin
901
+ puts "Waiting for chimpd jobs to complete for group #{@group}..."
902
+ all = ChimpDaemonClient.retrieve_group_info(@chimpd_host, @chimpd_port, @group, :all)
903
+ rescue RestClient::ResourceNotFound
904
+ if retry_count > 0
905
+ retry_count -= 1
906
+ sleep 5
907
+ retry
908
+ end
909
+
910
+ if @ignore_errors
911
+ exit 0
912
+ else
913
+ $stderr.puts "ERROR: Group \"#{group}\" not found!"
914
+ exit 1
915
+ end
916
+ end
917
+
918
+ ChimpQueue.instance.create_group(@group)
919
+ ChimpQueue[@group].set_jobs(all)
920
+
921
+ break if ChimpQueue[@group].done?
922
+
923
+ $stdout.print "."
924
+ $stdout.flush
925
+ sleep 5
926
+ end
927
+
928
+ #
929
+ # If verify_results returns true, then ask chimpd to requeue all failed jobs.
930
+ #
931
+ if verify_results(@group)
932
+ break
933
+ else
934
+ ChimpDaemonClient.retry_group(@chimpd_host, @chimpd_port, @group)
935
+ end
936
+ end
937
+ ensure
938
+ #$stdout.print " done\n"
939
+ end
940
+ end
941
+
942
+ #
943
+ # Disable rest_connection logging
944
+ #
945
+ def disable_logging
946
+ ENV['REST_CONNECTION_LOG'] = "/dev/null"
947
+ ENV['RESTCLIENT_LOG'] = "/dev/null"
948
+ end
949
+
950
+ #
951
+ # Configure the Log object
952
+ #
953
+ def self.set_verbose(v=true, q=false)
954
+ @@verbose = v
955
+ @@quiet = q
956
+
957
+ STDOUT.sync = true
958
+ STDERR.sync = true
959
+
960
+ if @@verbose == true
961
+ Log.threshold = Logger::DEBUG
962
+ elsif @@quiet == true
963
+ Log.threshold = Logger::WARN
964
+ else
965
+ Log.threshold = Logger::INFO
966
+ end
967
+ end
968
+
969
+ def self.verbose?
970
+ return @@verbose
971
+ end
972
+
973
+ #
974
+ # Always returns 0. Used for chimpd compatibility.
975
+ #
976
+ def job_id
977
+ return 0
978
+ end
979
+
980
+ ####################################################
981
+ private
982
+ ####################################################
983
+
984
+ #
985
+ # Allow the user to verify the list of servers that an
986
+ # operation will be run against.
987
+ #
988
+ def verify(message, items, confirm=true)
989
+ puts message
990
+ puts "=================================================="
991
+
992
+ i = 0
993
+ items.sort.each do |item|
994
+ i += 1
995
+ puts " %03d. #{item}" % i
996
+ end
997
+
998
+ puts "=================================================="
999
+
1000
+ if confirm
1001
+ puts "Press enter to confirm or ^C to exit"
1002
+ gets
1003
+ end
1004
+ end
1005
+
1006
+ #
1007
+ # Verify that the given rightscript_executable (the object corresponding to the script)
1008
+ # that is associated with the server_template exists in all servers
1009
+ # (No need to check server arrays, they must all have the same template.)
1010
+ #
1011
+ # Returns: none. Prints a warning if any server does not have the script in its template.
1012
+ #
1013
+ def warn_if_rightscript_not_in_all_servers(servers, server_template, rightscript_executable)
1014
+
1015
+ return if servers.length < 2 or not server_template or not rightscript_executable
1016
+
1017
+ main_server_template = server_template
1018
+ main_server_template_name = main_server_template.params['nickname']
1019
+ main_server_template_href = main_server_template.params['href']
1020
+
1021
+ # Find which server has the specified template (the "main" template)
1022
+ server_that_has_main_template = nil
1023
+ for i in (0..servers.length - 1)
1024
+ if servers[i] and servers[i]['server_template_href'] == main_server_template_href
1025
+ server_that_has_main_template = servers[i]
1026
+ break
1027
+ end
1028
+ end
1029
+ if not server_that_has_main_template
1030
+ puts "internal error validating rightscript presence in all servers"
1031
+ return
1032
+ end
1033
+
1034
+ some_servers_have_different_template = false
1035
+ num_servers_missing_rightscript = 0
1036
+
1037
+ for i in (0..servers.length - 1)
1038
+ next if servers[i].empty?
1039
+
1040
+ this_server_template_href = servers[i]['server_template_href']
1041
+
1042
+ # If the server's template has the same href, this server is good
1043
+ next if this_server_template_href == main_server_template_href
1044
+
1045
+ if not some_servers_have_different_template
1046
+ some_servers_have_different_template = true
1047
+ if not @@quiet
1048
+ puts "Note: servers below have different server templates:"
1049
+ puts " - server '#{server_that_has_main_template['nickname']}: "
1050
+ if @@verbose
1051
+ puts " template name: '#{main_server_template_name}'"
1052
+ puts " href: '#{main_server_template_href}'"
1053
+ end
1054
+ end
1055
+ end
1056
+
1057
+ this_server_template = ::ServerTemplate.find(this_server_template_href)
1058
+ next if this_server_template == nil
1059
+ if not @@quiet
1060
+ puts " - server '#{servers[i]['nickname']}: "
1061
+ if @@verbose
1062
+ puts " template name: '#{this_server_template.params['nickname']}'"
1063
+ puts " href: '#{this_server_template.params['href']}'"
1064
+ end
1065
+ end
1066
+
1067
+ # Now check if the offending template has the rightscript in question
1068
+ has_script = false
1069
+ this_server_template.executables.each do |cur_script|
1070
+ if rightscript_executable['right_script']['href'] == cur_script['right_script']['href']
1071
+ has_script = true
1072
+ break
1073
+ end
1074
+ end
1075
+ if not has_script
1076
+ if not @@quiet
1077
+ puts " >> WARNING: The above server's template does not include the execution rightscript!"
1078
+ end
1079
+ num_servers_missing_rightscript += 1
1080
+ if num_servers_missing_rightscript == 1
1081
+ if @@verbose
1082
+ puts " script name: \'#{rightscript_executable['right_script']['name']}\', href: \'#{rightscript_executable['right_script']['href']}\'"
1083
+ end
1084
+ end
1085
+ end
1086
+ end
1087
+ if some_servers_have_different_template
1088
+ if num_servers_missing_rightscript == 0
1089
+ puts "Script OK. The servers have different templates, but they all contain the script, \'#{rightscript_executable['right_script']['name']}\'"
1090
+ else
1091
+ puts "WARNING: total of #{num_servers_missing_rightscript} servers listed do not have the rightscript in their template."
1092
+ end
1093
+ else
1094
+ if not @@quiet
1095
+ puts "Script OK. All the servers share the same template and the script is included in it."
1096
+ end
1097
+ end
1098
+ puts
1099
+ end
1100
+
1101
+ #
1102
+ # Generate a human readable list of objects
1103
+ #
1104
+ def make_human_readable_list_of_objects
1105
+ list_of_objects = []
1106
+
1107
+ if @servers
1108
+ list_of_objects += @servers.map { |s| s['nickname'] }
1109
+ end
1110
+
1111
+ if @arrays
1112
+ @arrays.each do |a|
1113
+ i = filter_out_non_operational_servers(a.instances)
1114
+ list_of_objects += i.map { |j| j['nickname'] }
1115
+ end
1116
+ end
1117
+ return(list_of_objects)
1118
+ end
1119
+
1120
+ #
1121
+ # Print out help information
1122
+ #
1123
+ def help
1124
+ puts
1125
+ puts "chimp -- a RightScale Platform command-line tool"
1126
+ puts
1127
+ puts "To select servers using tags:"
1128
+ puts " --tag=<tag> example: --tag=service:dataservice=true"
1129
+ puts " --tag-use-and 'and' all tags when selecting servers (default)"
1130
+ puts " --tag-use-or 'or' all tags when selecting servers"
1131
+ puts
1132
+ puts "To select arrays or deployments:"
1133
+ puts " --array=<name> array to execute upon"
1134
+ puts " --deployment=<name> deployment to execute upon"
1135
+ puts
1136
+ puts "To perform an action, specify one of the following:"
1137
+ puts " --script=[<name>|<uri>|<id>] name/uri/id of RightScript to run, empty for opscripts list"
1138
+ puts " --report=<field-1>,<field-2>... produce a report (see below)"
1139
+ puts " --ssh=<command> command to execute via SSH"
1140
+ puts " --ssh-user=<username> username to use for SSH login (default: root)"
1141
+ puts
1142
+ puts "Action options:"
1143
+ puts " --input=\"<name>=<value>\" set input <name> for RightScript execution"
1144
+ puts
1145
+ puts "Execution options:"
1146
+ puts " --group=<name> specify an execution group"
1147
+ puts " --group-type=<serial|parallel> specify group execution type"
1148
+ puts " --group-concurrency=<n> specify group concurrency, e.g. for parallel groups"
1149
+ puts
1150
+ puts " --concurrency=<n> number of concurrent actions to perform. Default: 1"
1151
+ puts " --delay=<seconds> delay a number of seconds between operations"
1152
+ puts
1153
+ puts "General options:"
1154
+ puts " --dry-run only show what would be done"
1155
+ puts " --ignore-errors ignore errors when server selection fails"
1156
+ puts " --retry=<n> number of times to retry. Default: 0"
1157
+ puts " --timeout=<seconds> set the timeout to wait for a RightScript to complete"
1158
+ puts " --progress toggle progress indicator"
1159
+ puts " --noprompt don't prompt with list of objects to run against"
1160
+ puts " --noverify disable interactive verification of errors"
1161
+ puts " --verbose display rest_connection log messages"
1162
+ puts " --dont-check-templates don't check for script even if servers have diff. templates"
1163
+ puts " --quiet suppress non-essential output"
1164
+ puts " --version display version and exit"
1165
+ puts
1166
+ puts "chimpd options:"
1167
+ puts " --chimpd=<port> send jobs to chimpd listening on <port> on localhost"
1168
+ puts " --chimpd-wait-until-done wait until all chimpd jobs are done"
1169
+ puts
1170
+ puts "Misc Notes:"
1171
+ puts " * If you leave the name of a --script or --ssh command blank, chimp will prompt you"
1172
+ puts " * You cannot operate on array instances by selecting them with tag queries"
1173
+ puts " * URIs must be API URIs in the format https://my.rightscale.com/api/acct/<acct>/ec2_server_templates/<id>"
1174
+ puts " * The following reporting keywords can be used: nickname, ip-address, state, server_type, href"
1175
+ puts " server_template_href, deployment_href, created_at, updated_at"
1176
+ puts
1177
+ end
1178
+
1179
+ end
1180
+ end
1181
+