Tags are a very powerful feature of CloudForms. They allow us to add Smart Management capabilities to the objects in the WebUI such as virtual machines, hosts, or clusters; to create tag-related filters; and to group, sort, or categorize items by tag.
For example, we might assign a tag to a virtual machine to identify which department or cost center owns the VM. We could then create a chargeback rate for billing purposes and assign the rate to all VMs tagged as being owned by a particular department or cost center.
We might also tag virtual machines with a Location or Data Center tag. We could create a filter view in the WebUI to display all VMs at a particular location so that we instantly can see which systems might be affected if we run a data center failover or power test.
Tags are not only applied to virtual machines. We often tag our virtual infrastructure components—such as hosts, clusters, or datastores—with a Provisioning Scope tag. When we provision new virtual machines, our Automate workflow must determine where to put the new VM (a process known as placement). We can use the Provisioning Scope tag to determine a best fit for a particular virtual machine, based on a user’s group membership. In this way we might, for example, place all virtual machines provisioned by users in a development group on a nonproduction cluster.
These are just three examples of how tags can simplify systems administration and help our Automate workflows. Fortunately, Automate has comprehensive support for tag-related operations.
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_center'
)
$evm
.
execute
(
'category_create'
,
:name
=>
'data_center'
,
:single_value
=>
false
,
:perf_by_tag
=>
false
,
:description
=>
"Data Center"
)
end
We can also check whether a tag exists within a category, and if not, create it:
unless
$evm
.
execute
(
'tag_exists?'
,
'data_center'
,
'london'
)
$evm
.
execute
(
'tag_create'
,
'data_center'
,
:name
=>
'london'
,
:description
=>
'London East End'
)
end
Tag and category names must be lowercase, and optionally contain underscores. They have a maximum length of 30 characters. The tag and category descriptions can be free text.
We can test whether an object (in this case, a user group) is tagged with a particular tag, like so:
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"
)
end
end
We can use the tags
method to retrieve the list of all tags assigned to an object:
group_tags
=
group
.
tags
This method also enables us to retrieve 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
When called with no argument, the tags
method returns the tags as "category/tag"
strings. When called with an argument of tag category, the method returns the tag name as the string.
We use the find_tagged_with
method to search for objects tagged with a particular tag:
tag
=
"/managed/department/legal"
hosts
=
$evm
.
vmdb
(
:host
)
.
find_tagged_with
(
:all
=>
tag
,
:ns
=>
"*"
)
This example shows that categories themselves are organized 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 in CloudForms 3.1 but returned active records rather than MiqAeService
objects. It disappeared as an Automate method in CloudForms 3.2 but thankfully is back with CloudForms 4.0 and now returns service model objects as expected.
We could discover all infrastructure components tagged with /department/engineering
. We might wish to find out the service model class name of the object and the object’s name, for example. We could achieve this using the following code snippet:
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_model_class
=
"
#{
this_object
.
method_missing
(
:class
)
}
"
.
demodulize
$evm
.
log
(
"info"
,
"
#{
service_model_class
}
:
#{
this_object
.
name
}
"
)
end
end
On a small CloudForms 4.0 system, this prints:
MiqAeServiceManageIQ_Providers_Redhat_InfraManager_Template: rhel7-generic MiqAeServiceManageIQ_Providers_Redhat_InfraManager_Vm: rhel7srv010 MiqAeServiceManageIQ_Providers_Openstack_CloudManager_Vm: rhel7srv031 MiqAeServiceManageIQ_Providers_Redhat_InfraManager_Host: rhelh03.bit63.net MiqAeServiceStorage: Data
This code snippet shows an example of where we need to work with or around Distributed Ruby (dRuby). The following loop enumerates through these_objects
, substituting this_object
on each iteration:
these_objects
.
each
do
|
this_object
|
.
.
.
end
Normally this is transparent to us and we can refer to the object methods, such as name
, and all works as expected.
Behind the scenes, however, our automation script is accessing all of these objects remotely via its dRuby client object. We must bear this in mind if we also wish to find the class name of the remote 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
.
On versions prior to CloudForms 4.0, getting the list of tag categories was slightly challenging. Both tags and categories are listed in the same classifications
table, but tags also have a nonzero 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 category:
#{
category
.
name
}
(
#{
category
.
description
}
)"
)
end
With CloudForms 4.0 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 category:
#{
category
.
name
}
(
#{
category
.
description
}
)"
)
end
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
:
classification
=
$evm
.
vmdb
(
:classification
)
.
find_by_name
(
'cost_center'
)
cost_center_tags
=
{}
$evm
.
vmdb
(
:classification
)
.
find_all_by_parent_id
(
classification
.
id
)
.
each
do
|
tag
|
cost_center_tags
[
tag
.
name
]
=
tag
.
description
end
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 look up 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" = 44 LIMIT 1 Tag Inst Including Associations (0.1ms - 1rows) => "syseng"
We can just search for the tag object that matches a given category/tag, as follows:
tag
=
$evm
.
vmdb
(
:classification
)
.
find_by_name
(
'department/hr'
)
Anything returned from $evm.vmdb(:classification)
is a MiqAeServiceClassification
object, not a text string.
With CloudForms 4.0 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
(
"
#{
@user
}
:
#{
@pass
}
"
)
}
"
}
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'
]
@user
=
$evm
.
object
[
'username'
]
@pass
=
$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
unless
err
.
response
.
nil?
$evm
.
log
(
:error
,
"REST request failed, code:
#{
err
.
response
.
code
}
"
)
$evm
.
log
(
:error
,
"Response body:
#{
err
.
response
.
body
.
inspect
}
"
)
end
exit
MIQ_STOP
rescue
=>
err
$evm
.
log
(
:error
,
"[
#{
err
}
]
#{
err
.
backtrace
.
join
(
"
"
)
}
"
)
exit
MIQ_STOP
end
In this example we define a generic method called rest_action
that uses the Ruby rest-client
gem to handle the RESTful connection. We extract the CloudForms server’s credentials from the instance schema just as we did in Chapter 4, and we retrieve the service model of the tag category that we wish to delete, to get its ID.
Finally, we make a RESTful DELETE
call to the /api/categories URI, specifying the tag category ID to be deleted.
3.145.109.234