vmfloaty 0.9.1 → 1.0.0

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
67
  conn = Http.get_conn(verbose, 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
 
@@ -85,7 +108,7 @@ class ABS
85
108
  conn = Http.get_conn(verbose, 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 = []
@@ -100,6 +123,11 @@ class ABS
100
123
  requests.each do |req_hash|
101
124
  next unless req_hash['state'] == 'allocated' || req_hash['state'] == 'filled'
102
125
 
126
+ if hosts.include? req_hash['request']['job']['id']
127
+ jobs_to_delete.push(req_hash)
128
+ next
129
+ end
130
+
103
131
  req_hash['allocated_resources'].each do |vm_name, _i|
104
132
  if hosts.include? vm_name['hostname']
105
133
  if all_job_resources_accounted_for(req_hash['allocated_resources'], hosts)
@@ -108,7 +136,7 @@ class ABS
108
136
  }
109
137
  jobs_to_delete.push(req_hash)
110
138
  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']}"
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']}"
112
140
  end
113
141
  end
114
142
  end
@@ -122,7 +150,7 @@ class ABS
122
150
  'hosts' => job['allocated_resources'],
123
151
  }
124
152
 
125
- puts "Deleting #{req_obj}" if verbose
153
+ FloatyLogger.info "Deleting #{req_obj}" if verbose
126
154
 
127
155
  return_result = conn.post 'return', req_obj.to_json
128
156
  req_obj['hosts'].each do |host|
@@ -140,22 +168,59 @@ class ABS
140
168
  os_list = []
141
169
 
142
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
143
182
 
144
- res_body = JSON.parse(res.body)
145
- os_list << '*** VMPOOLER Pools ***'
146
- 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
147
196
 
148
197
  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'])
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
153
210
 
154
211
  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'])
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
159
224
 
160
225
  os_list.delete 'ok'
161
226
 
@@ -163,7 +228,7 @@ class ABS
163
228
  end
164
229
 
165
230
  # Retrieve an OS from ABS.
166
- def self.retrieve(verbose, os_types, token, url, user, options)
231
+ def self.retrieve(verbose, os_types, token, url, user, config, _ondemand = nil)
167
232
  #
168
233
  # Contents of post must be like:
169
234
  #
@@ -184,7 +249,7 @@ class ABS
184
249
  conn.headers['X-AUTH-TOKEN'] = token if token
185
250
 
186
251
  saved_job_id = DateTime.now.strftime('%Q')
187
-
252
+ vmpooler_config = Utils.get_vmpooler_service_config(config['vmpooler_fallback'])
188
253
  req_obj = {
189
254
  :resources => os_types,
190
255
  :job => {
@@ -193,38 +258,39 @@ class ABS
193
258
  :user => user,
194
259
  },
195
260
  },
261
+ :vm_token => vmpooler_config['token'] # request with this token, on behalf of this user
196
262
  }
197
263
 
198
- if options['priority']
199
- req_obj[:priority] = if options['priority'] == 'high'
264
+ if config['priority']
265
+ req_obj[:priority] = if config['priority'] == 'high'
200
266
  1
201
- elsif options['priority'] == 'medium'
267
+ elsif config['priority'] == 'medium'
202
268
  2
203
- elsif options['priority'] == 'low'
269
+ elsif config['priority'] == 'low'
204
270
  3
205
271
  else
206
- options['priority'].to_i
272
+ config['priority'].to_i
207
273
  end
208
274
  end
209
275
 
210
- puts "Posting to ABS #{req_obj.to_json}" if verbose
276
+ FloatyLogger.info "Posting to ABS #{req_obj.to_json}" if verbose
211
277
 
212
278
  # os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
213
279
  # 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."
280
+ FloatyLogger.info "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
215
281
  res = conn.post 'request', req_obj.to_json
216
282
 
217
283
  retries = 360
218
284
 
219
- raise AuthError, "HTTP #{res.status}: The token provided could not authenticate to the pooler.\n#{res_body}" if res.status == 401
285
+ validate_queue_status_response(res.status, res.body, "Initial request", verbose)
220
286
 
221
287
  (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
288
+ queue_place, res_body = check_queue(conn, saved_job_id, req_obj, verbose)
289
+ return translated(res_body, saved_job_id) if res_body
224
290
 
225
291
  sleep_seconds = 10 if i >= 10
226
292
  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})"
293
+ FloatyLogger.info "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
228
294
 
229
295
  sleep(sleep_seconds)
230
296
  end
@@ -234,8 +300,8 @@ class ABS
234
300
  #
