vmfloaty 0.8.2 → 0.9.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
- SHA1:
3
- metadata.gz: 04b7261aa013260aba635e266b003d845e4a2c58
4
- data.tar.gz: 83bbc1c12ace45c6a1ea288121762f13d3b841ae
2
+ SHA256:
3
+ metadata.gz: 7b08c0be5bb4c8a57ce706db190b37bdf89532e30964aca13bd53dc4b2649d46
4
+ data.tar.gz: aeaeb37cde837b342ddd438b6e3c389e9e84b12fd6caa36a40fcd87ff8b0427b
5
5
  SHA512:
6
- metadata.gz: 4e38aedb270708b75d9464744b73b507a6dfdab100567a079287b6b06aa0ac57fabaa03e0e18761c5d1b21e2921f15fbd36ab5beeddcbf3266cac2981d90931c
7
- data.tar.gz: 04ddaf46164b38bf84df47ee8d5a0e96d87b6be5d64941ee72a075e5264511c7556b8fdd85a2875fadfea37820cfbc307adc6dd46eb32621c1cb552d70cbec94
6
+ metadata.gz: d7bb8b68a6dcc391184c910883e4d05c1d7c8580dd7d969f141959fdd46b0e4a9ef4c7ec141a3d4526cab557923d80d0db62e8ae11e8fd76120d2d0e5367fde8
7
+ data.tar.gz: d0bb6b6ff8fd727a110814ebd3836dcd344b30c0e0fb5791ac4485ed0fafe2d715d00173e7d874ec9a41488eaecd0709d1ce53a8c5162419e3285912a1746b2d
data/README.md CHANGED
@@ -53,7 +53,7 @@ $ floaty --help
53
53
  Grabbing a token for authenticated pooler requests:
54
54
 
55
55
  ```
56
- floaty token get --user username --url https://vmpooler.mycompany.net/api/v1
56
+ floaty token get --user username --url https://vmpooler.example.net/api/v1
57
57
  ```
58
58
 
59
59
  This command will then ask you to log in. If successful, it will return a token that you can save either in a dotfile or use with other cli commands.
@@ -61,18 +61,18 @@ This command will then ask you to log in. If successful, it will return a token
61
61
  Grabbing vms:
62
62
 
63
63
  ```
64
- floaty get centos-7-x86_64=2 debian-7-x86_64 windows-10=3 --token mytokenstring --url https://vmpooler.mycompany.net/api/v1
64
+ floaty get centos-7-x86_64=2 debian-7-x86_64 windows-10=3 --token mytokenstring --url https://vmpooler.example.net/api/v1
65
65
  ```
66
66
 
67
67
  ### vmfloaty dotfile
68
68
 
69
- If you do not wish to continuely specify various config options with the cli, you can have a dotfile in your home directory for some defaults. For example:
69
+ If you do not wish to continually specify various config options with the cli, you can have a dotfile in your home directory for some defaults. For example:
70
70
 
71
71
  #### Basic configuration
72
72
 
73
73
  ```yaml
74
74
  # file at /Users/me/.vmfloaty.yml
75
- url: 'https://vmpooler.mycompany.net/api/v1'
75
+ url: 'https://vmpooler.example.net/api/v1'
76
76
  user: 'brian'
77
77
  token: 'tokenstring'
78
78
  ```
@@ -90,10 +90,10 @@ To configure multiple services, you can set up your `~/.vmfloaty.yml` config fil
90
90
  user: 'brian'
91
91
  services:
92
92
  main:
93
- url: 'https://vmpooler.mycompany.net/api/v1'
93
+ url: 'https://vmpooler.example.net/api/v1'
94
94
  token: 'tokenstring'
95
95
  alternate:
96
- url: 'https://vmpooler.alternate.net/api/v1'
96
+ url: 'https://vmpooler.example.com/api/v1'
97
97
  token: 'alternate-tokenstring'
