right_chimp 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+