vmfloaty 0.8.2 → 0.9.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,43 +1,37 @@
1
- class Ssh
1
+ # frozen_string_literal: true
2
2
 
3
+ class Ssh
3
4
  def self.which(cmd)
4
5
  # Gets path of executable for given command
5
6
 
6
7
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
7
8
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
8
- exts.each { |ext|
9
+ exts.each do |ext|
9
10
  exe = File.join(path, "#{cmd}#{ext}")
10
11
  return exe if File.executable?(exe) && !File.directory?(exe)
11
- }
12
+ end
12
13
  end
13
- return nil
14
+ nil
14
15
  end
15
16
 
16
17
  def self.ssh(verbose, host_os, token, url)
17
- ssh_path = which("ssh")
18
- if !ssh_path
19
- raise "Could not determine path to ssh"
20
- end
18
+ ssh_path = which('ssh')
19
+ raise 'Could not determine path to ssh' unless ssh_path
20
+
21
21
  os_types = {}
22
22
  os_types[host_os] = 1
23
23
 
24
24
  response = Pooler.retrieve(verbose, os_types, token, url)
25
- if response["ok"] == true
26
- if host_os =~ /win/
27
- user = "Administrator"
28
- else
29
- user = "root"
30
- end
25
+ raise "Could not get vm from vmpooler:\n #{response}" unless response['ok']
31
26
 
32
- hostname = "#{response[host_os]["hostname"]}.#{response["domain"]}"
33
- cmd = "#{ssh_path} #{user}@#{hostname}"
27
+ user = /win/.match?(host_os) ? 'Administrator' : 'root'
34
28
 
35
- # TODO: Should this respect more ssh settings? Can it be configured
36
- # by users ssh config and does this respect those settings?
37
- Kernel.exec(cmd)
38
- else
39
- raise "Could not get vm from vmpooler:\n #{response}"
40
- end
41
- return
29
+ hostname = "#{response[host_os]['hostname']}.#{response['domain']}"
30
+ cmd = "#{ssh_path} #{user}@#{hostname}"
31
+
32
+ # TODO: Should this respect more ssh settings? Can it be configured
33
+ # by users ssh config and does this respect those settings?
34
+ Kernel.exec(cmd)
35
+ nil
42
36
  end
43
37
  end
@@ -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,9 +31,14 @@ 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')
@@ -39,9 +47,7 @@ class Utils
39
47
 
40
48
  response_body.each do |os, value|
41
49
  hostnames = Array(value['hostname'])
42
- if domain
43
- hostnames.map! {|host| "#{host}.#{domain}"}
44
- end
50
+ hostnames.map! { |host| "#{host}.#{domain}" } if domain
45
51
  result[os] = hostnames
46
52
  end
47
53
 
@@ -65,13 +71,8 @@ class Utils
65
71
  # ...]
66
72
  os_types = {}
67
73
  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
74
+ os_arr = arg.split('=')
75
+ os_types[os_arr[0]] = os_arr.size == 1 ? 1 : os_arr[1].to_i
75
76
  end
76
77
  os_types
77
78
  end
@@ -84,26 +85,29 @@ class Utils
84
85
  host_data = response[hostname]
85
86
 
86
87
  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']}"
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']}>"
100
93
  end
101
- line += ')'
102
- puts line
103
- else
104
- raise "Invalid service type #{service.type}"
94
+ 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}"
105
109
  end
106
- rescue => e
110
+ rescue StandardError => e
107
111
  STDERR.puts("Something went wrong while trying to gather information on #{hostname}:")
108
112
  STDERR.puts(e)
109
113
  end
@@ -114,45 +118,48 @@ class Utils
114
118
  status_response = service.status(verbose)
115
119
 
116
120
  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
121
+ when 'Pooler'
122
+ message = status_response['status']['message']
123
+ pools = status_response['pools']
124
+ pools.select! { |_, pool| pool['ready'] < pool['max'] } unless verbose
125
+
126
+ width = pools.keys.map(&:length).max
127
+ pools.each do |name, pool|
128
+ begin
129
+ max = pool['max']
130
+ ready = pool['ready']
131
+ pending = pool['pending']
132
+ missing = max - ready - pending
133
+ char = 'o'
134
+ puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
135
+ rescue StandardError => e
136
+ puts "#{name.ljust(width)} #{e.red}"
134
137
  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
138
+ end
139
+ puts message.colorize(status_response['status']['ok'] ? :default : :red)
140
+ when 'NonstandardPooler'
141
+ pools = status_response
142
+ pools.delete 'ok'
143
+ pools.select! { |_, pool| pool['available_hosts'] < pool['total_hosts'] } unless verbose
144
+
145
+ width = pools.keys.map(&:length).max
146
+ pools.each do |name, pool|
147
+ begin
148
+ max = pool['total_hosts']
149
+ ready = pool['available_hosts']
150
+ pending = pool['pending'] || 0 # not available for nspooler
151
+ missing = max - ready - pending
152
+ char = 'o'
153
+ puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
154
+ rescue StandardError => e
155
+ puts "#{name.ljust(width)} #{e.red}"
153
156
  end
154
- else
155
- raise "Invalid service type #{service.type}"
157
+ end
158
+ when 'ABS'
159
+ puts 'ABS Not OK'.red unless status_response
160
+ puts 'ABS is OK'.green if status_response
161
+ else
162
+ raise "Invalid service type #{service.type}"
156
163
  end
157
164
  end
158
165
 
@@ -165,9 +172,12 @@ class Utils
165
172
  end
166
173
 
167
174
  def self.get_service_object(type = '')
168
- nspooler_strings = ['ns', 'nspooler', 'nonstandard', 'nonstandard_pooler']
175
+ nspooler_strings = %w[ns nspooler nonstandard nonstandard_pooler]
176
+ abs_strings = %w[abs alwaysbescheduling always_be_scheduling]
169
177
  if nspooler_strings.include? type.downcase
170
178
  NonstandardPooler
179
+ elsif abs_strings.include? type.downcase
180
+ ABS
171
181
  else
172
182
  Pooler
173
183
  end
@@ -176,10 +186,10 @@ class Utils
176
186
  def self.get_service_config(config, options)
177
187
  # The top-level url, user, and token values in the config file are treated as defaults
178
188
  service_config = {
179
- 'url' => config['url'],
180
- 'user' => config['user'],
181
- 'token' => config['token'],
182
- 'type' => config['type'] || 'vmpooler'
189
+ 'url' => config['url'],
190
+ 'user' => config['user'],
191
+ 'token' => config['token'],
192
+ 'type' => config['type'] || 'vmpooler',
183
193
  }
184
194
 
185
195
  if config['services']
@@ -190,16 +200,15 @@ class Utils
190
200
  service_config.merge! values
191
201
  else
192
202
  # 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
203
+ raise ArgumentError, "Could not find a configured service named '#{options.service}' in ~/.vmfloaty.yml" unless config['services'][options.service]
204
+
205
+ # If the service is configured but some values are missing, use the top-level defaults to fill them in
206
+ service_config.merge! config['services'][options.service]
199
207
  end
200
208
  end
201
209
 
202
210
  # Prioritize an explicitly specified url, user, or token if the user provided one
211
+ service_config['priority'] = options.priority unless options.priority.nil?
203
212
  service_config['url'] = options.url unless options.url.nil?
204
213
  service_config['token'] = options.token unless options.token.nil?
205
214
  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.9.0'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'vmfloaty'
2
4
  require 'webmock/rspec'
3
5
 
@@ -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,96 @@
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 Metrics/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 Metrics/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
+ end
96
+ end