From 8cf3d043bf6ae53914d951c405815fa0b69dbe8a Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 Mar 2017 08:58:35 -0700 Subject: [PATCH 1/6] (POOLER-70) Copy vSphere helper into the vSphere Provider This commit copies the code and tests from the vSphere Helper into the vSphere Provider and modifies the test initialisation for the new class name. --- lib/vmpooler/providers/vsphere.rb | 417 ++++++ spec/unit/providers/vsphere_spec.rb | 2104 ++++++++++++++++++++++++++- 2 files changed, 2519 insertions(+), 2 deletions(-) diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb index 3e50e00..7c59390 100644 --- a/lib/vmpooler/providers/vsphere.rb +++ b/lib/vmpooler/providers/vsphere.rb @@ -4,11 +4,428 @@ module Vmpooler class VSphere < Vmpooler::PoolManager::Provider::Base def initialize(options) super(options) + + # options will be a hash with the following keys: + # :config => The whole configuration object + # :metrics => A metrics object + + @credentials = options[:config][:vsphere] + @conf = options[:config][:config] + @metrics = options[:metrics] end def name 'vsphere' end + + # vSphere helper methods + ADAPTER_TYPE = 'lsiLogic' + DISK_TYPE = 'thin' + DISK_MODE = 'persistent' + + def ensure_connected(connection, credentials) + connection.serviceInstance.CurrentTime + rescue + connect_to_vsphere @credentials + end + + def connect_to_vsphere(credentials) + max_tries = @conf['max_tries'] || 3 + retry_factor = @conf['retry_factor'] || 10 + try = 1 + begin + @connection = RbVmomi::VIM.connect host: credentials['server'], + user: credentials['username'], + password: credentials['password'], + insecure: credentials['insecure'] || true + @metrics.increment("connect.open") + rescue => err + try += 1 + @metrics.increment("connect.fail") + raise err if try == max_tries + sleep(try * retry_factor) + retry + end + end + + def add_disk(vm, size, datastore) + ensure_connected @connection, @credentials + + return false unless size.to_i > 0 + + vmdk_datastore = find_datastore(datastore) + vmdk_file_name = "#{vm['name']}/#{vm['name']}_#{find_vmdks(vm['name'], datastore).length + 1}.vmdk" + + controller = find_disk_controller(vm) + + vmdk_spec = RbVmomi::VIM::FileBackedVirtualDiskSpec( + capacityKb: size.to_i * 1024 * 1024, + adapterType: ADAPTER_TYPE, + diskType: DISK_TYPE + ) + + vmdk_backing = RbVmomi::VIM::VirtualDiskFlatVer2BackingInfo( + datastore: vmdk_datastore, + diskMode: DISK_MODE, + fileName: "[#{vmdk_datastore.name}] #{vmdk_file_name}" + ) + + device = RbVmomi::VIM::VirtualDisk( + backing: vmdk_backing, + capacityInKB: size.to_i * 1024 * 1024, + controllerKey: controller.key, + key: -1, + unitNumber: find_disk_unit_number(vm, controller) + ) + + device_config_spec = RbVmomi::VIM::VirtualDeviceConfigSpec( + device: device, + operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation('add') + ) + + vm_config_spec = RbVmomi::VIM::VirtualMachineConfigSpec( + deviceChange: [device_config_spec] + ) + + @connection.serviceContent.virtualDiskManager.CreateVirtualDisk_Task( + datacenter: @connection.serviceInstance.find_datacenter, + name: "[#{vmdk_datastore.name}] #{vmdk_file_name}", + spec: vmdk_spec + ).wait_for_completion + + vm.ReconfigVM_Task(spec: vm_config_spec).wait_for_completion + + true + end + + def find_datastore(datastorename) + ensure_connected @connection, @credentials + + datacenter = @connection.serviceInstance.find_datacenter + datacenter.find_datastore(datastorename) + end + + def find_device(vm, deviceName) + ensure_connected @connection, @credentials + + vm.config.hardware.device.each do |device| + return device if device.deviceInfo.label == deviceName + end + + nil + end + + def find_disk_controller(vm) + ensure_connected @connection, @credentials + + devices = find_disk_devices(vm) + + devices.keys.sort.each do |device| + if devices[device]['children'].length < 15 + return find_device(vm, devices[device]['device'].deviceInfo.label) + end + end + + nil + end + + def find_disk_devices(vm) + ensure_connected @connection, @credentials + + devices = {} + + vm.config.hardware.device.each do |device| + if device.is_a? RbVmomi::VIM::VirtualSCSIController + if devices[device.controllerKey].nil? + devices[device.key] = {} + devices[device.key]['children'] = [] + end + + devices[device.key]['device'] = device + end + + if device.is_a? RbVmomi::VIM::VirtualDisk + if devices[device.controllerKey].nil? + devices[device.controllerKey] = {} + devices[device.controllerKey]['children'] = [] + end + + devices[device.controllerKey]['children'].push(device) + end + end + + devices + end + + def find_disk_unit_number(vm, controller) + ensure_connected @connection, @credentials + + used_unit_numbers = [] + available_unit_numbers = [] + + devices = find_disk_devices(vm) + + devices.keys.sort.each do |c| + next unless controller.key == devices[c]['device'].key + used_unit_numbers.push(devices[c]['device'].scsiCtlrUnitNumber) + devices[c]['children'].each do |disk| + used_unit_numbers.push(disk.unitNumber) + end + end + + (0..15).each do |scsi_id| + if used_unit_numbers.grep(scsi_id).length <= 0 + available_unit_numbers.push(scsi_id) + end + end + + available_unit_numbers.sort[0] + end + + def find_folder(foldername) + ensure_connected @connection, @credentials + + datacenter = @connection.serviceInstance.find_datacenter + base = datacenter.vmFolder + folders = foldername.split('/') + folders.each do |folder| + if base.is_a? RbVmomi::VIM::Folder + base = base.childEntity.find { |f| f.name == folder } + else + raise(RuntimeError, "Unexpected object type encountered (#{base.class}) while finding folder") + end + end + + base + end + + # Returns an array containing cumulative CPU and memory utilization of a host, and its object reference + # Params: + # +model+:: CPU arch version to match on + # +limit+:: Hard limit for CPU or memory utilization beyond which a host is excluded for deployments + def get_host_utilization(host, model=nil, limit=90) + if model + return nil unless host_has_cpu_model? host, model + end + return nil if host.runtime.inMaintenanceMode + return nil unless host.overallStatus == 'green' + + cpu_utilization = cpu_utilization_for host + memory_utilization = memory_utilization_for host + + return nil if cpu_utilization > limit + return nil if memory_utilization > limit + + [ cpu_utilization + memory_utilization, host ] + end + + def host_has_cpu_model?(host, model) + get_host_cpu_arch_version(host) == model + end + + def get_host_cpu_arch_version(host) + cpu_model = host.hardware.cpuPkg[0].description + cpu_model_parts = cpu_model.split() + arch_version = cpu_model_parts[4] + arch_version + end + + def cpu_utilization_for(host) + cpu_usage = host.summary.quickStats.overallCpuUsage + cpu_size = host.summary.hardware.cpuMhz * host.summary.hardware.numCpuCores + (cpu_usage.to_f / cpu_size.to_f) * 100 + end + + def memory_utilization_for(host) + memory_usage = host.summary.quickStats.overallMemoryUsage + memory_size = host.summary.hardware.memorySize / 1024 / 1024 + (memory_usage.to_f / memory_size.to_f) * 100 + end + + def find_least_used_host(cluster) + ensure_connected @connection, @credentials + + cluster_object = find_cluster(cluster) + target_hosts = get_cluster_host_utilization(cluster_object) + least_used_host = target_hosts.sort[0][1] + least_used_host + end + + def find_cluster(cluster) + datacenter = @connection.serviceInstance.find_datacenter + datacenter.hostFolder.children.find { |cluster_object| cluster_object.name == cluster } + end + + def get_cluster_host_utilization(cluster) + cluster_hosts = [] + cluster.host.each do |host| + host_usage = get_host_utilization(host) + cluster_hosts << host_usage if host_usage + end + cluster_hosts + end + + def find_least_used_compatible_host(vm) + ensure_connected @connection, @credentials + + source_host = vm.summary.runtime.host + model = get_host_cpu_arch_version(source_host) + cluster = source_host.parent + target_hosts = [] + cluster.host.each do |host| + host_usage = get_host_utilization(host, model) + target_hosts << host_usage if host_usage + end + target_host = target_hosts.sort[0][1] + [target_host, target_host.name] + end + + def find_pool(poolname) + ensure_connected @connection, @credentials + + datacenter = @connection.serviceInstance.find_datacenter + base = datacenter.hostFolder + pools = poolname.split('/') + pools.each do |pool| + case + when base.is_a?(RbVmomi::VIM::Folder) + base = base.childEntity.find { |f| f.name == pool } + when base.is_a?(RbVmomi::VIM::ClusterComputeResource) + base = base.resourcePool.resourcePool.find { |f| f.name == pool } + when base.is_a?(RbVmomi::VIM::ResourcePool) + base = base.resourcePool.find { |f| f.name == pool } + else + raise(RuntimeError, "Unexpected object type encountered (#{base.class}) while finding resource pool") + end + end + + base = base.resourcePool unless base.is_a?(RbVmomi::VIM::ResourcePool) && base.respond_to?(:resourcePool) + base + end + + def find_snapshot(vm, snapshotname) + if vm.snapshot + get_snapshot_list(vm.snapshot.rootSnapshotList, snapshotname) + end + end + + def find_vm(vmname) + ensure_connected @connection, @credentials + find_vm_light(vmname) || find_vm_heavy(vmname)[vmname] + end + + def find_vm_light(vmname) + ensure_connected @connection, @credentials + + @connection.searchIndex.FindByDnsName(vmSearch: true, dnsName: vmname) + end + + def find_vm_heavy(vmname) + ensure_connected @connection, @credentials + + vmname = vmname.is_a?(Array) ? vmname : [vmname] + containerView = get_base_vm_container_from @connection + propertyCollector = @connection.propertyCollector + + objectSet = [{ + obj: containerView, + skip: true, + selectSet: [RbVmomi::VIM::TraversalSpec.new( + name: 'gettingTheVMs', + path: 'view', + skip: false, + type: 'ContainerView' + )] + }] + + propSet = [{ + pathSet: ['name'], + type: 'VirtualMachine' + }] + + results = propertyCollector.RetrievePropertiesEx( + specSet: [{ + objectSet: objectSet, + propSet: propSet + }], + options: { maxObjects: nil } + ) + + vms = {} + results.objects.each do |result| + name = result.propSet.first.val + next unless vmname.include? name + vms[name] = result.obj + end + + while results.token + results = propertyCollector.ContinueRetrievePropertiesEx(token: results.token) + results.objects.each do |result| + name = result.propSet.first.val + next unless vmname.include? name + vms[name] = result.obj + end + end + + vms + end + + def find_vmdks(vmname, datastore) + ensure_connected @connection, @credentials + + disks = [] + + vmdk_datastore = find_datastore(datastore) + + vm_files = vmdk_datastore._connection.serviceContent.propertyCollector.collectMultiple vmdk_datastore.vm, 'layoutEx.file' + vm_files.keys.each do |f| + vm_files[f]['layoutEx.file'].each do |l| + if l.name.match(/^\[#{vmdk_datastore.name}\] #{vmname}\/#{vmname}_([0-9]+).vmdk/) + disks.push(l) + end + end + end + + disks + end + + def get_base_vm_container_from(connection) + ensure_connected @connection, @credentials + + viewManager = connection.serviceContent.viewManager + viewManager.CreateContainerView( + container: connection.serviceContent.rootFolder, + recursive: true, + type: ['VirtualMachine'] + ) + end + + def get_snapshot_list(tree, snapshotname) + snapshot = nil + + tree.each do |child| + if child.name == snapshotname + snapshot ||= child.snapshot + else + snapshot ||= get_snapshot_list(child.childSnapshotList, snapshotname) + end + end + + snapshot + end + + def migrate_vm_host(vm, host) + relospec = RbVmomi::VIM.VirtualMachineRelocateSpec(host: host) + vm.RelocateVM_Task(spec: relospec).wait_for_completion + end + + def close + @connection.close + end + + + + end end end diff --git a/spec/unit/providers/vsphere_spec.rb b/spec/unit/providers/vsphere_spec.rb index 86cdc82..d3c0d0e 100644 --- a/spec/unit/providers/vsphere_spec.rb +++ b/spec/unit/providers/vsphere_spec.rb @@ -1,7 +1,29 @@ require 'spec_helper' +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 + describe 'Vmpooler::PoolManager::Provider::VSphere' do - let(:config) { {} } + let(:metrics) { Vmpooler::DummyStatsd.new } + let(:config) { YAML.load(<<-EOT +--- +:config: + max_tries: 3 + retry_factor: 10 +:vsphere: + server: "vcenter.domain.local" + username: "vcenter_user" + password: "vcenter_password" + insecure: true +EOT + ) + } + let(:fake_vm) { fake_vm = {} fake_vm['name'] = 'vm1' @@ -13,7 +35,7 @@ describe 'Vmpooler::PoolManager::Provider::VSphere' do fake_vm } - subject { Vmpooler::PoolManager::Provider::VSphere.new(config) } + subject { Vmpooler::PoolManager::Provider::VSphere.new({:config => config, :metrics => metrics}) } describe '#name' do it 'should be vsphere' do @@ -85,5 +107,2083 @@ describe 'Vmpooler::PoolManager::Provider::VSphere' do expect(subject.vm_exists?('vm')).to eq(false) end + + # vSphere helper methods + let(:credentials) { config[:vsphere] } + + let(:connection_options) {{}} + let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } + let(:vmname) { 'vm1' } + + describe '#ensure_connected' do + context 'when connection has ok' do + it 'should not attempt to reconnect' do + expect(subject).to receive(:connect_to_vsphere).exactly(0).times + + subject.ensure_connected(connection,credentials) + end + end + + context 'when connection has broken' do + before(:each) do + expect(connection.serviceInstance).to receive(:CurrentTime).and_raise(RuntimeError,'MockConnectionError') + end + + it 'should not increment the connect.open metric' do + # https://github.com/puppetlabs/vmpooler/issues/195 + expect(metrics).to receive(:increment).with('connect.open').exactly(0).times + allow(subject).to receive(:connect_to_vsphere) + + subject.ensure_connected(connection,credentials) + end + + it 'should call connect_to_vsphere to reconnect' do + allow(metrics).to receive(:increment) + allow(subject).to receive(:connect_to_vsphere).with(credentials) + + subject.ensure_connected(connection,credentials) + end + end + end + + describe '#connect_to_vsphere' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + + allow(RbVmomi::VIM).to receive(:connect).and_return(connection) + end + + 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(credentials) + end + + it 'should honor the insecure setting' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/207') + config[:vsphere][:insecure] = false + + expect(RbVmomi::VIM).to receive(:connect).with({ + :host => credentials['server'], + :user => credentials['username'], + :password => credentials['password'], + :insecure => false, + }).and_return(connection) + subject.connect_to_vsphere(credentials) + end + + it 'should default to an insecure connection' do + config[: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(credentials) + end + + it 'should set the instance level connection object' do + # NOTE - Using instance_variable_get is a code smell of code that is not testable + expect(subject.instance_variable_get("@connection")).to be_nil + subject.connect_to_vsphere(credentials) + expect(subject.instance_variable_get("@connection")).to be(connection) + end + + it 'should increment the connect.open counter' do + expect(metrics).to receive(:increment).with('connect.open') + subject.connect_to_vsphere(credentials) + end + end + + context 'connection is initially unsuccessful' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + + # 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 set the instance level connection object' do + # NOTE - Using instance_variable_get is a code smell of code that is not testable + expect(subject.instance_variable_get("@connection")).to be_nil + subject.connect_to_vsphere(credentials) + expect(subject.instance_variable_get("@connection")).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(credentials) + end + end + + context 'connection is always unsuccessful' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + + allow(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError') + allow(subject).to receive(:sleep) + end + + it 'should raise an error' do + expect{subject.connect_to_vsphere(credentials)}.to raise_error(RuntimeError,'MockError') + end + + it 'should retry the connection attempt config.max_tries times' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + expect(RbVmomi::VIM).to receive(:connect).exactly(config[:config]['max_tries']).times.and_raise(RuntimeError,'MockError') + + begin + # Swallow any errors + subject.connect_to_vsphere(credentials) + rescue + end + end + + it 'should increment the connect.fail counter config.max_tries times' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + expect(metrics).to receive(:increment).with('connect.fail').exactly(config[:config]['max_tries']).times + + begin + # Swallow any errors + subject.connect_to_vsphere(credentials) + 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 + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + 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(credentials) + rescue + end + end + 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 requried datastore + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [datastorename] } + ] + } + }} + + let(:create_virtual_disk_task) { mock_RbVmomi_VIM_Task() } + let(:reconfig_vm_task) { mock_RbVmomi_VIM_Task() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + # NOTE - This method should not be using `_connection`, instead it should be using `@conection` + # This should not be required once https://github.com/puppetlabs/vmpooler/issues/213 is resolved + mock_ds = subject.find_datastore(datastorename) + allow(mock_ds).to receive(:_connection).and_return(connection) unless mock_ds.nil? + + # 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).and_return(create_virtual_disk_task) + 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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected).at_least(:once) + + subject.add_disk(vm_object,disk_size,datastorename) + end + + context 'Succesfully addding disk' do + it 'should return true' do + expect(subject.add_disk(vm_object,disk_size,datastorename)).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) + end + end + + context 'Requested disk size is 0' do + it 'should raise an error' do + expect(subject.add_disk(vm_object,0,datastorename)).to be false + end + end + + context 'No datastores or datastore missing' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => ['missing_datastore'] } + ] + } + }} + + it 'should return false' do + expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) + 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) }.to raise_error(NoMethodError) + end + end + end + + describe '#find_datastore' do + let(:datastorename) { 'datastore' } + let(:datastore_list) { [] } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + context 'No datastores in the datacenter' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [] } + ] + } + }} + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_datastore(datastorename) + end + + it 'should return nil if the datastore is not found' do + result = subject.find_datastore(datastorename) + expect(result).to be_nil + end + end + + context 'Many datastores in the datacenter' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => ['ds1','ds2',datastorename,'ds3'] } + ] + } + }} + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_datastore(datastorename) + end + + it 'should return nil if the datastore is not found' do + result = subject.find_datastore('missing_datastore') + expect(result).to be_nil + end + + it 'should find the datastore in the datacenter' do + result = subject.find_datastore(datastorename) + + 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 + } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_device(vm_object,devicename) + end + + 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 + } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + # TODO There's no reason for this as the connection is not used in this method + expect(subject).to receive(:ensure_connected).at_least(:once) + + result = subject.find_disk_controller(vm_object) + end + + 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 + } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + # TODO There's no reason for this as the connection is not used in this method + expect(subject).to receive(:ensure_connected) + + result = subject.find_disk_devices(vm_object) + end + + 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() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + # TODO There's no reason for this as the connection is not used in this method + expect(subject).to receive(:ensure_connected).at_least(:once) + + result = subject.find_disk_unit_number(vm_object,controller) + end + + 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_folder' do + let(:foldername) { 'folder'} + let(:missing_foldername) { 'missing_folder'} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'with no folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_folder(foldername) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername)).to be_nil + end + end + + context 'with a single layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + foldername => nil, + 'folder3' => nil, + } + }) } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_folder(foldername) + end + + it 'should return the folder when found' do + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername)).to be_nil + end + end + + context 'with a VM with the same name as a folder in a single layer folder hierarchy' do + # The folder hierarchy should include a VM with same name as folder, and appear BEFORE the + # folder in the child list. + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'vm1' => { :object_type => 'vm', :name => foldername }, + foldername => nil, + 'folder3' => nil, + } + }) } + + it 'should not return a VM' do + pending('https://github.com/puppetlabs/vmpooler/issues/204') + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false + end + end + + context 'with a multi layer folder hierarchy' do + let(:end_folder_name) { 'folder'} + let(:foldername) { 'folder2/folder4/' + end_folder_name} + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => { + :children => { + 'folder3' => nil, + 'folder4' => { + :children => { + end_folder_name => nil, + }, + } + }, + }, + 'folder5' => nil, + } + }) } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_folder(foldername) + end + + it 'should return the folder when found' do + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(end_folder_name) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername)).to be_nil + end + end + + context 'with a VM with the same name as a folder in a multi layer folder hierarchy' do + # The folder hierarchy should include a VM with same name as folder mid-hierarchy (i.e. not at the end level) + # and appear BEFORE the folder in the child list. + let(:end_folder_name) { 'folder'} + let(:foldername) { 'folder2/folder4/' + end_folder_name} + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => { + :children => { + 'folder3' => nil, + 'vm1' => { :object_type => 'vm', :name => 'folder4' }, + 'folder4' => { + :children => { + end_folder_name => nil, + }, + } + }, + }, + 'folder5' => nil, + } + }) } + + it 'should not return a VM' do + pending('https://github.com/puppetlabs/vmpooler/issues/204') + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false + 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) { 80 } + 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 + + # 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 + 20) + end + + it 'should return the host' do + expect(subject.get_host_utilization(host,model,limit)[1]).to eq(host) + 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 '#find_least_used_host' do + let(:cluster_name) { 'cluster' } + let(:missing_cluster_name) { 'missing_cluster' } + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + # This mocking is a little fragile but hard to do without a real vCenter instance + 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] } + + it 'should raise an error' do + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + 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] } + + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name) + + expect(result).to be(expected_host) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_host(cluster_name) + 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_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + 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] } + + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name) + + expect(result).to be(expected_host) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_host(cluster_name) + 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_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + 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] } + + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name) + + expect(result).to be(expected_host) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_host(cluster_name) + 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_host(missing_cluster_name) + expect(result).to_not be_nil + end + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/206') + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_host(cluster_name) + end + end + end + + describe '#find_cluster' do + let(:cluster) {'cluster'} + let(:missing_cluster) {'missing_cluster'} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'no clusters in the datacenter' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + before(:each) do + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster)).to be_nil + end + end + + context 'with a single layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :hostfolder_tree => { + 'cluster1' => {:object_type => 'compute_resource'}, + 'cluster2' => {:object_type => 'compute_resource'}, + cluster => {:object_type => 'compute_resource'}, + 'cluster3' => {:object_type => 'compute_resource'}, + } + }) } + + it 'should return the cluster when found' do + result = subject.find_cluster(cluster) + + expect(result).to_not be_nil + expect(result.name).to eq(cluster) + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster)).to be_nil + end + end + + context 'with a multi layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :hostfolder_tree => { + 'cluster1' => {:object_type => 'compute_resource'}, + 'folder2' => { + :children => { + cluster => {:object_type => 'compute_resource'}, + } + }, + 'cluster3' => {:object_type => 'compute_resource'}, + } + }) } + + it 'should return the cluster when found' do + pending('https://github.com/puppetlabs/vmpooler/issues/205') + result = subject.find_cluster(cluster) + + expect(result).to_not be_nil + expect(result.name).to eq(cluster) + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster)).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_compatible_host' do + let(:vm) { mock_RbVmomi_VIM_VirtualMachine() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + 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_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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_compatible_host(vm) + 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_compatible_host(vm)}.to raise_error(NoMethodError,/undefined method/) + 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_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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_compatible_host(vm) + 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_compatible_host(vm)}.to raise_error(NoMethodError,/undefined method/) + 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_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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_compatible_host(vm) + 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_compatible_host(vm) + + expect(result).to_not be_nil + end + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/206 is fixed') + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_compatible_host(vm) + end + end + end + + describe '#find_pool' do + let(:poolname) { 'pool'} + let(:missing_poolname) { 'missing_pool'} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'with empty folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject).to receive(:ensure_connected) + + subject.find_pool(poolname) + end + + it 'should return nil if the pool is not found' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject.find_pool(missing_poolname)).to be_nil + end + end + + [ + # Single layer Host folder hierarchy + { + :context => 'single layer folder hierarchy with a resource pool', + :poolpath => 'pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'pool' => {:object_type => 'resource_pool'}, + 'folder3' => nil, + }, + }, + { + :context => 'single layer folder hierarchy with a child resource pool', + :poolpath => 'parentpool/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'parentpool' => {:object_type => 'resource_pool', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + 'folder3' => nil, + }, + }, + { + :context => 'single layer folder hierarchy with a resource pool within a cluster', + :poolpath => 'cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + 'folder3' => nil, + }, + }, + # Multi layer Host folder hierarchy + { + :context => 'multi layer folder hierarchy with a resource pool', + :poolpath => 'folder2/folder4/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a child resource pool', + :poolpath => 'folder2/folder4/parentpool/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'parentpool' => {:object_type => 'resource_pool', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a resource pool within a cluster', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + ].each do |testcase| + context testcase[:context] do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_pool(testcase[:poolpath]) + end + + it 'should return the pool when found' do + result = subject.find_pool(testcase[:poolpath]) + + expect(result).to_not be_nil + expect(result.name).to eq(testcase[:poolname]) + expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true + end + + it 'should return nil if the poolname is not found' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject.find_pool(missing_poolname)).to be_nil + end + end + end + + # Tests for issue https://github.com/puppetlabs/vmpooler/issues/210 + [ + { + :context => 'multi layer folder hierarchy with a resource pool the same name as a folder', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'bad_pool' => {:object_type => 'resource_pool', :name => 'folder4'}, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a cluster the same name as a folder', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'bad_cluster' => {:object_type => 'cluster_compute_resource', :name => 'folder4'}, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + ].each do |testcase| + context testcase[:context] do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/210') + expect(subject).to receive(:ensure_connected) + + subject.find_pool(testcase[:poolpath]) + end + + it 'should return the pool when found' do + pending('https://github.com/puppetlabs/vmpooler/issues/210') + result = subject.find_pool(testcase[:poolpath]) + + expect(result).to_not be_nil + expect(result.name).to eq(testcase[:poolname]) + expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true + end + 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_VirtualMachine() } + + 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 + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + allow(subject).to receive(:find_vm_light).and_return('vmlight') + allow(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) + end + + it 'should ensure the connection' do + # TODO This seems like overkill as we immediately call vm_light and heavy which + # does the same thing. Also the connection isn't actually used in this method + expect(subject).to receive(:ensure_connected) + + subject.find_vm(vmname) + end + + it 'should call find_vm_light' do + expect(subject).to receive(:find_vm_light).and_return('vmlight') + + expect(subject.find_vm(vmname)).to eq('vmlight') + end + + it 'should not call find_vm_heavy if find_vm_light finds the VM' do + expect(subject).to receive(:find_vm_light).and_return('vmlight') + expect(subject).to receive(:find_vm_heavy).exactly(0).times + + expect(subject.find_vm(vmname)).to eq('vmlight') + end + + it 'should call find_vm_heavy when find_vm_light returns nil' do + expect(subject).to receive(:find_vm_light).and_return(nil) + expect(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) + + expect(subject.find_vm(vmname)).to eq('vmheavy') + end + end + + describe '#find_vm_light' do + let(:missing_vm) { 'missing_vm' } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + allow(connection.searchIndex).to receive(:FindByDnsName).and_return(nil) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_vm_light(vmname) + end + + it 'should call FindByDnsName with the correct parameters' do + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: vmname, + }) + + subject.find_vm_light(vmname) + end + + it 'should return the VM object when found' do + vm_object = mock_RbVmomi_VIM_VirtualMachine() + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: vmname, + }).and_return(vm_object) + + expect(subject.find_vm_light(vmname)).to be(vm_object) + end + + it 'should return nil if the VM is not found' do + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: missing_vm, + }).and_return(nil) + + expect(subject.find_vm_light(missing_vm)).to be_nil + end + end + + describe '#find_vm_heavy' do + let(:missing_vm) { 'missing_vm' } + # Return an empty result by default + let(:retrieve_result) {{}} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + allow(connection.propertyCollector).to receive(:RetrievePropertiesEx).and_return(mock_RbVmomi_VIM_RetrieveResult(retrieve_result)) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected).at_least(:once) + + subject.find_vm_heavy(vmname) + end + + context 'Search result is empty' do + it 'should return empty hash' do + expect(subject.find_vm_heavy(vmname)).to eq({}) + end + end + + context 'Search result contains VMs but no matches' do + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + ] + } + } + + it 'should return empty hash' do + expect(subject.find_vm_heavy(vmname)).to eq({}) + end + end + + context 'Search contains a single match' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => vmname, :object => vm_object }, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + ] + } + } + + it 'should return single result' do + result = subject.find_vm_heavy(vmname) + expect(result.keys.count).to eq(1) + end + + it 'should return the matching VM Object' do + result = subject.find_vm_heavy(vmname) + expect(result[vmname]).to be(vm_object) + end + end + + context 'Search contains a two matches' do + let(:vm_object1) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:vm_object2) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => vmname, :object => vm_object1 }, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + { 'name' => vmname, :object => vm_object2 }, + ] + } + } + + it 'should return one result' do + result = subject.find_vm_heavy(vmname) + expect(result.keys.count).to eq(1) + end + + it 'should return the last matching VM Object' do + result = subject.find_vm_heavy(vmname) + expect(result[vmname]).to be(vm_object2) + end + end + end + + describe '#find_vmdks' do + let(:datastorename) { 'datastore' } + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [datastorename] } + ] + } + }} + + let(:collectMultiple_response) { {} } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + # NOTE - This method should not be using `_connection`, instead it should be using `@conection` + mock_ds = subject.find_datastore(datastorename) + allow(mock_ds).to receive(:_connection).and_return(connection) + allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response) + end + + it 'should not use _connction to get the underlying connection object' do + pending('https://github.com/puppetlabs/vmpooler/issues/213') + + mock_ds = subject.find_datastore(datastorename) + expect(mock_ds).to receive(:_connection).exactly(0).times + + begin + # ignore all errors. What's important is that it doesn't call _connection + subject.find_vmdks(vmname,datastorename) + rescue + end + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected).at_least(:once) + + subject.find_vmdks(vmname,datastorename) + end + + context 'Searching all files for all VMs on a Datastore' do + # This is fairly fragile mocking + let(:collectMultiple_response) { { + 'FakeVMObject1' => { 'layoutEx.file' => + [ + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 101, :name => "[#{datastorename}] mock1/mock1_0.vmdk"}) + ]}, + vmname => { 'layoutEx.file' => + [ + # VMDKs which should match + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 1, :name => "[#{datastorename}] #{vmname}/#{vmname}_0.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 2, :name => "[#{datastorename}] #{vmname}/#{vmname}_1.vmdk"}), + # VMDKs which should not match + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 102, :name => "[otherdatastore] #{vmname}/#{vmname}_0.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 103, :name => "[otherdatastore] #{vmname}/#{vmname}.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 104, :name => "[otherdatastore] #{vmname}/#{vmname}_abc.vmdk"}), + ]}, + } } + + it 'should return empty array if no VMDKs match the VM name' do + expect(subject.find_vmdks('missing_vm_name',datastorename)).to eq([]) + end + + it 'should return matching VMDKs for the VM' do + result = subject.find_vmdks(vmname,datastorename) + expect(result).to_not be_nil + expect(result.count).to eq(2) + # The keys for each VMDK should be less that 100 as per the mocks + result.each do |fileinfo| + expect(fileinfo.key).to be < 100 + end + end + end + end + + describe '#get_base_vm_container_from' do + let(:local_connection) { mock_RbVmomi_VIM_Connection() } + + before(:each) do + allow(subject).to receive(:ensure_connected) + end + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/212') + expect(subject).to receive(:ensure_connected).with(local_connection,credentials) + + subject.get_base_vm_container_from(local_connection) + end + + it 'should return a recursive view of type VirtualMachine' do + result = subject.get_base_vm_container_from(local_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 RelovateVM_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 '#close' do + context 'no connection has been made' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + end + + it 'should not error' do + pending('https://github.com/puppetlabs/vmpooler/issues/211') + subject.close + end + end + + context 'on an open connection' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should close the underlying connection object' do + expect(connection).to receive(:close) + subject.close + end + end + end end end From e5db02b44f3ee7e9bb1e3f7b55aea8024d8ee0cf Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 Mar 2017 09:04:16 -0700 Subject: [PATCH 2/6] (POOLER-70) Rename conflicting method in vSphere Provider Previously, the vSphere Provider had two methods called `find_least_used_compatible_host`: one in the base class and one in the vSphere helper methods. This commit renames the vSphere helper methods and tests to `find_least_used_vsphere_compatible_host` to stop the conflict. --- lib/vmpooler/providers/vsphere.rb | 2 +- spec/unit/providers/vsphere_spec.rb | 3836 +++++++++++++-------------- 2 files changed, 1919 insertions(+), 1919 deletions(-) diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb index 7c59390..8bf7505 100644 --- a/lib/vmpooler/providers/vsphere.rb +++ b/lib/vmpooler/providers/vsphere.rb @@ -265,7 +265,7 @@ module Vmpooler cluster_hosts end - def find_least_used_compatible_host(vm) + def find_least_used_vpshere_compatible_host(vm) ensure_connected @connection, @credentials source_host = vm.summary.runtime.host diff --git a/spec/unit/providers/vsphere_spec.rb b/spec/unit/providers/vsphere_spec.rb index d3c0d0e..717b95e 100644 --- a/spec/unit/providers/vsphere_spec.rb +++ b/spec/unit/providers/vsphere_spec.rb @@ -107,2083 +107,2083 @@ EOT expect(subject.vm_exists?('vm')).to eq(false) end + end - # vSphere helper methods - let(:credentials) { config[:vsphere] } + # vSphere helper methods + let(:credentials) { config[:vsphere] } - let(:connection_options) {{}} - let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } - let(:vmname) { 'vm1' } + let(:connection_options) {{}} + let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } + let(:vmname) { 'vm1' } - describe '#ensure_connected' do - context 'when connection has ok' do - it 'should not attempt to reconnect' do - expect(subject).to receive(:connect_to_vsphere).exactly(0).times + describe '#ensure_connected' do + context 'when connection has ok' do + it 'should not attempt to reconnect' do + expect(subject).to receive(:connect_to_vsphere).exactly(0).times - subject.ensure_connected(connection,credentials) - end - end - - context 'when connection has broken' do - before(:each) do - expect(connection.serviceInstance).to receive(:CurrentTime).and_raise(RuntimeError,'MockConnectionError') - end - - it 'should not increment the connect.open metric' do - # https://github.com/puppetlabs/vmpooler/issues/195 - expect(metrics).to receive(:increment).with('connect.open').exactly(0).times - allow(subject).to receive(:connect_to_vsphere) - - subject.ensure_connected(connection,credentials) - end - - it 'should call connect_to_vsphere to reconnect' do - allow(metrics).to receive(:increment) - allow(subject).to receive(:connect_to_vsphere).with(credentials) - - subject.ensure_connected(connection,credentials) - end + subject.ensure_connected(connection,credentials) end end - describe '#connect_to_vsphere' do + context 'when connection has broken' do + before(:each) do + expect(connection.serviceInstance).to receive(:CurrentTime).and_raise(RuntimeError,'MockConnectionError') + end + + it 'should not increment the connect.open metric' do + # https://github.com/puppetlabs/vmpooler/issues/195 + expect(metrics).to receive(:increment).with('connect.open').exactly(0).times + allow(subject).to receive(:connect_to_vsphere) + + subject.ensure_connected(connection,credentials) + end + + it 'should call connect_to_vsphere to reconnect' do + allow(metrics).to receive(:increment) + allow(subject).to receive(:connect_to_vsphere).with(credentials) + + subject.ensure_connected(connection,credentials) + end + end + end + + describe '#connect_to_vsphere' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + + allow(RbVmomi::VIM).to receive(:connect).and_return(connection) + end + + 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(credentials) + end + + it 'should honor the insecure setting' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/207') + config[:vsphere][:insecure] = false + + expect(RbVmomi::VIM).to receive(:connect).with({ + :host => credentials['server'], + :user => credentials['username'], + :password => credentials['password'], + :insecure => false, + }).and_return(connection) + subject.connect_to_vsphere(credentials) + end + + it 'should default to an insecure connection' do + config[: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(credentials) + end + + it 'should set the instance level connection object' do + # NOTE - Using instance_variable_get is a code smell of code that is not testable + expect(subject.instance_variable_get("@connection")).to be_nil + subject.connect_to_vsphere(credentials) + expect(subject.instance_variable_get("@connection")).to be(connection) + end + + it 'should increment the connect.open counter' do + expect(metrics).to receive(:increment).with('connect.open') + subject.connect_to_vsphere(credentials) + end + end + + context 'connection is initially unsuccessful' do before(:each) do # NOTE - Using instance_variable_set is a code smell of code that is not testable subject.instance_variable_set("@connection",nil) - allow(RbVmomi::VIM).to receive(:connect).and_return(connection) + # 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 - 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(credentials) - end - - it 'should honor the insecure setting' do - pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/207') - config[:vsphere][:insecure] = false - - expect(RbVmomi::VIM).to receive(:connect).with({ - :host => credentials['server'], - :user => credentials['username'], - :password => credentials['password'], - :insecure => false, - }).and_return(connection) - subject.connect_to_vsphere(credentials) - end - - it 'should default to an insecure connection' do - config[: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(credentials) - end - - it 'should set the instance level connection object' do - # NOTE - Using instance_variable_get is a code smell of code that is not testable - expect(subject.instance_variable_get("@connection")).to be_nil - subject.connect_to_vsphere(credentials) - expect(subject.instance_variable_get("@connection")).to be(connection) - end - - it 'should increment the connect.open counter' do - expect(metrics).to receive(:increment).with('connect.open') + it 'should set the instance level connection object' do + # NOTE - Using instance_variable_get is a code smell of code that is not testable + expect(subject.instance_variable_get("@connection")).to be_nil + subject.connect_to_vsphere(credentials) + expect(subject.instance_variable_get("@connection")).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(credentials) + end + end + + context 'connection is always unsuccessful' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + + allow(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError') + allow(subject).to receive(:sleep) + end + + it 'should raise an error' do + expect{subject.connect_to_vsphere(credentials)}.to raise_error(RuntimeError,'MockError') + end + + it 'should retry the connection attempt config.max_tries times' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + expect(RbVmomi::VIM).to receive(:connect).exactly(config[:config]['max_tries']).times.and_raise(RuntimeError,'MockError') + + begin + # Swallow any errors subject.connect_to_vsphere(credentials) + rescue end end - context 'connection is initially unsuccessful' do - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",nil) + it 'should increment the connect.fail counter config.max_tries times' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + expect(metrics).to receive(:increment).with('connect.fail').exactly(config[:config]['max_tries']).times - # 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 set the instance level connection object' do - # NOTE - Using instance_variable_get is a code smell of code that is not testable - expect(subject.instance_variable_get("@connection")).to be_nil - subject.connect_to_vsphere(credentials) - expect(subject.instance_variable_get("@connection")).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 + begin + # Swallow any errors subject.connect_to_vsphere(credentials) + rescue end end - context 'connection is always unsuccessful' do - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",nil) + [{: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 + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + config[:config]['max_tries'] = testcase[:max_tries] + config[:config]['retry_factor'] = testcase[:retry_factor] - allow(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError') - allow(subject).to receive(:sleep) - end + (1..testcase[:max_tries] - 1).each do |try| + expect(subject).to receive(:sleep).with(testcase[:retry_factor] * try).ordered + end - it 'should raise an error' do - expect{subject.connect_to_vsphere(credentials)}.to raise_error(RuntimeError,'MockError') - end - - it 'should retry the connection attempt config.max_tries times' do - pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') - expect(RbVmomi::VIM).to receive(:connect).exactly(config[:config]['max_tries']).times.and_raise(RuntimeError,'MockError') - - begin - # Swallow any errors - subject.connect_to_vsphere(credentials) - rescue - end - end - - it 'should increment the connect.fail counter config.max_tries times' do - pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') - expect(metrics).to receive(:increment).with('connect.fail').exactly(config[:config]['max_tries']).times - - begin - # Swallow any errors - subject.connect_to_vsphere(credentials) - 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 - pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') - 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(credentials) - rescue - end + begin + # Swallow any errors + subject.connect_to_vsphere(credentials) + rescue end end end end end + end - describe '#add_disk' do - let(:datastorename) { 'datastore' } - let(:disk_size) { 30 } - let(:collectMultiple_response) { {} } + describe '#add_disk' do + let(:datastorename) { 'datastore' } + let(:disk_size) { 30 } + let(:collectMultiple_response) { {} } - let(:vm_scsi_controller) { mock_RbVmomi_VIM_VirtualSCSIController() } + 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 + # 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 + mock_vm + } + + # Require at least one DC with the requried datastore + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [datastorename] } + ] } + }} - # Require at least one DC with the requried datastore + let(:create_virtual_disk_task) { mock_RbVmomi_VIM_Task() } + let(:reconfig_vm_task) { mock_RbVmomi_VIM_Task() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + # NOTE - This method should not be using `_connection`, instead it should be using `@conection` + # This should not be required once https://github.com/puppetlabs/vmpooler/issues/213 is resolved + mock_ds = subject.find_datastore(datastorename) + allow(mock_ds).to receive(:_connection).and_return(connection) unless mock_ds.nil? + + # 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).and_return(create_virtual_disk_task) + 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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected).at_least(:once) + + subject.add_disk(vm_object,disk_size,datastorename) + end + + context 'Succesfully addding disk' do + it 'should return true' do + expect(subject.add_disk(vm_object,disk_size,datastorename)).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) + end + end + + context 'Requested disk size is 0' do + it 'should raise an error' do + expect(subject.add_disk(vm_object,0,datastorename)).to be false + end + end + + context 'No datastores or datastore missing' do let(:connection_options) {{ :serviceContent => { :datacenters => [ - { :name => 'MockDC', :datastores => [datastorename] } + { :name => 'MockDC', :datastores => ['missing_datastore'] } ] } }} - let(:create_virtual_disk_task) { mock_RbVmomi_VIM_Task() } - let(:reconfig_vm_task) { mock_RbVmomi_VIM_Task() } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - - # NOTE - This method should not be using `_connection`, instead it should be using `@conection` - # This should not be required once https://github.com/puppetlabs/vmpooler/issues/213 is resolved - mock_ds = subject.find_datastore(datastorename) - allow(mock_ds).to receive(:_connection).and_return(connection) unless mock_ds.nil? - - # 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).and_return(create_virtual_disk_task) - 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) + it 'should return false' do + expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) end + end - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected).at_least(:once) + context 'VM does not have a SCSI Controller' do + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine({ + :name => vmname, + }) - subject.add_disk(vm_object,disk_size,datastorename) + mock_vm + } + + it 'should raise an error' do + expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) end + end + end - context 'Succesfully addding disk' do - it 'should return true' do - expect(subject.add_disk(vm_object,disk_size,datastorename)).to be true - end + describe '#find_datastore' do + let(:datastorename) { 'datastore' } + let(:datastore_list) { [] } - 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) + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end - - subject.add_disk(vm_object,disk_size,datastorename) - end - end - - context 'Requested disk size is 0' do - it 'should raise an error' do - expect(subject.add_disk(vm_object,0,datastorename)).to be false - end - end - - context 'No datastores or datastore missing' do - let(:connection_options) {{ - :serviceContent => { - :datacenters => [ - { :name => 'MockDC', :datastores => ['missing_datastore'] } - ] - } - }} - - it 'should return false' do - expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) - end - end - - context 'VM does not have a SCSI Controller' do - let(:vm_object) { - mock_vm = mock_RbVmomi_VIM_VirtualMachine({ - :name => vmname, - }) - - mock_vm + context 'No datastores in the datacenter' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [] } + ] } - - it 'should raise an error' do - expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) - end - end - end - - describe '#find_datastore' do - let(:datastorename) { 'datastore' } - let(:datastore_list) { [] } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - context 'No datastores in the datacenter' do - let(:connection_options) {{ - :serviceContent => { - :datacenters => [ - { :name => 'MockDC', :datastores => [] } - ] - } - }} - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_datastore(datastorename) - end - - it 'should return nil if the datastore is not found' do - result = subject.find_datastore(datastorename) - expect(result).to be_nil - end - end - - context 'Many datastores in the datacenter' do - let(:connection_options) {{ - :serviceContent => { - :datacenters => [ - { :name => 'MockDC', :datastores => ['ds1','ds2',datastorename,'ds3'] } - ] - } - }} - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_datastore(datastorename) - end - - it 'should return nil if the datastore is not found' do - result = subject.find_datastore('missing_datastore') - expect(result).to be_nil - end - - it 'should find the datastore in the datacenter' do - result = subject.find_datastore(datastorename) - - 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 - } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end + }} it 'should ensure the connection' do expect(subject).to receive(:ensure_connected) - subject.find_device(vm_object,devicename) + subject.find_datastore(datastorename) end - 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') - + it 'should return nil if the datastore is not found' do + result = subject.find_datastore(datastorename) expect(result).to be_nil end end - describe '#find_disk_controller' do - let(:vm_object) { - mock_vm = mock_RbVmomi_VIM_VirtualMachine() - - mock_vm - } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end + context 'Many datastores in the datacenter' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => ['ds1','ds2',datastorename,'ds3'] } + ] + } + }} it 'should ensure the connection' do - # TODO There's no reason for this as the connection is not used in this method - expect(subject).to receive(:ensure_connected).at_least(:once) - - result = subject.find_disk_controller(vm_object) - end - - 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 - } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - it 'should ensure the connection' do - # TODO There's no reason for this as the connection is not used in this method expect(subject).to receive(:ensure_connected) - result = subject.find_disk_devices(vm_object) + subject.find_datastore(datastorename) end - it 'should return empty hash when there are no devices' do - result = subject.find_disk_devices(vm_object) - - expect(result).to eq({}) + it 'should return nil if the datastore is not found' do + result = subject.find_datastore('missing_datastore') + expect(result).to be_nil 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({}) + it 'should find the datastore in the datacenter' do + result = subject.find_datastore(datastorename) + + 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 - it 'should return a device for a VirtualSCSIController device with no children' do + 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 + } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_device(vm_object,devicename) + end + + 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 + } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + # TODO There's no reason for this as the connection is not used in this method + expect(subject).to receive(:ensure_connected).at_least(:once) + + result = subject.find_disk_controller(vm_object) + end + + 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'}) - result = subject.find_disk_devices(vm_object) + # Add the disks + (1..testcase).each do + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualDisk({ :controllerKey => mock_scsi.key }) + end - 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 + result = subject.find_disk_controller(vm_object) - 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) + expect(result).to eq(mock_scsi) end end - describe '#find_disk_unit_number' do - let(:vm_object) { - mock_vm = mock_RbVmomi_VIM_VirtualMachine() + [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'}) - mock_vm - } - let(:controller) { mock_RbVmomi_VIM_VirtualSCSIController() } + # 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 + } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + # TODO There's no reason for this as the connection is not used in this method + expect(subject).to receive(:ensure_connected) + + result = subject.find_disk_devices(vm_object) + end + + 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() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should ensure the connection' do + # TODO There's no reason for this as the connection is not used in this method + expect(subject).to receive(:ensure_connected).at_least(:once) + + result = subject.find_disk_unit_number(vm_object,controller) + end + + 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 - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) + 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_folder' do + let(:foldername) { 'folder'} + let(:missing_foldername) { 'missing_folder'} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'with no folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_folder(foldername) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername)).to be_nil + end + end + + context 'with a single layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + foldername => nil, + 'folder3' => nil, + } + }) } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_folder(foldername) + end + + it 'should return the folder when found' do + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername)).to be_nil + end + end + + context 'with a VM with the same name as a folder in a single layer folder hierarchy' do + # The folder hierarchy should include a VM with same name as folder, and appear BEFORE the + # folder in the child list. + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'vm1' => { :object_type => 'vm', :name => foldername }, + foldername => nil, + 'folder3' => nil, + } + }) } + + it 'should not return a VM' do + pending('https://github.com/puppetlabs/vmpooler/issues/204') + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false + end + end + + context 'with a multi layer folder hierarchy' do + let(:end_folder_name) { 'folder'} + let(:foldername) { 'folder2/folder4/' + end_folder_name} + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => { + :children => { + 'folder3' => nil, + 'folder4' => { + :children => { + end_folder_name => nil, + }, + } + }, + }, + 'folder5' => nil, + } + }) } + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_folder(foldername) + end + + it 'should return the folder when found' do + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(end_folder_name) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername)).to be_nil + end + end + + context 'with a VM with the same name as a folder in a multi layer folder hierarchy' do + # The folder hierarchy should include a VM with same name as folder mid-hierarchy (i.e. not at the end level) + # and appear BEFORE the folder in the child list. + let(:end_folder_name) { 'folder'} + let(:foldername) { 'folder2/folder4/' + end_folder_name} + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => { + :children => { + 'folder3' => nil, + 'vm1' => { :object_type => 'vm', :name => 'folder4' }, + 'folder4' => { + :children => { + end_folder_name => nil, + }, + } + }, + }, + 'folder5' => nil, + } + }) } + + it 'should not return a VM' do + pending('https://github.com/puppetlabs/vmpooler/issues/204') + result = subject.find_folder(foldername) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false + 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) { 80 } + 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 + + # 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 + 20) + end + + it 'should return the host' do + expect(subject.get_host_utilization(host,model,limit)[1]).to eq(host) + 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 '#find_least_used_host' do + let(:cluster_name) { 'cluster' } + let(:missing_cluster_name) { 'missing_cluster' } + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + # This mocking is a little fragile but hard to do without a real vCenter instance + 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] } + + it 'should raise an error' do + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) end it 'should ensure the connection' do - # TODO There's no reason for this as the connection is not used in this method - expect(subject).to receive(:ensure_connected).at_least(:once) + expect(subject).to receive(:ensure_connected) - result = subject.find_disk_unit_number(vm_object,controller) - end - - 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 + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) end end - describe '#find_folder' do - let(:foldername) { 'folder'} - let(:missing_foldername) { 'missing_folder'} - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) - end - - context 'with no folder hierarchy' do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_folder(foldername) - end - - it 'should return nil if the folder is not found' do - expect(subject.find_folder(missing_foldername)).to be_nil - end - end - - context 'with a single layer folder hierarchy' do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ - :vmfolder_tree => { - 'folder1' => nil, - 'folder2' => nil, - foldername => nil, - 'folder3' => nil, - } - }) } - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_folder(foldername) - end - - it 'should return the folder when found' do - result = subject.find_folder(foldername) - expect(result).to_not be_nil - expect(result.name).to eq(foldername) - end - - it 'should return nil if the folder is not found' do - expect(subject.find_folder(missing_foldername)).to be_nil - end - end - - context 'with a VM with the same name as a folder in a single layer folder hierarchy' do - # The folder hierarchy should include a VM with same name as folder, and appear BEFORE the - # folder in the child list. - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ - :vmfolder_tree => { - 'folder1' => nil, - 'vm1' => { :object_type => 'vm', :name => foldername }, - foldername => nil, - 'folder3' => nil, - } - }) } - - it 'should not return a VM' do - pending('https://github.com/puppetlabs/vmpooler/issues/204') - result = subject.find_folder(foldername) - expect(result).to_not be_nil - expect(result.name).to eq(foldername) - expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false - end - end - - context 'with a multi layer folder hierarchy' do - let(:end_folder_name) { 'folder'} - let(:foldername) { 'folder2/folder4/' + end_folder_name} - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ - :vmfolder_tree => { - 'folder1' => nil, - 'folder2' => { - :children => { - 'folder3' => nil, - 'folder4' => { - :children => { - end_folder_name => nil, - }, - } - }, - }, - 'folder5' => nil, - } - }) } - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_folder(foldername) - end - - it 'should return the folder when found' do - result = subject.find_folder(foldername) - expect(result).to_not be_nil - expect(result.name).to eq(end_folder_name) - end - - it 'should return nil if the folder is not found' do - expect(subject.find_folder(missing_foldername)).to be_nil - end - end - - context 'with a VM with the same name as a folder in a multi layer folder hierarchy' do - # The folder hierarchy should include a VM with same name as folder mid-hierarchy (i.e. not at the end level) - # and appear BEFORE the folder in the child list. - let(:end_folder_name) { 'folder'} - let(:foldername) { 'folder2/folder4/' + end_folder_name} - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ - :vmfolder_tree => { - 'folder1' => nil, - 'folder2' => { - :children => { - 'folder3' => nil, - 'vm1' => { :object_type => 'vm', :name => 'folder4' }, - 'folder4' => { - :children => { - end_folder_name => nil, - }, - } - }, - }, - 'folder5' => nil, - } - }) } - - it 'should not return a VM' do - pending('https://github.com/puppetlabs/vmpooler/issues/204') - result = subject.find_folder(foldername) - expect(result).to_not be_nil - expect(result.name).to eq(foldername) - expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false - 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) { 80 } - 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 - - # 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 + 20) - end - - it 'should return the host' do - expect(subject.get_host_utilization(host,model,limit)[1]).to eq(host) - 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 '#find_least_used_host' do - let(:cluster_name) { 'cluster' } - let(:missing_cluster_name) { 'missing_cluster' } - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - - # This mocking is a little fragile but hard to do without a real vCenter instance - 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({ + context 'standalone host within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [{ :name => cluster_name, - :hosts => [{ - :name => cluster_name, - }]})} - let(:expected_host) { cluster_object.host[0] } + }]})} + let(:expected_host) { cluster_object.host[0] } - it 'should raise an error' do - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) - end + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name) - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) - end + expect(result).to be(expected_host) 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] } + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) - it 'should return the standalone host' do - result = subject.find_least_used_host(cluster_name) - - expect(result).to be(expected_host) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - 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_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) - 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] } - - it 'should return the standalone host' do - result = subject.find_least_used_host(cluster_name) - - expect(result).to be(expected_host) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - 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_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) - 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] } - - it 'should return the standalone host' do - result = subject.find_least_used_host(cluster_name) - - expect(result).to be(expected_host) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - 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_host(missing_cluster_name) - expect(result).to_not be_nil - end - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/206') - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - end + result = subject.find_least_used_host(cluster_name) end end - describe '#find_cluster' do - let(:cluster) {'cluster'} - let(:missing_cluster) {'missing_cluster'} + 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] } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + it 'should raise an error' do + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) end - context 'no clusters in the datacenter' do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) - before(:each) do - end - - it 'should return nil if the cluster is not found' do - expect(subject.find_cluster(missing_cluster)).to be_nil - end - end - - context 'with a single layer folder hierarchy' do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ - :hostfolder_tree => { - 'cluster1' => {:object_type => 'compute_resource'}, - 'cluster2' => {:object_type => 'compute_resource'}, - cluster => {:object_type => 'compute_resource'}, - 'cluster3' => {:object_type => 'compute_resource'}, - } - }) } - - it 'should return the cluster when found' do - result = subject.find_cluster(cluster) - - expect(result).to_not be_nil - expect(result.name).to eq(cluster) - end - - it 'should return nil if the cluster is not found' do - expect(subject.find_cluster(missing_cluster)).to be_nil - end - end - - context 'with a multi layer folder hierarchy' do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ - :hostfolder_tree => { - 'cluster1' => {:object_type => 'compute_resource'}, - 'folder2' => { - :children => { - cluster => {:object_type => 'compute_resource'}, - } - }, - 'cluster3' => {:object_type => 'compute_resource'}, - } - }) } - - it 'should return the cluster when found' do - pending('https://github.com/puppetlabs/vmpooler/issues/205') - result = subject.find_cluster(cluster) - - expect(result).to_not be_nil - expect(result.name).to eq(cluster) - end - - it 'should return nil if the cluster is not found' do - expect(subject.find_cluster(missing_cluster)).to be_nil - end + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) 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_compatible_host' do - let(:vm) { mock_RbVmomi_VIM_VirtualMachine() } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - 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_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 - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_compatible_host(vm) - 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_compatible_host(vm)}.to raise_error(NoMethodError,/undefined method/) - end - end - - context 'cluster of 3 hosts within limits' do - let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + 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] } + ]}) } + 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 standalone host' do + result = subject.find_least_used_host(cluster_name) - it 'should return the least used host' do - result = subject.find_least_used_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 - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_compatible_host(vm) - end + expect(result).to be(expected_host) 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] } + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) - 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 + result = subject.find_least_used_host(cluster_name) + end + end - it 'should raise error' do - expect{subject.find_least_used_compatible_host(vm)}.to raise_error(NoMethodError,/undefined method/) - 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_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) 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 => [ + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + 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[2] } + ]}) } + 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 standalone host' do + result = subject.find_least_used_host(cluster_name) - it 'should return the least used host' do - result = subject.find_least_used_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 - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_compatible_host(vm) - end + expect(result).to be(expected_host) 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] } + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) - 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_compatible_host(vm) - - expect(result).to_not be_nil - end - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/206 is fixed') - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_compatible_host(vm) - end + result = subject.find_least_used_host(cluster_name) end end - describe '#find_pool' do - let(:poolname) { 'pool'} - let(:missing_poolname) { 'missing_pool'} + 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] } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + it 'should return a host' do + pending('https://github.com/puppetlabs/vmpooler/issues/206') + result = subject.find_least_used_host(missing_cluster_name) + expect(result).to_not be_nil end - context 'with empty folder hierarchy' do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/206') + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_host(cluster_name) + end + end + end + + describe '#find_cluster' do + let(:cluster) {'cluster'} + let(:missing_cluster) {'missing_cluster'} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'no clusters in the datacenter' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + before(:each) do + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster)).to be_nil + end + end + + context 'with a single layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :hostfolder_tree => { + 'cluster1' => {:object_type => 'compute_resource'}, + 'cluster2' => {:object_type => 'compute_resource'}, + cluster => {:object_type => 'compute_resource'}, + 'cluster3' => {:object_type => 'compute_resource'}, + } + }) } + + it 'should return the cluster when found' do + result = subject.find_cluster(cluster) + + expect(result).to_not be_nil + expect(result.name).to eq(cluster) + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster)).to be_nil + end + end + + context 'with a multi layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :hostfolder_tree => { + 'cluster1' => {:object_type => 'compute_resource'}, + 'folder2' => { + :children => { + cluster => {:object_type => 'compute_resource'}, + } + }, + 'cluster3' => {:object_type => 'compute_resource'}, + } + }) } + + it 'should return the cluster when found' do + pending('https://github.com/puppetlabs/vmpooler/issues/205') + result = subject.find_cluster(cluster) + + expect(result).to_not be_nil + expect(result.name).to eq(cluster) + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster)).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() } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + 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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_vpshere_compatible_host(vm) + 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(NoMethodError,/undefined method/) + 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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_vpshere_compatible_host(vm) + 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(NoMethodError,/undefined method/) + 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 + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_vpshere_compatible_host(vm) + 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 + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/206 is fixed') + expect(subject).to receive(:ensure_connected) + + result = subject.find_least_used_vpshere_compatible_host(vm) + end + end + end + + describe '#find_pool' do + let(:poolname) { 'pool'} + let(:missing_poolname) { 'missing_pool'} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'with empty folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject).to receive(:ensure_connected) + + subject.find_pool(poolname) + end + + it 'should return nil if the pool is not found' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject.find_pool(missing_poolname)).to be_nil + end + end + + [ + # Single layer Host folder hierarchy + { + :context => 'single layer folder hierarchy with a resource pool', + :poolpath => 'pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'pool' => {:object_type => 'resource_pool'}, + 'folder3' => nil, + }, + }, + { + :context => 'single layer folder hierarchy with a child resource pool', + :poolpath => 'parentpool/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'parentpool' => {:object_type => 'resource_pool', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + 'folder3' => nil, + }, + }, + { + :context => 'single layer folder hierarchy with a resource pool within a cluster', + :poolpath => 'cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + 'folder3' => nil, + }, + }, + # Multi layer Host folder hierarchy + { + :context => 'multi layer folder hierarchy with a resource pool', + :poolpath => 'folder2/folder4/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a child resource pool', + :poolpath => 'folder2/folder4/parentpool/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'parentpool' => {:object_type => 'resource_pool', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a resource pool within a cluster', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + ].each do |testcase| + context testcase[:context] do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/209') expect(subject).to receive(:ensure_connected) - subject.find_pool(poolname) + subject.find_pool(testcase[:poolpath]) end - it 'should return nil if the pool is not found' do + it 'should return the pool when found' do + result = subject.find_pool(testcase[:poolpath]) + + expect(result).to_not be_nil + expect(result.name).to eq(testcase[:poolname]) + expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true + end + + it 'should return nil if the poolname is not found' do pending('https://github.com/puppetlabs/vmpooler/issues/209') expect(subject.find_pool(missing_poolname)).to be_nil end end + end - [ - # Single layer Host folder hierarchy - { - :context => 'single layer folder hierarchy with a resource pool', - :poolpath => 'pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => nil, - 'pool' => {:object_type => 'resource_pool'}, + # Tests for issue https://github.com/puppetlabs/vmpooler/issues/210 + [ + { + :context => 'multi layer folder hierarchy with a resource pool the same name as a folder', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { 'folder3' => nil, - }, - }, - { - :context => 'single layer folder hierarchy with a child resource pool', - :poolpath => 'parentpool/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => nil, - 'parentpool' => {:object_type => 'resource_pool', :children => { - 'pool' => {:object_type => 'resource_pool'}, - }}, - 'folder3' => nil, - }, - }, - { - :context => 'single layer folder hierarchy with a resource pool within a cluster', - :poolpath => 'cluster/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => nil, - 'cluster' => {:object_type => 'cluster_compute_resource', :children => { - 'pool' => {:object_type => 'resource_pool'}, - }}, - 'folder3' => nil, - }, - }, - # Multi layer Host folder hierarchy - { - :context => 'multi layer folder hierarchy with a resource pool', - :poolpath => 'folder2/folder4/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => { :children => { - 'folder3' => nil, - 'folder4' => { :children => { + 'bad_pool' => {:object_type => 'resource_pool', :name => 'folder4'}, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { 'pool' => {:object_type => 'resource_pool'}, }}, }}, - 'folder5' => nil, - }, + }}, + 'folder5' => nil, }, - { - :context => 'multi layer folder hierarchy with a child resource pool', - :poolpath => 'folder2/folder4/parentpool/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => { :children => { - 'folder3' => nil, - 'folder4' => { :children => { - 'parentpool' => {:object_type => 'resource_pool', :children => { - 'pool' => {:object_type => 'resource_pool'}, - }}, + }, + { + :context => 'multi layer folder hierarchy with a cluster the same name as a folder', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'bad_cluster' => {:object_type => 'cluster_compute_resource', :name => 'folder4'}, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, }}, }}, - 'folder5' => nil, - }, + }}, + 'folder5' => nil, }, - { - :context => 'multi layer folder hierarchy with a resource pool within a cluster', - :poolpath => 'folder2/folder4/cluster/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => { :children => { - 'folder3' => nil, - 'folder4' => { :children => { - 'cluster' => {:object_type => 'cluster_compute_resource', :children => { - 'pool' => {:object_type => 'resource_pool'}, - }}, - }}, - }}, - 'folder5' => nil, - }, - }, - ].each do |testcase| - context testcase[:context] do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } + }, + ].each do |testcase| + context testcase[:context] do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/210') + expect(subject).to receive(:ensure_connected) - subject.find_pool(testcase[:poolpath]) - end - - it 'should return the pool when found' do - result = subject.find_pool(testcase[:poolpath]) - - expect(result).to_not be_nil - expect(result.name).to eq(testcase[:poolname]) - expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true - end - - it 'should return nil if the poolname is not found' do - pending('https://github.com/puppetlabs/vmpooler/issues/209') - expect(subject.find_pool(missing_poolname)).to be_nil - end - end - end - - # Tests for issue https://github.com/puppetlabs/vmpooler/issues/210 - [ - { - :context => 'multi layer folder hierarchy with a resource pool the same name as a folder', - :poolpath => 'folder2/folder4/cluster/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => { :children => { - 'folder3' => nil, - 'bad_pool' => {:object_type => 'resource_pool', :name => 'folder4'}, - 'folder4' => { :children => { - 'cluster' => {:object_type => 'cluster_compute_resource', :children => { - 'pool' => {:object_type => 'resource_pool'}, - }}, - }}, - }}, - 'folder5' => nil, - }, - }, - { - :context => 'multi layer folder hierarchy with a cluster the same name as a folder', - :poolpath => 'folder2/folder4/cluster/pool', - :poolname => 'pool', - :hostfolder_tree => { - 'folder1' => nil, - 'folder2' => { :children => { - 'folder3' => nil, - 'bad_cluster' => {:object_type => 'cluster_compute_resource', :name => 'folder4'}, - 'folder4' => { :children => { - 'cluster' => {:object_type => 'cluster_compute_resource', :children => { - 'pool' => {:object_type => 'resource_pool'}, - }}, - }}, - }}, - 'folder5' => nil, - }, - }, - ].each do |testcase| - context testcase[:context] do - let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/210') - expect(subject).to receive(:ensure_connected) - - subject.find_pool(testcase[:poolpath]) - end - - it 'should return the pool when found' do - pending('https://github.com/puppetlabs/vmpooler/issues/210') - result = subject.find_pool(testcase[:poolpath]) - - expect(result).to_not be_nil - expect(result.name).to eq(testcase[:poolname]) - expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true - end - 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_VirtualMachine() } - - 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) + subject.find_pool(testcase[:poolpath]) 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 + it 'should return the pool when found' do + pending('https://github.com/puppetlabs/vmpooler/issues/210') + result = subject.find_pool(testcase[:poolpath]) - 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 - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - - allow(subject).to receive(:find_vm_light).and_return('vmlight') - allow(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) - end - - it 'should ensure the connection' do - # TODO This seems like overkill as we immediately call vm_light and heavy which - # does the same thing. Also the connection isn't actually used in this method - expect(subject).to receive(:ensure_connected) - - subject.find_vm(vmname) - end - - it 'should call find_vm_light' do - expect(subject).to receive(:find_vm_light).and_return('vmlight') - - expect(subject.find_vm(vmname)).to eq('vmlight') - end - - it 'should not call find_vm_heavy if find_vm_light finds the VM' do - expect(subject).to receive(:find_vm_light).and_return('vmlight') - expect(subject).to receive(:find_vm_heavy).exactly(0).times - - expect(subject.find_vm(vmname)).to eq('vmlight') - end - - it 'should call find_vm_heavy when find_vm_light returns nil' do - expect(subject).to receive(:find_vm_light).and_return(nil) - expect(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) - - expect(subject.find_vm(vmname)).to eq('vmheavy') - end - end - - describe '#find_vm_light' do - let(:missing_vm) { 'missing_vm' } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - - allow(connection.searchIndex).to receive(:FindByDnsName).and_return(nil) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_vm_light(vmname) - end - - it 'should call FindByDnsName with the correct parameters' do - expect(connection.searchIndex).to receive(:FindByDnsName).with({ - :vmSearch => true, - dnsName: vmname, - }) - - subject.find_vm_light(vmname) - end - - it 'should return the VM object when found' do - vm_object = mock_RbVmomi_VIM_VirtualMachine() - expect(connection.searchIndex).to receive(:FindByDnsName).with({ - :vmSearch => true, - dnsName: vmname, - }).and_return(vm_object) - - expect(subject.find_vm_light(vmname)).to be(vm_object) - end - - it 'should return nil if the VM is not found' do - expect(connection.searchIndex).to receive(:FindByDnsName).with({ - :vmSearch => true, - dnsName: missing_vm, - }).and_return(nil) - - expect(subject.find_vm_light(missing_vm)).to be_nil - end - end - - describe '#find_vm_heavy' do - let(:missing_vm) { 'missing_vm' } - # Return an empty result by default - let(:retrieve_result) {{}} - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - - allow(connection.propertyCollector).to receive(:RetrievePropertiesEx).and_return(mock_RbVmomi_VIM_RetrieveResult(retrieve_result)) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected).at_least(:once) - - subject.find_vm_heavy(vmname) - end - - context 'Search result is empty' do - it 'should return empty hash' do - expect(subject.find_vm_heavy(vmname)).to eq({}) - end - end - - context 'Search result contains VMs but no matches' do - let(:retrieve_result) { - { :response => [ - { 'name' => 'no_match001'}, - { 'name' => 'no_match002'}, - { 'name' => 'no_match003'}, - { 'name' => 'no_match004'}, - ] - } - } - - it 'should return empty hash' do - expect(subject.find_vm_heavy(vmname)).to eq({}) - end - end - - context 'Search contains a single match' do - let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} - let(:retrieve_result) { - { :response => [ - { 'name' => 'no_match001'}, - { 'name' => 'no_match002'}, - { 'name' => vmname, :object => vm_object }, - { 'name' => 'no_match003'}, - { 'name' => 'no_match004'}, - ] - } - } - - it 'should return single result' do - result = subject.find_vm_heavy(vmname) - expect(result.keys.count).to eq(1) - end - - it 'should return the matching VM Object' do - result = subject.find_vm_heavy(vmname) - expect(result[vmname]).to be(vm_object) - end - end - - context 'Search contains a two matches' do - let(:vm_object1) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} - let(:vm_object2) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} - let(:retrieve_result) { - { :response => [ - { 'name' => 'no_match001'}, - { 'name' => 'no_match002'}, - { 'name' => vmname, :object => vm_object1 }, - { 'name' => 'no_match003'}, - { 'name' => 'no_match004'}, - { 'name' => vmname, :object => vm_object2 }, - ] - } - } - - it 'should return one result' do - result = subject.find_vm_heavy(vmname) - expect(result.keys.count).to eq(1) - end - - it 'should return the last matching VM Object' do - result = subject.find_vm_heavy(vmname) - expect(result[vmname]).to be(vm_object2) - end - end - end - - describe '#find_vmdks' do - let(:datastorename) { 'datastore' } - let(:connection_options) {{ - :serviceContent => { - :datacenters => [ - { :name => 'MockDC', :datastores => [datastorename] } - ] - } - }} - - let(:collectMultiple_response) { {} } - - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - - # NOTE - This method should not be using `_connection`, instead it should be using `@conection` - mock_ds = subject.find_datastore(datastorename) - allow(mock_ds).to receive(:_connection).and_return(connection) - allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response) - end - - it 'should not use _connction to get the underlying connection object' do - pending('https://github.com/puppetlabs/vmpooler/issues/213') - - mock_ds = subject.find_datastore(datastorename) - expect(mock_ds).to receive(:_connection).exactly(0).times - - begin - # ignore all errors. What's important is that it doesn't call _connection - subject.find_vmdks(vmname,datastorename) - rescue - end - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected).at_least(:once) - - subject.find_vmdks(vmname,datastorename) - end - - context 'Searching all files for all VMs on a Datastore' do - # This is fairly fragile mocking - let(:collectMultiple_response) { { - 'FakeVMObject1' => { 'layoutEx.file' => - [ - mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 101, :name => "[#{datastorename}] mock1/mock1_0.vmdk"}) - ]}, - vmname => { 'layoutEx.file' => - [ - # VMDKs which should match - mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 1, :name => "[#{datastorename}] #{vmname}/#{vmname}_0.vmdk"}), - mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 2, :name => "[#{datastorename}] #{vmname}/#{vmname}_1.vmdk"}), - # VMDKs which should not match - mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 102, :name => "[otherdatastore] #{vmname}/#{vmname}_0.vmdk"}), - mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 103, :name => "[otherdatastore] #{vmname}/#{vmname}.vmdk"}), - mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 104, :name => "[otherdatastore] #{vmname}/#{vmname}_abc.vmdk"}), - ]}, - } } - - it 'should return empty array if no VMDKs match the VM name' do - expect(subject.find_vmdks('missing_vm_name',datastorename)).to eq([]) - end - - it 'should return matching VMDKs for the VM' do - result = subject.find_vmdks(vmname,datastorename) expect(result).to_not be_nil - expect(result.count).to eq(2) - # The keys for each VMDK should be less that 100 as per the mocks - result.each do |fileinfo| - expect(fileinfo.key).to be < 100 - end - end - end - end - - describe '#get_base_vm_container_from' do - let(:local_connection) { mock_RbVmomi_VIM_Connection() } - - before(:each) do - allow(subject).to receive(:ensure_connected) - end - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/212') - expect(subject).to receive(:ensure_connected).with(local_connection,credentials) - - subject.get_base_vm_container_from(local_connection) - end - - it 'should return a recursive view of type VirtualMachine' do - result = subject.get_base_vm_container_from(local_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 RelovateVM_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 '#close' do - context 'no connection has been made' do - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",nil) - end - - it 'should not error' do - pending('https://github.com/puppetlabs/vmpooler/issues/211') - subject.close - end - end - - context 'on an open connection' do - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - it 'should close the underlying connection object' do - expect(connection).to receive(:close) - subject.close + expect(result.name).to eq(testcase[:poolname]) + expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true end 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_VirtualMachine() } + + 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 + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + allow(subject).to receive(:find_vm_light).and_return('vmlight') + allow(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) + end + + it 'should ensure the connection' do + # TODO This seems like overkill as we immediately call vm_light and heavy which + # does the same thing. Also the connection isn't actually used in this method + expect(subject).to receive(:ensure_connected) + + subject.find_vm(vmname) + end + + it 'should call find_vm_light' do + expect(subject).to receive(:find_vm_light).and_return('vmlight') + + expect(subject.find_vm(vmname)).to eq('vmlight') + end + + it 'should not call find_vm_heavy if find_vm_light finds the VM' do + expect(subject).to receive(:find_vm_light).and_return('vmlight') + expect(subject).to receive(:find_vm_heavy).exactly(0).times + + expect(subject.find_vm(vmname)).to eq('vmlight') + end + + it 'should call find_vm_heavy when find_vm_light returns nil' do + expect(subject).to receive(:find_vm_light).and_return(nil) + expect(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) + + expect(subject.find_vm(vmname)).to eq('vmheavy') + end + end + + describe '#find_vm_light' do + let(:missing_vm) { 'missing_vm' } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + allow(connection.searchIndex).to receive(:FindByDnsName).and_return(nil) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected) + + subject.find_vm_light(vmname) + end + + it 'should call FindByDnsName with the correct parameters' do + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: vmname, + }) + + subject.find_vm_light(vmname) + end + + it 'should return the VM object when found' do + vm_object = mock_RbVmomi_VIM_VirtualMachine() + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: vmname, + }).and_return(vm_object) + + expect(subject.find_vm_light(vmname)).to be(vm_object) + end + + it 'should return nil if the VM is not found' do + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: missing_vm, + }).and_return(nil) + + expect(subject.find_vm_light(missing_vm)).to be_nil + end + end + + describe '#find_vm_heavy' do + let(:missing_vm) { 'missing_vm' } + # Return an empty result by default + let(:retrieve_result) {{}} + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + allow(connection.propertyCollector).to receive(:RetrievePropertiesEx).and_return(mock_RbVmomi_VIM_RetrieveResult(retrieve_result)) + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected).at_least(:once) + + subject.find_vm_heavy(vmname) + end + + context 'Search result is empty' do + it 'should return empty hash' do + expect(subject.find_vm_heavy(vmname)).to eq({}) + end + end + + context 'Search result contains VMs but no matches' do + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + ] + } + } + + it 'should return empty hash' do + expect(subject.find_vm_heavy(vmname)).to eq({}) + end + end + + context 'Search contains a single match' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => vmname, :object => vm_object }, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + ] + } + } + + it 'should return single result' do + result = subject.find_vm_heavy(vmname) + expect(result.keys.count).to eq(1) + end + + it 'should return the matching VM Object' do + result = subject.find_vm_heavy(vmname) + expect(result[vmname]).to be(vm_object) + end + end + + context 'Search contains a two matches' do + let(:vm_object1) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:vm_object2) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => vmname, :object => vm_object1 }, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + { 'name' => vmname, :object => vm_object2 }, + ] + } + } + + it 'should return one result' do + result = subject.find_vm_heavy(vmname) + expect(result.keys.count).to eq(1) + end + + it 'should return the last matching VM Object' do + result = subject.find_vm_heavy(vmname) + expect(result[vmname]).to be(vm_object2) + end + end + end + + describe '#find_vmdks' do + let(:datastorename) { 'datastore' } + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [datastorename] } + ] + } + }} + + let(:collectMultiple_response) { {} } + + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + + # NOTE - This method should not be using `_connection`, instead it should be using `@conection` + mock_ds = subject.find_datastore(datastorename) + allow(mock_ds).to receive(:_connection).and_return(connection) + allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response) + end + + it 'should not use _connction to get the underlying connection object' do + pending('https://github.com/puppetlabs/vmpooler/issues/213') + + mock_ds = subject.find_datastore(datastorename) + expect(mock_ds).to receive(:_connection).exactly(0).times + + begin + # ignore all errors. What's important is that it doesn't call _connection + subject.find_vmdks(vmname,datastorename) + rescue + end + end + + it 'should ensure the connection' do + expect(subject).to receive(:ensure_connected).at_least(:once) + + subject.find_vmdks(vmname,datastorename) + end + + context 'Searching all files for all VMs on a Datastore' do + # This is fairly fragile mocking + let(:collectMultiple_response) { { + 'FakeVMObject1' => { 'layoutEx.file' => + [ + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 101, :name => "[#{datastorename}] mock1/mock1_0.vmdk"}) + ]}, + vmname => { 'layoutEx.file' => + [ + # VMDKs which should match + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 1, :name => "[#{datastorename}] #{vmname}/#{vmname}_0.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 2, :name => "[#{datastorename}] #{vmname}/#{vmname}_1.vmdk"}), + # VMDKs which should not match + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 102, :name => "[otherdatastore] #{vmname}/#{vmname}_0.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 103, :name => "[otherdatastore] #{vmname}/#{vmname}.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 104, :name => "[otherdatastore] #{vmname}/#{vmname}_abc.vmdk"}), + ]}, + } } + + it 'should return empty array if no VMDKs match the VM name' do + expect(subject.find_vmdks('missing_vm_name',datastorename)).to eq([]) + end + + it 'should return matching VMDKs for the VM' do + result = subject.find_vmdks(vmname,datastorename) + expect(result).to_not be_nil + expect(result.count).to eq(2) + # The keys for each VMDK should be less that 100 as per the mocks + result.each do |fileinfo| + expect(fileinfo.key).to be < 100 + end + end + end + end + + describe '#get_base_vm_container_from' do + let(:local_connection) { mock_RbVmomi_VIM_Connection() } + + before(:each) do + allow(subject).to receive(:ensure_connected) + end + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/212') + expect(subject).to receive(:ensure_connected).with(local_connection,credentials) + + subject.get_base_vm_container_from(local_connection) + end + + it 'should return a recursive view of type VirtualMachine' do + result = subject.get_base_vm_container_from(local_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 RelovateVM_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 '#close' do + context 'no connection has been made' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + end + + it 'should not error' do + pending('https://github.com/puppetlabs/vmpooler/issues/211') + subject.close + end + end + + context 'on an open connection' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should close the underlying connection object' do + expect(connection).to receive(:close) + subject.close + end + end + end end From 901ddde7c3fb5dd22aaf876816caf2d06e66bdb1 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 Mar 2017 10:23:43 -0700 Subject: [PATCH 3/6] (POOLER-52) Prepare the vSphere Provider for a connection pooler Previously, all calls to the vSphere API assumed an instance variable called `@connection`. This commit prepares the provider for a connection pooler: - Removes all references to `@connection` where needed and funnels all calls to get a vSphere connection through the newly renamed method `get_connection`. For the moment, this still uses `@connection` behind the scenes but will make it easier in the future to migrate to a connection pooler - Removes all references and tests for the ensure_connected method as it's no longer required - All methods that explicitly need a connection object will have this as part of the method parameters - The connect_to_vsphere method has been changed so that instead of setting the instance level `@connection` object it just returns the newly created connection. This can then be easily consumed by a connection pooler later. --- lib/vmpooler/providers/vsphere.rb | 94 +++---- spec/unit/providers/vsphere_spec.rb | 387 ++++++---------------------- 2 files changed, 115 insertions(+), 366 deletions(-) diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb index 8bf7505..0b3e49a 100644 --- a/lib/vmpooler/providers/vsphere.rb +++ b/lib/vmpooler/providers/vsphere.rb @@ -23,10 +23,12 @@ module Vmpooler DISK_TYPE = 'thin' DISK_MODE = 'persistent' - def ensure_connected(connection, credentials) - connection.serviceInstance.CurrentTime + def get_connection + @connection.serviceInstance.CurrentTime rescue - connect_to_vsphere @credentials + @connection = connect_to_vsphere @credentials + ensure + return @connection end def connect_to_vsphere(credentials) @@ -34,11 +36,12 @@ module Vmpooler retry_factor = @conf['retry_factor'] || 10 try = 1 begin - @connection = RbVmomi::VIM.connect host: credentials['server'], + connection = RbVmomi::VIM.connect host: credentials['server'], user: credentials['username'], password: credentials['password'], insecure: credentials['insecure'] || true @metrics.increment("connect.open") + return connection rescue => err try += 1 @metrics.increment("connect.fail") @@ -48,13 +51,11 @@ module Vmpooler end end - def add_disk(vm, size, datastore) - ensure_connected @connection, @credentials - + def add_disk(vm, size, datastore, connection) return false unless size.to_i > 0 - vmdk_datastore = find_datastore(datastore) - vmdk_file_name = "#{vm['name']}/#{vm['name']}_#{find_vmdks(vm['name'], datastore).length + 1}.vmdk" + vmdk_datastore = find_datastore(datastore, connection) + vmdk_file_name = "#{vm['name']}/#{vm['name']}_#{find_vmdks(vm['name'], datastore, connection).length + 1}.vmdk" controller = find_disk_controller(vm) @@ -87,8 +88,8 @@ module Vmpooler deviceChange: [device_config_spec] ) - @connection.serviceContent.virtualDiskManager.CreateVirtualDisk_Task( - datacenter: @connection.serviceInstance.find_datacenter, + connection.serviceContent.virtualDiskManager.CreateVirtualDisk_Task( + datacenter: connection.serviceInstance.find_datacenter, name: "[#{vmdk_datastore.name}] #{vmdk_file_name}", spec: vmdk_spec ).wait_for_completion @@ -98,16 +99,12 @@ module Vmpooler true end - def find_datastore(datastorename) - ensure_connected @connection, @credentials - - datacenter = @connection.serviceInstance.find_datacenter + def find_datastore(datastorename, connection) + datacenter = connection.serviceInstance.find_datacenter datacenter.find_datastore(datastorename) end def find_device(vm, deviceName) - ensure_connected @connection, @credentials - vm.config.hardware.device.each do |device| return device if device.deviceInfo.label == deviceName end @@ -116,8 +113,6 @@ module Vmpooler end def find_disk_controller(vm) - ensure_connected @connection, @credentials - devices = find_disk_devices(vm) devices.keys.sort.each do |device| @@ -130,8 +125,6 @@ module Vmpooler end def find_disk_devices(vm) - ensure_connected @connection, @credentials - devices = {} vm.config.hardware.device.each do |device| @@ -158,8 +151,6 @@ module Vmpooler end def find_disk_unit_number(vm, controller) - ensure_connected @connection, @credentials - used_unit_numbers = [] available_unit_numbers = [] @@ -182,10 +173,8 @@ module Vmpooler available_unit_numbers.sort[0] end - def find_folder(foldername) - ensure_connected @connection, @credentials - - datacenter = @connection.serviceInstance.find_datacenter + def find_folder(foldername, connection) + datacenter = connection.serviceInstance.find_datacenter base = datacenter.vmFolder folders = foldername.split('/') folders.each do |folder| @@ -205,7 +194,7 @@ module Vmpooler # +limit+:: Hard limit for CPU or memory utilization beyond which a host is excluded for deployments def get_host_utilization(host, model=nil, limit=90) if model - return nil unless host_has_cpu_model? host, model + return nil unless host_has_cpu_model?(host, model) end return nil if host.runtime.inMaintenanceMode return nil unless host.overallStatus == 'green' @@ -242,17 +231,15 @@ module Vmpooler (memory_usage.to_f / memory_size.to_f) * 100 end - def find_least_used_host(cluster) - ensure_connected @connection, @credentials - - cluster_object = find_cluster(cluster) + def find_least_used_host(cluster, connection) + cluster_object = find_cluster(cluster, connection) target_hosts = get_cluster_host_utilization(cluster_object) least_used_host = target_hosts.sort[0][1] least_used_host end - def find_cluster(cluster) - datacenter = @connection.serviceInstance.find_datacenter + def find_cluster(cluster, connection) + datacenter = connection.serviceInstance.find_datacenter datacenter.hostFolder.children.find { |cluster_object| cluster_object.name == cluster } end @@ -266,8 +253,6 @@ module Vmpooler end def find_least_used_vpshere_compatible_host(vm) - ensure_connected @connection, @credentials - source_host = vm.summary.runtime.host model = get_host_cpu_arch_version(source_host) cluster = source_host.parent @@ -280,10 +265,8 @@ module Vmpooler [target_host, target_host.name] end - def find_pool(poolname) - ensure_connected @connection, @credentials - - datacenter = @connection.serviceInstance.find_datacenter + def find_pool(poolname, connection) + datacenter = connection.serviceInstance.find_datacenter base = datacenter.hostFolder pools = poolname.split('/') pools.each do |pool| @@ -309,23 +292,18 @@ module Vmpooler end end - def find_vm(vmname) - ensure_connected @connection, @credentials - find_vm_light(vmname) || find_vm_heavy(vmname)[vmname] + def find_vm(vmname, connection) + find_vm_light(vmname, connection) || find_vm_heavy(vmname, connection)[vmname] end - def find_vm_light(vmname) - ensure_connected @connection, @credentials - - @connection.searchIndex.FindByDnsName(vmSearch: true, dnsName: vmname) + def find_vm_light(vmname, connection) + connection.searchIndex.FindByDnsName(vmSearch: true, dnsName: vmname) end - def find_vm_heavy(vmname) - ensure_connected @connection, @credentials - + def find_vm_heavy(vmname, connection) vmname = vmname.is_a?(Array) ? vmname : [vmname] - containerView = get_base_vm_container_from @connection - propertyCollector = @connection.propertyCollector + containerView = get_base_vm_container_from(connection) + propertyCollector = connection.propertyCollector objectSet = [{ obj: containerView, @@ -370,12 +348,10 @@ module Vmpooler vms end - def find_vmdks(vmname, datastore) - ensure_connected @connection, @credentials - + def find_vmdks(vmname, datastore, connection) disks = [] - vmdk_datastore = find_datastore(datastore) + vmdk_datastore = find_datastore(datastore, connection) vm_files = vmdk_datastore._connection.serviceContent.propertyCollector.collectMultiple vmdk_datastore.vm, 'layoutEx.file' vm_files.keys.each do |f| @@ -390,8 +366,6 @@ module Vmpooler end def get_base_vm_container_from(connection) - ensure_connected @connection, @credentials - viewManager = connection.serviceContent.viewManager viewManager.CreateContainerView( container: connection.serviceContent.rootFolder, @@ -422,10 +396,6 @@ module Vmpooler def close @connection.close end - - - - end end end diff --git a/spec/unit/providers/vsphere_spec.rb b/spec/unit/providers/vsphere_spec.rb index 717b95e..f64d1d0 100644 --- a/spec/unit/providers/vsphere_spec.rb +++ b/spec/unit/providers/vsphere_spec.rb @@ -116,12 +116,23 @@ EOT let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } let(:vmname) { 'vm1' } - describe '#ensure_connected' do - context 'when connection has ok' do + describe '#get_connection' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + context 'when connection is ok' do it 'should not attempt to reconnect' do expect(subject).to receive(:connect_to_vsphere).exactly(0).times - subject.ensure_connected(connection,credentials) + subject.get_connection() + end + + it 'should return a connection' do + result = subject.get_connection() + + expect(result).to be(connection) end end @@ -135,23 +146,29 @@ EOT expect(metrics).to receive(:increment).with('connect.open').exactly(0).times allow(subject).to receive(:connect_to_vsphere) - subject.ensure_connected(connection,credentials) + subject.get_connection() end it 'should call connect_to_vsphere to reconnect' do allow(metrics).to receive(:increment) - allow(subject).to receive(:connect_to_vsphere).with(credentials) + expect(subject).to receive(:connect_to_vsphere).with(credentials) - subject.ensure_connected(connection,credentials) + subject.get_connection() + end + + it 'should return a new connection' do + new_connection = mock_RbVmomi_VIM_Connection(connection_options) + expect(subject).to receive(:connect_to_vsphere).with(credentials).and_return(new_connection) + + result = subject.get_connection() + + expect(result).to be(new_connection) end end end describe '#connect_to_vsphere' do before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",nil) - allow(RbVmomi::VIM).to receive(:connect).and_return(connection) end @@ -192,11 +209,10 @@ EOT subject.connect_to_vsphere(credentials) end - it 'should set the instance level connection object' do - # NOTE - Using instance_variable_get is a code smell of code that is not testable - expect(subject.instance_variable_get("@connection")).to be_nil - subject.connect_to_vsphere(credentials) - expect(subject.instance_variable_get("@connection")).to be(connection) + it 'should return the connection object' do + result = subject.connect_to_vsphere(credentials) + + expect(result).to be(connection) end it 'should increment the connect.open counter' do @@ -207,9 +223,6 @@ EOT context 'connection is initially unsuccessful' do before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",nil) - # 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 @@ -217,11 +230,10 @@ EOT allow(subject).to receive(:sleep) end - it 'should set the instance level connection object' do - # NOTE - Using instance_variable_get is a code smell of code that is not testable - expect(subject.instance_variable_get("@connection")).to be_nil - subject.connect_to_vsphere(credentials) - expect(subject.instance_variable_get("@connection")).to be(connection) + it 'should return the connection object' do + result = subject.connect_to_vsphere(credentials) + + expect(result).to be(connection) end it 'should increment the connect.fail and then connect.open counter' do @@ -233,9 +245,6 @@ EOT context 'connection is always unsuccessful' do before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",nil) - allow(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError') allow(subject).to receive(:sleep) end @@ -320,12 +329,9 @@ EOT let(:reconfig_vm_task) { mock_RbVmomi_VIM_Task() } before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - # NOTE - This method should not be using `_connection`, instead it should be using `@conection` # This should not be required once https://github.com/puppetlabs/vmpooler/issues/213 is resolved - mock_ds = subject.find_datastore(datastorename) + mock_ds = subject.find_datastore(datastorename,connection) allow(mock_ds).to receive(:_connection).and_return(connection) unless mock_ds.nil? # Mocking for find_vmdks @@ -340,15 +346,9 @@ EOT allow(reconfig_vm_task).to receive(:wait_for_completion).and_return(true) end - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected).at_least(:once) - - subject.add_disk(vm_object,disk_size,datastorename) - end - context 'Succesfully addding disk' do it 'should return true' do - expect(subject.add_disk(vm_object,disk_size,datastorename)).to be true + expect(subject.add_disk(vm_object,disk_size,datastorename,connection)).to be true end it 'should request a disk of appropriate size' do @@ -357,13 +357,13 @@ EOT .and_return(create_virtual_disk_task) - subject.add_disk(vm_object,disk_size,datastorename) + subject.add_disk(vm_object,disk_size,datastorename,connection) end end context 'Requested disk size is 0' do it 'should raise an error' do - expect(subject.add_disk(vm_object,0,datastorename)).to be false + expect(subject.add_disk(vm_object,0,datastorename,connection)).to be false end end @@ -377,7 +377,7 @@ EOT }} it 'should return false' do - expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) + expect{ subject.add_disk(vm_object,disk_size,datastorename,connection) }.to raise_error(NoMethodError) end end @@ -391,7 +391,7 @@ EOT } it 'should raise an error' do - expect{ subject.add_disk(vm_object,disk_size,datastorename) }.to raise_error(NoMethodError) + expect{ subject.add_disk(vm_object,disk_size,datastorename,connection) }.to raise_error(NoMethodError) end end end @@ -400,11 +400,6 @@ EOT let(:datastorename) { 'datastore' } let(:datastore_list) { [] } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - context 'No datastores in the datacenter' do let(:connection_options) {{ :serviceContent => { @@ -414,14 +409,8 @@ EOT } }} - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_datastore(datastorename) - end - it 'should return nil if the datastore is not found' do - result = subject.find_datastore(datastorename) + result = subject.find_datastore(datastorename,connection) expect(result).to be_nil end end @@ -435,19 +424,13 @@ EOT } }} - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_datastore(datastorename) - end - it 'should return nil if the datastore is not found' do - result = subject.find_datastore('missing_datastore') + result = subject.find_datastore('missing_datastore',connection) expect(result).to be_nil end it 'should find the datastore in the datacenter' do - result = subject.find_datastore(datastorename) + result = subject.find_datastore(datastorename,connection) expect(result).to_not be_nil expect(result.is_a?(RbVmomi::VIM::Datastore)).to be true @@ -466,17 +449,6 @@ EOT mock_vm } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_device(vm_object,devicename) - end - it 'should return a device if the device name matches' do result = subject.find_device(vm_object,devicename) @@ -497,18 +469,6 @@ EOT mock_vm } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - it 'should ensure the connection' do - # TODO There's no reason for this as the connection is not used in this method - expect(subject).to receive(:ensure_connected).at_least(:once) - - result = subject.find_disk_controller(vm_object) - end - it 'should return nil when there are no devices' do result = subject.find_disk_controller(vm_object) @@ -569,18 +529,6 @@ EOT mock_vm } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - it 'should ensure the connection' do - # TODO There's no reason for this as the connection is not used in this method - expect(subject).to receive(:ensure_connected) - - result = subject.find_disk_devices(vm_object) - end - it 'should return empty hash when there are no devices' do result = subject.find_disk_devices(vm_object) @@ -652,18 +600,6 @@ EOT } let(:controller) { mock_RbVmomi_VIM_VirtualSCSIController() } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - - it 'should ensure the connection' do - # TODO There's no reason for this as the connection is not used in this method - expect(subject).to receive(:ensure_connected).at_least(:once) - - result = subject.find_disk_unit_number(vm_object,controller) - end - it 'should return 0 when there are no devices' do result = subject.find_disk_unit_number(vm_object,controller) @@ -721,22 +657,14 @@ EOT let(:missing_foldername) { 'missing_folder'} before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) end context 'with no folder hierarchy' do let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_folder(foldername) - end - it 'should return nil if the folder is not found' do - expect(subject.find_folder(missing_foldername)).to be_nil + expect(subject.find_folder(missing_foldername,connection)).to be_nil end end @@ -750,20 +678,14 @@ EOT } }) } - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_folder(foldername) - end - it 'should return the folder when found' do - result = subject.find_folder(foldername) + result = subject.find_folder(foldername,connection) expect(result).to_not be_nil expect(result.name).to eq(foldername) end it 'should return nil if the folder is not found' do - expect(subject.find_folder(missing_foldername)).to be_nil + expect(subject.find_folder(missing_foldername,connection)).to be_nil end end @@ -781,7 +703,7 @@ EOT it 'should not return a VM' do pending('https://github.com/puppetlabs/vmpooler/issues/204') - result = subject.find_folder(foldername) + result = subject.find_folder(foldername,connection) expect(result).to_not be_nil expect(result.name).to eq(foldername) expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false @@ -808,20 +730,14 @@ EOT } }) } - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_folder(foldername) - end - it 'should return the folder when found' do - result = subject.find_folder(foldername) + result = subject.find_folder(foldername,connection) expect(result).to_not be_nil expect(result.name).to eq(end_folder_name) end it 'should return nil if the folder is not found' do - expect(subject.find_folder(missing_foldername)).to be_nil + expect(subject.find_folder(missing_foldername,connection)).to be_nil end end @@ -850,7 +766,7 @@ EOT it 'should not return a VM' do pending('https://github.com/puppetlabs/vmpooler/issues/204') - result = subject.find_folder(foldername) + result = subject.find_folder(foldername,connection) expect(result).to_not be_nil expect(result.name).to eq(foldername) expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false @@ -1090,9 +1006,6 @@ EOT let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - # This mocking is a little fragile but hard to do without a real vCenter instance allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) datacenter_object.hostFolder.childEntity = [cluster_object] @@ -1107,13 +1020,7 @@ EOT let(:expected_host) { cluster_object.host[0] } it 'should raise an error' do - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + expect{subject.find_least_used_host(missing_cluster_name,connection)}.to raise_error(NoMethodError,/undefined method/) end end @@ -1126,16 +1033,10 @@ EOT let(:expected_host) { cluster_object.host[0] } it 'should return the standalone host' do - result = subject.find_least_used_host(cluster_name) + result = subject.find_least_used_host(cluster_name,connection) expect(result).to be(expected_host) end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - end end context 'standalone host outside the limits' do @@ -1148,13 +1049,7 @@ EOT let(:expected_host) { cluster_object.host[0] } it 'should raise an error' do - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + expect{subject.find_least_used_host(missing_cluster_name,connection)}.to raise_error(NoMethodError,/undefined method/) end end @@ -1169,16 +1064,10 @@ EOT let(:expected_host) { cluster_object.host[1] } it 'should return the standalone host' do - result = subject.find_least_used_host(cluster_name) + result = subject.find_least_used_host(cluster_name,connection) expect(result).to be(expected_host) end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - end end context 'cluster of 3 hosts all outside of the limits' do @@ -1192,13 +1081,7 @@ EOT let(:expected_host) { cluster_object.host[1] } it 'should raise an error' do - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError,/undefined method/) - end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - expect{subject.find_least_used_host(missing_cluster_name)}.to raise_error(NoMethodError) + expect{subject.find_least_used_host(missing_cluster_name,connection)}.to raise_error(NoMethodError,/undefined method/) end end @@ -1215,16 +1098,10 @@ EOT let(:expected_host) { cluster_object.host[1] } it 'should return the standalone host' do - result = subject.find_least_used_host(cluster_name) + result = subject.find_least_used_host(cluster_name,connection) expect(result).to be(expected_host) end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - end end context 'cluster of 3 hosts all outside of the limits' do @@ -1239,16 +1116,9 @@ EOT it 'should return a host' do pending('https://github.com/puppetlabs/vmpooler/issues/206') - result = subject.find_least_used_host(missing_cluster_name) + result = subject.find_least_used_host(missing_cluster_name,connection) expect(result).to_not be_nil end - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/206') - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_host(cluster_name) - end end end @@ -1257,8 +1127,6 @@ EOT let(:missing_cluster) {'missing_cluster'} before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) end @@ -1269,7 +1137,7 @@ EOT end it 'should return nil if the cluster is not found' do - expect(subject.find_cluster(missing_cluster)).to be_nil + expect(subject.find_cluster(missing_cluster,connection)).to be_nil end end @@ -1284,14 +1152,14 @@ EOT }) } it 'should return the cluster when found' do - result = subject.find_cluster(cluster) + result = subject.find_cluster(cluster,connection) expect(result).to_not be_nil expect(result.name).to eq(cluster) end it 'should return nil if the cluster is not found' do - expect(subject.find_cluster(missing_cluster)).to be_nil + expect(subject.find_cluster(missing_cluster,connection)).to be_nil end end @@ -1310,14 +1178,14 @@ EOT it 'should return the cluster when found' do pending('https://github.com/puppetlabs/vmpooler/issues/205') - result = subject.find_cluster(cluster) + result = subject.find_cluster(cluster,connection) expect(result).to_not be_nil expect(result.name).to eq(cluster) end it 'should return nil if the cluster is not found' do - expect(subject.find_cluster(missing_cluster)).to be_nil + expect(subject.find_cluster(missing_cluster,connection)).to be_nil end end end @@ -1393,11 +1261,6 @@ EOT describe '#find_least_used_vpshere_compatible_host' do let(:vm) { mock_RbVmomi_VIM_VirtualMachine() } - before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - end - context 'standalone host within limits' do let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [{}]}) } let(:standalone_host) { cluster_object.host[0] } @@ -1414,12 +1277,6 @@ EOT expect(result[0]).to be(standalone_host) expect(result[1]).to eq(standalone_host.name) end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_vpshere_compatible_host(vm) - end end context 'standalone host outside of limits' do @@ -1458,12 +1315,6 @@ EOT expect(result[0]).to be(expected_host) expect(result[1]).to eq(expected_host.name) end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_vpshere_compatible_host(vm) - end end context 'cluster of 3 hosts all outside of the limits' do @@ -1506,12 +1357,6 @@ EOT expect(result[0]).to be(expected_host) expect(result[1]).to eq(expected_host.name) end - - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_vpshere_compatible_host(vm) - end end context 'cluster of 3 hosts all with the same utilisation' do @@ -1533,13 +1378,6 @@ EOT expect(result).to_not be_nil end - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/206 is fixed') - expect(subject).to receive(:ensure_connected) - - result = subject.find_least_used_vpshere_compatible_host(vm) - end end end @@ -1548,8 +1386,6 @@ EOT let(:missing_poolname) { 'missing_pool'} before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) end @@ -1560,12 +1396,12 @@ EOT pending('https://github.com/puppetlabs/vmpooler/issues/209') expect(subject).to receive(:ensure_connected) - subject.find_pool(poolname) + subject.find_pool(poolname,connection) end it 'should return nil if the pool is not found' do pending('https://github.com/puppetlabs/vmpooler/issues/209') - expect(subject.find_pool(missing_poolname)).to be_nil + expect(subject.find_pool(missing_poolname,connection)).to be_nil end end @@ -1662,14 +1498,8 @@ EOT context testcase[:context] do let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_pool(testcase[:poolpath]) - end - it 'should return the pool when found' do - result = subject.find_pool(testcase[:poolpath]) + result = subject.find_pool(testcase[:poolpath],connection) expect(result).to_not be_nil expect(result.name).to eq(testcase[:poolname]) @@ -1678,7 +1508,7 @@ EOT it 'should return nil if the poolname is not found' do pending('https://github.com/puppetlabs/vmpooler/issues/209') - expect(subject.find_pool(missing_poolname)).to be_nil + expect(subject.find_pool(missing_poolname,connection)).to be_nil end end end @@ -1808,39 +1638,28 @@ EOT describe '#find_vm' do before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - allow(subject).to receive(:find_vm_light).and_return('vmlight') allow(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) end - it 'should ensure the connection' do - # TODO This seems like overkill as we immediately call vm_light and heavy which - # does the same thing. Also the connection isn't actually used in this method - expect(subject).to receive(:ensure_connected) - - subject.find_vm(vmname) - end - it 'should call find_vm_light' do expect(subject).to receive(:find_vm_light).and_return('vmlight') - expect(subject.find_vm(vmname)).to eq('vmlight') + expect(subject.find_vm(vmname,connection)).to eq('vmlight') end it 'should not call find_vm_heavy if find_vm_light finds the VM' do expect(subject).to receive(:find_vm_light).and_return('vmlight') expect(subject).to receive(:find_vm_heavy).exactly(0).times - expect(subject.find_vm(vmname)).to eq('vmlight') + expect(subject.find_vm(vmname,connection)).to eq('vmlight') end it 'should call find_vm_heavy when find_vm_light returns nil' do expect(subject).to receive(:find_vm_light).and_return(nil) expect(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) - expect(subject.find_vm(vmname)).to eq('vmheavy') + expect(subject.find_vm(vmname,connection)).to eq('vmheavy') end end @@ -1848,25 +1667,16 @@ EOT let(:missing_vm) { 'missing_vm' } before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - allow(connection.searchIndex).to receive(:FindByDnsName).and_return(nil) end - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected) - - subject.find_vm_light(vmname) - end - it 'should call FindByDnsName with the correct parameters' do expect(connection.searchIndex).to receive(:FindByDnsName).with({ :vmSearch => true, dnsName: vmname, }) - subject.find_vm_light(vmname) + subject.find_vm_light(vmname,connection) end it 'should return the VM object when found' do @@ -1876,7 +1686,7 @@ EOT dnsName: vmname, }).and_return(vm_object) - expect(subject.find_vm_light(vmname)).to be(vm_object) + expect(subject.find_vm_light(vmname,connection)).to be(vm_object) end it 'should return nil if the VM is not found' do @@ -1885,7 +1695,7 @@ EOT dnsName: missing_vm, }).and_return(nil) - expect(subject.find_vm_light(missing_vm)).to be_nil + expect(subject.find_vm_light(missing_vm,connection)).to be_nil end end @@ -1895,21 +1705,12 @@ EOT let(:retrieve_result) {{}} before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - allow(connection.propertyCollector).to receive(:RetrievePropertiesEx).and_return(mock_RbVmomi_VIM_RetrieveResult(retrieve_result)) end - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected).at_least(:once) - - subject.find_vm_heavy(vmname) - end - context 'Search result is empty' do it 'should return empty hash' do - expect(subject.find_vm_heavy(vmname)).to eq({}) + expect(subject.find_vm_heavy(vmname,connection)).to eq({}) end end @@ -1925,7 +1726,7 @@ EOT } it 'should return empty hash' do - expect(subject.find_vm_heavy(vmname)).to eq({}) + expect(subject.find_vm_heavy(vmname,connection)).to eq({}) end end @@ -1943,12 +1744,12 @@ EOT } it 'should return single result' do - result = subject.find_vm_heavy(vmname) + result = subject.find_vm_heavy(vmname,connection) expect(result.keys.count).to eq(1) end it 'should return the matching VM Object' do - result = subject.find_vm_heavy(vmname) + result = subject.find_vm_heavy(vmname,connection) expect(result[vmname]).to be(vm_object) end end @@ -1969,12 +1770,12 @@ EOT } it 'should return one result' do - result = subject.find_vm_heavy(vmname) + result = subject.find_vm_heavy(vmname,connection) expect(result.keys.count).to eq(1) end it 'should return the last matching VM Object' do - result = subject.find_vm_heavy(vmname) + result = subject.find_vm_heavy(vmname,connection) expect(result[vmname]).to be(vm_object2) end end @@ -1993,11 +1794,8 @@ EOT let(:collectMultiple_response) { {} } before(:each) do - # NOTE - Using instance_variable_set is a code smell of code that is not testable - subject.instance_variable_set("@connection",connection) - # NOTE - This method should not be using `_connection`, instead it should be using `@conection` - mock_ds = subject.find_datastore(datastorename) + mock_ds = subject.find_datastore(datastorename,connection) allow(mock_ds).to receive(:_connection).and_return(connection) allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response) end @@ -2010,17 +1808,11 @@ EOT begin # ignore all errors. What's important is that it doesn't call _connection - subject.find_vmdks(vmname,datastorename) + subject.find_vmdks(vmname,datastorename,connection) rescue end end - it 'should ensure the connection' do - expect(subject).to receive(:ensure_connected).at_least(:once) - - subject.find_vmdks(vmname,datastorename) - end - context 'Searching all files for all VMs on a Datastore' do # This is fairly fragile mocking let(:collectMultiple_response) { { @@ -2041,11 +1833,11 @@ EOT } } it 'should return empty array if no VMDKs match the VM name' do - expect(subject.find_vmdks('missing_vm_name',datastorename)).to eq([]) + expect(subject.find_vmdks('missing_vm_name',datastorename,connection)).to eq([]) end it 'should return matching VMDKs for the VM' do - result = subject.find_vmdks(vmname,datastorename) + result = subject.find_vmdks(vmname,datastorename,connection) expect(result).to_not be_nil expect(result.count).to eq(2) # The keys for each VMDK should be less that 100 as per the mocks @@ -2057,21 +1849,8 @@ EOT end describe '#get_base_vm_container_from' do - let(:local_connection) { mock_RbVmomi_VIM_Connection() } - - before(:each) do - allow(subject).to receive(:ensure_connected) - end - - it 'should ensure the connection' do - pending('https://github.com/puppetlabs/vmpooler/issues/212') - expect(subject).to receive(:ensure_connected).with(local_connection,credentials) - - subject.get_base_vm_container_from(local_connection) - end - it 'should return a recursive view of type VirtualMachine' do - result = subject.get_base_vm_container_from(local_connection) + result = subject.get_base_vm_container_from(connection) expect(result.recursive).to be true expect(result.type).to eq(['VirtualMachine']) From 821dcf45c277852c6d5623e00bed48e5a13d9998 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Tue, 28 Mar 2017 19:12:17 -0700 Subject: [PATCH 4/6] (POOLER-70) Update the provider base class Previously it was expected that the timeout setting should be passed when determining whether a VM was ready. However this should be in the pool configuration and is not required to be a method parameter. Previously many of the methods did not have a pool name passed as a parameter. While this may be ok for the vSphere provider, it may not for other providers. This commit changes the base provider to consistently use (pool,vm,other..) as method parameters. This commit also updates the base spec tests as well. Additionally: - Updated documentation around expected error states - Updated documentation to be more consistent in format - Added snapshot and disk manager functions and unit tests - Update the initialization method to take in a more formal defintion with required global objects for metrics, logging and configuration - Added helper functions - logger : Allows providers to log information as per Pool Manager - metrics : Allows providers to submit metrics as per Pool Manager - provider_options : Allows providers to access initialization options for a provider - pool_config : Get the configuration for a specific pool - provider_config : Get the configuration of this specific provider - global_config: Get the VMPooler global configuration --- lib/vmpooler/providers/base.rb | 186 +++++++++++++++++++++++-------- spec/unit/providers/base_spec.rb | 142 ++++++++++++++++++++--- 2 files changed, 266 insertions(+), 62 deletions(-) diff --git a/lib/vmpooler/providers/base.rb b/lib/vmpooler/providers/base.rb index d5515c4..579124b 100644 --- a/lib/vmpooler/providers/base.rb +++ b/lib/vmpooler/providers/base.rb @@ -4,101 +4,191 @@ module Vmpooler class Base # These defs must be overidden in child classes - def initialize(options) + # Helper Methods + # Global Logger object + attr_reader :logger + # Global Metrics object + attr_reader :metrics + # Provider options passed in during initialization + attr_reader :provider_options + + def initialize(config, logger, metrics, name, options) + @config = config + @logger = logger + @metrics = metrics + @provider_name = name + @provider_options = options end - # returns - # [String] Name of the provider service - def name - 'base' - end + # Helper Methods # inputs - # pool : hashtable from config file + # [String] pool_name : Name of the pool to get the configuration # returns - # hashtable - # name : name of the device <---- TODO is this all? - def vms_in_pool(_pool) + # [Hashtable] : The pools configuration from the config file. Returns nil if the pool does not exist + def pool_config(pool_name) + # Get the configuration of a specific pool + @config[:pools].each do |pool| + return pool if pool['name'] == pool_name + end + + nil + end + + # returns + # [Hashtable] : This provider's configuration from the config file. Returns nil if the provider does not exist + def provider_config + @config[:providers].each do |provider| + # Convert the symbol from the config into a string for comparison + return provider[1] if provider[0].to_s == @provider_name + end + + nil + end + + # returns + # [Hashtable] : The entire VMPooler configuration + def global_config + # This entire VM Pooler config + @config + end + + # returns + # [String] : Name of the provider service + def name + @provider_name + end + + # Pool Manager Methods + + # inputs + # [String] pool_name : Name of the pool + # returns + # Array[Hashtable] + # Hash contains: + # 'name' => [String] Name of VM + def vms_in_pool(_pool_name) raise("#{self.class.name} does not implement vms_in_pool") end # inputs - # vm_name: string + # [String]pool_name : Name of the pool + # [String] vm_name : Name of the VM # returns - # [String] hostname = Name of the host computer running the vm. If this is not a Virtual Machine, it returns the vm_name - def get_vm_host(_vm_name) + # [String] : Name of the host computer running the vm. If this is not a Virtual Machine, it returns the vm_name + def get_vm_host(_pool_name, _vm_name) raise("#{self.class.name} does not implement get_vm_host") end # inputs - # vm_name: string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM # returns - # [String] hostname = Name of the most appropriate host computer to run this VM. Useful for load balancing VMs in a cluster - # If this is not a Virtual Machine, it returns the vm_name - def find_least_used_compatible_host(_vm_name) + # [String] : Name of the most appropriate host computer to run this VM. Useful for load balancing VMs in a cluster + # If this is not a Virtual Machine, it returns the vm_name + def find_least_used_compatible_host(_pool_name, _vm_name) raise("#{self.class.name} does not implement find_least_used_compatible_host") end # inputs - # vm_name: string - # dest_host_name: string (Name of the host to migrate `vm_name` to) + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to migrate + # [String] dest_host_name : Name of the host to migrate `vm_name` to # returns - # [Boolean] Returns true on success or false on failure - def migrate_vm_to_host(_vm_name, _dest_host_name) + # [Boolean] : true on success or false on failure + def migrate_vm_to_host(_pool_name, _vm_name, _dest_host_name) raise("#{self.class.name} does not implement migrate_vm_to_host") end # inputs - # vm_name: string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to find # returns - # nil if it doesn't exist - # Hastable of the VM - # [String] name = Name of the VM - # [String] hostname = Name reported by Vmware tools (host.summary.guest.hostName) - # [String] template = This is the name of template exposed by the API. It must _match_ the poolname - # [String] poolname = Name of the pool the VM is located - # [Time] boottime = Time when the VM was created/booted - # [String] powerstate = Current power state of a VM. Valid values (as per vCenter API) + # nil if VM doesn't exist + # [Hastable] of the VM + # [String] name : Name of the VM + # [String] hostname : Name reported by Vmware tools (host.summary.guest.hostName) + # [String] template : This is the name of template exposed by the API. It must _match_ the poolname + # [String] poolname : Name of the pool the VM is located + # [Time] boottime : Time when the VM was created/booted + # [String] powerstate : Current power state of a VM. Valid values (as per vCenter API) # - 'PoweredOn','PoweredOff' - def get_vm(_vm_name) + def get_vm(_pool_name, _vm_name) raise("#{self.class.name} does not implement get_vm") end # inputs - # pool : hashtable from config file - # new_vmname : string Name the new VM should use + # [String] pool : Name of the pool + # [String] new_vmname : Name to give the new VM # returns - # Hashtable of the VM as per get_vm - def create_vm(_pool, _new_vmname) + # [Hashtable] of the VM as per get_vm + # Raises RuntimeError if the pool_name is not supported by the Provider + def create_vm(_pool_name, _new_vmname) raise("#{self.class.name} does not implement create_vm") end # inputs - # vm_name: string - # pool: string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to create the disk on + # [Integer] disk_size : Size of the disk to create in Gigabytes (GB) # returns - # boolean : true if success, false on error - def destroy_vm(_vm_name, _pool) + # [Boolean] : true if success, false if disk could not be created + # Raises RuntimeError if the Pool does not exist + # Raises RuntimeError if the VM does not exist + def create_disk(_pool_name, _vm_name, _disk_size) + raise("#{self.class.name} does not implement create_disk") + end + + # inputs + # [String] pool_name : Name of the pool + # [String] new_vmname : Name of the VM to create the snapshot on + # [String] new_snapshot_name : Name of the new snapshot to create + # returns + # [Boolean] : true if success, false if snapshot could not be created + # Raises RuntimeError if the VM does not exist + # Raises RuntimeError if the snapshot already exists + def create_snapshot(_pool_name, _vm_name, _new_snapshot_name) + raise("#{self.class.name} does not implement create_snapshot") + end + + # inputs + # [String] pool_name : Name of the pool + # [String] new_vmname : Name of the VM to restore + # [String] snapshot_name : Name of the snapshot to restore to + # returns + # [Boolean] : true if success, false if snapshot could not be revertted + # Raises RuntimeError if the VM does not exist + # Raises RuntimeError if the snapshot already exists + def revert_snapshot(_pool_name, _vm_name, _snapshot_name) + raise("#{self.class.name} does not implement revert_snapshot") + end + + # inputs + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to destroy + # returns + # [Boolean] : true if success, false on error. Should returns true if the VM is missing + def destroy_vm(_pool_name, _vm_name) raise("#{self.class.name} does not implement destroy_vm") end # inputs - # vm : string - # pool: string - # timeout: int (Seconds) + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to check if ready # returns - # result: boolean - def vm_ready?(_vm, _pool, _timeout) + # [Boolean] : true if ready, false if not + def vm_ready?(_pool_name, _vm_name) raise("#{self.class.name} does not implement vm_ready?") end # inputs - # vm : string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to check if it exists # returns - # result: boolean - def vm_exists?(vm) - !get_vm(vm).nil? + # [Boolean] : true if it exists, false if not + def vm_exists?(pool_name, vm_name) + !get_vm(pool_name, vm_name).nil? end end end diff --git a/spec/unit/providers/base_spec.rb b/spec/unit/providers/base_spec.rb index 73bd24d..b9b5155 100644 --- a/spec/unit/providers/base_spec.rb +++ b/spec/unit/providers/base_spec.rb @@ -4,7 +4,12 @@ require 'spec_helper' # to enforce that certain methods are defined in the base classes describe 'Vmpooler::PoolManager::Provider::Base' do + let(:logger) { MockLogger.new } + let(:metrics) { Vmpooler::DummyStatsd.new } let(:config) { {} } + let(:provider_name) { 'base' } + let(:provider_options) { { 'param' => 'value' } } + let(:fake_vm) { fake_vm = {} fake_vm['name'] = 'vm1' @@ -16,11 +21,102 @@ describe 'Vmpooler::PoolManager::Provider::Base' do fake_vm } - subject { Vmpooler::PoolManager::Provider::Base.new(config) } + subject { Vmpooler::PoolManager::Provider::Base.new(config, logger, metrics, provider_name, provider_options) } + # Helper attr_reader methods + describe '#logger' do + it 'should come from the provider initialization' do + expect(subject.logger).to be(logger) + end + end + + describe '#metrics' do + it 'should come from the provider initialization' do + expect(subject.metrics).to be(metrics) + end + end + + describe '#provider_options' do + it 'should come from the provider initialization' do + expect(subject.provider_options).to be(provider_options) + end + end + + describe '#pool_config' do + let(:poolname) { 'pool1' } + let(:config) { YAML.load(<<-EOT +--- +:pools: + - name: '#{poolname}' + alias: [ 'mockpool' ] + template: 'Templates/pool1' + folder: 'Pooler/pool1' + datastore: 'datastore0' + size: 5 + timeout: 10 + ready_ttl: 1440 + clone_target: 'cluster1' +EOT + ) + } + context 'Given a pool that does not exist' do + it 'should return nil' do + expect(subject.pool_config('missing_pool')).to be_nil + end + end + + context 'Given a pool that does exist' do + it 'should return the pool\'s configuration' do + result = subject.pool_config(poolname) + expect(result['name']).to eq(poolname) + end + end + end + + describe '#provider_config' do + let(:poolname) { 'pool1' } + let(:config) { YAML.load(<<-EOT +--- +:providers: + :#{provider_name}: + option1: 'value1' +EOT + ) + } + + context 'Given a misconfigured provider name' do + let(:config) { YAML.load(<<-EOT +--- +:providers: + :bad_provider: + option1: 'value1' + option2: 'value1' +EOT + ) + } + it 'should return nil' do + expect(subject.provider_config).to be_nil + end + end + + context 'Given a correct provider name' do + it 'should return the provider\'s configuration' do + result = subject.provider_config + expect(result['option1']).to eq('value1') + end + end + end + + describe '#global_config' do + it 'should come from the provider initialization' do + expect(subject.global_config).to be(config) + end + end + + # Pool Manager Methods describe '#name' do - it 'should be base' do - expect(subject.name).to eq('base') + it "should come from the provider initialization" do + expect(subject.name).to eq(provider_name) end end @@ -32,25 +128,25 @@ describe 'Vmpooler::PoolManager::Provider::Base' do describe '#get_vm_host' do it 'should raise error' do - expect{subject.get_vm_host('vm')}.to raise_error(/does not implement get_vm_host/) + expect{subject.get_vm_host('pool', 'vm')}.to raise_error(/does not implement get_vm_host/) end end describe '#find_least_used_compatible_host' do it 'should raise error' do - expect{subject.find_least_used_compatible_host('vm')}.to raise_error(/does not implement find_least_used_compatible_host/) + expect{subject.find_least_used_compatible_host('pool', 'vm')}.to raise_error(/does not implement find_least_used_compatible_host/) end end describe '#migrate_vm_to_host' do it 'should raise error' do - expect{subject.migrate_vm_to_host('vm','host')}.to raise_error(/does not implement migrate_vm_to_host/) + expect{subject.migrate_vm_to_host('pool', 'vm','host')}.to raise_error(/does not implement migrate_vm_to_host/) end end describe '#get_vm' do it 'should raise error' do - expect{subject.get_vm('vm')}.to raise_error(/does not implement get_vm/) + expect{subject.get_vm('pool', 'vm')}.to raise_error(/does not implement get_vm/) end end @@ -60,33 +156,51 @@ describe 'Vmpooler::PoolManager::Provider::Base' do end end + describe '#create_disk' do + it 'should raise error' do + expect{subject.create_disk('pool', 'vm', 10)}.to raise_error(/does not implement create_disk/) + end + end + + describe '#create_snapshot' do + it 'should raise error' do + expect{subject.create_snapshot('pool', 'vm', 'snapshot')}.to raise_error(/does not implement create_snapshot/) + end + end + + describe '#revert_snapshot' do + it 'should raise error' do + expect{subject.revert_snapshot('pool', 'vm', 'snapshot')}.to raise_error(/does not implement revert_snapshot/) + end + end + describe '#destroy_vm' do it 'should raise error' do - expect{subject.destroy_vm('vm','pool')}.to raise_error(/does not implement destroy_vm/) + expect{subject.destroy_vm('pool', 'vm')}.to raise_error(/does not implement destroy_vm/) end end describe '#vm_ready?' do it 'should raise error' do - expect{subject.vm_ready?('vm','pool','timeout')}.to raise_error(/does not implement vm_ready?/) + expect{subject.vm_ready?('pool', 'vm')}.to raise_error(/does not implement vm_ready?/) end end describe '#vm_exists?' do it 'should raise error' do - expect{subject.vm_exists?('vm')}.to raise_error(/does not implement/) + expect{subject.vm_exists?('pool', 'vm')}.to raise_error(/does not implement/) end it 'should return true when get_vm returns an object' do - allow(subject).to receive(:get_vm).with('vm').and_return(fake_vm) + allow(subject).to receive(:get_vm).with('pool', 'vm').and_return(fake_vm) - expect(subject.vm_exists?('vm')).to eq(true) + expect(subject.vm_exists?('pool', 'vm')).to eq(true) end it 'should return false when get_vm returns nil' do - allow(subject).to receive(:get_vm).with('vm').and_return(nil) + allow(subject).to receive(:get_vm).with('pool', 'vm').and_return(nil) - expect(subject.vm_exists?('vm')).to eq(false) + expect(subject.vm_exists?('pool', 'vm')).to eq(false) end end end From a155dca0814c77c5ecac94e85c2ec09f14862811 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 Mar 2017 21:35:53 -0700 Subject: [PATCH 5/6] (POOLER-70) Add Pool Manager based functions to vSphere Provider Previously the vSphere provider did not implement any of the required methods from the base class. This commit modifies the vSphere provider so that in can properly implement the following methods: - name - vms_in_pool - get_vm_host - find_least_used_compatible_host - migrate_vm_to_host - get_vm - create_vm - destroy_vm - vm_ready? - vm_exists? - create_disk - create_snapshot - revert_snapshot This commit also includes changes to syntax for rubocop violations. --- lib/vmpooler/providers/vsphere.rb | 403 ++++++++++-- spec/rbvmomi_helper.rb | 60 +- spec/unit/providers/vsphere_spec.rb | 972 ++++++++++++++++++++++++++-- 3 files changed, 1332 insertions(+), 103 deletions(-) diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb index 0b3e49a..38c6d51 100644 --- a/lib/vmpooler/providers/vsphere.rb +++ b/lib/vmpooler/providers/vsphere.rb @@ -2,33 +2,287 @@ module Vmpooler class PoolManager class Provider class VSphere < Vmpooler::PoolManager::Provider::Base - def initialize(options) - super(options) - - # options will be a hash with the following keys: - # :config => The whole configuration object - # :metrics => A metrics object - - @credentials = options[:config][:vsphere] - @conf = options[:config][:config] - @metrics = options[:metrics] + def initialize(config, logger, metrics, name, options) + super(config, logger, metrics, name, options) + @credentials = provider_config + @conf = global_config[:config] end def name 'vsphere' end + def vms_in_pool(pool_name) + connection = get_connection + + foldername = pool_config(pool_name)['folder'] + folder_object = find_folder(foldername, connection) + + vms = [] + + return vms if folder_object.nil? + + folder_object.childEntity.each do |vm| + vms << { 'name' => vm.name } + end + + vms + end + + def get_vm_host(_pool_name, vm_name) + connection = get_connection + + vm_object = find_vm(vm_name, connection) + return nil if vm_object.nil? + + host_name = nil + host_name = vm_object.summary.runtime.host.name if vm_object.summary && vm_object.summary.runtime && vm_object.summary.runtime.host + + host_name + end + + def find_least_used_compatible_host(_pool_name, vm_name) + connection = get_connection + + vm_object = find_vm(vm_name, connection) + + return nil if vm_object.nil? + host_object = find_least_used_vpshere_compatible_host(vm_object) + + return nil if host_object.nil? + host_object[0].name + end + + def migrate_vm_to_host(pool_name, vm_name, dest_host_name) + pool = pool_config(pool_name) + raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil? + + connection = get_connection + + vm_object = find_vm(vm_name, connection) + raise("VM #{vm_name} does not exist in Pool #{pool_name} for the provider #{name}") if vm_object.nil? + + target_cluster_name = get_target_cluster_from_config(pool_name) + cluster = find_cluster(target_cluster_name, connection) + raise("Pool #{pool_name} specifies cluster #{target_cluster_name} which does not exist for the provider #{name}") if cluster.nil? + + # Go through each host and initiate a migration when the correct host name is found + cluster.host.each do |host| + if host.name == dest_host_name + migrate_vm_host(vm_object, host) + return true + end + end + + false + end + + def get_vm(_pool_name, vm_name) + connection = get_connection + + vm_object = find_vm(vm_name, connection) + return nil if vm_object.nil? + + vm_folder_path = get_vm_folder_path(vm_object) + # Find the pool name based on the folder path + pool_name = nil + template_name = nil + global_config[:pools].each do |pool| + if pool['folder'] == vm_folder_path + pool_name = pool['name'] + template_name = pool['template'] + end + end + + generate_vm_hash(vm_object, template_name, pool_name) + end + + def create_vm(pool_name, new_vmname) + pool = pool_config(pool_name) + raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil? + + connection = get_connection + + # Assume all pool config is valid i.e. not missing + template_path = pool['template'] + target_folder_path = pool['folder'] + target_datastore = pool['datastore'] + target_cluster_name = get_target_cluster_from_config(pool_name) + + # Extract the template VM name from the full path + raise("Pool #{pool_name} did specify a full path for the template for the provider #{name}") unless template_path =~ /\// + templatefolders = template_path.split('/') + template_name = templatefolders.pop + + # Get the actual objects from vSphere + template_folder_object = find_folder(templatefolders.join('/'), connection) + raise("Pool #{pool_name} specifies a template folder of #{templatefolders.join('/')} which does not exist for the provider #{name}") if template_folder_object.nil? + + template_vm_object = template_folder_object.find(template_name) + raise("Pool #{pool_name} specifies a template VM of #{template_name} which does not exist for the provider #{name}") if template_vm_object.nil? + + # Annotate with creation time, origin template, etc. + # Add extraconfig options that can be queried by vmtools + config_spec = RbVmomi::VIM.VirtualMachineConfigSpec( + annotation: JSON.pretty_generate( + name: new_vmname, + created_by: provider_config['username'], + base_template: template_path, + creation_timestamp: Time.now.utc + ), + extraConfig: [ + { key: 'guestinfo.hostname', value: new_vmname } + ] + ) + + # Choose a cluster/host to place the new VM on + target_host_object = find_least_used_host(target_cluster_name, connection) + + # Put the VM in the specified folder and resource pool + relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec( + datastore: find_datastore(target_datastore, connection), + host: target_host_object, + diskMoveType: :moveChildMostDiskBacking + ) + + # Create a clone spec + clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec( + location: relocate_spec, + config: config_spec, + powerOn: true, + template: false + ) + + # Create the new VM + new_vm_object = template_vm_object.CloneVM_Task( + folder: find_folder(target_folder_path, connection), + name: new_vmname, + spec: clone_spec + ).wait_for_completion + + generate_vm_hash(new_vm_object, template_path, pool_name) + end + + def create_disk(pool_name, vm_name, disk_size) + pool = pool_config(pool_name) + raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil? + + datastore_name = pool['datastore'] + raise("Pool #{pool_name} does not have a datastore defined for the provider #{name}") if datastore_name.nil? + + connection = get_connection + + vm_object = find_vm(vm_name, connection) + raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if vm_object.nil? + + add_disk(vm_object, disk_size, datastore_name, connection) + + true + end + + def create_snapshot(pool_name, vm_name, new_snapshot_name) + connection = get_connection + + vm_object = find_vm(vm_name, connection) + raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if vm_object.nil? + + old_snap = find_snapshot(vm_object, new_snapshot_name) + raise("Snapshot #{new_snapshot_name} for VM #{vm_name} in pool #{pool_name} already exists for the provider #{name}") unless old_snap.nil? + + vm_object.CreateSnapshot_Task( + name: new_snapshot_name, + description: 'vmpooler', + memory: true, + quiesce: true + ).wait_for_completion + + true + end + + def revert_snapshot(pool_name, vm_name, snapshot_name) + connection = get_connection + + vm_object = find_vm(vm_name, connection) + raise("VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if vm_object.nil? + + snapshot_object = find_snapshot(vm_object, snapshot_name) + raise("Snapshot #{snapshot_name} for VM #{vm_name} in pool #{pool_name} does not exist for the provider #{name}") if snapshot_object.nil? + + snapshot_object.RevertToSnapshot_Task.wait_for_completion + + true + end + + def destroy_vm(_pool_name, vm_name) + connection = get_connection + + vm_object = find_vm(vm_name, connection) + # If a VM doesn't exist then it is effectively deleted + return true if vm_object.nil? + + # Poweroff the VM if it's running + vm_object.PowerOffVM_Task.wait_for_completion if vm_object.runtime && vm_object.runtime.powerState && vm_object.runtime.powerState == 'poweredOn' + + # Kill it with fire + vm_object.Destroy_Task.wait_for_completion + + true + end + + def vm_ready?(_pool_name, vm_name) + begin + open_socket(vm_name) + rescue => _err + return false + end + + true + end + + def provider_config + # The vSphere configuration is currently in it's own root. This will + # eventually shift into the same location base expects it + global_config[:vsphere] + end + + # VSphere Helper methods + + def get_target_cluster_from_config(pool_name) + pool = pool_config(pool_name) + return nil if pool.nil? + + return pool['clone_target'] unless pool['clone_target'].nil? + return global_config[:config]['clone_target'] unless global_config[:config]['clone_target'].nil? + + nil + end + + def generate_vm_hash(vm_object, template_name, pool_name) + hash = { 'name' => nil, 'hostname' => nil, 'template' => nil, 'poolname' => nil, 'boottime' => nil, 'powerstate' => nil } + + hash['name'] = vm_object.name + hash['hostname'] = vm_object.summary.guest.hostName if vm_object.summary && vm_object.summary.guest && vm_object.summary.guest.hostName + hash['template'] = template_name + hash['poolname'] = pool_name + hash['boottime'] = vm_object.runtime.bootTime if vm_object.runtime && vm_object.runtime.bootTime + hash['powerstate'] = vm_object.runtime.powerState if vm_object.runtime && vm_object.runtime.powerState + + hash + end + # vSphere helper methods - ADAPTER_TYPE = 'lsiLogic' - DISK_TYPE = 'thin' - DISK_MODE = 'persistent' + ADAPTER_TYPE = 'lsiLogic'.freeze + DISK_TYPE = 'thin'.freeze + DISK_MODE = 'persistent'.freeze def get_connection - @connection.serviceInstance.CurrentTime - rescue - @connection = connect_to_vsphere @credentials - ensure - return @connection + begin + @connection.serviceInstance.CurrentTime + rescue + @connection = connect_to_vsphere @credentials + end + + @connection end def connect_to_vsphere(credentials) @@ -40,17 +294,55 @@ module Vmpooler user: credentials['username'], password: credentials['password'], insecure: credentials['insecure'] || true - @metrics.increment("connect.open") + metrics.increment('connect.open') return connection rescue => err try += 1 - @metrics.increment("connect.fail") + metrics.increment('connect.fail') raise err if try == max_tries sleep(try * retry_factor) retry end end + # This should supercede the open_socket method in the Pool Manager + def open_socket(host, domain = nil, timeout = 5, port = 22, &_block) + Timeout.timeout(timeout) do + target_host = host + target_host = "#{host}.#{domain}" if domain + sock = TCPSocket.new target_host, port + begin + yield sock if block_given? + ensure + sock.close + end + end + end + + def get_vm_folder_path(vm_object) + # This gives an array starting from the root Datacenters folder all the way to the VM + # [ [Object, String], [Object, String ] ... ] + # It's then reversed so that it now goes from the VM to the Datacenter + full_path = vm_object.path.reverse + + # Find the Datacenter object + dc_index = full_path.index { |p| p[0].is_a?(RbVmomi::VIM::Datacenter) } + return nil if dc_index.nil? + # The Datacenter should be at least 2 otherwise there's something + # wrong with the array passed in + # This is the minimum: + # [ VM (0), VM ROOT FOLDER (1), DC (2)] + return nil if dc_index <= 1 + + # Remove the VM name (Starting position of 1 in the slice) + # Up until the Root VM Folder of DataCenter Node (dc_index - 2) + full_path = full_path.slice(1..dc_index - 2) + + # Reverse the array back to normal and + # then convert the array of paths into a '/' seperated string + (full_path.reverse.map { |p| p[1] }).join('/') + end + def add_disk(vm, size, datastore, connection) return false unless size.to_i > 0 @@ -104,9 +396,9 @@ module Vmpooler datacenter.find_datastore(datastorename) end - def find_device(vm, deviceName) + def find_device(vm, device_name) vm.config.hardware.device.each do |device| - return device if device.deviceInfo.label == deviceName + return device if device.deviceInfo.label == device_name end nil @@ -176,13 +468,11 @@ module Vmpooler def find_folder(foldername, connection) datacenter = connection.serviceInstance.find_datacenter base = datacenter.vmFolder + folders = foldername.split('/') folders.each do |folder| - if base.is_a? RbVmomi::VIM::Folder - base = base.childEntity.find { |f| f.name == folder } - else - raise(RuntimeError, "Unexpected object type encountered (#{base.class}) while finding folder") - end + raise("Unexpected object type encountered (#{base.class}) while finding folder") unless base.is_a? RbVmomi::VIM::Folder + base = base.childEntity.find { |f| f.name == folder } end base @@ -192,7 +482,7 @@ module Vmpooler # Params: # +model+:: CPU arch version to match on # +limit+:: Hard limit for CPU or memory utilization beyond which a host is excluded for deployments - def get_host_utilization(host, model=nil, limit=90) + def get_host_utilization(host, model = nil, limit = 90) if model return nil unless host_has_cpu_model?(host, model) end @@ -205,7 +495,7 @@ module Vmpooler return nil if cpu_utilization > limit return nil if memory_utilization > limit - [ cpu_utilization + memory_utilization, host ] + [cpu_utilization + memory_utilization, host] end def host_has_cpu_model?(host, model) @@ -214,7 +504,7 @@ module Vmpooler def get_host_cpu_arch_version(host) cpu_model = host.hardware.cpuPkg[0].description - cpu_model_parts = cpu_model.split() + cpu_model_parts = cpu_model.split arch_version = cpu_model_parts[4] arch_version end @@ -270,15 +560,14 @@ module Vmpooler base = datacenter.hostFolder pools = poolname.split('/') pools.each do |pool| - case - when base.is_a?(RbVmomi::VIM::Folder) - base = base.childEntity.find { |f| f.name == pool } - when base.is_a?(RbVmomi::VIM::ClusterComputeResource) - base = base.resourcePool.resourcePool.find { |f| f.name == pool } - when base.is_a?(RbVmomi::VIM::ResourcePool) - base = base.resourcePool.find { |f| f.name == pool } - else - raise(RuntimeError, "Unexpected object type encountered (#{base.class}) while finding resource pool") + if base.is_a?(RbVmomi::VIM::Folder) + base = base.childEntity.find { |f| f.name == pool } + elsif base.is_a?(RbVmomi::VIM::ClusterComputeResource) + base = base.resourcePool.resourcePool.find { |f| f.name == pool } + elsif base.is_a?(RbVmomi::VIM::ResourcePool) + base = base.resourcePool.find { |f| f.name == pool } + else + raise("Unexpected object type encountered (#{base.class}) while finding resource pool") end end @@ -287,9 +576,7 @@ module Vmpooler end def find_snapshot(vm, snapshotname) - if vm.snapshot - get_snapshot_list(vm.snapshot.rootSnapshotList, snapshotname) - end + get_snapshot_list(vm.snapshot.rootSnapshotList, snapshotname) if vm.snapshot end def find_vm(vmname, connection) @@ -302,29 +589,29 @@ module Vmpooler def find_vm_heavy(vmname, connection) vmname = vmname.is_a?(Array) ? vmname : [vmname] - containerView = get_base_vm_container_from(connection) - propertyCollector = connection.propertyCollector + container_view = get_base_vm_container_from(connection) + property_collector = connection.propertyCollector - objectSet = [{ - obj: containerView, + object_set = [{ + obj: container_view, skip: true, selectSet: [RbVmomi::VIM::TraversalSpec.new( - name: 'gettingTheVMs', - path: 'view', - skip: false, - type: 'ContainerView' + name: 'gettingTheVMs', + path: 'view', + skip: false, + type: 'ContainerView' )] }] - propSet = [{ + prop_set = [{ pathSet: ['name'], type: 'VirtualMachine' }] - results = propertyCollector.RetrievePropertiesEx( + results = property_collector.RetrievePropertiesEx( specSet: [{ - objectSet: objectSet, - propSet: propSet + objectSet: object_set, + propSet: prop_set }], options: { maxObjects: nil } ) @@ -337,7 +624,7 @@ module Vmpooler end while results.token - results = propertyCollector.ContinueRetrievePropertiesEx(token: results.token) + results = property_collector.ContinueRetrievePropertiesEx(token: results.token) results.objects.each do |result| name = result.propSet.first.val next unless vmname.include? name @@ -356,7 +643,7 @@ module Vmpooler vm_files = vmdk_datastore._connection.serviceContent.propertyCollector.collectMultiple vmdk_datastore.vm, 'layoutEx.file' vm_files.keys.each do |f| vm_files[f]['layoutEx.file'].each do |l| - if l.name.match(/^\[#{vmdk_datastore.name}\] #{vmname}\/#{vmname}_([0-9]+).vmdk/) + if l.name =~ /^\[#{vmdk_datastore.name}\] #{vmname}\/#{vmname}_([0-9]+).vmdk/ disks.push(l) end end @@ -366,8 +653,8 @@ module Vmpooler end def get_base_vm_container_from(connection) - viewManager = connection.serviceContent.viewManager - viewManager.CreateContainerView( + view_manager = connection.serviceContent.viewManager + view_manager.CreateContainerView( container: connection.serviceContent.rootFolder, recursive: true, type: ['VirtualMachine'] diff --git a/spec/rbvmomi_helper.rb b/spec/rbvmomi_helper.rb index 3f06631..375fd56 100644 --- a/spec/rbvmomi_helper.rb +++ b/spec/rbvmomi_helper.rb @@ -60,6 +60,22 @@ MockFolder = Struct.new( def children childEntity end + + # https://github.com/vmware/rbvmomi/blob/master/lib/rbvmomi/vim/Folder.rb#L9-L12 + def find(name, type=Object) + # Fake the searchIndex + childEntity.each do |child| + if child.name == name + if child.kind_of?(type) + return child + else + return nil + end + end + end + + nil + end end MockHostSystem = Struct.new( @@ -103,7 +119,7 @@ MockServiceInstance = Struct.new( # In our mocked instance, DataCenters are always in the root Folder. # If path is nil the first DC is returned otherwise match by name content.rootFolder.childEntity.each do |child| - if child.is_a?(MockDatacenter) + if child.is_a?(RbVmomi::VIM::Datacenter) return child if path.nil? || child.name == path end end @@ -143,9 +159,12 @@ MockVirtualDiskManager = Object MockVirtualMachine = Struct.new( # https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.VirtualMachine.html # From VirtualMachine - :config, :snapshot, :summary, + :config, :runtime, :snapshot, :summary, # From ManagedEntity - :name + :name, + # From RbVmomi::VIM::ManagedEntity + # https://github.com/vmware/rbvmomi/blob/master/lib/rbvmomi/vim/ManagedEntity.rb + :path ) MockVirtualMachineSnapshot = Struct.new( @@ -256,6 +275,12 @@ MockVirtualMachineFileLayoutExFileInfo = Struct.new( :key, :name, :size, :type, :uniqueSize ) +MockVirtualMachineGuestSummary = Struct.new( + # https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.vm.Summary.GuestSummary.html + # From VirtualMachineGuestSummary + :hostName +) + MockVirtualMachineRuntimeInfo = Struct.new( # https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.vm.RuntimeInfo.html # From VirtualMachineRuntimeInfo @@ -399,6 +424,10 @@ def mock_RbVmomi_VIM_Datacenter(options = {}) mock.datastore << mock_ds end + allow(mock).to receive(:is_a?) do |expected_type| + expected_type == RbVmomi::VIM::Datacenter + end + mock end @@ -626,14 +655,21 @@ end def mock_RbVmomi_VIM_VirtualMachine(options = {}) options[:snapshot_tree] = nil if options[:snapshot_tree].nil? options[:name] = 'VM' + rand(65536).to_s if options[:name].nil? + options[:path] = [] if options[:path].nil? mock = MockVirtualMachine.new() mock.config = MockVirtualMachineConfigInfo.new() mock.config.hardware = MockVirtualHardware.new([]) mock.summary = MockVirtualMachineSummary.new() mock.summary.runtime = MockVirtualMachineRuntimeInfo.new() + mock.summary.guest = MockVirtualMachineGuestSummary.new() + mock.runtime = mock.summary.runtime mock.name = options[:name] + mock.summary.guest.hostName = options[:hostname] + mock.runtime.bootTime = options[:boottime] + mock.runtime.powerState = options[:powerstate] + unless options[:snapshot_tree].nil? mock.snapshot = MockVirtualMachineSnapshotInfo.new() mock.snapshot.rootSnapshotList = [] @@ -642,6 +678,24 @@ def mock_RbVmomi_VIM_VirtualMachine(options = {}) # Create a recursive snapshot tree recurse_snapshot_tree(options[:snapshot_tree],mock.snapshot.rootSnapshotList,index) end + + # Create an array of items that describe the path of the VM from the root folder + # all the way to the VM itself + mock.path = [] + options[:path].each do |path_item| + mock_item = nil + case path_item[:type] + when 'folder' + mock_item = mock_RbVmomi_VIM_Folder({ :name => path_item[:name] }) + when 'datacenter' + mock_item = mock_RbVmomi_VIM_Datacenter({ :name => path_item[:name] }) + else + raise("Unknown mock type #{path_item[:type]} for mock_RbVmomi_VIM_VirtualMachine") + end + mock.path << [mock_item,path_item[:name]] + end + mock.path << [mock,options[:name]] + allow(mock).to receive(:is_a?) do |expected_type| expected_type == RbVmomi::VIM::VirtualMachine end diff --git a/spec/unit/providers/vsphere_spec.rb b/spec/unit/providers/vsphere_spec.rb index f64d1d0..906f1b3 100644 --- a/spec/unit/providers/vsphere_spec.rb +++ b/spec/unit/providers/vsphere_spec.rb @@ -8,8 +8,40 @@ 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(:config) { YAML.load(<<-EOT --- :config: @@ -20,22 +52,27 @@ describe 'Vmpooler::PoolManager::Provider::VSphere' do username: "vcenter_user" password: "vcenter_password" insecure: 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(:fake_vm) { - fake_vm = {} - fake_vm['name'] = 'vm1' - fake_vm['hostname'] = 'vm1' - fake_vm['template'] = 'pool1' - fake_vm['boottime'] = Time.now - fake_vm['powerstate'] = 'PoweredOn' + let(:credentials) { config[:vsphere] } - fake_vm - } + let(:connection_options) {{}} + let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } + let(:vmname) { 'vm1' } - subject { Vmpooler::PoolManager::Provider::VSphere.new({:config => config, :metrics => metrics}) } + subject { Vmpooler::PoolManager::Provider::VSphere.new(config, logger, metrics, 'vsphere', provider_options) } describe '#name' do it 'should be vsphere' do @@ -44,78 +81,802 @@ EOT end describe '#vms_in_pool' do - it 'should raise error' do - expect{subject.vms_in_pool('pool')}.to raise_error(/does not implement vms_in_pool/) + let(:folder_object) { mock_RbVmomi_VIM_Folder({ :name => 'pool1'}) } + let(:pool_config) { config[:pools][0] } + + before(:each) do + allow(subject).to receive(:get_connection).and_return(connection) + end + + context 'Given a pool folder that is missing' do + before(:each) do + expect(subject).to receive(:find_folder).with(pool_config['folder'],connection).and_return(nil) + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).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_folder).with(pool_config['folder'],connection).and_return(folder_object) + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).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_folder).with(pool_config['folder'],connection).and_return(folder_object) + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).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 end describe '#get_vm_host' do - it 'should raise error' do - expect{subject.get_vm_host('vm')}.to raise_error(/does not implement get_vm_host/) + before(:each) do + allow(subject).to receive(:get_connection).and_return(connection) + expect(subject).to receive(:find_vm).with(vmname,connection).and_return(vm_object) + end + + context 'when VM does not exist' do + let(:vm_object) { nil } + + it 'should get a connection' do + expect(subject).to receive(:get_connection).and_return(connection) + + subject.get_vm_host(poolname,vmname) + end + + it 'should return nil' do + expect(subject.get_vm_host(poolname,vmname)).to be_nil + end + end + + context 'when VM exists but missing runtime information' do + # For example if the VM is shutdown + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ + :name => vmname, + }) + } + + before(:each) do + vm_object.summary.runtime = nil + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).and_return(connection) + + subject.get_vm_host(poolname,vmname) + end + + it 'should return nil' do + expect(subject.get_vm_host(poolname,vmname)).to be_nil + end + end + + context 'when VM exists and is running on a host' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ + :name => vmname, + }) + } + let(:hostname) { 'HOST001' } + + before(:each) do + mock_host = mock_RbVmomi_VIM_HostSystem({ :name => hostname }) + vm_object.summary.runtime.host = mock_host + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).and_return(connection) + + subject.get_vm_host(poolname,vmname) + end + + it 'should return the hostname' do + expect(subject.get_vm_host(poolname,vmname)).to eq(hostname) + end end end describe '#find_least_used_compatible_host' do - it 'should raise error' do - expect{subject.find_least_used_compatible_host('vm')}.to raise_error(/does not implement find_least_used_compatible_host/) + let(:vm_object) { nil } + + before(:each) do + allow(subject).to receive(:get_connection).and_return(connection) + expect(subject).to receive(:find_vm).with(vmname,connection).and_return(vm_object) + end + + context 'when VM does not exist' do + let(:vm_object) { nil } + + it 'should get a connection' do + expect(subject).to receive(:get_connection).and_return(connection) + + subject.find_least_used_compatible_host(poolname,vmname) + end + + it 'should return nil' do + expect(subject.find_least_used_compatible_host(poolname,vmname)).to be_nil + end + end + + context 'when VM exists but no compatible host' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) } + let(:host_list) { nil } + + before(:each) do + expect(subject).to receive(:find_least_used_vpshere_compatible_host).with(vm_object).and_return(host_list) + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).and_return(connection) + + subject.find_least_used_compatible_host(poolname,vmname) + end + + it 'should return nil' do + expect(subject.find_least_used_compatible_host(poolname,vmname)).to be_nil + end + end + + context 'when VM exists and a compatible host' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) } + let(:hostname) { 'HOST001' } + # As per find_least_used_vpshere_compatible_host, the return value is an array + # [ , ] + let(:host_list) { [mock_RbVmomi_VIM_HostSystem({ :name => hostname }), hostname] } + + before(:each) do + expect(subject).to receive(:find_least_used_vpshere_compatible_host).with(vm_object).and_return(host_list) + end + + it 'should get a connection' do + expect(subject).to receive(:get_connection).and_return(connection) + + subject.find_least_used_compatible_host(poolname,vmname) + end + + it 'should return the hostname' do + expect(subject.find_least_used_compatible_host(poolname,vmname)).to eq(hostname) + end end end describe '#migrate_vm_to_host' do - it 'should raise error' do - expect{subject.migrate_vm_to_host('vm','host')}.to raise_error(/does not implement migrate_vm_to_host/) + let(:dest_host_name) { 'HOST002' } + let(:cluster_name) { 'CLUSTER001' } + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ + :name => vmname, + }) + } + + before(:each) do + config[:pools][0]['clone_target'] = cluster_name + allow(subject).to receive(:get_connection).and_return(connection) + allow(subject).to receive(:find_vm).and_return(vm_object) + end + + context 'Given an invalid pool name' do + it 'should raise an error' do + expect{ subject.migrate_vm_to_host('missing_pool', vmname, dest_host_name) }.to raise_error(/missing_pool does not exist/) + end + end + + context 'Given a missing VM name' do + before(:each) do + expect(subject).to receive(:find_vm).and_return(nil) + end + + it 'should raise an error' do + expect{ subject.migrate_vm_to_host(poolname, 'missing_vm', dest_host_name) }.to raise_error(/missing_vm does not exist/) + end + end + + context 'Given a missing cluster name in the pool configuration' do + let(:cluster_name) { 'missing_cluster' } + + before(:each) do + config[:pools][0]['clone_target'] = cluster_name + expect(subject).to receive(:find_cluster).with(cluster_name,connection).and_return(nil) + end + + it 'should raise an error' do + expect{ subject.migrate_vm_to_host(poolname, vmname, dest_host_name) }.to raise_error(/#{cluster_name} which does not exist/) + end + end + + context 'Given a missing cluster name in the global configuration' do + let(:cluster_name) { 'missing_cluster' } + + before(:each) do + config[:pools][0]['clone_target'] = nil + config[:config]['clone_target'] = cluster_name + expect(subject).to receive(:find_cluster).with(cluster_name,connection).and_return(nil) + end + + it 'should raise an error' do + expect{ subject.migrate_vm_to_host(poolname, vmname, dest_host_name) }.to raise_error(/#{cluster_name} which does not exist/) + end + end + + context 'Given a missing hostname in the cluster' do + before(:each) do + config[:pools][0]['clone_target'] = cluster_name + mock_cluster = mock_RbVmomi_VIM_ComputeResource({ + :hosts => [ { :name => 'HOST001' },{ :name => dest_host_name} ] + }) + expect(subject).to receive(:find_cluster).with(cluster_name,connection).and_return(mock_cluster) + expect(subject).to receive(:migrate_vm_host).exactly(0).times + end + + it 'should return true' do + expect(subject.migrate_vm_to_host(poolname, vmname, 'missing_host')).to be false + end + end + + context 'Given an error during migration' do + before(:each) do + config[:pools][0]['clone_target'] = cluster_name + mock_cluster = mock_RbVmomi_VIM_ComputeResource({ + :hosts => [ { :name => 'HOST001' },{ :name => dest_host_name} ] + }) + expect(subject).to receive(:find_cluster).with(cluster_name,connection).and_return(mock_cluster) + expect(subject).to receive(:migrate_vm_host).with(Object,Object).and_raise(RuntimeError,'MockMigrationError') + end + + it 'should raise an error' do + expect{ subject.migrate_vm_to_host(poolname, vmname, dest_host_name) }.to raise_error('MockMigrationError') + end + end + + context 'Given a successful migration' do + before(:each) do + config[:pools][0]['clone_target'] = cluster_name + mock_cluster = mock_RbVmomi_VIM_ComputeResource({ + :hosts => [ { :name => 'HOST001' },{ :name => dest_host_name} ] + }) + expect(subject).to receive(:find_cluster).with(cluster_name,connection).and_return(mock_cluster) + expect(subject).to receive(:migrate_vm_host).with(Object,Object).and_return(nil) + end + + it 'should return true' do + expect(subject.migrate_vm_to_host(poolname, vmname, dest_host_name)).to be true + end end end describe '#get_vm' do - it 'should raise error' do - expect{subject.get_vm('vm')}.to raise_error(/does not implement get_vm/) + let(:vm_object) { nil } + before(:each) do + allow(subject).to receive(:get_connection).and_return(connection) + expect(subject).to receive(:find_vm).with(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','template','poolname','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 - it 'should raise error' do - expect{subject.create_vm('pool','newname')}.to raise_error(/does not implement create_vm/) + 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 }) } + + before(:each) do + allow(subject).to receive(:get_connection).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 specify a full path for the template/) + end + end + + context 'Given a template path that does not exist' do + before(:each) do + config[:pools][0]['template'] = 'missing_Templates/pool1' + end + + it 'should raise an error' do + expect{ subject.create_vm(poolname, vmname) }.to raise_error(/specifies a template folder of .+ which does not exist/) + end + end + + context 'Given a template VM that does not exist' do + before(:each) do + config[:pools][0]['template'] = 'Templates/missing_template' + 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 + before(:each) do + template_vm = subject.find_folder('Templates',connection).find('pool1') + 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) + 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 = subject.find_folder('Templates',connection).find('pool1') + 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(:get_connection).and_return(connection) + allow(subject).to receive(:find_vm).with(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(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) + 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(:get_connection).and_return(connection) + allow(subject).to receive(:find_vm).with(vmname,connection).and_return(vm_object) + end + + context 'when VM does not exist' do + before(:each) do + expect(subject).to receive(:find_vm).with(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(:get_connection).and_return(connection) + allow(subject).to receive(:find_vm).with(vmname,connection).and_return(vm_object) + end + + context 'when VM does not exist' do + before(:each) do + expect(subject).to receive(:find_vm).with(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 - it 'should raise error' do - expect{subject.destroy_vm('vm','pool')}.to raise_error(/does not implement destroy_vm/) + let(:power_off_task) { mock_RbVmomi_VIM_Task() } + let(:destroy_task) { mock_RbVmomi_VIM_Task() } + + before(:each) do + allow(subject).to receive(:get_connection).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 - it 'should raise error' do - expect{subject.vm_ready?('vm','pool','timeout')}.to raise_error(/does not implement vm_ready?/) + context 'When a VM is ready' do + before(:each) do + expect(subject).to receive(:open_socket).with(vmname) + 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) + 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 raise error' do - expect{subject.vm_exists?('vm')}.to raise_error(/does not implement/) - end - it 'should return true when get_vm returns an object' do - allow(subject).to receive(:get_vm).with('vm').and_return(fake_vm) + allow(subject).to receive(:get_vm).with(poolname,vmname).and_return(mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })) - expect(subject.vm_exists?('vm')).to eq(true) + 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('vm').and_return(nil) + allow(subject).to receive(:get_vm).with(poolname,vmname).and_return(nil) - expect(subject.vm_exists?('vm')).to eq(false) + expect(subject.vm_exists?(poolname,vmname)).to eq(false) end end # vSphere helper methods - let(:credentials) { config[:vsphere] } - - let(:connection_options) {{}} - let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } - let(:vmname) { 'vm1' } - describe '#get_connection' do before(:each) do # NOTE - Using instance_variable_set is a code smell of code that is not testable @@ -299,6 +1060,133 @@ EOT 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 } @@ -316,7 +1204,7 @@ EOT mock_vm } - # Require at least one DC with the requried datastore + # Require at least one DC with the required datastore let(:connection_options) {{ :serviceContent => { :datacenters => [ @@ -1578,7 +2466,7 @@ EOT let(:snapshot_name) {'snapshot'} let(:missing_snapshot_name) {'missing_snapshot'} let(:vm) { mock_RbVmomi_VIM_VirtualMachine(mock_options) } - let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachine() } + let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachineSnapshot() } context 'VM with no snapshots' do let(:mock_options) {{ :snapshot_tree => nil }} From d94b5e6896ba2068a80e54294b6928a028104493 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 29 Mar 2017 17:17:17 -0700 Subject: [PATCH 6/6] (maint) Update rubocop.yml to ignore 2 cops This commit updates the rubocop_todo.yml file for all of the new code that has been introduced. This commit adds two cops to the ignore list - Style/ConditionalAssignment - Next In some cases the readability of code is better even when the cops are raised. This commit disables the cops so they will not be flagged as violations --- .rubocop.yml | 7 +++++++ .rubocop_todo.yml | 41 +++++++++++++++-------------------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index f0fb43c..c5cf24d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -51,3 +51,10 @@ Style/WordArray: # Either sytnax for regex is ok Style/RegexpLiteral: Enabled: false + +# In some cases readability is better without these cops enabled +Style/ConditionalAssignment: + Enabled: false +Next: + Enabled: false + diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 027cd7f..048645a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-03-16 15:37:18 -0700 using RuboCop version 0.47.1. +# on 2017-03-30 17:30:59 -0700 using RuboCop version 0.47.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -56,6 +56,11 @@ Performance/RedundantMatch: - 'lib/vmpooler/api/v1.rb' - 'lib/vmpooler/vsphere_helper.rb' +# Offense count: 1 +Style/AccessorMethodName: + Exclude: + - 'lib/vmpooler/providers/vsphere.rb' + # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. @@ -97,7 +102,7 @@ Style/CaseEquality: Exclude: - 'lib/vmpooler/api/helpers.rb' -# Offense count: 13 +# Offense count: 12 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentOneStep, IndentationWidth. # SupportedStyles: case, end @@ -115,9 +120,7 @@ Style/ClosingParenthesisIndentation: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. -# SupportedStyles: assign_to_condition, assign_inside_condition -Style/ConditionalAssignment: +Style/EmptyCaseCondition: Exclude: - 'lib/vmpooler/vsphere_helper.rb' @@ -173,7 +176,7 @@ Style/FormatString: Exclude: - 'lib/vmpooler/pool_manager.rb' -# Offense count: 9 +# Offense count: 10 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: @@ -272,18 +275,6 @@ Style/NegatedIf: - 'lib/vmpooler/api/v1.rb' - 'lib/vmpooler/pool_manager.rb' -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'lib/vmpooler/api/dashboard.rb' - - 'lib/vmpooler/api/helpers.rb' - - 'lib/vmpooler/api/v1.rb' - - 'lib/vmpooler/pool_manager.rb' - - 'lib/vmpooler/vsphere_helper.rb' - # Offense count: 3 # Cop supports --auto-correct. Style/Not: @@ -337,6 +328,12 @@ Style/RedundantBegin: Exclude: - 'lib/vmpooler/pool_manager.rb' +# Offense count: 2 +# Cop supports --auto-correct. +Style/RedundantException: + Exclude: + - 'lib/vmpooler/vsphere_helper.rb' + # Offense count: 26 # Cop supports --auto-correct. Style/RedundantParentheses: @@ -452,14 +449,6 @@ Style/VariableName: - 'lib/vmpooler/pool_manager.rb' - 'lib/vmpooler/vsphere_helper.rb' -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: -Infinity - # Offense count: 2 # Cop supports --auto-correct. Style/ZeroLengthPredicate: