From a155dca0814c77c5ecac94e85c2ec09f14862811 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 Mar 2017 21:35:53 -0700 Subject: [PATCH] (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 }}