vmfloaty 0.8.1 → 0.10.0

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