From 4ace3e406baf235c0a1dc5d9e047278e4b5a2f0c Mon Sep 17 00:00:00 2001 From: brusq-RH <76945214+brusq-RH@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:00:31 +0200 Subject: [PATCH] up --- aap/playbooks/create-vm.yaml | 348 +++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 aap/playbooks/create-vm.yaml diff --git a/aap/playbooks/create-vm.yaml b/aap/playbooks/create-vm.yaml new file mode 100644 index 0000000..e24d46e --- /dev/null +++ b/aap/playbooks/create-vm.yaml @@ -0,0 +1,348 @@ +## +## Pre-requisites on the Ansible Controller +## +## $ dnf install -y ansible python3-jmespath python3-netaddr +## +- name: Create a VM with cloud-init and using an OCI artefact as disk source + hosts: rhde + gather_facts: false + ## + ## Pre-requisites on the managed node + ## + ## $ dnf install -y genisoimage + ## + vars: + ## + ## Default values + ## + libvirt_images_path: /var/lib/libvirt/images + libvirt_domains_debug: '{{ debug | default(True) | bool }}' + has_podman_artifact_extract: false + libvirt_domain_mac_address: "{{ '04:00:' ~ (libvirt_domain_parameters.ipv4_address | ansible.utils.ipv4('address') | ansible.utils.ip4_hex(':')) }}" + + ## + ## Parameters of the VM to create + ## + + libvirt_domain_parameters: + name: "{( vm_name )}" + ipv4_address: "{( vm_ip )}"/24 + ipv4_gateway: 192.168.122.1 + ipv4_nameserver: 192.168.122.1 + network: default + rhel_version: "{( rhel_version )}" + ram: "{( vm_ram )}" + vcpu: "{( vm_vcpu )}" + disk: "{( vm_disk )}" + architecture: x86_64 + + ## + ## Template of the VM to create + ## + + libvirt_domain: + name: '{{ libvirt_domain_parameters.name }}' + state: '{{ state | default("present") }}' + # Cloud-init sources + cloud_init: + templates: + - src: cloud-init/user-data.j2 + dest: user-data + - src: cloud-init/meta-data.j2 + dest: meta-data + - src: cloud-init/network-config.j2 + dest: network-config + # Root disk source + root_disk: + ## + ## HEADS UP ! The RHEL qcow2 is stored in an OCI registry as a Podman artifact + ## + ## $ podman artifact add edge-registry.itix.fr/demo-edge-retail/rhel9:x86_64 rhel-9.6-x86_64-kvm.qcow2 + ## $ podman artifact push edge-registry.itix.fr/demo-edge-retail/rhel9:x86_64 + ## $ podman artifact rm edge-registry.itix.fr/demo-edge-retail/rhel9:x86_64 + ## + src: edge-registry.itix.fr/demo-edge-retail/rhel{{ libvirt_domain_parameters.rhel_version }}:{{ libvirt_domain_parameters.architecture }} + dest: root.qcow2 + # virt-install parameters + virt_install: + - autostart: + - cpu: host-passthrough + - vcpus: '{{ libvirt_domain_parameters.vcpu }}' + - ram: '{{ libvirt_domain_parameters.ram }}' + - os-variant: 'rhel{{ libvirt_domain_parameters.rhel_version }}-unknown' + - disk: + path: '{{ libvirt_images_path }}/{{ libvirt_domain_parameters.name }}/root.qcow2' + bus: virtio + serial: root + size: '{{ libvirt_domain_parameters.disk }}' + - network: + network: '{{ libvirt_domain_parameters.network }}' + mac.address: '{{ libvirt_domain_mac_address }}' + - console: + pty: + target.type: virtio + - serial: + pty: + - graphics: none + - disk: + path: '{{ libvirt_images_path }}/{{ libvirt_domain_parameters.name }}/cloud-init.iso' + readonly: on + - import: + - sysinfo: + system.serial: ds=nocloud + # Post-install: add_host parameters + add_host: + group: vm + ansible_user: demo + ansible_become: yes + ansible_ssh_common_args: -J {{ ansible_user }}@{{ inventory_hostname }} -o StrictHostKeyChecking=no + + tasks: + + ## + ## 1. Variable setup and debug + ## + + - debug: + var: libvirt_domain + when: libvirt_domains_debug|bool + + - set_fact: + libvirt_domain_name: '{{ libvirt_domain.name }}' + libvirt_domain_path: '/var/lib/libvirt/images/{{ libvirt_domain.name }}' + libvirt_domain_action: none + + - community.libvirt.virt: + command: list_vms + register: virsh_list + + - community.libvirt.virt: + command: list_vms + state: running + register: virsh_list_running + + - set_fact: + libvirt_existing_domains: '{{ virsh_list.list_vms }}' + libvirt_running_domains: '{{ virsh_list_running.list_vms }}' + + - debug: + var: variables + vars: + variables: + libvirt_domain_name: '{{ libvirt_domain_name }}' + libvirt_domain_path: '{{ libvirt_domain_path }}' + libvirt_existing_domains: '{{ libvirt_existing_domains }}' + when: libvirt_domains_debug | bool + + ## + ## 2. Delete the VM if state is "absent" + ## + + - block: + - name: Shutdown Libvirt domain + community.libvirt.virt: + name: '{{ libvirt_domain_name }}' + command: destroy + when: libvirt_domain_name in libvirt_running_domains + + - name: Delete Libvirt domain + community.libvirt.virt: + name: '{{ libvirt_domain_name }}' + command: undefine + + - name: Remove the domain files + ansible.builtin.file: + path: '{{ libvirt_domain_path }}' + state: absent + when: > + libvirt_domain.state | default("present") == "absent" + and libvirt_domain_name in libvirt_existing_domains + + ## + ## 3. Create the VM if state is "present" and the VM does not already exist + ## + + - block: + ## + ## 3.1. Pre-requisites + ## + - name: Ensure the domain root directory exists + file: + path: '{{ libvirt_domain_path }}' + owner: qemu + group: qemu + mode: 0700 + state: directory + + ## + ## 3.2. Cloud-init setup + ## + - name: Create a directory for cloud-init + file: + path: '{{ libvirt_domain_path }}/cloud-init' + owner: qemu + group: qemu + mode: 0700 + state: directory + + - name: Template all cloud-init files + template: + dest: '{{ libvirt_domain_path }}/cloud-init/{{ cloud_init_template.dest }}' + src: '{{ cloud_init_template.src }}' + loop: '{{ libvirt_domain.cloud_init.templates }}' + loop_control: + label: '{{ cloud_init_template.dest }}' + loop_var: cloud_init_template + + - name: Create the cloud-init.iso + command: + cmd: genisoimage -output {{ target_file }} -volid cidata -joliet -rock {{ source_files | join(" ") }} + chdir: '{{ libvirt_domain_path }}/cloud-init' + creates: '{{ target_file }}' + vars: + target_file: '{{ libvirt_domain_path }}/cloud-init.iso' + source_files: '{{ [ libvirt_domain_path ~ "/cloud-init/" ] | product(libvirt_domain.cloud_init.templates | map(attribute="dest")) | map("join") | list }}' + + - name: Fix permissions on the cloud-init image + file: + path: '{{ libvirt_domain_path }}/cloud-init.iso' + owner: qemu + group: qemu + mode: 0600 + + ## + ## 3.3. Root disk setup + ## + - name: Check if domain root disk exists + ansible.builtin.stat: + path: '{{ libvirt_domain_path }}/{{ libvirt_domain.root_disk.dest }}' + register: domain_root_disk + + - block: + - name: Pull the OCI artefact + command: + cmd: podman artifact pull {{ libvirt_domain.root_disk.src }} + environment: + REGISTRY_AUTH_FILE: /etc/ostree/auth.json + + - name: Extract the OCI artefact + command: + cmd: podman artifact extract {{ libvirt_domain.root_disk.src }} {{ libvirt_domain_path }}/{{ libvirt_domain.root_disk.dest }} + creates: '{{ libvirt_domain_path }}/{{ libvirt_domain.root_disk.dest }}' + environment: + REGISTRY_AUTH_FILE: /etc/ostree/auth.json + when: has_podman_artifact_extract | bool + + - block: + - name: Get artifact metadata + ansible.builtin.command: "podman artifact inspect {{ libvirt_domain.root_disk.src }}" + register: artifact_inspect_result + changed_when: false + + - name: Find blob in podman storage + ansible.builtin.find: + paths: /var/lib/containers/storage + patterns: "{{ blob_hash }}" + recurse: true + register: find_blob_result + + - name: Blob not found + ansible.builtin.fail: + msg: "Blob with hash {{ blob_hash }} cannot be found." + when: find_blob_result.matched == 0 + + - name: Copy blob to final destination + ansible.builtin.copy: + src: "{{ find_blob_result.files[0].path }}" + dest: '{{ libvirt_domain_path }}/{{ libvirt_domain.root_disk.dest }}' + remote_src: true + owner: qemu + group: qemu + mode: 0600 + + when: not has_podman_artifact_extract | bool + vars: + artifact_manifest: "{{ artifact_inspect_result.stdout | from_json }}" + layer_info: "{{ artifact_manifest.Manifest.layers[0] }}" + blob_hash: "{{ layer_info.digest | replace('sha256:', '') }}" + + when: not domain_root_disk.stat.exists + + ## + ## 3.4. VM creation + ## + - block: + - debug: + var: virt_install_commandline_s + when: libvirt_domains_debug | bool + vars: + virt_install_commandline_s: '{{ virt_install_commandline | join(" ") }}' + + - name: Run virt-install + command: + argv: '{{ virt_install_commandline }}' + + - set_fact: + libvirt_domain_action: created + vars: + virt_install_commandline: '{{ lookup("template", "virt-install-cmdline.j2") }}' + + when: > + libvirt_domain.state | default("present") == "present" + and libvirt_domain_name not in libvirt_existing_domains + + ## + ## 4. Post-installation tasks + ## + + - block: + ## + ## 4.1. Fetch the IP address of the VM + ## + - name: Fetch addresses from the Qemu Guest agent + command: virsh domifaddr {{ libvirt_domain_name }} --source agent --full + environment: + LANG: C + LC_ALL: C + register: virsh_domifaddr + failed_when: > + virsh_domifaddr.rc != 0 or virsh_domifaddr.stdout_lines | length < 5 + or external_ip | length < 1 + until: "virsh_domifaddr is not failed" + retries: 100 + delay: 5 + + - set_fact: + libvirt_domain_domifaddr: '{{ addresses }}' + libvirt_domain_domifaddr_external_ip: '{{ external_ip }}' + + - debug: + var: variables + vars: + variables: + libvirt_domain_domifaddr: '{{ libvirt_domain_domifaddr }}' + libvirt_domain_domifaddr_external_ip: '{{ libvirt_domain_domifaddr_external_ip }}' + when: libvirt_domains_debug | bool + + vars: + domifaddr_output: '{{ virsh_domifaddr.stdout_lines }}' + addresses: > + {{ domifaddr_output[2:] | map("split", None) | community.general.json_query('[*].{"ifname": [0], "mac": [1], "proto": [2], "address": [3]}') }} + external_ip: '{{ addresses | rejectattr("ifname", "eq", "lo") | selectattr("proto", "eq", "ipv4") | map(attribute="address") | ansible.utils.ipaddr("address") | list }}' + when: > + libvirt_domain.state | default("present") == "present" + + ## + ## 4.2. Update SSH known_hosts + ## + - name: Remove IP / hostnames of the previous VM from the SSH known_hosts file + local_action: + module: command + args: ssh-keygen -R {{ hostname }} + vars: + ansible_become: false + loop: '{{ libvirt_domain_domifaddr_external_ip | default([]) }}' + loop_control: + loop_var: hostname + label: '{{ hostname }}' + when: libvirt_domain_action == "created"