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