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.

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

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 beginrescue 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.

results matching ""

    No results matching ""