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 }}