## ## 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 ## ## Parameters of the VM to create ## libvirt_domain_parameters: name: test-vm mac: 52:54:00:6b:3c:58 network: default rhel_version: 9 ram: 2048 vcpu: 2 disk: 20 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 # 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_parameters.mac }}' - 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" ## ## 4.3. Add the VM to Ansible inventory ## - block: - debug: var: add_host_params when: libvirt_domains_debug | bool - name: Add the VM to Ansible inventory ansible.builtin.add_host: '{{ add_host_params }}' vars: add_host_params: '{{ original_args | combine(extra_arg_ip) | combine(extra_arg_hostname) }}' extra_arg_hostname: '{{ {"hostname": libvirt_domain_name } if "name" not in original_args and "hostname" not in original_args and "host" not in original_args else {} }}' extra_arg_ip: '{{ {"ansible_ssh_host": vm_ip } if "ansible_ssh_host" not in original_args else {} }}' vm_ip: '{{ libvirt_domain_domifaddr_external_ip | ansible.utils.ipaddr("ipv4") | list | first }}' original_args: '{{ libvirt_domain.add_host }}' when: > libvirt_domain.state | default("present") == "present" ## ## 5. Configure the VM ## - name: Configure a RHEL VM created with cloud-init hosts: vm gather_facts: no tasks: - wait_for_connection: - ansible.builtin.setup: - debug: msg: "Hello from {{ inventory_hostname }} in {{ group_names | join(',') }} !"