Using Tags from Automate

Tags are a very powerful feature of CloudForms/ManageIQ, and fortunately Automate has extensive support for tag-related operations.

Creating Tags and Categories

Tags are defined and used within the context of tag categories. We can check whether a category exists, and if not create it:

unless $evm.execute('category_exists?', 'data_centre')
  $evm.execute('category_create',
                      :name => 'data_centre',
                      :single_value => false,
                      :perf_by_tag => false,
                      :description => "Data Centre")
end

We can also check whether a tag exists within a category, and if not create it:

unless $evm.execute('tag_exists?', 'data_centre', 'london')
  $evm.execute('tag_create', 
                      'data_centre',
                      :name => 'london',
                      :description => 'London East End')
end

Note that tag and category names must be lower-case, and optionally contain underscores. They have a maximum length of 30 characters. The tag and category descriptions can be free text.

Assigning and Removing Tags

We can assign a category/tag to an object (in this case a VM):

vm = $evm.root['vm']
vm.tag_assign("data_center/london")

or we can remove a category/tag from an object:

vm = $evm.root['vm']
vm.tag_unassign("data_center/paris")

Testing Whether an Object is Tagged

We can test whether an object (in this case a user group) is tagged with a particular tag:

ci_owner = 'engineering'
groups = $evm.vmdb(:miq_group).find(:all)
groups.each do |group|
  if group.tagged_with?("department", ci_owner)
    $evm.log("info", "Group #{group.description} is tagged with department/#{ci_owner}")
  end
end

Retrieving an Object's Tags

We can retrieve the list of all tags assigned to an object:

group_tags = group.tags

...or the tags in a particular category (in this case using the tag name as a symbol):

all_department_tags = group.tags(:department)
first_department_tag = group.tags(:department).first

Note: The .tags method returns the tags as "category/tag" strings.

Searching for Specifically-Tagged Objects

We can search for objects tagged with a particular tag using .find_tagged_with:

tag = "/managed/department/legal"
hosts = $evm.vmdb(:host).find_tagged_with(:all => tag, :ns => "*")

This example shows that categories themselves are organised into namespaces behind the scenes. In practice the only namespace that seems to be in use is /managed and we rarely need to specify this. The .find_tagged_with method has a slightly ambiguous past. It was present with ManageIQ Anand (CloudForms Management Engine 5.3), but returned Active Records rather than MiqAeService objects. It was unavailable from Automate with ManageIQ Botvinnik (CloudForms Management Engine 5.4), but is thankfully back with ManageIQ Capablanca (CloudForms Management Engine 5.5), and now returns service objects as expected.


Practical example

We could use this to discover all infrastructure components tagged with '/department/engineering' as follows:

tag = '/department/engineering'
[:vm_or_template, :host, :ems_cluster, :storage].each do |service_object|
  these_objects = $evm.vmdb(service_object).find_tagged_with(:all => tag, :ns => "/managed")
  these_objects.each do |this_object|
    service_object_class = "#{this_object.method_missing(:class)}".demodulize
    $evm.log("info", "#{service_object_class}: #{this_object.name} is tagged")
  end
end

On a small CloudForms Management Engine 5.5 system this prints:

MiqAeServiceManageIQ_Providers_Redhat_InfraManager_Template: rhel7-generic is tagged
MiqAeServiceManageIQ_Providers_Redhat_InfraManager_Vm: rhel7srv010 is tagged
MiqAeServiceManageIQ_Providers_Openstack_CloudManager_Vm: rhel7srv031 is tagged
MiqAeServiceManageIQ_Providers_Redhat_InfraManager_Host: rhelh03.bit63.net is tagged
MiqAeServiceStorage: Data is tagged

This code snippet shows an example of where we need to work with or around Distributed Ruby (dRuby), which is the client-server mechanism that links our Ruby automate method with the Automation Engine running in a CloudForms / ManageIQ Generic Worker. The loop:

these_objects.each do |this_object|
  ...
end

enumerates through these_objects, returning a dRuby client object as this_object for each pass through. Normally this is transparent to us and we can refer to the dRuby server object methods such as .name, and all works as expected.

