vmfloaty 0.8.1 → 0.10.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 +5 -5
- data/README.md +96 -55
- data/bin/floaty +2 -1
- data/lib/vmfloaty.rb +60 -53
- data/lib/vmfloaty/abs.rb +318 -0
- data/lib/vmfloaty/auth.rb +14 -22
- data/lib/vmfloaty/conf.rb +3 -2
- data/lib/vmfloaty/errors.rb +6 -4
- data/lib/vmfloaty/http.rb +14 -25
- data/lib/vmfloaty/nonstandard_pooler.rb +15 -31
- data/lib/vmfloaty/pooler.rb +64 -55
- data/lib/vmfloaty/service.rb +25 -17
- data/lib/vmfloaty/ssh.rb +25 -25
- data/lib/vmfloaty/utils.rb +103 -97
- data/lib/vmfloaty/version.rb +3 -1
- data/spec/spec_helper.rb +13 -0
- data/spec/vmfloaty/abs/auth_spec.rb +84 -0
- data/spec/vmfloaty/abs_spec.rb +126 -0
- data/spec/vmfloaty/auth_spec.rb +39 -43
- data/spec/vmfloaty/nonstandard_pooler_spec.rb +132 -146
- data/spec/vmfloaty/pooler_spec.rb +121 -101
- data/spec/vmfloaty/service_spec.rb +17 -17
- data/spec/vmfloaty/ssh_spec.rb +49 -0
- data/spec/vmfloaty/utils_spec.rb +123 -98
- data/spec/vmfloaty/vmfloaty_services_spec.rb +39 -0
- metadata +38 -22
data/lib/vmfloaty/abs.rb
ADDED
@@ -0,0 +1,318 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'vmfloaty/errors'
|
4
|
+
require 'vmfloaty/http'
|
5
|
+
require 'faraday'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
class ABS
|
9
|
+
# List active VMs in ABS
|
10
|
+
# This is what a job request looks like:
|
11
|
+
# {
|
12
|
+
# "state":"filled",
|
13
|
+
# "last_processed":"2019-10-31 20:59:33 +0000",
|
14
|
+
# "allocated_resources": [
|
15
|
+
# {
|
16
|
+
# "hostname":"h3oyntawjm7xdch.delivery.puppetlabs.net",
|
17
|
+
# "type":"centos-7.2-tmpfs-x86_64",
|
18
|
+
# "engine":"vmpooler"}
|
19
|
+
# ],
|
20
|
+
# "audit_log":{
|
21
|
+
# "2019-10-30 20:33:12 +0000":"Allocated h3oyntawjm7xdch.delivery.puppetlabs.net for job 1572467589"
|
22
|
+
# },
|
23
|
+
# "request":{
|
24
|
+
# "resources":{
|
25
|
+
# "centos-7.2-tmpfs-x86_64":1
|
26
|
+
# },
|
27
|
+
# "job": {
|
28
|
+
# "id":1572467589,
|
29
|
+
# "tags": {
|
30
|
+
# "user":"mikker",
|
31
|
+
# "url_string":"floaty://mikker/1572467589"
|
32
|
+
# },
|
33
|
+
# "user":"mikker",
|
34
|
+
# "time-received":1572467589
|
35
|
+
# }
|
36
|
+
# }
|
37
|
+
# }
|
38
|
+
#
|
39
|
+
|
40
|
+
@active_hostnames = {}
|
41
|
+
|
42
|
+
def self.list_active(verbose, url, _token, user)
|
43
|
+
all_jobs = []
|
44
|
+
@active_hostnames = {}
|
45
|
+
|
46
|
+
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
|
49
|
+
end
|
50
|
+
|
51
|
+
all_jobs
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.get_active_requests(verbose, url, user)
|
55
|
+
conn = Http.get_conn(verbose, url)
|
56
|
+
res = conn.get 'status/queue'
|
57
|
+
requests = JSON.parse(res.body)
|
58
|
+
|
59
|
+
ret_val = []
|
60
|
+
|
61
|
+
requests.each do |req|
|
62
|
+
next if req == 'null'
|
63
|
+
|
64
|
+
req_hash = JSON.parse(req)
|
65
|
+
|
66
|
+
begin
|
67
|
+
next unless user == req_hash['request']['job']['user']
|
68
|
+
|
69
|
+
ret_val.push(req_hash)
|
70
|
+
rescue NoMethodError
|
71
|
+
puts "Warning: couldn't parse line returned from abs/status/queue: ".yellow
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
ret_val
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.all_job_resources_accounted_for(allocated_resources, hosts)
|
79
|
+
allocated_host_list = allocated_resources.map { |ar| ar['hostname'] }
|
80
|
+
(allocated_host_list - hosts).empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.delete(verbose, url, hosts, token, user)
|
84
|
+
# In ABS terms, this is a "returned" host.
|
85
|
+
conn = Http.get_conn(verbose, url)
|
86
|
+
conn.headers['X-AUTH-TOKEN'] = token if token
|
87
|
+
|
88
|
+
puts "Trying to delete hosts #{hosts}" if verbose
|
89
|
+
requests = get_active_requests(verbose, url, user)
|
90
|
+
|
91
|
+
jobs_to_delete = []
|
92
|
+
|
93
|
+
ret_status = {}
|
94
|
+
hosts.each do |host|
|
95
|
+
ret_status[host] = {
|
96
|
+
'ok' => false,
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
requests.each do |req_hash|
|
101
|
+
next unless req_hash['state'] == 'allocated' || req_hash['state'] == 'filled'
|
102
|
+
|
103
|
+
if hosts.include? req_hash['request']['job']['id']
|
104
|
+
jobs_to_delete.push(req_hash)
|
105
|
+
next
|
106
|
+
end
|
107
|
+
|
108
|
+
req_hash['allocated_resources'].each do |vm_name, _i|
|
109
|
+
if hosts.include? vm_name['hostname']
|
110
|
+
if all_job_resources_accounted_for(req_hash['allocated_resources'], hosts)
|
111
|
+
ret_status[vm_name['hostname']] = {
|
112
|
+
'ok' => true,
|
113
|
+
}
|
114
|
+
jobs_to_delete.push(req_hash)
|
115
|
+
else
|
116
|
+
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']}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
response_body = {}
|
123
|
+
|
124
|
+
jobs_to_delete.each do |job|
|
125
|
+
req_obj = {
|
126
|
+
'job_id' => job['request']['job']['id'],
|
127
|
+
'hosts' => job['allocated_resources'],
|
128
|
+
}
|
129
|
+
|
130
|
+
puts "Deleting #{req_obj}" if verbose
|
131
|
+
|
132
|
+
return_result = conn.post 'return', req_obj.to_json
|
133
|
+
req_obj['hosts'].each do |host|
|
134
|
+
response_body[host['hostname']] = { 'ok' => true } if return_result.body == 'OK'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
response_body
|
139
|
+
end
|
140
|
+
|
141
|
+
# List available VMs in ABS
|
142
|
+
def self.list(verbose, url, os_filter = nil)
|
143
|
+
conn = Http.get_conn(verbose, url)
|
144
|
+
|
145
|
+
os_list = []
|
146
|
+
|
147
|
+
res = conn.get 'status/platforms/vmpooler'
|
148
|
+
|
149
|
+
res_body = JSON.parse(res.body)
|
150
|
+
os_list << '*** VMPOOLER Pools ***'
|
151
|
+
os_list += JSON.parse(res_body['vmpooler_platforms'])
|
152
|
+
|
153
|
+
res = conn.get 'status/platforms/ondemand_vmpooler'
|
154
|
+
res_body = JSON.parse(res.body)
|
155
|
+
unless res_body['ondemand_vmpooler_platforms'] == '[]'
|
156
|
+
os_list << ''
|
157
|
+
os_list << '*** VMPOOLER ONDEMAND Pools ***'
|
158
|
+
os_list += JSON.parse(res_body['ondemand_vmpooler_platforms'])
|
159
|
+
end
|
160
|
+
|
161
|
+
res = conn.get 'status/platforms/nspooler'
|
162
|
+
res_body = JSON.parse(res.body)
|
163
|
+
os_list << ''
|
164
|
+
os_list << '*** NSPOOLER Pools ***'
|
165
|
+
os_list += JSON.parse(res_body['nspooler_platforms'])
|
166
|
+
|
167
|
+
res = conn.get 'status/platforms/aws'
|
168
|
+
res_body = JSON.parse(res.body)
|
169
|
+
os_list << ''
|
170
|
+
os_list << '*** AWS Pools ***'
|
171
|
+
os_list += JSON.parse(res_body['aws_platforms'])
|
172
|
+
|
173
|
+
os_list.delete 'ok'
|
174
|
+
|
175
|
+
os_filter ? os_list.select { |i| i[/#{os_filter}/] } : os_list
|
176
|
+
end
|
177
|
+
|
178
|
+
# Retrieve an OS from ABS.
|
179
|
+
def self.retrieve(verbose, os_types, token, url, user, options, _ondemand = nil)
|
180
|
+
#
|
181
|
+
# Contents of post must be like:
|
182
|
+
#
|
183
|
+
# {
|
184
|
+
# "resources": {
|
185
|
+
# "centos-7-i386": 1,
|
186
|
+
# "ubuntu-1404-x86_64": 2
|
187
|
+
# },
|
188
|
+
# "job": {
|
189
|
+
# "id": "12345",
|
190
|
+
# "tags": {
|
191
|
+
# "user": "username",
|
192
|
+
# }
|
193
|
+
# }
|
194
|
+
# }
|
195
|
+
|
196
|
+
conn = Http.get_conn(verbose, url)
|
197
|
+
conn.headers['X-AUTH-TOKEN'] = token if token
|
198
|
+
|
199
|
+
saved_job_id = DateTime.now.strftime('%Q')
|
200
|
+
|
201
|
+
req_obj = {
|
202
|
+
:resources => os_types,
|
203
|
+
:job => {
|
204
|
+
:id => saved_job_id,
|
205
|
+
:tags => {
|
206
|
+
:user => user,
|
207
|
+
},
|
208
|
+
},
|
209
|
+
}
|
210
|
+
|
211
|
+
if options['priority']
|
212
|
+
req_obj[:priority] = if options['priority'] == 'high'
|
213
|
+
1
|
214
|
+
elsif options['priority'] == 'medium'
|
215
|
+
2
|
216
|
+
elsif options['priority'] == 'low'
|
217
|
+
3
|
218
|
+
else
|
219
|
+
options['priority'].to_i
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
puts "Posting to ABS #{req_obj.to_json}" if verbose
|
224
|
+
|
225
|
+
# os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
|
226
|
+
# raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
|
227
|
+
puts "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
|
228
|
+
res = conn.post 'request', req_obj.to_json
|
229
|
+
|
230
|
+
retries = 360
|
231
|
+
|
232
|
+
raise AuthError, "HTTP #{res.status}: The token provided could not authenticate to the pooler.\n#{res_body}" if res.status == 401
|
233
|
+
|
234
|
+
(1..retries).each do |i|
|
235
|
+
queue_place, res_body = check_queue(conn, saved_job_id, req_obj)
|
236
|
+
return translated(res_body) if res_body
|
237
|
+
|
238
|
+
sleep_seconds = 10 if i >= 10
|
239
|
+
sleep_seconds = i if i < 10
|
240
|
+
puts "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
|
241
|
+
|
242
|
+
sleep(sleep_seconds)
|
243
|
+
end
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# We should fix the ABS API to be more like the vmpooler or nspooler api, but for now
|
249
|
+
#
|
250
|
+
def self.translated(res_body)
|
251
|
+
vmpooler_formatted_body = {}
|
252
|
+
|
253
|
+
res_body.each do |host|
|
254
|
+
if vmpooler_formatted_body[host['type']] && vmpooler_formatted_body[host['type']]['hostname'].class == Array
|
255
|
+
vmpooler_formatted_body[host['type']]['hostname'] << host['hostname']
|
256
|
+
else
|
257
|
+
vmpooler_formatted_body[host['type']] = { 'hostname' => [host['hostname']] }
|
258
|
+
end
|
259
|
+
end
|
260
|
+
vmpooler_formatted_body['ok'] = true
|
261
|
+
|
262
|
+
vmpooler_formatted_body
|
263
|
+
end
|
264
|
+
|
265
|
+
def self.check_queue(conn, job_id, req_obj)
|
266
|
+
queue_info_res = conn.get "status/queue/info/#{job_id}"
|
267
|
+
queue_info = JSON.parse(queue_info_res.body)
|
268
|
+
|
269
|
+
res = conn.post 'request', req_obj.to_json
|
270
|
+
|
271
|
+
unless res.body.empty?
|
272
|
+
res_body = JSON.parse(res.body)
|
273
|
+
return queue_info['queue_place'], res_body
|
274
|
+
end
|
275
|
+
[queue_info['queue_place'], nil]
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.snapshot(_verbose, _url, _hostname, _token)
|
279
|
+
puts "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.status(verbose, url)
|
283
|
+
conn = Http.get_conn(verbose, url)
|
284
|
+
|
285
|
+
res = conn.get 'status'
|
286
|
+
|
287
|
+
res.body == 'OK'
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.summary(verbose, url)
|
291
|
+
conn = Http.get_conn(verbose, url)
|
292
|
+
|
293
|
+
res = conn.get 'summary'
|
294
|
+
JSON.parse(res.body)
|
295
|
+
end
|
296
|
+
|
297
|
+
def self.query(verbose, url, hostname)
|
298
|
+
return @active_hostnames if @active_hostnames
|
299
|
+
|
300
|
+
puts "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
|
301
|
+
conn = Http.get_conn(verbose, url)
|
302
|
+
|
303
|
+
res = conn.get "host/#{hostname}"
|
304
|
+
JSON.parse(res.body)
|
305
|
+
end
|
306
|
+
|
307
|
+
def self.modify(_verbose, _url, _hostname, _token, _modify_hash)
|
308
|
+
raise NoMethodError, 'modify is not defined for ABS'
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.disk(_verbose, _url, _hostname, _token, _disk)
|
312
|
+
raise NoMethodError, 'disk is not defined for ABS'
|
313
|
+
end
|
314
|
+
|
315
|
+
def self.revert(_verbose, _url, _hostname, _token, _snapshot_sha)
|
316
|
+
raise NoMethodError, 'revert is not defined for ABS'
|
317
|
+
end
|
318
|
+
end
|
data/lib/vmfloaty/auth.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'json'
|
3
5
|
require 'vmfloaty/http'
|
@@ -7,46 +9,36 @@ class Auth
|
|
7
9
|
def self.get_token(verbose, url, user, password)
|
8
10
|
conn = Http.get_conn_with_auth(verbose, url, user, password)
|
9
11
|
|
10
|
-
resp = conn.post
|
12
|
+
resp = conn.post 'token'
|
11
13
|
|
12
14
|
res_body = JSON.parse(resp.body)
|
13
|
-
if res_body[
|
14
|
-
|
15
|
-
|
16
|
-
raise TokenError, "HTTP #{resp.status}: There was a problem requesting a token:\n#{res_body}"
|
17
|
-
end
|
15
|
+
return res_body['token'] if res_body['ok']
|
16
|
+
|
17
|
+
raise TokenError, "HTTP #{resp.status}: There was a problem requesting a token:\n#{res_body}"
|
18
18
|
end
|
19
19
|
|
20
20
|
def self.delete_token(verbose, url, user, password, token)
|
21
|
-
if token.nil?
|
22
|
-
raise TokenError, 'You did not provide a token'
|
23
|
-
end
|
21
|
+
raise TokenError, 'You did not provide a token' if token.nil?
|
24
22
|
|
25
23
|
conn = Http.get_conn_with_auth(verbose, url, user, password)
|
26
24
|
|
27
25
|
response = conn.delete "token/#{token}"
|
28
26
|
res_body = JSON.parse(response.body)
|
29
|
-
if res_body[
|
30
|
-
|
31
|
-
|
32
|
-
raise TokenError, "HTTP #{response.status}: There was a problem deleting a token:\n#{res_body}"
|
33
|
-
end
|
27
|
+
return res_body if res_body['ok']
|
28
|
+
|
29
|
+
raise TokenError, "HTTP #{response.status}: There was a problem deleting a token:\n#{res_body}"
|
34
30
|
end
|
35
31
|
|
36
32
|
def self.token_status(verbose, url, token)
|
37
|
-
if token.nil?
|
38
|
-
raise TokenError, 'You did not provide a token'
|
39
|
-
end
|
33
|
+
raise TokenError, 'You did not provide a token' if token.nil?
|
40
34
|
|
41
35
|
conn = Http.get_conn(verbose, url)
|
42
36
|
|
43
37
|
response = conn.get "token/#{token}"
|
44
38
|
res_body = JSON.parse(response.body)
|
45
39
|
|
46
|
-
if res_body[
|
47
|
-
|
48
|
-
|
49
|
-
raise TokenError, "HTTP #{response.status}: There was a problem getting the status of a token:\n#{res_body}"
|
50
|
-
end
|
40
|
+
return res_body if res_body['ok']
|
41
|
+
|
42
|
+
raise TokenError, "HTTP #{response.status}: There was a problem getting the status of a token:\n#{res_body}"
|
51
43
|
end
|
52
44
|
end
|
data/lib/vmfloaty/conf.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'yaml'
|
2
4
|
|
3
5
|
class Conf
|
4
|
-
|
5
6
|
def self.read_config
|
6
7
|
conf = {}
|
7
8
|
begin
|
8
9
|
conf = YAML.load_file("#{Dir.home}/.vmfloaty.yml")
|
9
|
-
rescue
|
10
|
+
rescue StandardError
|
10
11
|
STDERR.puts "WARNING: There was no config file at #{Dir.home}/.vmfloaty.yml"
|
11
12
|
end
|
12
13
|
conf
|
data/lib/vmfloaty/errors.rb
CHANGED
@@ -1,23 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class AuthError < StandardError
|
2
|
-
def initialize(msg=
|
4
|
+
def initialize(msg = 'Could not authenticate to pooler')
|
3
5
|
super
|
4
6
|
end
|
5
7
|
end
|
6
8
|
|
7
9
|
class TokenError < StandardError
|
8
|
-
def initialize(msg=
|
10
|
+
def initialize(msg = 'Could not do operation with token provided')
|
9
11
|
super
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
13
15
|
class MissingParamError < StandardError
|
14
|
-
def initialize(msg=
|
16
|
+
def initialize(msg = 'Argument provided to function is missing')
|
15
17
|
super
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
19
21
|
class ModifyError < StandardError
|
20
|
-
def initialize(msg=
|
22
|
+
def initialize(msg = 'Could not modify VM')
|
21
23
|
super
|
22
24
|
end
|
23
25
|
end
|
data/lib/vmfloaty/http.rb
CHANGED
@@ -1,60 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'uri'
|
3
5
|
|
4
6
|
class Http
|
5
|
-
def self.
|
7
|
+
def self.url?(url)
|
6
8
|
# This method exists because it seems like Farady
|
7
9
|
# has no handling around if a user gives us a URI
|
8
10
|
# with no protocol on the beginning of the URL
|
9
11
|
|
10
12
|
uri = URI.parse(url)
|
11
13
|
|
12
|
-
if uri.
|
13
|
-
return true
|
14
|
-
end
|
14
|
+
return true if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
15
15
|
|
16
|
-
|
16
|
+
false
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.get_conn(verbose, url)
|
20
|
-
if url.nil?
|
21
|
-
raise "Did not provide a url to connect to"
|
22
|
-
end
|
20
|
+
raise 'Did not provide a url to connect to' if url.nil?
|
23
21
|
|
24
|
-
unless
|
25
|
-
url = "https://#{url}"
|
26
|
-
end
|
22
|
+
url = "https://#{url}" unless url?(url)
|
27
23
|
|
28
|
-
conn = Faraday.new(:url => url, :ssl => {:verify => false}) do |faraday|
|
24
|
+
conn = Faraday.new(:url => url, :ssl => { :verify => false }) do |faraday|
|
29
25
|
faraday.request :url_encoded
|
30
26
|
faraday.response :logger if verbose
|
31
27
|
faraday.adapter Faraday.default_adapter
|
32
28
|
end
|
33
29
|
|
34
|
-
|
30
|
+
conn
|
35
31
|
end
|
36
32
|
|
37
33
|
def self.get_conn_with_auth(verbose, url, user, password)
|
38
|
-
if url.nil?
|
39
|
-
raise "Did not provide a url to connect to"
|
40
|
-
end
|
34
|
+
raise 'Did not provide a url to connect to' if url.nil?
|
41
35
|
|
42
|
-
if user.nil?
|
43
|
-
raise "You did not provide a user to authenticate with"
|
44
|
-
end
|
36
|
+
raise 'You did not provide a user to authenticate with' if user.nil?
|
45
37
|
|
46
|
-
unless
|
47
|
-
url = "https://#{url}"
|
48
|
-
end
|
38
|
+
url = "https://#{url}" unless url?(url)
|
49
39
|
|
50
|
-
conn = Faraday.new(:url => url, :ssl => {:verify => false}) do |faraday|
|
40
|
+
conn = Faraday.new(:url => url, :ssl => { :verify => false }) do |faraday|
|
51
41
|
faraday.request :url_encoded
|
52
42
|
faraday.request :basic_auth, user, password
|
53
43
|
faraday.response :logger if verbose
|
54
44
|
faraday.adapter Faraday.default_adapter
|
55
45
|
end
|
56
46
|
|
57
|
-
|
47
|
+
conn
|
58
48
|
end
|
59
|
-
|
60
49
|
end
|