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) { MockRedis.new } subject { Vmpooler::PoolManager::Provider::VSphere.new(config, logger, metrics, '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) $redis = redis end it 'should remove redis data and expire the vm key' do allow(Time).to receive(:now).and_return(now) expect(redis).to receive(:srem).with("vmpooler__completed__#{pool}", vmname) expect(redis).to receive(:hdel).with("vmpooler__active__#{pool}", vmname) expect(redis).to receive(:hset).with("vmpooler__vm__#{vmname}", 'destroy', now) expect(redis).to receive(:expire).with("vmpooler__vm__#{vmname}", data_ttl * 60 * 60) subject.destroy_vm_and_log(vmname, vm_object, pool, data_ttl) end it 'should log a message that the vm is destroyed' do 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("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 '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) allow($redis).to receive(:hset) 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 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($redis).to receive(:hget).with("vmpooler__vm__#{vmname}", 'checkout').and_return((Time.now - 1).to_s) 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($redis).to receive(:hset).with("vmpooler__vm__#{vmname}", 'host', parent_host) 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($redis).to receive(:scard).with('vmpooler__migration').and_return(5) 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 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($redis).to receive(:sadd).with('vmpooler__migration', vmname) expect(subject).to receive(:select_next_host).and_return(new_host) expect($redis).to receive(:hset).with("vmpooler__vm__#{vmname}", 'host', new_host) expect($redis).to receive(:hset).with("vmpooler__vm__#{vmname}", 'migrated', true) 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") expect($redis).to receive(:srem).with('vmpooler__migration', vmname) 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, type=RbVmomi::VIM::Folder, create=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, type=RbVmomi::VIM::Folder, create=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