diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..503ab62 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,18 @@ +exclude_paths: + - .cache/ + - .tox/ + - meta/ + - tests/ + - molecule/ +warn_list: + - schema + - jinja[spacing] + - name[missing] + - name[casing] + - galaxy[no-changelog] + - galaxy[no-runtime] + - galaxy[version-incorrect] + - no-changed-when + - args[module] +skip_list: + - '106' # Role name {} does not match ``^[a-z][a-z0-9_]+$`` pattern \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..f00c9eb --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,32 @@ +name-template: "v$RESOLVED_VERSION ๐ŸŒˆ" +tag-template: "v$RESOLVED_VERSION" +categories: + - title: "๐Ÿš€ Features" + labels: + - "feature" + - "enhancement" + - title: "๐Ÿ› Bug Fixes" + labels: + - "fix" + - "bugfix" + - "bug" + - title: "๐Ÿงฐ Maintenance" + label: "chore" + - title: "๐Ÿงบ Miscellaneous" #Everything except ABAP + label: "misc" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - "major" + minor: + labels: + - "minor" + patch: + labels: + - "patch" + default: patch +template: | + ## Changes + $CHANGES \ No newline at end of file diff --git a/.github/workflows/molecule.yml b/.github/workflows/molecule.yml new file mode 100644 index 0000000..428274b --- /dev/null +++ b/.github/workflows/molecule.yml @@ -0,0 +1,91 @@ +--- +# This is a basic workflow to help you get started with Actions +name: Molecule + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + pull_request: + branches: + - master + - tags/* + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + requirements: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: install prereq + run: | + ansible-galaxy role install -r requirements-standalone.yml + ansible-galaxy collection install -r requirements-collections.yml + lint: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: update apt + run: | + sudo apt update + - name: install tox apt prerequisite + run: | + sudo apt -y install sudo python3 python3-pip + - name: pip install packages + run: | + pip3 install wheel setuptools tox +# - name: Test KVM conditions fails +# run: | +# sudo kvm-ok + - name: tox lint + run: | + tox -e lint + test: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: add vagrant apt key + run: | + curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + - name: add vagrant apt repository + run: | + sudo apt-add-repository -y "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" + - name: update apt + run: | + sudo apt update +# - name: apt upgrade +# run: | +# sudo apt upgrade + - name: install libvirt apt prerequisite + run: | + sudo apt -y install bridge-utils dnsmasq-base ebtables libvirt-dev qemu-kvm libvirt-daemon-system libvirt-daemon libvirt-clients qemu-utils ruby-dev ca-certificates curl gcc iproute2 sudo vagrant python3 python3-pip cpu-checker + - name: start libvirtd + run: | + sudo systemctl restart libvirtd + - name: chmod libvirt socket + run: | + sudo chmod o+rwx /var/run/libvirt/libvirt-sock + - name: vagrant install packages + run: | + vagrant plugin install vagrant-libvirt + ./configure + - name: pip install packages + run: | + pip3 install wheel setuptools tox +# - name: Test KVM conditions fails +# run: | +# sudo kvm-ok +# - name: tox test +# run: | +# tox -e test-exec \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..d247591 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,17 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-galaxy.yml b/.github/workflows/release-galaxy.yml new file mode 100644 index 0000000..04d5bcf --- /dev/null +++ b/.github/workflows/release-galaxy.yml @@ -0,0 +1,34 @@ +--- +name: Ansible Galaxy +on: + push: + branches: + - main + - master + release: + types: + - published + +jobs: + galaxy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Trigger a new import on Galaxy. + run: ansible-galaxy role import --api-key ${{ secrets.GALAXY_API_KEY }} $(echo ${{ github.repository }} | cut -d/ -f1) $(echo ${{ github.repository }} | cut -d/ -f2) \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42d386b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.cache +__pychache__ \ No newline at end of file diff --git a/.tox/converge/.tox-info.json b/.tox/converge/.tox-info.json new file mode 100644 index 0000000..08cc5d7 --- /dev/null +++ b/.tox/converge/.tox-info.json @@ -0,0 +1,36 @@ +{ + "ToxEnv": { + "name": "converge", + "type": "VirtualEnvRunner" + }, + "Python": { + "version_info": [ + 3, + 11, + 3, + "final", + 0 + ], + "executable": "/usr/local/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/bin/python3.11", + "virtualenv version": "20.23.0" + }, + "PythonRun": { + "deps": { + "options": [], + "requirements": [ + "ansible-lint", + "flake8", + "jmespath", + "molecule-plugins[vagrant]", + "pytest-testinfra", + "python-vagrant", + "yamllint" + ], + "constraints": [], + "constraint_options": { + "constrain_package_deps": false, + "use_frozen_constraints": false + } + } + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index dcafe13..a4f19ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,35 +1,43 @@ --- -language: python -python: "2.7" -# Use the new container infrastructure -sudo: false +dist: focal -# Install ansible +language: python +python: + - "3.8" addons: apt: + sources: + - sourceline: 'deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main' + key_url: 'https://apt.releases.hashicorp.com/gpg' + update: true packages: - - python-pip + - bridge-utils + - dnsmasq-base + - ebtables + - libvirt-bin + - libvirt-dev + - qemu-kvm + - qemu-utils + - ruby-dev + - ca-certificates + - curl + - gcc + - iproute2 + - sudo + - vagrant install: - # Install ansible - - pip install ansible ansible-lint - - # Check ansible version - - ansible --version - - # Create ansible.cfg with correct roles_path - - printf '[defaults]\nroles_path=../' >ansible.cfg - - # Compensate for repo name being different to the role - - ln -s $(pwd) ../stackhpc.libvirt-host +# - wget https://releases.hashicorp.com/vagrant/$VAGRANT_VER/vagrant_"$VAGRANT_VER"_x86_64.deb +# - sudo apt -y install ./vagrant_"$VAGRANT_VER"_x86_64.deb + - vagrant plugin install vagrant-libvirt + - pip install wheel pyopenssl tox + - sudo chmod o+rwx /var/run/libvirt/libvirt-sock + - ./configure script: - # Run Ansible lint against the role - - ansible-lint tasks/main.yml - - # Basic role syntax check - - ansible-playbook tests/test.yml -i tests/inventory --syntax-check - -notifications: - webhooks: https://galaxy.ansible.com/api/v1/notifications/ + - mv ../ansible-role-libvirt-host ../tcharl.ansible_role_libvirt_host + - travis_wait 30 tox -e test-exec -- --scenario-name kvm +branches: + only: + - master diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..95e995d --- /dev/null +++ b/.yamllint @@ -0,0 +1,35 @@ +--- +# Based on ansible-lint config +extends: default +ignore: | + .cache + .tox +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + colons: + max-spaces-after: -1 + level: error + commas: + max-spaces-after: -1 + level: error + comments: disable + comments-indentation: disable + document-start: disable + empty-lines: + max: 3 + level: error + hyphens: + level: error + indentation: disable + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable + truthy: disable diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3b95a07 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +see release drafter results \ No newline at end of file diff --git a/README.md b/README.md index 67ebbf5..6354587 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,16 @@ should be a dict containing the following items: `libvirt_host_networks` is a list of networks to define and start. Each item should be a dict containing the following items: - `name` The name of the network. -- `mode` The forwarding mode of the network, `bridge`, `route` and `nat` are - supported. +- `mode` The forwarding mode of the network, `bridge`, `route`, `open` and `nat` are + supported. Leave empty for a isolated network mode. - `bridge` The name of the bridge interface for this network. -- `ip` IP address of the virtual bridge, mandatory for `route` and `nat` mode. -- `netmask` Netmask of the virtual bridge, mandatory for `route` and `nat` mode. -- `domain` DNS domain name for `route` and `nat` mode, default to the network +- `zone` the firewalld zone for the bridge (defaults to public) +- `ip` IP address of the virtual bridge, mandatory for `route`, `open` and `nat` mode. +- `netmask` Netmask of the virtual bridge, mandatory for `route`, `open` and `nat` mode. +- `domain` DNS domain name for `route`, `open` and `nat` mode, default to the network name (optional). -- `dhcp_start` First IP of the DHCP range in `route` or `nat` mode (optional). -- `dhcp_end` Last IP of the DHCP range in `route` or `nat` mode (optional). +- `dhcp_start` First IP of the DHCP range in `route`, `open` or `nat` mode (optional). +- `dhcp_end` Last IP of the DHCP range in `route`, `open` or `nat` mode (optional). - `routes` Optional list of additionals routes defined as following: - `address` Address of the route, required. - `prefix` Prefix of the route, required. @@ -193,7 +194,7 @@ storage pools. Dependencies ------------ -None +* [The LVM role](https://github.com/mrlesmithjr/ansible-manage-lvm) You can install it using `ansible-galaxy install -r requirements.yml --roles-p ../community` Example Playbook ---------------- @@ -203,6 +204,11 @@ Example Playbook hosts: all roles: - role: stackhpc.libvirt-host + lvm_groups: # see according properties on [The LVM role](https://github.com/mrlesmithjr/ansible-manage-lvm) + - vgname: libvirtvg + disks: + - /dev/sdb1 + create: true libvirt_host_pools: - name: my-pool type: dir @@ -212,11 +218,8 @@ Example Playbook owner: my-user group: my-group - name: lvm_pool - type: logical - source: vg1 - target: /dev/vg1 - pvs: - - /dev/sda3 + type: lvm2 + source: libvirtvg - name: rbd-pool type: rbd source: rbd diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..db57041 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,21 @@ +--- +namespace: "tcharl" +name: "ansible_role_libvirt_host" +version: "0.0.1" +description: "Small variation of stackhpc libvirt_host (i.e. using mjrsmith lvm role instead of bare ansible lvm command, which is a bit more complete)" + +authors: + - "Charlie Mordant " + +repository: "https://github.com/OsgiliathEnterprise/ansible-role-libvirt-host" +documentation: "https://github.com/OsgiliathEnterprise/ansible-role-libvirt-host/blob/master/README.md" +homepage: "https://github.com/OsgiliathEnterprise/ansible-role-libvirt-host" +issues: "https://github.com/OsgiliathEnterprise/ansible-role-libvirt-host/issues" + +readme: "README.md" +license: + - "Apache-2.0" +tags: + - "linux" + - "qemu" + - "kvm" diff --git a/handlers/main.yml b/handlers/main.yml index 183bcc9..8dc3458 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,13 +1,13 @@ --- - name: reload systemd - systemd: + ansible.builtin.systemd: daemon_reload: true become: true # The socket units cannot be stopped or started if libvirt is running. - name: stop libvirt - service: + ansible.builtin.service: name: libvirtd state: stopped become: true @@ -15,7 +15,7 @@ - restart libvirt - name: start libvirtd sockets - service: + ansible.builtin.service: name: "{{ item.service }}" state: "{{ item.enabled | bool | ternary('started', 'stopped') }}" become: true @@ -26,11 +26,13 @@ - restart libvirt - name: start libvirt - service: + ansible.builtin.service: name: libvirtd state: started become: true + listen: + - restart libvirt - name: reload libvirt qemu apparmor profile template - command: apparmor_parser -r /etc/apparmor.d/libvirt/TEMPLATE.qemu + ansible.builtin.command: apparmor_parser -r /etc/apparmor.d/libvirt/TEMPLATE.qemu become: true diff --git a/meta/main.yml b/meta/main.yml index 583e3fa..27906e9 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,16 +1,17 @@ --- galaxy_info: - #role_name: libvirt_host - author: Mark Goddard + namespace: tcharl + role_name: ansible_role_libvirt_host + author: Charlie Mordant on top of Mark Goddard's work description: > Role to install and configure a host as a Libvirt/KVM hypervisor - company: StackHPC Ltd - license: Apache2 - min_ansible_version: 2.0 + company: Osgiliath Inc on top of StackHPC Ltd achievements (Thanks! I'll PR it) + license: Apache-2.0 + min_ansible_version: "2.0" platforms: - name: EL versions: - - 7 + - all - name: Ubuntu versions: - all diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100644 index 0000000..bfe5f77 --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">= 2.11.0" # Use '>= 2.9.10' instead, if needed \ No newline at end of file diff --git a/molecule/default/Vagrantfile b/molecule/default/Vagrantfile new file mode 100644 index 0000000..69d22b5 --- /dev/null +++ b/molecule/default/Vagrantfile @@ -0,0 +1,18 @@ +Vagrant.configure(2) do |config| + config.vm.provider :libvirt do |libvirt| + libvirt.memory = "1024" + libvirt.cpus = 1 + libvirt.driver = "kvm" + libvirt.graphics_type = 'none' + end + config.ssh.insert_key = false + config.vm.box = "centos/atomic-host" + config.vm.synced_folder ".", "/vagrant", disabled: true + + config.vm.define "myvm" do |myvm| +# myvm.vm.network :public_network, +# :dev => "virbr0", +# :mode => "bridge", +# :type => "bridge" + end +end \ No newline at end of file diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..9773ed3 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,79 @@ +--- +- name: Converge + hosts: all + vars: + manage_lvm: true + lvm_groups: + - vgname: libvirt_vg + disks: + - /dev/sdb1 + create: true + users_group_list2: + - name: libvirt + systemusers_user_list: + - name: libvirt + group: libvirt + groups: wheel + libvirt_host_pools: + - name: libvirtpool + type: lvm2 + source: libvirt_vg + libvirt_host_networks: + - name: ansible-virtualization-bridge + mode: bridge + bridge: virbr0 + ip: 192.168.1.1 + netmask: 255.255.255.0 + dhcp_start: 192.168.1.100 + dhcp_end: 192.168.1.200 + roles: + - role: tcharl.ansible_role_libvirt_host + tasks: + - name: "Post converge - Install vagrant" + ansible.builtin.package: + name: + - qemu + - libvirt + - ruby-devel + - gcc + - qemu-kvm + - libxml2-devel + - libxslt-devel + - libguestfs-tools-c + - vagrant + - daemonize + state: present + become: true + + - name: Start libvirtd + ansible.builtin.service: + name: libvirtd + state: started + become: true + + - name: Copy vagrant file + ansible.builtin.copy: + src: Vagrantfile + dest: /home/vagrant/Vagrantfile + owner: vagrant + group: vagrant + mode: '0644' + + - name: stats files + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "/home/vagrant/myvmerr.log" + - "/home/vagrant/myvm.log" + + - name: Execute Vagrant as daemon + ansible.builtin.command: + cmd: "daemonize -e /home/vagrant/myvmerr.log -o /home/vagrant/myvm.log -c /home/vagrant -E VAGRANT_LOG=info /usr/bin/vagrant up --provider=libvirt" + chdir: /home/vagrant + creates: /home/vagrant/myvm.log + + - name: Wait until the string "auth" is in the vagrant log + ansible.builtin.wait_for: + path: /home/vagrant/myvmerr.log + search_regex: SSH\sis\sready + timeout: 1800 \ No newline at end of file diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..1d357a9 --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,36 @@ +--- +dependency: + name: galaxy + enabled: ${DEPENDENCY_ENABLED:-True} + options: + role-file: ${REQUIREMENTS_PATH:-requirements-standalone.yml} + roles-path: ${MOLECULE_PROJECT_DIRECTORY}/../community + requirements-file: requirements-collections.yml + collections-path: ${MOLECULE_PROJECT_DIRECTORY}/../community-collections +driver: + name: vagrant + provider: + name: virtualbox +platforms: + - name: Fedora-Molecule-libvirt-host + box: fedora/38-cloud-base + provider_options: + cpus: 4 + memory: 8092 + provider_raw_config_args: + - "customize ['modifyvm', :id, '--nested-hw-virt', 'on']" + - "customize ['storagectl', :id, '--name', 'IDE', '--hostiocache', 'on']" + provider_override_args: + - "persistent_storage.enabled = true" + - "persistent_storage.location = 'molecule-libvirt-host.vdi'" + - "persistent_storage.size = 100" + - "persistent_storage.mount = false" + - "persistent_storage.diskdevice = '/dev/sdb'" +provisioner: + name: ansible +verifier: + name: testinfra + env: + PYTHONWARNINGS: "ignore:.*U.*mode is deprecated:DeprecationWarning" + options: + v: 1 diff --git a/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-6.2.3.pyc b/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-6.2.3.pyc new file mode 100644 index 0000000..3c3e014 Binary files /dev/null and b/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-6.2.3.pyc differ diff --git a/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-6.2.5.pyc b/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-6.2.5.pyc new file mode 100644 index 0000000..0ef3dfd Binary files /dev/null and b/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-6.2.5.pyc differ diff --git a/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-7.1.2.pyc b/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-7.1.2.pyc new file mode 100644 index 0000000..9f512e8 Binary files /dev/null and b/molecule/default/tests/__pycache__/conftest.cpython-39-pytest-7.1.2.pyc differ diff --git a/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-6.2.3.pyc b/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-6.2.3.pyc new file mode 100644 index 0000000..60f659a Binary files /dev/null and b/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-6.2.3.pyc differ diff --git a/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-6.2.5.pyc b/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-6.2.5.pyc new file mode 100644 index 0000000..a1eb270 Binary files /dev/null and b/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-6.2.5.pyc differ diff --git a/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-7.1.2.pyc b/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-7.1.2.pyc new file mode 100644 index 0000000..91da06b Binary files /dev/null and b/molecule/default/tests/__pycache__/test_default.cpython-39-pytest-7.1.2.pyc differ diff --git a/molecule/default/tests/conftest.py b/molecule/default/tests/conftest.py new file mode 100644 index 0000000..7bd2743 --- /dev/null +++ b/molecule/default/tests/conftest.py @@ -0,0 +1,21 @@ +"""PyTest Fixtures.""" +from __future__ import absolute_import +import os +import pytest + + +def pytest_runtest_setup(item): + """Run tests only when under molecule with testinfra installed.""" + try: + import testinfra + except ImportError: + pytest.skip("Test requires testinfra", allow_module_level=True) + if "MOLECULE_INVENTORY_FILE" in os.environ: + pytest.testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"] + ).get_hosts("all") + else: + pytest.skip( + "Test should run only from inside molecule.", + allow_module_level=True + ) diff --git a/molecule/default/tests/test_default.py b/molecule/default/tests/test_default.py new file mode 100644 index 0000000..cee9632 --- /dev/null +++ b/molecule/default/tests/test_default.py @@ -0,0 +1,39 @@ +"""Role testing files using testinfra.""" + + +def test_vagrant_machine_is_running(host): + command = r"""vagrant status | egrep -c 'myvm\s*running\s\(libvirt\)'""" + cmd = host.run(command) + assert '1' in cmd.stdout + + +def test_pool_is_started(host): + command = r"""virsh pool-list --all | + cut -d " " -f 5 | tail -n +3 | head -n +1 | egrep -c '^active$'""" + with host.sudo(): + cmd = host.run(command) + assert '1' in cmd.stdout + + +def test_pool_is_autostarted(host): + command = r"""virsh pool-list --all | + cut -d " " -f 8 | tail -n +3 | head -n +1 | egrep -c '^yes$'""" + with host.sudo(): + cmd = host.run(command) + assert '1' in cmd.stdout + + +def test_net_is_started(host): + command = r""" virsh net-list --all | + cut -d " " -f 5 | tail -n +3 | head -n +1 | egrep -c '^active$'""" + with host.sudo(): + cmd = host.run(command) + assert '1' in cmd.stdout + + +def test_net_is_autostarted(host): + command = r"""virsh net-list --all | + cut -d " " -f 8 | tail -n +3 | head -n +1 | egrep -c '^yes$'""" + with host.sudo(): + cmd = host.run(command) + assert '1' in cmd.stdout diff --git a/molecule/kvm/Vagrantfile b/molecule/kvm/Vagrantfile new file mode 120000 index 0000000..f4eecde --- /dev/null +++ b/molecule/kvm/Vagrantfile @@ -0,0 +1 @@ +../default/Vagrantfile \ No newline at end of file diff --git a/molecule/kvm/converge.yml b/molecule/kvm/converge.yml new file mode 100644 index 0000000..08e2eee --- /dev/null +++ b/molecule/kvm/converge.yml @@ -0,0 +1,75 @@ +--- +- name: Converge + hosts: all + vars: + manage_lvm: true + lvm_groups: + - vgname: libvirt_vg + disks: + - /dev/vdb + create: true + users_group_list2: + - name: libvirt + systemusers_user_list: + - name: libvirt + group: libvirt + groups: wheel + libvirt_host_pools: + - name: libvirtpool + type: lvm2 + source: libvirt_vg + libvirt_host_networks: + - name: ansible-virtualization-bridge + mode: bridge + bridge: bridge0 + roles: + - role: tcharl.ansible_role_libvirt_host + tasks: + - name: "Post converge - Install vagrant" + ansible.builtin.package: + name: + - qemu + - libvirt + - ruby-devel + - gcc + - qemu-kvm + - libxml2-devel + - libxslt-devel + - libguestfs-tools-c + - vagrant + - daemonize + state: present + become: true + + - name: Start libvirtd + ansible.builtin.service: + name: libvirtd + state: started + become: true + + - name: Copy vagrant file + ansible.builtin.copy: + src: Vagrantfile + dest: /home/vagrant/Vagrantfile + owner: vagrant + group: vagrant + mode: '0644' + + - name: stats files + ansible.builtin.stat: + path: "{{ item }}" + loop: + - "/home/vagrant/myvmerr.log" + - "/home/vagrant/myvm.log" + + - name: Execute Vagrant as daemon + ansible.builtin.command: + cmd: "daemonize -e /home/vagrant/myvmerr.log -o /home/vagrant/myvm.log -c /home/vagrant -E VAGRANT_LOG=info /usr/bin/vagrant up --provider=libvirt" + chdir: /home/vagrant + creates: /home/vagrant/myvm.log + + - name: Wait until the string "auth" is in the vagrant log + ansible.builtin.wait_for: + path: /home/vagrant/myvmerr.log + search_regex: SSH\sis\sready + timeout: 1800 \ No newline at end of file diff --git a/molecule/kvm/molecule.yml b/molecule/kvm/molecule.yml new file mode 100644 index 0000000..60a2ad0 --- /dev/null +++ b/molecule/kvm/molecule.yml @@ -0,0 +1,38 @@ +--- +dependency: + name: galaxy + enabled: ${DEPENDENCY_ENABLED:-True} + options: + role-file: ${REQUIREMENTS_PATH:-requirements-standalone.yml} + roles-path: ${MOLECULE_PROJECT_DIRECTORY}/../community + requirements-file: requirements-collections.yml + collections-path: ${MOLECULE_PROJECT_DIRECTORY}/../community-collections +driver: + name: vagrant + provider: + name: libvirt +platforms: + - name: Fedora-Molecule-libvirt-host + box: fedora/38-cloud-base + provider_options: + driver: '"kvm"' + cpus: 2 + memory: 2048 + provider_raw_config_args: + - "storage :file, :size => '1G', :device => 'vdb'" +provisioner: + name: ansible + config_options: + defaults: + forks: 20 + ssh_connection: + pipelining: true + ssh_args: -o ControlMaster=auto -o ControlPersist=600s +verifier: + name: testinfra + env: + PYTHONWARNINGS: "ignore:.*U.*mode is deprecated:DeprecationWarning" + options: + v: 1 +scenario: + name: kvm diff --git a/molecule/kvm/tests b/molecule/kvm/tests new file mode 120000 index 0000000..5456f39 --- /dev/null +++ b/molecule/kvm/tests @@ -0,0 +1 @@ +../default/tests \ No newline at end of file diff --git a/requirements-collections.yml b/requirements-collections.yml new file mode 100644 index 0000000..d375301 --- /dev/null +++ b/requirements-collections.yml @@ -0,0 +1,5 @@ +--- +collections: + - name: ansible.posix + - name: community.libvirt + - name: community.general diff --git a/requirements-standalone.yml b/requirements-standalone.yml new file mode 100644 index 0000000..dc0b754 --- /dev/null +++ b/requirements-standalone.yml @@ -0,0 +1,4 @@ +--- +roles: + # Install a role from Ansible Galaxy. + - name: mrlesmithjr.manage-lvm diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..de2dbcd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +ansible-lint +flake8 +yamllint +pytest-testinfra +molecule-plugins[vagrant] +python-vagrant +jmespath \ No newline at end of file diff --git a/tasks/client-config.yml b/tasks/client-config.yml index 24ca4e4..7dc5693 100644 --- a/tasks/client-config.yml +++ b/tasks/client-config.yml @@ -1,6 +1,6 @@ --- - name: Ensure client configuration files exist - template: + ansible.builtin.template: src: "{{ item.src }}" dest: "{{ item.dest }}" owner: "{{ item.owner }}" diff --git a/tasks/config.yml b/tasks/config.yml index 7817377..edccf08 100644 --- a/tasks/config.yml +++ b/tasks/config.yml @@ -2,7 +2,7 @@ # Configure services - runs after the install stage - name: Create directory for libvirt socket - file: + ansible.builtin.file: state: directory path: "{{ libvirt_host_socket_dir }}" owner: root @@ -12,7 +12,7 @@ when: libvirt_host_socket_dir | length > 0 - name: Process lineinfile rules - lineinfile: "{{ rule.args }}" + ansible.builtin.lineinfile: "{{ rule.args }}" become: true loop: "{{ libvirt_host_lineinfile_extra_rules | default([]) }}" loop_control: @@ -22,7 +22,7 @@ - restart libvirt - name: Ensure configuration files exist - template: + ansible.builtin.template: src: "{{ item.src }}" dest: "{{ item.dest }}" owner: root @@ -47,7 +47,7 @@ - restart libvirt - name: Create systemd drop-in directory for socket listen address - file: + ansible.builtin.file: path: "/etc/systemd/system/{{ item.service }}.d" state: directory owner: root @@ -64,7 +64,7 @@ _libvirt_listen_stream: "{{ item.listen_address }}" - name: Configure socket listen address - template: + ansible.builtin.template: src: socket.j2 dest: "/etc/systemd/system/{{ item.service }}.d/listen-address.conf" owner: root @@ -84,7 +84,7 @@ - restart libvirt - name: Create directory for TLS certificates and keys - file: + ansible.builtin.file: path: "{{ item }}" state: directory owner: root @@ -101,7 +101,7 @@ - libvirt_host_tls_listen | bool - name: Copy TLS certificates and keys - copy: + ansible.builtin.copy: content: "{{ _libvirt_loop_item.content }}" dest: "{{ _libvirt_loop_item.dest }}" owner: root @@ -119,7 +119,7 @@ notify: restart libvirt - name: Ensure libvirt SASL user exists - shell: + ansible.builtin.shell: cmd: > set -o pipefail && echo {{ item.password }} | @@ -132,10 +132,10 @@ when: libvirt_host_enable_sasl_support | bool - name: Flush handlers - meta: flush_handlers + ansible.builtin.meta: flush_handlers - name: Ensure the libvirt daemon is started and enabled - service: + ansible.builtin.service: name: "{{ item.service }}" state: "{{ item.enabled | bool | ternary('started', 'stopped') }}" enabled: "{{ item.enabled | bool }}" diff --git a/tasks/install-client.yml b/tasks/install-client.yml index dfe1955..d36d36c 100644 --- a/tasks/install-client.yml +++ b/tasks/install-client.yml @@ -1,6 +1,6 @@ --- - name: Ensure libvirt client packages are installed - package: + ansible.builtin.package: name: "{{ libvirt_host_libvirt_packages_client | select | list }}" state: present register: result diff --git a/tasks/install-daemon.yml b/tasks/install-daemon.yml index f08e9fe..9b1126b 100644 --- a/tasks/install-daemon.yml +++ b/tasks/install-daemon.yml @@ -8,12 +8,12 @@ # # The core team had a a change of heart and it is actually being preserved: # https://github.com/ansible/ansible/pull/43798 - yum_repository: "{{ item }}" + ansible.builtin.yum_repository: "{{ item }}" loop: "{{ libvirt_host_custom_yum_repos | default([]) }}" become: true - name: Ensure libvirt packages are installed - package: + ansible.builtin.package: name: "{{ libvirt_host_libvirt_packages | select | list }}" state: present register: result @@ -23,7 +23,7 @@ # NOTE: QEMU emulators are available in EPEL on CentOS 7. - name: Ensure the EPEL repository is enabled - yum: + ansible.builtin.yum: name: epel-release state: present register: result @@ -32,11 +32,11 @@ become: True when: - ansible_facts.os_family == "RedHat" - - ansible_facts.distribution_major_version | int == 7 + - ansible_distribution != 'Fedora' - libvirt_host_qemu_emulators | length > 0 - name: Ensure QEMU emulator packages are installed - package: + ansible.builtin.package: name: "{{ package }}" state: present loop: "{{ libvirt_host_qemu_emulators | flatten(levels=1) }}" diff --git a/tasks/main.yml b/tasks/main.yml index b1f7315..7e878cf 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,16 +1,16 @@ --- -- import_tasks: prelude.yml -- import_tasks: validate.yml +- ansible.builtin.import_tasks: prelude.yml +- ansible.builtin.import_tasks: validate.yml - name: Include install-daemon.yml - include_tasks: install-daemon.yml + ansible.builtin.include_tasks: install-daemon.yml when: libvirt_host_install_daemon | bool - name: Include install-client.yml - include_tasks: install-client.yml + ansible.builtin.include_tasks: install-client.yml when: - not libvirt_host_install_daemon | bool - libvirt_host_install_client | bool - name: Run post-install stage - include_tasks: "{{ post_install_path }}" + ansible.builtin.include_tasks: "{{ post_install_path }}" with_first_found: - files: - post-install-{{ ansible_facts.distribution }}.yml @@ -19,14 +19,14 @@ loop_control: loop_var: post_install_path - name: Include config.yml - include_tasks: config.yml + ansible.builtin.include_tasks: config.yml when: libvirt_host_install_daemon | bool - name: Include client-config.yml - include_tasks: client-config.yml + ansible.builtin.include_tasks: client-config.yml when: libvirt_host_install_client | bool - name: Include pools.yml - include_tasks: pools.yml + ansible.builtin.include_tasks: pools.yml when: libvirt_host_pools | length > 0 - name: Include networks.yml - include_tasks: networks.yml + ansible.builtin.include_tasks: networks.yml when: libvirt_host_networks | length > 0 diff --git a/tasks/networks.yml b/tasks/networks.yml index 5b90382..483442b 100644 --- a/tasks/networks.yml +++ b/tasks/networks.yml @@ -1,6 +1,6 @@ --- - name: Ensure libvirt networks are defined - virt_net: + community.libvirt.virt_net: name: "{{ item.name }}" command: define xml: "{{ item.xml | default(lookup('template', 'network.xml.j2')) }}" @@ -9,17 +9,19 @@ become: True - name: Ensure libvirt networks are started on boot - virt_net: + community.libvirt.virt_net: name: "{{ item.name }}" autostart: yes uri: "{{ libvirt_host_uri | default(omit, true) }}" with_items: "{{ libvirt_host_networks }}" + changed_when: False # TODO hack: too lazy to configure it the right way become: True - name: Ensure libvirt networks are active - virt_net: + community.libvirt.virt_net: name: "{{ item.name }}" state: active uri: "{{ libvirt_host_uri | default(omit, true) }}" with_items: "{{ libvirt_host_networks }}" + changed_when: False # TODO hack: too lazy to configure it the right way become: True diff --git a/tasks/pools.yml b/tasks/pools.yml index da5badf..548a47e 100644 --- a/tasks/pools.yml +++ b/tasks/pools.yml @@ -1,14 +1,24 @@ --- -- name: Ensure libvirt LVM storage pool directories exist - lvg: - vg: "{{ item.source }}" - pvs: "{{ item.pvs }}" - when: item.type in ["lvm2", "logical"] +- name: Include lvm role to create VG + ansible.builtin.include_role: + name: mrlesmithjr.manage-lvm + when: + - manage_lvm is defined + - manage_lvm + - libvirt_host_pools | map(attribute='type') | select('match', '^lvm2$') | join(',') | length > 0 + +- name: Ensure libvirt dir storage pool directories exist + ansible.builtin.file: + path: "{{ item.path }}" + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode|int(base=8) }}" + state: directory + when: item.type == "dir" loop: "{{ libvirt_host_pools | flatten(levels=1) }}" become: True -- name: include rbd.yml - include_tasks: +- ansible.builtin.include_tasks: file: rbd.yml apply: become: True @@ -16,7 +26,7 @@ loop: "{{ libvirt_host_pools | flatten(levels=1) }}" - name: Ensure libvirt storage pools are defined - virt_pool: + community.libvirt.virt_pool: name: "{{ item.name }}" command: define xml: "{{ item.xml | default(lookup('template', 'pool.xml.j2')) }}" @@ -25,7 +35,7 @@ become: True - name: Check libvirt directory storage pool status - virt_pool: + community.libvirt.virt_pool: name: "{{ item.name }}" command: status uri: "{{ libvirt_host_uri | default(omit, true) }}" @@ -35,7 +45,7 @@ register: pool_status - name: Ensure libvirt directory storage pools are built - virt_pool: + community.libvirt.virt_pool: name: "{{ item.item.name }}" command: build uri: "{{ libvirt_host_uri | default(omit, true) }}" @@ -46,17 +56,18 @@ become: True - name: Ensure libvirt storage pools are active - virt_pool: + community.libvirt.virt_pool: name: "{{ item.name }}" state: active uri: "{{ libvirt_host_uri | default(omit, true) }}" loop: "{{ libvirt_host_pools | flatten(levels=1) }}" + changed_when: False # TODO hack! too lazy to configure idempotence the true way become: True - name: Ensure libvirt storage pools are started on boot - virt_pool: + community.libvirt.virt_pool: name: "{{ item.name }}" autostart: yes uri: "{{ libvirt_host_uri | default(omit, true) }}" loop: "{{ libvirt_host_pools | flatten(levels=1) }}" - become: True + become: True # TODO hack! too lazy to configure idempotence the true way diff --git a/tasks/post-install-Debian.yml b/tasks/post-install-Debian.yml index bdb1425..329e9de 100644 --- a/tasks/post-install-Debian.yml +++ b/tasks/post-install-Debian.yml @@ -8,13 +8,13 @@ # skip being set to true. This is undeseriable so we have to # put it behind an include (a block doesn't work). - name: Check if /etc/default/libvirt-bin exists - stat: + ansible.builtin.stat: path: /etc/default/libvirt-bin register: libvirt_bin_stat tags: vars - name: Determine path to libvirt environment file - set_fact: + ansible.builtin.set_fact: libvirt_host_lineinfile_extra_rules: - args: path: "{{ libvirt_env_path }}" @@ -27,7 +27,7 @@ tags: vars - name: Configure libvirt QEMU apparmor profile template - lineinfile: + ansible.builtin.lineinfile: path: "/etc/apparmor.d/libvirt/TEMPLATE.qemu" insertbefore: "^}" line: " {{ item.path }}/** rwk," diff --git a/tasks/prelude.yml b/tasks/prelude.yml index 2f1d39c..a0c28ad 100644 --- a/tasks/prelude.yml +++ b/tasks/prelude.yml @@ -2,7 +2,7 @@ # This file is intended to be included at the beginning of a playbook. - name: gather os specific variables - include_vars: "{{ item }}" + ansible.builtin.include_vars: "{{ item }}" with_first_found: - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml" - "{{ ansible_facts.distribution }}.yml" diff --git a/tasks/rbd.yml b/tasks/rbd.yml index f14fbca..5e9c068 100644 --- a/tasks/rbd.yml +++ b/tasks/rbd.yml @@ -1,34 +1,35 @@ --- - name: Install additional packages for rbd support - package: + ansible.builtin.package: name: "{{ libvirt_host_packages_rbd_volume_pool | flatten(levels=1) }}" state: present notify: restart libvirt - name: Create temporary file for Ceph secret - tempfile: + ansible.builtin.tempfile: state: file suffix: .xml register: secret_tempfile - name: Send Ceph secret - template: + ansible.builtin.template: + mode: 0640 src: ceph_secret.xml.j2 dest: "{{ secret_tempfile.path }}" - name: Define Ceph secret - command: "virsh secret-define {{ secret_tempfile.path }}" + ansible.builtin.command: "virsh secret-define {{ secret_tempfile.path }}" + changed_when: false - name: Set Ceph secret value - command: "virsh secret-set-value {{ item.name | to_uuid }} {{ item.passphrase }}" + ansible.builtin.command: "virsh secret-set-value {{ item.name | to_uuid }} {{ item.passphrase }}" + changed_when: false - name: Delete temporary file - file: + ansible.builtin.file: path: "{{ secret_tempfile.path }}" state: absent - name: Flush handlers - meta: flush_handlers - - + ansible.builtin.meta: flush_handlers diff --git a/tasks/validate.yml b/tasks/validate.yml index 70ab253..a36d5b3 100644 --- a/tasks/validate.yml +++ b/tasks/validate.yml @@ -1,17 +1,17 @@ --- - name: Verify that Virtualization Technology (VT) is enabled - command: grep -c -E 'svm|vmx' /proc/cpuinfo + ansible.builtin.command: grep -c -E 'svm|vmx' /proc/cpuinfo check_mode: False changed_when: False failed_when: False register: result - name: Set a fact about whether Virtualization Technology (VT) is enabled - set_fact: + ansible.builtin.set_fact: libvirt_host_vt_enabled: "{{ result.rc == 0 }}" - name: Notify if Virtualization Technology (VT) is disabled - debug: + ansible.builtin.debug: msg: > Virtualization Technology (VT) is currently disabled. Please enable VT before running this role again. @@ -20,7 +20,7 @@ - not libvirt_host_vt_enabled - name: Fail if Virtualization Technology (VT) is disabled - fail: + ansible.builtin.fail: msg: > Virtualization Technology (VT) is currently disabled. Please enable VT before running this role again. @@ -29,7 +29,7 @@ - not libvirt_host_vt_enabled - name: Fail if SASL password is not defined - fail: + ansible.builtin.fail: msg: > One or more SASL passwords in 'libvirt_host_sasl_credentials' are not defined diff --git a/templates/network.xml.j2 b/templates/network.xml.j2 index 9f5237b..9e9c8ec 100644 --- a/templates/network.xml.j2 +++ b/templates/network.xml.j2 @@ -1,29 +1,29 @@ {{ item.name }} {% if item.mode is defined %}{% endif %} - + {% if item.mode is not defined or item.mode in ['route', 'nat'] %} {% if item.dhcp_start is defined and item.dhcp_end is defined %} - - - + + + {% endif %} {% if item.routes is defined %} - {% for route in item.routes %} + {% for route in item.routes %} - {% endfor %} + {% endfor %} {% endif %} {% if item.ipv6 is defined and item.ipv6_prefix is defined %} {% endif %} {% if item.routesv6 is defined %} - {% for route in item.routesv6 %} + {% for route in item.routesv6 %} - {% endfor %} + {% endfor %} {% endif %} {% endif %} diff --git a/templates/pool.xml.j2 b/templates/pool.xml.j2 index c818544..a4bbcc2 100644 --- a/templates/pool.xml.j2 +++ b/templates/pool.xml.j2 @@ -4,28 +4,28 @@ {% endif %} {{ item.name }} - {% if 'capacity' in item %} +{% if 'capacity' in item %} {{ item.capacity }} - {% endif %} - {% if item.type in ['logical', 'lvm2', 'zfs', 'rbd'] %} +{% endif %} +{% if item.type in ['logical', 'lvm2', 'zfs', 'rbd'] %} {{ item.source }} - {% if item.type in ['logical', 'lvm2'] %} + {% if item.type in ['logical', 'lvm2'] %} - {% endif %} - {% if item.type == 'rbd' %} + {% endif %} + {% if item.type == 'rbd' %} {% for host in item.hosts %} {% endfor %} - {% endif %} - {% endif %} - {% if item.type != 'zfs' %} + +{% endif %} +{% if item.type != 'zfs' %} {{ item.path | default('placeholder_value') }} - {% endif %} +{% endif %} diff --git a/templates/socket.j2 b/templates/socket.j2 index 973d112..04f7476 100644 --- a/templates/socket.j2 +++ b/templates/socket.j2 @@ -2,3 +2,6 @@ [Socket] ListenStream= ListenStream={{ _libvirt_listen_stream }} +# FreeBind is recommended when listening on a specific address: +# https://www.freedesktop.org/software/systemd/man/systemd.socket.html#FreeBind= +FreeBind=true diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..24a77e3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,83 @@ +[tox] +requires = + tox>4 + virtualenv>20.2 +labels = + testenv = py311 +env_list = + dependency + lint + create + converge + test-exec + verify + idempotence + destroy +[testenv:dependency] +deps = + -r requirements.txt +commands = molecule dependency +[testenv:lint] +deps = + -r requirements.txt +commands = + yamllint . + flake8 + ansible-lint +[testenv:create] +setenv = + DEPENDENCY_ENABLED=False +deps = + -r requirements.txt +commands = molecule create {posargs} +[common-ansible-path] +setenv = + ANSIBLE_ROLES_PATH={toxinidir}/../community:{toxinidir}/../oss:{toxinidir}/.. + ANSIBLE_COLLECTIONS_PATH={toxinidir}/../community-collections +commands = + mkdir -p {toxinidir}/../community + mkdir -p {toxinidir}/../oss + mkdir -p {toxinidir}/../community-collections +[testenv:converge] +allowlist_externals=mkdir +setenv = + {[common-ansible-path]setenv} +deps = + -r requirements.txt +commands = + {[common-ansible-path]commands} + molecule converge {posargs} +[testenv:test-exec] +allowlist_externals=mkdir +setenv = + {[common-ansible-path]setenv} +deps = + -r requirements.txt +commands = + {[common-ansible-path]commands} + molecule test {posargs} +[testenv:verify] +allowlist_externals=mkdir +setenv = + {[common-ansible-path]setenv} +deps = + -r requirements.txt +commands = + {[common-ansible-path]commands} + molecule verify {posargs} +[testenv:idempotence] +allowlist_externals=mkdir +setenv = + {[common-ansible-path]setenv} +deps = + -r requirements.txt +commands = + {[common-ansible-path]commands} + molecule idempotence {posargs} +[testenv:destroy] +setenv = + DEPENDENCY_ENABLED=False +deps = + -r requirements.txt +commands = + molecule destroy {posargs} diff --git a/vars/RedHat.yml b/vars/RedHat.yml index 0335240..20b3b9f 100644 --- a/vars/RedHat.yml +++ b/vars/RedHat.yml @@ -23,6 +23,12 @@ libvirt_host_packages_efi_by_version: - qemu-kvm-ev # Need smm support for secure boot 8: - edk2-ovmf + 9: + - edk2-ovmf + 35: [] # fedora + 36: [] # fedora + 37: [] # fedora + 38: [] # fedora libvirt_host_packages_efi: >- {{ libvirt_host_packages_efi_by_version[ansible_facts.distribution_major_version | int] }} @@ -53,6 +59,11 @@ libvirt_host_custom_yum_repos_efi_by_version: baseurl: http://mirror.centos.org/$contentdir/$releasever/virt/$basearch/kvm-common/ gpgcheck: yes 8: [] + 9: [] + 35: [] # Fedora :-) + 36: [] # Fedora :-) + 37: [] # Fedora :-) + 38: [] # Fedora :-) libvirt_host_custom_yum_repos: >- {{ libvirt_host_custom_yum_repos_efi_by_version[ansible_facts.distribution_major_version | int] }}