Integrate nonstandard pooler service into vmfloaty

This commit is contained in:
Casey Williams 2017-09-19 12:08:09 -07:00
parent e78bcc6216
commit ca5b0f5e8b
12 changed files with 1190 additions and 482 deletions

View file

@ -1,6 +1,13 @@
require 'vmfloaty'
require 'webmock/rspec'
# Mock Commander Options object to allow pre-population with values
class MockOptions < Commander::Command::Options
def initialize(values = {})
@table = values
end
end
RSpec.configure do |config|
config.color = true
config.tty = true

View file

@ -0,0 +1,325 @@
require 'spec_helper'
require 'vmfloaty/utils'
require 'vmfloaty/errors'
require 'vmfloaty/nonstandard_pooler'
describe NonstandardPooler do
before :each do
@nspooler_url = 'https://nspooler.example.com'
@post_request_headers = {
'Accept' => '*/*',
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
'User-Agent' => 'Faraday v0.9.2',
'X-Auth-Token' => 'token-value'
}
@get_request_headers = {
'Accept' => '*/*',
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
'User-Agent' => 'Faraday v0.9.2',
'X-Auth-Token' => 'token-value'
}
@get_request_headers_notoken = @get_request_headers.tap do |headers|
headers.delete('X-Auth-Token')
end
end
describe '#list' do
before :each do
@status_response_body = <<-BODY
{
"ok": true,
"solaris-10-sparc": {
"total_hosts": 11,
"available_hosts": 11
},
"ubuntu-16.04-power8": {
"total_hosts": 10,
"available_hosts": 10
},
"aix-7.2-power": {
"total_hosts": 5,
"available_hosts": 4
}
}
BODY
end
it 'returns an array with operating systems from the pooler' do
stub_request(:get, "#{@nspooler_url}/status")
.to_return(status: 200, body: @status_response_body, headers: {})
list = NonstandardPooler.list(false, @nspooler_url, nil)
expect(list).to be_an_instance_of Array
end
it 'filters operating systems based on the filter param' do
stub_request(:get, "#{@nspooler_url}/status")
.to_return(status: 200, body: @status_response_body, headers: {})
list = NonstandardPooler.list(false, @nspooler_url, 'aix')
expect(list).to be_an_instance_of Array
expect(list.size).to equal 1
end
it 'returns nothing if the filter does not match' do
stub_request(:get, "#{@nspooler_url}/status")
.to_return(status: 199, body: @status_response_body, headers: {})
list = NonstandardPooler.list(false, @nspooler_url, 'windows')
expect(list).to be_an_instance_of Array
expect(list.size).to equal 0
end
end
describe '#list_active' do
before :each do
@token_status_body_active = <<-BODY
{
"ok": true,
"user": "first.last",
"created": "2017-09-18 01:25:41 +0000",
"last_accessed": "2017-09-21 19:46:25 +0000",
"reserved_hosts": ["sol10-9", "sol10-11"]
}
BODY
@token_status_body_empty = <<-BODY
{
"ok": true,
"user": "first.last",
"created": "2017-09-18 01:25:41 +0000",
"last_accessed": "2017-09-21 19:46:25 +0000",
"reserved_hosts": []
}
BODY
end
it 'prints an output of fqdn, template, and duration' do
allow(Auth).to receive(:token_status)
.with(false, @nspooler_url, 'token-value')
.and_return(JSON.parse(@token_status_body_active))
list = NonstandardPooler.list_active(false, @nspooler_url, 'token-value')
expect(list).to eql ['sol10-9', 'sol10-11']
end
end
describe '#retrieve' do
before :each do
@retrieve_response_body_single = <<-BODY
{
"ok": true,
"solaris-11-sparc": {
"hostname": "sol11-4.delivery.puppetlabs.net"
}
}
BODY
@retrieve_response_body_many = <<-BODY
{
"ok": true,
"solaris-10-sparc": {
"hostname": [
"sol10-9.delivery.puppetlabs.net",
"sol10-10.delivery.puppetlabs.net"
]
},
"aix-7.1-power": {
"hostname": "pe-aix-71-ci-acceptance.delivery.puppetlabs.net"
}
}
BODY
end
it 'raises an AuthError if the token is invalid' do
stub_request(:post, "#{@nspooler_url}/host/solaris-11-sparc")
.with(headers: @post_request_headers)
.to_return(status: 401, body: '{"ok":false,"reason": "token: token-value does not exist"}', headers: {})
vm_hash = { 'solaris-11-sparc' => 1 }
expect { NonstandardPooler.retrieve(false, vm_hash, 'token-value', @nspooler_url) }.to raise_error(AuthError)
end
it 'retrieves a single vm with a token' do
stub_request(:post, "#{@nspooler_url}/host/solaris-11-sparc")
.with(headers: @post_request_headers)
.to_return(status: 200, body: @retrieve_response_body_single, headers: {})
vm_hash = { 'solaris-11-sparc' => 1 }
vm_req = NonstandardPooler.retrieve(false, vm_hash, 'token-value', @nspooler_url)
expect(vm_req).to be_an_instance_of Hash
expect(vm_req['ok']).to equal true
expect(vm_req['solaris-11-sparc']['hostname']).to eq 'sol11-4.delivery.puppetlabs.net'
end
it 'retrieves a multiple vms with a token' do
stub_request(:post,"#{@nspooler_url}/host/aix-7.1-power+solaris-10-sparc+solaris-10-sparc")
.with(headers: @post_request_headers)
.to_return(status: 200, body: @retrieve_response_body_many, headers: {})
vm_hash = { 'aix-7.1-power' => 1, 'solaris-10-sparc' => 2 }
vm_req = NonstandardPooler.retrieve(false, vm_hash, 'token-value', @nspooler_url)
expect(vm_req).to be_an_instance_of Hash
expect(vm_req['ok']).to equal true
expect(vm_req['solaris-10-sparc']['hostname']).to be_an_instance_of Array
expect(vm_req['solaris-10-sparc']['hostname']).to eq ['sol10-9.delivery.puppetlabs.net', 'sol10-10.delivery.puppetlabs.net']
expect(vm_req['aix-7.1-power']['hostname']).to eq 'pe-aix-71-ci-acceptance.delivery.puppetlabs.net'
end
end
describe '#modify' do
before :each do
@modify_response_body_success = '{"ok":true}'
end
it 'raises an error if the user tries to modify an unsupported attribute' do
stub_request(:put, "https://nspooler.example.com/host/myfakehost").
with(body: {"{}"=>true},
headers: {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'Faraday v0.9.2', 'X-Auth-Token'=>'token-value'}).
to_return(status: 200, body: "", headers: {})
details = { lifetime: 12 }
expect { NonstandardPooler.modify(false, @nspooler_url, 'myfakehost', 'token-value', details) }
.to raise_error(ModifyError)
end
it 'modifies the reason of a vm' do
modify_request_body = { '{"reserved_for_reason":"testing"}' => true }
stub_request(:put, "#{@nspooler_url}/host/myfakehost")
.with(body: modify_request_body,
headers: @post_request_headers)
.to_return(status: 200, body: '{"ok": true}', headers: {})
modify_hash = { reason: "testing" }
modify_req = NonstandardPooler.modify(false, @nspooler_url, 'myfakehost', 'token-value', modify_hash)
expect(modify_req['ok']).to be true
end
end
describe '#status' do
before :each do
@status_response_body = '{"capacity":{"current":716,"total":717,"percent": 99.9},"status":{"ok":true,"message":"Battle station fully armed and operational."}}'
# TODO: make this report stuff like 'broken'
@status_response_body = <<-BODY
{
"ok": true,
"solaris-10-sparc": {
"total_hosts": 11,
"available_hosts": 10
},
"ubuntu-16.04-power8": {
"total_hosts": 10,
"available_hosts": 10
},
"aix-7.2-power": {
"total_hosts": 5,
"available_hosts": 4
}
}
BODY
end
it 'prints the status' do
stub_request(:get, "#{@nspooler_url}/status")
.with(headers: @get_request_headers)
.to_return(status: 200, body: @status_response_body, headers: {})
status = NonstandardPooler.status(false, @nspooler_url)
expect(status).to be_an_instance_of Hash
end
end
describe '#summary' do
before :each do
@status_response_body = <<-BODY
{
"ok": true,
"total": 57,
"available": 39,
"in_use": 16,
"resetting": 2,
"broken": 0
}
BODY
end
it 'prints the summary' do
stub_request(:get, "#{@nspooler_url}/summary")
.with(headers: @get_request_headers)
.to_return(status: 200, body: @status_response_body, headers: {})
summary = NonstandardPooler.summary(false, @nspooler_url)
expect(summary).to be_an_instance_of Hash
end
end
describe '#query' do
before :each do
@query_response_body = <<-BODY
{
"ok": true,
"sol10-11": {
"fqdn": "sol10-11.delivery.puppetlabs.net",
"os_triple": "solaris-10-sparc",
"reserved_by_user": "first.last",
"reserved_for_reason": "testing",
"hours_left_on_reservation": 29.12
}
}
BODY
end
it 'makes a query about a vm' do
stub_request(:get, "#{@nspooler_url}/host/sol10-11")
.with(headers: @get_request_headers_notoken)
.to_return(status: 200, body: @query_response_body, headers: {})
query_req = NonstandardPooler.query(false, @nspooler_url, 'sol10-11')
expect(query_req).to be_an_instance_of Hash
end
end
describe '#delete' do
before :each do
@delete_response_success = '{"ok": true}'
@delete_response_failure = '{"ok": false, "failure": "ERROR: fakehost does not exist"}'
end
it 'deletes a single existing vm' do
stub_request(:delete, "#{@nspooler_url}/host/sol11-7")
.with(headers: @post_request_headers)
.to_return(status: 200, body: @delete_response_success, headers: {})
request = NonstandardPooler.delete(false, @nspooler_url, 'sol11-7', 'token-value')
expect(request['sol11-7']['ok']).to be true
end
it 'does not delete a nonexistant vm' do
stub_request(:delete, "#{@nspooler_url}/host/fakehost")
.with(headers: @post_request_headers)
.to_return(status: 401, body: @delete_response_failure, headers: {})
request = NonstandardPooler.delete(false, @nspooler_url, 'fakehost', 'token-value')
expect(request['fakehost']['ok']).to be false
end
end
describe '#snapshot' do
it 'logs an error explaining that snapshots are not supported' do
expect { NonstandardPooler.snapshot(false, @nspooler_url, 'myfakehost', 'token-value') }
.to raise_error(ModifyError)
end
end
describe '#revert' do
it 'logs an error explaining that snapshots are not supported' do
expect { NonstandardPooler.revert(false, @nspooler_url, 'myfakehost', 'token-value', 'snapshot-sha') }
.to raise_error(ModifyError)
end
end
describe '#disk' do
it 'logs an error explaining that disk modification is not supported' do
expect { NonstandardPooler.disk(false, @nspooler_url, 'myfakehost', 'token-value', 'diskname') }
.to raise_error(ModifyError)
end
end
end

