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.
- checksums.yaml +4 -4
- data/README.md +84 -104
- data/extras/completions/floaty.bash +2 -2
- data/extras/completions/floaty.zsh +37 -0
- data/lib/vmfloaty.rb +154 -40
- data/lib/vmfloaty/abs.rb +152 -52
- data/lib/vmfloaty/conf.rb +1 -1
- data/lib/vmfloaty/logger.rb +27 -0
- data/lib/vmfloaty/nonstandard_pooler.rb +1 -1
- data/lib/vmfloaty/pooler.rb +33 -3
- data/lib/vmfloaty/service.rb +32 -17
- data/lib/vmfloaty/ssh.rb +11 -5
- data/lib/vmfloaty/utils.rb +127 -33
- data/lib/vmfloaty/version.rb +2 -1
- data/spec/spec_helper.rb +11 -0
- data/spec/vmfloaty/abs_spec.rb +83 -6
- data/spec/vmfloaty/pooler_spec.rb +20 -0
- data/spec/vmfloaty/ssh_spec.rb +49 -0
- data/spec/vmfloaty/utils_spec.rb +436 -73
- metadata +24 -13
data/lib/vmfloaty/abs.rb
CHANGED
@@ -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.
|
43
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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,
|
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
|
199
|
-
req_obj[:priority] = if
|
264
|
+
if config['priority']
|
265
|
+
req_obj[:priority] = if config['priority'] == 'high'
|
200
266
|
1
|
201
|
-
elsif
|
267
|
+
elsif config['priority'] == 'medium'
|
202
268
|
2
|
203
|
-
elsif
|
269
|
+
elsif config['priority'] == 'low'
|
204
270
|
3
|
205
271
|
else
|
206
|
-
|
272
|
+
config['priority'].to_i
|
207
273
|
end
|
208
274
|
end
|
209
275
|
|
210
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
285
|
-
return
|
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
|
-
|
359
|
+
# If using the cli query job_id
|
288
360
|
conn = Http.get_conn(verbose, url)
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
data/lib/vmfloaty/conf.rb
CHANGED
@@ -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
|
|
data/lib/vmfloaty/pooler.rb
CHANGED
@@ -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
|
|