vmfloaty 0.8.2 → 0.11.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.
@@ -1,5 +1,8 @@
1
- require 'vmfloaty/pooler'
1
+ # frozen_string_literal: true
2
+
3
+ require 'vmfloaty/abs'
2
4
  require 'vmfloaty/nonstandard_pooler'
5
+ require 'vmfloaty/pooler'
3
6
 
4
7
  class Utils
5
8
  # TODO: Takes the json response body from an HTTP GET
@@ -28,20 +31,24 @@ class Utils
28
31
  # }
29
32
  # }
30
33
 
31
- unless response_body.delete('ok')
32
- raise ArgumentError, "Bad GET response passed to format_hosts: #{response_body.to_json}"
33
- end
34
+ # abs pooler response body example when `floaty get` arguments are :
35
+ # {
36
+ # "hostname"=>"thin-soutane.delivery.puppetlabs.net",
37
+ # "type"=>"centos-7.2-tmpfs-x86_64",
38
+ # "engine"=>"vmpooler"
39
+ # }
40
+
41
+ raise ArgumentError, "Bad GET response passed to format_hosts: #{response_body.to_json}" unless response_body.delete('ok')
34
42
 
35
43
  # vmpooler reports the domain separately from the hostname
36
44
  domain = response_body.delete('domain')
37
45
 
38
46
  result = {}
39
47
 
40
- 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|
41
50
  hostnames = Array(value['hostname'])
42
- if domain
43
- hostnames.map! {|host| "#{host}.#{domain}"}
44
- end
51
+ hostnames.map! { |host| "#{host}.#{domain}" } if domain
45
52
  result[os] = hostnames
46
53
  end
47
54
 
@@ -65,18 +72,13 @@ class Utils
65
72
  # ...]
66
73
  os_types = {}
67
74
  os_args.each do |arg|
68
- os_arr = arg.split("=")
69
- if os_arr.size == 1
70
- # assume they didn't specify an = sign if split returns 1 size
71
- os_types[os_arr[0]] = 1
72
- else
73
- os_types[os_arr[0]] = os_arr[1].to_i
74
- end
75
+ os_arr = arg.split('=')
76
+ os_types[os_arr[0]] = os_arr.size == 1 ? 1 : os_arr[1].to_i
75
77
  end
76
78
  os_types
77
79
  end
78
80
 
79
- def self.pretty_print_hosts(verbose, service, hostnames = [])
81
+ def self.pretty_print_hosts(verbose, service, hostnames = [], print_to_stderr = false)
80
82
  hostnames = [hostnames] unless hostnames.is_a? Array
81
83
  hostnames.each do |hostname|
82
84
  begin
@@ -84,28 +86,31 @@ class Utils
84
86
  host_data = response[hostname]
85
87
 
86
88
  case service.type
87
- when 'Pooler'
88
- tag_pairs = []
89
- unless host_data['tags'].nil?
90
- tag_pairs = host_data['tags'].map {|key, value| "#{key}: #{value}"}
91
- end
92
- duration = "#{host_data['running']}/#{host_data['lifetime']} hours"
93
- metadata = [host_data['template'], duration, *tag_pairs]
94
- puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(", ")})"
95
- when 'NonstandardPooler'
96
- line = "- #{host_data['fqdn']} (#{host_data['os_triple']}"
97
- line += ", #{host_data['hours_left_on_reservation']}h remaining"
98
- unless host_data['reserved_for_reason'].empty?
99
- line += ", reason: #{host_data['reserved_for_reason']}"
89
+ when 'ABS'
90
+ # For ABS, 'hostname' variable is the jobID
91
+ if host_data['state'] == 'allocated' || host_data['state'] == 'filled'
92
+ host_data['allocated_resources'].each do |vm_name, _i|
93
+ puts "- [JobID:#{host_data['request']['job']['id']}] #{vm_name['hostname']} (#{vm_name['type']}) <#{host_data['state']}>"
100
94
  end
