vmfloaty 0.9.1 → 1.0.0

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
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