def relocate_vm(vm)
#
# Get our host name
#
our_host = vm.host_name # <-- Virtual Column
#
# Loop through the other hosts in our cluster
#
target_host = nil
vm.ems_cluster.hosts.each do |this_host| # <-- Two levels of Association
next if this_host.name == our_host
host_invalid = false
this_host.vms.each do |this_vm| # <-- Association
if this_vm.tags(:server_role).first == our_server_role
host_invalid = true
break
end
end
next if host_invalid
#
# If we get to here then no duplicate server_role VMs have been found
# on this host
#
target_host = this_host
break
end
if target_host.nil?
raise "No suitable Host found to migrate VM #{vm.name} to"
else
$evm.log(:info, "Migrating VM #{vm.name} to host: #{target_host.name}")
#
# Migrate the VM to this host
#
vm.migrate(target_host) # <-- Method
end
return target_host.name
end
A Practical Example: Enforcing Anti-Affinity Rules
We can use the techniques that we’ve learnt so far to write an automation script to solve a realistic task.
Task
Write an Automate method that enforces anti-affinity rules for virtual machines, based on a server_role tag applied to each VM. There should be only one VM of any server_role type running on any host in the cluster.
The Automate method should be run from a button visible on the VM details page. If another VM with the same server_role tag is found running on the same host (hypervisor) as the displayed VM, then we live migrate the current VM to another host with no other such tagged VMs. We also email all users in the EvmGroup-administrator group that the migration occurred.
Solution
We can achieve the task in the following way (the entire script is here). We’ll define two methods internally within our Ruby script, relocate_vm
and send_email
. Our main code will be a simple iteration loop.
relocate_vm
The first method relocate_vm
makes use of a virtual column (vm.host_name
), and several associations to find a suitable host (hypervisor) to migrate the virtual machine to. These associations are vm.ems_cluster
to find the cluster that our VM is running on, ems_cluster.hosts
to find the other hypervisors in the cluster, and host.vms
to get the list of VMs running on a hypervisor. Finally it calls a method (vm.migrate
) to perform the VM migration.
send_email
The second method, send_email
, sends an email to all members of a user group. It makes use of an association group.users
to find all users in a particular group, an attribute user.email
to find a user’s email address, and calls $evm.execute
to run the internal :send_email
method.
def send_email(group_name, vm_name, new_host)
#
# Find the group passed to us, and pull out the user emails
#
recipients = []
group = $evm.vmdb('miq_group').find_by_description(group_name)
group.users.each do |group_member| # <-- Association
recipients << group_member.email # <-- Attribute
end
#
# 'from' is the current logged-user who clicked the button
#
from = $evm.root['user'].email
subject = "VM migration"
body = "VM Name: #{vm_name} was live-migrated to Host: #{new_host}"
body += " in accordance with anti-affinity rules"
#
# Send emails
#
recipients.each do |recipient|
$evm.log(:info, "Sending email to <#{recipient}> from <#{from}> \
subject: <#{subject}>")
$evm.execute(:send_email, recipient, from, subject, body)
end
end
Main Code
We’ll wrap our main section of code in a begin
→ rescue
block so that we can catch and handle any exceptions.
begin
#----------------------------------------------------------------------------
# Main code
#----------------------------------------------------------------------------
#
# We've been called from a button on the VM object, so we know that
# $evm.root['vm'] will be loaded
#
vm = $evm.root['vm']
#
# Find out this VM's server_role tag
#
our_server_role = vm.tags(:server_role).first
unless our_server_role.blank?
$evm.log(:info, "VM #{vm.name} has a server_role tag of: #{our_server_role}")
#
# Loop through the other VMs on the same host
#
vm.host.vms.each do |this_vm| # <-- Two levels of Association
next if this_vm.name == vm.name
if this_vm.tags(:server_role).first == our_server_role
$evm.log(:info, "VM #{this_vm.name} also has a server_role tag of: \
#{our_server_role}, taking remedial action")
new_host = relocate_vm(vm)
send_email('EvmGroup-administrator', vm.name, new_host)
end
end
end
exit MIQ_OK
rescue => err
$evm.log(:error, "[#{err}]\n#{err.backtrace.join("\n")}")
exit MIQ_STOP
end
The main code determines the virtual machine service model object from $evm.root['vm']
, and retrieves the first 'server_role' tag applied to the VM (see The VM Provision State Machine for more details on using tags from Automate). It then chains two associations together (vm.host
and host.vms
) to determine the other VMs running on the same hypervisor as our VM. If any of these VMs has the same 'server_role' tag as our VM, we call the relocate_vm
method, and email the 'EvmGroup-administrator' group that the VM has been relocated.
Summary
Here we’ve shown how we can achieve a realistic task with a relatively simple Ruby script, using many of the concepts that we’ve learned so far in the book. We’ve worked with service model objects representing a user, a group, a virtual machine, a cluster and a hypervisor, and we’ve traversed the associations between some of them. We’ve read from an object’s attribute and virtual column, and called an object’s method to perform the migrate operation. Finally, we’ve explored working with tags, and we’ve used $evm.execute
to send an email.
Although most modern virtualisation platforms have an anti-affinity capability built in, this is still a useful example of how we can achieve selected workload placement based on tags. When we implement this kind of tag-based placement, we need to ensure that our VM workloads aren’t tagged multiple times with possibly conflicting results, for example one tag implying affinity, and another anti-affinity.