vmfloaty 0.9.0 → 0.11.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.
@@ -68,7 +68,7 @@ class ABS
68
68
 
69
69
  ret_val.push(req_hash)
70
70
  rescue NoMethodError
71
- puts "Warning: couldn't parse line returned from abs/status/queue: ".yellow
71
+ FloatyLogger.warn "Warning: couldn't parse line returned from abs/status/queue: "
72
72
  end
73
73
  end
74
74
 
@@ -85,7 +85,7 @@ class ABS
85
85
  conn = Http.get_conn(verbose, url)
86
86
  conn.headers['X-AUTH-TOKEN'] = token if token
87
87
 
88
- puts "Trying to delete hosts #{hosts}" if verbose
88
+ FloatyLogger.info "Trying to delete hosts #{hosts}" if verbose
89
89
  requests = get_active_requests(verbose, url, user)
90
90
 
91
91
  jobs_to_delete = []
@@ -100,6 +100,11 @@ class ABS
100
100
  requests.each do |req_hash|
101
101
  next unless req_hash['state'] == 'allocated' || req_hash['state'] == 'filled'
102
102
 
103
+ if hosts.include? req_hash['request']['job']['id']
104
+ jobs_to_delete.push(req_hash)
105
+ next
106
+ end
107
+
103
108
  req_hash['allocated_resources'].each do |vm_name, _i|
104
109
  if hosts.include? vm_name['hostname']
105
110
  if all_job_resources_accounted_for(req_hash['allocated_resources'], hosts)
@@ -108,7 +113,7 @@ class ABS
108
113
  }
109
114
  jobs_to_delete.push(req_hash)
110
115
  else
111
- 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']}"
116
+ 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']}"
112
117
  end
113
118
  end
114
119
  end
@@ -122,7 +127,7 @@ class ABS
122
127
  'hosts' => job['allocated_resources'],
123
128
  }
124
129
 
125
- puts "Deleting #{req_obj}" if verbose
130
+ FloatyLogger.info "Deleting #{req_obj}" if verbose
126
131
 
127
132
  return_result = conn.post 'return', req_obj.to_json
128
133
  req_obj['hosts'].each do |host|
@@ -145,6 +150,14 @@ class ABS
145
150
  os_list << '*** VMPOOLER Pools ***'
146
151
  os_list += JSON.parse(res_body['vmpooler_platforms'])
147
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
+
148
161
  res = conn.get 'status/platforms/nspooler'
149
162
  res_body = JSON.parse(res.body)
150
163
  os_list << ''
@@ -163,7 +176,7 @@ class ABS
163
176
  end
164
177
 
165
178
  # Retrieve an OS from ABS.
166
- def self.retrieve(verbose, os_types, token, url, user, options)
179
+ def self.retrieve(verbose, os_types, token, url, user, options, _ondemand = nil)
167
180
  #
168
181
  # Contents of post must be like:
169
182
  #
@@ -207,24 +220,24 @@ class ABS
207
220
  end
208
221
  end
209
222
 
210
- puts "Posting to ABS #{req_obj.to_json}" if verbose
223
+ FloatyLogger.info "Posting to ABS #{req_obj.to_json}" if verbose
211
224
 
212
225
  # os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
213
226
  # raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
214
- puts "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
227
+ FloatyLogger.info "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
215
228
  res = conn.post 'request', req_obj.to_json
216
229
 
217
230
  retries = 360
218
231
 
219
- raise AuthError, "HTTP #{res.status}: The token provided could not authenticate to the pooler.\n#{res_body}" if res.status == 401
232
+ validate_queue_status_response(res.status, res.body, "Initial request", verbose)
220
233
 
221
234
  (1..retries).each do |i|
222
- queue_place, res_body = check_queue(conn, saved_job_id, req_obj)
235
+ queue_place, res_body = check_queue(conn, saved_job_id, req_obj, verbose)
223
236
  return translated(res_body) if res_body
224
237
 
225
238
  sleep_seconds = 10 if i >= 10
226
239
  sleep_seconds = i if i < 10
227
- puts "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
240
+ FloatyLogger.info "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
228
241
 
229
242
  sleep(sleep_seconds)
230
243
  end
@@ -249,11 +262,12 @@ class ABS
249
262
  vmpooler_formatted_body
250
263
  end
251
264
 
252
- def self.check_queue(conn, job_id, req_obj)
265
+ def self.check_queue(conn, job_id, req_obj, verbose)
253
266
  queue_info_res = conn.get "status/queue/info/#{job_id}"
254
267
  queue_info = JSON.parse(queue_info_res.body)
255
268
 
256
269
  res = conn.post 'request', req_obj.to_json