98
98
  ```
99
99
 
@@ -125,12 +125,17 @@ vmfloaty is capable of working with Puppet's [nonstandard pooler](https://github
125
125
  user: 'brian'
126
126
  services:
127
127
  vm:
128
- url: 'https://vmpooler.mycompany.net/api/v1'
128
+ url: 'https://vmpooler.example.net/api/v1'
129
129
  token: 'tokenstring'
130
130
  ns:
131
- url: 'https://nspooler.mycompany.net/api/v1'
131
+ url: 'https://nspooler.example.net/api/v1'
132
132
  token: 'nspooler-tokenstring'
133
133
  type: 'nonstandard' # <-- 'type' is necessary for any non-vmpooler service
134
+ abs:
135
+ url: 'https://abs.example.net/'
136
+ token: 'abs-tokenstring'
137
+ type: 'abs' # <-- 'type' is necessary for any non-vmpooler service
138
+
134
139
  ```
135
140
 
136
141
  With this configuration, you could list available OS types from nspooler like this:
data/bin/floaty CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
4
5
 
5
6
  require 'vmfloaty'
6
7
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rubygems'
4
4
  require 'commander'
@@ -17,7 +17,7 @@ require 'vmfloaty/ssh'
17
17
  class Vmfloaty
18
18
  include Commander::Methods
19
19
 
20
- def run
20
+ def run # rubocop:disable Metrics/AbcSize
21
21
  program :version, Vmfloaty::VERSION
22
22
  program :description, 'A CLI helper tool for Puppet Labs VM poolers to help you stay afloat'
23
23
 
@@ -33,6 +33,7 @@ class Vmfloaty
33
33
  c.option '--user STRING', String, 'User to authenticate with'
34
34
  c.option '--url STRING', String, 'URL of pooler service'
35
35
  c.option '--token STRING', String, 'Token for pooler service'
36
+ c.option '--priority STRING', 'Priority for supported backends(ABS) (High(1), Medium(2), Low(3))'
36
37
  c.option '--notoken', 'Makes a request without a token'
37
38
  c.option '--force', 'Forces vmfloaty to get requested vms'
38
39
  c.option '--json', 'Prints retrieved vms in JSON format'
@@ -43,22 +44,22 @@ class Vmfloaty
43
44
  force = options.force
44
45
 
45
46
  if args.empty?
46
- STDERR.puts "No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs."
47
+ STDERR.puts 'No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs.'
47
48
  exit 1
48
49
  end
49
50
 
50
51
  os_types = Utils.generate_os_hash(args)
51
52
 
52
53
  max_pool_request = 5
53
- large_pool_requests = os_types.select{|_,v| v > max_pool_request}
54
- if ! large_pool_requests.empty? and ! force
54
+ large_pool_requests = os_types.select { |_, v| v > max_pool_request }
55
+ if !large_pool_requests.empty? && !force
55
56
  STDERR.puts "Requesting vms over #{max_pool_request} requires a --force flag."
56
- STDERR.puts "Try again with `floaty get --force`"
57
+ STDERR.puts 'Try again with `floaty get --force`'
57
58
  exit 1
58
59
  end
59
60
 
60
61
  if os_types.empty?
61
- STDERR.puts "No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs."
62
+ STDERR.puts 'No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs.'
62
63
  exit 1
63
64
  end
64
65
 
@@ -84,6 +85,7 @@ class Vmfloaty
84
85
  c.option '--url STRING', String, 'URL of pooler service'
85
86
  c.action do |args, options|
86
87
  verbose = options.verbose || config['verbose']
88
+
87
89
  service = Service.new(options, config)
88
90
  filter = args[0]
89
91
 
@@ -143,18 +145,18 @@ class Vmfloaty
143
145
  hostname = args[0]
144
146
  modify_all = options.all
145
147
 
146
- if hostname.nil? and !modify_all
147
- STDERR.puts "ERROR: Provide a hostname or specify --all."
148
+ if hostname.nil? && !modify_all
149
+ STDERR.puts 'ERROR: Provide a hostname or specify --all.'
148
150
  exit 1
149
151
  end