View file

@ -91,16 +91,17 @@ describe Pooler do
end
it "raises a TokenError if token provided is nil" do
expect{ Pooler.modify(false, @vmpooler_url, 'myfakehost', nil, 12, nil) }.to raise_error(TokenError)
expect{ Pooler.modify(false, @vmpooler_url, 'myfakehost', nil, {}) }.to raise_error(TokenError)
end
it "modifies the TTL of a vm" do
modify_hash = { :lifetime => 12 }
stub_request(:put, "#{@vmpooler_url}/vm/fq6qlpjlsskycq6").
with(:body => {"{\"lifetime\":12}"=>true},
with(:body => {'{"lifetime":12}'=>true},
:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'Faraday v0.9.2', 'X-Auth-Token'=>'mytokenfile'}).
to_return(:status => 200, :body => @modify_response_body_success, :headers => {})
modify_req = Pooler.modify(false, @vmpooler_url, 'fq6qlpjlsskycq6', 'mytokenfile', 12, nil)
modify_req = Pooler.modify(false, @vmpooler_url, 'fq6qlpjlsskycq6', 'mytokenfile', modify_hash)
expect(modify_req["ok"]).to be true
end
end

View file

@ -0,0 +1,79 @@
require_relative '../../lib/vmfloaty/service'
describe Service do
describe '#initialize' do
it 'store configuration options' do
options = MockOptions.new({})
config = {'url' => 'http://example.url'}
service = Service.new(options, config)
expect(service.config).to include config
end
end
describe '#get_new_token' do
it 'prompts the user for their password and retrieves a token' do
config = { 'user' => 'first.last', 'url' => 'http://default.url' }
service = Service.new(MockOptions.new, config)
allow(STDOUT).to receive(:puts).with('Enter your pooler service password:')
allow(Commander::UI).to(receive(:password)
.with('Enter your pooler service password:', '*')
.and_return('hunter2'))
allow(Auth).to(receive(:get_token)
.with(nil, config['url'], config['user'], 'hunter2')
.and_return('token-value'))
expect(service.get_new_token(nil)).to eql 'token-value'
end
it 'prompts the user for their username and password if the username is unknown' do
config = { 'url' => 'http://default.url' }
service = Service.new(MockOptions.new({}), config)
allow(STDOUT).to receive(:puts).with 'Enter your pooler service username:'
allow(STDOUT).to receive(:puts).with "\n"
allow(STDIN).to receive(:gets).and_return('first.last')
allow(Commander::UI).to(receive(:password)
.with('Enter your pooler service password:', '*')
.and_return('hunter2'))
allow(Auth).to(receive(:get_token)
.with(nil, config['url'], 'first.last', 'hunter2')
.and_return('token-value'))
expect(service.get_new_token(nil)).to eql 'token-value'
end
end
describe '#delete_token' do
it 'deletes a token' do
service = Service.new(MockOptions.new,{'user' => 'first.last', 'url' => 'http://default.url'})
allow(Commander::UI).to(receive(:password)
.with('Enter your pooler service password:', '*')
.and_return('hunter2'))
allow(Auth).to(receive(:delete_token)
.with(nil, 'http://default.url', 'first.last', 'hunter2', 'token-value')
.and_return('ok' => true))
expect(service.delete_token(nil, 'token-value')).to eql({'ok' => true})
end
end
describe '#token_status' do
it 'reports the status of a token' do
config = {
'user' => 'first.last',
'url' => 'http://default.url'
}
options = MockOptions.new('token' => 'token-value')
service = Service.new(options, config)
status = {
'ok' => true,
'user' => config['user'],
'created' => '2017-09-22 02:04:18 +0000',
'last_accessed' => '2017-09-22 02:04:28 +0000',
'reserved_hosts' => []
}
allow(Auth).to(receive(:token_status)
.with(nil, config['url'], 'token-value')
.and_return(status))
expect(service.token_status(nil, 'token-value')).to eql(status)
end
end
end