101
- line += ')'
102
- puts line
103
- else
104
- raise "Invalid service type #{service.type}"
95
+ end
96
+ when 'Pooler'
97
+ tag_pairs = []
98
+ tag_pairs = host_data['tags'].map { |key, value| "#{key}: #{value}" } unless host_data['tags'].nil?
99
+ duration = "#{host_data['running']}/#{host_data['lifetime']} hours"
100
+ metadata = [host_data['template'], duration, *tag_pairs]
101
+ puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(', ')})"
102
+ when 'NonstandardPooler'
103
+ line = "- #{host_data['fqdn']} (#{host_data['os_triple']}"
104
+ line += ", #{host_data['hours_left_on_reservation']}h remaining"
105
+ line += ", reason: #{host_data['reserved_for_reason']}" unless host_data['reserved_for_reason'].empty?
106
+ line += ')'
107
+ puts line
108
+ else
109
+ raise "Invalid service type #{service.type}"
105
110
  end
106
- rescue => e
107
- STDERR.puts("Something went wrong while trying to gather information on #{hostname}:")
108
- STDERR.puts(e)
111
+ rescue StandardError => e
112
+ FloatyLogger.error("Something went wrong while trying to gather information on #{hostname}:")
113
+ FloatyLogger.error(e)
109
114
  end
110
115
  end
111
116
  end
@@ -114,45 +119,48 @@ class Utils
114
119
  status_response = service.status(verbose)
115
120
 
116
121
  case service.type
117
- when 'Pooler'
118
- message = status_response['status']['message']
119
- pools = status_response['pools']
120
- pools.select! {|_, pool| pool['ready'] < pool['max']} unless verbose
121
-
122
- width = pools.keys.map(&:length).max
123
- pools.each do |name, pool|
124
- begin
125
- max = pool['max']
126
- ready = pool['ready']
127
- pending = pool['pending']
128
- missing = max - ready - pending
129
- char = 'o'
130
- puts "#{name.ljust(width)} #{(char*ready).green}#{(char*pending).yellow}#{(char*missing).red}"
131
- rescue => e
132
- puts "#{name.ljust(width)} #{e.red}"
133
- end
122
+ when 'Pooler'
123
+ message = status_response['status']['message']
124
+ pools = status_response['pools']
125
+ pools.select! { |_, pool| pool['ready'] < pool['max'] } unless verbose
126
+
127
+ width = pools.keys.map(&:length).max
128
+ pools.each do |name, pool|
129
+ begin
130
+ max = pool['max']
131
+ ready = pool['ready']
132
+ pending = pool['pending']
133
+ missing = max - ready - pending
134
+ char = 'o'
135
+ puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
136
+ rescue StandardError => e
137
+ FloatyLogger.error "#{name.ljust(width)} #{e.red}"
134
138
  end
135
- puts message.colorize(status_response['status']['ok'] ? :default : :red)
136
- when 'NonstandardPooler'
137
- pools = status_response
138
- pools.delete 'ok'
139
- pools.select! {|_, pool| pool['available_hosts'] < pool['total_hosts']} unless verbose
140
-
141
- width = pools.keys.map(&:length).max
142
- pools.each do |name, pool|
143
- begin
144
- max = pool['total_hosts']
145
- ready = pool['available_hosts']
146
- pending = pool['pending'] || 0 # not available for nspooler
147
- missing = max - ready - pending
148
- char = 'o'
149
- puts "#{name.ljust(width)} #{(char*ready).green}#{(char*pending).yellow}#{(char*missing).red}"
150
- rescue => e
151
- puts "#{name.ljust(width)} #{e.red}"
152
- end
139
+ end
140
+ puts message.colorize(status_response['status']['ok'] ? :default : :red)
141
+ when 'NonstandardPooler'
142
+ pools = status_response
143
+ pools.delete 'ok'
144
+ pools.select! { |_, pool| pool['available_hosts'] < pool['total_hosts'] } unless verbose
145
+
146
+ width = pools.keys.map(&:length).max
147
+ pools.each do |name, pool|
148
+ begin
149
+ max = pool['total_hosts']
150
+ ready = pool['available_hosts']
151
+ pending = pool['pending'] || 0 # not available for nspooler
152
+ missing = max - ready - pending
153
+ char = 'o'
154
+ puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
155
+ rescue StandardError => e
156
+ FloatyLogger.error "#{name.ljust(width)} #{e.red}"
153
157
  end
