vmfloaty 0.9.2 → 1.1.1

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