270
+ validate_queue_status_response(res.status, res.body, "Check queue request", verbose)
257
271
 
258
272
  unless res.body.empty?
259
273
  res_body = JSON.parse(res.body)
@@ -263,7 +277,7 @@ class ABS
263
277
  end
264
278
 
265
279
  def self.snapshot(_verbose, _url, _hostname, _token)
266
- puts "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
280
+ FloatyLogger.info "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
267
281
  end
268
282
 
269
283
  def self.status(verbose, url)
@@ -284,10 +298,39 @@ class ABS
284
298
  def self.query(verbose, url, hostname)
285
299
  return @active_hostnames if @active_hostnames
286
300
 
287
- puts "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
301
+ FloatyLogger.info "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
288
302
  conn = Http.get_conn(verbose, url)
289
303
 
290
304
  res = conn.get "host/#{hostname}"
291
305
  JSON.parse(res.body)
292
306
  end
307
+
308
+ def self.modify(_verbose, _url, _hostname, _token, _modify_hash)
309
+ raise NoMethodError, 'modify is not defined for ABS'
310
+ end
311
+
312
+ def self.disk(_verbose, _url, _hostname, _token, _disk)
313
+ raise NoMethodError, 'disk is not defined for ABS'
314
+ end
315
+
316
+ def self.revert(_verbose, _url, _hostname, _token, _snapshot_sha)
317
+ raise NoMethodError, 'revert is not defined for ABS'
318
+ end
319
+
320
+ # Validate the http code returned during a queue status request.
321
+ #
322
+ # Return a success message that can be displayed if the status code is
323
+ # success, otherwise raise an error.
324
+ def self.validate_queue_status_response(status_code, body, request_name, verbose)
325
+ case status_code
326
+ when 200
327
+ "#{request_name} returned success (Code 200)" if verbose
328
+ when 202
329
+ "#{request_name} returned accepted, processing (Code 202)" if verbose
330
+ when 401
331
+ raise AuthError, "HTTP #{status_code}: The token provided could not authenticate.\n#{body}"
332
+ else
333
+ raise "HTTP #{status_code}: #{request_name} request to ABS failed!\n#{body}"
334
+ end
335
+ end
293
336
  end
@@ -8,7 +8,7 @@ class Conf
8
8
  begin
9
9
  conf = YAML.load_file("#{Dir.home}/.vmfloaty.yml")
10
10
  rescue StandardError
11
- STDERR.puts "WARNING: There was no config file at #{Dir.home}/.vmfloaty.yml"
11
+ FloatyLogger.warn "WARNING: There was no config file at #{Dir.home}/.vmfloaty.yml"
12
12
  end
13
13
  conf
14
14
  end
@@ -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
 
@@ -77,7 +77,7 @@ class NonstandardPooler
77
77
  raise ModifyError, 'Configured service type does not support snapshots'
78
78
  end
79
79
 
80
- def self.delete(verbose, url, hosts, token)
80
+ def self.delete(verbose, url, hosts, token, _user)
81
81
  raise TokenError, 'Token provided was nil; Request cannot be made to delete VM' if token.nil?
82
82
 
83
83
  conn = Http.get_conn(verbose, url)
@@ -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
 
@@ -94,7 +124,7 @@ class Pooler
94
124
  res_body
95
125
  end
96
126
 
97
- def self.delete(verbose, url, hosts, token)
127
+ def self.delete(verbose, url, hosts, token, _user)
98
128
  raise TokenError, 'Token provided was nil. Request cannot be made to delete vm' if token.nil?
99
129
 
100
130
  conn = Http.get_conn(verbose, url)
@@ -36,7 +36,7 @@ class Service
36
36
 
37
37
  def user
38
38
  unless @config['user']
39
- puts "Enter your #{@config['url']} service username:"
39
+ FloatyLogger.info "Enter your #{@config['url']} service username:"
40
40
  @config['user'] = STDIN.gets.chomp
41
41
  end
42
42
  @config['user']
@@ -44,7 +44,7 @@ class Service
44
44
 
45
45
  def token
46
46
  unless @config['token']
47
- puts 'No token found. Retrieving a token...'
47
+ FloatyLogger.info 'No token found. Retrieving a token...'
48
48
  @config['token'] = get_new_token(nil)
49
49
  end
50
50
  @config['token']
@@ -75,10 +75,14 @@ class Service
75
75
  @service_object.list_active verbose, url, token, user
76
76
  end
77
77
 
78
- def retrieve(verbose, os_types, use_token = true)
79
- puts 'Requesting a vm without a token...' unless use_token
78
+ def retrieve(verbose, os_types, use_token = true, ondemand = nil)
79
+ FloatyLogger.info 'Requesting a vm without a token...' unless use_token
80
80
  token_value = use_token ? token : nil