154
- else
155
- raise "Invalid service type #{service.type}"
158
+ end
159
+ when 'ABS'
160
+ FloatyLogger.error 'ABS Not OK' unless status_response
161
+ puts 'ABS is OK'.green if status_response
162
+ else
163
+ raise "Invalid service type #{service.type}"
156
164
  end
157
165
  end
158
166
 
@@ -165,9 +173,12 @@ class Utils
165
173
  end
166
174
 
167
175
  def self.get_service_object(type = '')
168
- nspooler_strings = ['ns', 'nspooler', 'nonstandard', 'nonstandard_pooler']
176
+ nspooler_strings = %w[ns nspooler nonstandard nonstandard_pooler]
177
+ abs_strings = %w[abs alwaysbescheduling always_be_scheduling]
169
178
  if nspooler_strings.include? type.downcase
170
179
  NonstandardPooler
180
+ elsif abs_strings.include? type.downcase
181
+ ABS
171
182
  else
172
183
  Pooler
173
184
  end
@@ -176,10 +187,10 @@ class Utils
176
187
  def self.get_service_config(config, options)
177
188
  # The top-level url, user, and token values in the config file are treated as defaults
178
189
  service_config = {
179
- 'url' => config['url'],
180
- 'user' => config['user'],
181
- 'token' => config['token'],
182
- 'type' => config['type'] || 'vmpooler'
190
+ 'url' => config['url'],
191
+ 'user' => config['user'],
192
+ 'token' => config['token'],
193
+ 'type' => config['type'] || 'vmpooler',
183
194
  }
184
195
 
185
196
  if config['services']
@@ -190,16 +201,15 @@ class Utils
190
201
  service_config.merge! values
191
202
  else
192
203
  # If the user provided a service name at the command line, use that service if posible, or fail
193
- if config['services'][options.service]
194
- # If the service is configured but some values are missing, use the top-level defaults to fill them in
195
- service_config.merge! config['services'][options.service]
196
- else
197
- raise ArgumentError, "Could not find a configured service named '#{options.service}' in ~/.vmfloaty.yml"
198
- end
204
+ raise ArgumentError, "Could not find a configured service named '#{options.service}' in ~/.vmfloaty.yml" unless config['services'][options.service]
205
+
206
+ # If the service is configured but some values are missing, use the top-level defaults to fill them in
207
+ service_config.merge! config['services'][options.service]
199
208
  end
200
209
  end
201
210
 
202
211
  # Prioritize an explicitly specified url, user, or token if the user provided one
212
+ service_config['priority'] = options.priority unless options.priority.nil?
203
213
  service_config['url'] = options.url unless options.url.nil?
204
214
  service_config['token'] = options.token unless options.token.nil?
205
215
  service_config['user'] = options.user unless options.user.nil?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Vmfloaty
2
- VERSION = '0.8.2'.freeze
4
+ VERSION = '0.11.0'
3
5
  end
@@ -1,3 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ require 'coveralls'
5
+
6
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
7
+ SimpleCov::Formatter::HTMLFormatter,
8
+ Coveralls::SimpleCov::Formatter
9
+ ])
10
+ SimpleCov.start do
11
+ add_filter %r{^/spec/}
12
+ end
13
+
1
14
  require 'vmfloaty'
2
15
  require 'webmock/rspec'
