diff --git a/.rubocop.yml b/.rubocop.yml index f0fb43c..c5cf24d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -51,3 +51,10 @@ Style/WordArray: # Either sytnax for regex is ok Style/RegexpLiteral: Enabled: false + +# In some cases readability is better without these cops enabled +Style/ConditionalAssignment: + Enabled: false +Next: + Enabled: false + diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 027cd7f..048645a 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-03-16 15:37:18 -0700 using RuboCop version 0.47.1. +# on 2017-03-30 17:30:59 -0700 using RuboCop version 0.47.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -56,6 +56,11 @@ Performance/RedundantMatch: - 'lib/vmpooler/api/v1.rb' - 'lib/vmpooler/vsphere_helper.rb' +# Offense count: 1 +Style/AccessorMethodName: + Exclude: + - 'lib/vmpooler/providers/vsphere.rb' + # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. @@ -97,7 +102,7 @@ Style/CaseEquality: Exclude: - 'lib/vmpooler/api/helpers.rb' -# Offense count: 13 +# Offense count: 12 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentOneStep, IndentationWidth. # SupportedStyles: case, end @@ -115,9 +120,7 @@ Style/ClosingParenthesisIndentation: # Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. -# SupportedStyles: assign_to_condition, assign_inside_condition -Style/ConditionalAssignment: +Style/EmptyCaseCondition: Exclude: - 'lib/vmpooler/vsphere_helper.rb' @@ -173,7 +176,7 @@ Style/FormatString: Exclude: - 'lib/vmpooler/pool_manager.rb' -# Offense count: 9 +# Offense count: 10 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: @@ -272,18 +275,6 @@ Style/NegatedIf: - 'lib/vmpooler/api/v1.rb' - 'lib/vmpooler/pool_manager.rb' -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. -# SupportedStyles: skip_modifier_ifs, always -Style/Next: - Exclude: - - 'lib/vmpooler/api/dashboard.rb' - - 'lib/vmpooler/api/helpers.rb' - - 'lib/vmpooler/api/v1.rb' - - 'lib/vmpooler/pool_manager.rb' - - 'lib/vmpooler/vsphere_helper.rb' - # Offense count: 3 # Cop supports --auto-correct. Style/Not: @@ -337,6 +328,12 @@ Style/RedundantBegin: Exclude: - 'lib/vmpooler/pool_manager.rb' +# Offense count: 2 +# Cop supports --auto-correct. +Style/RedundantException: + Exclude: + - 'lib/vmpooler/vsphere_helper.rb' + # Offense count: 26 # Cop supports --auto-correct. Style/RedundantParentheses: @@ -452,14 +449,6 @@ Style/VariableName: - 'lib/vmpooler/pool_manager.rb' - 'lib/vmpooler/vsphere_helper.rb' -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: -Infinity - # Offense count: 2 # Cop supports --auto-correct. Style/ZeroLengthPredicate: diff --git a/lib/vmpooler/providers/base.rb b/lib/vmpooler/providers/base.rb index d5515c4..579124b 100644 --- a/lib/vmpooler/providers/base.rb +++ b/lib/vmpooler/providers/base.rb @@ -4,101 +4,191 @@ module Vmpooler class Base # These defs must be overidden in child classes - def initialize(options) + # Helper Methods + # Global Logger object + attr_reader :logger + # Global Metrics object + attr_reader :metrics + # Provider options passed in during initialization + attr_reader :provider_options + + def initialize(config, logger, metrics, name, options) + @config = config + @logger = logger + @metrics = metrics + @provider_name = name + @provider_options = options end - # returns - # [String] Name of the provider service - def name - 'base' - end + # Helper Methods # inputs - # pool : hashtable from config file + # [String] pool_name : Name of the pool to get the configuration # returns - # hashtable - # name : name of the device <---- TODO is this all? - def vms_in_pool(_pool) + # [Hashtable] : The pools configuration from the config file. Returns nil if the pool does not exist + def pool_config(pool_name) + # Get the configuration of a specific pool + @config[:pools].each do |pool| + return pool if pool['name'] == pool_name + end + + nil + end + + # returns + # [Hashtable] : This provider's configuration from the config file. Returns nil if the provider does not exist + def provider_config + @config[:providers].each do |provider| + # Convert the symbol from the config into a string for comparison + return provider[1] if provider[0].to_s == @provider_name + end + + nil + end + + # returns + # [Hashtable] : The entire VMPooler configuration + def global_config + # This entire VM Pooler config + @config + end + + # returns + # [String] : Name of the provider service + def name + @provider_name + end + + # Pool Manager Methods + + # inputs + # [String] pool_name : Name of the pool + # returns + # Array[Hashtable] + # Hash contains: + # 'name' => [String] Name of VM + def vms_in_pool(_pool_name) raise("#{self.class.name} does not implement vms_in_pool") end # inputs - # vm_name: string + # [String]pool_name : Name of the pool + # [String] vm_name : Name of the VM # returns - # [String] hostname = Name of the host computer running the vm. If this is not a Virtual Machine, it returns the vm_name - def get_vm_host(_vm_name) + # [String] : Name of the host computer running the vm. If this is not a Virtual Machine, it returns the vm_name + def get_vm_host(_pool_name, _vm_name) raise("#{self.class.name} does not implement get_vm_host") end # inputs - # vm_name: string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM # returns - # [String] hostname = Name of the most appropriate host computer to run this VM. Useful for load balancing VMs in a cluster - # If this is not a Virtual Machine, it returns the vm_name - def find_least_used_compatible_host(_vm_name) + # [String] : Name of the most appropriate host computer to run this VM. Useful for load balancing VMs in a cluster + # If this is not a Virtual Machine, it returns the vm_name + def find_least_used_compatible_host(_pool_name, _vm_name) raise("#{self.class.name} does not implement find_least_used_compatible_host") end # inputs - # vm_name: string - # dest_host_name: string (Name of the host to migrate `vm_name` to) + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to migrate + # [String] dest_host_name : Name of the host to migrate `vm_name` to # returns - # [Boolean] Returns true on success or false on failure - def migrate_vm_to_host(_vm_name, _dest_host_name) + # [Boolean] : true on success or false on failure + def migrate_vm_to_host(_pool_name, _vm_name, _dest_host_name) raise("#{self.class.name} does not implement migrate_vm_to_host") end # inputs - # vm_name: string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to find # returns - # nil if it doesn't exist - # Hastable of the VM - # [String] name = Name of the VM - # [String] hostname = Name reported by Vmware tools (host.summary.guest.hostName) - # [String] template = This is the name of template exposed by the API. It must _match_ the poolname - # [String] poolname = Name of the pool the VM is located - # [Time] boottime = Time when the VM was created/booted - # [String] powerstate = Current power state of a VM. Valid values (as per vCenter API) + # nil if VM doesn't exist + # [Hastable] of the VM + # [String] name : Name of the VM + # [String] hostname : Name reported by Vmware tools (host.summary.guest.hostName) + # [String] template : This is the name of template exposed by the API. It must _match_ the poolname + # [String] poolname : Name of the pool the VM is located + # [Time] boottime : Time when the VM was created/booted + # [String] powerstate : Current power state of a VM. Valid values (as per vCenter API) # - 'PoweredOn','PoweredOff' - def get_vm(_vm_name) + def get_vm(_pool_name, _vm_name) raise("#{self.class.name} does not implement get_vm") end # inputs - # pool : hashtable from config file - # new_vmname : string Name the new VM should use + # [String] pool : Name of the pool + # [String] new_vmname : Name to give the new VM # returns - # Hashtable of the VM as per get_vm - def create_vm(_pool, _new_vmname) + # [Hashtable] of the VM as per get_vm + # Raises RuntimeError if the pool_name is not supported by the Provider + def create_vm(_pool_name, _new_vmname) raise("#{self.class.name} does not implement create_vm") end # inputs - # vm_name: string - # pool: string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to create the disk on + # [Integer] disk_size : Size of the disk to create in Gigabytes (GB) # returns - # boolean : true if success, false on error - def destroy_vm(_vm_name, _pool) + # [Boolean] : true if success, false if disk could not be created + # Raises RuntimeError if the Pool does not exist + # Raises RuntimeError if the VM does not exist + def create_disk(_pool_name, _vm_name, _disk_size) + raise("#{self.class.name} does not implement create_disk") + end + + # inputs + # [String] pool_name : Name of the pool + # [String] new_vmname : Name of the VM to create the snapshot on + # [String] new_snapshot_name : Name of the new snapshot to create + # returns + # [Boolean] : true if success, false if snapshot could not be created + # Raises RuntimeError if the VM does not exist + # Raises RuntimeError if the snapshot already exists + def create_snapshot(_pool_name, _vm_name, _new_snapshot_name) + raise("#{self.class.name} does not implement create_snapshot") + end + + # inputs + # [String] pool_name : Name of the pool + # [String] new_vmname : Name of the VM to restore + # [String] snapshot_name : Name of the snapshot to restore to + # returns + # [Boolean] : true if success, false if snapshot could not be revertted + # Raises RuntimeError if the VM does not exist + # Raises RuntimeError if the snapshot already exists + def revert_snapshot(_pool_name, _vm_name, _snapshot_name) + raise("#{self.class.name} does not implement revert_snapshot") + end + + # inputs + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to destroy + # returns + # [Boolean] : true if success, false on error. Should returns true if the VM is missing + def destroy_vm(_pool_name, _vm_name) raise("#{self.class.name} does not implement destroy_vm") end # inputs - # vm : string - # pool: string - # timeout: int (Seconds) + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to check if ready # returns - # result: boolean - def vm_ready?(_vm, _pool, _timeout) + # [Boolean] : true if ready, false if not + def vm_ready?(_pool_name, _vm_name) raise("#{self.class.name} does not implement vm_ready?") end # inputs - # vm : string + # [String] pool_name : Name of the pool + # [String] vm_name : Name of the VM to check if it exists # returns - # result: boolean - def vm_exists?(vm) - !get_vm(vm).nil? + # [Boolean] : true if it exists, false if not + def vm_exists?(pool_name, vm_name) + !get_vm(pool_name, vm_name).nil? end end end diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb index 3e50e00..38c6d51 100644 --- a/lib/vmpooler/providers/vsphere.rb +++ b/lib/vmpooler/providers/vsphere.rb @@ -2,13 +2,687 @@ module Vmpooler class PoolManager class Provider class VSphere < Vmpooler::PoolManager::Provider::Base - def initialize(options) - super(options) + 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'.freeze + DISK_TYPE = 'thin'.freeze + DISK_MODE = 'persistent'.freeze + + def get_connection + begin + @connection.serviceInstance.CurrentTime + rescue + @connection = connect_to_vsphere @credentials + end + + @connection + end + + def connect_to_vsphere(credentials) + max_tries = @conf['max_tries'] || 3 + retry_factor = @conf['retry_factor'] || 10 + try = 1 + begin + connection = RbVmomi::VIM.connect host: credentials['server'], + user: credentials['username'], + password: credentials['password'], + insecure: credentials['insecure'] || true + metrics.increment('connect.open') + return connection + rescue => err + try += 1 + 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 + + vmdk_datastore = find_datastore(datastore, connection) + vmdk_file_name = "#{vm['name']}/#{vm['name']}_#{find_vmdks(vm['name'], datastore, connection).length + 1}.vmdk" + + controller = find_disk_controller(vm) + + vmdk_spec = RbVmomi::VIM::FileBackedVirtualDiskSpec( + capacityKb: size.to_i * 1024 * 1024, + adapterType: ADAPTER_TYPE, + diskType: DISK_TYPE + ) + + vmdk_backing = RbVmomi::VIM::VirtualDiskFlatVer2BackingInfo( + datastore: vmdk_datastore, + diskMode: DISK_MODE, + fileName: "[#{vmdk_datastore.name}] #{vmdk_file_name}" + ) + + device = RbVmomi::VIM::VirtualDisk( + backing: vmdk_backing, + capacityInKB: size.to_i * 1024 * 1024, + controllerKey: controller.key, + key: -1, + unitNumber: find_disk_unit_number(vm, controller) + ) + + device_config_spec = RbVmomi::VIM::VirtualDeviceConfigSpec( + device: device, + operation: RbVmomi::VIM::VirtualDeviceConfigSpecOperation('add') + ) + + vm_config_spec = RbVmomi::VIM::VirtualMachineConfigSpec( + deviceChange: [device_config_spec] + ) + + connection.serviceContent.virtualDiskManager.CreateVirtualDisk_Task( + datacenter: connection.serviceInstance.find_datacenter, + name: "[#{vmdk_datastore.name}] #{vmdk_file_name}", + spec: vmdk_spec + ).wait_for_completion + + vm.ReconfigVM_Task(spec: vm_config_spec).wait_for_completion + + true + end + + def find_datastore(datastorename, connection) + datacenter = connection.serviceInstance.find_datacenter + datacenter.find_datastore(datastorename) + end + + def find_device(vm, device_name) + vm.config.hardware.device.each do |device| + return device if device.deviceInfo.label == device_name + end + + nil + end + + def find_disk_controller(vm) + devices = find_disk_devices(vm) + + devices.keys.sort.each do |device| + if devices[device]['children'].length < 15 + return find_device(vm, devices[device]['device'].deviceInfo.label) + end + end + + nil + end + + def find_disk_devices(vm) + devices = {} + + vm.config.hardware.device.each do |device| + if device.is_a? RbVmomi::VIM::VirtualSCSIController + if devices[device.controllerKey].nil? + devices[device.key] = {} + devices[device.key]['children'] = [] + end + + devices[device.key]['device'] = device + end + + if device.is_a? RbVmomi::VIM::VirtualDisk + if devices[device.controllerKey].nil? + devices[device.controllerKey] = {} + devices[device.controllerKey]['children'] = [] + end + + devices[device.controllerKey]['children'].push(device) + end + end + + devices + end + + def find_disk_unit_number(vm, controller) + used_unit_numbers = [] + available_unit_numbers = [] + + devices = find_disk_devices(vm) + + devices.keys.sort.each do |c| + next unless controller.key == devices[c]['device'].key + used_unit_numbers.push(devices[c]['device'].scsiCtlrUnitNumber) + devices[c]['children'].each do |disk| + used_unit_numbers.push(disk.unitNumber) + end + end + + (0..15).each do |scsi_id| + if used_unit_numbers.grep(scsi_id).length <= 0 + available_unit_numbers.push(scsi_id) + end + end + + available_unit_numbers.sort[0] + end + + def find_folder(foldername, connection) + datacenter = connection.serviceInstance.find_datacenter + base = datacenter.vmFolder + + folders = foldername.split('/') + folders.each do |folder| + 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 + end + + # Returns an array containing cumulative CPU and memory utilization of a host, and its object reference + # Params: + # +model+:: CPU arch version to match on + # +limit+:: Hard limit for CPU or memory utilization beyond which a host is excluded for deployments + def get_host_utilization(host, model = nil, limit = 90) + if model + return nil unless host_has_cpu_model?(host, model) + end + return nil if host.runtime.inMaintenanceMode + return nil unless host.overallStatus == 'green' + + cpu_utilization = cpu_utilization_for host + memory_utilization = memory_utilization_for host + + return nil if cpu_utilization > limit + return nil if memory_utilization > limit + + [cpu_utilization + memory_utilization, host] + end + + def host_has_cpu_model?(host, model) + get_host_cpu_arch_version(host) == model + end + + def get_host_cpu_arch_version(host) + cpu_model = host.hardware.cpuPkg[0].description + cpu_model_parts = cpu_model.split + arch_version = cpu_model_parts[4] + arch_version + end + + def cpu_utilization_for(host) + cpu_usage = host.summary.quickStats.overallCpuUsage + cpu_size = host.summary.hardware.cpuMhz * host.summary.hardware.numCpuCores + (cpu_usage.to_f / cpu_size.to_f) * 100 + end + + def memory_utilization_for(host) + memory_usage = host.summary.quickStats.overallMemoryUsage + memory_size = host.summary.hardware.memorySize / 1024 / 1024 + (memory_usage.to_f / memory_size.to_f) * 100 + end + + def find_least_used_host(cluster, connection) + cluster_object = find_cluster(cluster, connection) + target_hosts = get_cluster_host_utilization(cluster_object) + least_used_host = target_hosts.sort[0][1] + least_used_host + end + + def find_cluster(cluster, connection) + datacenter = connection.serviceInstance.find_datacenter + datacenter.hostFolder.children.find { |cluster_object| cluster_object.name == cluster } + end + + def get_cluster_host_utilization(cluster) + cluster_hosts = [] + cluster.host.each do |host| + host_usage = get_host_utilization(host) + cluster_hosts << host_usage if host_usage + end + cluster_hosts + end + + def find_least_used_vpshere_compatible_host(vm) + source_host = vm.summary.runtime.host + model = get_host_cpu_arch_version(source_host) + cluster = source_host.parent + target_hosts = [] + cluster.host.each do |host| + host_usage = get_host_utilization(host, model) + target_hosts << host_usage if host_usage + end + target_host = target_hosts.sort[0][1] + [target_host, target_host.name] + end + + def find_pool(poolname, connection) + datacenter = connection.serviceInstance.find_datacenter + base = datacenter.hostFolder + pools = poolname.split('/') + pools.each do |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 + + base = base.resourcePool unless base.is_a?(RbVmomi::VIM::ResourcePool) && base.respond_to?(:resourcePool) + base + end + + def find_snapshot(vm, snapshotname) + get_snapshot_list(vm.snapshot.rootSnapshotList, snapshotname) if vm.snapshot + end + + def find_vm(vmname, connection) + find_vm_light(vmname, connection) || find_vm_heavy(vmname, connection)[vmname] + end + + def find_vm_light(vmname, connection) + connection.searchIndex.FindByDnsName(vmSearch: true, dnsName: vmname) + end + + def find_vm_heavy(vmname, connection) + vmname = vmname.is_a?(Array) ? vmname : [vmname] + container_view = get_base_vm_container_from(connection) + property_collector = connection.propertyCollector + + object_set = [{ + obj: container_view, + skip: true, + selectSet: [RbVmomi::VIM::TraversalSpec.new( + name: 'gettingTheVMs', + path: 'view', + skip: false, + type: 'ContainerView' + )] + }] + + prop_set = [{ + pathSet: ['name'], + type: 'VirtualMachine' + }] + + results = property_collector.RetrievePropertiesEx( + specSet: [{ + objectSet: object_set, + propSet: prop_set + }], + options: { maxObjects: nil } + ) + + vms = {} + results.objects.each do |result| + name = result.propSet.first.val + next unless vmname.include? name + vms[name] = result.obj + end + + while results.token + results = property_collector.ContinueRetrievePropertiesEx(token: results.token) + results.objects.each do |result| + name = result.propSet.first.val + next unless vmname.include? name + vms[name] = result.obj + end + end + + vms + end + + def find_vmdks(vmname, datastore, connection) + disks = [] + + vmdk_datastore = find_datastore(datastore, connection) + + vm_files = vmdk_datastore._connection.serviceContent.propertyCollector.collectMultiple vmdk_datastore.vm, 'layoutEx.file' + vm_files.keys.each do |f| + vm_files[f]['layoutEx.file'].each do |l| + if l.name =~ /^\[#{vmdk_datastore.name}\] #{vmname}\/#{vmname}_([0-9]+).vmdk/ + disks.push(l) + end + end + end + + disks + end + + def get_base_vm_container_from(connection) + view_manager = connection.serviceContent.viewManager + view_manager.CreateContainerView( + container: connection.serviceContent.rootFolder, + recursive: true, + type: ['VirtualMachine'] + ) + end + + def get_snapshot_list(tree, snapshotname) + snapshot = nil + + tree.each do |child| + if child.name == snapshotname + snapshot ||= child.snapshot + else + snapshot ||= get_snapshot_list(child.childSnapshotList, snapshotname) + end + end + + snapshot + end + + def migrate_vm_host(vm, host) + relospec = RbVmomi::VIM.VirtualMachineRelocateSpec(host: host) + vm.RelocateVM_Task(spec: relospec).wait_for_completion + end + + def close + @connection.close + end end end end diff --git a/spec/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/base_spec.rb b/spec/unit/providers/base_spec.rb index 73bd24d..b9b5155 100644 --- a/spec/unit/providers/base_spec.rb +++ b/spec/unit/providers/base_spec.rb @@ -4,7 +4,12 @@ require 'spec_helper' # to enforce that certain methods are defined in the base classes describe 'Vmpooler::PoolManager::Provider::Base' do + let(:logger) { MockLogger.new } + let(:metrics) { Vmpooler::DummyStatsd.new } let(:config) { {} } + let(:provider_name) { 'base' } + let(:provider_options) { { 'param' => 'value' } } + let(:fake_vm) { fake_vm = {} fake_vm['name'] = 'vm1' @@ -16,11 +21,102 @@ describe 'Vmpooler::PoolManager::Provider::Base' do fake_vm } - subject { Vmpooler::PoolManager::Provider::Base.new(config) } + subject { Vmpooler::PoolManager::Provider::Base.new(config, logger, metrics, provider_name, provider_options) } + # Helper attr_reader methods + describe '#logger' do + it 'should come from the provider initialization' do + expect(subject.logger).to be(logger) + end + end + + describe '#metrics' do + it 'should come from the provider initialization' do + expect(subject.metrics).to be(metrics) + end + end + + describe '#provider_options' do + it 'should come from the provider initialization' do + expect(subject.provider_options).to be(provider_options) + end + end + + describe '#pool_config' do + let(:poolname) { 'pool1' } + let(:config) { YAML.load(<<-EOT +--- +:pools: + - name: '#{poolname}' + alias: [ 'mockpool' ] + template: 'Templates/pool1' + folder: 'Pooler/pool1' + datastore: 'datastore0' + size: 5 + timeout: 10 + ready_ttl: 1440 + clone_target: 'cluster1' +EOT + ) + } + context 'Given a pool that does not exist' do + it 'should return nil' do + expect(subject.pool_config('missing_pool')).to be_nil + end + end + + context 'Given a pool that does exist' do + it 'should return the pool\'s configuration' do + result = subject.pool_config(poolname) + expect(result['name']).to eq(poolname) + end + end + end + + describe '#provider_config' do + let(:poolname) { 'pool1' } + let(:config) { YAML.load(<<-EOT +--- +:providers: + :#{provider_name}: + option1: 'value1' +EOT + ) + } + + context 'Given a misconfigured provider name' do + let(:config) { YAML.load(<<-EOT +--- +:providers: + :bad_provider: + option1: 'value1' + option2: 'value1' +EOT + ) + } + it 'should return nil' do + expect(subject.provider_config).to be_nil + end + end + + context 'Given a correct provider name' do + it 'should return the provider\'s configuration' do + result = subject.provider_config + expect(result['option1']).to eq('value1') + end + end + end + + describe '#global_config' do + it 'should come from the provider initialization' do + expect(subject.global_config).to be(config) + end + end + + # Pool Manager Methods describe '#name' do - it 'should be base' do - expect(subject.name).to eq('base') + it "should come from the provider initialization" do + expect(subject.name).to eq(provider_name) end end @@ -32,25 +128,25 @@ describe 'Vmpooler::PoolManager::Provider::Base' do describe '#get_vm_host' do it 'should raise error' do - expect{subject.get_vm_host('vm')}.to raise_error(/does not implement get_vm_host/) + expect{subject.get_vm_host('pool', 'vm')}.to raise_error(/does not implement get_vm_host/) end end describe '#find_least_used_compatible_host' do it 'should raise error' do - expect{subject.find_least_used_compatible_host('vm')}.to raise_error(/does not implement find_least_used_compatible_host/) + expect{subject.find_least_used_compatible_host('pool', 'vm')}.to raise_error(/does not implement find_least_used_compatible_host/) end end describe '#migrate_vm_to_host' do it 'should raise error' do - expect{subject.migrate_vm_to_host('vm','host')}.to raise_error(/does not implement migrate_vm_to_host/) + expect{subject.migrate_vm_to_host('pool', 'vm','host')}.to raise_error(/does not implement migrate_vm_to_host/) end end describe '#get_vm' do it 'should raise error' do - expect{subject.get_vm('vm')}.to raise_error(/does not implement get_vm/) + expect{subject.get_vm('pool', 'vm')}.to raise_error(/does not implement get_vm/) end end @@ -60,33 +156,51 @@ describe 'Vmpooler::PoolManager::Provider::Base' do end end + describe '#create_disk' do + it 'should raise error' do + expect{subject.create_disk('pool', 'vm', 10)}.to raise_error(/does not implement create_disk/) + end + end + + describe '#create_snapshot' do + it 'should raise error' do + expect{subject.create_snapshot('pool', 'vm', 'snapshot')}.to raise_error(/does not implement create_snapshot/) + end + end + + describe '#revert_snapshot' do + it 'should raise error' do + expect{subject.revert_snapshot('pool', 'vm', 'snapshot')}.to raise_error(/does not implement revert_snapshot/) + end + end + describe '#destroy_vm' do it 'should raise error' do - expect{subject.destroy_vm('vm','pool')}.to raise_error(/does not implement destroy_vm/) + expect{subject.destroy_vm('pool', 'vm')}.to raise_error(/does not implement destroy_vm/) end end describe '#vm_ready?' do it 'should raise error' do - expect{subject.vm_ready?('vm','pool','timeout')}.to raise_error(/does not implement vm_ready?/) + expect{subject.vm_ready?('pool', 'vm')}.to raise_error(/does not implement vm_ready?/) end end describe '#vm_exists?' do it 'should raise error' do - expect{subject.vm_exists?('vm')}.to raise_error(/does not implement/) + expect{subject.vm_exists?('pool', 'vm')}.to raise_error(/does not implement/) end it 'should return true when get_vm returns an object' do - allow(subject).to receive(:get_vm).with('vm').and_return(fake_vm) + allow(subject).to receive(:get_vm).with('pool', 'vm').and_return(fake_vm) - expect(subject.vm_exists?('vm')).to eq(true) + expect(subject.vm_exists?('pool', 'vm')).to eq(true) end it 'should return false when get_vm returns nil' do - allow(subject).to receive(:get_vm).with('vm').and_return(nil) + allow(subject).to receive(:get_vm).with('pool', 'vm').and_return(nil) - expect(subject.vm_exists?('vm')).to eq(false) + expect(subject.vm_exists?('pool', 'vm')).to eq(false) end end end diff --git a/spec/unit/providers/vsphere_spec.rb b/spec/unit/providers/vsphere_spec.rb index 86cdc82..906f1b3 100644 --- a/spec/unit/providers/vsphere_spec.rb +++ b/spec/unit/providers/vsphere_spec.rb @@ -1,19 +1,78 @@ require 'spec_helper' -describe 'Vmpooler::PoolManager::Provider::VSphere' do - let(:config) { {} } - 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' +RSpec::Matchers.define :relocation_spec_with_host do |value| + match { |actual| actual[:spec].host == value } +end - fake_vm +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: + max_tries: 3 + retry_factor: 10 +:vsphere: + server: "vcenter.domain.local" + 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 + ) } - subject { Vmpooler::PoolManager::Provider::VSphere.new(config) } + let(:credentials) { config[:vsphere] } + + let(:connection_options) {{}} + let(:connection) { mock_RbVmomi_VIM_Connection(connection_options) } + let(:vmname) { 'vm1' } + + subject { Vmpooler::PoolManager::Provider::VSphere.new(config, logger, metrics, 'vsphere', provider_options) } describe '#name' do it 'should be vsphere' do @@ -22,68 +81,2776 @@ describe 'Vmpooler::PoolManager::Provider::VSphere' do 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 + describe '#get_connection' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + context 'when connection is ok' do + it 'should not attempt to reconnect' do + expect(subject).to receive(:connect_to_vsphere).exactly(0).times + + subject.get_connection() + end + + it 'should return a connection' do + result = subject.get_connection() + + expect(result).to be(connection) + end + end + + context 'when connection has broken' do + before(:each) do + expect(connection.serviceInstance).to receive(:CurrentTime).and_raise(RuntimeError,'MockConnectionError') + end + + it 'should not increment the connect.open metric' do + # https://github.com/puppetlabs/vmpooler/issues/195 + expect(metrics).to receive(:increment).with('connect.open').exactly(0).times + allow(subject).to receive(:connect_to_vsphere) + + subject.get_connection() + end + + it 'should call connect_to_vsphere to reconnect' do + allow(metrics).to receive(:increment) + expect(subject).to receive(:connect_to_vsphere).with(credentials) + + subject.get_connection() + end + + it 'should return a new connection' do + new_connection = mock_RbVmomi_VIM_Connection(connection_options) + expect(subject).to receive(:connect_to_vsphere).with(credentials).and_return(new_connection) + + result = subject.get_connection() + + expect(result).to be(new_connection) + end + end + end + + describe '#connect_to_vsphere' do + before(:each) do + allow(RbVmomi::VIM).to receive(:connect).and_return(connection) + end + + context 'succesful connection' do + it 'should use the supplied credentials' do + expect(RbVmomi::VIM).to receive(:connect).with({ + :host => credentials['server'], + :user => credentials['username'], + :password => credentials['password'], + :insecure => credentials['insecure'] + }).and_return(connection) + subject.connect_to_vsphere(credentials) + end + + it 'should honor the insecure setting' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/207') + config[:vsphere][:insecure] = false + + expect(RbVmomi::VIM).to receive(:connect).with({ + :host => credentials['server'], + :user => credentials['username'], + :password => credentials['password'], + :insecure => false, + }).and_return(connection) + subject.connect_to_vsphere(credentials) + end + + it 'should default to an insecure connection' do + config[:vsphere][:insecure] = nil + + expect(RbVmomi::VIM).to receive(:connect).with({ + :host => credentials['server'], + :user => credentials['username'], + :password => credentials['password'], + :insecure => true + }).and_return(connection) + + subject.connect_to_vsphere(credentials) + end + + it 'should return the connection object' do + result = subject.connect_to_vsphere(credentials) + + expect(result).to be(connection) + end + + it 'should increment the connect.open counter' do + expect(metrics).to receive(:increment).with('connect.open') + subject.connect_to_vsphere(credentials) + end + end + + context 'connection is initially unsuccessful' do + before(:each) do + # Simulate a failure and then success + expect(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError').ordered + expect(RbVmomi::VIM).to receive(:connect).and_return(connection).ordered + + allow(subject).to receive(:sleep) + end + + it 'should return the connection object' do + result = subject.connect_to_vsphere(credentials) + + expect(result).to be(connection) + end + + it 'should increment the connect.fail and then connect.open counter' do + expect(metrics).to receive(:increment).with('connect.fail').exactly(1).times + expect(metrics).to receive(:increment).with('connect.open').exactly(1).times + subject.connect_to_vsphere(credentials) + end + end + + context 'connection is always unsuccessful' do + before(:each) do + allow(RbVmomi::VIM).to receive(:connect).and_raise(RuntimeError,'MockError') + allow(subject).to receive(:sleep) + end + + it 'should raise an error' do + expect{subject.connect_to_vsphere(credentials)}.to raise_error(RuntimeError,'MockError') + end + + it 'should retry the connection attempt config.max_tries times' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + expect(RbVmomi::VIM).to receive(:connect).exactly(config[:config]['max_tries']).times.and_raise(RuntimeError,'MockError') + + begin + # Swallow any errors + subject.connect_to_vsphere(credentials) + rescue + end + end + + it 'should increment the connect.fail counter config.max_tries times' do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + expect(metrics).to receive(:increment).with('connect.fail').exactly(config[:config]['max_tries']).times + + begin + # Swallow any errors + subject.connect_to_vsphere(credentials) + rescue + end + end + + [{:max_tries => 5, :retry_factor => 1}, + {:max_tries => 8, :retry_factor => 5}, + ].each do |testcase| + context "Configuration set for max_tries of #{testcase[:max_tries]} and retry_facter of #{testcase[:retry_factor]}" do + it "should sleep #{testcase[:max_tries] - 1} times between attempts with increasing timeout" do + pending('Resolution of issue https://github.com/puppetlabs/vmpooler/issues/199') + config[:config]['max_tries'] = testcase[:max_tries] + config[:config]['retry_factor'] = testcase[:retry_factor] + + (1..testcase[:max_tries] - 1).each do |try| + expect(subject).to receive(:sleep).with(testcase[:retry_factor] * try).ordered + end + + begin + # Swallow any errors + subject.connect_to_vsphere(credentials) + rescue + end + end + end + end + end + end + + describe '#open_socket' do + let(:TCPSocket) { double('tcpsocket') } + let(:socket) { double('tcpsocket') } + let(:hostname) { 'host' } + let(:domain) { 'domain.local'} + let(:default_socket) { 22 } + + before do + expect(subject).not_to be_nil + allow(socket).to receive(:close) + end + + it 'opens socket with defaults' do + expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_return(socket) + + expect(subject.open_socket(hostname)).to eq(nil) + end + + it 'yields the socket if a block is given' do + expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_return(socket) + + expect{ |socket| subject.open_socket(hostname,nil,nil,default_socket,&socket) }.to yield_control.exactly(1).times + end + + it 'closes the opened socket' do + expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_return(socket) + expect(socket).to receive(:close) + + expect(subject.open_socket(hostname)).to eq(nil) + end + + it 'opens a specific socket' do + expect(TCPSocket).to receive(:new).with(hostname,80).and_return(socket) + + expect(subject.open_socket(hostname,nil,nil,80)).to eq(nil) + end + + it 'uses a specific domain with the hostname' do + expect(TCPSocket).to receive(:new).with("#{hostname}.#{domain}",default_socket).and_return(socket) + + expect(subject.open_socket(hostname,domain)).to eq(nil) + end + + it 'raises error if host is not resolvable' do + expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_raise(SocketError,'getaddrinfo: No such host is known') + + expect { subject.open_socket(hostname,nil,1) }.to raise_error(SocketError) + end + + it 'raises error if socket is not listening' do + expect(TCPSocket).to receive(:new).with(hostname,default_socket).and_raise(SocketError,'No connection could be made because the target machine actively refused it') + + expect { subject.open_socket(hostname,nil,1) }.to raise_error(SocketError) + end + end + + describe '#get_vm_folder_path' do + [ + { :path_description => 'Datacenters/DC01/vm/Pooler/pool1/vm1', + :expected_path => 'Pooler/pool1', + :vm_object_path => [ + { :type => 'folder', :name => 'Datacenters' }, + { :type => 'datacenter', :name => 'DC01' }, + { :type => 'folder', :name => 'vm' }, + { :type => 'folder', :name => 'Pooler' }, + { :type => 'folder', :name => 'pool1'}, + ], + }, + { :path_description => 'Datacenters/DC01/vm/something/subfolder/pool/vm1', + :expected_path => 'something/subfolder/pool', + :vm_object_path => [ + { :type => 'folder', :name => 'Datacenters' }, + { :type => 'datacenter', :name => 'DC01' }, + { :type => 'folder', :name => 'vm' }, + { :type => 'folder', :name => 'something' }, + { :type => 'folder', :name => 'subfolder' }, + { :type => 'folder', :name => 'pool'}, + ], + }, + { :path_description => 'Datacenters/DC01/vm/vm1', + :expected_path => '', + :vm_object_path => [ + { :type => 'folder', :name => 'Datacenters' }, + { :type => 'datacenter', :name => 'DC01' }, + { :type => 'folder', :name => 'vm' }, + ], + }, + ].each do |testcase| + context "given a path of #{testcase[:path_description]}" do + it "should return '#{testcase[:expected_path]}'" do + vm_object = mock_RbVmomi_VIM_VirtualMachine({ + :name => 'vm1', + :path => testcase[:vm_object_path], + }) + expect(subject.get_vm_folder_path(vm_object)).to eq(testcase[:expected_path]) + end + end + end + + [ + { :path_description => 'a path missing a Datacenter', + :vm_object_path => [ + { :type => 'folder', :name => 'Datacenters' }, + { :type => 'folder', :name => 'vm' }, + { :type => 'folder', :name => 'Pooler' }, + { :type => 'folder', :name => 'pool1'}, + ], + }, + { :path_description => 'a path missing root VM folder', + :vm_object_path => [ + { :type => 'folder', :name => 'Datacenters' }, + { :type => 'datacenter', :name => 'DC01' }, + ], + }, + ].each do |testcase| + context "given #{testcase[:path_description]}" do + it "should return nil" do + vm_object = mock_RbVmomi_VIM_VirtualMachine({ + :name => 'vm1', + :path => testcase[:vm_object_path], + }) + expect(subject.get_vm_folder_path(vm_object)).to be_nil + end + end + end + end + + describe '#add_disk' do + let(:datastorename) { 'datastore' } + let(:disk_size) { 30 } + let(:collectMultiple_response) { {} } + + let(:vm_scsi_controller) { mock_RbVmomi_VIM_VirtualSCSIController() } + + # Require at least one SCSI Controller + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine({ + :name => vmname, + }) + mock_vm.config.hardware.device << vm_scsi_controller + + mock_vm + } + + # Require at least one DC with the required datastore + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [datastorename] } + ] + } + }} + + let(:create_virtual_disk_task) { mock_RbVmomi_VIM_Task() } + let(:reconfig_vm_task) { mock_RbVmomi_VIM_Task() } + + before(:each) do + # NOTE - This method should not be using `_connection`, instead it should be using `@conection` + # This should not be required once https://github.com/puppetlabs/vmpooler/issues/213 is resolved + mock_ds = subject.find_datastore(datastorename,connection) + allow(mock_ds).to receive(:_connection).and_return(connection) unless mock_ds.nil? + + # Mocking for find_vmdks + allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response) + + # Mocking for creating the disk + allow(connection.serviceContent.virtualDiskManager).to receive(:CreateVirtualDisk_Task).and_return(create_virtual_disk_task) + allow(create_virtual_disk_task).to receive(:wait_for_completion).and_return(true) + + # Mocking for adding disk to the VM + allow(vm_object).to receive(:ReconfigVM_Task).and_return(reconfig_vm_task) + allow(reconfig_vm_task).to receive(:wait_for_completion).and_return(true) + end + + context 'Succesfully addding disk' do + it 'should return true' do + expect(subject.add_disk(vm_object,disk_size,datastorename,connection)).to be true + end + + it 'should request a disk of appropriate size' do + expect(connection.serviceContent.virtualDiskManager).to receive(:CreateVirtualDisk_Task) + .with(create_virtual_disk_with_size(disk_size)) + .and_return(create_virtual_disk_task) + + + subject.add_disk(vm_object,disk_size,datastorename,connection) + end + end + + context 'Requested disk size is 0' do + it 'should raise an error' do + expect(subject.add_disk(vm_object,0,datastorename,connection)).to be false + end + end + + context 'No datastores or datastore missing' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => ['missing_datastore'] } + ] + } + }} + + it 'should return false' do + expect{ subject.add_disk(vm_object,disk_size,datastorename,connection) }.to raise_error(NoMethodError) + end + end + + context 'VM does not have a SCSI Controller' do + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine({ + :name => vmname, + }) + + mock_vm + } + + it 'should raise an error' do + expect{ subject.add_disk(vm_object,disk_size,datastorename,connection) }.to raise_error(NoMethodError) + end + end + end + + describe '#find_datastore' do + let(:datastorename) { 'datastore' } + let(:datastore_list) { [] } + + context 'No datastores in the datacenter' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [] } + ] + } + }} + + it 'should return nil if the datastore is not found' do + result = subject.find_datastore(datastorename,connection) + expect(result).to be_nil + end + end + + context 'Many datastores in the datacenter' do + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => ['ds1','ds2',datastorename,'ds3'] } + ] + } + }} + + it 'should return nil if the datastore is not found' do + result = subject.find_datastore('missing_datastore',connection) + expect(result).to be_nil + end + + it 'should find the datastore in the datacenter' do + result = subject.find_datastore(datastorename,connection) + + expect(result).to_not be_nil + expect(result.is_a?(RbVmomi::VIM::Datastore)).to be true + expect(result.name).to eq(datastorename) + end + end + end + + describe '#find_device' do + let(:devicename) { 'device1' } + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine() + mock_vm.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + mock_vm.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'}) + + mock_vm + } + + it 'should return a device if the device name matches' do + result = subject.find_device(vm_object,devicename) + + expect(result.deviceInfo.label).to eq(devicename) + end + + it 'should return nil if the device name does not match' do + result = subject.find_device(vm_object,'missing_device') + + expect(result).to be_nil + end + end + + describe '#find_disk_controller' do + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine() + + mock_vm + } + + it 'should return nil when there are no devices' do + result = subject.find_disk_controller(vm_object) + + expect(result).to be_nil + end + + [0,1,14].each do |testcase| + it "should return a device for a single VirtualSCSIController with #{testcase} attached disks" do + mock_scsi = mock_RbVmomi_VIM_VirtualSCSIController() + vm_object.config.hardware.device << mock_scsi + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'}) + + # Add the disks + (1..testcase).each do + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualDisk({ :controllerKey => mock_scsi.key }) + end + + result = subject.find_disk_controller(vm_object) + + expect(result).to eq(mock_scsi) + end + end + + [15].each do |testcase| + it "should return nil for a single VirtualSCSIController with #{testcase} attached disks" do + mock_scsi = mock_RbVmomi_VIM_VirtualSCSIController() + vm_object.config.hardware.device << mock_scsi + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'}) + + # Add the disks + (1..testcase).each do + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualDisk({ :controllerKey => mock_scsi.key }) + end + + result = subject.find_disk_controller(vm_object) + + expect(result).to be_nil + end + end + + it 'should raise if a VirtualDisk is missing a controller' do + # Note - Typically this is not possible as a VirtualDisk requires a controller (SCSI, PVSCSI or IDE) + mock_scsi = mock_RbVmomi_VIM_VirtualDisk() + vm_object.config.hardware.device << mock_scsi + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'}) + + expect{subject.find_disk_controller(vm_object)}.to raise_error(NoMethodError) + end + end + + describe '#find_disk_devices' do + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine() + + mock_vm + } + + it 'should return empty hash when there are no devices' do + result = subject.find_disk_devices(vm_object) + + expect(result).to eq({}) + end + + it 'should return empty hash when there are no VirtualSCSIController or VirtualDisk devices' do + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device2'}) + + result = subject.find_disk_devices(vm_object) + + expect(result).to eq({}) + end + + it 'should return a device for a VirtualSCSIController device with no children' do + mock_scsi = mock_RbVmomi_VIM_VirtualSCSIController() + vm_object.config.hardware.device << mock_scsi + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + + result = subject.find_disk_devices(vm_object) + + expect(result.count).to eq(1) + expect(result[mock_scsi.key]).to_not be_nil + expect(result[mock_scsi.key]['children']).to eq([]) + expect(result[mock_scsi.key]['device']).to eq(mock_scsi) + end + + it 'should return a device for a VirtualDisk device' do + mock_disk = mock_RbVmomi_VIM_VirtualDisk() + vm_object.config.hardware.device << mock_disk + vm_object.config.hardware.device << mock_RbVmomi_VIM_VirtualMachineDevice({:label => 'device1'}) + + result = subject.find_disk_devices(vm_object) + + expect(result.count).to eq(1) + expect(result[mock_disk.controllerKey]).to_not be_nil + expect(result[mock_disk.controllerKey]['children'][0]).to eq(mock_disk) + end + + it 'should return one device for many VirtualDisk devices on the same controller' do + controller1Key = rand(2000) + controller2Key = controller1Key + 1 + mock_disk1 = mock_RbVmomi_VIM_VirtualDisk({:controllerKey => controller1Key}) + mock_disk2 = mock_RbVmomi_VIM_VirtualDisk({:controllerKey => controller1Key}) + mock_disk3 = mock_RbVmomi_VIM_VirtualDisk({:controllerKey => controller2Key}) + + vm_object.config.hardware.device << mock_disk2 + vm_object.config.hardware.device << mock_disk1 + vm_object.config.hardware.device << mock_disk3 + + result = subject.find_disk_devices(vm_object) + + expect(result.count).to eq(2) + + expect(result[controller1Key]).to_not be_nil + expect(result[controller2Key]).to_not be_nil + + expect(result[controller1Key]['children']).to contain_exactly(mock_disk1,mock_disk2) + expect(result[controller2Key]['children']).to contain_exactly(mock_disk3) + end + end + + describe '#find_disk_unit_number' do + let(:vm_object) { + mock_vm = mock_RbVmomi_VIM_VirtualMachine() + + mock_vm + } + let(:controller) { mock_RbVmomi_VIM_VirtualSCSIController() } + + it 'should return 0 when there are no devices' do + result = subject.find_disk_unit_number(vm_object,controller) + + expect(result).to eq(0) + end + + context 'with a single SCSI Controller' do + before(:each) do + vm_object.config.hardware.device << controller + end + + it 'should return 1 when the host bus controller is at 0' do + controller.scsiCtlrUnitNumber = 0 + + result = subject.find_disk_unit_number(vm_object,controller) + + expect(result).to eq(1) + end + + it 'should return the next lowest id when disks are attached' do + expected_id = 9 + controller.scsiCtlrUnitNumber = 0 + + (1..expected_id-1).each do |disk_id| + mock_disk = mock_RbVmomi_VIM_VirtualDisk({ + :controllerKey => controller.key, + :unitNumber => disk_id, + }) + vm_object.config.hardware.device << mock_disk + end + result = subject.find_disk_unit_number(vm_object,controller) + + expect(result).to eq(expected_id) + end + + it 'should return nil when there are no spare units' do + controller.scsiCtlrUnitNumber = 0 + + (1..15).each do |disk_id| + mock_disk = mock_RbVmomi_VIM_VirtualDisk({ + :controllerKey => controller.key, + :unitNumber => disk_id, + }) + vm_object.config.hardware.device << mock_disk + end + result = subject.find_disk_unit_number(vm_object,controller) + + expect(result).to eq(nil) + end + end + end + + describe '#find_folder' do + let(:foldername) { 'folder'} + let(:missing_foldername) { 'missing_folder'} + + before(:each) do + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'with no folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername,connection)).to be_nil + end + end + + context 'with a single layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + foldername => nil, + 'folder3' => nil, + } + }) } + + it 'should return the folder when found' do + result = subject.find_folder(foldername,connection) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername,connection)).to be_nil + end + end + + context 'with a VM with the same name as a folder in a single layer folder hierarchy' do + # The folder hierarchy should include a VM with same name as folder, and appear BEFORE the + # folder in the child list. + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'vm1' => { :object_type => 'vm', :name => foldername }, + foldername => nil, + 'folder3' => nil, + } + }) } + + it 'should not return a VM' do + pending('https://github.com/puppetlabs/vmpooler/issues/204') + result = subject.find_folder(foldername,connection) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false + end + end + + context 'with a multi layer folder hierarchy' do + let(:end_folder_name) { 'folder'} + let(:foldername) { 'folder2/folder4/' + end_folder_name} + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => { + :children => { + 'folder3' => nil, + 'folder4' => { + :children => { + end_folder_name => nil, + }, + } + }, + }, + 'folder5' => nil, + } + }) } + + it 'should return the folder when found' do + result = subject.find_folder(foldername,connection) + expect(result).to_not be_nil + expect(result.name).to eq(end_folder_name) + end + + it 'should return nil if the folder is not found' do + expect(subject.find_folder(missing_foldername,connection)).to be_nil + end + end + + context 'with a VM with the same name as a folder in a multi layer folder hierarchy' do + # The folder hierarchy should include a VM with same name as folder mid-hierarchy (i.e. not at the end level) + # and appear BEFORE the folder in the child list. + let(:end_folder_name) { 'folder'} + let(:foldername) { 'folder2/folder4/' + end_folder_name} + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :vmfolder_tree => { + 'folder1' => nil, + 'folder2' => { + :children => { + 'folder3' => nil, + 'vm1' => { :object_type => 'vm', :name => 'folder4' }, + 'folder4' => { + :children => { + end_folder_name => nil, + }, + } + }, + }, + 'folder5' => nil, + } + }) } + + it 'should not return a VM' do + pending('https://github.com/puppetlabs/vmpooler/issues/204') + result = subject.find_folder(foldername,connection) + expect(result).to_not be_nil + expect(result.name).to eq(foldername) + expect(result.is_a? RbVmomi::VIM::VirtualMachine).to be false + end + end + end + + describe '#get_host_utilization' do + let(:cpu_model) { 'vendor line type sku v4 speed' } + let(:model) { 'v4' } + let(:different_model) { 'different_model' } + let(:limit) { 80 } + let(:default_limit) { 90 } + + context "host with a different model" do + let(:host) { mock_RbVmomi_VIM_HostSystem() } + it 'should return nil' do + expect(subject.get_host_utilization(host,different_model,limit)).to be_nil + end + end + + context "host in maintenance mode" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :maintenance_mode => true, + }) + } + it 'should return nil' do + host.runtime.inMaintenanceMode = true + + expect(subject.get_host_utilization(host,model,limit)).to be_nil + end + end + + context "host with status of not green" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_status => 'purple_alert', + }) + } + it 'should return nil' do + expect(subject.get_host_utilization(host,model,limit)).to be_nil + end + end + + # CPU utilization + context "host which exceeds limit in CPU utilization" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => 100, + :overall_memory_usage => 1, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should return nil' do + expect(subject.get_host_utilization(host,model,limit)).to be_nil + end + end + + context "host which exceeds default limit in CPU utilization" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => default_limit + 1.0, + :overall_memory_usage => 1, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should return nil' do + expect(subject.get_host_utilization(host,model)).to be_nil + end + end + + context "host which does not exceed default limit in CPU utilization" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => default_limit, + :overall_memory_usage => 1, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should not return nil' do + expect(subject.get_host_utilization(host,model)).to_not be_nil + end + end + + # Memory utilization + context "host which exceeds limit in Memory utilization" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => 1, + :overall_memory_usage => 100, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should return nil' do + # Set the Memory Usage to 100% + expect(subject.get_host_utilization(host,model,limit)).to be_nil + end + end + + context "host which exceeds default limit in Memory utilization" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => 1, + :overall_memory_usage => default_limit + 1.0, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should return nil' do + expect(subject.get_host_utilization(host,model)).to be_nil + end + end + + context "host which does not exceed default limit in Memory utilization" do + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => 1, + :overall_memory_usage => default_limit, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should not return nil' do + expect(subject.get_host_utilization(host,model)).to_not be_nil + end + end + + context "host which does not exceed limits" do + # Set CPU to 10% + # Set Memory to 20% + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :overall_cpu_usage => 10, + :overall_memory_usage => 20, + :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }) + } + it 'should return the sum of CPU and Memory utilization' do + expect(subject.get_host_utilization(host,model,limit)[0]).to eq(10 + 20) + end + + it 'should return the host' do + expect(subject.get_host_utilization(host,model,limit)[1]).to eq(host) + end + end + end + + describe '#host_has_cpu_model?' do + let(:cpu_model) { 'vendor line type sku v4 speed' } + let(:model) { 'v4' } + let(:different_model) { 'different_model' } + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :cpu_model => cpu_model, + }) + } + + it 'should return true if the model matches' do + expect(subject.host_has_cpu_model?(host,model)).to eq(true) + end + + it 'should return false if the model is different' do + expect(subject.host_has_cpu_model?(host,different_model)).to eq(false) + end + end + + describe '#get_host_cpu_arch_version' do + let(:cpu_model) { 'vendor line type sku v4 speed' } + let(:model) { 'v4' } + let(:different_model) { 'different_model' } + let(:host) { mock_RbVmomi_VIM_HostSystem({ + :cpu_model => cpu_model, + :num_cpu => 2, + }) + } + + it 'should return the fifth element in the string delimited by spaces' do + expect(subject.get_host_cpu_arch_version(host)).to eq(model) + end + + it 'should use the description of the first CPU' do + host.hardware.cpuPkg[0].description = 'vendor line type sku v6 speed' + expect(subject.get_host_cpu_arch_version(host)).to eq('v6') + end + end + + describe '#cpu_utilization_for' do + [{ :cpu_usage => 10.0, + :core_speed => 10.0, + :num_cores => 2, + :expected_value => 50.0, + }, + { :cpu_usage => 10.0, + :core_speed => 10.0, + :num_cores => 4, + :expected_value => 25.0, + }, + { :cpu_usage => 14.0, + :core_speed => 12.0, + :num_cores => 5, + :expected_value => 23.0 + 1.0/3.0, + }, + ].each do |testcase| + context "CPU Usage of #{testcase[:cpu_usage]}MHz with #{testcase[:num_cores]} x #{testcase[:core_speed]}MHz cores" do + it "should be #{testcase[:expected_value]}%" do + host = mock_RbVmomi_VIM_HostSystem({ + :num_cores_per_cpu => testcase[:num_cores], + :cpu_speed => testcase[:core_speed], + :overall_cpu_usage => testcase[:cpu_usage], + }) + + expect(subject.cpu_utilization_for(host)).to eq(testcase[:expected_value]) + end + end + end + end + + describe '#memory_utilization_for' do + [{ :memory_usage_gigbytes => 10.0, + :memory_size_bytes => 10.0 * 1024 * 1024, + :expected_value => 100.0, + }, + { :memory_usage_gigbytes => 15.0, + :memory_size_bytes => 25.0 * 1024 * 1024, + :expected_value => 60.0, + }, + { :memory_usage_gigbytes => 9.0, + :memory_size_bytes => 31.0 * 1024 * 1024, + :expected_value => 29.03225806451613, + }, + ].each do |testcase| + context "Memory Usage of #{testcase[:memory_usage_gigbytes]}GBytes with #{testcase[:memory_size_bytes]}Bytes of total memory" do + it "should be #{testcase[:expected_value]}%" do + host = mock_RbVmomi_VIM_HostSystem({ + :memory_size => testcase[:memory_size_bytes], + :overall_memory_usage => testcase[:memory_usage_gigbytes], + }) + + expect(subject.memory_utilization_for(host)).to eq(testcase[:expected_value]) + end + end + end + end + + describe '#find_least_used_host' do + let(:cluster_name) { 'cluster' } + let(:missing_cluster_name) { 'missing_cluster' } + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + datacenter_object.hostFolder.childEntity = [cluster_object] + end + + context 'missing cluster' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [{ + :name => cluster_name, + }]})} + let(:expected_host) { cluster_object.host[0] } + + it 'should raise an error' do + expect{subject.find_least_used_host(missing_cluster_name,connection)}.to raise_error(NoMethodError,/undefined method/) + end + end + + context 'standalone host within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [{ + :name => cluster_name, + }]})} + let(:expected_host) { cluster_object.host[0] } + + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name,connection) + + expect(result).to be(expected_host) + end + end + + context 'standalone host outside the limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [{ + :name => cluster_name, + :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024, + }]})} + let(:expected_host) { cluster_object.host[0] } + + it 'should raise an error' do + expect{subject.find_least_used_host(missing_cluster_name,connection)}.to raise_error(NoMethodError,/undefined method/) + end + end + + context 'cluster of 3 hosts within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [ + { :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name,connection) + + expect(result).to be(expected_host) + end + end + + context 'cluster of 3 hosts all outside of the limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [ + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + it 'should raise an error' do + expect{subject.find_least_used_host(missing_cluster_name,connection)}.to raise_error(NoMethodError,/undefined method/) + end + end + + context 'cluster of 5 hosts of which one is out of limits and one has wrong CPU type' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [ + { :overall_cpu_usage => 31, :overall_memory_usage => 31, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :cpu_model => 'different cpu model', :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + it 'should return the standalone host' do + result = subject.find_least_used_host(cluster_name,connection) + + expect(result).to be(expected_host) + end + end + + context 'cluster of 3 hosts all outside of the limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({ + :name => cluster_name, + :hosts => [ + { :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + it 'should return a host' do + pending('https://github.com/puppetlabs/vmpooler/issues/206') + result = subject.find_least_used_host(missing_cluster_name,connection) + expect(result).to_not be_nil + end + end + end + + describe '#find_cluster' do + let(:cluster) {'cluster'} + let(:missing_cluster) {'missing_cluster'} + + before(:each) do + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'no clusters in the datacenter' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + before(:each) do + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster,connection)).to be_nil + end + end + + context 'with a single layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :hostfolder_tree => { + 'cluster1' => {:object_type => 'compute_resource'}, + 'cluster2' => {:object_type => 'compute_resource'}, + cluster => {:object_type => 'compute_resource'}, + 'cluster3' => {:object_type => 'compute_resource'}, + } + }) } + + it 'should return the cluster when found' do + result = subject.find_cluster(cluster,connection) + + expect(result).to_not be_nil + expect(result.name).to eq(cluster) + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster,connection)).to be_nil + end + end + + context 'with a multi layer folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :hostfolder_tree => { + 'cluster1' => {:object_type => 'compute_resource'}, + 'folder2' => { + :children => { + cluster => {:object_type => 'compute_resource'}, + } + }, + 'cluster3' => {:object_type => 'compute_resource'}, + } + }) } + + it 'should return the cluster when found' do + pending('https://github.com/puppetlabs/vmpooler/issues/205') + result = subject.find_cluster(cluster,connection) + + expect(result).to_not be_nil + expect(result.name).to eq(cluster) + end + + it 'should return nil if the cluster is not found' do + expect(subject.find_cluster(missing_cluster,connection)).to be_nil + end + end + end + + describe '#get_cluster_host_utilization' do + context 'standalone host within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [{}]}) } + + it 'should return array with one element' do + result = subject.get_cluster_host_utilization(cluster_object) + expect(result).to_not be_nil + expect(result.count).to eq(1) + end + end + + context 'standalone host which is out the limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + + it 'should return array with 0 elements' do + result = subject.get_cluster_host_utilization(cluster_object) + expect(result).to_not be_nil + expect(result.count).to eq(0) + end + end + + context 'cluster with 3 hosts within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + + it 'should return array with 3 elements' do + result = subject.get_cluster_host_utilization(cluster_object) + expect(result).to_not be_nil + expect(result.count).to eq(3) + end + end + + context 'cluster with 5 hosts of which 3 within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + + it 'should return array with 3 elements' do + result = subject.get_cluster_host_utilization(cluster_object) + expect(result).to_not be_nil + expect(result.count).to eq(3) + end + end + + context 'cluster with 3 hosts of which none are within the limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + + it 'should return array with 0 elements' do + result = subject.get_cluster_host_utilization(cluster_object) + expect(result).to_not be_nil + expect(result.count).to eq(0) + end + end + end + + describe '#find_least_used_vpshere_compatible_host' do + let(:vm) { mock_RbVmomi_VIM_VirtualMachine() } + + context 'standalone host within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [{}]}) } + let(:standalone_host) { cluster_object.host[0] } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + vm.summary.runtime.host = standalone_host + end + + it 'should return the standalone host' do + result = subject.find_least_used_vpshere_compatible_host(vm) + + expect(result).to_not be_nil + expect(result[0]).to be(standalone_host) + expect(result[1]).to eq(standalone_host.name) + end + end + + context 'standalone host outside of limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:standalone_host) { cluster_object.host[0] } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + vm.summary.runtime.host = standalone_host + end + + it 'should raise error' do + expect{subject.find_least_used_vpshere_compatible_host(vm)}.to raise_error(NoMethodError,/undefined method/) + end + end + + context 'cluster of 3 hosts within limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + vm.summary.runtime.host = expected_host + end + + it 'should return the least used host' do + result = subject.find_least_used_vpshere_compatible_host(vm) + + expect(result).to_not be_nil + expect(result[0]).to be(expected_host) + expect(result[1]).to eq(expected_host.name) + end + end + + context 'cluster of 3 hosts all outside of the limits' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + vm.summary.runtime.host = expected_host + end + + it 'should raise error' do + expect{subject.find_least_used_vpshere_compatible_host(vm)}.to raise_error(NoMethodError,/undefined method/) + end + end + + context 'cluster of 5 hosts of which one is out of limits and one has wrong CPU type' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 31, :overall_memory_usage => 31, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :cpu_model => 'different cpu model', :overall_cpu_usage => 1, :overall_memory_usage => 1, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 11, :overall_memory_usage => 11, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 100, :overall_memory_usage => 100, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 21, :overall_memory_usage => 21, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[2] } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + vm.summary.runtime.host = expected_host + end + + it 'should return the least used host' do + result = subject.find_least_used_vpshere_compatible_host(vm) + + expect(result).to_not be_nil + expect(result[0]).to be(expected_host) + expect(result[1]).to eq(expected_host.name) + end + end + + context 'cluster of 3 hosts all with the same utilisation' do + let(:cluster_object) { mock_RbVmomi_VIM_ComputeResource({:hosts => [ + { :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + { :overall_cpu_usage => 10, :overall_memory_usage => 10, :cpu_speed => 100, :num_cores_per_cpu => 1, :num_cpu => 1, :memory_size => 100.0 * 1024 * 1024 }, + ]}) } + let(:expected_host) { cluster_object.host[1] } + + before(:each) do + # This mocking is a little fragile but hard to do without a real vCenter instance + vm.summary.runtime.host = expected_host + end + + it 'should return a host' do + pending('https://github.com/puppetlabs/vmpooler/issues/206 is fixed') + result = subject.find_least_used_vpshere_compatible_host(vm) + + expect(result).to_not be_nil + end + end + end + + describe '#find_pool' do + let(:poolname) { 'pool'} + let(:missing_poolname) { 'missing_pool'} + + before(:each) do + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'with empty folder hierarchy' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter() } + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject).to receive(:ensure_connected) + + subject.find_pool(poolname,connection) + end + + it 'should return nil if the pool is not found' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject.find_pool(missing_poolname,connection)).to be_nil + end + end + + [ + # Single layer Host folder hierarchy + { + :context => 'single layer folder hierarchy with a resource pool', + :poolpath => 'pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'pool' => {:object_type => 'resource_pool'}, + 'folder3' => nil, + }, + }, + { + :context => 'single layer folder hierarchy with a child resource pool', + :poolpath => 'parentpool/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'parentpool' => {:object_type => 'resource_pool', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + 'folder3' => nil, + }, + }, + { + :context => 'single layer folder hierarchy with a resource pool within a cluster', + :poolpath => 'cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => nil, + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + 'folder3' => nil, + }, + }, + # Multi layer Host folder hierarchy + { + :context => 'multi layer folder hierarchy with a resource pool', + :poolpath => 'folder2/folder4/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a child resource pool', + :poolpath => 'folder2/folder4/parentpool/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'parentpool' => {:object_type => 'resource_pool', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a resource pool within a cluster', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + ].each do |testcase| + context testcase[:context] do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } + + it 'should return the pool when found' do + result = subject.find_pool(testcase[:poolpath],connection) + + expect(result).to_not be_nil + expect(result.name).to eq(testcase[:poolname]) + expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true + end + + it 'should return nil if the poolname is not found' do + pending('https://github.com/puppetlabs/vmpooler/issues/209') + expect(subject.find_pool(missing_poolname,connection)).to be_nil + end + end + end + + # Tests for issue https://github.com/puppetlabs/vmpooler/issues/210 + [ + { + :context => 'multi layer folder hierarchy with a resource pool the same name as a folder', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'bad_pool' => {:object_type => 'resource_pool', :name => 'folder4'}, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + { + :context => 'multi layer folder hierarchy with a cluster the same name as a folder', + :poolpath => 'folder2/folder4/cluster/pool', + :poolname => 'pool', + :hostfolder_tree => { + 'folder1' => nil, + 'folder2' => { :children => { + 'folder3' => nil, + 'bad_cluster' => {:object_type => 'cluster_compute_resource', :name => 'folder4'}, + 'folder4' => { :children => { + 'cluster' => {:object_type => 'cluster_compute_resource', :children => { + 'pool' => {:object_type => 'resource_pool'}, + }}, + }}, + }}, + 'folder5' => nil, + }, + }, + ].each do |testcase| + context testcase[:context] do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ :hostfolder_tree => testcase[:hostfolder_tree]}) } + + it 'should ensure the connection' do + pending('https://github.com/puppetlabs/vmpooler/issues/210') + expect(subject).to receive(:ensure_connected) + + subject.find_pool(testcase[:poolpath]) + end + + it 'should return the pool when found' do + pending('https://github.com/puppetlabs/vmpooler/issues/210') + result = subject.find_pool(testcase[:poolpath]) + + expect(result).to_not be_nil + expect(result.name).to eq(testcase[:poolname]) + expect(result.is_a?(RbVmomi::VIM::ResourcePool)).to be true + end + end + end + end + + describe '#find_snapshot' do + let(:snapshot_name) {'snapshot'} + let(:missing_snapshot_name) {'missing_snapshot'} + let(:vm) { mock_RbVmomi_VIM_VirtualMachine(mock_options) } + let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachineSnapshot() } + + context 'VM with no snapshots' do + let(:mock_options) {{ :snapshot_tree => nil }} + it 'should return nil' do + expect(subject.find_snapshot(vm,snapshot_name)).to be_nil + end + end + + context 'VM with a single layer of snapshots' do + let(:mock_options) {{ + :snapshot_tree => { + 'snapshot1' => nil, + 'snapshot2' => nil, + 'snapshot3' => nil, + 'snapshot4' => nil, + snapshot_name => { :ref => snapshot_object}, + } + }} + + it 'should return snapshot which matches the name' do + result = subject.find_snapshot(vm,snapshot_name) + expect(result).to be(snapshot_object) + end + + it 'should return nil which no matches are found' do + result = subject.find_snapshot(vm,missing_snapshot_name) + expect(result).to be_nil + end + end + + context 'VM with a nested layers of snapshots' do + let(:mock_options) {{ + :snapshot_tree => { + 'snapshot1' => nil, + 'snapshot2' => nil, + 'snapshot3' => { :children => { + 'snapshot4' => nil, + 'snapshot5' => { :children => { + snapshot_name => { :ref => snapshot_object}, + }}, + }}, + 'snapshot6' => nil, + } + }} + + it 'should return snapshot which matches the name' do + result = subject.find_snapshot(vm,snapshot_name) + expect(result).to be(snapshot_object) + end + + it 'should return nil which no matches are found' do + result = subject.find_snapshot(vm,missing_snapshot_name) + expect(result).to be_nil + end + end + end + + describe '#find_vm' do + before(:each) do + allow(subject).to receive(:find_vm_light).and_return('vmlight') + allow(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) + end + + it 'should call find_vm_light' do + expect(subject).to receive(:find_vm_light).and_return('vmlight') + + expect(subject.find_vm(vmname,connection)).to eq('vmlight') + end + + it 'should not call find_vm_heavy if find_vm_light finds the VM' do + expect(subject).to receive(:find_vm_light).and_return('vmlight') + expect(subject).to receive(:find_vm_heavy).exactly(0).times + + expect(subject.find_vm(vmname,connection)).to eq('vmlight') + end + + it 'should call find_vm_heavy when find_vm_light returns nil' do + expect(subject).to receive(:find_vm_light).and_return(nil) + expect(subject).to receive(:find_vm_heavy).and_return( { vmname => 'vmheavy' }) + + expect(subject.find_vm(vmname,connection)).to eq('vmheavy') + end + end + + describe '#find_vm_light' do + let(:missing_vm) { 'missing_vm' } + + before(:each) do + allow(connection.searchIndex).to receive(:FindByDnsName).and_return(nil) + end + + it 'should call FindByDnsName with the correct parameters' do + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: vmname, + }) + + subject.find_vm_light(vmname,connection) + end + + it 'should return the VM object when found' do + vm_object = mock_RbVmomi_VIM_VirtualMachine() + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: vmname, + }).and_return(vm_object) + + expect(subject.find_vm_light(vmname,connection)).to be(vm_object) + end + + it 'should return nil if the VM is not found' do + expect(connection.searchIndex).to receive(:FindByDnsName).with({ + :vmSearch => true, + dnsName: missing_vm, + }).and_return(nil) + + expect(subject.find_vm_light(missing_vm,connection)).to be_nil + end + end + + describe '#find_vm_heavy' do + let(:missing_vm) { 'missing_vm' } + # Return an empty result by default + let(:retrieve_result) {{}} + + before(:each) do + allow(connection.propertyCollector).to receive(:RetrievePropertiesEx).and_return(mock_RbVmomi_VIM_RetrieveResult(retrieve_result)) + end + + context 'Search result is empty' do + it 'should return empty hash' do + expect(subject.find_vm_heavy(vmname,connection)).to eq({}) + end + end + + context 'Search result contains VMs but no matches' do + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + ] + } + } + + it 'should return empty hash' do + expect(subject.find_vm_heavy(vmname,connection)).to eq({}) + end + end + + context 'Search contains a single match' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => vmname, :object => vm_object }, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + ] + } + } + + it 'should return single result' do + result = subject.find_vm_heavy(vmname,connection) + expect(result.keys.count).to eq(1) + end + + it 'should return the matching VM Object' do + result = subject.find_vm_heavy(vmname,connection) + expect(result[vmname]).to be(vm_object) + end + end + + context 'Search contains a two matches' do + let(:vm_object1) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:vm_object2) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:retrieve_result) { + { :response => [ + { 'name' => 'no_match001'}, + { 'name' => 'no_match002'}, + { 'name' => vmname, :object => vm_object1 }, + { 'name' => 'no_match003'}, + { 'name' => 'no_match004'}, + { 'name' => vmname, :object => vm_object2 }, + ] + } + } + + it 'should return one result' do + result = subject.find_vm_heavy(vmname,connection) + expect(result.keys.count).to eq(1) + end + + it 'should return the last matching VM Object' do + result = subject.find_vm_heavy(vmname,connection) + expect(result[vmname]).to be(vm_object2) + end + end + end + + describe '#find_vmdks' do + let(:datastorename) { 'datastore' } + let(:connection_options) {{ + :serviceContent => { + :datacenters => [ + { :name => 'MockDC', :datastores => [datastorename] } + ] + } + }} + + let(:collectMultiple_response) { {} } + + before(:each) do + # NOTE - This method should not be using `_connection`, instead it should be using `@conection` + mock_ds = subject.find_datastore(datastorename,connection) + allow(mock_ds).to receive(:_connection).and_return(connection) + allow(connection.serviceContent.propertyCollector).to receive(:collectMultiple).and_return(collectMultiple_response) + end + + it 'should not use _connction to get the underlying connection object' do + pending('https://github.com/puppetlabs/vmpooler/issues/213') + + mock_ds = subject.find_datastore(datastorename) + expect(mock_ds).to receive(:_connection).exactly(0).times + + begin + # ignore all errors. What's important is that it doesn't call _connection + subject.find_vmdks(vmname,datastorename,connection) + rescue + end + end + + context 'Searching all files for all VMs on a Datastore' do + # This is fairly fragile mocking + let(:collectMultiple_response) { { + 'FakeVMObject1' => { 'layoutEx.file' => + [ + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 101, :name => "[#{datastorename}] mock1/mock1_0.vmdk"}) + ]}, + vmname => { 'layoutEx.file' => + [ + # VMDKs which should match + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 1, :name => "[#{datastorename}] #{vmname}/#{vmname}_0.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 2, :name => "[#{datastorename}] #{vmname}/#{vmname}_1.vmdk"}), + # VMDKs which should not match + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 102, :name => "[otherdatastore] #{vmname}/#{vmname}_0.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 103, :name => "[otherdatastore] #{vmname}/#{vmname}.vmdk"}), + mock_RbVmomi_VIM_VirtualMachineFileLayoutExFileInfo({ :key => 104, :name => "[otherdatastore] #{vmname}/#{vmname}_abc.vmdk"}), + ]}, + } } + + it 'should return empty array if no VMDKs match the VM name' do + expect(subject.find_vmdks('missing_vm_name',datastorename,connection)).to eq([]) + end + + it 'should return matching VMDKs for the VM' do + result = subject.find_vmdks(vmname,datastorename,connection) + expect(result).to_not be_nil + expect(result.count).to eq(2) + # The keys for each VMDK should be less that 100 as per the mocks + result.each do |fileinfo| + expect(fileinfo.key).to be < 100 + end + end + end + end + + describe '#get_base_vm_container_from' do + it 'should return a recursive view of type VirtualMachine' do + result = subject.get_base_vm_container_from(connection) + + expect(result.recursive).to be true + expect(result.type).to eq(['VirtualMachine']) + end + end + + describe '#get_snapshot_list' do + let(:snapshot_name) {'snapshot'} + let(:snapshot_tree) { mock_RbVmomi_VIM_VirtualMachine(mock_options).snapshot.rootSnapshotList } + let(:snapshot_object) { mock_RbVmomi_VIM_VirtualMachine() } + + it 'should raise if the snapshot tree is nil' do + expect{ subject.get_snapshot_list(nil,snapshot_name)}.to raise_error(NoMethodError) + end + + context 'VM with a single layer of snapshots' do + let(:mock_options) {{ + :snapshot_tree => { + 'snapshot1' => nil, + 'snapshot2' => nil, + 'snapshot3' => nil, + 'snapshot4' => nil, + snapshot_name => { :ref => snapshot_object}, + } + }} + + it 'should return snapshot which matches the name' do + result = subject.get_snapshot_list(snapshot_tree,snapshot_name) + expect(result).to be(snapshot_object) + end + end + + context 'VM with a nested layers of snapshots' do + let(:mock_options) {{ + :snapshot_tree => { + 'snapshot1' => nil, + 'snapshot2' => nil, + 'snapshot3' => { :children => { + 'snapshot4' => nil, + 'snapshot5' => { :children => { + snapshot_name => { :ref => snapshot_object}, + }}, + }}, + 'snapshot6' => nil, + } + }} + + it 'should return snapshot which matches the name' do + result = subject.get_snapshot_list(snapshot_tree,snapshot_name) + expect(result).to be(snapshot_object) + end + end + end + + describe '#migrate_vm_host' do + let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname })} + let(:host_object) { mock_RbVmomi_VIM_HostSystem({ :name => 'HOST' })} + let(:relocate_task) { mock_RbVmomi_VIM_Task() } + + before(:each) do + allow(vm_object).to receive(:RelocateVM_Task).and_return(relocate_task) + allow(relocate_task).to receive(:wait_for_completion) + end + + it 'should call RelovateVM_Task' do + expect(vm_object).to receive(:RelocateVM_Task).and_return(relocate_task) + + subject.migrate_vm_host(vm_object,host_object) + end + + it 'should use a Relocation Spec object with correct host' do + expect(vm_object).to receive(:RelocateVM_Task).with(relocation_spec_with_host(host_object)) + + subject.migrate_vm_host(vm_object,host_object) + end + + it 'should wait for the relocation to complete' do + expect(relocate_task).to receive(:wait_for_completion) + + subject.migrate_vm_host(vm_object,host_object) + end + + it 'should return the result of the relocation' do + expect(relocate_task).to receive(:wait_for_completion).and_return('RELOCATE_RESULT') + + expect(subject.migrate_vm_host(vm_object,host_object)).to eq('RELOCATE_RESULT') + end + end + + describe '#close' do + context 'no connection has been made' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",nil) + end + + it 'should not error' do + pending('https://github.com/puppetlabs/vmpooler/issues/211') + subject.close + end + end + + context 'on an open connection' do + before(:each) do + # NOTE - Using instance_variable_set is a code smell of code that is not testable + subject.instance_variable_set("@connection",connection) + end + + it 'should close the underlying connection object' do + expect(connection).to receive(:close) + subject.close + end end end end