150
- running_vms = modify_all ? service.list_active(verbose) : hostname.split(",")
152
+ running_vms = modify_all ? service.list_active(verbose) : hostname.split(',')
151
153
 
152
154
  tags = options.tags ? JSON.parse(options.tags) : nil
153
155
  modify_hash = {
154
- lifetime: options.lifetime,
155
- disk: options.disk,
156
- tags: tags,
157
- reason: options.reason
156
+ :lifetime => options.lifetime,
157
+ :disk => options.disk,
158
+ :tags => tags,
159
+ :reason => options.reason,
158
160
  }
159
161
  modify_hash.delete_if { |_, value| value.nil? }
160
162
 
@@ -171,11 +173,11 @@ class Vmfloaty
171
173
  end
172
174
  if ok
173
175
  if modify_all
174
- puts "Successfully modified all VMs."
176
+ puts 'Successfully modified all VMs.'
175
177
  else
176
178
  puts "Successfully modified VM #{hostname}."
177
179
  end
178
- puts "Use `floaty list --active` to see the results."
180
+ puts 'Use `floaty list --active` to see the results.'
179
181
  end
180
182
  end
181
183
  end
@@ -205,15 +207,13 @@ class Vmfloaty
205
207
  if delete_all
206
208
  running_vms = service.list_active(verbose)
207
209
  if running_vms.empty?
208
- STDERR.puts "You have no running VMs."
210
+ STDERR.puts 'You have no running VMs.'
209
211
  else
210
212
  Utils.pretty_print_hosts(verbose, service, running_vms)
211
213
  # Confirm deletion
212
214
  puts
213
215
  confirmed = true
214
- unless force
215
- confirmed = agree('Delete all these VMs? [y/N]')
216
- end
216
+ confirmed = agree('Delete all these VMs? [y/N]') unless force
217
217
  if confirmed
218
218
  response = service.delete(verbose, running_vms)
219
219
  response.each do |hostname, result|
@@ -236,7 +236,7 @@ class Vmfloaty
236
236
  end
237
237
  end
238
238
  else
239
- STDERR.puts "You did not provide any hosts to delete"
239
+ STDERR.puts 'You did not provide any hosts to delete'
240
240
  exit 1
241
241
  end
242
242
 
@@ -302,9 +302,7 @@ class Vmfloaty
302
302
  hostname = args[0]
303
303
  snapshot_sha = args[1] || options.snapshot
304
304
 
305
- if args[1] && options.snapshot
306
- STDERR.puts "Two snapshot arguments were given....using snapshot #{snapshot_sha}"
307
- end
305
+ STDERR.puts "Two snapshot arguments were given....using snapshot #{snapshot_sha}" if args[1] && options.snapshot
308
306
 
309
307
  begin
310
308
  revert_req = service.revert(verbose, hostname, snapshot_sha)
@@ -372,25 +370,23 @@ class Vmfloaty
372
370
 
373
371
  begin
374
372
  case action
375
- when 'get'
376
- token = service.get_new_token(verbose)
377
- puts token
378
- when 'delete'
379
- result = service.delete_token(verbose, options.token)
380
- puts result
381
- when 'status'
382
- token_value = options.token
383
- if token_value.nil?
384
- token_value = args[1]
385
- end
386
- status = service.token_status(verbose, token_value)
387
- puts status
388
- when nil
389
- STDERR.puts 'No action provided'
390
- exit 1
391
- else
392
- STDERR.puts "Unknown action: #{action}"
393
- exit 1
373
+ when 'get'
374
+ token = service.get_new_token(verbose)
375
+ puts token
376
+ when 'delete'
377
+ result = service.delete_token(verbose, options.token)
378
+ puts result
379
+ when 'status'
380
+ token_value = options.token
381
+ token_value = args[1] if token_value.nil?
382
+ status = service.token_status(verbose, token_value)
383
+ puts status
384
+ when nil
385
+ STDERR.puts 'No action provided'
386
+ exit 1
387
+ else
388
+ STDERR.puts "Unknown action: #{action}"
389
+ exit 1
394
390
  end