235
301
  # We should fix the ABS API to be more like the vmpooler or nspooler api, but for now
236
302
  #
237
- def self.translated(res_body)
238
- vmpooler_formatted_body = {}
303
+ def self.translated(res_body, job_id)
304
+ vmpooler_formatted_body = {'job_id' => job_id}
239
305
 
240
306
  res_body.each do |host|
241
307
  if vmpooler_formatted_body[host['type']] && vmpooler_formatted_body[host['type']]['hostname'].class == Array
@@ -249,13 +315,19 @@ class ABS
249
315
  vmpooler_formatted_body
250
316
  end
251
317
 
252
- def self.check_queue(conn, job_id, req_obj)
318
+ def self.check_queue(conn, job_id, req_obj, verbose)
253
319
  queue_info_res = conn.get "status/queue/info/#{job_id}"
254
- queue_info = JSON.parse(queue_info_res.body)
320
+ if valid_json?(queue_info_res.body)
321
+ queue_info = JSON.parse(queue_info_res.body)
322
+ else
323
+ FloatyLogger.warn "Could not parse the status/queue/info/#{job_id}"
324
+ return [nil, nil]
325
+ end
255
326
 
256
327
  res = conn.post 'request', req_obj.to_json
328
+ validate_queue_status_response(res.status, res.body, "Check queue request", verbose)
257
329
 
258
- unless res.body.empty?
330
+ unless res.body.empty? || !valid_json?(res.body)
259
331
  res_body = JSON.parse(res.body)
260
332
  return queue_info['queue_place'], res_body
261
333
  end
@@ -263,7 +335,7 @@ class ABS
263
335
  end
264
336
 
265
337
  def self.snapshot(_verbose, _url, _hostname, _token)
266
- puts "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
338
+ raise NoMethodError, "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
267
339
  end
268
340
 
269
341
  def self.status(verbose, url)
@@ -275,20 +347,24 @@ class ABS
275
347
  end
276
348
 
277
349
  def self.summary(verbose, url)
278
- conn = Http.get_conn(verbose, url)
279
-
280
- res = conn.get 'summary'
281
- JSON.parse(res.body)
350
+ raise NoMethodError, 'summary is not defined for ABS'
282
351
  end
283
352
 
284
- def self.query(verbose, url, hostname)
285
- return @active_hostnames if @active_hostnames
353
+ def self.query(verbose, url, job_id)
354
+ # return saved hostnames from the last time list_active was run
355
+ # preventing having to query the API again.
356
+ # This works as long as query is called after list_active
357
+ return @active_hostnames if @active_hostnames && !@active_hostnames.empty?
286
358
 
287
- puts "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
359
+ # If using the cli query job_id
288
360
  conn = Http.get_conn(verbose, url)
289
-
290
- res = conn.get "host/#{hostname}"
291
- JSON.parse(res.body)
361
+ queue_info_res = conn.get "status/queue/info/#{job_id}"
362
+ if valid_json?(queue_info_res.body)
363
+ queue_info = JSON.parse(queue_info_res.body)
364
+ else
365
+ FloatyLogger.warn "Could not parse the status/queue/info/#{job_id}"
366
+ end
367
+ queue_info
292
368
  end
293
369
 
294
370
  def self.modify(_verbose, _url, _hostname, _token, _modify_hash)
@@ -302,4 +378,28 @@ class ABS
302
378
  def self.revert(_verbose, _url, _hostname, _token, _snapshot_sha)
303
379
  raise NoMethodError, 'revert is not defined for ABS'
304
380
  end
381
+
382
+ # Validate the http code returned during a queue status request.
383
+ #
384
+ # Return a success message that can be displayed if the status code is
385
+ # success, otherwise raise an error.
386
+ def self.validate_queue_status_response(status_code, body, request_name, verbose)
387
+ case status_code
388
+ when 200
389
+ "#{request_name} returned success (Code 200)" if verbose
390
+ when 202
391
+ "#{request_name} returned accepted, processing (Code 202)" if verbose
392
+ when 401
393
+ raise AuthError, "HTTP #{status_code}: The token provided could not authenticate.\n#{body}"
394
+ else
395
+ raise "HTTP #{status_code}: #{request_name} request to ABS failed!\n#{body}"
396
+ end
397
+ end
398
+
399
+ def self.valid_json?(json)
400
+ JSON.parse(json)
401
+ return true
402
+ rescue TypeError, JSON::ParserError => e
403
+ return false
404
+ end
305
405
  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