Using Hooks¶
The Hook subsystem enables the execution of custom scripts tied to a change in state in a particular resource, or API call. This opens a wide area of automation for system administrators to tailor their cloud infrastructures. It also features a logging mechanism that allows a convenient way to query the execution history or to retry the execution of a given hook.
Overview¶
The hook subsystem has two main components:
Hook Manager Driver: it receives information about every event triggered by OpenNebula service and publishes it to an event queue. Custom components can also use this queue to subscribe to OpenNebula events, more information here.
Hook Execution Manager (HEM): It registers itself to the events that triggers the hooks defined in the system. When an event is received it takes care of executing the corresponding hook command.
Both components are started together with the OpenNebula service. Note that, provided the network communication is secure, you can grant network access to the event queue and hence deploy HEM in a different server.
Configuration¶
Hook Manager¶
Hook Manager configuration is set in HM_MAD
section in /etc/one/oned.conf
. The configuration attributes are described below:
Parameter |
Description |
---|---|
Executable |
Path of the hook driver executable, can be an absolute path or relative to $ONE_LOCATION/lib/mads (or /usr/lib/one/mads/ if OpenNebula was installed in /) |
Arguments |
Arguments for the driver executable, the following values are supported:
|
Hook Execution Manager¶
Hook Execution Manager configuration is set in /etc/one/onehem-server.conf
:
Parameter |
Description |
---|---|
debug_level |
Set the log debug level shown in |
hook_base_path |
Base location to look for hook scripts when commands use a relative path (default value |
remote_hook_base_path |
Base location to look for hook scripts when commands use a relative path and |
subscriber_endpoint |
To subscribe for OpenNebula events, must match those in |
replier_endpoint |
To send hook execution results (reply to events) to oned, it must match those in |
concurrency |
Number of hooks executed simultaneously. |
Hook Log¶
You can check the execution results of a hook using the hook log. The hook log stores a record for each execution including the standard output and error of the command, as well as the arguments used. This information is used to retry the execution of a given record.
The number of execution log records stored in the database for each hook can be configured in oned.conf. For example, to keep only the last 10 execution records of each hook use LOG_RETENTION = 10
. This value can be tuned taking into account the number of hooks and how many times hooks are executed:
HOOK_LOG_CONF = [
LOG_RETENTION = 10 ]
Hook Types¶
There are two types of hooks, API hooks (triggered on API calls) and State hooks (triggered on state change). The main attributes for both types are described in the table below:
Option |
Mandatory | Description |
|
---|---|---|
NAME |
YES |
The name of the hook. |
COMMAND |
YES |
The command that will be executed when hook is triggered. Typically a path to a script. |
ARGUMENTS |
NO |
The arguments that will be passed to the script when the hook is triggered. |
ARGUMENTS_STDIN |
NO |
If |
TYPE |
YES |
The hook type either |
TIMEOUT |
NO |
Hook execution timeout. If not used the timeout is infinite. |
API Hooks¶
The API hooks are triggered after the execution of an API call. The specific attributes for API hooks are listed below:
Option |
Mandatory |
Description |
---|---|---|
CALL |
YES |
The API call which trigger the hook. For more info about API calls please check XML-RPC API section. |
For API hooks, the $API
keyword can be used in the ARGUMENTS
attribute to get the information about the API call. If $API
is used, a base64-encoded XML document containing the API call arguments and result will be passed to the hook command. The schema of the XML is defined here.
Note
If the API method defined in CALL
is an allocate
or delete
, the $API
document will also include the template of the corresponding resource.
The following example defines an API hook that executes the command /var/lib/one/remotes/hooks/log_new_user.rb
whenever a new user is created:
NAME = hook-API
TYPE = api
COMMAND = "log_new_user.rb"
ARGUMENTS = $API
CALL = "one.user.allocate"
ARGUMENTS_STDIN = yes
State Hooks¶
The state hooks are only available for Hosts and Virtual Machines and they are triggered on specific state transitions. The specific attributes to define state hooks are:
Option |
Mandatory |
Description |
---|---|---|
RESOURCE |
YES |
Type of the resource, supported values are |
REMOTE |
NO |
If |
STATE |
YES |
The state that triggers the hook. |
LCM_STATE |
YES |
The LCM state that triggers the hook (Only for VM hooks) |
ON |
YES |
For |
Warning
Note that ON
is mandatory for VM hooks, use ON=CUSTOM
with STATE
and LCM_STATE
to define hooks on specific state transitions.
For state hooks, $TEMPLATE
can be used in the ARGUMENTS
attribute to get the template (in XML format, base64 encoded) of the resource which triggered the hook. The XSD schema files for the XML document of each object are available here
The following examples define two state hooks for VMs, hosts and images:
# VM
NAME = hook-vm
TYPE = state
COMMAND = new_vm.rb
ARGUMENTS = $TEMPLATE
ON = PROLOG
RESOURCE = VM
# HOST
NAME = hook-host
TYPE = state
COMMAND = host-disabled.rb
STATE = DISABLED
RESOURCE = HOST
REMOTE = yes
# IMAGE
NAME = hook-image
TYPE = state
COMMAND = image-ready.rb
STATE = READY
RESOURCE = IMAGE
Note
Check each resource guide for info about Image states, VM states and Host states
Managing Hooks¶
Hooks can be managed via the CLI through the onehook
command and the API. This section describes the common operations to control the life-cycle of a hook using the CLI.
Creating Hooks¶
In order to create a new hook you need to create a hook template:
$ cat > hook.tmpl << EOF
NAME = hook-vm
TYPE = state
COMMAND = vm-pending.rb
ARGUMENTS = "\$TEMPLATE pending"
ON = CUSTOM
RESOURCE = VM
STATE = PENDING
LCM_STATE = LCM_INIT
EOF
Then, simply create the hook by running the following command:
$ onehook create hook.tmpl
ID: 0
We have just created a hook which will be triggered each time a VM switch to PENDING
state.
Note
Note that just one hook can be created for each trigger event.
Checking Hook Execution¶
We can check the execution records of a hook by accessing its detailed information. For example, to get the execution history of the previous hook use onehook show 0
:
$ onevm create --cpu 1 --memory 2 --name test
ID: 0
$ onehook show 0
HOOK 0 INFORMATION
ID : 0
NAME : hook-vm
TYPE : state
LOCK : None
HOOK TEMPLATE
ARGUMENTS="$TEMPLATE pending"
COMMAND="vm-pending.rb"
LCM_STATE="LCM_INIT"
REMOTE="NO"
RESOURCE="VM"
STATE="PENDING"
EXECUTION LOG
ID TIMESTAMP EXECUTION
0 09/23 15:10 ERROR (255)
We can see that there is an execution which have failed with error code 255. To get more information about a specific execution use the -e
option:
$ onehook show 0 -e 0
HOOK 0 INFORMATION
ID : 0
NAME : hook-vm
TYPE : state
LOCK : None
HOOK EXECUTION RECORD
EXECUTION ID : 0
TIMESTAMP : 09/23 15:10:38
COMMAND : /var/lib/one/remotes/hooks/vm-pending.rb PFZNPgogIDxJR...8+CjwvVk0+ pending
ARGUMENTS :
<VM>
<ID>0</ID>
<UID>0</UID>
<GID>0</GID>
<UNAME>oneadmin</UNAME>
<GNAME>oneadmin</GNAME>
<NAME>test</NAME>
<PERMISSIONS>
<OWNER_U>1</OWNER_U>
<OWNER_M>1</OWNER_M>
<OWNER_A>0</OWNER_A>
<GROUP_U>0</GROUP_U>
<GROUP_M>0</GROUP_M>
<GROUP_A>0</GROUP_A>
<OTHER_U>0</OTHER_U>
<OTHER_M>0</OTHER_M>
<OTHER_A>0</OTHER_A>
</PERMISSIONS>
<LAST_POLL>0</LAST_POLL>
<STATE>1</STATE>
<LCM_STATE>0</LCM_STATE>
<PREV_STATE>1</PREV_STATE>
<PREV_LCM_STATE>0</PREV_LCM_STATE>
<RESCHED>0</RESCHED>
<STIME>1569244238</STIME>
<ETIME>0</ETIME>
<DEPLOY_ID/>
<MONITORING/>
<TEMPLATE>
<AUTOMATIC_REQUIREMENTS><![CDATA[!(PUBLIC_CLOUD = YES) & !(PIN_POLICY = PINNED)]]></AUTOMATIC_REQUIREMENTS>
<CPU><![CDATA[1]]></CPU>
<MEMORY><![CDATA[2]]></MEMORY>
<VMID><![CDATA[0]]></VMID>
</TEMPLATE>
<USER_TEMPLATE/>
<HISTORY_RECORDS/>
</VM> pending
EXIT CODE : 255
EXECUTION STDOUT
EXECUTION STDERR
ERROR MESSAGE --8<------
Internal error No such file or directory - /var/lib/one/remotes/hooks/vm-pending.rb
ERROR MESSAGE ------>8--
The EXECUTION STDERR
message shows the reason for the hook execution failure, the script does not exists:
Internal error No such file or directory - /var/lib/one/remotes/hooks/vm-pending.rb
Important
The hook log can be queried and filtered by several criteria using onehook log
. More info about onehook log
command can be found running onehook log --help
.
Retrying Hook Executions¶
We are going to fix the previous error, let’s first create the vm-pending.rb
script, and then retry the hook execution.
Note
Note that any hook execution can be retried regardless of it result.
$ vim /var/lib/one/remotes/hooks/vm-pending.rb
#!/usr/bin/ruby
puts "Executed!"
$ chmod 760 /var/lib/one/remotes/hooks/vm-pending.rb
$ onehook retry 0 0
$ onehook show 0
HOOK 0 INFORMATION
ID : 0
NAME : hook-vm
TYPE : state
LOCK : None
HOOK TEMPLATE
ARGUMENTS="$TEMPLATE pending"
COMMAND="vm-pending.rb"
LCM_STATE="LCM_INIT"
REMOTE="NO"
RESOURCE="VM"
STATE="PENDING"
EXECUTION LOG
ID TIMESTAMP RC EXECUTION
0 09/23 15:10 255 ERROR
1 09/23 15:59 0 SUCCESS
Note the last successful execution record!
Important
When a hook execution is retried the same execution context is used, i.e. arguments and $TEMPLATE/$API values.
Developing Hooks¶
First thing you need to decide is the type of hook you are interested in, being it API or STATE hooks. Each type of hook is triggered by a different event and requires/provides different runtime information.
In this section you’ll find two simple script hooks (in ruby) for each type. This examples are good starting points for developing custom hooks.
Warning
Note that the scripts below import some dependencies that need to be installed properly in the front-end node. As most of the dependencies are OpenNebula dependencies these dependencies can be used by adding the snippet below to your scripts.
ONE_LOCATION = ENV['ONE_LOCATION']
if !ONE_LOCATION
RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
GEMS_LOCATION = '/usr/share/one/gems'
else
RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
GEMS_LOCATION = ONE_LOCATION + '/share/gems'
end
if File.directory?(GEMS_LOCATION)
Gem.use_paths(GEMS_LOCATION)
end
$LOAD_PATH << RUBY_LIB_LOCATION
API Hook example¶
This script prints to stdout the result of one.user.create API call and the username of new user.
# Hook template
#
# NAME = user-create
# TYPE = api
# COMMAND = "user_create.rb"
# ARGUMENTS = "$API"
# CALL = "one.user.allocate"
#!/usr/bin/ruby
require 'base64'
require 'nokogiri'
#api_info= Nokogiri::XML(Base64::decode64(STDIN.gets.chomp)) for reading from STDIN
api_info = Nokogiri::XML(Base64::decode64(ARGV[0]))
success = api_info.xpath("/CALL_INFO/RESULT").text.to_i == 1
uname = api_info.xpath('//PARAMETER[TYPE="IN" and POSITION=2]/VALUE').text
if !success
puts "Failing to create user"
exit -1
end
puts "User #{uname} successfully created"
State Hook example (HOST)¶
This script prints to stdout when an host is in error state.
# Hook template
#
#NAME = host-error
#TYPE = state
#COMMAND = host_error.rb
#ARGUMENTS="$TEMPLATE"
#STATE = ERROR
#RESOURCE = HOST
#!/usr/bin/ruby
require 'base64'
require 'nokogiri'
#host_template = Nokogiri::XML(Base64::decode64(STDIN.gets.chomp)) for reading from STDIN
host_template = Nokogiri::XML(Base64::decode64(ARGV[0]))
host_id = host_template.xpath("//ID").text.to_i
puts "Host #{host_id} is in error state!!"
State Hook example (VM)¶
This script prints to stdout if when a VM is in prolog state.
# Hook template
#
#NAME = vm-prolog
#TYPE = state
#COMMAND = vm_prolog.rb
#ARGUMENTS = $TEMPLATE
#ON = PROLOG
#RESOURCE = VM
#!/usr/bin/ruby
require 'base64'
require 'nokogiri'
#vm_template = Nokogiri::XML(Base64::decode64(STDIN.gets.chomp)) for reading from STDIN
vm_template = Nokogiri::XML(Base64::decode64(ARGV[0]))
vm_id = vm_template.xpath("//ID").text.to_i
puts "VM #{vm_id} is in PROLOG state"
State Hook example (IMAGE)¶
This script prints to stdout the ID of image when it is ready to use.
# Hook template
#
# NAME = image-ready
# TYPE = state
# COMMAND = image_ready.rb
# STATE = READY
# RESOURCE = IMAGE
# ARGUMENTS = "$TEMPLATE"
#!/usr/bin/ruby
require 'base64'
require 'nokogiri'
#img_template = Nokogiri::XML(Base64::decode64(STDIN.gets.chomp)) for reading from STDIN
img_template = Nokogiri::XML(Base64::decode64(ARGV[0]))
img_id = img_template.xpath("//ID").text.to_i
puts "Image #{img_id} ready to use!!"
A Complete Example: Autostart Hooks for KVM¶
OpenNebula creates transient KVM domains, i.e. they only exist while the domain is running. Therefore, the KVM autostart feature cannot be activated via libvirt configuration. This example shows how to implement VM autostart for the KVM hypervisor in OpenNebula using state hooks.
The hooks track Hosts reboots and resume VMs allocated to the Host that includes the AUTOSTART
attribute in their template. If AUTOSTART=yes
the VM will be automatically restarted if it was running before the host was rebooted, if AUTOSTART=always
the VM will be automatically restarted always regardless its previous state. This functionality is implemented by two state hooks: one for the HOST
and one for the VM
resource. The code for both hooks can be found in
/var/lib/one/remotes/hooks/autostart/
folder.
To install the hooks create a definition file for each one. The autostart-host.tmpl
definition file for the Host hook will be as follows:
NAME = autostart-host
TYPE = state
COMMAND = autostart/host
ARGUMENTS = \$TEMPLATE
ARGUMENTS_STDIN = yes
RESOURCE = HOST
STATE = MONITORED
Similarly for the VM hook, the autostart-vm.tmpl
definition file will be as follows:
NAME = autostart-vm
TYPE = state
COMMAND = autostart/vm
ARGUMENTS = \$TEMPLATE
ARGUMENTS_STDIN = yes
RESOURCE = VM
STATE = POWEROFF
LCM_STATE = LCM_INIT
ON = CUSTOM
Now you can create the hooks using these two files:
$ onehook create autostart-host.tmpl
$ onehook create autostart-vm.tmpl
Note
Note that any command can be specified in COMMAND
, for debugging (COMMAND="/usr/bin/echo"
) can be very helpful.
Hook Manager Events¶
The Hook Manager publish the OpenNebula events over a ZeroMQ publisher socket. This can be used for developing custom components that subscribe to specific events to perform custom actions.
Hook Manager Messages¶
The hook manager sends two different types of messages:
EVENT messages: represent an OpenNebula event, which potentially could trigger a hook if the Hook Execution Manager is subscribed to it.
RETRY messages: represent a retry action of a given hook execution. These event messages are specific to the Hook Execution Manager.
Both messages have a key that can be used by the subscriber to receive messages related to an specific event class; and a content that contains the information related to the event encoded in base64.
Event Messages Format¶
There are two different types of EVENT messages, representing API and state events. The key format for both types are listed below:
EVENT |
Key format |
---|---|
API |
|
STATE (HOST) |
|
STATE (VM) |
|
CHANGE (SERVICE) |
|
Keys are used to subscribe to specific events. Note also that you do not need to specify the whole key, form example EVENT STATE HOST/ERROR/
will subscribe for state changes to ERROR
on the hosts, while EVENT STATE HOST/
will subscribe for all state changes of the hosts.
The content of an event message is an XML document containing information related to the event, the XSD representing this content are available here.
Retry Messages Format¶
The key format of the retry messages is just the word RETRY
, no more info is needed in the key. The retry messages content is an XML file with information about the execution to retry. The XSD representing this content is available here.
Subscriber script example¶
Below there is an example of script which retrieve all the event messages and prints their key and their content.
#!/usr/bin/ruby
require 'ffi-rzmq'
require 'base64'
@context = ZMQ::Context.new(1)
@subscriber = @context.socket(ZMQ::SUB)
@subscriber.setsockopt(ZMQ::SUBSCRIBE, "EVENT")
@subscriber.connect("tcp://localhost:2101")
key = ''
content = ''
loop do
@subscriber.recv_string(key)
@subscriber.recv_string(content)
puts "Key: #{key}\nContent: #{Base64.decode64(content)}"
puts "\n===================================================================\n"
end
Note
Note that the key and the content as read separately as ZeroMQ envelopes are used.