View file

@ -1,22 +1,63 @@
require 'spec_helper'
require 'json'
require 'commander/command'
require_relative '../../lib/vmfloaty/utils'
describe Utils do
describe "#get_hosts" do
describe "#format_hosts" do
before :each do
@hostname_hash = "{\"ok\":true,\"debian-7-i386\":{\"hostname\":[\"sc0o4xqtodlul5w\",\"4m4dkhqiufnjmxy\"]},\"debian-7-x86_64\":{\"hostname\":\"zb91y9qbrbf6d3q\"},\"domain\":\"company.com\"}"
@format_hash = "{\"debian-7-i386\":[\"sc0o4xqtodlul5w.company.com\",\"4m4dkhqiufnjmxy.company.com\"],\"debian-7-x86_64\":\"zb91y9qbrbf6d3q.company.com\"}"
@vmpooler_response_body ='{
"ok": true,
"domain": "delivery.mycompany.net",
"ubuntu-1610-x86_64": {
"hostname": ["gdoy8q3nckuob0i", "ctnktsd0u11p9tm"]
},
"centos-7-x86_64": {
"hostname": "dlgietfmgeegry2"
}
}'
@nonstandard_response_body = '{
"ok": true,
"solaris-10-sparc": {
"hostname": ["sol10-10.delivery.mycompany.net", "sol10-11.delivery.mycompany.net"]
},
"ubuntu-16.04-power8": {
"hostname": "power8-ubuntu16.04-6.delivery.mycompany.net"
}
}'
@vmpooler_output = <<-OUT
- gdoy8q3nckuob0i.delivery.mycompany.net (ubuntu-1610-x86_64)
- ctnktsd0u11p9tm.delivery.mycompany.net (ubuntu-1610-x86_64)
- dlgietfmgeegry2.delivery.mycompany.net (centos-7-x86_64)
OUT
@nonstandard_output = <<-OUT
- sol10-10.delivery.mycompany.net (solaris-10-sparc)
- sol10-11.delivery.mycompany.net (solaris-10-sparc)
- power8-ubuntu16.04-6.delivery.mycompany.net (ubuntu-16.04-power8)
OUT
end
it "formats a hostname hash into os, hostnames, and domain name" do
it "formats a hostname hash from vmpooler into a list that includes the os" do
expect { Utils.format_hosts(JSON.parse(@vmpooler_response_body)) }.to output( @vmpooler_output).to_stdout_from_any_process
end
expect(Utils.format_hosts(JSON.parse(@hostname_hash))).to eq @format_hash
it "formats a hostname hash from the nonstandard pooler into a list that includes the os" do
expect { Utils.format_hosts(JSON.parse(@nonstandard_response_body)) }.to output(@nonstandard_output).to_stdout_from_any_process
end
end
describe "#get_service_from_config" do
describe "#get_service_object" do
it "assumes vmpooler by default" do
expect(Utils.get_service_object).to be Pooler
end
it "uses nspooler when told explicitly" do
expect(Utils.get_service_object "nspooler").to be NonstandardPooler
end
end
describe "#get_service_config" do
before :each do
@default_config = {
"url" => "http://default.url",
@ -41,29 +82,34 @@ describe Utils do
it "returns the first service configured under 'services' as the default if available" do
config = @default_config.merge @services_config
expect(Utils.get_service_from_config(config)).to include @services_config['services']['vm']
end
it "uses top-level service config values as defaults when service values are missing" do
config = {"services" => { "vm" => {}}}
config.merge! @default_config
expect(Utils.get_service_from_config(config, 'vm')).to include @default_config
options = MockOptions.new({})
expect(Utils.get_service_config(config, options)).to include @services_config['services']['vm']
end
it "allows selection by configured service key" do
config = @default_config.merge @services_config
expect(Utils.get_service_from_config(config, 'ns')).to include @services_config['services']['ns']
options = MockOptions.new({:service => "ns"})
expect(Utils.get_service_config(config, options)).to include @services_config['services']['ns']
end
it "fills in missing values in configured services with the defaults" do
it "uses top-level service config values as defaults when configured service values are missing" do
config = @default_config.merge @services_config
config["services"]['vm'].delete 'url'
expect(Utils.get_service_from_config(config, 'vm')['url']).to eq 'http://default.url'
options = MockOptions.new({:service => "vm"})
expect(Utils.get_service_config(config, options)['url']).to eq 'http://default.url'
end
it "returns an empty hash if passed a service name that hasn't been configured" do
it "raises an error if passed a service name that hasn't been configured" do
config = @default_config.merge @services_config
expect(Utils.get_service_from_config(config, 'nil')).to eq({})
options = MockOptions.new({:service => "none"})
expect { Utils.get_service_config(config, options) }.to raise_error ArgumentError
end
it "prioritizes values passed as command line options over configuration options" do
config = @default_config
options = MockOptions.new({:url => "http://alternate.url", :token => "alternate-token"})
expected = config.merge({"url" => "http://alternate.url", "token" => "alternate-token"})
expect(Utils.get_service_config(config, options)).to include expected
end
end
@ -83,60 +129,97 @@ describe Utils do
end
end
describe '#prettyprint_hosts' do
let(:host_without_tags) { 'mcpy42eqjxli9g2' }
let(:host_with_tags) { 'aiydvzpg23r415q' }
describe '#pretty_print_hosts' do
let(:url) { 'http://pooler.example.com' }
let(:host_info_with_tags) do
{
host_with_tags => {
"template" => "redhat-7-x86_64",
"lifetime" => 48,
"running" => 7.67,
"tags" => {
"user" => "bob",
"role" => "agent"
it 'prints a vmpooler output with host fqdn, template and duration info' do
hostname = 'mcpy42eqjxli9g2'
response_body = { hostname => {
'template' => 'ubuntu-1604-x86_64',
'lifetime' => 12,
'running' => 9.66,
'state' => 'running',
'ip' => '127.0.0.1',
'domain' => 'delivery.mycompany.net'
}}
output = "- mcpy42eqjxli9g2.delivery.mycompany.net (ubuntu-1604-x86_64, 9.66/12 hours)"
expect(Utils).to receive(:puts).with(output)
service = Service.new(MockOptions.new, {'url' => url})
allow(service).to receive(:query)
.with(nil, hostname)
.and_return(response_body)
Utils.pretty_print_hosts(nil, service, hostname)
end
it 'prints a vmpooler output with host fqdn, template, duration info, and tags when supplied' do
hostname = 'aiydvzpg23r415q'
response_body = { hostname => {
'template' => 'redhat-7-x86_64',
'lifetime' => 48,
'running' => 7.67,
'state' => 'running',
'tags' => {
'user' => 'bob',
'role' => 'agent'
},
"domain" => "delivery.puppetlabs.net"
}
}
'ip' => '127.0.0.1',
'domain' => 'delivery.mycompany.net'
}}
output = "- aiydvzpg23r415q.delivery.mycompany.net (redhat-7-x86_64, 7.67/48 hours, user: bob, role: agent)"
expect(Utils).to receive(:puts).with(output)
service = Service.new(MockOptions.new, {'url' => url})
allow(service).to receive(:query)
.with(nil, hostname)
.and_return(response_body)
Utils.pretty_print_hosts(nil, service, hostname)
end
let(:host_info_without_tags) do
{
host_without_tags => {
"template" => "ubuntu-1604-x86_64",
"lifetime" => 12,
"running" => 9.66,
"domain" => "delivery.puppetlabs.net"
}
}
it 'prints a nonstandard pooler output with host, template, and time remaining' do
hostname = "sol11-9.delivery.mycompany.net"
response_body = { hostname => {
'fqdn' => hostname,
'os_triple' => 'solaris-11-sparc',
'reserved_by_user' => 'first.last',
'reserved_for_reason' => '',
'hours_left_on_reservation' => 35.89
}}
output = "- sol11-9.delivery.mycompany.net (solaris-11-sparc, 35.89h remaining)"
expect(Utils).to receive(:puts).with(output)
service = Service.new(MockOptions.new, {'url' => url, 'type' => 'ns'})
allow(service).to receive(:query)
.with(nil, hostname)
.and_return(response_body)
Utils.pretty_print_hosts(nil, service, hostname)
end
let(:output_with_tags) { "- #{host_with_tags}.delivery.puppetlabs.net (redhat-7-x86_64, 7.67/48 hours, user: bob, role: agent)" }
let(:output_without_tags) { "- #{host_without_tags}.delivery.puppetlabs.net (ubuntu-1604-x86_64, 9.66/12 hours)" }
it 'prints a nonstandard pooler output with host, template, time remaining, and reason' do
hostname = 'sol11-9.delivery.mycompany.net'
response_body = { hostname => {
'fqdn' => hostname,
'os_triple' => 'solaris-11-sparc',
'reserved_by_user' => 'first.last',
'reserved_for_reason' => 'testing',
'hours_left_on_reservation' => 35.89
}}
output = "- sol11-9.delivery.mycompany.net (solaris-11-sparc, 35.89h remaining, reason: testing)"
it 'prints an output with host fqdn, template and duration info' do
allow(Utils).to receive(:get_vm_info).
with(host_without_tags, false, url).
and_return(host_info_without_tags)
expect(Utils).to receive(:puts).with(output)
expect(Utils).to receive(:puts).with("Running VMs:")
expect(Utils).to receive(:puts).with(output_without_tags)
service = Service.new(MockOptions.new, {'url' => url, 'type' => 'ns'})
allow(service).to receive(:query)
.with(nil, hostname)
.and_return(response_body)
Utils.prettyprint_hosts(host_without_tags, false, url)
end
it 'prints an output with host fqdn, template, duration info, and tags when supplied' do
allow(Utils).to receive(:get_vm_info).
with(host_with_tags, false, url).
and_return(host_info_with_tags)
expect(Utils).to receive(:puts).with("Running VMs:")
expect(Utils).to receive(:puts).with(output_with_tags)
Utils.prettyprint_hosts(host_with_tags, false, url)
Utils.pretty_print_hosts(nil, service, hostname)
end
end
end