floatyhelper 2.0.6 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12632be13fdd03beaa837d72ea01a0ba7fff08494a79ed4f839070debfc35e89
4
- data.tar.gz: d9ca928fc876f292aca393aa4b1071508797b4883e3e64797a2ff75fb436f12e
3
+ metadata.gz: a0a23b077fb6f7d707267d06af856f7eb2c5854bd1ae65b325fd8098a57c5623
4
+ data.tar.gz: 812e17bf9526dc862df4095119ee388d6e4e8113773a7311600fe6e35ccde743
5
5
  SHA512:
6
- metadata.gz: ec8bc4eedba90e8457588265add5cdf55924f4583f65b9da4e5e1404771d009b1cfb5cbf09abcb8376ff7eeeb5cdf65b8f96d63ad5643fcaf11dcae9663489d1
7
- data.tar.gz: 3fa22d4d9a06edae1e6bf5db430b9b383f36549666c59056dfb5a87adc754b652da76af3ed0c1dd35f1222e8ca4f486d6298ce6951fff2cf7f580a122f2d8a8d
6
+ metadata.gz: 70b52c8b29fcb0dd530bd39efd69961da64784383705c57fc315e6c7bd013f9acc5339c46d66aeca3bdcb549bdc3b11f0b40e7ed0a987e93867ebd596c6afcb2
7
+ data.tar.gz: fe9a5d8fae49c366df1c6a125c1600d9d11b6c9c759bcc25e4aec7f2e9fce3a6a28785a61e42c9fae79efc943307694f0e71b39114b2e796e71637749040e8b8
@@ -3,6 +3,7 @@ require 'json'
3
3
  require 'open3'
4
4
  require 'colorize'
5
5
  require 'pty'
6
+ require 'thread'
6
7
 
7
8
  class Floaty
8
9
  FLOATY_SERVICES = {
@@ -137,10 +138,16 @@ class Floaty
137
138
  while !Process.waitpid(io.pid, Process::WNOHANG)
138
139
  line = io.gets
139
140
  unless line.nil?
140
- puts line
141
+ # Always print initial job_id request, VM results, or if we aren't running in a separate thread.
142
+ puts line if line =~ /^\s*-\s*[\w-]+\./ || line =~ /Requesting VMs/ || Thread.current['print'].nil? || Thread.current['print'] == true
141
143
  output += line
142
144
  end
143
145
  end
146
+ # If the process finishes before we get all the output, get the rest and handle it.
147
+ while line = io.gets
148
+ puts line if line =~ /^\s*-\s*[\w-]+\./ || Thread.current['print'].nil? || Thread.current['print'] == true
149
+ output += line
150
+ end
144
151
  end
145
152
  # Need to check exit status
146
153
  output
@@ -1,3 +1,3 @@
1
1
  module FloatyhelperVersion
2
- VERSION = '2.0.6'.freeze
2
+ VERSION = '2.1.0'.freeze
3
3
  end
@@ -7,8 +7,10 @@ require 'floatyhelper/config'
7
7
  require 'colorize'
8
8
  require 'net/http'
9
9
  require 'json'
10
+ require 'thread'
10
11
 
11
12
  class VM
13
+ # Assume we only have 1 of each if we can't find the info
12
14
  def self.pooled_platforms_default
13
15
  [
14
16
  'centos-7-x86_64',
@@ -17,10 +19,13 @@ class VM
17
19
  'redhat-7-x86_64',
18
20
  'redhat-8-x86_64',
19
21
  'redhat-fips-7-x86_64',
22
+ 'redhat-fips-8-x86_64',
20
23
  'scientific-7-x86_64',
21
24
  'sles-12-x86_64',
25
+ 'sles-15-x86_64',
22
26
  'ubuntu-1804-x86_64',
23
- ]
27
+ 'ubuntu-2004-x86_64',
28
+ ].map { |x| [x,1] }.to_h
24
29
  end
25
30
 
26
31
  def self.find_pooled_platforms
@@ -33,7 +38,7 @@ class VM
33
38
  result = JSON.parse(result.body)
34
39
  # Techinally, 'max > 0' tells you if it's a pooled platform, but if
35
40
  # the pool is empty, we'll want to fall back to ABS anyway.