3
16
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative '../../../lib/vmfloaty/auth'
5
+
6
+ describe Pooler do
7
+ before :each do
8
+ @abs_url = 'https://abs.example.com/api/v2'
9
+ end
10
+
11
+ describe '#get_token' do
12
+ before :each do
13
+ @get_token_response = '{"ok": true,"token":"utpg2i2xswor6h8ttjhu3d47z53yy47y"}'
14
+ @token = 'utpg2i2xswor6h8ttjhu3d47z53yy47y'
15
+ end
16
+
17
+ it 'returns a token from abs' do
18
+ stub_request(:post, 'https://first.last:password@abs.example.com/api/v2/token')
19
+ .to_return(:status => 200, :body => @get_token_response, :headers => {})
20
+
21
+ token = Auth.get_token(false, @abs_url, 'first.last', 'password')
22
+ expect(token).to eq @token
23
+ end
24
+
25
+ it 'raises a token error if something goes wrong' do
26
+ stub_request(:post, 'https://first.last:password@abs.example.com/api/v2/token')
27
+ .to_return(:status => 500, :body => '{"ok":false}', :headers => {})
28
+
29
+ expect { Auth.get_token(false, @abs_url, 'first.last', 'password') }.to raise_error(TokenError)
30
+ end
31
+ end
32
+
33
+ describe '#delete_token' do
34
+ before :each do
35
+ @delete_token_response = '{"ok":true}'
36
+ @token = 'utpg2i2xswor6h8ttjhu3d47z53yy47y'
37
+ end
38
+
39
+ it 'deletes the specified token' do
40
+ stub_request(:delete, 'https://first.last:password@abs.example.com/api/v2/token/utpg2i2xswor6h8ttjhu3d47z53yy47y')
41
+ .to_return(:status => 200, :body => @delete_token_response, :headers => {})
42
+
43
+ expect(Auth.delete_token(false, @abs_url, 'first.last', 'password', @token)).to eq JSON.parse(@delete_token_response)
44
+ end
45
+
46
+ it 'raises a token error if something goes wrong' do
47
+ stub_request(:delete, 'https://first.last:password@abs.example.com/api/v2/token/utpg2i2xswor6h8ttjhu3d47z53yy47y')
48
+ .to_return(:status => 500, :body => '{"ok":false}', :headers => {})
49
+
50
+ expect { Auth.delete_token(false, @abs_url, 'first.last', 'password', @token) }.to raise_error(TokenError)
51
+ end
52
+
53
+ it 'raises a token error if no token provided' do
54
+ expect { Auth.delete_token(false, @abs_url, 'first.last', 'password', nil) }.to raise_error(TokenError)
55
+ end
56
+ end
57
+
58
+ describe '#token_status' do
59
+ before :each do
60
+ @token_status_response = '{"ok":true,"utpg2i2xswor6h8ttjhu3d47z53yy47y":{"created":"2015-04-28 19:17:47 -0700"}}'
61
+ @token = 'utpg2i2xswor6h8ttjhu3d47z53yy47y'
62
+ end
63
+
64
+ it 'checks the status of a token' do
65
+ stub_request(:get, "#{@abs_url}/token/utpg2i2xswor6h8ttjhu3d47z53yy47y")
66
+ .with(:headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' })
67
+ .to_return(:status => 200, :body => @token_status_response, :headers => {})
68
+
69
+ expect(Auth.token_status(false, @abs_url, @token)).to eq JSON.parse(@token_status_response)
70
+ end
71
+
72
+ it 'raises a token error if something goes wrong' do
73
+ stub_request(:get, "#{@abs_url}/token/utpg2i2xswor6h8ttjhu3d47z53yy47y")
74
+ .with(:headers => { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' })
75
+ .to_return(:status => 500, :body => '{"ok":false}', :headers => {})
76
+
77
+ expect { Auth.token_status(false, @abs_url, @token) }.to raise_error(TokenError)
78
+ end
79
+
80
+ it 'raises a token error if no token provided' do
81
+ expect { Auth.token_status(false, @abs_url, nil) }.to raise_error(TokenError)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'vmfloaty/utils'
5
+ require 'vmfloaty/errors'
6
+ require 'vmfloaty/abs'
7
+
8
+ describe ABS do
9
+ before :each do
10
+ end
11
+
12
+ describe '#format' do
13
+ it 'returns an hash formatted like a vmpooler return' do
14
+ abs_formatted_response = [
15
+ { 'hostname' => 'aaaaaaaaaaaaaaa.delivery.puppetlabs.net', 'type' => 'centos-7.2-x86_64', 'engine' => 'vmpooler' },
16
+ { 'hostname' => 'aaaaaaaaaaaaaab.delivery.puppetlabs.net', 'type' => 'centos-7.2-x86_64', 'engine' => 'vmpooler' },
17
+ { 'hostname' => 'aaaaaaaaaaaaaac.delivery.puppetlabs.net', 'type' => 'ubuntu-7.2-x86_64', 'engine' => 'vmpooler' },
18
+ ]
19
+
20
+ vmpooler_formatted_response = ABS.translated(abs_formatted_response)
21
+
22
+ vmpooler_formatted_compare = {
23
+ 'centos-7.2-x86_64' => {},
24
+ 'ubuntu-7.2-x86_64' => {},
25
+ }
26
+
27
+ vmpooler_formatted_compare['centos-7.2-x86_64']['hostname'] = ['aaaaaaaaaaaaaaa.delivery.puppetlabs.net', 'aaaaaaaaaaaaaab.delivery.puppetlabs.net']
28
+ vmpooler_formatted_compare['ubuntu-7.2-x86_64']['hostname'] = ['aaaaaaaaaaaaaac.delivery.puppetlabs.net']
29
+
30
+ vmpooler_formatted_compare['ok'] = true
31
+
32
+ expect(vmpooler_formatted_response).to eq(vmpooler_formatted_compare)
33
+ vmpooler_formatted_response.delete('ok')
34
+ vmpooler_formatted_compare.delete('ok')
35
+ expect(vmpooler_formatted_response).to eq(vmpooler_formatted_compare)
36
+ end
37
+
38
+ it 'won\'t delete a job if not all vms are listed' do
39
+ hosts = ['host1']
40
+ allocated_resources = [
41
+ {
42
+ 'hostname' => 'host1',
43
+ },
44
+ {
45
+ 'hostname' => 'host2',
46
+ },
47
+ ]
48
+ expect(ABS.all_job_resources_accounted_for(allocated_resources, hosts)).to eq(false)
49
+
50
+ hosts = ['host1', 'host2']
51
+ allocated_resources = [
52
+ {
53
+ 'hostname' => 'host1',
54
+ },
55
+ {
56
+ 'hostname' => 'host2',
57
+ },
58
+ ]
59
+ expect(ABS.all_job_resources_accounted_for(allocated_resources, hosts)).to eq(true)
60
+ end
61
+
62
+ before :each do
63
+ @abs_url = 'https://abs.example.com'
64
+ end
65
+
66
+ describe '#test_abs_status_queue_endpoint' do
67
+ before :each do
68
+ # rubocop:disable Layout/LineLength
69
+ @active_requests_response = '
70
+ [
71
+ "{ \"state\":\"allocated\",\"last_processed\":\"2019-12-16 23:00:34 +0000\",\"allocated_resources\":[{\"hostname\":\"take-this.delivery.puppetlabs.net\",\"type\":\"win-2012r2-x86_64\",\"engine\":\"vmpooler\"}],\"audit_log\":{\"2019-12-13 16:45:29 +0000\":\"Allocated take-this.delivery.puppetlabs.net for job 1576255517241\"},\"request\":{\"resources\":{\"win-2012r2-x86_64\":1},\"job\":{\"id\":\"1576255517241\",\"tags\":{\"user\":\"test-user\"},\"user\":\"test-user\",\"time-received\":1576255519},\"priority\":1}}",
72
+ "null",
73
+ "{\"state\":\"allocated\",\"last_processed\":\"2019-12-16 23:00:34 +0000\",\"allocated_resources\":[{\"hostname\":\"not-this.delivery.puppetlabs.net\",\"type\":\"win-2012r2-x86_64\",\"engine\":\"vmpooler\"}],\"audit_log\":{\"2019-12-13 16:46:14 +0000\":\"Allocated not-this.delivery.puppetlabs.net for job 1576255565159\"},\"request\":{\"resources\":{\"win-2012r2-x86_64\":1},\"job\":{\"id\":\"1576255565159\",\"tags\":{\"user\":\"not-test-user\"},\"user\":\"not-test-user\",\"time-received\":1576255566},\"priority\":1}}"
74
+ ]'
75
+ # rubocop:enable Layout/LineLength
76
+ @token = 'utpg2i2xswor6h8ttjhu3d47z53yy47y'
77
+ @test_user = 'test-user'
78
+ end
79
+
80
+ it 'will skip a line with a null value returned from abs' do
81
+ stub_request(:get, 'https://abs.example.com/status/queue')
82
+ .to_return(:status => 200, :body => @active_requests_response, :headers => {})
83
+
84
+ ret = ABS.get_active_requests(false, @abs_url, @test_user)
85
+
86
+ expect(ret[0]).to include(
87
+ 'allocated_resources' => [{
88
+ 'hostname' => 'take-this.delivery.puppetlabs.net',
89
+ 'type' => 'win-2012r2-x86_64',
90
+ 'engine' => 'vmpooler',
91
+ }],
92
+ )
93
+ end
94
+ end
95
+
96
+ describe '#test_abs_delete_jobid' do
97
+ before :each do
98
+ # rubocop:disable Layout/LineLength
99
+ @active_requests_response = '
100
+ [
101
+ "{ \"state\":\"allocated\", \"last_processed\":\"2020-01-17 22:29:13 +0000\", \"allocated_resources\":[{\"hostname\":\"craggy-chord.delivery.puppetlabs.net\", \"type\":\"centos-7-x86_64\", \"engine\":\"vmpooler\"}, {\"hostname\":\"visible-revival.delivery.puppetlabs.net\", \"type\":\"centos-7-x86_64\", \"engine\":\"vmpooler\"}], \"audit_log\":{\"2020-01-17 22:28:45 +0000\":\"Allocated craggy-chord.delivery.puppetlabs.net, visible-revival.delivery.puppetlabs.net for job 1579300120799\"}, \"request\":{\"resources\":{\"centos-7-x86_64\":2}, \"job\":{\"id\":\"1579300120799\", \"tags\":{\"user\":\"test-user\"}, \"user\":\"test-user\", \"time-received\":1579300120}, \"priority\":3}}"
102
+ ]'
103
+ @return_request = { '{"job_id":"1579300120799","hosts":{"hostname":"craggy-chord.delivery.puppetlabs.net","type":"centos-7-x86_64","engine":"vmpooler"},{"hostname":"visible-revival.delivery.puppetlabs.net","type":"centos-7-x86_64","engine":"vmpooler"}}'=>true }
104
+ # rubocop:enable Layout/LineLength
105
+ @token = 'utpg2i2xswor6h8ttjhu3d47z53yy47y'
106
+ @test_user = 'test-user'
107
+ # Job ID
108
+ @hosts = ['1579300120799']
109
+ end
110
+
111
+ it 'will delete the whole job' do
112
+ stub_request(:get, 'https://abs.example.com/status/queue')
113
+ .to_return(:status => 200, :body => @active_requests_response, :headers => {})
114
+ stub_request(:post, 'https://abs.example.com/return')
115
+ .with(:body => @return_request)
116
+ .to_return(:status => 200, :body => 'OK', :headers => {})
117
+
118
+ ret = ABS.delete(false, @abs_url, @hosts, @token, @test_user)
119
+
120
+ expect(ret).to include(
121
+ 'craggy-chord.delivery.puppetlabs.net' => { 'ok'=>true }, 'visible-revival.delivery.puppetlabs.net' => { 'ok'=>true },
122
+ )
123
+ end
124
+ end
125
+ end
126
+ end