In Chapter 21 we saw how the VM provision state machine was designed to be customizable. In this chapter we’ll go through the steps involved in copying and extending the state machine to add a second hard disk to the virtual machine. This is a simple example but a typical real-world requirement.
We are using an RHEV provider with our CloudForms installation, and we can successfully provision virtual machines using the Native Clone provision type from fully configured RHEV templates. The templates all have a single 30 GB thin-provisioned hard drive.
We would like all virtual machines provisioned from these templates to have a second 30 GB hard drive added automatically during provisioning. The second drive should be created in the same RHEV storage domain as the first drive (i.e., not hardcoded to a storage domain).
Edit the VMProvision_VM state machine to add two new states to perform the task. We’ll add the second disk using the RHEV RESTful API, using credentials stored for the provider. We can achieve this in a series of steps.
We’re going to extend the VM provisioning state machine by adding states, but we cannot do this to the state machine in the locked ManageIQ domain.
The first thing that we must do is copy the ManageIQ/Infrastructure/VM/Provisioning/StateMachines/VMProvision_VM/Provision VM from Template (template) state machine instance into our own ACME domain so that we can edit the schema.
Now we edit the schema of the copied class (see Figure 22-1).
We add two more steps, AddDisk
and StartVM
, to the bottom of the schema (see Figure 22-2).
Now we adjust the class schema sequence so that our new states come after PostProvision
(see Figure 22-3).
We’re going to override the default behavior of the VM provisioning workflow, which is to autostart a VM after provisioning. We do this because we want to add our new disk with the VM powered off, and then power on the VM ourselves afterward.
We copy the /Infrastructure/VM/Provisioning/StateMachines/Methods/redhat_CustomizeRequest method from the RedHat domain into ours (see Figure 22-4).
The RedHat domain contains an enhanced version of redhat_CustomizeRequest. Make sure you copy and extend the RedHat version rather than the ManageIQ domain version.
We edit redhat_CustomizeRequest to set the options hash key :vm_auto_start
to be false
. We must do this after the line:
prov
=
$evm
.
root
[
"miq_provision"
]
The additional lines are as follows:
# Get provisioning object
prov
=
$evm
.
root
[
"miq_provision"
]
#### Add the following lines
# Set the autostart parameter to false so that RHEV won't start the VM directly
$evm
.
log
(
:info
,
"Setting vm_auto_start to false"
)
prov
.
set_option
(
:vm_auto_start
,
[
false
,
0
]
)
#### End of additional lines
We’ll create a new namespace, Integration/RedHat, in our own domain, and create a simple one-field Methods class as we did in Chapter 3. We add two new instances, AddDisk and StartVM, and two new methods, add_disk and start_vm, to this class (see Figure 22-5).
Next we’ll examine the interesting parts of the code in each of the methods.
add_disk defines its own method, call_rhev, that handles the REST communication with the Red Hat Enterprise Virtualizaton Manager:
def
call_rhev
(
servername
,
username
,
password
,
action
,
ref
=
nil
,
body_type
=
:xml
,
body
=
nil
)
#
# If ref is a url then use that one instead
#
unless
ref
.
nil?
url
=
ref
if
ref
.
include?
(
'http'
)
end
url
||=
"https://
#{
servername
}#{
ref
}
"
params
=
{
:method
=>
action
,
:url
=>
url
,
:user
=>
username
,
:password
=>
password
,
:headers
=>
{
:content_type
=>
body_type
,
:accept
=>
:xml
},
:verify_ssl
=>
false
}
params
[
:payload
]
=
body
if
body
rest_response
=
RestClient
:
:Request
.
new
(
params
)
.
execute
#
# RestClient raises an exception for us on any non-200 error
#
return
rest_response
end
In the main section of code we account for the fact that we’re allowing add_disk to be callable in either of two ways: from a button on a virtual machine in the WebUI, or as part of the VM provision workflow (see Chapter 11). We first need to find out how add_disk has been called and retrieve the virtual machine service model object accordingly.
We also need to determine the new disk size. If add_disk has been called from a button, the new disk size will have been passed as a service dialog element. If it’s called as part of a VM provisioning operation, we’ll hardcode this as the NEW_DISK_SIZE
constant (for this example it’s 30 GB):
case
$evm
.
root
[
'vmdb_object_type'
]
when
'miq_provision'
# called from a VM provision workflow
vm
=
$evm
.
root
[
'miq_provision'
].
destination
disk_size_bytes
=
NEW_DISK_SIZE
*
1024
**
3
when
'vm'
vm
=
$evm
.
root
[
'vm'
]
# called from a button
disk_size_bytes
=
$evm
.
root
[
'dialog_disk_size_gb'
].
to_i
*
1024
**
3
end
We’re going to create the new disk on the same storage domain as the existing first disk, so we need to find the existing storage domain details:
storage_id
=
vm
.
storage_id
rescue
nil
#
# Extract the RHEV-specific Storage Domain ID
#
unless
storage_id
.
nil?
||
storage_id
.
blank?
storage
=
$evm
.
vmdb
(
'storage'
)
.
find_by_id
(
storage_id
)
storage_domain_id
=
storage
.
ems_ref
.
match
(
/.*/(w.*)$/
)
[
1
]
end
Next we extract the credentials of the RHEV Manager (from the ext_management_system
object), as we’ll need to use these when we make the REST call. We also build our XML payload using the Nokogiri
gem:
unless
storage_domain_id
.
nil?
#
# Extract the IP address and credentials for the RHEV provider
#
servername
=
vm
.
ext_management_system
.
ipaddress
||
vm
.
ext_management_system
.
hostname
username
=
vm
.
ext_management_system
.
authentication_userid
password
=
vm
.
ext_management_system
.
authentication_password
builder
=
Nokogiri
:
:XML
::
Builder
.
new
do
|
xml
|
xml
.
disk
{
xml
.
storage_domains
{
xml
.
storage_domain
:id
=>
storage_domain_id
}
xml
.
size
disk_size_bytes
xml
.
type
'system'
xml
.
interface
'virtio'
xml
.
format
'cow'
xml
.
bootable
'false'
}
end
body
=
builder
.
to_xml
We make the REST call to the RHEV Manager and parse the response:
$evm
.
log
(
:info
,
"Adding
#{
disk_size_bytes
/
1024
**
3
}
GByte disk to VM:
#{
vm
.
name
}
"
)
response
=
call_rhev
(
servername
,
username
,
password
,
:post
,
"
#{
vm
.
ems_ref
}
/disks"
,
:xml
,
body
)
#
# Parse the response body XML
#
doc
=
Nokogiri
:
:XML
.
parse
(
response
.
body
)
The initial response back from the API contains some href
s that we need to use, so we extract those:
#
# Pull out some reusable hrefs from the initial response
#
disk_href
=
doc
.
at_xpath
(
"/disk"
)
[
'href'
]
creation_status_href
=
doc
.
at_xpath
(
"/disk/link[@rel='creation_status']"
)
[
'href'
]
activate_href
=
doc
.
at_xpath
(
"/disk/actions/link[@rel='activate']"
)
[
'href'
]
We poll the API for the completion status:
It’s not good practice to sleep
in an Automate method. For simplicity in this example, we’re handling the sleep → retry counter logic ourselves to avoid the possibility of sleeping forever. In a production environment we’d use the built-in state machine retry logic to handle this for us.
#
# Validate the creation_status (wait for up to a minute)
#
creation_status
=
doc
.
at_xpath
(
"/disk/creation_status/state"
)
.
text
counter
=
13
while
creation_status
!=
"complete"
counter
-=
1
if
counter
==
0
raise
"Timeout waiting for new disk creation_status to reach
"
complete
"
: Creation Status =
#{
creation_status
}
"
else
sleep
5
response
=
call_rhev
(
servername
,
username
,
password
,
:get
,
creation_status_href
,
:xml
,
nil
)
doc
=
Nokogiri
:
:XML
.
parse
(
response
.
body
)
creation_status
=
doc
.
at_xpath
(
"/creation/status/state"
)
.
text
end
end
If the disk has been attached to a powered-on VM (as it may have been if the method is called from a button), we would need to activate the disk in RHEV. If the VM is powered off when the disk is added, this stage is unnecessary:
#
# Disk has been created successfully,
# now check its activation status and if necessary activate it
#
response
=
call_rhev
(
servername
,
username
,
password
,
:get
,
disk_href
,
:xml
,
nil
)
doc
=
Nokogiri
:
:XML
.
parse
(
response
.
body
)
if
doc
.
at_xpath
(
"/disk/active"
)
.
text
!=
"true"
$evm
.
log
(
:info
,
"Activating disk"
)
body
=
"<action/>"
response
=
call_rhev
(
servername
,
username
,
password
,
:post
,
activate_href
,
:xml
,
body
)
else
$evm
.
log
(
:info
,
"New disk already active"
)
end
end
#
# Exit method
#
$evm
.
root
[
'ae_result'
]
=
'ok'
exit
MIQ_OK
The code for start_vm is as follows:
begin
vm
=
$evm
.
root
[
'miq_provision'
].
destination
$evm
.
log
(
:info
,
"Current VM power state =
#{
vm
.
power_state
}
"
)
unless
vm
.
power_state
==
'on'
vm
.
start
vm
.
refresh
$evm
.
root
[
'ae_result'
]
=
'retry'
$evm
.
root
[
'ae_retry_interval'
]
=
'30.seconds'
else
$evm
.
root
[
'ae_result'
]
=
'ok'
end
rescue
=>
err
$evm
.
log
(
:error
,
"[
#{
err
}
]
#{
err
.
backtrace
.
join
(
"
"
)
}
"
)
$evm
.
root
[
'ae_result'
]
=
'error'
end
The full scripts are also available from GitHub.
Now we edit our copied Provision VM from Template
state machine instance to add the AddDisk and StartVM instance URIs to the appropriate steps (see Figure 22-6).
We’ll provision a VM to test this. We should see that the VM is not immediately started after creation, and suitable messages in automation.log show that our additional methods are working:
...<AEMethod add_disk> Adding 30GB disk to VM: rhel7srv006 ...<AEMethod add_disk> Creation Status: pending ...<AEMethod add_disk> Creation Status: complete ...<AEMethod add_disk> New disk already active ... ...<AEMethod start_vm> Current VM power state = off ...<AEMethod start_vm> Current VM power state = unknown ...<AEMethod start_vm> Current VM power state = on
We can take a look at the number of disks in the virtual machine details page in the CloudForms WebUI (see Figure 22-7).
Here we see the second disk attached to the virtual machine. Our modified VM provisioning workflow has been successful.
This chapter has shown how we can extend the provisioning state machine to add our own workflow stages. Although this has been a simple example, some kind of provisioning workflow extension is very common in practice. We see another example in Chapter 28 where we extend the workflow to register our newly provisioned virtual machine with a Satellite 6 server.
The example has also shown the integration functionality of CloudForms, and how we can use API calls—in this case, using the REST client—to extend our workflows into the wider enterprise.
3.137.167.195