395
391
  rescue TokenError => e
396
392
  STDERR.puts e
@@ -417,15 +413,13 @@ class Vmfloaty
417
413
  use_token = !options.notoken
418
414
 
419
415
  if args.empty?
420
- STDERR.puts "No operating systems provided to obtain. See `floaty ssh --help` for more information on how to get VMs."
416
+ STDERR.puts 'No operating systems provided to obtain. See `floaty ssh --help` for more information on how to get VMs.'
421
417
  exit 1
422
418
  end
423
419
 
424
420
  host_os = args.first
425
421
 
426
- if args.length > 1
427
- STDERR.puts "Can't ssh to multiple hosts; Using #{host_os} only..."
428
- end
422
+ STDERR.puts "Can't ssh to multiple hosts; Using #{host_os} only..." if args.length > 1
429
423
 
430
424
  service.ssh(verbose, host_os, use_token)
431
425
  exit 0
@@ -435,13 +429,13 @@ class Vmfloaty
435
429
  command :completion do |c|
436
430
  c.syntax = 'floaty completion [options]'
437
431
  c.summary = 'Outputs path to completion script'
438
- c.description = Utils.strip_heredoc(<<-EOF)
432
+ c.description = Utils.strip_heredoc(<<-DESCRIPTION)
439
433
  Outputs path to a completion script for the specified shell (or 'bash' if not specified). This makes it easy to add the completion script to your profile:
440
434
 
441
435
  source $(floaty completion --shell bash)
442
436
 
443
437
  This subcommand will exit non-zero with an error message if no completion script is available for the requested shell.
444
- EOF
438
+ DESCRIPTION
445
439
  c.example 'Gets path to bash tab completion script', 'floaty completion --shell bash'
446
440
  c.option '--shell STRING', String, 'Shell to request completion script for'
