diff --git a/README.md b/README.md
index af61fcf..817e769 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,10 @@ Requirements
The host should have Virtualization Technology (VT) enabled and should
be preconfigured with libvirt/KVM.
+`genisoimage` is required for cloud-init support.
+
+`swtpm` and `swtpm-tools` packages are required for TPM support.
+
Role Variables
--------------
@@ -61,6 +65,9 @@ Role Variables
This gets mapped to the `trustGuestRxFilters` attribute of VM interfaces.
Default is `false`
+- `libvirt_vm_cloud_init_dir`: Directory in which cloud-init files are
+ stored. Default is '{{ libvirt_volume_default_images_path }}/cloud-init'.
+
- `libvirt_vms`: list of VMs to be created/destroyed. Each one may have the
following attributes:
@@ -172,10 +179,16 @@ Role Variables
interfaces. Default is `libvirt_vm_trust_guest_rx_filters`.
- `model`: The name of the interface model. Eg. `e1000` or `ne2k_pci`, if undefined
it defaults to `virtio`.
- - `alias`: An optional interface alias. This can be used to tie specific network
- configuration to persistent network devices via name. The user defined alias is
- always prefixed with `ua-` to be compliant (aliases without `ua-` are ignored by libvirt.
- If undefined it defaults to libvirt managed `vnetX`.
+ - `alias`: An optional interface alias. When cloud-init is enabled, this is required
+ and used as the interface name in the guest. The alias is
+ always prefixed with `ua-` to be compliant (aliases without `ua-` are ignored by libvirt).
+ If undefined it defaults to libvirt managed `vnetX`.
+ - `address`: Optional static IP address in CIDR notation (e.g., "192.168.1.100/24").
+ Requires cloud-init to be enabled.
+ - `gateway`: Optional gateway IP address for the interface.
+ Requires cloud-init to be enabled.
+ - `nameservers`: Optional list of DNS nameservers.
+ Requires cloud-init to be enabled.
- `console_log_enabled`: if `true`, log console output to a file at the
path specified by `console_log_path`, **instead of** to a PTY. If
`false`, direct terminal output to a PTY at serial port 0. Default is
@@ -192,6 +205,21 @@ Role Variables
- `boot_firmware`: Can be one of: `bios`, or `efi`. Defaults to `bios`.
+ - `tpm_enabled`: Whether to enable TPM for this VM. Default is `false`.
+
+ - `tpm_version`: TPM version to use. Can be '1.2' or '2.0'. Default is '2.0'.
+
+ - `cloud_init_enabled`: Whether to enable cloud-init for this VM. Default is `false`.
+
+ - `cloud_init_user_data`: Cloud-init user configuration in YAML format.
+ You can find examples of the format here: [Cloud-init User Data Examples](https://docs.cloud-init.io/en/latest/reference/examples.html)
+
+ - `cloud_init_meta_data`: Optional cloud-init metadata in YAML format.
+
+ - `cloud_init_network_config`: Optional custom network configuration in YAML format.
+ If not specified, network configuration will be generated from interface settings.
+ You can find examples of the format here: [Cloud-init Network Configuration](https://docs.cloud-init.io/en/latest/reference/network-config-format-v2.html)
+
- `xml_file`: Optionally supply a modified XML template. Base customisation
off the default `vm.xml.j2` template so as to include the expected jinja
expressions the role uses.
@@ -274,14 +302,35 @@ Example Playbook
type: 'file'
file_path: '/srv/cloud/images'
capacity: '900GB'
+ tpm_enabled: true
+ tpm_version: "2.0"
+ cloud_init_enabled: true
+ cloud_init_user_data:
+ users:
+ - name: myuser
+ sudo: ALL=(ALL) NOPASSWD:ALL
+ ssh_authorized_keys:
+ - ssh-rsa AAAAB...
+ cloud_init_meta_data:
+ foo: bar
interfaces:
- type: 'direct'
source:
dev: 'eth123'
mode: 'private'
+ mac: '00:11:22:33:44:55'
+ alias: 'eth0'
+ address: '192.168.122.10/24'
+ gateway: '192.168.122.1'
+ nameservers:
+ - '8.8.8.8'
+ - '8.8.4.4'
- type: 'bridge'
source:
dev: 'br-datacentre'
+ mac: '00:11:22:33:44:56'
+ alias: 'eth1'
+ # This interface will use dhcp to get an ip address
Author Information
diff --git a/defaults/main.yml b/defaults/main.yml
index c676c07..83f14cd 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -102,6 +102,9 @@ libvirt_vm_sudo: true
# Default CPU mode if libvirt_vm_cpu_mode or vm.cpu_mode is undefined
libvirt_cpu_mode_default: "{{ 'host-passthrough' if libvirt_vm_engine == 'kvm' else 'host-model' }}"
+# Path where cloud-init files will be stored
+libvirt_vm_cloud_init_dir: "{{ libvirt_volume_default_images_path }}/cloud-init"
+
### DEPRECATED ###
# Use the above settings for each item within `libvirt_vms`, instead of the
# below deprecated variables.
diff --git a/tasks/check-interface.yml b/tasks/check-interface.yml
index d5dc19d..bd0ef0c 100644
--- a/tasks/check-interface.yml
+++ b/tasks/check-interface.yml
@@ -19,3 +19,15 @@
- interface.type == 'direct'
- interface.source is not defined or
interface.source.dev is not defined
+
+- name: Validate network configuration requirements
+ ansible.builtin.fail:
+ msg: >
+ {% if vm.cloud_init_enabled | default(false) | bool and not (interface.alias is defined and interface.mac is defined) %}
+ When cloud-init is enabled, both 'alias' and 'mac' must be defined for all interfaces
+ {% elif not (vm.cloud_init_enabled | default(false) | bool) and (interface.address is defined or interface.gateway is defined or interface.nameservers is defined) %}
+ Cloud-init must be enabled when configuring network settings (address, gateway, or nameservers)
+ {% endif %}
+ when:
+ - vm.cloud_init_enabled | default(false) | bool and not (interface.alias is defined and interface.mac is defined) or
+ not (vm.cloud_init_enabled | default(false) | bool) and (interface.address is defined or interface.gateway is defined or interface.nameservers is defined)
\ No newline at end of file
diff --git a/tasks/cloud-init.yml b/tasks/cloud-init.yml
new file mode 100644
index 0000000..9e7b822
--- /dev/null
+++ b/tasks/cloud-init.yml
@@ -0,0 +1,57 @@
+---
+# Tasks for generating cloud-init configuration and ISO
+
+- name: Get VM UUID
+ ansible.builtin.command:
+ cmd: virsh -q domuuid {{ vm.name }}
+ register: vm_uuid
+ changed_when: false
+ failed_when: false
+
+- name: Ensure VM-specific cloud-init directory exists
+ ansible.builtin.file:
+ path: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}"
+ state: directory
+ mode: '0755'
+ become: "{{ libvirt_vm_sudo }}"
+
+- name: Generate cloud-init meta-data file
+ ansible.builtin.template:
+ src: cloud-init/meta-data.j2
+ dest: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/meta-data"
+ mode: '0644'
+ become: "{{ libvirt_vm_sudo }}"
+ register: meta_data_task
+
+- name: Generate cloud-init user-data file
+ ansible.builtin.template:
+ src: cloud-init/user-data.j2
+ dest: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/user-data"
+ mode: '0644'
+ become: "{{ libvirt_vm_sudo }}"
+ register: user_data_task
+
+- name: Generate cloud-init network-config file
+ ansible.builtin.template:
+ src: cloud-init/network-config.j2
+ dest: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/network-config"
+ mode: '0644'
+ become: "{{ libvirt_vm_sudo }}"
+ register: network_config_task
+
+- name: Check if cloud-init ISO exists
+ ansible.builtin.stat:
+ path: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/cloud-init.iso"
+ become: "{{ libvirt_vm_sudo }}"
+ register: cloud_init_iso_exists
+
+- name: Generate cloud-init ISO
+ ansible.builtin.command:
+ cmd: >-
+ genisoimage -output {{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/cloud-init.iso
+ -volid cidata -joliet -rock -input-charset utf-8
+ {{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/meta-data
+ {{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/user-data
+ {{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}/network-config
+ become: "{{ libvirt_vm_sudo }}"
+ when: meta_data_task.changed or user_data_task.changed or network_config_task.changed or not cloud_init_iso_exists.stat.exists
\ No newline at end of file
diff --git a/tasks/destroy-vm.yml b/tasks/destroy-vm.yml
index 3214c08..faed9e8 100644
--- a/tasks/destroy-vm.yml
+++ b/tasks/destroy-vm.yml
@@ -9,7 +9,7 @@
register: result
become: true
-- name: Destory the VM is existing
+- name: Destroy the VM if existing
block:
- name: Ensure the VM is absent
community.libvirt.virt:
@@ -33,4 +33,10 @@
{{ vm.name }}
become: true
changed_when: true
+
+ - name: Remove cloud-init directory
+ ansible.builtin.file:
+ path: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}"
+ state: absent
+ become: true
when: vm.name in result.list_vms
diff --git a/tasks/main.yml b/tasks/main.yml
index f9dd247..22188d1 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -61,6 +61,19 @@
loop_var: vm
when: (vm.state | default('present', true)) == 'present'
+- name: Remove cloud-init directory if disabled
+ ansible.builtin.file:
+ path: "{{ libvirt_vm_cloud_init_dir }}/{{ vm.name }}"
+ state: absent
+ mode: '0755'
+ become: "{{ libvirt_vm_sudo }}"
+ with_items: "{{ libvirt_vms }}"
+ loop_control:
+ loop_var: vm
+ when: >-
+ not (vm.cloud_init_enabled | default(false) | bool)
+ and (vm.state | default('present', true)) == 'present'
+
- include_tasks: destroy-vm.yml
vars:
boot_firmware: "{{ vm.boot_firmware | default('bios', true) | lower }}"
diff --git a/tasks/vm.yml b/tasks/vm.yml
index 8163c2f..7b141ad 100644
--- a/tasks/vm.yml
+++ b/tasks/vm.yml
@@ -29,6 +29,10 @@
uri: "{{ libvirt_vm_uri | default(omit, true) }}"
become: "{{ libvirt_vm_sudo }}"
+- name: Include cloud-init tasks
+ ansible.builtin.include_tasks: cloud-init.yml
+ when: vm.cloud_init_enabled | default(false) | bool
+
- name: Ensure the VM is running and started at boot
community.libvirt.virt:
name: "{{ vm.name }}"
diff --git a/templates/cloud-init/meta-data.j2 b/templates/cloud-init/meta-data.j2
new file mode 100644
index 0000000..666c13c
--- /dev/null
+++ b/templates/cloud-init/meta-data.j2
@@ -0,0 +1,7 @@
+{% if vm_uuid.rc == 0 %}
+instance-id: {{ vm_uuid.stdout }}
+{% else %}
+instance-id: {{ vm.name }}
+{% endif %}
+local-hostname: {{ vm.name }}
+{{ vm.cloud_init_meta_data | default({}) | to_nice_yaml(indent=2) }}
diff --git a/templates/cloud-init/network-config.j2 b/templates/cloud-init/network-config.j2
new file mode 100644
index 0000000..6d8db01
--- /dev/null
+++ b/templates/cloud-init/network-config.j2
@@ -0,0 +1,32 @@
+version: 2
+{% if vm.cloud_init_network_config is defined %}
+{{ vm.cloud_init_network_config | to_nice_yaml(indent=2) }}
+{% else %}
+ethernets:
+{% for interface in interfaces %}
+{% if interface.alias is defined %}
+ {{ interface.alias }}:
+ match:
+ macaddress: "{{ interface.mac }}"
+ set-name: {{ interface.alias }}
+{% if interface.address is defined %}
+ addresses:
+ - {{ interface.address }}
+{% if interface.gateway is defined %}
+ routes:
+ - to: default
+ via: {{ interface.gateway }}
+{% endif %}
+{% if interface.nameservers is defined %}
+ nameservers:
+ addresses: {{ interface.nameservers }}
+{% endif %}
+{% else %}
+ dhcp4: true
+{% endif %}
+{% else %}
+ eth{{ loop.index0 }}:
+ dhcp4: true
+{% endif %}
+{% endfor %}
+{% endif %}
\ No newline at end of file
diff --git a/templates/cloud-init/user-data.j2 b/templates/cloud-init/user-data.j2
new file mode 100644
index 0000000..c2db35a
--- /dev/null
+++ b/templates/cloud-init/user-data.j2
@@ -0,0 +1,2 @@
+#cloud-config
+{{ vm.cloud_init_user_data | default({}) | to_nice_yaml(indent=2) }}
diff --git a/templates/vm.xml.j2 b/templates/vm.xml.j2
index 93688b0..71c8d8c 100644
--- a/templates/vm.xml.j2
+++ b/templates/vm.xml.j2
@@ -65,12 +65,20 @@
{% endif %}
{% if volume.target is undefined %}
-
+
{% else %}
{% endif %}
{% endfor %}
+{% if vm.cloud_init_enabled | default(false) | bool %}
+
+
+
+
+
+
+{% endif %}
{% for interface in interfaces %}
{% if interface.type is defined and interface.type == 'direct' %}
@@ -137,6 +145,11 @@
{% endfor %}
+ {% if vm.tpm_enabled | default(false) | bool %}
+
+
+
+ {% endif %}
/dev/urandom