81
- @service_object.retrieve verbose, os_types, token_value, url, user, @config
81
+ @service_object.retrieve verbose, os_types, token_value, url, user, @config, ondemand
82
+ end
83
+
84
+ def wait_for_request(verbose, requestid)
85
+ @service_object.wait_for_request verbose, requestid, url
82
86
  end
83
87
 
84
88
  def ssh(verbose, host_os, use_token = true)
@@ -87,20 +91,11 @@ class Service
87
91
  begin
88
92
  token_value = token || get_new_token(verbose)
89
93
  rescue TokenError => e
90
- STDERR.puts e
91
- STDERR.puts 'Could not get token... requesting vm without a token anyway...'
94
+ FloatyLogger.error e
95
+ FloatyLogger.info 'Could not get token... requesting vm without a token anyway...'
92
96
  end
93
97
  end
94
- Ssh.ssh(verbose, host_os, token_value, url)
95
- end
96
-
97
- def pretty_print_running(verbose, hostnames = [])
98
- if hostnames.empty?
99
- puts 'You have no running VMs.'
100
- else
101
- puts 'Running VMs:'
102
- @service_object.pretty_print_hosts(verbose, hostnames, url)
103
- end
98
+ Ssh.ssh(verbose, self, host_os, token_value)
104
99
  end
105
100
 
106
101
  def query(verbose, hostname)
@@ -14,21 +14,27 @@ class Ssh
14
14
  nil
15
15
  end
16
16
 
17
- def self.ssh(verbose, host_os, token, url)
17
+ def self.command_string(verbose, service, host_os, use_token)
18
18
  ssh_path = which('ssh')
19
19
  raise 'Could not determine path to ssh' unless ssh_path
20
20
 
21
21
  os_types = {}
22
22
  os_types[host_os] = 1
23
23
 
24
- response = Pooler.retrieve(verbose, os_types, token, url)
25
- raise "Could not get vm from vmpooler:\n #{response}" unless response['ok']
24
+ response = service.retrieve(verbose, os_types, use_token)
25
+ raise "Could not get vm from #{service.type}:\n #{response}" unless response['ok']
26
26
 
27
27
  user = /win/.match?(host_os) ? 'Administrator' : 'root'
28
28
 
29
- hostname = "#{response[host_os]['hostname']}.#{response['domain']}"
30
- cmd = "#{ssh_path} #{user}@#{hostname}"
29
+ hostname = response[host_os]['hostname']
30
+ hostname = response[host_os]['hostname'][0] if response[host_os]['hostname'].is_a?(Array)
31
+ hostname = "#{hostname}.#{response['domain']}" unless hostname.end_with?('puppetlabs.net')
31
32
 
33
+ "#{ssh_path} #{user}@#{hostname}"
34
+ end
35
+
36
+ def self.ssh(verbose, service, host_os, use_token)
37
+ cmd = command_string(verbose, service, host_os, use_token)
32
38
  # TODO: Should this respect more ssh settings? Can it be configured
33
39
  # by users ssh config and does this respect those settings?
34
40
  Kernel.exec(cmd)
@@ -45,7 +45,8 @@ class Utils
45
45
 
46
46
  result = {}
47
47
 
48
- response_body.each do |os, value|
48
+ filtered_response_body = response_body.reject { |key, _| key == 'request_id' || key == 'ready' }
49
+ filtered_response_body.each do |os, value|
49
50
  hostnames = Array(value['hostname'])
50
51
  hostnames.map! { |host| "#{host}.#{domain}" } if domain
51
52
  result[os] = hostnames
@@ -77,41 +78,63 @@ class Utils
77
78
  os_types
78
79
  end
79
80
 
80
- def self.pretty_print_hosts(verbose, service, hostnames = [])
81
+ def self.pretty_print_hosts(verbose, service, hostnames = [], print_to_stderr = false)
82
+ fetched_data = self.get_host_data(verbose, service, hostnames)
83
+ fetched_data.each do |hostname, host_data|
84
+ case service.type
85
+ when 'ABS'
86
+ # For ABS, 'hostname' variable is the jobID
87
+ host_data['allocated_resources'].each do |vm_name, _i|
88
+ puts "- [JobID:#{host_data['request']['job']['id']}] #{vm_name['hostname']} (#{vm_name['type']}) <#{host_data['state']}>"
89
+ end
90
+ when 'Pooler'
91
+ tag_pairs = []
92
+ tag_pairs = host_data['tags'].map { |key, value| "#{key}: #{value}" } unless host_data['tags'].nil?
93
+ duration = "#{host_data['running']}/#{host_data['lifetime']} hours"
94
+ metadata = [host_data['template'], duration, *tag_pairs]
95
+ puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(', ')})"
96
+ when 'NonstandardPooler'
97
+ line = "- #{host_data['fqdn']} (#{host_data['os_triple']}"
98
+ line += ", #{host_data['hours_left_on_reservation']}h remaining"
99
+ line += ", reason: #{host_data['reserved_for_reason']}" unless host_data['reserved_for_reason'].empty?
100
+ line += ')'
101
+ puts line
102
+ else
103
+ raise "Invalid service type #{service.type}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def self.get_host_data(verbose, service, hostnames = [])
109
+ result = {}
81
110
  hostnames = [hostnames] unless hostnames.is_a? Array
