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