vmfloaty 0.9.2 → 1.1.1

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'vmfloaty/errors'
4
4
  require 'vmfloaty/http'
5
+ require 'vmfloaty/utils'
5
6
  require 'faraday'
6
7
  require 'json'
7
8
 
@@ -36,39 +37,61 @@ class ABS
36
37
  # }
37
38
  # }
38
39
  #
39
-
40
40
  @active_hostnames = {}
41
41
 
42
- def self.list_active(verbose, url, _token, user)
43
- all_jobs = []
42
+ def self.list_active_job_ids(verbose, url, user)
43
+ all_job_ids = []
44
44
  @active_hostnames = {}
45
+ get_active_requests(verbose, url, user).each do |req_hash|
46
+ @active_hostnames[req_hash['request']['job']['id']] = req_hash # full hash saved for later retrieval
47
+ all_job_ids.push(req_hash['request']['job']['id'])
48
+ end
49
+
50
+ all_job_ids
51
+ end
45
52
 
53
+ def self.list_active(verbose, url, _token, user)
54
+ hosts = []
46
55
  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
56
+ if req_hash.key?('allocated_resources')
57
+ req_hash['allocated_resources'].each do |onehost|
58
+ hosts.push(onehost['hostname'])
59
+ end
60
+ end
49
61
  end
50
62
 
51
- all_jobs
63
+ hosts
52
64
  end
53
65
 
54
66
  def self.get_active_requests(verbose, url, user)
55
- conn = Http.get_conn(verbose, url)
67
+ conn = Http.get_conn(verbose, supported_abs_url(url))
56
68
  res = conn.get 'status/queue'
57
- requests = JSON.parse(res.body)
69
+ if valid_json?(res.body)
70
+ requests = JSON.parse(res.body)
71
+ else
72
+ FloatyLogger.warn "Warning: couldn't parse body returned from abs/status/queue"
73
+ end
58
74
 
59
75
  ret_val = []
60
76
 
61
77
  requests.each do |req|
62
78
  next if req == 'null'
63
79
 
64
- req_hash = JSON.parse(req)
80
+ if valid_json?(req) # legacy ABS had another JSON string always-be-scheduling/pull/306
81
+ req_hash = JSON.parse(req)
82
+ elsif req.is_a?(Hash)
83
+ req_hash = req
84
+ else
85
+ FloatyLogger.warn "Warning: couldn't parse request returned from abs/status/queue"
86
+ next
87
+ end
65
88
 
66
89
  begin
67
90
  next unless user == req_hash['request']['job']['user']
68
91
 
69
92
  ret_val.push(req_hash)
70
93
  rescue NoMethodError
71
- puts "Warning: couldn't parse line returned from abs/status/queue: ".yellow
94
+ FloatyLogger.warn "Warning: couldn't parse user returned from abs/status/queue: "
72
95
  end
73
96
  end
74
97
 
@@ -82,10 +105,10 @@ class ABS
82
105
 
83
106
  def self.delete(verbose, url, hosts, token, user)
84
107
  # In ABS terms, this is a "returned" host.
85
- conn = Http.get_conn(verbose, url)
108
+ conn = Http.get_conn(verbose, supported_abs_url(url))
86
109
  conn.headers['X-AUTH-TOKEN'] = token if token
87
110
 
88
- puts "Trying to delete hosts #{hosts}" if verbose
111
+ FloatyLogger.info "Trying to delete hosts #{hosts}" if verbose
89
112
  requests = get_active_requests(verbose, url, user)
90
113
 
91
114
  jobs_to_delete = []
@@ -113,7 +136,7 @@ class ABS
113
136
  }
114
137
  jobs_to_delete.push(req_hash)
115
138
  else
116
- 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']}"
139
+ FloatyLogger.info "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']}"
117
140
  end
118
141
  end
119
142
  end
@@ -127,7 +150,7 @@ class ABS
127
150
  'hosts' => job['allocated_resources'],
128
151
  }
129
152
 
130
- puts "Deleting #{req_obj}" if verbose
153
+ FloatyLogger.info "Deleting #{req_obj}" if verbose
131
154
 
132
155
  return_result = conn.post 'return', req_obj.to_json
133
156
  req_obj['hosts'].each do |host|