36
- result['pools'].select { |_pool, info| info['ready'].positive? }.map { |pool, _info| pool.gsub('-pixa4','') }
41
+ result['pools'].select { |_pool, info| info['ready'].positive? }.map { |pool, info| [pool.gsub('-pixa4','').gsub('-pooled',''), info['ready']] }.to_h
37
42
  rescue StandardError
38
43
  # Not a great practice to swallow all errors, but this list is probably
39
44
  # pretty stable, so let's just pass along the default.
@@ -48,7 +53,7 @@ class VM
48
53
  Groups.delete_tag(id) if Groups.tag?(id)
49
54
  Groups.delete_all if id == 'all'
50
55
  hosts = hosts.select { |host| alive(host) }
51
- puts Floaty.floaty_cmd("delete #{hosts.join(',')} --service vmpooler") unless hosts.empty?
56
+ hosts.each_slice(10) { |s| puts Floaty.floaty_cmd("delete #{s.join(',')} --service vmpooler") } unless hosts.empty?
52
57
  end
53
58
 
54
59
  def self.query(host)
@@ -149,28 +154,98 @@ class VM
149
154
  end
150
155
 
151
156
  def self.revert(id, snaptag)
157
+ clr = Config.get_config_setting('vertical_snapshot_status')
158
+ clr = clr.to_s.downcase == 'true'
152
159
  snaptag ||= 'Blank Snaptag'
153
160
  shas = VM.getsnapshot(id, snaptag)
161
+ lastlog = {}
162
+ puts `tput clear` if clr
154
163
  shas.each do |host, sha|
164
+ # Force a session to show up in lastlog
165
+ lastlog[host] = `ssh -t root@#{host} lastlog -u root 2>/dev/null`.gsub("\r",'').gsub("\n",'')
155
166
  print "#{host}: #{sha} --- "
156
167
  puts Floaty.floaty_cmd("revert #{host} #{sha} --service vmpooler")
157
168
  end
