cloudstack

Use any binary as Cloudstack transformation hook
Edited: Tuesday 12 May 2026

cloudstack python

What is a transformation hook and why is it needed

Sometimes you need to customize the Libvirt domain XML that Cloudstack uses for virtual machines it launches (for example, you want to pass-through some PCI devices, or add some metadata, or change some other hardcoded values).

In this case you can use a transformation hook script on the virtualization host. It works like this in the nutshell:

  1. Cloudstack renders an XML definition of the future Libvirt domain.
  2. It executes some script and passes this XML to it.
  3. The script returns modified XML to Cloudstack.
  4. Cloudstack launches a virtual machine using the modified XML.

Typically, those scripts has been written in Groovy language - Cloudstack supported it since long time ago.

However, this commit silently added “shell” transformer, which in fact can be any executable. As the result, you don’t have to struggle with the Groovy anymore.

How does it work

Cloudstack will call your script of choice with two command-line arguments:

  1. Method name (transform seems to be the single option here)
  2. The full multi-line XML of the Libvirt domain (yes! a multi-line argument value with spaces and other symbols. Python seems to have no trouble with it, though).
  3. Cloudstack expects the script to exit with zero status, and will read the modified XML from the standard output. If the script exists with non-zero status, then Cloudstack will use the original XML.

How to use it

Edit your agent.properties file, and add these statements:

agent.hooks.basedir=/etc/cloudstack/agent/hooks
agent.hooks.libvirt_vm_xml_transformer.shell_script=<your script filename>

Place your transformer script into the /etc/cloudstack/agent/hooks directory. In my case, it was in Python, a script that adds more Hyper-V enlightenments to Windows VMs, and disables HPET (which eats extra CPU on the host for some reason):

 1#!/usr/bin/env python3
 2import sys
 3from xml.etree import ElementTree
 4
 5def enable_all_hyperv_enlightments(xmlroot):
 6    """
 7    Finds a features->hyperv tag in the XML.
 8    If found, adds more enlightenments to it,
 9    and modifies the <clock> tag to remote HPET, and add hypervclock
10    """
11    hv = xmlroot.find('./features/hyperv')
12    if hv is None:
13        return
14    
15    # Remove children elements
16    hv.clear()
17    hv.set('mode', 'custom')
18    
19    ElementTree.SubElement(hv, 'relaxed').set('state', 'on')
20    ElementTree.SubElement(hv, 'vapic').set('state', 'on')
21    spinlocks = ElementTree.SubElement(hv, 'spinlocks')
22    spinlocks.set('state', 'on')
23    spinlocks.set('retries', '8191')
24    ElementTree.SubElement(hv, 'synic').set('state', 'on')
25    ElementTree.SubElement(hv, 'stimer').set('state', 'on')
26    ElementTree.SubElement(hv, 'frequencies').set('state', 'on')
27    ElementTree.SubElement(hv, 'vpindex').set('state', 'on')
28    ElementTree.SubElement(hv, 'ipi').set('state', 'on')
29    
30    # Only appears in Libvirt 10.7.0+ and qemu 7.1+
31    # ElementTree.SubElement(hv, 'xmm_input').set('state', 'on')
32
33    clock = xmlroot.find('./clock')
34    clock.clear()
35    clock.set('offset', 'localtime')
36
37    hvtimer = ElementTree.SubElement(clock, 'timer')
38    hvtimer.set('name', 'hypervclock')
39    hvtimer.set('present', 'yes')
40
41    rtc = ElementTree.SubElement(clock, 'timer')
42    rtc.set('name', 'rtc')
43    rtc.set('tickpolicy', 'catchup')
44    
45    pit = ElementTree.SubElement(clock, 'timer')
46    pit.set('name', 'pit')
47    pit.set('tickpolicy', 'delay')
48
49    hpet = ElementTree.SubElement(clock, 'timer')
50    hpet.set('name', 'hpet')
51    hpet.set('present', 'no')
52
53try:
54    method = sys.argv[1]
55    data = sys.argv[2]
56
57    # Only work with 'transform' method
58    if method != 'transform':
59        raise Exception(f"wrong method supplied: {method}")
60except Exception as e:
61    sys.stderr.write(f"Got exception: {e}\n")
62    sys.exit(1)
63
64root = ElementTree.fromstring(data)
65enable_all_hyperv_enlightments(root)
66
67# return the modified XML
68print(ElementTree.tostring(root).decode())