@@ -140,27 +163,64 @@ class ABS
140
163
 
141
164
  # List available VMs in ABS
142
165
  def self.list(verbose, url, os_filter = nil)
143
- conn = Http.get_conn(verbose, url)
166
+ conn = Http.get_conn(verbose, supported_abs_url(url))
144
167
 
145
168
  os_list = []
146
169
 
147
170
  res = conn.get 'status/platforms/vmpooler'
171
+ if valid_json?(res.body)
172
+ res_body = JSON.parse(res.body)
173
+ if res_body.key?('vmpooler_platforms')
174
+ os_list << '*** VMPOOLER Pools ***'
175
+ if res_body['vmpooler_platforms'].is_a?(String)
176
+ os_list += JSON.parse(res_body['vmpooler_platforms']) # legacy ABS had another JSON string always-be-scheduling/pull/306
177
+ else
178
+ os_list += res_body['vmpooler_platforms']
179
+ end
180
+ end
181
+ end
148
182
 
149
- res_body = JSON.parse(res.body)
150
- os_list << '*** VMPOOLER Pools ***'
151
- os_list += JSON.parse(res_body['vmpooler_platforms'])
183
+ res = conn.get 'status/platforms/ondemand_vmpooler'
184
+ if valid_json?(res.body)
185
+ res_body = JSON.parse(res.body)
186
+ if res_body.key?('ondemand_vmpooler_platforms') && res_body['ondemand_vmpooler_platforms'] != '[]'
187
+ os_list << ''
188
+ os_list << '*** VMPOOLER ONDEMAND Pools ***'
189
+ if res_body['ondemand_vmpooler_platforms'].is_a?(String)
190
+ os_list += JSON.parse(res_body['ondemand_vmpooler_platforms']) # legacy ABS had another JSON string always-be-scheduling/pull/306
191
+ else
192
+ os_list += res_body['ondemand_vmpooler_platforms']
193
+ end
194
+ end
195
+ end
152
196
 
153
197
  res = conn.get 'status/platforms/nspooler'
154
- res_body = JSON.parse(res.body)
155
- os_list << ''
156
- os_list << '*** NSPOOLER Pools ***'
157
- os_list += JSON.parse(res_body['nspooler_platforms'])
198
+ if valid_json?(res.body)
199
+ res_body = JSON.parse(res.body)
200
+ if res_body.key?('nspooler_platforms')
201
+ os_list << ''
202
+ os_list << '*** NSPOOLER Pools ***'
203
+ if res_body['nspooler_platforms'].is_a?(String)
204
+ os_list += JSON.parse(res_body['nspooler_platforms']) # legacy ABS had another JSON string always-be-scheduling/pull/306
205
+ else
206
+ os_list += res_body['nspooler_platforms']
207
+ end
208
+ end
209
+ end
158
210
 
159
211
  res = conn.get 'status/platforms/aws'
160
- res_body = JSON.parse(res.body)
161
- os_list << ''
162
- os_list << '*** AWS Pools ***'
163
- os_list += JSON.parse(res_body['aws_platforms'])
212
+ if valid_json?(res.body)
213
+ res_body = JSON.parse(res.body)
214
+ if res_body.key?('aws_platforms')
215
+ os_list << ''
216
+ os_list << '*** AWS Pools ***'
217
+ if res_body['aws_platforms'].is_a?(String)
218
+ os_list += JSON.parse(res_body['aws_platforms']) # legacy ABS had another JSON string always-be-scheduling/pull/306
219
+ else
220
+ os_list += res_body['aws_platforms']
221
+ end
222
+ end
223
+ end
164
224
 
165
225
  os_list.delete 'ok'
166
226
 
@@ -168,7 +228,7 @@ class ABS
168
228
  end
169
229
 
170
230
  # Retrieve an OS from ABS.
171
- def self.retrieve(verbose, os_types, token, url, user, options)
231
+ def self.retrieve(verbose, os_types, token, url, user, config, _ondemand = nil)
172
232
  #
173
233
  # Contents of post must be like:
174
234
  #
@@ -185,11 +245,10 @@ class ABS
185
245
  # }
186
246
  # }
187
247
 
