Merge pull request #214 from glennsarti/ticket/master/POOLER-73-begin-vsphere-migrate

(POOLER-70)(POOLER-52) Create a functional vSphere Provider
This commit is contained in:
mattkirby 2017-04-03 11:46:28 -07:00 committed by GitHub
commit f0f3504c05
7 changed files with 3823 additions and 128 deletions

View file

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

View file

@ -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:

View file

@ -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
# [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(_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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff