vmfloaty 0.9.2 → 1.1.1
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 +17 -9
- data/extras/completions/floaty.zsh +43 -0
- data/lib/vmfloaty.rb +151 -39
- data/lib/vmfloaty/abs.rb +178 -63
- 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 +31 -16
- data/lib/vmfloaty/utils.rb +128 -33
- data/lib/vmfloaty/version.rb +2 -1
- data/spec/spec_helper.rb +11 -0
- data/spec/vmfloaty/abs_spec.rb +55 -8
- data/spec/vmfloaty/pooler_spec.rb +20 -0
- data/spec/vmfloaty/utils_spec.rb +451 -73
- metadata +23 -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
|
-
conn = Http.get_conn(verbose, url)
|
67
|
+
conn = Http.get_conn(verbose, supported_abs_url(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
|
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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,
|
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
|
204
|
-
|
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
|
271
|
+
elsif config['priority'] == 'medium'
|
207
272
|
2
|
208
|
-
elsif
|
273
|
+
elsif config['priority'] == 'low'
|
209
274
|
3
|
210
275
|
else
|
211
|
-
|
276
|
+
config['priority'].to_i
|
212
277
|
end
|
213
278
|
end
|
214
279
|
|
215
|
-
|
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
|
-
|
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
|
-
|
289
|
+
validate_queue_status_response(res.status, res.body, "Initial request", verbose)
|
225
290
|
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
290
|
-
return
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
296
|
-
|
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
|
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
|
|