188
- conn = Http.get_conn(verbose, url)
248
+ conn = Http.get_conn(verbose, supported_abs_url(url))
189
249
  conn.headers['X-AUTH-TOKEN'] = token if token
190
250
 
191
- saved_job_id = DateTime.now.strftime('%Q')
192
-
251
+ saved_job_id = user + "-" + DateTime.now.strftime('%Q')
193
252
  req_obj = {
194
253
  :resources => os_types,
195
254
  :job => {
@@ -200,38 +259,49 @@ class ABS
200
259
  },
201
260
  }
202
261
 
203
- if options['priority']
204
- req_obj[:priority] = if options['priority'] == 'high'
262
+ if config['vmpooler_fallback'] # optional and not available as cli flag
263
+ vmpooler_config = Utils.get_vmpooler_service_config(config['vmpooler_fallback'])
264
+ # request with this token, on behalf of this user
265
+ req_obj[:vm_token] = vmpooler_config['token']
266
+ end
267
+
268
+ if config['priority']
269
+ req_obj[:priority] = if config['priority'] == 'high'
205
270
  1
206
- elsif options['priority'] == 'medium'
271
+ elsif config['priority'] == 'medium'
207
272
  2
208
- elsif options['priority'] == 'low'
273
+ elsif config['priority'] == 'low'
209
274
  3
210
275
  else
211
- options['priority'].to_i
276
+ config['priority'].to_i
212
277
  end
213
278
  end
214
279
 
215
- puts "Posting to ABS #{req_obj.to_json}" if verbose
280
+ FloatyLogger.info "Posting to ABS #{req_obj.to_json}" if verbose
216
281
 
217
282
  # os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
218
283
  # raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
219
- puts "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
284
+ FloatyLogger.info "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
220
285
  res = conn.post 'request', req_obj.to_json
221
286
 
222
287
  retries = 360
223
288
 
224
- raise AuthError, "HTTP #{res.status}: The token provided could not authenticate to the pooler.\n#{res_body}" if res.status == 401
289
+ validate_queue_status_response(res.status, res.body, "Initial request", verbose)
225
290
 
226
- (1..retries).each do |i|
227
- queue_place, res_body = check_queue(conn, saved_job_id, req_obj)
228
- return translated(res_body) if res_body
291
+ begin
292
+ (1..retries).each do |i|
293
+ queue_place, res_body = check_queue(conn, saved_job_id, req_obj, verbose)
294
+ return translated(res_body, saved_job_id) if res_body
229
295
 
230
- sleep_seconds = 10 if i >= 10
231
- sleep_seconds = i if i < 10
232
- puts "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
296
+ sleep_seconds = 10 if i >= 10
297
+ sleep_seconds = i if i < 10
298
+ FloatyLogger.info "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
233
299
 
234
- sleep(sleep_seconds)
300
+ sleep(sleep_seconds)
301
+ end
302
+ rescue SystemExit, Interrupt
303
+ FloatyLogger.info "\n\nFloaty interrupted, you can query the state of your request via\n1) `floaty query #{saved_job_id}` or delete it via\n2) `floaty delete #{saved_job_id}`"
304
+ exit 1
235
305
  end
236
306
  nil
237
307
  end
@@ -239,8 +309,8 @@ class ABS
239
309
  #
240
310
  # We should fix the ABS API to be more like the vmpooler or nspooler api, but for now
241
311
  #
242
- def self.translated(res_body)
243
- vmpooler_formatted_body = {}
312
+ def self.translated(res_body, job_id)
313
+ vmpooler_formatted_body = {'job_id' => job_id}
244
314
 
245
315
  res_body.each do |host|
246
316
  if vmpooler_formatted_body[host['type']] && vmpooler_formatted_body[host['type']]['hostname'].class == Array
@@ -254,13 +324,19 @@ class ABS
254
324
  vmpooler_formatted_body
255
325
  end
256
326
 
257
- def self.check_queue(conn, job_id, req_obj)
327
+ def self.check_queue(conn, job_id, req_obj, verbose)
258
328
  queue_info_res = conn.get "status/queue/info/#{job_id}"