82
111
  hostnames.each do |hostname|
83
112
  begin
84
113
  response = service.query(verbose, hostname)
85
114
  host_data = response[hostname]
86
-
87
- case service.type
88
- when 'ABS'
89
- # For ABS, 'hostname' variable is the jobID
90
- if host_data['state'] == 'allocated' || host_data['state'] == 'filled'
91
- host_data['allocated_resources'].each do |vm_name, _i|
92
- puts "- [JobID:#{host_data['request']['job']['id']}] #{vm_name['hostname']} (#{vm_name['type']}) <#{host_data['state']}>"
115
+ if block_given?
116
+ yield host_data result
117
+ else
118
+ case service.type
119
+ when 'ABS'
120
+ # For ABS, 'hostname' variable is the jobID
121
+ if host_data['state'] == 'allocated' || host_data['state'] == 'filled'
122
+ result[hostname] = host_data
93
123
  end
124
+ when 'Pooler'
125
+ result[hostname] = host_data
126
+ when 'NonstandardPooler'
127
+ result[hostname] = host_data
128
+ else
129
+ raise "Invalid service type #{service.type}"
94
130
  end
95
- when 'Pooler'
96
- tag_pairs = []
97
- tag_pairs = host_data['tags'].map { |key, value| "#{key}: #{value}" } unless host_data['tags'].nil?
98
- duration = "#{host_data['running']}/#{host_data['lifetime']} hours"
99
- metadata = [host_data['template'], duration, *tag_pairs]
100
- puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(', ')})"
101
- when 'NonstandardPooler'
102
- line = "- #{host_data['fqdn']} (#{host_data['os_triple']}"
103
- line += ", #{host_data['hours_left_on_reservation']}h remaining"
104
- line += ", reason: #{host_data['reserved_for_reason']}" unless host_data['reserved_for_reason'].empty?
105
- line += ')'
106
- puts line
107
- else
108
- raise "Invalid service type #{service.type}"
109
131
  end
110
132
  rescue StandardError => e
111
- STDERR.puts("Something went wrong while trying to gather information on #{hostname}:")
112
- STDERR.puts(e)
133
+ FloatyLogger.error("Something went wrong while trying to gather information on #{hostname}:")
134
+ FloatyLogger.error(e)
113
135
  end
114
136
  end
137
+ result
115
138
  end
116
139
 
117
140
  def self.pretty_print_status(verbose, service)
@@ -133,7 +156,7 @@ class Utils
133
156
  char = 'o'
134
157
  puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
135
158
  rescue StandardError => e
136
- puts "#{name.ljust(width)} #{e.red}"
159
+ FloatyLogger.error "#{name.ljust(width)} #{e.red}"
137
160
  end
138
161
  end
139
162
  puts message.colorize(status_response['status']['ok'] ? :default : :red)
@@ -152,11 +175,11 @@ class Utils
152
175
  char = 'o'
153
176
  puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
154
177
  rescue StandardError => e
155
- puts "#{name.ljust(width)} #{e.red}"
178
+ FloatyLogger.error "#{name.ljust(width)} #{e.red}"
156
179
  end
157
180
  end
158
181
  when 'ABS'
159
- puts 'ABS Not OK'.red unless status_response
182
+ FloatyLogger.error 'ABS Not OK' unless status_response
160
183
  puts 'ABS is OK'.green if status_response
161
184
  else
162
185
  raise "Invalid service type #{service.type}"
@@ -205,6 +228,9 @@ class Utils
205
228
  # If the service is configured but some values are missing, use the top-level defaults to fill them in
206
229
  service_config.merge! config['services'][options.service]
207
230
  end
231
+ # No config file but service is declared on command line
232
+ elsif !config['services'] && options.service
233
+ service_config['type'] = options.service
208
234
  end
209
235
 
210
236
  # Prioritize an explicitly specified url, user, or token if the user provided one