In this case however we also wish to find the class name of the object. If we call this_object.class we get the string "DRb::DRbObject", which is the correct class name for a dRuby client object. We have to tell dRuby to forward the .class method call on to the dRuby server , and we do this by calling this_object.method_missing(:class). Now we get returned the full module::class name of the remote dRuby object (such as MiqAeMethodService::MiqAeServiceStorage), but we can call the .demodulize method on the string to strip the MiqAeMethodService:: module path from the name, leaving us with MiqAeServiceStorage.


Getting the List of Tag Categories

On versions prior to ManageIQ Capablanca (CloudForms Management Engine 5.5) this was slightly challenging. Both tags and categories are listed in the same classifications table, but tags also have a non-zero parent_id value that ties them to their category. To find the categories from the classifications table we had to search for records with a parent_id of zero:

categories = $evm.vmdb('classification').find(:all, :conditions => ["parent_id = 0"])
categories.each do |category|
  $evm.log(:info, "Found tag category: #{category.name} (#{category.description})")
end

With ManageIQ Capablanca we now have a .categories association directly from an MiqAeServiceClassification object, so we can say:

$evm.vmdb(:classification).categories.each do |category|
  $evm.log(:info, "Found tag category: #{category.name} (#{category.description})")
end

Getting the List of Tags in a Category

We occasionally need to retrieve the list of tags in a particular category, and for this we have to perform a double-lookup; once to get the classification ID, and again to find MiqAeServiceClassification objects with that parent_id:

tag_classification = $evm.vmdb('classification').find_by_name('cost_centre')
cost_centre_tags = {}
$evm.vmdb('classification').find_all_by_parent_id(tag_classification.id).each do |tag|
  cost_centre_tags[tag.name] = tag.description
end

Finding a Tag's Name, Given its Description

Sometimes we need to add a tag to an object, but we only have the tag's free-text description (perhaps this matches a value read from an external source). We need to find the tag's snake_case name to use with the .tag_apply method, but we can use more Rails-syntax in our .find call to lookup two fields at once:

department_classification = $evm.vmdb(:classification).find_by_name('department')
tag = $evm.vmdb('classification').find(:first,
                                       :conditions => ["parent_id = ? AND description = ?",
                                       department_classification.id, 'Systems Engineering'])
tag_name = tag.name

The tag names aren't in the classifications table (just the tag description). When we call tag.name, Rails runs an implicit search of the tags table for us, based on the tag.id:

irb(main):051:0> tag.name
  Tag Load (0.6ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" = 1000000000044 LIMIT 1
  Tag Inst Including Associations (0.1ms - 1rows)
    => "syseng"

Finding a Specific Tag (MiqAeServiceClassification) Object

We can just search for the tag object that matches a given category/tag:

tag = $evm.vmdb(:classification).find_by_name('department/hr')

Note: Anything returned from $evm.vmdb(:classification) is an MiqAeServiceClassification object, not a text string.

Deleting a Tag Category

With ManageIQ Capablanca (CloudForms Management Engine 5.5) we can now delete a tag category using the RESTful API:

require 'rest-client'
require 'json'
require 'openssl'
require 'base64'

begin

  def rest_action(uri, verb, payload=nil)
    headers = {
      :content_type  => 'application/json',
      :accept        => 'application/json;version=2',
      :authorization => "Basic #{Base64.strict_encode64("#{@username}:#{@password}")}"
    }
    response = RestClient::Request.new(
      :method      => verb,
      :url         => uri,
      :headers     => headers,
      :payload     => payload,
      verify_ssl: false
    ).execute
    return JSON.parse(response.to_str) unless response.code.to_i == 204
  end

  servername   = $evm.object['servername']
  @username    = $evm.object['username']
  @password    = $evm.object.decrypt('password')

  uri_base = "https://#{servername}/api/"

  category = $evm.vmdb(:classification).find_by_name('network_location')
  rest_return = rest_action("#{uri_base}/categories/#{category.id}", :delete)
  exit MIQ_OK
rescue RestClient::Exception => err
  $evm.log(:error, "REST request failed, code: #{err.response.code}") unless err.response.nil?
  $evm.log(:error, "Response body:\n#{err.response.body.inspect}") unless err.response.nil?
  exit MIQ_STOP
rescue => err
  $evm.log(:error, "[#{err}]\n#{err.backtrace.join("\n")}")
  exit MIQ_STOP
end

results matching ""

    No results matching ""