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.
@@ -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
@@ -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 "token"
12
+ resp = conn.post 'token'
11
13
 
12
14
  res_body = JSON.parse(resp.body)
13
- if res_body["ok"]
14
- return res_body["token"]
15
- else
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["ok"]
30
- return res_body
31
- else
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["ok"]
47
- return res_body
48
- else
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
@@ -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
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class AuthError < StandardError
2
- def initialize(msg="Could not authenticate to pooler")
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="Could not do operation with token provided")
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="Argument provided to function is missing")
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="Could not modify VM")
22
+ def initialize(msg = 'Could not modify VM')
21
23
  super
22
24
  end
23
25
  end
@@ -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.is_url(url)
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.kind_of?(URI::HTTP) or uri.kind_of?(URI::HTTPS)
13
- return true
14
- end
14
+ return true if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
15
15
 
16
- return false
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 is_url(url)
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
- return conn
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 is_url(url)
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
- return conn
47
+ conn
58
48
  end
59
-
60
49
  end