VirtOps #3: Ansible with Xen Orchestra

Drive your whole Xen stack with infrastructure-as-code, by combining superpowers of Ansible and Xen Orchestra!

VirtOps #3: Ansible with Xen Orchestra

With the release of Ansible Community 4.1.0 came a new inventory plugin for Xen Orchestra. This plugin allows the listing and grouping of XOA virtual machines, hosts and pools. In this article we will see how to install and use it.

Ansible: a quick intro

Ansible is an open-source software provisioning, configuration management, and application-deployment tool enabling infrastructure as code.

It helps to manage multiple machines by selecting portions of Ansible's inventory stored in simple ASCII text files. The inventory is configurable, and target machine inventory can be sourced dynamically or from cloud-based sources in different formats (YAML, INI).

If you want to learn more about it, check the Wikipedia page, or the official website.

Ansible Collections

Collections are a distribution format for Ansible content that can include playbooks, roles, modules, and plugins.

Ansible + Xen Orchestra = 💖

We decided to add Ansible capabilities on top of Xen Orchestra for the same reasons we created a Terraform plugin for it: XO is acting as a real central point -or middleware- for your entire infrastructure.

One Xen Orchestra to rule all your hosts!

At some point, that's the same architecture choice VMware did with vCenter.

Xen Orchestra Inventory plugin

This package is not included in ansible-core, you will have to install it using ansible-galaxy:

$ ansible-galaxy collection install community.general

Let's start by creating the plugin configuration file, its filename must end with xen_orchestra.yml or xen_orchestra.yaml:

plugin: community.general.xen_orchestra
api_host: 192.168.1.255
user: xo
password: xo_pwd
my.xen_orchestra.yml

You can now explore our inventory:

ansible-inventory -i my.xen_orchestra.yml --list 
{
    "_meta": {
        "hostvars": {
            "099c01b4-6e21-4466-a6c8-037d41d36d7c": {
                "address": "192.168.1.11",
                "cpus": {
                    "cores": 16,
                    "sockets": 1
                },
                "enabled": true,
                "hostname": "r740",
                "memory": {
                    "size": 34287759360,
                    "usage": 7765843968
                },
                "power_state": "running",
                "product_brand": "XCP-ng",
                "tags": [],
                "type": "host",
                "version": "8.2.0"
            },
            "df4577f2-efa2-685f-af98-e82da3050dc0": {
                "ansible_host": "192.168.1.12",
                "cpus": 1,
                "has_ip": true,
                "ip": "192.168.1.12",
                "is_managed": true,
                "memory": 2147463168,
                "name_label": "Test machine",
                "os_version": {
                    "distro": "ubuntu",
                    "major": "20",
                    "minor": "04",
                    "name": "Ubuntu 20.04.3 LTS",
                    "uname": "5.4.0-90-generic"
                },
                "power_state": "running",
                "tags": [],
                "type": "VM",
                "uuid": "df4577f2-efa2-685f-af98-e82da3050dc0"
            }
    	}
    },
    "all": {
        "children": [
            "halted",
            "paused",
            "running",
            "suspended",
            "ungrouped",
            "with_ip",
            "xo_host_r740",
            "xo_hosts",
            "xo_pool_xcp_ng_main",
            "xo_pools"
        ]
    },
    "running": {
        "hosts": [
            "df4577f2-efa2-685f-af98-e82da3050dc0"
        ]
    },
    "with_ip": {
        "hosts": [
            "df4577f2-efa2-685f-af98-e82da3050dc0"
        ]
    },
    "xo_host_r740": {
        "hosts": [
            "df4577f2-efa2-685f-af98-e82da3050dc0"
        ]
    },
    "xo_hosts": {
        "hosts": [
            "099c01b4-6e21-4466-a6c8-037d41d36d7c"
        ]
    },
    "xo_pool_xcp_ng_main": {
        "hosts": [
            "099c01b4-6e21-4466-a6c8-037d41d36d7c",
            "df4577f2-efa2-685f-af98-e82da3050dc0"
        ]
    }
}

This is the inventory generated from with one Virtual Machine named "Test Machine" hosted on "R740" in the "XCP Ng Main" Pool.

Let's break it down!

  • halted, running, paused and suspended group Virtual Machines by their power states
  • with_ip and without_ip group Virtual Machines by our ability to get an IP address from them. If we cannot find the IP (most likely because the guest-tools are not installed), then the Virtual Machine is added to the without_ip group.
  • xo_host_* will group Virtual Machines that are hosted on a given host
  • xo_hosts are the UUIDs of all the known hosts
  • xo_pool_* will group Virtual Machines that are hosted in a given pool

Going further

The default groups would allow you to ping every known running machines for example:

$ ansible running -i my.xen_orchestra.yml -m ping
df4577f2-efa2-685f-af98-e82da3050dc0 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

But we could also do something more useful, let's go back to our plugin configuration:

plugin: community.general.xen_orchestra
api_host: 192.168.1.255
user: xo
password: xo_pwd
groups:
  webservers: "'webserver' in tags"
my.xen_orchestra.yml

And add a webserver tag on our Virtual Machine

If we explore our inventory again:

$ ansible-inventory -i my.xen_orchestra.yml --list

We now have a webservers group

    ...
    "webservers": {
        "hosts": [
            "df4577f2-efa2-685f-af98-e82da3050dc0"
        ]
    },
    ...

Real world usage

Let's create a playbook and run it:

---
- name: Update web servers
  hosts: webservers
  remote_user: root

  tasks:
  - name: Ensure apache is at the latest version
    ansible.builtin.yum:
      name: httpd
      state: latest
  - name: Write the apache config file
    ansible.builtin.template:
      src: /srv/httpd.j2
      dest: /etc/httpd.conf
main.yml
$ ansible-playbook main.yml -i my.xen_orchestra.yml
PLAY [Update web servers]

TASK [Update web servers : Ensure apache is at the latest version] 
ok: [df4577f2-efa2-685f-af98-e82da3050dc0]

TASK [Update web servers : Write the apache config file] 
ok: [df4577f2-efa2-685f-af98-e82da3050dc0]

A small note

We use uuid as what Ansible calls host, this is because we cannot guarantee that all the Virtual Machines returned by the Xen Orchestra API will have an IP address, for example if guest-tools are not installed.

For Virtual Machines that do not have an known IP address, the value of the magic variable ansible_host will not be defined and therefore Ansible will try to use the value of the uuid variable to connect. This will fail.

For this reason, you may need to disable fact gathering with gather_facts: no.

Doing more

With that list of Virtual Machines, Hosts and Pools, you could imagine doing:

  • Applying updates to a list of outdated machines 🚨
  • Doing specific tasks on a group of distros 🧑‍🔧
  • Patching hosts 🩹
  • And many other things! 🚀

Finally, this initial inventory plugin is just a start. We'll add more Ansible-related features, through our great community input. On that, please share your feedback on our dedicated forum thread!