259
- queue_info = JSON.parse(queue_info_res.body)
329
+ if valid_json?(queue_info_res.body)
330
+ queue_info = JSON.parse(queue_info_res.body)
331
+ else
332
+ FloatyLogger.warn "Could not parse the status/queue/info/#{job_id}"
333
+ return [nil, nil]
334
+ end
260
335
 
261
336
  res = conn.post 'request', req_obj.to_json
337
+ validate_queue_status_response(res.status, res.body, "Check queue request", verbose)
262
338
 
263
- unless res.body.empty?
339
+ unless res.body.empty? || !valid_json?(res.body)
264
340
  res_body = JSON.parse(res.body)
265
341
  return queue_info['queue_place'], res_body
266
342
  end
@@ -268,11 +344,11 @@ class ABS
268
344
  end
269
345
 
270
346
  def self.snapshot(_verbose, _url, _hostname, _token)
271
- puts "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
347
+ raise NoMethodError, "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
272
348
  end
273
349
 
274
350
  def self.status(verbose, url)
275
- conn = Http.get_conn(verbose, url)
351
+ conn = Http.get_conn(verbose, supported_abs_url(url))
276
352
 
277
353
  res = conn.get 'status'
278
354
 
@@ -280,20 +356,24 @@ class ABS
280
356
  end
281
357
 
282
358
  def self.summary(verbose, url)
283
- conn = Http.get_conn(verbose, url)
284
-
285
- res = conn.get 'summary'
286
- JSON.parse(res.body)
359
+ raise NoMethodError, 'summary is not defined for ABS'
287
360
  end
288
361
 
289
- def self.query(verbose, url, hostname)
290
- return @active_hostnames if @active_hostnames
291
-
292
- puts "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
293
- conn = Http.get_conn(verbose, url)
362
+ def self.query(verbose, url, job_id)
363
+ # return saved hostnames from the last time list_active was run
364
+ # preventing having to query the API again.
365
+ # This works as long as query is called after list_active
366
+ return @active_hostnames if @active_hostnames && !@active_hostnames.empty?
294
367
 
295
- res = conn.get "host/#{hostname}"
296
- JSON.parse(res.body)
368
+ # If using the cli query job_id
369
+ conn = Http.get_conn(verbose, supported_abs_url(url))
370
+ queue_info_res = conn.get "status/queue/info/#{job_id}"
371
+ if valid_json?(queue_info_res.body)
372
+ queue_info = JSON.parse(queue_info_res.body)
373
+ else
374
+ FloatyLogger.warn "Could not parse the status/queue/info/#{job_id}"
375
+ end
376
+ queue_info
297
377
  end
298
378
 
299
379
  def self.modify(_verbose, _url, _hostname, _token, _modify_hash)
@@ -307,4 +387,39 @@ class ABS
307
387
  def self.revert(_verbose, _url, _hostname, _token, _snapshot_sha)
308
388
  raise NoMethodError, 'revert is not defined for ABS'
309
389
  end
390
+
391
+ # Validate the http code returned during a queue status request.
392
+ #
393
+ # Return a success message that can be displayed if the status code is
394
+ # success, otherwise raise an error.
395
+ def self.validate_queue_status_response(status_code, body, request_name, verbose)
396
+ case status_code
397
+ when 200
398
+ "#{request_name} returned success (Code 200)" if verbose
399
+ when 202
400
+ "#{request_name} returned accepted, processing (Code 202)" if verbose
401
+ when 401
402
+ raise AuthError, "HTTP #{status_code}: The token provided could not authenticate.\n#{body}"
403
+ else
404
+ raise "HTTP #{status_code}: #{request_name} request to ABS failed!\n#{body}"
405
+ end
406
+ end
407
+
408
+ def self.valid_json?(json)
409
+ JSON.parse(json)
410
+ return true
411
+ rescue TypeError, JSON::ParserError => e
412
+ return false
413
+ end
414
+
415
+ # when missing, adds the required api/v2 in the url
416
+ def self.supported_abs_url(url)
417
+ expected_ending = "api/v2"
418
+ if !url.include?(expected_ending)
419
+ # add a slash if missing
420
+ expected_ending = "/#{expected_ending}" if url[-1] != "/"
421
+ url = "#{url}#{expected_ending}"
422
+ end
423
+ url
424
+ end
310
425
  end
@@ -8,7 +8,7 @@ class Conf
8
8
  begin