158
- puts 'Waiting 10 seconds for revert to take effect'
159
- sleep(10)
169
+ puts
170
+ puts 'Waiting for all VMs to finish reverting...'
171
+ alldone = false
172
+ until alldone
173
+ puts `tput cup #{shas.count + 2}` if clr
174
+ alldone = true
175
+ print "\r" unless clr
176
+ lastlog.each do |host, value|
177
+ begin
178
+ result =`ssh root@#{host} lastlog -u root 2>/dev/null`.gsub("\r",'').gsub("\n",'')
179
+ # Original will have some garbage about pseudo-terminal that we won't have here
180
+ # Also, can't do this on Windows yet, so pretend we're done for those hosts.
181
+ done = !lastlog[host].include?(result) || result.empty?
182
+ rescue
183
+ done = false
184
+ end
185
+ status = done ? 'Done'.green : 'Wait'.yellow
186
+ if clr
187
+ puts "* %s #{status} *" % "#{host}:".ljust(16)
188
+ else
189
+ print "* %s #{status} *" % "#{host}:".ljust(16)
190
+ end
191
+ alldone &= done
192
+ end
193
+ sleep(1)
194
+ end
195
+ puts ''
196
+ end
197
+
198
+ # Response should be something like:
199
+ # - vm1.delivery.puppetlabs.net (platform-tag)\n
200
+ # - vm2.delivery.puppetlabs.net (platform-tag)\n
201
+ def self.parse_response(response)
202
+ response.scan(/- ([\w-]+)(?:.vmpooler-prod.puppet.net|.delivery.puppetlabs.net) \(/).flatten
160
203
  end
161
204
 
162
205
  ### Get a VM from floaty ###
163
- def self.get_vm(platform: 'centos-7-x86_64', force_abs: false)
164
- if !find_pooled_platforms.include?(platform.gsub('-pixa4','')) || force_abs
165
- response = Floaty.floaty_cmd("get #{platform} --service abs --priority 1", use_pty: true)
166
- vmlinesplit = response.chomp.split('- ')
206
+ def self.get_vm(platform: 'centos-7-x86_64', force_abs: false, number: 1)
207
+ pools = find_pooled_platforms
208
+ platform = platform.gsub('-pixa4','')
209
+ vms_per_thread = 10
210
+ if !pools.keys.include?(platform) || force_abs || pools[platform] < number
211
+ numthreads = number / vms_per_thread
212
+ numthreads += 1 if number % vms_per_thread != 0
213
+ threads = []
214
+ responses = []
215
+ mutex = Mutex.new
216
+ numleft = number
217
+ numthreads.times do
218
+ num = numleft < vms_per_thread ? numleft : vms_per_thread
219
+ numleft -= num
220
+ threads << Thread.new do
221
+ Thread.current['print'] = false
222
+ myresponse = Floaty.floaty_cmd("get #{platform}=#{num} --service abs --priority 1 --force", use_pty: true)
223
+ mutex.synchronize { responses << myresponse }
224
+ end
225
+ end
226
+ # Setting print to true then joining causes the thread not
227
+ # to pick up the variable change sometimes. So this polls
228
+ # the thread status and sets the print variable in a loop.
229
+ puts "Fetching VMs using #{threads.count} thread#{threads.count > 1 ? 's' : ''}. Output shown will be from one of the currently alive threads, as well as the result of any completed threads, until all threads are finished.".cyan unless threads.count == 1
230
+ while threads.any? { |t| t.alive? } do
231
+ # If we have no living threads with print set to true,
232
+ # pick one and turn it on.
233
+ if threads.select { |t| t.alive? && t['print'] == true }.empty?
234
+ t = threads.find{ |t| t.alive? }
235
+ t['print'] = true if !t.nil?
236
+ end
237
+ end
238
+ # Just in case
239
+ threads.each { |t| t.join }
240
+ vms = []
241
+ responses.each { |r| vms += parse_response(r) }
167
242
  # When use_pty is true, we've already printed stderr/stdout, so no need to do so again.
168
- raise "Error obtaining a VM" if vmlinesplit.count == 1
169
- vmlinesplit[1].split[0].split('.')[0]
243
+ raise "Error obtaining a VM" if vms.empty?
244
+ vms
170
245
  else
171
- response = Floaty.floaty_cmd("get #{platform} --service vmpooler")
246
+ response = Floaty.floaty_cmd("get #{platform}=#{number} --service vmpooler")
172
247
  raise "Error obtaining a VM: #{response}" if response.include?('error')
173
- response.split[1].split('.')[0]
248
+ parse_response(response)
174
249
  end
175
250
  end
176
251
  end
data/lib/floatyhelper.rb CHANGED
@@ -10,6 +10,8 @@ require 'floatyhelper/groups'
10
10
  require 'floatyhelper/vm'
11
11
  require 'floatyhelper/version'
12
12
  require 'colorize'
13
+ require 'open3'
14
+ require 'thread'
13
15
 
14
16
  class Floatyhelper
15
17
  include Commander::Methods
@@ -371,15 +373,24 @@ class Floatyhelper
371
373
  ABS with the --abs flag.
372
374
  EOT
373
375
  c.option '--abs', 'Force use of ABS to get platform, even if platform is pooled.'
376
+ c.option '--number INTEGER', Integer, 'Number of VMs of this flavor to check out. Default 1.'
377
+ c.option '-y', "Don't ask if we're sure we want to request more than 10 VMs."
374
378
  c.action do |args, options|
375
379
  platform = args[0] || 'centos-7-x86_64'
376
380
  tag = args.length > 1 ? args[1] : 'Unassigned'
381
+ num = options.number || 1
382
+ if num > 10 && !options.y
383
+ answer = ask("Checking out more than 10 VMs at once can have an impact on the Vmpooler infrastructure. Please do not use this option unless you know what you're doing and clean them up when you're finished. Proceed? [Y/n]".yellow)
384
+ if !answer.empty? && answer.capitalize != 'Y'
385
+ return
386
+ end
387
+ end
377
388
  begin
378
- host = VM.get_vm(platform: platform, force_abs: options.abs)
379
- Groups.addhosts([host], tag)
380
- puts "Added #{host} to tag #{tag}".green
389
+ hosts = VM.get_vm(platform: platform, force_abs: options.abs, number: num)
390
+ Groups.addhosts(hosts, tag)
391
+ puts "Added #{hosts.join(', ')} to tag #{tag}".green
381
392
  amount = Config.get_config_setting('increaselife').to_i
382
- VM.increaselife(host, amount)
393
+ VM.increaselife(hosts, amount)
383
394
  rescue StandardError => e
384
395
  puts e.message.red
385
396
  end
@@ -395,6 +406,101 @@ class Floatyhelper
395
406
  end
396
407
  end
397
408
 
409
+ command :runon do |c|
410
+ c.syntax = 'floatyhelper runon <tag> <command>'
411
+ c.summary = 'Run a command on all hosts under the given tag'
412
+ c.description = 'Run a command on all hosts under the given tag using SSH. Requires that the key to SSH into the hosts is in ssh-agent already.'
413
+ c.option '--user', 'Run command as a user other than root'
414
+ c.action do |args, options|
415
+ if args.length == 0
416
+ puts 'Must specify a tag and command to run'.red
417
+ return
418
+ end
419
+ if args.length == 1
420
+ puts 'Must specify a command to run'.red
421
+ return
422
+ end
423
+
424
+ tag = args[0]
425
+ command = args[1..].join(' ')
426
+ user = options.user || 'root'
427
+
428
+ if !Groups.tag?(tag)
429
+ puts "#{tag} does not appear to be a tag managed by floatyhelper.".red
430
+ return
431
+ end
432
+ hosts = Groups.get_hosts(tag)
433
+ threads = []
434
+ responses = []
435
+ mutex = Mutex.new
436
+ hosts.each do |host|
437
+ threads << Thread.new do
438
+ output, status = Open3.capture2e("ssh -A -o StrictHostKeyChecking=no #{user}@#{host}.delivery.puppetlabs.net \"#{command}\"")
439
+ mutex.synchronize do
440
+ responses << [host,output,status]
441
+ end
442
+ end
443
+ end
444
+ puts
445
+ (1..threads.count).each do |i|
446
+ print "\rWaiting for thread #{i}/#{threads.count} to finish..."
447
+ threads[i-1].join
448
+ end
449
+ print "\rDone\n"
450
+ responses.each do |r|
451
+ color = r[2].exitstatus.zero? ? :white : :red
452
+ puts "*** #{r[0]}: Exit code #{r[2].exitstatus} ***".colorize(color)
453
+ puts r[1].colorize(color)
454
+ end
455
+ end
456
+ end
457
+
458
+ command :scpto do |c|
459
+ c.syntax = 'floatyhelper scp <tag> <path_to_local_file> <path_to_remote_file>'
460
+ c.summary = 'SCP a file from this host to all hosts under the given tag at the given path'
461
+ c.description = 'SCP a file from this host to all hosts under the given tag at the given path. This assumes the necessary SSH key is already loaded into ssh-agent.'
462
+ c.option '--user', 'Log in as a user other than root'
463
+ c.action do |args, options|
464
+ if args.length < 3
465
+ puts 'Must specify a tag, local path, and remote path'.red
466
+ return
467
+ end
468
+
469
+ tag = args[0]
470
+ local = args[1]
471
+ remote = args[2]
472
+ user = options.user || 'root'
473
+
474
+ if !Groups.tag?(tag)
475
+ puts "#{tag} does not appear to be a tag managed by floatyhelper.".red
476
+ return
477
+ end
478
+ hosts = Groups.get_hosts(tag)
479
+ threads = []
480
+ responses = []
481
+ mutex = Mutex.new
482
+ hosts.each do |host|
483
+ threads << Thread.new do
484
+ output, status = Open3.capture2e("scp #{local} #{user}@#{host}.delivery.puppetlabs.net:#{remote}")
485
+ mutex.synchronize do
486
+ responses << [host,output,status]
487
+ end
488
+ end
489
+ end
490
+ puts
491
+ (1..threads.count).each do |i|
492
+ print "\rWaiting for thread #{i}/#{threads.count} to finish..."
493
+ threads[i-1].join
494
+ end
495
+ print "\rDone\n"
496
+ responses.each do |r|
497
+ color = r[2].exitstatus.zero? ? :white : :red
498
+ puts "*** #{r[0]}: Exit code #{r[2].exitstatus} ***".colorize(color)
499
+ puts r[1].colorize(color)
500
+ end
501
+ end
502
+ end
503
+
398
504
  run!
399
505
  end
400
506
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floatyhelper
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.6
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Burgan-Illig
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-21 00:00:00.000000000 Z
11
+ date: 2023-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler