diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..24e3ab1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @stackhpc/ansible diff --git a/.github/workflows/publish-role.yml b/.github/workflows/publish-role.yml new file mode 100644 index 0000000..51ecc35 --- /dev/null +++ b/.github/workflows/publish-role.yml @@ -0,0 +1,12 @@ +--- +name: Publish Ansible Role +'on': + push: + tags: + - "v?[0-9]+.[0-9]+.[0-9]+" + workflow_dispatch: +jobs: + publish_role: + uses: stackhpc/.github/.github/workflows/publish-role.yml@main + secrets: + GALAXY_API_KEY: ${{ secrets.GALAXY_API_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c88cce --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +**/__pychache__ +.cache diff --git a/README.md b/README.md index a389da0..af61fcf 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Role Variables - `enable_spice`: If true enables SPICE listening for use with Virtual Machine Manager and similar tools + - `enable_guest_virtio`: If true enables guest virtio device for use with + Qemu guest agent + - `volumes`: a list of volumes to attach to the VM. Each volume is defined with the following dict: - `type`: What type of backing volume does the instance use? All @@ -120,6 +123,27 @@ Role Variables - `dev`: (optional) Block device path when type is `block`. - `remote_src`: (optional) When type is `file` or `block`, specify wether `image` points to a remote file (true) or a file local to the host that launched the playbook (false). Defaults to true. + - `usb_devices`: a list of usb devices to present to the vm from the host. + + Each usb device is defined with the following dict: + + - `vendor`: The vendor id of the USB device. + - `product`: The product id of the USB device. + + Note - Libvirt will error if the VM is provisioned and the USB device is not attached. + + To obtain the vendor id and product id of the usb device from the host running as sudo / root with the usb device plugged in + run `lsusb -v`. Example below with an attached Sandisk USB Memory Stick with vendor id: `0x0781` and product id: `0x5567` + + ``` + lsusb -v | grep -A4 -i sandisk + + idVendor 0x0781 SanDisk Corp. + idProduct 0x5567 Cruzer Blade + bcdDevice 1.00 + iManufacturer 1 + iProduct 2 + ``` - `interfaces`: a list of network interfaces to attach to the VM. Each network interface is defined with the following dict: @@ -231,6 +255,10 @@ Example Playbook interfaces: - network: 'br-datacentre' + + usb_devices: + - vendor: '0x0781' + product: '0x5567' - state: present name: 'vm2' diff --git a/tasks/autodetect.yml b/tasks/autodetect.yml index fc3969d..659e8fc 100644 --- a/tasks/autodetect.yml +++ b/tasks/autodetect.yml @@ -2,18 +2,18 @@ - name: Detect the virtualisation engine block: - name: Load the kvm kernel module - modprobe: + community.general.modprobe: name: kvm become: true failed_when: false - name: Check for the KVM device - stat: + ansible.builtin.stat: path: /dev/kvm register: stat_kvm - name: Set a fact containing the virtualisation engine - set_fact: + ansible.builtin.set_fact: libvirt_vm_engine: >- {%- if ansible_facts.architecture != libvirt_vm_arch -%} {# Virtualisation instructions are generally available only for the host @@ -32,7 +32,7 @@ block: - block: - name: Detect the KVM emulator binary path - stat: + ansible.builtin.stat: path: "{{ item }}" register: kvm_emulator_result with_items: @@ -49,12 +49,12 @@ - block: - name: Detect the QEMU emulator binary path - stat: + ansible.builtin.stat: path: /usr/libexec/qemu-kvm register: kvm_emulator_result - name: Set a fact containing the QEMU emulator binary path - set_fact: + ansible.builtin.set_fact: libvirt_vm_emulator: "{{ kvm_emulator_result.stat.path }}" when: kvm_emulator_result.stat.exists when: @@ -64,12 +64,12 @@ - block: - name: Detect the QEMU emulator binary path - shell: which qemu-system-{{ libvirt_vm_arch }} + ansible.builtin.command: which qemu-system-{{ libvirt_vm_arch }} register: qemu_emulator_result changed_when: false - name: Set a fact containing the QEMU emulator binary path - set_fact: + ansible.builtin.set_fact: libvirt_vm_emulator: "{{ qemu_emulator_result.stdout }}" when: @@ -77,7 +77,7 @@ - ansible_facts.os_family != 'RedHat' or ansible_facts.distribution_major_version | int == 7 - name: Fail if unable to detect the emulator - fail: + ansible.builtin.fail: msg: Unable to detect emulator for engine {{ libvirt_vm_engine }}. when: libvirt_vm_emulator is none when: libvirt_vm_emulator is none or libvirt_vm_emulator | length == 0 diff --git a/tasks/check-interface.yml b/tasks/check-interface.yml index 2aaa375..d5dc19d 100644 --- a/tasks/check-interface.yml +++ b/tasks/check-interface.yml @@ -1,6 +1,6 @@ --- - name: Check network interface has a network name - fail: + ansible.builtin.fail: msg: > The interface definition {{ interface }} has type 'network', but does not have a network name defined. @@ -10,7 +10,7 @@ - interface.network is not defined - name: Check direct interface has an interface device name - fail: + ansible.builtin.fail: msg: > The interface definition {{ interface }} has type 'direct', but does not have a host source device defined. diff --git a/tasks/check-usb-devices.yml b/tasks/check-usb-devices.yml new file mode 100644 index 0000000..4f9ad46 --- /dev/null +++ b/tasks/check-usb-devices.yml @@ -0,0 +1,15 @@ +--- +- name: List USB hardware + ansible.builtin.command: lsusb -d {{ usb_device.vendor }}:{{ usb_device.product }} + register: host_attached_usb_device + become: true + changed_when: false + failed_when: false + +- name: Check USB device is present on Host system + ansible.builtin.fail: + msg: > + The USB Device with Vendor ID:{{ usb_device.vendor }} and Product ID:{{ usb_device.product }} is not seen on host system + Is the USB device plugged in correctly ? + when: + - host_attached_usb_device.rc != 0 diff --git a/tasks/destroy-vm.yml b/tasks/destroy-vm.yml index 366e101..1177d28 100644 --- a/tasks/destroy-vm.yml +++ b/tasks/destroy-vm.yml @@ -2,20 +2,20 @@ # The destroyed state does not seem to be idempotent, so check whether the VM # exists before destroying it. - name: Check the VM's status - virt: + community.libvirt.virt: name: "{{ vm.name }}" command: list_vms uri: "{{ libvirt_vm_uri | default(omit, true) }}" register: result - become: yes + become: true - block: - name: Ensure the VM is absent - virt: + community.libvirt.virt: name: "{{ vm.name }}" state: destroyed uri: "{{ libvirt_vm_uri | default(omit, true) }}" - become: yes + become: true # note(wszumski): the virt module does not seem to support # removing vms with nvram defined - as a workaround, use the @@ -23,13 +23,13 @@ # actually contains an nvram element rather than relying on # boot_firmware having the correct value. - name: Ensure the VM is undefined - command: + ansible.builtin.command: cmd: >- virsh {% if libvirt_vm_uri %}-c {{ libvirt_vm_uri }}{% endif %} undefine {% if boot_firmware == 'efi' %} --nvram{% endif %} {{ vm.name }} - become: yes + become: true changed_when: true when: vm.name in result.list_vms diff --git a/tasks/destroy-volumes.yml b/tasks/destroy-volumes.yml index b7c0704..7af23d4 100644 --- a/tasks/destroy-volumes.yml +++ b/tasks/destroy-volumes.yml @@ -1,6 +1,6 @@ --- - name: Ensure the VM volumes do not exist - script: > + ansible.builtin.script: > destroy_virt_volume.sh {{ item.name }} {{ item.pool | default('default') }} @@ -11,4 +11,4 @@ changed_when: - volume_result is success - (volume_result.stdout | from_json).changed | default(True) - become: yes + become: true diff --git a/tasks/main.yml b/tasks/main.yml index 6ddacbe..6c8263b 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Gather os specific variables - include_vars: "{{ item }}" + ansible.builtin.include_vars: "{{ item }}" with_first_found: - files: - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml" @@ -8,7 +8,7 @@ - "{{ ansible_facts.os_family }}.yml" tags: vars -- include_tasks: autodetect.yml +- ansible.builtin.include_tasks: autodetect.yml # We don't need to know the engine and emulator if we're not creating any new # VMs. when: >- @@ -17,12 +17,12 @@ # Libvirt requires qemu-img to create qcow2 files. - name: Ensure qemu-img is installed - package: + ansible.builtin.package: name: "{{ 'qemu-img' if ansible_facts.os_family == 'RedHat' else 'qemu-utils' }}" update_cache: "{{ True if ansible_facts.pkg_mgr == 'apt' else omit }}" become: true -- include_tasks: volumes.yml +- ansible.builtin.include_tasks: volumes.yml vars: volumes: "{{ vm.volumes | default([], true) }}" with_items: "{{ libvirt_vms }}" @@ -30,7 +30,7 @@ loop_var: vm when: (vm.state | default('present', true)) == 'present' -- include_tasks: vm.yml +- ansible.builtin.include_tasks: vm.yml vars: console_log_enabled: "{{ vm.console_log_enabled | default(false) }}" console_log_path: >- @@ -41,17 +41,19 @@ cpu_mode: "{{ vm.cpu_mode | default(libvirt_cpu_mode_default) }}" volumes: "{{ vm.volumes | default([], true) }}" interfaces: "{{ vm.interfaces | default([], true) }}" + usb_devices: "{{ vm.usb_devices | default([], false) }}" start: "{{ vm.start | default(true) }}" autostart: "{{ vm.autostart | default(true) }}" enable_vnc: "{{ vm.enable_vnc | default(false) }}" enable_spice: "{{ vm.enable_spice | default(false) }}" + enable_guest_virtio: "{{ vm.enable_guest_virtio | default(false) }}" boot_firmware: "{{ vm.boot_firmware | default('bios', true) | lower }}" with_items: "{{ libvirt_vms }}" loop_control: loop_var: vm when: (vm.state | default('present', true)) == 'present' -- include_tasks: destroy-vm.yml +- ansible.builtin.include_tasks: destroy-vm.yml vars: boot_firmware: "{{ vm.boot_firmware | default('bios', true) | lower }}" with_items: "{{ libvirt_vms }}" @@ -59,7 +61,7 @@ loop_var: vm when: (vm.state | default('present', true)) == 'absent' -- include_tasks: destroy-volumes.yml +- ansible.builtin.include_tasks: destroy-volumes.yml vars: volumes: "{{ vm.volumes | default([], true) }}" with_items: "{{ libvirt_vms }}" diff --git a/tasks/vm.yml b/tasks/vm.yml index 6839cb4..d8a6da7 100644 --- a/tasks/vm.yml +++ b/tasks/vm.yml @@ -1,6 +1,6 @@ --- - name: Ensure the VM console log directory exists - file: + ansible.builtin.file: path: "{{ console_log_path | dirname }}" state: directory owner: "{{ libvirt_vm_log_owner }}" @@ -11,20 +11,27 @@ become: "{{ libvirt_vm_sudo }}" - name: Validate VM interfaces - include_tasks: check-interface.yml + ansible.builtin.include_tasks: check-interface.yml vars: interface: "{{ item }}" with_items: "{{ interfaces }}" +- name: Validate Host USB Devices + ansible.builtin.include_tasks: check-usb-devices.yml + vars: + usb_device: "{{ item }}" + with_items: "{{ usb_devices }}" + - name: Ensure the VM is defined - virt: + community.libvirt.virt: + name: "{{ vm.name }}" command: define xml: "{{ lookup('template', vm.xml_file | default('vm.xml.j2')) }}" uri: "{{ libvirt_vm_uri | default(omit, true) }}" become: "{{ libvirt_vm_sudo }}" - name: Ensure the VM is running and started at boot - virt: + community.libvirt.virt: name: "{{ vm.name }}" autostart: "{{ autostart | bool }}" state: "{{ 'running' if (start | bool) else 'shutdown' }}" diff --git a/tasks/volumes.yml b/tasks/volumes.yml index 32d2fc6..9d81b8a 100644 --- a/tasks/volumes.yml +++ b/tasks/volumes.yml @@ -1,6 +1,6 @@ --- - name: Ensure remote images are downloaded - get_url: + ansible.builtin.get_url: url: "{{ item.image }}" dest: "{{ libvirt_vm_image_cache_path }}/{{ item.image | basename }}" checksum: "{{ item.checksum | default(omit) }}" @@ -8,7 +8,7 @@ when: "'http' in item.image" - name: Ensure local images are copied - copy: + ansible.builtin.copy: src: "{{ item.image }}" dest: "{{ libvirt_vm_image_cache_path }}/{{ item.image | basename }}" checksum: "{{ item.checksum | default(omit) }}" @@ -17,7 +17,7 @@ when: "'http' not in item.image" - name: Ensure the VM disk volumes exist - script: > + ansible.builtin.script: > virt_volume.sh -n {{ item.name }} -p {{ item.pool |default('default') }} @@ -40,7 +40,7 @@ become: true - name: Ensure the VM network volumes exist - command: qemu-img create -f {{ item.source.protocol }} {{ item.source.protocol }}:{{ item.source.name }} {{ item.capacity }} + ansible.builtin.command: qemu-img create -f {{ item.source.protocol }} {{ item.source.protocol }}:{{ item.source.name }} {{ item.capacity }} with_items: "{{ volumes }}" when: item.type | default(libvirt_volume_default_type) == 'network' register: volume_result_network diff --git a/templates/vm.xml.j2 b/templates/vm.xml.j2 index 2c3a84a..e94feec 100644 --- a/templates/vm.xml.j2 +++ b/templates/vm.xml.j2 @@ -1,17 +1,17 @@ {{ vm.name }} - {% if vm.uuid is defined %} +{% if vm.uuid is defined %} {{ vm.uuid }} - {% elif (libvirt_vm_default_uuid_deterministic | bool) or (vm.uuid_deterministic is defined and (vm.uuid_deterministic | bool)) %} +{% elif (libvirt_vm_default_uuid_deterministic | bool) or (vm.uuid_deterministic is defined and (vm.uuid_deterministic | bool)) %} {{ vm.name | to_uuid }} - {% endif %} +{% endif %} {{ vm.memory_mb | int * 1024 }} {{ vm.vcpus }} - {% if vm.clock_offset |default( libvirt_vm_clock_offset ) %} +{% if vm.clock_offset |default( libvirt_vm_clock_offset ) %} - {% else %} +{% else %} - {% endif %} +{% endif %} destroy restart destroy @@ -34,67 +34,67 @@ - {% if cpu_mode %} +{% if cpu_mode %} - {% endif %} +{% endif %} {{ libvirt_vm_emulator }} {% for volume in volumes %} - - {% if volume.type | default(libvirt_volume_default_type) == 'file' %} + + {% if volume.type | default(libvirt_volume_default_type) == 'file' %} - {% elif volume.type | default(libvirt_volume_default_type) == 'network' %} - {% if volume.auth.username is defined %} + {% elif volume.type | default(libvirt_volume_default_type) == 'network' %} + {% if volume.auth.username is defined %} - {% endif %} {# End volume.auth.username check #} - {% if volume.source.name is defined %} + {% endif %} {# End volume.auth.username check #} + {% if volume.source.name is defined %} - {% for host in volume.source.hosts_list %} + {% for host in volume.source.hosts_list %} - {% endfor %} + {% endfor %} - {% endif %} {# End volume.source.name check #} - {% elif volume.type | default(libvirt_volume_default_type) == 'block' %} + {% endif %} {# End volume.source.name check #} + {% elif volume.type | default(libvirt_volume_default_type) == 'block' %} - {% else %} {# End elif volume.type is defined #} + {% else %} {# End elif volume.type is defined #} - {% endif %} - {% if volume.target is undefined %} + {% endif %} + {% if volume.target is undefined %} - {% else %} + {% else %} - {% endif %} + {% endif %} {% endfor %} {% for interface in interfaces %} -{% if interface.type is defined and interface.type == 'direct' %} + {% if interface.type is defined and interface.type == 'direct' %} -{% elif interface.type is defined and interface.type == 'bridge' %} + {% elif interface.type is defined and interface.type == 'bridge' %} -{% elif interface.type is not defined or interface.type == 'network' %} + {% elif interface.type is not defined or interface.type == 'network' %} -{% endif %} - {% if interface.mac is defined %} + {% endif %} + {% if interface.mac is defined %} - {% endif %} + {% endif %} {# if the network configuration is invalid this can still appear in the xml #} {# (say you enter 'bond' instead of 'bridge' in your variables) #} - {% if interface.model is defined %} + {% if interface.model is defined %} - {% else %} + {% else %} - {% endif %} - {% if interface.alias is defined %} + {% endif %} + {% if interface.alias is defined %} - {% endif %} + {% endif %} {% endfor %} {% if console_log_enabled | bool %} @@ -124,6 +124,19 @@ {% endif %} +{% if enable_guest_virtio |bool %} + + + +{% endif %} +{% for usb_device in usb_devices %} + + + + + + + {% endfor %} /dev/urandom