9
9
  conf = YAML.load_file("#{Dir.home}/.vmfloaty.yml")
10
10
  rescue StandardError
11
- STDERR.puts "WARNING: There was no config file at #{Dir.home}/.vmfloaty.yml"
11
+ # ignore
12
12
  end
13
13
  conf
14
14
  end
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+
3
+ class FloatyLogger < ::Logger
4
+ def self.logger
5
+ @@logger ||= FloatyLogger.new
6
+ end
7
+
8
+ def self.info(msg)
9
+ FloatyLogger.logger.info msg
10
+ end
11
+
12
+ def self.warn(msg)
13
+ FloatyLogger.logger.warn msg
14
+ end
15
+
16
+ def self.error(msg)
17
+ FloatyLogger.logger.error msg
18
+ end
19
+
20
+ def initialize
21
+ super(STDERR)
22
+ self.level = ::Logger::INFO
23
+ self.formatter = proc do |severity, datetime, progname, msg|
24
+ "#{msg}\n"
25
+ end
26
+ end
27
+ end
@@ -22,7 +22,7 @@ class NonstandardPooler
22
22
  status['reserved_hosts'] || []
23
23
  end
24
24
 
25
- def self.retrieve(verbose, os_type, token, url, _user, _options)
25
+ def self.retrieve(verbose, os_type, token, url, _user, _options, ondemand = nil)
26
26
  conn = Http.get_conn(verbose, url)
27
27
  conn.headers['X-AUTH-TOKEN'] = token if token
28
28
 
@@ -28,7 +28,7 @@ class Pooler
28
28
  vms
29
29
  end
30
30
 
31
- def self.retrieve(verbose, os_type, token, url, _user, _options)
31
+ def self.retrieve(verbose, os_type, token, url, _user, _options, ondemand = nil)
32
32
  # NOTE:
33
33
  # Developers can use `Utils.generate_os_hash` to
34
34
  # generate the os_type param.
@@ -38,7 +38,8 @@ class Pooler
38
38
  os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
39
39
  raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
40
40
 
41
- response = conn.post "vm/#{os_string}"
41
+ response = conn.post "vm/#{os_string}" unless ondemand
42
+ response ||= conn.post "ondemandvm/#{os_string}"
42
43
 
43
44
  res_body = JSON.parse(response.body)
44
45
 
@@ -46,11 +47,40 @@ class Pooler
46
47
  res_body
47
48
  elsif response.status == 401
48
49
  raise AuthError, "HTTP #{response.status}: The token provided could not authenticate to the pooler.\n#{res_body}"
50
+ elsif response.status == 403
51
+ raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/vm/#{os_string}. Request exceeds the configured per pool maximum. #{res_body}"
49
52
  else
50
- raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/vm/#{os_string}. #{res_body}"
53
+ raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/vm/#{os_string}. #{res_body}" unless ondemand
54
+ raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/ondemandvm/#{os_string}. #{res_body}"
51
55
  end
52
56
  end
53
57
 
58
+ def self.wait_for_request(verbose, request_id, url, timeout = 300)
59
+ start_time = Time.now
60
+ while check_ondemandvm(verbose, request_id, url) == false
61
+ return false if (Time.now - start_time).to_i > timeout
62
+
63
+ FloatyLogger.info "waiting for request #{request_id} to be fulfilled"
64
+ sleep 5
65
+ end
66
+ FloatyLogger.info "The request has been fulfilled"
67
+ check_ondemandvm(verbose, request_id, url)
68
+ end
69
+
70
+ def self.check_ondemandvm(verbose, request_id, url)
71
+ conn = Http.get_conn(verbose, url)
72
+
73
+ response = conn.get "ondemandvm/#{request_id}"
74
+ res_body = JSON.parse(response.body)
75
+ return res_body if response.status == 200
76
+
77
+ return false if response.status == 202
78
+
79
+ raise "HTTP #{response.status}: The request cannot be found, or an unknown error occurred" if response.status == 404
80
+
81
+ false
82
+ end
83
+
54
84
  def self.modify(verbose, url, hostname, token, modify_hash)
55
85
  raise TokenError, 'Token provided was nil. Request cannot be made to modify vm' if token.nil?
56
86