(POOLER-72) Add Dummy Provider

Previously the only VM Provider was vSphere however this made testing and making
changes difficult as it required a functioning vSphere instance.  This commit
adds a Dummy Provider which presents a VM provider to Pool Manager but manages
provisioned "VM"s in a hashtable.  The Dummy Provider can also be configured to
randomly fail operations and take random amounts of time to perform operations,
 such as cloning a VM, which is useful to see how the Pool Manager copes with
these events.

This commit also updates the configuration YAML documentation and adds
appropriate unit tests.
This commit is contained in:
Glenn Sarti 2017-03-31 16:28:34 -07:00
parent d9d45109f2
commit 77afc86aeb
4 changed files with 962 additions and 1 deletions

View file

@ -1,4 +1,4 @@
%w(base vsphere).each do |lib|
%w(base dummy vsphere).each do |lib|
begin
require "vmpooler/providers/#{lib}"
rescue LoadError

View file

@ -0,0 +1,360 @@
require 'yaml'
module Vmpooler
class PoolManager
class Provider
class Dummy < Vmpooler::PoolManager::Provider::Base
# Fake VM Provider for testing
def initialize(config, logger, metrics, name, options)
super(config, logger, metrics, name, options)
dummyfilename = provider_config['filename']
# This initial_state option is only intended to be used by spec tests
@dummylist = provider_options['initial_state'].nil? ? {} : provider_options['initial_state']
@dummylist = YAML.load_file(dummyfilename) if !dummyfilename.nil? && File.exist?(dummyfilename)
# Even though this code is using Mutexes, it's still no 100% atomic i.e. it's still possible for
# duplicate actions to put the @dummylist hashtable into a bad state, for example;
# Deleting a VM while it's in the middle of adding a disk.
@write_lock = Mutex.new
end
def name
'dummy'
end
def vms_in_pool(pool_name)
vmlist = []
get_dummy_pool_object(pool_name).each do |vm|
vmlist << { 'name' => vm['name'] }
end
vmlist
end
def get_vm_host(pool_name, vm_name)
current_vm = get_dummy_vm(pool_name, vm_name)
current_vm.nil? ? raise("VM #{vm_name} does not exist") : current_vm['vm_host']
end
def find_least_used_compatible_host(pool_name, vm_name)
current_vm = get_dummy_vm(pool_name, vm_name)
# Unless migratevm_couldmove_percent is specified, don't migrate
return current_vm['vm_host'] if provider_config['migratevm_couldmove_percent'].nil?
# Only migrate if migratevm_couldmove_percent is met
return current_vm['vm_host'] if 1 + rand(100) > provider_config['migratevm_couldmove_percent']
# Simulate a 10 node cluster and randomly pick a different one
new_host = 'HOST' + (1 + rand(10)).to_s while new_host == current_vm['vm_host']
new_host
end
def migrate_vm_to_host(pool_name, vm_name, dest_host_name)
current_vm = get_dummy_vm(pool_name, vm_name)
# Inject migration delay
unless provider_config['migratevm_max_time'].nil?
migrate_time = 1 + rand(provider_config['migratevm_max_time'])
sleep(migrate_time)
end
# Inject clone failure
unless provider_config['migratevm_fail_percent'].nil?
raise('Dummy Failure for migratevm_fail_percent') if 1 + rand(100) <= provider_config['migratevm_fail_percent']
end
@write_lock.synchronize do
current_vm = get_dummy_vm(pool_name, vm_name)
current_vm['vm_host'] = dest_host_name
write_backing_file
end
true
end
def get_vm(pool_name, vm_name)
dummy = get_dummy_vm(pool_name, vm_name)
return nil if dummy.nil?
# Randomly power off the VM
unless dummy['powerstate'] != 'PoweredOn' || provider_config['getvm_poweroff_percent'].nil?
if 1 + rand(100) <= provider_config['getvm_poweroff_percent']
@write_lock.synchronize do
dummy = get_dummy_vm(pool_name, vm_name)
dummy['powerstate'] = 'PoweredOff'
write_backing_file
end
logger.log('d', "[ ] [#{dummy['poolname']}] '#{dummy['name']}' is being Dummy Powered Off")
end
end
# Randomly rename the host
unless dummy['hostname'] != dummy['name'] || provider_config['getvm_rename_percent'].nil?
if 1 + rand(100) <= provider_config['getvm_rename_percent']
@write_lock.synchronize do
dummy = get_dummy_vm(pool_name, vm_name)
dummy['hostname'] = 'DUMMY' + dummy['name']
write_backing_file
end
logger.log('d', "[ ] [#{dummy['poolname']}] '#{dummy['name']}' is being Dummy renamed")
end
end
obj = {}
obj['name'] = dummy['name']
obj['hostname'] = dummy['hostname']
obj['boottime'] = dummy['boottime']
obj['template'] = dummy['template']
obj['poolname'] = dummy['poolname']
obj['powerstate'] = dummy['powerstate']
obj['snapshots'] = dummy['snapshots']
obj
end
def create_vm(pool_name, dummy_hostname)
pool = pool_config(pool_name)
raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
template_name = pool['template']
vm = {}
vm['name'] = dummy_hostname
vm['hostname'] = dummy_hostname
vm['domain'] = 'dummy.local'
# 'vm_template' is the name of the template to use to clone the VM from <----- Do we need this?!?!?
vm['vm_template'] = template_name
# 'template' is the Template name in VM Pooler API, in our case that's the poolname.
vm['template'] = pool_name
vm['poolname'] = pool_name
vm['ready'] = false
vm['boottime'] = Time.now
vm['powerstate'] = 'PoweredOn'
vm['vm_host'] = 'HOST1'
vm['dummy_state'] = 'UNKNOWN'
vm['snapshots'] = []
vm['disks'] = []
# Make sure the pool exists in the dummy list
@write_lock.synchronize do
get_dummy_pool_object(pool_name)
@dummylist['pool'][pool_name] << vm
write_backing_file
end
logger.log('d', "[ ] [#{pool_name}] '#{dummy_hostname}' is being cloned from '#{template_name}'")
# Inject clone time delay
unless provider_config['createvm_max_time'].nil?
@write_lock.synchronize do
vm['dummy_state'] = 'CLONING'
write_backing_file
end
clone_time = 1 + rand(provider_config['createvm_max_time'])
sleep(clone_time)
end
begin
# Inject clone failure
unless provider_config['createvm_fail_percent'].nil?
raise('Dummy Failure for createvm_fail_percent') if 1 + rand(100) <= provider_config['createvm_fail_percent']
end
# Assert the VM is ready for use
@write_lock.synchronize do
vm['dummy_state'] = 'RUNNING'
write_backing_file
end
rescue => _err
@write_lock.synchronize do
remove_dummy_vm(pool_name, dummy_hostname)
write_backing_file
end
raise
end
get_vm(pool_name, dummy_hostname)
end
def create_disk(pool_name, vm_name, disk_size)
vm_object = get_dummy_vm(pool_name, vm_name)
raise("VM #{vm_name} does not exist in Pool #{pool_name} for the provider #{name}") if vm_object.nil?
# Inject create time delay
unless provider_config['createdisk_max_time'].nil?
delay = 1 + rand(provider_config['createdisk_max_time'])
sleep(delay)
end
# Inject create failure
unless provider_config['createdisk_fail_percent'].nil?
raise('Dummy Failure for createdisk_fail_percent') if 1 + rand(100) <= provider_config['createdisk_fail_percent']
end
@write_lock.synchronize do
vm_object = get_dummy_vm(pool_name, vm_name)
vm_object['disks'] << disk_size
write_backing_file
end
true
end
def create_snapshot(pool_name, vm_name, snapshot_name)
vm_object = get_dummy_vm(pool_name, vm_name)
raise("VM #{vm_name} does not exist in Pool #{pool_name} for the provider #{name}") if vm_object.nil?
# Inject create time delay
unless provider_config['createsnapshot_max_time'].nil?
delay = 1 + rand(provider_config['createsnapshot_max_time'])
sleep(delay)
end
# Inject create failure
unless provider_config['createsnapshot_fail_percent'].nil?
raise('Dummy Failure for createsnapshot_fail_percent') if 1 + rand(100) <= provider_config['createsnapshot_fail_percent']
end
@write_lock.synchronize do
vm_object = get_dummy_vm(pool_name, vm_name)
vm_object['snapshots'] << snapshot_name
write_backing_file
end
true
end
def revert_snapshot(pool_name, vm_name, snapshot_name)
vm_object = get_dummy_vm(pool_name, vm_name)
raise("VM #{vm_name} does not exist in Pool #{pool_name} for the provider #{name}") if vm_object.nil?
# Inject create time delay
unless provider_config['revertsnapshot_max_time'].nil?
delay = 1 + rand(provider_config['revertsnapshot_max_time'])
sleep(delay)
end
# Inject create failure
unless provider_config['revertsnapshot_fail_percent'].nil?
raise('Dummy Failure for revertsnapshot_fail_percent') if 1 + rand(100) <= provider_config['revertsnapshot_fail_percent']
end
vm_object['snapshots'].include?(snapshot_name)
end
def destroy_vm(pool_name, vm_name)
vm = get_dummy_vm(pool_name, vm_name)
return false if vm.nil?
return false if vm['poolname'] != pool_name
# Shutdown down the VM if it's poweredOn
if vm['powerstate'] == 'PoweredOn'
logger.log('d', "[ ] [#{pool_name}] '#{vm_name}' is being shut down")
# Inject shutdown delay time
unless provider_config['destroyvm_max_shutdown_time'].nil?
shutdown_time = 1 + rand(provider_config['destroyvm_max_shutdown_time'])
sleep(shutdown_time)
end
@write_lock.synchronize do
vm = get_dummy_vm(pool_name, vm_name)
vm['powerstate'] = 'PoweredOff'
write_backing_file
end
end
# Inject destroy VM delay
unless provider_config['destroyvm_max_time'].nil?
destroy_time = 1 + rand(provider_config['destroyvm_max_time'])
sleep(destroy_time)
end
# Inject destroy VM failure
unless provider_config['destroyvm_fail_percent'].nil?
raise('Dummy Failure for migratevm_fail_percent') if 1 + rand(100) <= provider_config['destroyvm_fail_percent']
end
# 'Destroy' the VM
@write_lock.synchronize do
remove_dummy_vm(pool_name, vm_name)
write_backing_file
end
true
end
def vm_ready?(pool_name, vm_name)
vm_object = get_dummy_vm(pool_name, vm_name)
return false if vm_object.nil?
return false if vm_object['poolname'] != pool_name
return true if vm_object['ready']
timeout = provider_config['is_ready_timeout'] || 5
Timeout.timeout(timeout) do
while vm_object['dummy_state'] != 'RUNNING'
sleep(2)
vm_object = get_dummy_vm(pool_name, vm_name)
end
end
# Simulate how long it takes from a VM being powered on until
# it's ready to receive a connection
sleep(2)
unless provider_config['vmready_fail_percent'].nil?
raise('Dummy Failure for vmready_fail_percent') if 1 + rand(100) <= provider_config['vmready_fail_percent']
end
@write_lock.synchronize do
vm_object['ready'] = true
write_backing_file
end
true
end
private
# Note - NEVER EVER use the @write_lock object in the private methods!!!! Deadlocks will ensue
def remove_dummy_vm(pool_name, vm_name)
return if @dummylist['pool'][pool_name].nil?
new_poollist = @dummylist['pool'][pool_name].delete_if { |vm| vm['name'] == vm_name }
@dummylist['pool'][pool_name] = new_poollist
end
# Get's the pool config safely from the in-memory hashtable
def get_dummy_pool_object(pool_name)
@dummylist['pool'] = {} if @dummylist['pool'].nil?
@dummylist['pool'][pool_name] = [] if @dummylist['pool'][pool_name].nil?
@dummylist['pool'][pool_name]
end
def get_dummy_vm(pool_name, vm_name)
return nil if @dummylist['pool'][pool_name].nil?
@dummylist['pool'][pool_name].each do |poolvm|
return poolvm if poolvm['name'] == vm_name
end
nil
end
def write_backing_file
dummyfilename = provider_config['filename']
return if dummyfilename.nil?
File.open(dummyfilename, 'w') { |file| file.write(YAML.dump(@dummylist)) }
end
end
end
end
end