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.
@@ -1,10 +1,13 @@
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
6
9
  # request and "pretty prints" it
7
- def self.format_hosts(response_body)
10
+ def self.standardize_hostnames(response_body)
8
11
  # vmpooler response body example when `floaty get` arguments are `ubuntu-1610-x86_64=2 centos-7-x86_64`:
9
12
  # {
10
13
  # "ok": true,
@@ -28,35 +31,35 @@ 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
+ # }
34
40
 
35
- hostnames = []
41
+ raise ArgumentError, "Bad GET response passed to format_hosts: #{response_body.to_json}" unless response_body.delete('ok')
36
42
 
37
43
  # vmpooler reports the domain separately from the hostname
38
44
  domain = response_body.delete('domain')
39
45
 
40
- if domain
41
- # vmpooler output
42
- response_body.each do |os, hosts|
43
- if hosts['hostname'].kind_of?(Array)
44
- hosts['hostname'].map!{ |host| hostnames << host + "." + domain + " (#{os})"}
45
- else
46
- hostnames << hosts["hostname"] + ".#{domain} (#{os})"
47
- end
48
- end
49
- else
50
- response_body.each do |os, hosts|
51
- if hosts['hostname'].kind_of?(Array)
52
- hosts['hostname'].map!{ |host| hostnames << host + " (#{os})" }
53
- else
54
- hostnames << hosts['hostname'] + " (#{os})"
55
- end
56
- end
46
+ result = {}
47
+
48
+ STDOUT.puts "response body is #{response_body}"
49
+ filtered_response_body = response_body.reject { |key, _| key == 'request_id' || key == 'ready' }
50
+ filtered_response_body.each do |os, value|
51
+ hostnames = Array(value['hostname'])
52
+ hostnames.map! { |host| "#{host}.#{domain}" } if domain
53
+ result[os] = hostnames
57
54
  end
58
55
 
59
- hostnames.map { |hostname| puts "- #{hostname}" }
56
+ result
57
+ end
58
+
59
+ def self.format_host_output(hosts)
60
+ hosts.flat_map do |os, names|
61
+ names.map { |name| "- #{name} (#{os})" }
62
+ end.join("\n")
60
63
  end
61
64
 
62
65
  def self.generate_os_hash(os_args)
@@ -70,13 +73,8 @@ class Utils
70
73
  # ...]
71
74
  os_types = {}
72
75
  os_args.each do |arg|
73
- os_arr = arg.split("=")
74
- if os_arr.size == 1
75
- # assume they didn't specify an = sign if split returns 1 size
76
- os_types[os_arr[0]] = 1
77
- else
78
- os_types[os_arr[0]] = os_arr[1].to_i
79
- end
76
+ os_arr = arg.split('=')
77
+ os_types[os_arr[0]] = os_arr.size == 1 ? 1 : os_arr[1].to_i
80
78
  end
81
79
  os_types
82
80
  end
@@ -89,26 +87,29 @@ class Utils
89
87
  host_data = response[hostname]
90
88
 
91
89
  case service.type
92
- when 'Pooler'
93
- tag_pairs = []
94
- unless host_data['tags'].nil?
95
- tag_pairs = host_data['tags'].map {|key, value| "#{key}: #{value}"}
96
- end
97
- duration = "#{host_data['running']}/#{host_data['lifetime']} hours"
98
- metadata = [host_data['template'], duration, *tag_pairs]
99
- puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(", ")})"
100
- when 'NonstandardPooler'
101
- line = "- #{host_data['fqdn']} (#{host_data['os_triple']}"
102
- line += ", #{host_data['hours_left_on_reservation']}h remaining"
103
- unless host_data['reserved_for_reason'].empty?
104
- line += ", reason: #{host_data['reserved_for_reason']}"
90
+ when 'ABS'
91
+ # For ABS, 'hostname' variable is the jobID
92
+ if host_data['state'] == 'allocated' || host_data['state'] == 'filled'
93
+ host_data['allocated_resources'].each do |vm_name, _i|
94
+ puts "- [JobID:#{host_data['request']['job']['id']}] #{vm_name['hostname']} (#{vm_name['type']}) <#{host_data['state']}>"
105
95
  end
106
- line += ')'
107
- puts line
108
- else
109
- raise "Invalid service type #{service.type}"
96
+ end
97
+ when 'Pooler'
98
+ tag_pairs = []
99
+ tag_pairs = host_data['tags'].map { |key, value| "#{key}: #{value}" } unless host_data['tags'].nil?
100
+ duration = "#{host_data['running']}/#{host_data['lifetime']} hours"
101
+ metadata = [host_data['template'], duration, *tag_pairs]
102
+ puts "- #{hostname}.#{host_data['domain']} (#{metadata.join(', ')})"
103
+ when 'NonstandardPooler'
104
+ line = "- #{host_data['fqdn']} (#{host_data['os_triple']}"
105
+ line += ", #{host_data['hours_left_on_reservation']}h remaining"
106
+ line += ", reason: #{host_data['reserved_for_reason']}" unless host_data['reserved_for_reason'].empty?
107
+ line += ')'
108
+ puts line
109
+ else
110
+ raise "Invalid service type #{service.type}"
110
111
  end
111
- rescue => e
112
+ rescue StandardError => e
112
113
  STDERR.puts("Something went wrong while trying to gather information on #{hostname}:")
113
114
  STDERR.puts(e)
114
115
  end
@@ -119,45 +120,48 @@ class Utils
119
120
  status_response = service.status(verbose)
120
121
 
121
122
  case service.type
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 => e
137
- puts "#{name.ljust(width)} #{e.red}"
138
- end
123
+ when 'Pooler'
124
+ message = status_response['status']['message']
125
+ pools = status_response['pools']
126
+ pools.select! { |_, pool| pool['ready'] < pool['max'] } unless verbose
127
+
128
+ width = pools.keys.map(&:length).max
129
+ pools.each do |name, pool|
130
+ begin
131
+ max = pool['max']
132
+ ready = pool['ready']
133
+ pending = pool['pending']
134
+ missing = max - ready - pending
135
+ char = 'o'
136
+ puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
137
+ rescue StandardError => e
138
+ puts "#{name.ljust(width)} #{e.red}"
139
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 => e
156
- puts "#{name.ljust(width)} #{e.red}"
157
- end
140
+ end
141
+ puts message.colorize(status_response['status']['ok'] ? :default : :red)
142
+ when 'NonstandardPooler'
143
+ pools = status_response
144
+ pools.delete 'ok'
145
+ pools.select! { |_, pool| pool['available_hosts'] < pool['total_hosts'] } unless verbose
146
+
147
+ width = pools.keys.map(&:length).max
148
+ pools.each do |name, pool|
149
+ begin
150
+ max = pool['total_hosts']
151
+ ready = pool['available_hosts']
152
+ pending = pool['pending'] || 0 # not available for nspooler
153
+ missing = max - ready - pending
154
+ char = 'o'
155
+ puts "#{name.ljust(width)} #{(char * ready).green}#{(char * pending).yellow}#{(char * missing).red}"
156
+ rescue StandardError => e
157
+ puts "#{name.ljust(width)} #{e.red}"
158
158
  end
159
- else
160
- raise "Invalid service type #{service.type}"
159
+ end
160
+ when 'ABS'
161
+ puts 'ABS Not OK'.red unless status_response
162
+ puts 'ABS is OK'.green if status_response
163
+ else
164
+ raise "Invalid service type #{service.type}"
161
165
  end
162
166
  end
163
167
 
@@ -170,9 +174,12 @@ class Utils
170
174
  end
171
175
 
172
176
  def self.get_service_object(type = '')
173
- nspooler_strings = ['ns', 'nspooler', 'nonstandard', 'nonstandard_pooler']
177
+ nspooler_strings = %w[ns nspooler nonstandard nonstandard_pooler]
178
+ abs_strings = %w[abs alwaysbescheduling always_be_scheduling]
174
179
  if nspooler_strings.include? type.downcase
175
180
  NonstandardPooler
181
+ elsif abs_strings.include? type.downcase
182
+ ABS
176
183
  else
177
184
  Pooler
178
185
  end
@@ -181,10 +188,10 @@ class Utils
181
188
  def self.get_service_config(config, options)
182
189
  # The top-level url, user, and token values in the config file are treated as defaults
183
190
  service_config = {
184
- 'url' => config['url'],
185
- 'user' => config['user'],
186
- 'token' => config['token'],
187
- 'type' => config['type'] || 'vmpooler'
191
+ 'url' => config['url'],
192
+ 'user' => config['user'],
193
+ 'token' => config['token'],
194
+ 'type' => config['type'] || 'vmpooler',
188
195
  }
189
196
 
190
197
  if config['services']
@@ -195,16 +202,15 @@ class Utils
195
202
  service_config.merge! values
196
203
  else
197
204
  # If the user provided a service name at the command line, use that service if posible, or fail
198
- if config['services'][options.service]
199
- # If the service is configured but some values are missing, use the top-level defaults to fill them in
200
- service_config.merge! config['services'][options.service]
201
- else
202
- raise ArgumentError, "Could not find a configured service named '#{options.service}' in ~/.vmfloaty.yml"
203
- end
205
+ raise ArgumentError, "Could not find a configured service named '#{options.service}' in ~/.vmfloaty.yml" unless config['services'][options.service]
206
+
207
+ # If the service is configured but some values are missing, use the top-level defaults to fill them in
208
+ service_config.merge! config['services'][options.service]
204
209
  end
205
210
  end
206
211
 
207
212
  # Prioritize an explicitly specified url, user, or token if the user provided one
213
+ service_config['priority'] = options.priority unless options.priority.nil?
208
214
  service_config['url'] = options.url unless options.url.nil?
209
215
  service_config['token'] = options.token unless options.token.nil?
210
216
  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.1'.freeze
4
+ VERSION = '0.10.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