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