vmpooler/spec/unit/providers/vsphere_spec.rb
kirby@puppetlabs.com 811fd8b60f (POOLER-158) Add capability to provision VMs on demand
This change adds a capability to vmpooler to provision instances on
demand. Without this change vmpooler only supports retrieving machines
from pre-provisioned pools.

Additionally, this change refactors redis interactions to reduce round
trips to redis. Specifically, multi and pipelined redis commands are
added where possible to reduce the number of times we are calling redis.

To support the redis refactor the redis interaction has changed to
leveraging a connection pool. In addition to offering multiple
connections for pool manager to use, the redis interactions in pool
manager are now thread safe.

Ready TTL is now a global parameter that can be set as a default for all
pools. A default of 0 has been removed, because this is an unreasonable
default behavior, which would leave a provisioned instance in the pool
indefinitely.

Pool empty messages have been removed when the pool size is set to 0.
Without this change, when a pool was set to a size of 0 the API and pool
manager would both show that a pool is empty.
2020-06-03 14:00:04 -07:00

3501 lines
121 KiB
Ruby

require 'spec_helper'
require 'mock_redis'
require 'vmpooler/providers/vsphere'
RSpec::Matchers.define :relocation_spec_with_host do |value|
match { |actual| actual[:spec].host == value }
end
RSpec::Matchers.define :create_virtual_disk_with_size do |value|
match { |actual| actual[:spec].capacityKb == value * 1024 * 1024 }
end
RSpec::Matchers.define :create_vm_spec do |new_name,target_folder_name,datastore|
match { |actual|
# Should have the correct new name
actual[:name] == new_name &&
# Should be in the new folder
actual[:folder].name == target_folder_name &&
# Should be poweredOn after clone
actual[:spec].powerOn == true &&
# Should be on the correct datastore
actual[:spec][:location].datastore.name == datastore &&
# Should contain annotation data
actual[:spec][:config].annotation != '' &&
# Should contain VIC information
actual[:spec][:config].extraConfig[0][:key] == 'guestinfo.hostname' &&
actual[:spec][:config].extraConfig[0][:value] == new_name
}
end
RSpec::Matchers.define :create_snapshot_spec do |new_snapshot_name|
match { |actual|
# Should have the correct new name
actual[:name] == new_snapshot_name &&
# Should snapshot the memory too
actual[:memory] == true &&
# Should quiesce the disk
actual[:quiesce] == true
}
end
describe 'Vmpooler::PoolManager::Provider::VSphere' do
let(:logger) { MockLogger.new }
let(:metrics) { Vmpooler::DummyStatsd.new }
let(:poolname) { 'pool1'}
let(:provider_options) { { 'param' => 'value' } }
let(:datacenter_name) { 'MockDC' }
let(:config) { YAML.load(<<-EOT
---
:config:
max_tries: 3
retry_factor: 10
:providers:
:vsphere:
server: "vcenter.domain.local"
username: "vcenter_user"
password: "vcenter_password"
insecure: true
# Drop the connection pool timeout way down for spec tests so they fail fast
connection_pool_timeout: 1
datacenter: MockDC
create_linked_clones: true
:pools:
- name: '#{poolname}'
alias: [ 'mockpool' ]
template: 'Templates/pool1'
folder: 'Pooler/pool1'
datastore: 'datastore0'
size: 5
timeout: 10
ready_ttl: 1440
clone_target: 'cluster1'
EOT
)
}
let(:connection_options) {{}}
let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) }
let(:vmname) { 'vm1' }
let(:redis_connection_pool) { Vmpooler::PoolManager::GenericConnectionPool.new(
metrics: metrics,
metric_prefix: 'redis_connection_pool',
size: 1,
timeout: 5
) { MockRedis.new }
}
subject { Vmpooler::PoolManager::Provider::VSphere.new(config, logger, metrics, redis_connection_pool, 'vsphere', provider_options) }
before(:each) do
allow(subject).to receive(:vsphere_connection_ok?).and_return(true)
end
describe '#name' do
it 'should be vsphere' do
expect(subject.name).to eq('vsphere')
end
end
describe '#folder_configured?' do
let(:folder_title) { 'folder1' }
let(:other_folder) { 'folder2' }
let(:base_folder) { 'dc1/vm/base' }
let(:configured_folders) { { folder_title => base_folder } }
let(:whitelist) { nil }
it 'should return true when configured_folders includes the folder_title' do
expect(subject.folder_configured?(folder_title, base_folder, configured_folders, whitelist)).to be true
end
it 'should return false when title is not in configured_folders' do
expect(subject.folder_configured?(other_folder, base_folder, configured_folders, whitelist)).to be false
end
context 'with another base folder' do
let(:base_folder) { 'dc2/vm/base' }
let(:configured_folders) { { folder_title => 'dc1/vm/base' } }
it 'should return false' do
expect(subject.folder_configured?(folder_title, base_folder, configured_folders, whitelist)).to be false
end
end
context 'with a whitelist set' do
let(:whitelist) { [ other_folder ] }
it 'should return true' do
expect(subject.folder_configured?(other_folder, base_folder, configured_folders, whitelist)).to be true
end
end
context 'with string whitelist value' do
let(:whitelist) { 'whitelist' }
it 'should raise an error' do
expect(whitelist).to receive(:include?).and_raise('mockerror')
expect{ subject.folder_configured?(other_folder, base_folder, configured_folders, whitelist) }.to raise_error(RuntimeError, 'mockerror')
end
end
end
describe '#destroy_vm_and_log' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
:powerstate => 'poweredOn',
})
}
let(:pool) { 'pool1' }
let(:power_off_task) { mock_RbVmomi_VIM_Task() }
let(:destroy_task) { mock_RbVmomi_VIM_Task() }
let(:data_ttl) { 1 }
let(:finish) { '0.00' }
let(:now) { Time.now }
context 'when destroying a vm' do
before(:each) do
allow(power_off_task).to receive(:wait_for_completion)
allow(destroy_task).to receive(:wait_for_completion)
allow(vm_object).to receive(:PowerOffVM_Task).and_return(power_off_task)
allow(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
end
it 'should log a message that the vm is destroyed' do
# Ensure Time returns a consistent value so finish is predictable
# Otherwise finish occasionally increases to 0.01 and causes a failure
allow(Time).to receive(:now).and_return(Time.now)
expect(logger).to receive(:log).with('s', "[-] [#{pool}] '#{vmname}' destroyed in #{finish} seconds")
subject.destroy_vm_and_log(vmname, vm_object, pool, data_ttl)
end
it 'should record metrics' do
expect(metrics).to receive(:timing).with('redis_connection_pool.waited', 0)
expect(metrics).to receive(:timing).with("destroy.#{pool}", finish)
subject.destroy_vm_and_log(vmname, vm_object, pool, data_ttl)
end
it 'should power off and destroy the vm' do
allow(destroy_task).to receive(:wait_for_completion)
expect(vm_object).to receive(:PowerOffVM_Task).and_return(power_off_task)
expect(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
subject.destroy_vm_and_log(vmname, vm_object, pool, data_ttl)
end
end
context 'with a powered off vm' do
before(:each) do
vm_object.runtime.powerState = 'poweredOff'
end
it 'should destroy the vm without attempting to power it off' do
allow(destroy_task).to receive(:wait_for_completion)
expect(vm_object).to_not receive(:PowerOffVM_Task)
expect(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
subject.destroy_vm_and_log(vmname, vm_object, pool, data_ttl)
end
end
context 'with a folder object' do
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => vmname }) }
it 'should log that a folder object was received' do
expect(logger).to receive(:log).with('s', "[!] [#{pool}] '#{vmname}' is a folder, bailing on destroying")
expect{ subject.destroy_vm_and_log(vmname, folder_object, pool, data_ttl) }.to raise_error(RuntimeError, 'Expected VM, but received a folder object')
end
it 'should raise an error' do
expect{ subject.destroy_vm_and_log(vmname, folder_object, pool, data_ttl) }.to raise_error(RuntimeError, 'Expected VM, but received a folder object')
end
end
context 'with an error that is not a RuntimeError' do
it 'should retry three times' do
expect(vm_object).to receive(:PowerOffVM_Task).and_throw(:powerofffailed, 'failed').exactly(3).times
expect{ subject.destroy_vm_and_log(vmname, vm_object, pool, data_ttl) }.to raise_error(/failed/)
end
end
end
describe '#destroy_folder_and_children' do
let(:data_ttl) { 1 }
let(:config) {
{
redis: {
'data_ttl' => data_ttl
}
}
}
let(:foldername) { 'pool1' }
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => foldername }) }
before(:each) do
$config = config
end
context 'with an empty folder' do
it 'should destroy the folder' do
expect(subject).to_not receive(:destroy_vm_and_log)
expect(subject).to receive(:destroy_folder).with(folder_object).and_return(nil)
subject.destroy_folder_and_children(folder_object)
end
end
context 'with a folder containing vms' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) }
before(:each) do
folder_object.childEntity << vm_object
end
it 'should destroy the vms' do
allow(subject).to receive(:destroy_vm_and_log).and_return(nil)
allow(subject).to receive(:destroy_folder).and_return(nil)
expect(subject).to receive(:destroy_vm_and_log).with(vmname, vm_object, foldername, data_ttl)
subject.destroy_folder_and_children(folder_object)
end
end
it 'should raise any errors' do
expect(subject).to receive(:destroy_folder).and_throw('mockerror')
expect{ subject.destroy_folder_and_children(folder_object) }.to raise_error(/mockerror/)
end
end
describe '#destroy_folder' do
let(:foldername) { 'pool1' }
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => foldername }) }
let(:destroy_task) { mock_RbVmomi_VIM_Task() }
before(:each) do
allow(folder_object).to receive(:Destroy_Task).and_return(destroy_task)
allow(destroy_task).to receive(:wait_for_completion)
end
it 'should destroy the folder' do
expect(folder_object).to receive(:Destroy_Task).and_return(destroy_task)
subject.destroy_folder(folder_object)
end
it 'should log that the folder is being destroyed' do
expect(logger).to receive(:log).with('s', "[-] [#{foldername}] removing unconfigured folder")
subject.destroy_folder(folder_object)
end
it 'should retry three times when failing' do
expect(folder_object).to receive(:Destroy_Task).and_throw('mockerror').exactly(3).times
expect{ subject.destroy_folder(folder_object) }.to raise_error(/mockerror/)
end
end
describe '#purge_unconfigured_folders' do
let(:folder_title) { 'folder1' }
let(:base_folder) { 'dc1/vm/base' }
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => base_folder }) }
let(:child_folder) { mock_RbVmomi_VIM_Folder({ :name => folder_title }) }
let(:whitelist) { nil }
let(:base_folders) { [ base_folder ] }
let(:configured_folders) { { folder_title => base_folder } }
let(:folder_children) { [ folder_title => child_folder ] }
let(:empty_list) { [] }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
context 'with an empty folder' do
it 'should not attempt to destroy any folders' do
expect(subject).to receive(:get_folder_children).with(base_folder, connection).and_return(empty_list)
expect(subject).to_not receive(:destroy_folder_and_children)
subject.purge_unconfigured_folders(base_folders, configured_folders, whitelist)
end
end
it 'should retrieve the folder children' do
expect(subject).to receive(:get_folder_children).with(base_folder, connection).and_return(folder_children)
allow(subject).to receive(:folder_configured?).and_return(true)
subject.purge_unconfigured_folders(base_folders, configured_folders, whitelist)
end
context 'with a folder that is not configured' do
before(:each) do
expect(subject).to receive(:get_folder_children).with(base_folder, connection).and_return(folder_children)
allow(subject).to receive(:folder_configured?).and_return(false)
end
it 'should destroy the folder and children' do
expect(subject).to receive(:destroy_folder_and_children).with(child_folder).and_return(nil)
subject.purge_unconfigured_folders(base_folders, configured_folders, whitelist)
end
end
it 'should raise any errors' do
expect(subject).to receive(:get_folder_children).and_throw('mockerror')
expect{ subject.purge_unconfigured_folders(base_folders, configured_folders, whitelist) }.to raise_error(/mockerror/)
end
end
describe '#get_folder_children' do
let(:base_folder) { 'dc1/vm/base' }
let(:base_folder_object) { mock_RbVmomi_VIM_Folder({ :name => base_folder }) }
let(:foldername) { 'folder1' }
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => foldername }) }
let(:folder_return) { [ { foldername => folder_object } ] }
before(:each) do
base_folder_object.childEntity << folder_object
end
it 'should return an array of configured folder hashes' do
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(base_folder_object)
result = subject.get_folder_children(foldername, connection)
expect(result).to eq(folder_return)
end
it 'should raise any errors' do
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_throw('mockerror')
expect{ subject.get_folder_children(foldername, connection) }.to raise_error(/mockerror/)
end
end
describe '#vms_in_pool' do
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => 'pool1'}) }
let(:pool_config) { config[:pools][0] }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
context 'Given a pool folder that is missing' do
before(:each) do
expect(subject).to receive(:find_vm_folder).with(poolname,connection).and_return(nil)
end
it 'should get a connection' do
expect(subject).to receive(:connect_to_vsphere).and_return(connection)
subject.vms_in_pool(poolname)
end
it 'should return an empty array' do
result = subject.vms_in_pool(poolname)
expect(result).to eq([])
end
end
context 'Given an empty pool folder' do
before(:each) do
expect(subject).to receive(:find_vm_folder).with(poolname,connection).and_return(folder_object)
end
it 'should get a connection' do
expect(subject).to receive(:connect_to_vsphere).and_return(connection)
subject.vms_in_pool(poolname)
end
it 'should return an empty array' do
result = subject.vms_in_pool(poolname)
expect(result).to eq([])
end
end
context 'Given a pool folder with many VMs' do
let(:expected_vm_list) {[
{ 'name' => 'vm1'},
{ 'name' => 'vm2'},
{ 'name' => 'vm3'}
]}
before(:each) do
expected_vm_list.each do |vm_hash|
mock_vm = mock_RbVmomi_VIM_VirtualMachine({ :name => vm_hash['name'] })
# Add the mocked VM to the folder
folder_object.childEntity << mock_vm
end
expect(subject).to receive(:find_vm_folder).with(poolname,connection).and_return(folder_object)
end
it 'should get a connection' do
expect(subject).to receive(:connect_to_vsphere).and_return(connection)
subject.vms_in_pool(poolname)
end
it 'should list all VMs in the VM folder for the pool' do
result = subject.vms_in_pool(poolname)
expect(result).to eq(expected_vm_list)
end
end
context 'given a pool folder with a folder and vms' do
let(:expected_vm_list) {[
{ 'name' => 'vm1'},
{ 'name' => 'vm2'},
{ 'name' => 'vm3'}
]}
let(:folder_object2) { mock_RbVmomi_VIM_Folder({ :name => 'pool2'}) }
before(:each) do
expected_vm_list.each do |vm_hash|
mock_vm = mock_RbVmomi_VIM_VirtualMachine({ :name => vm_hash['name'] })
# Add the mocked VM to the folder
folder_object.childEntity << mock_vm
end
folder_object.childEntity << folder_object2
expect(subject).to receive(:find_vm_folder).with(poolname,connection).and_return(folder_object)
end
it 'should return the vms without the folder' do
result = subject.vms_in_pool(poolname)
expect(result).to eq(expected_vm_list)
end
end
end
describe '#get_vm' do
let(:vm_object) { nil }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
expect(subject).to receive(:find_vm).with(poolname,vmname,connection).and_return(vm_object)
end
context 'when VM does not exist' do
it 'should return nil' do
expect(subject.get_vm(poolname,vmname)).to be_nil
end
end
context 'when VM exists but is missing information' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
})
}
it 'should return a hash' do
expect(subject.get_vm(poolname,vmname)).to be_kind_of(Hash)
end
it 'should return the VM name' do
result = subject.get_vm(poolname,vmname)
expect(result['name']).to eq(vmname)
end
['hostname','boottime','powerstate'].each do |testcase|
it "should return nil for #{testcase}" do
result = subject.get_vm(poolname,vmname)
expect(result[testcase]).to be_nil
end
end
end
context 'when VM exists and contains all information' do
let(:vm_hostname) { "#{vmname}.demo.local" }
let(:boot_time) { Time.now }
let(:power_state) { 'MockPowerState' }
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
:hostname => vm_hostname,
:powerstate => power_state,
:boottime => boot_time,
# This path should match the folder in the mocked pool in the config above
:path => [
{ :type => 'folder', :name => 'Datacenters' },
{ :type => 'datacenter', :name => 'DC01' },
{ :type => 'folder', :name => 'vm' },
{ :type => 'folder', :name => 'Pooler' },
{ :type => 'folder', :name => 'pool1'},
]
})
}
let(:pool_info) { config[:pools][0]}
it 'should return a hash' do
expect(subject.get_vm(poolname,vmname)).to be_kind_of(Hash)
end
it 'should return the VM name' do
result = subject.get_vm(poolname,vmname)
expect(result['name']).to eq(vmname)
end
it 'should return the VM hostname' do
result = subject.get_vm(poolname,vmname)
expect(result['hostname']).to eq(vm_hostname)
end
it 'should return the template name' do
result = subject.get_vm(poolname,vmname)
expect(result['template']).to eq(pool_info['template'])
end
it 'should return the pool name' do
result = subject.get_vm(poolname,vmname)
expect(result['poolname']).to eq(pool_info['name'])
end
it 'should return the boot time' do
result = subject.get_vm(poolname,vmname)
expect(result['boottime']).to eq(boot_time)
end
it 'should return the powerstate' do
result = subject.get_vm(poolname,vmname)
expect(result['powerstate']).to eq(power_state)
end
end
end
describe '#create_vm' do
let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({
:datastores => ['datastore0'],
:vmfolder_tree => {
'Templates' => { :children => {
'pool1' => { :object_type => 'vm', :name => 'pool1' },
}},
'Pooler' => { :children => {
'pool1' => nil,
}},
},
:hostfolder_tree => {
'cluster1' => {:object_type => 'compute_resource'},
}
})
}
let(:clone_vm_task) { mock_RbVmomi_VIM_Task() }
let(:new_vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) }
let(:new_template_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object)
end
context 'Given an invalid pool name' do
it 'should raise an error' do
expect{ subject.create_vm('missing_pool', vmname) }.to raise_error(/missing_pool does not exist/)
end
end
context 'Given an invalid template path in the pool config' do
before(:each) do
config[:pools][0]['template'] = 'bad_template'
end
it 'should raise an error' do
expect{ subject.create_vm(poolname, vmname) }.to raise_error(/did not specify a full path for the template/)
end
end
context 'Given a template that starts with /' do
before(:each) do
config[:pools][0]['template'] = '/bad_template'
end
it 'should raise an error' do
expect{ subject.create_vm(poolname, vmname) }.to raise_error(/did not specify a full path for the template/)
end
end
context 'Given a template that ends with /' do
before(:each) do
config[:pools][0]['template'] = 'bad_template/'
end
it 'should raise an error' do
expect{ subject.create_vm(poolname, vmname) }.to raise_error(/did not specify a full path for the template/)
end
end
context 'Given a template VM that does not exist' do
before(:each) do
config[:pools][0]['template'] = 'Templates/missing_template'
expect(subject).to receive(:find_template_vm).and_raise("specifies a template VM of #{vmname} which does not exist")
end
it 'should raise an error' do
expect{ subject.create_vm(poolname, vmname) }.to raise_error(/specifies a template VM of .+ which does not exist/)
end
end
context 'when create_vm_folder returns nil' do
before(:each) do
template_vm = new_template_object
allow(subject).to receive(:find_template_vm).and_return(new_template_object)
expect(subject).to receive(:find_vm_folder).and_return(nil)
end
it 'should raise an error' do
expect{ subject.create_vm(poolname, vmname) }.to raise_error(ArgumentError)
end
end
context 'Given a successful creation' do
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => 'pool1'}) }
before(:each) do
template_vm = new_template_object
allow(subject).to receive(:find_template_vm).and_return(new_template_object)
allow(template_vm).to receive(:CloneVM_Task).and_return(clone_vm_task)
allow(clone_vm_task).to receive(:wait_for_completion).and_return(new_vm_object)
allow(subject).to receive(:find_vm_folder).and_return(folder_object)
end
it 'should return a hash' do
result = subject.create_vm(poolname, vmname)
expect(result.is_a?(Hash)).to be true
end
it 'should use the appropriate Create_VM spec' do
template_vm = new_template_object
expect(template_vm).to receive(:CloneVM_Task)
.with(create_vm_spec(vmname,'pool1','datastore0'))
.and_return(clone_vm_task)
subject.create_vm(poolname, vmname)
end
it 'should have the new VM name' do
result = subject.create_vm(poolname, vmname)
expect(result['name']).to eq(vmname)
end
end
end
describe '#create_disk' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) }
let(:datastorename) { 'datastore0' }
let(:disk_size) { 10 }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
allow(subject).to receive(:find_vm).with(poolname, vmname, connection).and_return(vm_object)
end
context 'Given an invalid pool name' do
it 'should raise an error' do
expect{ subject.create_disk('missing_pool',vmname,disk_size) }.to raise_error(/missing_pool does not exist/)
end
end
context 'Given a missing datastore in the pool config' do
before(:each) do
config[:pools][0]['datastore'] = nil
end
it 'should raise an error' do
expect{ subject.create_disk(poolname,vmname,disk_size) }.to raise_error(/does not have a datastore defined/)
end
end
context 'when VM does not exist' do
before(:each) do
expect(subject).to receive(:find_vm).with(poolname, vmname, connection).and_return(nil)
end
it 'should raise an error' do
expect{ subject.create_disk(poolname,vmname,disk_size) }.to raise_error(/VM #{vmname} .+ does not exist/)
end
end
context 'when adding the disk raises an error' do
before(:each) do
expect(subject).to receive(:add_disk).and_raise(RuntimeError,'Mock Disk Error')
end
it 'should raise an error' do
expect{ subject.create_disk(poolname,vmname,disk_size) }.to raise_error(/Mock Disk Error/)
end
end
context 'when adding the disk succeeds' do
before(:each) do
expect(subject).to receive(:add_disk).with(vm_object, disk_size, datastorename, connection, datacenter_name)
end
it 'should return true' do
expect(subject.create_disk(poolname,vmname,disk_size)).to be true
end
end
end
describe '#create_snapshot' do
let(:snapshot_task) { mock_RbVmomi_VIM_Task() }
let(:snapshot_name) { 'snapshot' }
let(:snapshot_tree) {{}}
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname, :snapshot_tree => snapshot_tree }) }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
allow(subject).to receive(:find_vm).with(poolname, vmname,connection).and_return(vm_object)
end
context 'when VM does not exist' do
before(:each) do
expect(subject).to receive(:find_vm).with(poolname, vmname, connection).and_return(nil)
end
it 'should raise an error' do
expect{ subject.create_snapshot(poolname,vmname,snapshot_name) }.to raise_error(/VM #{vmname} .+ does not exist/)
end
end
context 'when snapshot already exists' do
let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachineSnapshot() }
let(:snapshot_tree) { { snapshot_name => { :ref => snapshot_object } } }
it 'should raise an error' do
expect{ subject.create_snapshot(poolname,vmname,snapshot_name) }.to raise_error(/Snapshot #{snapshot_name} .+ already exists /)
end
end
context 'when snapshot raises an error' do
before(:each) do
expect(vm_object).to receive(:CreateSnapshot_Task).and_raise(RuntimeError,'Mock Snapshot Error')
end
it 'should raise an error' do
expect{ subject.create_snapshot(poolname,vmname,snapshot_name) }.to raise_error(/Mock Snapshot Error/)
end
end
context 'when snapshot succeeds' do
before(:each) do
expect(vm_object).to receive(:CreateSnapshot_Task)
.with(create_snapshot_spec(snapshot_name))
.and_return(snapshot_task)
expect(snapshot_task).to receive(:wait_for_completion).and_return(nil)
end
it 'should return true' do
expect(subject.create_snapshot(poolname,vmname,snapshot_name)).to be true
end
end
end
describe '#revert_snapshot' do
let(:snapshot_task) { mock_RbVmomi_VIM_Task() }
let(:snapshot_name) { 'snapshot' }
let(:snapshot_tree) { { snapshot_name => { :ref => snapshot_object } } }
let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachineSnapshot() }
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname, :snapshot_tree => snapshot_tree }) }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
allow(subject).to receive(:find_vm).with(poolname,vmname,connection).and_return(vm_object)
end
context 'when VM does not exist' do
before(:each) do
expect(subject).to receive(:find_vm).with(poolname,vmname,connection).and_return(nil)
end
it 'should raise an error' do
expect{ subject.revert_snapshot(poolname,vmname,snapshot_name) }.to raise_error(/VM #{vmname} .+ does not exist/)
end
end
context 'when snapshot does not exist' do
let(:snapshot_tree) {{}}
it 'should raise an error' do
expect{ subject.revert_snapshot(poolname,vmname,snapshot_name) }.to raise_error(/Snapshot #{snapshot_name} .+ does not exist /)
end
end
context 'when revert to snapshot raises an error' do
before(:each) do
expect(snapshot_object).to receive(:RevertToSnapshot_Task).and_raise(RuntimeError,'Mock Snapshot Error')
end
it 'should raise an error' do
expect{ subject.revert_snapshot(poolname,vmname,snapshot_name) }.to raise_error(/Mock Snapshot Error/)
end
end
context 'when revert to snapshot succeeds' do
before(:each) do
expect(snapshot_object).to receive(:RevertToSnapshot_Task).and_return(snapshot_task)
expect(snapshot_task).to receive(:wait_for_completion).and_return(nil)
end
it 'should return true' do
expect(subject.revert_snapshot(poolname,vmname,snapshot_name)).to be true
end
end
end
describe '#destroy_vm' do
let(:power_off_task) { mock_RbVmomi_VIM_Task() }
let(:destroy_task) { mock_RbVmomi_VIM_Task() }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
context 'Given a missing VM name' do
before(:each) do
expect(subject).to receive(:find_vm).and_return(nil)
end
it 'should return true' do
expect(subject.destroy_vm(poolname, 'missing_vm')).to be true
end
end
context 'Given a powered on VM' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
:powerstate => 'poweredOn',
})
}
before(:each) do
expect(subject).to receive(:find_vm).and_return(vm_object)
allow(vm_object).to receive(:PowerOffVM_Task).and_return(power_off_task)
allow(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
allow(power_off_task).to receive(:wait_for_completion)
allow(destroy_task).to receive(:wait_for_completion)
end
it 'should call PowerOffVM_Task on the VM' do
expect(vm_object).to receive(:PowerOffVM_Task).and_return(power_off_task)
subject.destroy_vm(poolname, vmname)
end
it 'should call Destroy_Task on the VM' do
expect(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
subject.destroy_vm(poolname, vmname)
end
it 'should return true' do
expect(subject.destroy_vm(poolname, vmname)).to be true
end
end
context 'Given a powered off VM' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
:powerstate => 'poweredOff',
})
}
before(:each) do
expect(subject).to receive(:find_vm).and_return(vm_object)
allow(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
allow(destroy_task).to receive(:wait_for_completion)
end
it 'should not call PowerOffVM_Task on the VM' do
expect(vm_object).to receive(:PowerOffVM_Task).exactly(0).times
subject.destroy_vm(poolname, vmname)
end
it 'should call Destroy_Task on the VM' do
expect(vm_object).to receive(:Destroy_Task).and_return(destroy_task)
subject.destroy_vm(poolname, vmname)
end
it 'should return true' do
expect(subject.destroy_vm(poolname, vmname)).to be true
end
end
end
describe '#vm_ready?' do
let(:domain) { nil }
context 'When a VM is ready' do
before(:each) do
expect(subject).to receive(:open_socket).with(vmname, domain)
end
it 'should return true' do
expect(subject.vm_ready?(poolname,vmname)).to be true
end
end
context 'When a VM is ready but the pool does not exist' do
# TODO not sure how to handle a VM that is passed in but
# not located in the pool. Is that ready or not?
before(:each) do
expect(subject).to receive(:open_socket).with(vmname, domain)
end
it 'should return true' do
expect(subject.vm_ready?('missing_pool',vmname)).to be true
end
end
context 'When an error occurs connecting to the VM' do
# TODO not sure how to handle a VM that is passed in but
# not located in the pool. Is that ready or not?
before(:each) do
expect(subject).to receive(:open_socket).and_raise(RuntimeError,'MockError')
end
it 'should return false' do
expect(subject.vm_ready?(poolname,vmname)).to be false
end
end
end
describe '#vm_exists?' do
it 'should return true when get_vm returns an object' do
allow(subject).to receive(:get_vm).with(poolname,vmname).and_return(mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }))
expect(subject.vm_exists?(poolname,vmname)).to eq(true)
end
it 'should return false when get_vm returns nil' do
allow(subject).to receive(:get_vm).with(poolname,vmname).and_return(nil)
expect(subject.vm_exists?(poolname,vmname)).to eq(false)
end
end
# vSphere helper methods
describe '#get_target_datacenter_from_config' do
let(:pool_dc) { 'PoolDC'}
let(:provider_dc) { 'ProvDC'}
context 'when not specified' do
let(:config) { YAML.load(<<-EOT
---
:config:
:providers:
:vsphere:
server: "vcenter.domain.local"
username: "vcenter_user"
password: "vcenter_password"
:pools:
- name: '#{poolname}'
EOT
)
}
it 'returns nil' do
expect(subject.get_target_datacenter_from_config(poolname)).to be_nil
end
end
context 'when specified only in the pool' do
let(:config) { YAML.load(<<-EOT
---
:config:
:providers:
:vsphere:
server: "vcenter.domain.local"
username: "vcenter_user"
password: "vcenter_password"
:pools:
- name: '#{poolname}'
datacenter: '#{pool_dc}'
EOT
)
}
it 'returns the pool datacenter' do
expect(subject.get_target_datacenter_from_config(poolname)).to eq(pool_dc)
end
end
context 'when specified only in the provider' do
let(:config) { YAML.load(<<-EOT
---
:config:
:providers:
:vsphere:
server: "vcenter.domain.local"
username: "vcenter_user"
password: "vcenter_password"
datacenter: '#{provider_dc}'
:pools:
- name: '#{poolname}'
EOT
)
}
it 'returns the provider datacenter' do
expect(subject.get_target_datacenter_from_config(poolname)).to eq(provider_dc)
end
end
context 'when specified in the provider and pool' do
let(:config) { YAML.load(<<-EOT
---
:config:
:providers:
:vsphere:
server: "vcenter.domain.local"
username: "vcenter_user"
password: "vcenter_password"
datacenter: '#{provider_dc}'
:pools:
- name: '#{poolname}'
datacenter: '#{pool_dc}'
EOT
)
}
it 'returns the pool datacenter' do
expect(subject.get_target_datacenter_from_config(poolname)).to eq(pool_dc)
end
end
end
# vSphere helper methods
describe '#ensured_vsphere_connection' do
let(:config) { YAML.load(<<-EOT
---
:config:
:providers:
:vsphere:
# Drop the connection pool timeout way down for spec tests so they fail fast
connection_pool_timeout: 1
connection_pool_size: 1
:pools:
EOT
)
}
let(:connection1) { mock_RbVmomi_VIM_Connection(connection_options) }
let(:connection2) { mock_RbVmomi_VIM_Connection(connection_options) }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection1)
end
# This is to ensure that the pool_size of 1 is in effect
it 'should return the same connection object when calling the pool multiple times' do
subject.connection_pool.with_metrics do |pool_object|
expect(pool_object[:connection]).to be(connection1)
end
subject.connection_pool.with_metrics do |pool_object|
expect(pool_object[:connection]).to be(connection1)
end
subject.connection_pool.with_metrics do |pool_object|
expect(pool_object[:connection]).to be(connection1)
end
end
context 'when the connection breaks' do
before(:each) do
# Emulate the connection state being good, then bad, then good again
expect(subject).to receive(:vsphere_connection_ok?).and_return(true, false, true)
expect(subject).to receive(:connect_to_vsphere).and_return(connection1, connection2)
end
it 'should restore the connection' do
subject.connection_pool.with_metrics do |pool_object|
# This line needs to be added to all instances of the connection_pool allocation
connection = subject.ensured_vsphere_connection(pool_object)
expect(connection).to be(connection1)
end
subject.connection_pool.with_metrics do |pool_object|
connection = subject.ensured_vsphere_connection(pool_object)
# The second connection would have failed. This test ensures that a
# new connection object was created.
expect(connection).to be(connection2)
end
subject.connection_pool.with_metrics do |pool_object|
connection = subject.ensured_vsphere_connection(pool_object)
expect(connection).to be(connection2)
end
end
end
end
describe '#connect_to_vsphere' do
before(:each) do
allow(RbVmomi::VIM).to receive(:connect).and_return(connection)
end
let (:credentials) { config[:providers][:vsphere] }
context 'succesful connection' do
it 'should use the supplied credentials' do
expect(RbVmomi::VIM).to receive(:connect).with({
:host => credentials['server'],
:user => credentials['username'],
:password => credentials['password'],
:insecure => credentials['insecure']
}).and_return(connection)
subject.connect_to_vsphere
end
it 'should honor the insecure setting' do
config[:providers][:vsphere][:insecure] = true
expect(RbVmomi::VIM).to receive(:connect).with({
:host => credentials['server'],
:user => credentials['username'],
:password => credentials['password'],
:insecure => true,
}).and_return(connection)
subject.connect_to_vsphere
end
it 'should default to an insecure connection' do
config[:providers][:vsphere][:insecure] = nil
expect(RbVmomi::VIM).to receive(:connect).with({
:host => credentials['server'],
:user => credentials['username'],
:password => credentials['password'],
:insecure => true
}).and_return(connection)
subject.connect_to_vsphere
end
it 'should return the connection object' do
result = subject.connect_to_vsphere
expect(result).to be(connection)
end
it 'should increment the connect.open counter' do
expect(metrics).to receive(:increment).with('connect.open')
subject.connect_to_vsphere
end
end
context 'connection is initially unsuccessful' do
before(:each) do
# Simulate a failure and then success
expect(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError').ordered
expect(RbVmomi::VIM).to receive(:connect).and_return(connection).ordered
allow(subject).to receive(:sleep)
end
it 'should return the connection object' do
result = subject.connect_to_vsphere
expect(result).to be(connection)
end
it 'should increment the connect.fail and then connect.open counter' do
expect(metrics).to receive(:increment).with('connect.fail').exactly(1).times
expect(metrics).to receive(:increment).with('connect.open').exactly(1).times
subject.connect_to_vsphere
end
end
context 'connection is always unsuccessful' do
before(:each) do
allow(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError')
allow(subject).to receive(:sleep)
end
it 'should retry the connection attempt config.max_tries times' do
expect(RbVmomi::VIM).to receive(:connect).exactly(config[:config]['max_tries']).times
begin
# Swallow any errors
subject.connect_to_vsphere
rescue
end
end
it 'should increment the connect.fail counter config.max_tries times' do
expect(metrics).to receive(:increment).with('connect.fail').exactly(config[:config]['max_tries']).times
begin
# Swallow any errors
subject.connect_to_vsphere
rescue
end
end
[{:max_tries => 5, :retry_factor => 1},
{:max_tries => 8, :retry_factor => 5},
].each do |testcase|
context "Configuration set for max_tries of #{testcase[:max_tries]} and retry_facter of #{testcase[:retry_factor]}" do
it "should sleep #{testcase[:max_tries] - 1} times between attempts with increasing timeout" do
config[:config]['max_tries'] = testcase[:max_tries]
config[:config]['retry_factor'] = testcase[:retry_factor]
(1..testcase[:max_tries] - 1).each do |try|
expect(subject).to receive(:sleep).with(testcase[:retry_factor] * try).ordered
end
begin
# Swallow any errors
subject.connect_to_vsphere
rescue
end
end
end
end
end
end
describe '#open_socket' do
let(:TCPSocket) { double('tcpsocket') }
let(:socket) { double('tcpsocket') }
let(:hostname) { 'host' }
let(:domain) { 'domain.local'}
let(:default_socket) { 22 }
before do
expect(subject).not_to be_nil
allow(socket).to receive(:close)
end
it 'opens socket with defaults' do
expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_return(socket)
expect(subject.open_socket(hostname)).to eq(nil)
end
it 'yields the socket if a block is given' do
expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_return(socket)
expect{ |socket| subject.open_socket(hostname,nil,nil,default_socket,&socket) }.to yield_control.exactly(1).times
end
it 'closes the opened socket' do
expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_return(socket)
expect(socket).to receive(:close)
expect(subject.open_socket(hostname)).to eq(nil)
end
it 'opens a specific socket' do
expect(TCPSocket).to receive(:new).with(hostname,80).and_return(socket)
expect(subject.open_socket(hostname,nil,nil,80)).to eq(nil)
end
it 'uses a specific domain with the hostname' do
expect(TCPSocket).to receive(:new).with("#{hostname}.#{domain}",default_socket).and_return(socket)
expect(subject.open_socket(hostname,domain)).to eq(nil)
end
it 'raises error if host is not resolvable' do
expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_raise(SocketError,'getaddrinfo: No such host is known')
expect { subject.open_socket(hostname,nil,1) }.to raise_error(SocketError)
end
it 'raises error if socket is not listening' do
expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_raise(SocketError,'No connection could be made because the target machine actively refused it')
expect { subject.open_socket(hostname,nil,1) }.to raise_error(SocketError)
end
end
describe '#get_vm_folder_path' do
[
{ :path_description => 'Datacenters/DC01/vm/Pooler/pool1/vm1',
:expected_path => 'Pooler/pool1',
:vm_object_path => [
{ :type => 'folder', :name => 'Datacenters' },
{ :type => 'datacenter', :name => 'DC01' },
{ :type => 'folder', :name => 'vm' },
{ :type => 'folder', :name => 'Pooler' },
{ :type => 'folder', :name => 'pool1'},
],
},
{ :path_description => 'Datacenters/DC01/vm/something/subfolder/pool/vm1',
:expected_path => 'something/subfolder/pool',
:vm_object_path => [
{ :type => 'folder', :name => 'Datacenters' },
{ :type => 'datacenter', :name => 'DC01' },
{ :type => 'folder', :name => 'vm' },
{ :type => 'folder', :name => 'something' },
{ :type => 'folder', :name => 'subfolder' },
{ :type => 'folder', :name => 'pool'},
],
},
{ :path_description => 'Datacenters/DC01/vm/vm1',
:expected_path => '',
:vm_object_path => [
{ :type => 'folder', :name => 'Datacenters' },
{ :type => 'datacenter', :name => 'DC01' },
{ :type => 'folder', :name => 'vm' },
],
},
].each do |testcase|
context "given a path of #{testcase[:path_description]}" do
it "should return '#{testcase[:expected_path]}'" do
vm_object = mock_RbVmomi_VIM_VirtualMachine({
:name => 'vm1',
:path => testcase[:vm_object_path],
})
expect(subject.get_vm_folder_path(vm_object)).to eq(testcase[:expected_path])
end
end
end
[
{ :path_description => 'a path missing a Datacenter',
:vm_object_path => [
{ :type => 'folder', :name => 'Datacenters' },
{ :type => 'folder', :name => 'vm' },
{ :type => 'folder', :name => 'Pooler' },
{ :type => 'folder', :name => 'pool1'},
],
},
{ :path_description => 'a path missing root VM folder',
:vm_object_path => [
{ :type => 'folder', :name => 'Datacenters' },
{ :type => 'datacenter', :name => 'DC01' },
],
},
].each do |testcase|
context "given #{testcase[:path_description]}" do
it "should return nil" do
vm_object = mock_RbVmomi_VIM_VirtualMachine({
:name => 'vm1',
:path => testcase[:vm_object_path],
})
expect(subject.get_vm_folder_path(vm_object)).to be_nil
end
end
end
end
describe '#add_disk' do
let(:datastorename) { 'datastore' }
let(:disk_size) { 30 }
let(:collectMultiple_response) { {} }
let(:vm_scsi_controller) { mock_RbVmomi_VIM_VirtualSCSIController() }
# Require at least one SCSI Controller
let(:vm_object) {
mock_vm = mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
})
mock_vm.config.hardware.device << vm_scsi_controller
mock_vm
}
# Require at least one DC with the required datastore
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name, :datastores => [datastorename] }
]
}
}}
let(:create_virtual_disk_task) { mock_RbVmomi_VIM_Task() }
let(:reconfig_vm_task) { mock_RbVmomi_VIM_Task() }
before(:each) do
# Mocking for find_vmdks
allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response)
# Mocking for creating the disk
allow(connection.serviceContent.virtualDiskManager).to receive(:CreateVirtualDisk_Task) do |options|
if options[:datacenter][:name] == datacenter_name
create_virtual_disk_task
end
end
allow(create_virtual_disk_task).to receive(:wait_for_completion).and_return(true)
# Mocking for adding disk to the VM
allow(vm_object).to receive(:ReconfigVM_Task).and_return(reconfig_vm_task)
allow(reconfig_vm_task).to receive(:wait_for_completion).and_return(true)
end
context 'Succesfully addding disk' do
it 'should return true' do
expect(subject.add_disk(vm_object,disk_size,datastorename,connection,datacenter_name)).to be true
end
it 'should request a disk of appropriate size' do
expect(connection.serviceContent.virtualDiskManager).to receive(:CreateVirtualDisk_Task)
.with(create_virtual_disk_with_size(disk_size))
.and_return(create_virtual_disk_task)
subject.add_disk(vm_object,disk_size,datastorename,connection,datacenter_name)
end
end
context 'Requested disk size is 0' do
it 'should raise an error' do
expect(subject.add_disk(vm_object,0,datastorename,connection,datacenter_name)).to be false
end
end
context 'No datastores or datastore missing' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name, :datastores => ['missing_datastore'] }
]
}
}}
it 'should raise error' do
expect{ subject.add_disk(vm_object,disk_size,datastorename,connection,datacenter_name) }.to raise_error(/does not exist/)
end
end
context 'Multiple datacenters with multiple datastores' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => 'AnotherDC', :datastores => ['dc1','dc2'] },
{ :name => datacenter_name, :datastores => ['dc3',datastorename,'dc4'] },
]
}
}}
it 'should return true' do
expect(subject.add_disk(vm_object,disk_size,datastorename,connection,datacenter_name)).to be true
end
end
context 'VM does not have a SCSI Controller' do
let(:vm_object) {
mock_vm = mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
})
mock_vm
}
it 'should raise an error' do
expect{ subject.add_disk(vm_object,disk_size,datastorename,connection,datacenter_name) }.to raise_error(NoMethodError)
end
end
end
describe '#find_datastore' do
let(:datastorename) { 'datastore' }
let(:datastore_list) { [] }
context 'No datastores in the datacenter' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name, :datastores => [] }
]
}
}}
it 'should return nil if the datastore is not found' do
result = subject.find_datastore(datastorename,connection,datacenter_name)
expect(result).to be_nil
end
end
context 'Many datastores in the datacenter' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name, :datastores => ['ds1','ds2',datastorename,'ds3'] }
]
}
}}
it 'should return nil if the datastore is not found' do
result = subject.find_datastore('missing_datastore',connection,datacenter_name)
expect(result).to be_nil
end
it 'should find the datastore in the datacenter' do
result = subject.find_datastore(datastorename,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.is_a?(RbVmomi::VIM::Datastore)).to be true
expect(result.name).to eq(datastorename)
end
end
context 'Many datastores in many datacenters' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => 'AnotherDC', :datastores => ['ds1','ds2','ds3'] },
{ :name => datacenter_name, :datastores => ['ds3','ds4',datastorename,'ds5'] },
]
}
}}
it 'should return nil if the datastore is not found' do
result = subject.find_datastore(datastorename,connection,'AnotherDC')
expect(result).to be_nil
end
it 'should find the datastore in the datacenter' do
result = subject.find_datastore(datastorename,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.is_a?(RbVmomi::VIM::Datastore)).to be true
expect(result.name).to eq(datastorename)
end
end
end
describe '#find_device' do
let(:devicename) { 'device1' }
let(:vm_object) {
mock_vm = mock_RbVmomi_VIM_VirtualMachine()
mock_vm.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
mock_vm.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'})
mock_vm
}
it 'should return a device if the device name matches' do
result = subject.find_device(vm_object,devicename)
expect(result.deviceInfo.label).to eq(devicename)
end
it 'should return nil if the device name does not match' do
result = subject.find_device(vm_object,'missing_device')
expect(result).to be_nil
end
end
describe '#find_disk_controller' do
let(:vm_object) {
mock_vm = mock_RbVmomi_VIM_VirtualMachine()
mock_vm
}
it 'should return nil when there are no devices' do
result = subject.find_disk_controller(vm_object)
expect(result).to be_nil
end
[0,1,14].each do |testcase|
it "should return a device for a single VirtualSCSIController with #{testcase} attached disks" do
mock_scsi = mock_RbVmomi_VIM_VirtualSCSIController()
vm_object.config.hardware.device << mock_scsi
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'})
# Add the disks
(1..testcase).each do
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualDisk({ :controllerKey => mock_scsi.key })
end
result = subject.find_disk_controller(vm_object)
expect(result).to eq(mock_scsi)
end
end
[15].each do |testcase|
it "should return nil for a single VirtualSCSIController with #{testcase} attached disks" do
mock_scsi = mock_RbVmomi_VIM_VirtualSCSIController()
vm_object.config.hardware.device << mock_scsi
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'})
# Add the disks
(1..testcase).each do
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualDisk({ :controllerKey => mock_scsi.key })
end
result = subject.find_disk_controller(vm_object)
expect(result).to be_nil
end
end
it 'should raise if a VirtualDisk is missing a controller' do
# Note - Typically this is not possible as a VirtualDisk requires a controller (SCSI, PVSCSI or IDE)
mock_scsi = mock_RbVmomi_VIM_VirtualDisk()
vm_object.config.hardware.device << mock_scsi
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'})
expect{subject.find_disk_controller(vm_object)}.to raise_error(NoMethodError)
end
end
describe '#find_disk_devices' do
let(:vm_object) {
mock_vm = mock_RbVmomi_VIM_VirtualMachine()
mock_vm
}
it 'should return empty hash when there are no devices' do
result = subject.find_disk_devices(vm_object)
expect(result).to eq({})
end
it 'should return empty hash when there are no VirtualSCSIController or VirtualDisk devices' do
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'})
result = subject.find_disk_devices(vm_object)
expect(result).to eq({})
end
it 'should return a device for a VirtualSCSIController device with no children' do
mock_scsi = mock_RbVmomi_VIM_VirtualSCSIController()
vm_object.config.hardware.device << mock_scsi
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
result = subject.find_disk_devices(vm_object)
expect(result.count).to eq(1)
expect(result[mock_scsi.key]).to_not be_nil
expect(result[mock_scsi.key]['children']).to eq([])
expect(result[mock_scsi.key]['device']).to eq(mock_scsi)
end
it 'should return a device for a VirtualDisk device' do
mock_disk = mock_RbVmomi_VIM_VirtualDisk()
vm_object.config.hardware.device << mock_disk
vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'})
result = subject.find_disk_devices(vm_object)
expect(result.count).to eq(1)
expect(result[mock_disk.controllerKey]).to_not be_nil
expect(result[mock_disk.controllerKey]['children'][0]).to eq(mock_disk)
end
it 'should return one device for many VirtualDisk devices on the same controller' do
controller1Key = rand(2000)
controller2Key = controller1Key + 1
mock_disk1 = mock_RbVmomi_VIM_VirtualDisk({:controllerKey => controller1Key})
mock_disk2 = mock_RbVmomi_VIM_VirtualDisk({:controllerKey => controller1Key})
mock_disk3 = mock_RbVmomi_VIM_VirtualDisk({:controllerKey => controller2Key})
vm_object.config.hardware.device << mock_disk2
vm_object.config.hardware.device << mock_disk1
vm_object.config.hardware.device << mock_disk3
result = subject.find_disk_devices(vm_object)
expect(result.count).to eq(2)
expect(result[controller1Key]).to_not be_nil
expect(result[controller2Key]).to_not be_nil
expect(result[controller1Key]['children']).to contain_exactly(mock_disk1,mock_disk2)
expect(result[controller2Key]['children']).to contain_exactly(mock_disk3)
end
end
describe '#find_disk_unit_number' do
let(:vm_object) {
mock_vm = mock_RbVmomi_VIM_VirtualMachine()
mock_vm
}
let(:controller) { mock_RbVmomi_VIM_VirtualSCSIController() }
it 'should return 0 when there are no devices' do
result = subject.find_disk_unit_number(vm_object,controller)
expect(result).to eq(0)
end
context 'with a single SCSI Controller' do
before(:each) do
vm_object.config.hardware.device << controller
end
it 'should return 1 when the host bus controller is at 0' do
controller.scsiCtlrUnitNumber = 0
result = subject.find_disk_unit_number(vm_object,controller)
expect(result).to eq(1)
end
it 'should return the next lowest id when disks are attached' do
expected_id = 9
controller.scsiCtlrUnitNumber = 0
(1..expected_id-1).each do |disk_id|
mock_disk = mock_RbVmomi_VIM_VirtualDisk({
:controllerKey => controller.key,
:unitNumber => disk_id,
})
vm_object.config.hardware.device << mock_disk
end
result = subject.find_disk_unit_number(vm_object,controller)
expect(result).to eq(expected_id)
end
it 'should return nil when there are no spare units' do
controller.scsiCtlrUnitNumber = 0
(1..15).each do |disk_id|
mock_disk = mock_RbVmomi_VIM_VirtualDisk({
:controllerKey => controller.key,
:unitNumber => disk_id,
})
vm_object.config.hardware.device << mock_disk
end
result = subject.find_disk_unit_number(vm_object,controller)
expect(result).to eq(nil)
end
end
end
describe '#find_vm_folder' do
let(:foldername) { 'folder'}
context 'with no folder hierarchy' do
it 'should return nil if the folder is not found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
expect(subject.find_vm_folder(poolname,connection)).to be_nil
end
end
context 'with a single layer folder hierarchy' do
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => foldername}) }
it 'should return the folder when found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(folder_object)
allow(folder_object).to receive(:class).and_return(RbVmomi::VIM::Folder)
result = subject.find_vm_folder(poolname,connection)
expect(result.name).to eq(foldername)
end
it 'should return nil if the folder is not found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
expect(subject.find_vm_folder(poolname,connection)).to be_nil
end
end
context 'with a single layer folder hierarchy in many datacenters' do
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => foldername}) }
it 'should return the folder when found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(folder_object)
allow(folder_object).to receive(:class).and_return(RbVmomi::VIM::Folder)
result = subject.find_vm_folder(poolname,connection)
expect(result.name).to eq(foldername)
end
it 'should return nil if the folder is not found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
expect(subject.find_vm_folder(poolname,connection)).to be_nil
end
end
context 'with a multi layer folder hierarchy' do
let(:foldername) { 'folder2/folder4/folder' }
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => foldername}) }
it 'should return the folder when found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(folder_object)
allow(folder_object).to receive(:class).and_return(RbVmomi::VIM::Folder)
result = subject.find_vm_folder(poolname,connection)
expect(result.name).to eq(foldername)
end
it 'should return nil if the folder is not found' do
allow(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
expect(subject.find_vm_folder(poolname,connection)).to be_nil
end
end
end
describe '#get_host_utilization' do
let(:cpu_model) { 'vendor line type sku v4 speed' }
let(:model) { 'v4' }
let(:different_model) { 'different_model' }
let(:limit) { 75 }
let(:default_limit) { 90 }
context "host with a different model" do
let(:host) { mock_RbVmomi_VIM_HostSystem() }
it 'should return nil' do
expect(subject.get_host_utilization(host,different_model,limit)).to be_nil
end
end
context "host in maintenance mode" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:maintenance_mode => true,
})
}
it 'should return nil' do
host.runtime.inMaintenanceMode = true
expect(subject.get_host_utilization(host,model,limit)).to be_nil
end
end
context "host with status of not green" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_status => 'purple_alert',
})
}
it 'should return nil' do
expect(subject.get_host_utilization(host,model,limit)).to be_nil
end
end
context "host with configuration issue" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:config_issue => 'No quickstats',
})
}
it 'should return nil' do
expect(subject.get_host_utilization(host,model,limit)).to be_nil
end
end
# CPU utilization
context "host which exceeds limit in CPU utilization" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => 100,
:overall_memory_usage => 1,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should return nil' do
expect(subject.get_host_utilization(host,model,limit)).to be_nil
end
end
context "host which exceeds default limit in CPU utilization" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => default_limit + 1.0,
:overall_memory_usage => 1,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should return nil' do
expect(subject.get_host_utilization(host,model)).to be_nil
end
end
context "host which does not exceed default limit in CPU utilization" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => default_limit,
:overall_memory_usage => 1,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should not return nil' do
expect(subject.get_host_utilization(host,model)).to_not be_nil
end
end
# Memory utilization
context "host which exceeds limit in Memory utilization" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => 1,
:overall_memory_usage => 100,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should return nil' do
# Set the Memory Usage to 100%
expect(subject.get_host_utilization(host,model,limit)).to be_nil
end
end
context "host which exceeds default limit in Memory utilization" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => 1,
:overall_memory_usage => default_limit + 1.0,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should return nil' do
expect(subject.get_host_utilization(host,model)).to be_nil
end
end
context "host which does not exceed default limit in Memory utilization" do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => 1,
:overall_memory_usage => default_limit,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should not return nil' do
expect(subject.get_host_utilization(host,model)).to_not be_nil
end
end
context "host which does not exceed limits" do
# Set CPU to 10%
# Set Memory to 20%
let(:host) { mock_RbVmomi_VIM_HostSystem({
:overall_cpu_usage => 10,
:overall_memory_usage => 20,
:cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
})
}
it 'should return the sum of CPU and Memory utilization' do
expect(subject.get_host_utilization(host,model,limit)[0]).to eq(10)
end
it 'should return the host' do
expect(subject.get_host_utilization(host,model,limit)[1]).to eq(host)
end
end
context 'host with no quickstats' do
let(:host) { mock_RbVmomi_VIM_HostSystem({
:cpu_speed => 100,
:num_cores_per_cpu => 1,
:num_cpu => 1,
:memory_size => 100.0 * 1024 * 1024
})
}
before(:each) do
host.summary.quickStats.overallCpuUsage = nil
end
it 'should return nil' do
result = subject.get_host_utilization(host,model,limit)
expect(result).to be nil
end
end
end
describe '#host_has_cpu_model?' do
let(:cpu_model) { 'vendor line type sku v4 speed' }
let(:model) { 'v4' }
let(:different_model) { 'different_model' }
let(:host) { mock_RbVmomi_VIM_HostSystem({
:cpu_model => cpu_model,
})
}
it 'should return true if the model matches' do
expect(subject.host_has_cpu_model?(host,model)).to eq(true)
end
it 'should return false if the model is different' do
expect(subject.host_has_cpu_model?(host,different_model)).to eq(false)
end
end
describe '#get_host_cpu_arch_version' do
let(:cpu_model) { 'vendor line type sku v4 speed' }
let(:model) { 'v4' }
let(:different_model) { 'different_model' }
let(:host) { mock_RbVmomi_VIM_HostSystem({
:cpu_model => cpu_model,
:num_cpu => 2,
})
}
it 'should return the fifth element in the string delimited by spaces' do
expect(subject.get_host_cpu_arch_version(host)).to eq(model)
end
it 'should use the description of the first CPU' do
host.hardware.cpuPkg[0].description = 'vendor line type sku v6 speed'
expect(subject.get_host_cpu_arch_version(host)).to eq('v6')
end
end
describe '#cpu_utilization_for' do
[{ :cpu_usage => 10.0,
:core_speed => 10.0,
:num_cores => 2,
:expected_value => 50.0,
},
{ :cpu_usage => 10.0,
:core_speed => 10.0,
:num_cores => 4,
:expected_value => 25.0,
},
{ :cpu_usage => 14.0,
:core_speed => 12.0,
:num_cores => 5,
:expected_value => 23.0 + 1.0/3.0,
},
].each do |testcase|
context "CPU Usage of #{testcase[:cpu_usage]}MHz with #{testcase[:num_cores]} x #{testcase[:core_speed]}MHz cores" do
it "should be #{testcase[:expected_value]}%" do
host = mock_RbVmomi_VIM_HostSystem({
:num_cores_per_cpu => testcase[:num_cores],
:cpu_speed => testcase[:core_speed],
:overall_cpu_usage => testcase[:cpu_usage],
})
expect(subject.cpu_utilization_for(host)).to eq(testcase[:expected_value])
end
end
end
end
describe '#memory_utilization_for' do
[{ :memory_usage_gigbytes => 10.0,
:memory_size_bytes => 10.0 * 1024 * 1024,
:expected_value => 100.0,
},
{ :memory_usage_gigbytes => 15.0,
:memory_size_bytes => 25.0 * 1024 * 1024,
:expected_value => 60.0,
},
{ :memory_usage_gigbytes => 9.0,
:memory_size_bytes => 31.0 * 1024 * 1024,
:expected_value => 29.03225806451613,
},
].each do |testcase|
context "Memory Usage of #{testcase[:memory_usage_gigbytes]}GBytes with #{testcase[:memory_size_bytes]}Bytes of total memory" do
it "should be #{testcase[:expected_value]}%" do
host = mock_RbVmomi_VIM_HostSystem({
:memory_size => testcase[:memory_size_bytes],
:overall_memory_usage => testcase[:memory_usage_gigbytes],
})
expect(subject.memory_utilization_for(host)).to eq(testcase[:expected_value])
end
end
end
end
describe '#select_target_hosts' do
let(:target) { {} }
let(:cluster) { 'cluster1' }
let(:missing_cluster_name) { 'missing_cluster' }
let(:datacenter) { 'dc1' }
let(:architecture) { 'v3' }
let(:host) { 'host1' }
let(:hosts_hash) {
{
'hosts' => [host],
'architectures' => {
architecture => [host]
}
}
}
it 'returns a hash of the least used hosts by cluster and architecture' do
expect(subject).to receive(:find_least_used_hosts).and_return(hosts_hash)
subject.select_target_hosts(target, cluster, datacenter)
expect(target["#{datacenter}_#{cluster}"]).to eq(hosts_hash)
end
context 'with a cluster specified that does not exist' do
it 'raises an error' do
expect(subject).to receive(:find_least_used_hosts).with(missing_cluster_name, datacenter, 100).and_raise("Cluster #{cluster} cannot be found")
expect{subject.select_target_hosts(target, missing_cluster_name, datacenter)}.to raise_error(RuntimeError,/Cluster #{cluster} cannot be found/)
end
end
end
describe '#get_average_cluster_utilization' do
let(:hosts) {
[
[60, 'host1'],
[100, 'host2'],
[200, 'host3']
]
}
it 'returns the average utilization for a given set of host utilizations assuming the first member of the list for each host is the utilization value' do
expect(subject.get_average_cluster_utilization(hosts)).to eq(120)
end
end
describe '#build_compatible_hosts_lists' do
let(:host1) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST1' })}
let(:host2) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST2' })}
let(:host3) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST3' })}
let(:architecture) { 'v4' }
let(:percentage) { 100 }
let(:hosts) {
[
[60, host1],
[100, host2],
[200, host3]
]
}
let(:result) {
{
architecture => ['HOST1','HOST2']
}
}
it 'returns a hash of target host architecture versions containing lists of target hosts' do
expect(subject.build_compatible_hosts_lists(hosts, percentage)).to eq(result)
end
end
describe '#select_least_used_hosts' do
let(:percentage) { 100 }
let(:host1) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST1' })}
let(:host2) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST2' })}
let(:host3) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST3' })}
let(:hosts) {
[
[60, host1],
[100, host2],
[200, host3]
]
}
let(:result) { ['HOST1','HOST2'] }
it 'returns the percentage specified of the least used hosts in the cluster determined by selecting from less than or equal to average cluster utilization' do
expect(subject.select_least_used_hosts(hosts, percentage)).to eq(result)
end
context 'when selecting 20 percent of hosts below average' do
let(:percentage) { 20 }
let(:result) { ['HOST1'] }
it 'should return the result' do
expect(subject.select_least_used_hosts(hosts, percentage)).to eq(result)
end
end
it 'should raise' do
expect{subject.select_least_used_hosts([], percentage)}.to raise_error(RuntimeError,/Provided hosts list to select_least_used_hosts is empty/)
end
end
describe '#run_select_hosts' do
let(:target) { {} }
let(:cluster) { 'cluster1' }
let(:missing_cluster_name) { 'missing_cluster' }
let(:datacenter) { 'dc1' }
let(:architecture) { 'v3' }
let(:host) { 'host1' }
let(:loop_delay) { 5 }
let(:max_age) { 60 }
let(:dc) { "#{datacenter}_#{cluster}" }
let(:hosts_hash) {
{
dc => {
'hosts' => [host],
'architectures' => {
architecture => [host]
}
}
}
}
let(:config) { YAML.load(<<-EOT
---
:config:
max_age: 60
:pools:
- name: '#{poolname}'
datacenter: '#{datacenter}'
clone_target: '#{cluster}'
EOT
)
}
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
context 'target does not have key dc' do
let(:hosts_hash) { { } }
it 'should run select_target_hosts' do
expect(subject).to receive(:select_target_hosts).with(hosts_hash, cluster, datacenter)
subject.run_select_hosts(poolname, hosts_hash)
end
end
context 'when check_time_finished is greater than max_age' do
before(:each) do
hosts_hash[dc]['check_time_finished'] = Time.now - (max_age + 10)
end
it 'should run select_target_hosts' do
expect(subject).to receive(:select_target_hosts).with(hosts_hash, cluster, datacenter)
subject.run_select_hosts(poolname, hosts_hash)
end
end
context 'when check_time_finished is not greater than max_age' do
before(:each) do
hosts_hash[dc]['check_time_finished'] = Time.now - (max_age / 2)
end
it 'should not run select_target_hosts' do
expect(subject).to_not receive(:select_target_hosts).with(hosts_hash, cluster, datacenter)
subject.run_select_hosts(poolname, hosts_hash)
end
end
context 'when hosts are being selected' do
before(:each) do
hosts_hash[dc]['checking'] = true
end
it 'should run wait_for_host_selection' do
expect(subject).to receive(:wait_for_host_selection).with(dc, hosts_hash, loop_delay, max_age)
subject.run_select_hosts(poolname, hosts_hash)
end
end
context 'with no clone_target' do
before(:each) do
config[:pools][0].delete('clone_target')
end
it 'should raise an error' do
expect{subject.run_select_hosts(poolname, hosts_hash)}.to raise_error(/cluster for pool #{poolname} cannot be identified/)
end
end
context 'with no datacenter' do
before(:each) do
config[:pools][0].delete('datacenter')
end
it 'should raise an error' do
expect{subject.run_select_hosts(poolname, hosts_hash)}.to raise_error(/datacenter for pool #{poolname} cannot be identified/)
end
end
end
describe '#wait_for_host_selection' do
let(:datacenter) { 'dc1' }
let(:cluster) { 'cluster1' }
let(:dc) { "#{datacenter}_#{cluster}" }
let(:maxloop) { 1 }
let(:loop_delay) { 1 }
let(:max_age) { 60 }
let(:target) {
{
dc => { }
}
}
context 'when target does not have key check_time_finished' do
it 'should sleep for loop_delay maxloop times' do
expect(subject).to receive(:sleep).with(loop_delay).once
subject.wait_for_host_selection(dc, target, maxloop, loop_delay, max_age)
end
end
context 'when target has dc and check_time_finished is greater than max_age' do
before(:each) do
target[dc]['check_time_finished'] = Time.now - (max_age + 10)
end
it 'should sleep for loop_delay maxloop times' do
expect(subject).to receive(:sleep).with(loop_delay).once
subject.wait_for_host_selection(dc, target, maxloop, loop_delay, max_age)
end
end
context 'when target has dc and check_time_finished difference from now is less than max_age' do
before(:each) do
target[dc]['check_time_finished'] = Time.now - (max_age / 2)
end
it 'should not wait' do
expect(subject).to_not receive(:sleep).with(loop_delay)
subject.wait_for_host_selection(dc, target, maxloop, loop_delay, max_age)
end
end
end
describe '#select_next_host' do
let(:cluster) { 'cluster1' }
let(:datacenter) { 'dc1' }
let(:architecture) { 'v3' }
let(:host) { 'host1' }
let(:loop_delay) { 5 }
let(:max_age) { 60 }
let(:dc) { "#{datacenter}_#{cluster}" }
let(:hosts_hash) {
{
dc => {
'hosts' => [host, 'host2'],
'architectures' => {
architecture => ['host3', 'host4']
}
}
}
}
let(:config) { YAML.load(<<-EOT
---
:config:
max_age: 60
:pools:
- name: '#{poolname}'
datacenter: '#{datacenter}'
clone_target: '#{cluster}'
EOT
)
}
context 'when host is requested' do
it 'should return the first host' do
result = subject.select_next_host(poolname, hosts_hash)
expect(result).to eq(host)
end
it 'should return the second host if called twice' do
result = subject.select_next_host(poolname, hosts_hash)
result2 = subject.select_next_host(poolname, hosts_hash)
expect(result2).to eq('host2')
end
end
context 'when host and architecture are requested' do
it 'should return the first host' do
result = subject.select_next_host(poolname, hosts_hash, architecture)
expect(result).to eq('host3')
end
it 'should return the second host if called twice' do
result = subject.select_next_host(poolname, hosts_hash, architecture)
result2 = subject.select_next_host(poolname, hosts_hash, architecture)
expect(result2).to eq('host4')
end
end
context 'with no hosts available' do
before(:each) do
hosts_hash[dc].delete('hosts')
hosts_hash[dc].delete('architectures')
end
it 'should raise an error' do
expect{subject.select_next_host(poolname, hosts_hash)}.to raise_error("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory")
end
it 'should raise an error when selecting with architecture' do
expect{subject.select_next_host(poolname, hosts_hash, architecture)}.to raise_error("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory")
end
end
context 'with no datacenter set for pool' do
before(:each) do
config[:pools][0].delete('datacenter')
end
it 'should raise an error' do
expect{subject.select_next_host(poolname, hosts_hash)}.to raise_error("datacenter for pool #{poolname} cannot be identified")
end
end
context 'with no clone_target set for pool' do
before(:each) do
config[:pools][0].delete('clone_target')
end
it 'should raise an error' do
expect{subject.select_next_host(poolname, hosts_hash)}.to raise_error("cluster for pool #{poolname} cannot be identified")
end
end
end
describe '#vm_in_target?' do
let(:parent_host) { 'host1' }
let(:architecture) { 'v3' }
let(:datacenter) { 'dc1' }
let(:cluster) { 'cluster1' }
let(:dc) { "#{datacenter}_#{cluster}" }
let(:maxloop) { 1 }
let(:loop_delay) { 1 }
let(:max_age) { 60 }
let(:target) {
{
dc => {
'hosts' => [parent_host],
'architectures' => {
architecture => [parent_host]
}
}
}
}
let(:config) { YAML.load(<<-EOT
---
:pools:
- name: '#{poolname}'
datacenter: '#{datacenter}'
clone_target: '#{cluster}'
EOT
)
}
it 'returns true when parent_host is in hosts' do
result = subject.vm_in_target?(poolname, parent_host, architecture, target)
expect(result).to be true
end
context 'with parent_host only in architectures' do
before(:each) do
target[dc]['hosts'] = ['host2']
end
it 'returns true when parent_host is in architectures' do
result = subject.vm_in_target?(poolname, parent_host, architecture, target)
expect(result).to be true
end
end
it 'returns false when parent_host is not in hosts or architectures' do
result = subject.vm_in_target?(poolname, 'host3', architecture, target)
expect(result).to be false
end
context 'with no hosts key' do
before(:each) do
target[dc].delete('hosts')
end
it 'should raise an error' do
expect{subject.vm_in_target?(poolname, parent_host, architecture, target)}.to raise_error("there is no candidate in vcenter that meets all the required conditions, that the cluster has available hosts in a 'green' status, not in maintenance mode and not overloaded CPU and memory")
end
end
end
describe '#get_vm_details' do
let(:parent_host) { 'host1' }
let(:host_object) { mock_RbVmomi_VIM_HostSystem({ :name => parent_host })}
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })}
let(:architecture) { 'v4' }
let(:vm_details) {
{
'host_name' => parent_host,
'object' => vm_object,
'architecture' => architecture
}
}
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
it 'returns nil when vm_object is not found' do
expect(subject).to receive(:find_vm).with(poolname, vmname, connection).and_return(nil)
result = subject.get_vm_details(poolname, vmname, connection)
expect(result).to be nil
end
it 'raises an error when unable to determine parent_host' do
expect(subject).to receive(:find_vm).with(poolname, vmname, connection).and_return(vm_object)
expect{subject.get_vm_details(poolname, vmname, connection)}.to raise_error('Unable to determine which host the VM is running on')
end
context 'when it can find parent host' do
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm_object.summary.runtime.host = host_object
end
it 'returns vm details hash' do
expect(subject).to receive(:find_vm).with(poolname, vmname, connection).and_return(vm_object)
result = subject.get_vm_details(poolname, vmname, connection)
expect(result).to eq(vm_details)
end
end
end
describe '#migrate_vm' do
let(:parent_host) { 'host1' }
let(:new_host) { 'host2' }
let(:datacenter) { 'dc1' }
let(:architecture) { 'v4' }
let(:cluster) { 'cluster1' }
let(:vm_details) {
{
'host_name' => parent_host,
'object' => vm_object,
'architecture' => architecture
}
}
let(:host_object) { mock_RbVmomi_VIM_HostSystem({ :name => parent_host })}
let(:new_host_object) { mock_RbVmomi_VIM_HostSystem({ :name => new_host })}
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })}
let(:config) { YAML.load(<<-EOT
---
:config:
migration_limit: 5
:pools:
- name: '#{poolname}'
datacenter: '#{datacenter}'
clone_target: '#{cluster}'
EOT
)
}
let(:dc) { "#{datacenter}_#{cluster}" }
let(:provider_hosts) {
{
dc => {
'hosts' => [new_host],
'architectures' => {
architecture => [new_host]
},
'check_time_finished' => Time.now
}
}
}
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
context 'when vm should be migrated' do
before(:each) do
subject.connection_pool.with_metrics do |pool_object|
expect(subject).to receive(:ensured_vsphere_connection).with(pool_object).and_return(connection)
expect(subject).to receive(:get_vm_details).and_return(vm_details)
expect(subject).to receive(:run_select_hosts).with(poolname, {})
expect(subject).to receive(:vm_in_target?).and_return false
expect(subject).to receive(:migration_enabled?).and_return true
redis_connection_pool.with do |redis|
redis.hset("vmpooler__vm__#{vmname}", 'checkout', Time.now)
end
end
vm_object.summary.runtime.host = host_object
end
it 'logs a message' do
expect(subject).to receive(:select_next_host).and_return(new_host)
expect(subject).to receive(:find_host_by_dnsname).and_return(new_host_object)
expect(subject).to receive(:migrate_vm_host).with(vm_object, new_host_object)
expect(logger).to receive(:log).with('s', "[>] [#{poolname}] '#{vmname}' migrated from #{parent_host} to #{new_host} in 0.00 seconds")
subject.migrate_vm(poolname, vmname)
end
it 'migrates a vm' do
expect(subject).to receive(:migrate_vm_to_new_host).with(poolname, vmname, vm_details, connection)
subject.migrate_vm(poolname, vmname)
end
end
context 'current host is in the list of target hosts' do
before(:each) do
subject.connection_pool.with_metrics do |pool_object|
expect(subject).to receive(:ensured_vsphere_connection).with(pool_object).and_return(connection)
expect(subject).to receive(:get_vm_details).and_return(vm_details)
expect(subject).to receive(:run_select_hosts).with(poolname, {})
expect(subject).to receive(:vm_in_target?).and_return true
expect(subject).to receive(:migration_enabled?).and_return true
end
vm_object.summary.runtime.host = host_object
end
it 'logs a message that no migration is required' do
expect(logger).to receive(:log).with('s', "[ ] [#{poolname}] No migration required for '#{vmname}' running on #{parent_host}")
subject.migrate_vm(poolname, vmname)
end
end
context 'with migration limit exceeded' do
before(:each) do
subject.connection_pool.with_metrics do |pool_object|
expect(subject).to receive(:ensured_vsphere_connection).with(pool_object).and_return(connection)
expect(subject).to receive(:get_vm_details).and_return(vm_details)
expect(subject).to_not receive(:run_select_hosts)
expect(subject).to_not receive(:vm_in_target?)
expect(subject).to receive(:migration_enabled?).and_return true
redis_connection_pool.with do |redis|
expect(redis).to receive(:scard).with('vmpooler__migration').and_return(5)
end
end
vm_object.summary.runtime.host = host_object
end
it 'should log the current host' do
expect(logger).to receive(:log).with('s', "[ ] [#{poolname}] '#{vmname}' is running on #{parent_host}. No migration will be evaluated since the migration_limit has been reached")
subject.migrate_vm(poolname, vmname)
end
end
context 'when an error occurs' do
before(:each) do
subject.connection_pool.with_metrics do |pool_object|
expect(subject).to receive(:ensured_vsphere_connection).with(pool_object).and_return(connection)
expect(subject).to receive(:get_vm_details).and_return(vm_details)
expect(subject).to receive(:run_select_hosts)
expect(subject).to receive(:vm_in_target?).and_return false
expect(subject).to receive(:migration_enabled?).and_return true
expect(subject).to receive(:select_next_host).and_raise(RuntimeError,'Mock migration error')
end
vm_object.summary.runtime.host = host_object
end
it 'should log the current host' do
expect(logger).to receive(:log).with('s', "[!] [#{poolname}] '#{vmname}' is running on #{parent_host}")
expect{subject.migrate_vm(poolname, vmname)}.to raise_error(RuntimeError, 'Mock migration error')
end
end
end
describe '#migrate_vm_to_new_host' do
let(:parent_host) { 'host1' }
let(:new_host) { 'host2' }
let(:datacenter) { 'dc1' }
let(:cluster) { 'cluster1' }
let(:architecture) { 'v4' }
let(:host_object) { mock_RbVmomi_VIM_HostSystem({ :name => parent_host })}
let(:new_host_object) { mock_RbVmomi_VIM_HostSystem({ :name => new_host })}
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })}
let(:config) { YAML.load(<<-EOT
---
:config:
migration_limit: 5
:pools:
- name: '#{poolname}'
datacenter: '#{datacenter}'
clone_target: '#{cluster}'
EOT
)
}
let(:vm_details) {
{
'host_name' => parent_host,
'object' => vm_object,
'architecture' => architecture
}
}
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
it' migrates a vm' do
expect(subject).to receive(:select_next_host).and_return(new_host)
expect(subject).to receive(:find_host_by_dnsname).and_return(new_host_object)
expect(subject).to receive(:migrate_vm_and_record_timing).and_return(format('%.2f', (Time.now - (Time.now - 15))))
expect(logger).to receive(:log).with('s', "[>] [#{poolname}] '#{vmname}' migrated from host1 to host2 in 15.00 seconds")
subject.migrate_vm_to_new_host(poolname, vmname, vm_details, connection)
end
end
describe '#create_folder' do
let(:datacenter) { 'dc1' }
let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() }
let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => 'pool1'}) }
let(:new_folder) { poolname }
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
it 'creates a folder' do
expect(connection.serviceInstance).to receive(:find_datacenter).with(datacenter).and_return(datacenter_object)
expect(datacenter_object.vmFolder).to receive(:traverse).with(new_folder, RbVmomi::VIM::Folder, true).and_return(folder_object)
result = subject.create_folder(connection, new_folder, datacenter)
expect(result).to eq(folder_object)
end
context 'with folder_object returning nil' do
it 'shoud raise an error' do
expect(connection.serviceInstance).to receive(:find_datacenter).with(datacenter).and_return(datacenter_object)
expect(datacenter_object.vmFolder).to receive(:traverse).with(new_folder, RbVmomi::VIM::Folder, true).and_return(nil)
expect{subject.create_folder(connection, new_folder, datacenter)}.to raise_error("Cannot create folder #{new_folder}")
end
end
end
describe '#migration_enabled?' do
let(:config) {
{ :config => { 'migration_limit' => 5 } }
}
it 'returns true if migration is enabled' do
result = subject.migration_enabled?(config)
expect(result).to be true
end
context 'with migration disabled' do
let(:config) {
{ :config => { } }
}
it 'should return false' do
result = subject.migration_enabled?(config)
expect(result).to be false
end
end
context 'with non-integer value for migration limit' do
let(:config) {
{ :config => { 'migration_limit' => 1.0 } }
}
it 'should return false' do
result = subject.migration_enabled?(config)
expect(result).to be false
end
end
end
describe '#find_least_used_hosts' do
let(:cluster_name) { 'cluster' }
let(:missing_cluster_name) { 'missing_cluster' }
let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() }
let(:percentage) { 100 }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object)
datacenter_object.hostFolder.childEntity = [cluster_object]
end
context 'missing cluster' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [{
:name => cluster_name,
}]})}
let(:expected_host) { cluster_object.host[0] }
#,datacenter_name
it 'should raise an error' do
expect{subject.find_least_used_hosts(missing_cluster_name,datacenter_name,percentage)}.to raise_error(RuntimeError,/Cluster #{missing_cluster_name} cannot be found/)
end
end
context 'standalone host within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [{
:name => cluster_name,
}]})}
let(:expected_host) { cluster_object.host[0][:name] }
it 'should return the standalone host' do
result = subject.find_least_used_hosts(cluster_name,datacenter_name,percentage)
expect(result['hosts'][0]).to be(expected_host)
end
end
context 'standalone host outside the limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [{
:name => cluster_name,
:overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024,
}]})}
let(:expected_host) { cluster_object.host[0] }
it 'should raise an error' do
expect{subject.find_least_used_hosts(missing_cluster_name,datacenter_name,percentage)}.to raise_error(RuntimeError,/Cluster #{missing_cluster_name} cannot be found/)
end
end
context 'cluster of 3 hosts within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [
{ :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1].name }
it 'should return the standalone host' do
result = subject.find_least_used_hosts(cluster_name,datacenter_name,percentage)
expect(result['hosts'][0]).to be(expected_host)
end
end
context 'cluster of 3 hosts all outside of the limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1] }
it 'should raise an error' do
expect{subject.find_least_used_hosts(missing_cluster_name,datacenter_name,percentage)}.to raise_error(RuntimeError,/Cluster #{missing_cluster_name} cannot be found/)
end
end
context 'cluster of 5 hosts of which one is out of limits and one has wrong CPU type' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [
{ :overall_cpu_usage => 31, :overall_memory_usage => 31, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :cpu_model => 'different cpu model', :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1].name }
it 'should return the standalone host' do
result = subject.find_least_used_hosts(cluster_name,datacenter_name,percentage)
expect(result['hosts'][0]).to be(expected_host)
end
end
context 'cluster of 3 hosts all outside of the limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({
:name => cluster_name,
:hosts => [
{ :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1] }
it 'should return a host' do
pending('https://github.com/puppetlabs/vmpooler/issues/206')
result = subject.find_least_used_hosts(missing_cluster_name,datacenter_name,percentage)
expect(result).to_not be_nil
end
end
end
describe '#find_cluster' do
let(:cluster) {'cluster'}
let(:host) { 'host' }
let(:missing_cluster) {'missing_cluster'}
context 'no clusters in the datacenter' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name }
]
}
}}
it 'should return nil if the cluster is not found' do
expect(subject.find_cluster(missing_cluster,connection,datacenter_name)).to be_nil
end
end
context 'with a single layer folder hierarchy' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name,
:hostfolder_tree => {
'cluster1' => {:object_type => 'cluster_compute_resource'},
'cluster2' => {:object_type => 'cluster_compute_resource'},
cluster => {:object_type => 'cluster_compute_resource'},
'cluster3' => {:object_type => 'cluster_compute_resource'},
host => {:object_type => 'compute_resource'},
}
}
]
}
}}
it 'should return the cluster when found' do
result = subject.find_cluster(cluster,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.name).to eq(cluster)
end
it 'should return the single host when found' do
result = subject.find_cluster(host,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.name).to eq(host)
end
it 'should return nil if the cluster is not found' do
expect(subject.find_cluster(missing_cluster,connection,datacenter_name)).to be_nil
end
end
context 'with a single layer folder hierarchy with multiple datacenters' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => 'AnotherDC',
:hostfolder_tree => {
'cluster1' => {:object_type => 'cluster_compute_resource'},
'cluster2' => {:object_type => 'cluster_compute_resource'},
}
},
{ :name => datacenter_name,
:hostfolder_tree => {
cluster => {:object_type => 'cluster_compute_resource'},
'cluster3' => {:object_type => 'cluster_compute_resource'},
host => {:object_type => 'compute_resource'}
}
}
]
}
}}
it 'should return the cluster when found' do
result = subject.find_cluster(cluster,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.name).to eq(cluster)
end
it 'should return the single host when found' do
result = subject.find_cluster(host,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.name).to eq(host)
end
it 'should return nil if the cluster is not found' do
expect(subject.find_cluster(missing_cluster,connection,'AnotherDC')).to be_nil
end
end
context 'with a multi layer folder hierarchy' do
let(:connection_options) {{
:serviceContent => {
:datacenters => [
{ :name => datacenter_name,
:hostfolder_tree => {
'cluster1' => {:object_type => 'cluster_compute_resource'},
'folder2' => {
:children => {
cluster => {:object_type => 'cluster_compute_resource'},
}
},
'cluster3' => {:object_type => 'cluster_compute_resource'},
'folder4' => {
:children => {
host => {:object_type => 'compute_resource'},
}
}
}
}
]
}
}}
it 'should return the cluster when found' do
result = subject.find_cluster(cluster,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.name).to eq(cluster)
end
it 'should return the host when found' do
result = subject.find_cluster(host,connection,datacenter_name)
expect(result).to_not be_nil
expect(result.name).to eq(host)
end
it 'should return nil if the cluster is not found' do
expect(subject.find_cluster(missing_cluster,connection,datacenter_name)).to be_nil
end
end
end
describe '#get_cluster_host_utilization' do
context 'standalone host within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [{}]}) }
it 'should return array with one element' do
result = subject.get_cluster_host_utilization(cluster_object)
expect(result).to_not be_nil
expect(result.count).to eq(1)
end
end
context 'standalone host which is out the limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
it 'should return array with 0 elements' do
result = subject.get_cluster_host_utilization(cluster_object)
expect(result).to_not be_nil
expect(result.count).to eq(0)
end
end
context 'cluster with 3 hosts within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
it 'should return array with 3 elements' do
result = subject.get_cluster_host_utilization(cluster_object)
expect(result).to_not be_nil
expect(result.count).to eq(3)
end
end
context 'cluster with 5 hosts of which 3 within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
it 'should return array with 3 elements' do
result = subject.get_cluster_host_utilization(cluster_object)
expect(result).to_not be_nil
expect(result.count).to eq(3)
end
end
context 'cluster with 3 hosts of which none are within the limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
it 'should return array with 0 elements' do
result = subject.get_cluster_host_utilization(cluster_object)
expect(result).to_not be_nil
expect(result.count).to eq(0)
end
end
end
describe '#find_least_used_vpshere_compatible_host' do
let(:vm) { mock_RbVmomi_VIM_VirtualMachine() }
context 'standalone host within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [{}]}) }
let(:standalone_host) { cluster_object.host[0] }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm.summary.runtime.host = standalone_host
end
it 'should return the standalone host' do
result = subject.find_least_used_vpshere_compatible_host(vm)
expect(result).to_not be_nil
expect(result[0]).to be(standalone_host)
expect(result[1]).to eq(standalone_host.name)
end
end
context 'standalone host outside of limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:standalone_host) { cluster_object.host[0] }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm.summary.runtime.host = standalone_host
end
it 'should raise error' do
expect{subject.find_least_used_vpshere_compatible_host(vm)}.to raise_error(/There is no host candidate in vcenter that meets all the required conditions/)
end
end
context 'cluster of 3 hosts within limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1] }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm.summary.runtime.host = expected_host
end
it 'should return the least used host' do
result = subject.find_least_used_vpshere_compatible_host(vm)
expect(result).to_not be_nil
expect(result[0]).to be(expected_host)
expect(result[1]).to eq(expected_host.name)
end
end
context 'cluster of 3 hosts all outside of the limits' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1] }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm.summary.runtime.host = expected_host
end
it 'should raise error' do
expect{subject.find_least_used_vpshere_compatible_host(vm)}.to raise_error(/There is no host candidate in vcenter that meets all the required conditions/)
end
end
context 'cluster of 5 hosts of which one is out of limits and one has wrong CPU type' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 31, :overall_memory_usage => 31, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :cpu_model => 'different cpu model', :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[2] }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm.summary.runtime.host = expected_host
end
it 'should return the least used host' do
result = subject.find_least_used_vpshere_compatible_host(vm)
expect(result).to_not be_nil
expect(result[0]).to be(expected_host)
expect(result[1]).to eq(expected_host.name)
end
end
context 'cluster of 3 hosts all with the same utilisation' do
let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [
{ :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
{ :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 },
]}) }
let(:expected_host) { cluster_object.host[1] }
before(:each) do
# This mocking is a little fragile but hard to do without a real vCenter instance
vm.summary.runtime.host = expected_host
end
it 'should return a host' do
pending('https://github.com/puppetlabs/vmpooler/issues/206 is fixed')
result = subject.find_least_used_vpshere_compatible_host(vm)
expect(result).to_not be_nil
end
end
end
describe '#find_snapshot' do
let(:snapshot_name) {'snapshot'}
let(:missing_snapshot_name) {'missing_snapshot'}
let(:vm) { mock_RbVmomi_VIM_VirtualMachine(mock_options) }
let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachineSnapshot() }
context 'VM with no snapshots' do
let(:mock_options) {{ :snapshot_tree => nil }}
it 'should return nil' do
expect(subject.find_snapshot(vm,snapshot_name)).to be_nil
end
end
context 'VM with a single layer of snapshots' do
let(:mock_options) {{
:snapshot_tree => {
'snapshot1' => nil,
'snapshot2' => nil,
'snapshot3' => nil,
'snapshot4' => nil,
snapshot_name => { :ref => snapshot_object},
}
}}
it 'should return snapshot which matches the name' do
result = subject.find_snapshot(vm,snapshot_name)
expect(result).to be(snapshot_object)
end
it 'should return nil which no matches are found' do
result = subject.find_snapshot(vm,missing_snapshot_name)
expect(result).to be_nil
end
end
context 'VM with a nested layers of snapshots' do
let(:mock_options) {{
:snapshot_tree => {
'snapshot1' => nil,
'snapshot2' => nil,
'snapshot3' => { :children => {
'snapshot4' => nil,
'snapshot5' => { :children => {
snapshot_name => { :ref => snapshot_object},
}},
}},
'snapshot6' => nil,
}
}}
it 'should return snapshot which matches the name' do
result = subject.find_snapshot(vm,snapshot_name)
expect(result).to be(snapshot_object)
end
it 'should return nil which no matches are found' do
result = subject.find_snapshot(vm,missing_snapshot_name)
expect(result).to be_nil
end
end
end
describe '#find_vm' do
let(:missing_vm) { 'missing_vm' }
let(:folder) { 'Pooler/pool1' }
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine() }
before(:each) do
allow(connection.searchIndex).to receive(:FindByInventoryPath)
end
it 'should call FindByInventoryPath with the correct parameters' do
expect(connection.searchIndex).to receive(:FindByInventoryPath)
subject.find_vm(poolname,vmname,connection)
end
it 'should return the VM object when found' do
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(vm_object)
expect(subject.find_vm(poolname,vmname,connection)).to be(vm_object)
end
it 'should return nil if the VM is not found' do
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
expect(subject.find_vm(poolname,missing_vm,connection)).to be_nil
end
end
describe '#get_base_vm_container_from' do
it 'should return a recursive view of type VirtualMachine' do
result = subject.get_base_vm_container_from(connection)
expect(result.recursive).to be true
expect(result.type).to eq(['VirtualMachine'])
end
end
describe '#get_snapshot_list' do
let(:snapshot_name) {'snapshot'}
let(:snapshot_tree) { mock_RbVmomi_VIM_VirtualMachine(mock_options).snapshot.rootSnapshotList }
let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachine() }
it 'should raise if the snapshot tree is nil' do
expect{ subject.get_snapshot_list(nil,snapshot_name)}.to raise_error(NoMethodError)
end
context 'VM with a single layer of snapshots' do
let(:mock_options) {{
:snapshot_tree => {
'snapshot1' => nil,
'snapshot2' => nil,
'snapshot3' => nil,
'snapshot4' => nil,
snapshot_name => { :ref => snapshot_object},
}
}}
it 'should return snapshot which matches the name' do
result = subject.get_snapshot_list(snapshot_tree,snapshot_name)
expect(result).to be(snapshot_object)
end
end
context 'VM with a nested layers of snapshots' do
let(:mock_options) {{
:snapshot_tree => {
'snapshot1' => nil,
'snapshot2' => nil,
'snapshot3' => { :children => {
'snapshot4' => nil,
'snapshot5' => { :children => {
snapshot_name => { :ref => snapshot_object},
}},
}},
'snapshot6' => nil,
}
}}
it 'should return snapshot which matches the name' do
result = subject.get_snapshot_list(snapshot_tree,snapshot_name)
expect(result).to be(snapshot_object)
end
end
end
describe '#migrate_vm_host' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })}
let(:host_object) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST' })}
let(:relocate_task) { mock_RbVmomi_VIM_Task() }
before(:each) do
allow(vm_object).to receive(:RelocateVM_Task).and_return(relocate_task)
allow(relocate_task).to receive(:wait_for_completion)
end
it 'should call RelocateVM_Task' do
expect(vm_object).to receive(:RelocateVM_Task).and_return(relocate_task)
subject.migrate_vm_host(vm_object,host_object)
end
it 'should use a Relocation Spec object with correct host' do
expect(vm_object).to receive(:RelocateVM_Task).with(relocation_spec_with_host(host_object))
subject.migrate_vm_host(vm_object,host_object)
end
it 'should wait for the relocation to complete' do
expect(relocate_task).to receive(:wait_for_completion)
subject.migrate_vm_host(vm_object,host_object)
end
it 'should return the result of the relocation' do
expect(relocate_task).to receive(:wait_for_completion).and_return('RELOCATE_RESULT')
expect(subject.migrate_vm_host(vm_object,host_object)).to eq('RELOCATE_RESULT')
end
end
describe 'find_template_vm' do
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine() }
before(:each) do
allow(connection.searchIndex).to receive(:FindByInventoryPath)
end
it 'should raise an error when the datacenter cannot be found' do
config[:providers][:vsphere]['datacenter'] = nil
expect{ subject.find_template_vm(config[:pools][0],connection) }.to raise_error('cannot find datacenter')
end
it 'should raise an error when the template specified cannot be found' do
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
expect{ subject.find_template_vm(config[:pools][0],connection) }.to raise_error("Pool #{poolname} specifies a template VM of #{config[:pools][0]['template']} which does not exist for the provider vsphere")
end
it 'should return the vm object' do
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(vm_object)
subject.find_template_vm(config[:pools][0],connection)
end
end
describe 'valid_template_path?' do
it 'should return true with a valid template path' do
expect(subject.valid_template_path?('test/template')).to eq(true)
end
it 'should return false when no / is found' do
expect(subject.valid_template_path?('testtemplate')).to eq(false)
end
it 'should return false when template path begins with /' do
expect(subject.valid_template_path?('/testtemplate')).to eq(false)
end
it 'should return false when template path ends with /' do
expect(subject.valid_template_path?('testtemplate/')).to eq(false)
end
end
describe 'create_template_delta_disks' do
let(:template_object) { mock_RbVmomi_VIM_VirtualMachine({
:name => vmname,
})
}
before(:each) do
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
end
context 'with a template VM found' do
before(:each) do
expect(subject).to receive(:find_template_vm).and_return(template_object)
end
it 'should reconfigure the VM creating delta disks' do
expect(template_object).to receive(:add_delta_disk_layer_on_all_disks)
subject.create_template_delta_disks(config[:pools][0])
end
end
end
describe 'get_disk_backing' do
it 'should return moveChildMostDiskBacking when linked clone enabled' do
expect( subject.get_disk_backing({'create_linked_clone' => true}) ).to eq(:moveChildMostDiskBacking)
end
it 'should return moveAllDiskBackingsAndConsolidate when no preference is specified' do
expect( subject.get_disk_backing({})).to eq(:moveAllDiskBackingsAndConsolidate)
end
it 'should return moveAllDiskBackingsAndConsolidate when linked clone is false' do
expect( subject.get_disk_backing({create_linked_clone: false})).to eq(:moveAllDiskBackingsAndConsolidate)
end
end
describe 'linked_clone?' do
it 'should return true when linked clone is enabled on the pool' do
expect( subject.linked_clone?({'create_linked_clone' => true}) ).to be true
end
it 'should return nil when linked clone is not enabled on the pool' do
expect( subject.linked_clone?({}) ).to be nil
end
end
end