447
441
  c.action do |_, options|
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vmfloaty/errors'
4
+ require 'vmfloaty/http'
5
+ require 'faraday'
6
+ require 'json'
7
+
8
+ class ABS
9
+ # List active VMs in ABS
10
+ # This is what a job request looks like:
11
+ # {
12
+ # "state":"filled",
13
+ # "last_processed":"2019-10-31 20:59:33 +0000",
14
+ # "allocated_resources": [
15
+ # {
16
+ # "hostname":"h3oyntawjm7xdch.delivery.puppetlabs.net",
17
+ # "type":"centos-7.2-tmpfs-x86_64",
18
+ # "engine":"vmpooler"}
19
+ # ],
20
+ # "audit_log":{
21
+ # "2019-10-30 20:33:12 +0000":"Allocated h3oyntawjm7xdch.delivery.puppetlabs.net for job 1572467589"
22
+ # },
23
+ # "request":{
24
+ # "resources":{
25
+ # "centos-7.2-tmpfs-x86_64":1
26
+ # },
27
+ # "job": {
28
+ # "id":1572467589,
29
+ # "tags": {
30
+ # "user":"mikker",
31
+ # "url_string":"floaty://mikker/1572467589"
32
+ # },
33
+ # "user":"mikker",
34
+ # "time-received":1572467589
35
+ # }
36
+ # }
37
+ # }
38
+ #
39
+
40
+ @active_hostnames = {}
41
+
42
+ def self.list_active(verbose, url, _token, user)
43
+ all_jobs = []
44
+ @active_hostnames = {}
45
+
46
+ get_active_requests(verbose, url, user).each do |req_hash|
47
+ all_jobs.push(req_hash['request']['job']['id'])
48
+ @active_hostnames[req_hash['request']['job']['id']] = req_hash
49
+ end
50
+
51
+ all_jobs
52
+ end
53
+
54
+ def self.get_active_requests(verbose, url, user)
55
+ conn = Http.get_conn(verbose, url)
56
+ res = conn.get 'status/queue'
57
+ requests = JSON.parse(res.body)
58
+
59
+ ret_val = []
60
+
61
+ requests.each do |req|
62
+ next if req == 'null'
63
+
64
+ req_hash = JSON.parse(req)
65
+
66
+ begin
67
+ next unless user == req_hash['request']['job']['user']
68
+
69
+ ret_val.push(req_hash)
70
+ rescue NoMethodError
71
+ puts "Warning: couldn't parse line returned from abs/status/queue: ".yellow
72
+ end
73
+ end
74
+
75
+ ret_val
76
+ end
77
+
78
+ def self.all_job_resources_accounted_for(allocated_resources, hosts)
79
+ allocated_host_list = allocated_resources.map { |ar| ar['hostname'] }
80
+ (allocated_host_list - hosts).empty?
81
+ end
82
+
83
+ def self.delete(verbose, url, hosts, token, user)
84
+ # In ABS terms, this is a "returned" host.
85
+ conn = Http.get_conn(verbose, url)
86
+ conn.headers['X-AUTH-TOKEN'] = token if token
87
+
88
+ puts "Trying to delete hosts #{hosts}" if verbose
89
+ requests = get_active_requests(verbose, url, user)
90
+
91
+ jobs_to_delete = []
92
+
93
+ ret_status = {}
94
+ hosts.each do |host|
95
+ ret_status[host] = {
96
+ 'ok' => false,
97
+ }
98
+ end
99
+
100
+ requests.each do |req_hash|
101
+ next unless req_hash['state'] == 'allocated' || req_hash['state'] == 'filled'
102
+
103
+ req_hash['allocated_resources'].each do |vm_name, _i|
104
+ if hosts.include? vm_name['hostname']
105
+ if all_job_resources_accounted_for(req_hash['allocated_resources'], hosts)
106
+ ret_status[vm_name['hostname']] = {
107
+ 'ok' => true,
108
+ }
109
+ jobs_to_delete.push(req_hash)
110
+ else
111
+ puts "When using ABS you must delete all vms that you requested at the same time: Can't delete #{req_hash['request']['job']['id']}: #{hosts} does not include all of #{req_hash['allocated_resources']}"
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ response_body = {}
118
+
119
+ jobs_to_delete.each do |job|
120
+ req_obj = {
121
+ 'job_id' => job['request']['job']['id'],
122
+ 'hosts' => job['allocated_resources'],
123
+ }
124
+
125
+ puts "Deleting #{req_obj}" if verbose
126
+
127
+ return_result = conn.post 'return', req_obj.to_json
128
+ req_obj['hosts'].each do |host|
129
+ response_body[host['hostname']] = { 'ok' => true } if return_result.body == 'OK'
130
+ end
131
+ end
132
+
133
+ response_body
134
+ end
135
+
136
+ # List available VMs in ABS
137
+ def self.list(verbose, url, os_filter = nil)
138
+ conn = Http.get_conn(verbose, url)
139
+
140
+ os_list = []
141
+
142
+ res = conn.get 'status/platforms/vmpooler'
143
+
144
+ res_body = JSON.parse(res.body)
145
+ os_list << '*** VMPOOLER Pools ***'
146
+ os_list += JSON.parse(res_body['vmpooler_platforms'])
147
+
148
+ res = conn.get 'status/platforms/nspooler'
149
+ res_body = JSON.parse(res.body)
150
+ os_list << ''
151
+ os_list << '*** NSPOOLER Pools ***'
152
+ os_list += JSON.parse(res_body['nspooler_platforms'])
153
+
154
+ res = conn.get 'status/platforms/aws'
155
+ res_body = JSON.parse(res.body)
156
+ os_list << ''
157
+ os_list << '*** AWS Pools ***'
158
+ os_list += JSON.parse(res_body['aws_platforms'])
159
+
160
+ os_list.delete 'ok'
161
+
162
+ os_filter ? os_list.select { |i| i[/#{os_filter}/] } : os_list
163
+ end
164
+
165
+ # Retrieve an OS from ABS.
166
+ def self.retrieve(verbose, os_types, token, url, user, options)
167
+ #
168
+ # Contents of post must be like:
169
+ #
170
+ # {
171
+ # "resources": {
172
+ # "centos-7-i386": 1,
173
+ # "ubuntu-1404-x86_64": 2
174
+ # },
175
+ # "job": {
176
+ # "id": "12345",
177
+ # "tags": {
178
+ # "user": "username",
179
+ # }
180
+ # }
181
+ # }
182
+
183
+ conn = Http.get_conn(verbose, url)
184
+ conn.headers['X-AUTH-TOKEN'] = token if token
185
+
186
+ saved_job_id = DateTime.now.strftime('%Q')
187
+
188
+ req_obj = {
189
+ :resources => os_types,
190
+ :job => {
191
+ :id => saved_job_id,
192
+ :tags => {
193
+ :user => user,
194
+ },
195
+ },
196
+ }
197
+
198
+ if options['priority']
199
+ req_obj[:priority] = if options['priority'] == 'high'
200
+ 1
201
+ elsif options['priority'] == 'medium'
202
+ 2
203
+ elsif options['priority'] == 'low'
204
+ 3
205
+ else
206
+ options['priority'].to_i
207
+ end
208
+ end
209
+
210
+ puts "Posting to ABS #{req_obj.to_json}" if verbose
211
+
212
+ # os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
213
+ # raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
214
+ puts "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
215
+ res = conn.post 'request', req_obj.to_json
216
+
217
+ retries = 360
218
+
219
+ raise AuthError, "HTTP #{res.status}: The token provided could not authenticate to the pooler.\n#{res_body}" if res.status == 401
220
+
221
+ (1..retries).each do |i|
222
+ queue_place, res_body = check_queue(conn, saved_job_id, req_obj)
223
+ return translated(res_body) if res_body
224
+
225
+ sleep_seconds = 10 if i >= 10
226
+ sleep_seconds = i if i < 10
227
+ puts "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
228
+
229
+ sleep(sleep_seconds)
230
+ end
231
+ nil
232
+ end
233
+
234
+ #
235
+ # We should fix the ABS API to be more like the vmpooler or nspooler api, but for now
236
+ #
237
+ def self.translated(res_body)
238
+ vmpooler_formatted_body = {}
239
+
240
+ res_body.each do |host|
241
+ if vmpooler_formatted_body[host['type']] && vmpooler_formatted_body[host['type']]['hostname'].class == Array
242
+ vmpooler_formatted_body[host['type']]['hostname'] << host['hostname']
243
+ else
244
+ vmpooler_formatted_body[host['type']] = { 'hostname' => [host['hostname']] }
245
+ end
246
+ end
247
+ vmpooler_formatted_body['ok'] = true
248
+
249
+ vmpooler_formatted_body
250
+ end
251
+
252
+ def self.check_queue(conn, job_id, req_obj)
253
+ queue_info_res = conn.get "status/queue/info/#{job_id}"
254
+ queue_info = JSON.parse(queue_info_res.body)
255
+
256
+ res = conn.post 'request', req_obj.to_json
257
+
258
+ unless res.body.empty?
259
+ res_body = JSON.parse(res.body)
260
+ return queue_info['queue_place'], res_body
261
+ end
262
+ [queue_info['queue_place'], nil]
263
+ end
264
+
265
+ def self.snapshot(_verbose, _url, _hostname, _token)
266
+ puts "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
267
+ end
268
+
269
+ def self.status(verbose, url)
270
+ conn = Http.get_conn(verbose, url)
271
+
272
+ res = conn.get 'status'
273
+
274
+ res.body == 'OK'
275
+ end
276
+
277
+ def self.summary(verbose, url)
278
+ conn = Http.get_conn(verbose, url)
279
+
280
+ res = conn.get 'summary'
281
+ JSON.parse(res.body)
282
+ end
283
+
284
+ def self.query(verbose, url, hostname)
285
+ return @active_hostnames if @active_hostnames
286
+
287
+ puts "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
288
+ conn = Http.get_conn(verbose, url)
289
+
290
+ res = conn.get "host/#{hostname}"
291
+ JSON.parse(res.body)
292
+ end
293
+ end