From 2b63c54bcd33a74b79a1e0d3cce70498cd41aa9b Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 27 Mar 2026 17:07:53 +0100 Subject: [PATCH 01/36] feat(molecule): add container-based tests for apps role --- extensions/molecule/apps/install/cleanup.yml | 2 + extensions/molecule/apps/install/converge.yml | 4 ++ .../group_vars/systems_under_test.yml | 3 + .../molecule/apps/install/inventory/hosts.yml | 11 ++++ extensions/molecule/apps/install/molecule.yml | 0 extensions/molecule/apps/install/verify.yml | 11 ++++ extensions/molecule/apps/molecule.yml | 9 +++ extensions/molecule/apps/remove/cleanup.yml | 2 + extensions/molecule/apps/remove/converge.yml | 4 ++ .../group_vars/systems_under_test.yml | 3 + .../molecule/apps/remove/inventory/hosts.yml | 11 ++++ extensions/molecule/apps/remove/molecule.yml | 0 extensions/molecule/apps/remove/verify.yml | 11 ++++ extensions/molecule/config.yml | 66 +++++++++++++++++++ extensions/molecule/default/molecule.yml | 0 .../host_vars/debian11_container.yml | 6 ++ .../host_vars/debian12_container.yml | 6 ++ .../host_vars/debian13_container.yml | 6 ++ .../inventory/host_vars/rocky10_container.yml | 6 ++ .../inventory/host_vars/rocky8_container.yml | 6 ++ .../inventory/host_vars/rocky9_container.yml | 6 ++ .../host_vars/ubuntu2004_container.yml | 6 ++ .../host_vars/ubuntu2204_container.yml | 6 ++ .../host_vars/ubuntu2404_container.yml | 6 ++ extensions/molecule/inventory/hosts.yml | 0 extensions/molecule/playbooks/create.yml | 34 ++++++++++ extensions/molecule/playbooks/destroy.yml | 21 ++++++ extensions/molecule/playbooks/prepare.yml | 19 ++++++ .../molecule/playbooks/tasks/create-fail.yml | 13 ++++ extensions/molecule/requirements.yml | 4 ++ 30 files changed, 282 insertions(+) create mode 100644 extensions/molecule/apps/install/cleanup.yml create mode 100644 extensions/molecule/apps/install/converge.yml create mode 100644 extensions/molecule/apps/install/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/apps/install/inventory/hosts.yml create mode 100644 extensions/molecule/apps/install/molecule.yml create mode 100644 extensions/molecule/apps/install/verify.yml create mode 100644 extensions/molecule/apps/molecule.yml create mode 100644 extensions/molecule/apps/remove/cleanup.yml create mode 100644 extensions/molecule/apps/remove/converge.yml create mode 100644 extensions/molecule/apps/remove/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/apps/remove/inventory/hosts.yml create mode 100644 extensions/molecule/apps/remove/molecule.yml create mode 100644 extensions/molecule/apps/remove/verify.yml create mode 100644 extensions/molecule/config.yml create mode 100644 extensions/molecule/default/molecule.yml create mode 100644 extensions/molecule/inventory/host_vars/debian11_container.yml create mode 100644 extensions/molecule/inventory/host_vars/debian12_container.yml create mode 100644 extensions/molecule/inventory/host_vars/debian13_container.yml create mode 100644 extensions/molecule/inventory/host_vars/rocky10_container.yml create mode 100644 extensions/molecule/inventory/host_vars/rocky8_container.yml create mode 100644 extensions/molecule/inventory/host_vars/rocky9_container.yml create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2004_container.yml create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2204_container.yml create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2404_container.yml create mode 100644 extensions/molecule/inventory/hosts.yml create mode 100644 extensions/molecule/playbooks/create.yml create mode 100644 extensions/molecule/playbooks/destroy.yml create mode 100644 extensions/molecule/playbooks/prepare.yml create mode 100644 extensions/molecule/playbooks/tasks/create-fail.yml create mode 100644 extensions/molecule/requirements.yml diff --git a/extensions/molecule/apps/install/cleanup.yml b/extensions/molecule/apps/install/cleanup.yml new file mode 100644 index 000000000..a7be1cb80 --- /dev/null +++ b/extensions/molecule/apps/install/cleanup.yml @@ -0,0 +1,2 @@ +- name: 'Converge' + hosts: 'systems_under_test' diff --git a/extensions/molecule/apps/install/converge.yml b/extensions/molecule/apps/install/converge.yml new file mode 100644 index 000000000..ac53e7b96 --- /dev/null +++ b/extensions/molecule/apps/install/converge.yml @@ -0,0 +1,4 @@ +- name: 'Converge' + hosts: 'systems_under_test' + roles: + - role: 'linuxfabrik.lfops.apps' diff --git a/extensions/molecule/apps/install/inventory/group_vars/systems_under_test.yml b/extensions/molecule/apps/install/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..c559815dd --- /dev/null +++ b/extensions/molecule/apps/install/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,3 @@ +apps__apps__group_var: + - name: 'zsh' + state: 'present' diff --git a/extensions/molecule/apps/install/inventory/hosts.yml b/extensions/molecule/apps/install/inventory/hosts.yml new file mode 100644 index 000000000..1f66fe92d --- /dev/null +++ b/extensions/molecule/apps/install/inventory/hosts.yml @@ -0,0 +1,11 @@ +systems_under_test: + hosts: + debian11_container: + debian12_container: + debian13_container: + rocky8_container: + rocky9_container: + rocky10_container: + ubuntu2004_container: + ubuntu2204_container: + ubuntu2404_container: diff --git a/extensions/molecule/apps/install/molecule.yml b/extensions/molecule/apps/install/molecule.yml new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/molecule/apps/install/verify.yml b/extensions/molecule/apps/install/verify.yml new file mode 100644 index 000000000..cede6ccaf --- /dev/null +++ b/extensions/molecule/apps/install/verify.yml @@ -0,0 +1,11 @@ +- name: 'Verify' + hosts: 'systems_under_test' + gather_facts: false + tasks: + - name: 'Gather the package facts' + ansible.builtin.package_facts: + manager: 'auto' + + - name: 'Assert' + ansible.builtin.assert: + that: '"zsh" in ansible_facts.packages' diff --git a/extensions/molecule/apps/molecule.yml b/extensions/molecule/apps/molecule.yml new file mode 100644 index 000000000..2cc4a7111 --- /dev/null +++ b/extensions/molecule/apps/molecule.yml @@ -0,0 +1,9 @@ +scenario: + test_sequence: + - 'create' + - 'prepare' + - 'converge' + - 'verify' + - 'idempotence' + - 'verify' + - 'destroy' diff --git a/extensions/molecule/apps/remove/cleanup.yml b/extensions/molecule/apps/remove/cleanup.yml new file mode 100644 index 000000000..a7be1cb80 --- /dev/null +++ b/extensions/molecule/apps/remove/cleanup.yml @@ -0,0 +1,2 @@ +- name: 'Converge' + hosts: 'systems_under_test' diff --git a/extensions/molecule/apps/remove/converge.yml b/extensions/molecule/apps/remove/converge.yml new file mode 100644 index 000000000..ac53e7b96 --- /dev/null +++ b/extensions/molecule/apps/remove/converge.yml @@ -0,0 +1,4 @@ +- name: 'Converge' + hosts: 'systems_under_test' + roles: + - role: 'linuxfabrik.lfops.apps' diff --git a/extensions/molecule/apps/remove/inventory/group_vars/systems_under_test.yml b/extensions/molecule/apps/remove/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..e7bd8516a --- /dev/null +++ b/extensions/molecule/apps/remove/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,3 @@ +apps__apps__group_var: + - name: 'less' + state: 'absent' diff --git a/extensions/molecule/apps/remove/inventory/hosts.yml b/extensions/molecule/apps/remove/inventory/hosts.yml new file mode 100644 index 000000000..1f66fe92d --- /dev/null +++ b/extensions/molecule/apps/remove/inventory/hosts.yml @@ -0,0 +1,11 @@ +systems_under_test: + hosts: + debian11_container: + debian12_container: + debian13_container: + rocky8_container: + rocky9_container: + rocky10_container: + ubuntu2004_container: + ubuntu2204_container: + ubuntu2404_container: diff --git a/extensions/molecule/apps/remove/molecule.yml b/extensions/molecule/apps/remove/molecule.yml new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/molecule/apps/remove/verify.yml b/extensions/molecule/apps/remove/verify.yml new file mode 100644 index 000000000..69d846070 --- /dev/null +++ b/extensions/molecule/apps/remove/verify.yml @@ -0,0 +1,11 @@ +- name: 'Verify' + hosts: 'systems_under_test' + gather_facts: true + tasks: + - name: 'Gather the package facts' + ansible.builtin.package_facts: + manager: 'auto' + + - name: 'Assert' + ansible.builtin.assert: + that: '"less" not in ansible_facts.packages' diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml new file mode 100644 index 000000000..bd43d94bf --- /dev/null +++ b/extensions/molecule/config.yml @@ -0,0 +1,66 @@ +dependency: + name: 'galaxy' + options: + requirements-file: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/requirements.yml' + +ansible: + cfg: + defaults: + accelerate_timeout: 5 + ansible_managed: 'This file is managed by Ansible - do not edit' + callbacks_enabled: 'profile_tasks' + fact_caching: 'jsonfile' + fact_caching_connection: '${MOLECULE_EPHEMERAL_DIRECTORY}/.ansible_cache' + fact_caching_timeout: 86400 + forks: 30 + gathering: 'smart' + host_key_checking: false + inventory: 'hosts' + nocows: 1 + retry_files_enabled: true + timeout: 60 + log_path: '${MOLECULE_EPHEMERAL_DIRECTORY}/ansible.log' + inventory_ignore_extensions: + - '~' + - '.orig' + - '.bak' + - '.ini' + - '.cfg' + - '.retry' + - '.pyc' + - '.pyo' + - '.csv' + - '.md' + inventory_ignore_patterns: '(host|group)_files' + stdout_callback: 'yaml' + ssh_connections: + pipelining: true + ssh_args: '-o ControlMaster=auto -o ControlPersist=60s' + executor: + backend: 'ansible-playbook' # or 'ansible-navigator' + args: + ansible_navigator: + - '--inventory=${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/inventory/' + - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' + - '--limit=${MOLECULE_TARGET}' + ansible_playbook: + - '--inventory=${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/inventory/' + - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' + - '--limit=${MOLECULE_TARGET}' + +provisioner: + playbooks: + create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/create.yml' + destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/destroy.yml' + prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/prepare.yml' + +scenario: + test_sequence: + - 'create' + - 'prepare' + - 'converge' + - 'verify' + - 'idempotence' + - 'verify' + - 'cleanup' + - 'destroy' diff --git a/extensions/molecule/default/molecule.yml b/extensions/molecule/default/molecule.yml new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/molecule/inventory/host_vars/debian11_container.yml b/extensions/molecule/inventory/host_vars/debian11_container.yml new file mode 100644 index 000000000..dfb4a5c68 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/debian11_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'debian11_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/debian:11' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/debian12_container.yml b/extensions/molecule/inventory/host_vars/debian12_container.yml new file mode 100644 index 000000000..9aaf69d1a --- /dev/null +++ b/extensions/molecule/inventory/host_vars/debian12_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'debian12_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/debian:12' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/debian13_container.yml b/extensions/molecule/inventory/host_vars/debian13_container.yml new file mode 100644 index 000000000..d70c7fcb8 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/debian13_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'debian13_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/debian:13' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/rocky10_container.yml b/extensions/molecule/inventory/host_vars/rocky10_container.yml new file mode 100644 index 000000000..7ec5284e5 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/rocky10_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'rocky10_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/rockylinux/rockylinux:10' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/rocky8_container.yml b/extensions/molecule/inventory/host_vars/rocky8_container.yml new file mode 100644 index 000000000..74093781b --- /dev/null +++ b/extensions/molecule/inventory/host_vars/rocky8_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'rocky8_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/rockylinux/rockylinux:8' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/rocky9_container.yml b/extensions/molecule/inventory/host_vars/rocky9_container.yml new file mode 100644 index 000000000..154fc081e --- /dev/null +++ b/extensions/molecule/inventory/host_vars/rocky9_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'rocky9_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/rockylinux/rockylinux:9' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004_container.yml b/extensions/molecule/inventory/host_vars/ubuntu2004_container.yml new file mode 100644 index 000000000..61d8d2342 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2004_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'ubuntu2004_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/ubuntu:20.04' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204_container.yml b/extensions/molecule/inventory/host_vars/ubuntu2204_container.yml new file mode 100644 index 000000000..8c74c4786 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2204_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'ubuntu2204_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/ubuntu:22.04' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404_container.yml b/extensions/molecule/inventory/host_vars/ubuntu2404_container.yml new file mode 100644 index 000000000..39815ab11 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2404_container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'ubuntu2404_container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/ubuntu:24.04' +container_privileged: false diff --git a/extensions/molecule/inventory/hosts.yml b/extensions/molecule/inventory/hosts.yml new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/molecule/playbooks/create.yml b/extensions/molecule/playbooks/create.yml new file mode 100644 index 000000000..72b63af24 --- /dev/null +++ b/extensions/molecule/playbooks/create.yml @@ -0,0 +1,34 @@ +- name: 'Create container instances' + hosts: 'localhost' + gather_facts: false + tasks: + - name: 'Create containers from inventory' + containers.podman.podman_container: + hostname: '{{ item }}' + name: '{{ item }}' + image: '{{ hostvars[item]["container_image"] }}' + command: '{{ hostvars[item]["container_command"] | default("sleep 1d") }}' + privileged: '{{ hostvars[item]["container_privileged"] | default(false) }}' + volumes: '{{ hostvars[item]["container_volumes"] | default(omit) }}' + capabilities: '{{ hostvars[item]["container_capabilities"] | default(omit) }}' + systemd: '{{ hostvars[item]["container_systemd"] | default(false) }}' + log_driver: '{{ hostvars[item]["container_log_driver"] | default("json-file") }}' + state: 'started' + register: result + loop: '{{ groups["systems_under_test"] }}' + + - name: 'Verify containers are running' + ansible.builtin.include_tasks: + file: 'tasks/create-fail.yml' + when: > + item.container.State.ExitCode != 0 or + not item.container.State.Running + loop: '{{ result.results }}' + loop_control: + label: '{{ item.container.Name }}' + + #- name: 'Wait for containers to be ready' + # ansible.builtin.wait_for_connection: + # timeout: 30 + # delegate_to: '{{ item }}' + # loop: '{{ groups["systems_under_test"] }}' diff --git a/extensions/molecule/playbooks/destroy.yml b/extensions/molecule/playbooks/destroy.yml new file mode 100644 index 000000000..490af1ae8 --- /dev/null +++ b/extensions/molecule/playbooks/destroy.yml @@ -0,0 +1,21 @@ +- name: 'Destroy container instances' + hosts: 'localhost' + gather_facts: false + tasks: + - name: 'Get info for all containers' + containers.podman.podman_container_info: + name: '{{ item }}' + loop: '{{ groups["systems_under_test"] }}' + register: 'podman_infos' + + - name: 'Kill container if running' + containers.podman.podman_container: + name: '{{ item.item }}' + state: 'absent' #'stopped' + timeout: 2 + loop: '{{ podman_infos.results }}' + loop_control: + label: '{{ item.item }}' + when: + - 'item.containers | length > 0' + - 'item.containers[0].State.Status == "running"' diff --git a/extensions/molecule/playbooks/prepare.yml b/extensions/molecule/playbooks/prepare.yml new file mode 100644 index 000000000..6d6f31f07 --- /dev/null +++ b/extensions/molecule/playbooks/prepare.yml @@ -0,0 +1,19 @@ +- name: 'Prepare containers for Ansible' + hosts: 'systems_under_test' + gather_facts: false + tasks: + - name: 'Install Python using raw module' + ansible.builtin.raw: | + sh -c ''' + if command -v dnf > /dev/null 2>&1; then + dnf install -y python3 + elif command -v yum > /dev/null 2>&1; then + yum install -y python3 + elif command -v apt-get > /dev/null 2>&1; then + apt-get update && apt-get install -y python3 + fi + ''' + changed_when: true + + - name: 'Gather facts after Python is available' + ansible.builtin.setup: diff --git a/extensions/molecule/playbooks/tasks/create-fail.yml b/extensions/molecule/playbooks/tasks/create-fail.yml new file mode 100644 index 000000000..f44eb21d4 --- /dev/null +++ b/extensions/molecule/playbooks/tasks/create-fail.yml @@ -0,0 +1,13 @@ +- name: 'Retrieve container log' + ansible.builtin.command: + cmd: 'podman logs {{ item.container.Name }}' + changed_when: false + register: 'logfile_cmd' + +- name: 'Display container log and fail' + ansible.builtin.fail: + msg: | + Container {{ item.container.Name }} failed to start properly. + Exit Code: {{ item.container.State.ExitCode }} + Running: {{ item.container.State.Running }} + Log output: {{ logfile_cmd.stdout | default('No logs available') }} diff --git a/extensions/molecule/requirements.yml b/extensions/molecule/requirements.yml new file mode 100644 index 000000000..b2ef5cb74 --- /dev/null +++ b/extensions/molecule/requirements.yml @@ -0,0 +1,4 @@ +collections: + - name: 'containers.podman' + version: '>=1.10.0' + - name: 'community.libvirt' From 26f911f11829733f4f490eb961a3c022b5b0ccfa Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 17 Apr 2026 17:02:32 +0200 Subject: [PATCH 02/36] feat(molecule): add vm-based tests for apps role --- extensions/molecule/apps/install/cleanup.yml | 2 - .../inventory/host_vars/debian11_vm.yml | 17 +++ .../inventory/host_vars/debian12_vm.yml | 17 +++ .../inventory/host_vars/debian13_vm.yml | 17 +++ .../inventory/host_vars/rocky10_vm.yml | 17 +++ .../inventory/host_vars/rocky8_vm.yml | 17 +++ .../inventory/host_vars/rocky9_vm.yml | 17 +++ .../kernel_settings/sysfs/converge.yml | 4 + .../group_vars/systems_under_test.yml | 5 + .../kernel_settings/sysfs/inventory/hosts.yml | 11 ++ .../kernel_settings/sysfs/molecule.yml | 5 + extensions/molecule/playbooks/vm-create.yml | 109 ++++++++++++++++++ extensions/molecule/playbooks/vm-destroy.yml | 28 +++++ extensions/molecule/playbooks/vm-prepare.yml | 10 ++ 14 files changed, 274 insertions(+), 2 deletions(-) delete mode 100644 extensions/molecule/apps/install/cleanup.yml create mode 100644 extensions/molecule/inventory/host_vars/debian11_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/debian12_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/debian13_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/rocky10_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/rocky8_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/rocky9_vm.yml create mode 100644 extensions/molecule/kernel_settings/sysfs/converge.yml create mode 100644 extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml create mode 100644 extensions/molecule/kernel_settings/sysfs/molecule.yml create mode 100644 extensions/molecule/playbooks/vm-create.yml create mode 100644 extensions/molecule/playbooks/vm-destroy.yml create mode 100644 extensions/molecule/playbooks/vm-prepare.yml diff --git a/extensions/molecule/apps/install/cleanup.yml b/extensions/molecule/apps/install/cleanup.yml deleted file mode 100644 index a7be1cb80..000000000 --- a/extensions/molecule/apps/install/cleanup.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: 'Converge' - hosts: 'systems_under_test' diff --git a/extensions/molecule/inventory/host_vars/debian11_vm.yml b/extensions/molecule/inventory/host_vars/debian11_vm.yml new file mode 100644 index 000000000..6e9479e51 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/debian11_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-amd64.qcow2' + +kvm_vm__autostart: false +kvm_vm__base_image: 'debian-11-genericcloud-amd64.qcow2' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'debian11' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/debian12_vm.yml b/extensions/molecule/inventory/host_vars/debian12_vm.yml new file mode 100644 index 000000000..68ae91634 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/debian12_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2' + +kvm_vm__autostart: false +kvm_vm__base_image: 'debian-12-genericcloud-amd64.qcow2' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'debian12' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/debian13_vm.yml b/extensions/molecule/inventory/host_vars/debian13_vm.yml new file mode 100644 index 000000000..6cf4fbf84 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/debian13_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2' + +kvm_vm__autostart: false +kvm_vm__base_image: 'debian-13-genericcloud-amd64.qcow2' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'debian13' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/rocky10_vm.yml b/extensions/molecule/inventory/host_vars/rocky10_vm.yml new file mode 100644 index 000000000..b2d9d39cb --- /dev/null +++ b/extensions/molecule/inventory/host_vars/rocky10_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://dl.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2' + +kvm_vm__autostart: false +kvm_vm__base_image: 'Rocky-10-GenericCloud-Base.latest.x86_64.qcow2' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'rocky10' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/rocky8_vm.yml b/extensions/molecule/inventory/host_vars/rocky8_vm.yml new file mode 100644 index 000000000..0520d860e --- /dev/null +++ b/extensions/molecule/inventory/host_vars/rocky8_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2' + +kvm_vm__autostart: false +kvm_vm__base_image: 'Rocky-8-GenericCloud-Base.latest.x86_64.qcow2' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'rocky8' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/rocky9_vm.yml b/extensions/molecule/inventory/host_vars/rocky9_vm.yml new file mode 100644 index 000000000..77072841a --- /dev/null +++ b/extensions/molecule/inventory/host_vars/rocky9_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2' + +kvm_vm__autostart: false +kvm_vm__base_image: 'Rocky-9-GenericCloud-Base.latest.x86_64.qcow2' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'rocky9' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/kernel_settings/sysfs/converge.yml b/extensions/molecule/kernel_settings/sysfs/converge.yml new file mode 100644 index 000000000..991e95dcd --- /dev/null +++ b/extensions/molecule/kernel_settings/sysfs/converge.yml @@ -0,0 +1,4 @@ +- name: 'Converge' + hosts: 'systems_under_test' + roles: + - role: 'linuxfabrik.lfops.kernel_settings' diff --git a/extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml b/extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..4433ae6dc --- /dev/null +++ b/extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,5 @@ +kernel_settings__sysfs__group_var: + - name: '/sys/kernel/debug/x86/pti_enabled' + value: 0 + - name: '/sys/kernel/debug/x86/retp_enabled' + value: 0 diff --git a/extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml b/extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml new file mode 100644 index 000000000..fe066742f --- /dev/null +++ b/extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml @@ -0,0 +1,11 @@ +systems_under_test: + hosts: + debian11_vm: + debian12_vm: + debian13_vm: + rocky8_vm: + rocky9_vm: + rocky10_vm: + #ubuntu2004_container: + #ubuntu2204_container: + #ubuntu2404_container: diff --git a/extensions/molecule/kernel_settings/sysfs/molecule.yml b/extensions/molecule/kernel_settings/sysfs/molecule.yml new file mode 100644 index 000000000..4c0b6ce0a --- /dev/null +++ b/extensions/molecule/kernel_settings/sysfs/molecule.yml @@ -0,0 +1,5 @@ +provisioner: + playbooks: + create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-create.yml' + destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-destroy.yml' + prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-prepare.yml' diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml new file mode 100644 index 000000000..152ee7558 --- /dev/null +++ b/extensions/molecule/playbooks/vm-create.yml @@ -0,0 +1,109 @@ +- name: 'Prepare for VM creation' + hosts: 'localhost' + gather_facts: false + vars: + molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' + molecule_vm_hosts: >- + {{ groups['systems_under_test'] | default([]) + | select('in', hostvars | dict2items + | selectattr('value.vm_image_url', 'defined') + | map(attribute='key') | list) + | list }} + tasks: + + - name: 'Generate ephemeral SSH keypair' + community.crypto.openssh_keypair: + path: '{{ molecule_ephemeral_dir }}/molecule_key' + type: 'ed25519' + comment: 'molecule' + + - name: 'Read SSH public key' + ansible.builtin.slurp: + src: '{{ molecule_ephemeral_dir }}/molecule_key.pub' + register: 'molecule_pubkey_result' + + - name: 'Set SSH public key fact' + ansible.builtin.set_fact: + molecule_ssh_pubkey: '{{ molecule_pubkey_result["content"] | b64decode | trim }}' + + - name: 'Get info on storage pools' + community.libvirt.virt_pool: + command: 'info' + register: 'molecule_pool_info' + + - name: 'Download cloud images into storage pool' + ansible.builtin.get_url: + url: '{{ hostvars[item]["vm_image_url"] }}' + dest: '{{ molecule_pool_info["pools"]["default"]["path"] }}/{{ hostvars[item]["kvm_vm__base_image"] }}' + mode: '0644' + loop: '{{ molecule_vm_hosts }}' + loop_control: + label: '{{ hostvars[item]["kvm_vm__base_image"] }}' + + - name: 'Ensure ephemeral inventory directory exists' + ansible.builtin.file: + path: '{{ molecule_ephemeral_dir }}/inventory' + state: 'directory' + mode: '0755' + + +- name: 'Create VM instances using kvm_vm role' + hosts: 'molecule' + gather_facts: false + roles: + - role: 'linuxfabrik.lfops.kvm_vm' + vars: + kvm_vm__ssh_authorized_keys: + - '{{ hostvars["localhost"]["molecule_ssh_pubkey"] }}' + kvm_vm__state: 'running' + + +- name: 'Discover VM IPs and write dynamic inventory' + hosts: 'localhost' + gather_facts: false + vars: + molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' + molecule_vm_hosts: >- + {{ groups['systems_under_test'] | default([]) + | select('in', hostvars | dict2items + | selectattr('value.vm_image_url', 'defined') + | map(attribute='key') | list) + | list }} + tasks: + + - name: 'Wait for VMs to obtain IP addresses' + ansible.builtin.command: + cmd: 'virsh domifaddr {{ item }} --source arp' + register: 'domifaddr_result' + until: 'domifaddr_result.stdout_lines | select("match", ".*ipv4.*") | list | length > 0' + retries: 30 + delay: 5 + changed_when: false + loop: '{{ molecule_vm_hosts }}' + + - name: 'Parse VM IP addresses' + ansible.builtin.set_fact: + molecule_vm_ips: >- + {{ molecule_vm_ips | default({}) | combine({ + item.item: item.stdout_lines + | select('match', '.*ipv4.*') + | first + | regex_replace('.*\s+(\d+\.\d+\.\d+\.\d+)\/.*', '\1') + }) }} + loop: '{{ domifaddr_result.results }}' + loop_control: + label: '{{ item.item }}' + + - name: 'Write dynamic VM inventory' + ansible.builtin.copy: + content: | + molecule: + hosts: + {% for host in molecule_vm_hosts %} + {{ host }}: + ansible_host: '{{ molecule_vm_ips[host] }}' + ansible_ssh_private_key_file: '{{ molecule_ephemeral_dir }}/molecule_key' + ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + {% endfor %} + dest: '{{ molecule_ephemeral_dir }}/inventory/molecule_vm.yml' + mode: '0644' diff --git a/extensions/molecule/playbooks/vm-destroy.yml b/extensions/molecule/playbooks/vm-destroy.yml new file mode 100644 index 000000000..f68633baf --- /dev/null +++ b/extensions/molecule/playbooks/vm-destroy.yml @@ -0,0 +1,28 @@ +- name: 'Destroy VM instances using kvm_vm role' + hosts: 'systems_under_test' + gather_facts: false + roles: + - role: 'linuxfabrik.lfops.kvm_vm' + vars: + kvm_vm__state: 'absent' + + +- name: 'Clean up Molecule artifacts' + hosts: 'localhost' + gather_facts: false + vars: + molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' + tasks: + + - name: 'Remove ephemeral SSH keypair' + ansible.builtin.file: + path: '{{ molecule_ephemeral_dir }}/{{ item }}' + state: 'absent' + loop: + - 'molecule_key' + - 'molecule_key.pub' + + - name: 'Remove dynamic inventory' + ansible.builtin.file: + path: '{{ molecule_ephemeral_dir }}/inventory' + state: 'absent' diff --git a/extensions/molecule/playbooks/vm-prepare.yml b/extensions/molecule/playbooks/vm-prepare.yml new file mode 100644 index 000000000..9ecccf529 --- /dev/null +++ b/extensions/molecule/playbooks/vm-prepare.yml @@ -0,0 +1,10 @@ +- name: 'Prepare VMs for Ansible' + hosts: 'systems_under_test' + gather_facts: false + tasks: + - name: 'Wait for SSH to be available' + ansible.builtin.wait_for_connection: + timeout: 120 + + - name: 'Gather facts' + ansible.builtin.setup: From b7f74b1dd41b55579e873b77757fe6789943cbd6 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 24 Apr 2026 17:03:38 +0200 Subject: [PATCH 03/36] fix(molecule): fix vms for tests not being correctly created --- .gitignore | 1 + .../inventory/host_vars/ubuntu2004_vm.yml | 17 +++++ .../inventory/host_vars/ubuntu2204_vm.yml | 17 +++++ .../inventory/host_vars/ubuntu2404_vm.yml | 17 +++++ .../{sysfs => sysctl}/converge.yml | 0 .../group_vars/systems_under_test.yml | 3 + .../{sysfs => sysctl}/inventory/hosts.yml | 6 +- .../{sysfs => sysctl}/molecule.yml | 0 .../kernel_settings/sysctl/verify.yml | 12 ++++ .../group_vars/systems_under_test.yml | 5 -- extensions/molecule/playbooks/vm-create.yml | 67 +++++++++---------- extensions/molecule/playbooks/vm-destroy.yml | 1 + 12 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2004_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2204_vm.yml create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2404_vm.yml rename extensions/molecule/kernel_settings/{sysfs => sysctl}/converge.yml (100%) create mode 100644 extensions/molecule/kernel_settings/sysctl/inventory/group_vars/systems_under_test.yml rename extensions/molecule/kernel_settings/{sysfs => sysctl}/inventory/hosts.yml (60%) rename extensions/molecule/kernel_settings/{sysfs => sysctl}/molecule.yml (100%) create mode 100644 extensions/molecule/kernel_settings/sysctl/verify.yml delete mode 100644 extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml diff --git a/.gitignore b/.gitignore index 7007b1648..de97de82e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ playbooks/test.yml roles/test context/ particle/.vagrant +extensions/molecule/**/*.retry # mkdocs documentation /docs/CHANGELOG.md diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004_vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2004_vm.yml new file mode 100644 index 000000000..8202ec9da --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2004_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img' + +kvm_vm__autostart: false +kvm_vm__base_image: 'focal-server-cloudimg-amd64.img' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'ubuntu20.04' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204_vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2204_vm.yml new file mode 100644 index 000000000..944045dbc --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2204_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img' + +kvm_vm__autostart: false +kvm_vm__base_image: 'jammy-server-cloudimg-amd64.img' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'ubuntu22.04' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404_vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2404_vm.yml new file mode 100644 index 000000000..c21fc4068 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2404_vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img' + +kvm_vm__autostart: false +kvm_vm__base_image: 'noble-server-cloudimg-amd64.img' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'ubuntu24.04' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/kernel_settings/sysfs/converge.yml b/extensions/molecule/kernel_settings/sysctl/converge.yml similarity index 100% rename from extensions/molecule/kernel_settings/sysfs/converge.yml rename to extensions/molecule/kernel_settings/sysctl/converge.yml diff --git a/extensions/molecule/kernel_settings/sysctl/inventory/group_vars/systems_under_test.yml b/extensions/molecule/kernel_settings/sysctl/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..b7927d41e --- /dev/null +++ b/extensions/molecule/kernel_settings/sysctl/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,3 @@ +kernel_settings__sysctl__group_var: + - name: 'vm.overcommit_memory' + value: 1 diff --git a/extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml similarity index 60% rename from extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml rename to extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml index fe066742f..05a410225 100644 --- a/extensions/molecule/kernel_settings/sysfs/inventory/hosts.yml +++ b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml @@ -6,6 +6,6 @@ systems_under_test: rocky8_vm: rocky9_vm: rocky10_vm: - #ubuntu2004_container: - #ubuntu2204_container: - #ubuntu2404_container: + ubuntu2004_vm: + ubuntu2204_vm: + ubuntu2404_vm: diff --git a/extensions/molecule/kernel_settings/sysfs/molecule.yml b/extensions/molecule/kernel_settings/sysctl/molecule.yml similarity index 100% rename from extensions/molecule/kernel_settings/sysfs/molecule.yml rename to extensions/molecule/kernel_settings/sysctl/molecule.yml diff --git a/extensions/molecule/kernel_settings/sysctl/verify.yml b/extensions/molecule/kernel_settings/sysctl/verify.yml new file mode 100644 index 000000000..e40812250 --- /dev/null +++ b/extensions/molecule/kernel_settings/sysctl/verify.yml @@ -0,0 +1,12 @@ +- name: 'Verify' + hosts: 'systems_under_test' + gather_facts: false + tasks: + - name: 'Read sysctl vm.overcommit_memory from procfs' + ansible.builtin.slurp: + src: '/proc/sys/vm/overcommit_memory' + register: 'sysctl_vm_overcommit_memory_result' + + - name: 'Assert' + ansible.builtin.assert: + that: 'sysctl_vm_overcommit_memory_result.content | b64decode | int == 1' diff --git a/extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml b/extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml deleted file mode 100644 index 4433ae6dc..000000000 --- a/extensions/molecule/kernel_settings/sysfs/inventory/group_vars/systems_under_test.yml +++ /dev/null @@ -1,5 +0,0 @@ -kernel_settings__sysfs__group_var: - - name: '/sys/kernel/debug/x86/pti_enabled' - value: 0 - - name: '/sys/kernel/debug/x86/retp_enabled' - value: 0 diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml index 152ee7558..ecacb2212 100644 --- a/extensions/molecule/playbooks/vm-create.yml +++ b/extensions/molecule/playbooks/vm-create.yml @@ -1,14 +1,8 @@ - name: 'Prepare for VM creation' - hosts: 'localhost' + hosts: 'systems_under_test' gather_facts: false vars: molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' - molecule_vm_hosts: >- - {{ groups['systems_under_test'] | default([]) - | select('in', hostvars | dict2items - | selectattr('value.vm_image_url', 'defined') - | map(attribute='key') | list) - | list }} tasks: - name: 'Generate ephemeral SSH keypair' @@ -16,39 +10,51 @@ path: '{{ molecule_ephemeral_dir }}/molecule_key' type: 'ed25519' comment: 'molecule' + delegate_to: 'localhost' + run_once: true - name: 'Read SSH public key' ansible.builtin.slurp: src: '{{ molecule_ephemeral_dir }}/molecule_key.pub' + delegate_to: 'localhost' register: 'molecule_pubkey_result' + run_once: true - name: 'Set SSH public key fact' ansible.builtin.set_fact: molecule_ssh_pubkey: '{{ molecule_pubkey_result["content"] | b64decode | trim }}' + delegate_to: 'localhost' + delegate_facts: true + run_once: true - name: 'Get info on storage pools' community.libvirt.virt_pool: command: 'info' + delegate_to: 'localhost' register: 'molecule_pool_info' + run_once: true - name: 'Download cloud images into storage pool' ansible.builtin.get_url: - url: '{{ hostvars[item]["vm_image_url"] }}' - dest: '{{ molecule_pool_info["pools"]["default"]["path"] }}/{{ hostvars[item]["kvm_vm__base_image"] }}' + url: '{{ vm_image_url }}' + dest: '{{ molecule_pool_info["pools"]["default"]["path"] }}/{{ kvm_vm__base_image }}' mode: '0644' - loop: '{{ molecule_vm_hosts }}' - loop_control: - label: '{{ hostvars[item]["kvm_vm__base_image"] }}' + become: true + delegate_to: 'localhost' + when: 'vm_image_url is defined' - name: 'Ensure ephemeral inventory directory exists' ansible.builtin.file: path: '{{ molecule_ephemeral_dir }}/inventory' state: 'directory' mode: '0755' + delegate_to: 'localhost' + run_once: true - name: 'Create VM instances using kvm_vm role' - hosts: 'molecule' + become: true + hosts: 'systems_under_test' gather_facts: false roles: - role: 'linuxfabrik.lfops.kvm_vm' @@ -59,51 +65,44 @@ - name: 'Discover VM IPs and write dynamic inventory' - hosts: 'localhost' + hosts: 'systems_under_test' gather_facts: false vars: molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' - molecule_vm_hosts: >- - {{ groups['systems_under_test'] | default([]) - | select('in', hostvars | dict2items - | selectattr('value.vm_image_url', 'defined') - | map(attribute='key') | list) - | list }} tasks: - name: 'Wait for VMs to obtain IP addresses' ansible.builtin.command: - cmd: 'virsh domifaddr {{ item }} --source arp' + cmd: 'virsh domifaddr {{ inventory_hostname }} --source arp' register: 'domifaddr_result' until: 'domifaddr_result.stdout_lines | select("match", ".*ipv4.*") | list | length > 0' retries: 30 delay: 5 changed_when: false - loop: '{{ molecule_vm_hosts }}' + delegate_to: 'localhost' - name: 'Parse VM IP addresses' ansible.builtin.set_fact: - molecule_vm_ips: >- - {{ molecule_vm_ips | default({}) | combine({ - item.item: item.stdout_lines - | select('match', '.*ipv4.*') - | first - | regex_replace('.*\s+(\d+\.\d+\.\d+\.\d+)\/.*', '\1') - }) }} - loop: '{{ domifaddr_result.results }}' - loop_control: - label: '{{ item.item }}' + molecule_vm_ip: >- + {{ + domifaddr_result.stdout_lines + | select('match', '.*ipv4.*') + | first + | regex_replace('.*\s+(\d+\.\d+\.\d+\.\d+)\/.*', '\1') + }} - name: 'Write dynamic VM inventory' ansible.builtin.copy: content: | molecule: hosts: - {% for host in molecule_vm_hosts %} + {% for host in ansible_play_hosts %} {{ host }}: - ansible_host: '{{ molecule_vm_ips[host] }}' + ansible_host: '{{ hostvars[host]["molecule_vm_ip"] }}' ansible_ssh_private_key_file: '{{ molecule_ephemeral_dir }}/molecule_key' ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' {% endfor %} dest: '{{ molecule_ephemeral_dir }}/inventory/molecule_vm.yml' mode: '0644' + delegate_to: 'localhost' + run_once: true diff --git a/extensions/molecule/playbooks/vm-destroy.yml b/extensions/molecule/playbooks/vm-destroy.yml index f68633baf..a33533eb7 100644 --- a/extensions/molecule/playbooks/vm-destroy.yml +++ b/extensions/molecule/playbooks/vm-destroy.yml @@ -1,4 +1,5 @@ - name: 'Destroy VM instances using kvm_vm role' + become: true hosts: 'systems_under_test' gather_facts: false roles: From 588e4f72f644dc9d21280d7e293a8278503dcd8c Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 24 Apr 2026 17:05:04 +0200 Subject: [PATCH 04/36] fix(molecule): replace loops with explicit host matching for better stability --- extensions/molecule/apps/install/molecule.yml | 1 + extensions/molecule/apps/remove/molecule.yml | 1 + extensions/molecule/playbooks/create.yml | 40 ++++++++----------- extensions/molecule/playbooks/destroy.yml | 21 ++++------ extensions/molecule/playbooks/prepare.yml | 2 +- 5 files changed, 28 insertions(+), 37 deletions(-) diff --git a/extensions/molecule/apps/install/molecule.yml b/extensions/molecule/apps/install/molecule.yml index e69de29bb..1e47cbff8 100644 --- a/extensions/molecule/apps/install/molecule.yml +++ b/extensions/molecule/apps/install/molecule.yml @@ -0,0 +1 @@ +# Molecule scenario marker diff --git a/extensions/molecule/apps/remove/molecule.yml b/extensions/molecule/apps/remove/molecule.yml index e69de29bb..1e47cbff8 100644 --- a/extensions/molecule/apps/remove/molecule.yml +++ b/extensions/molecule/apps/remove/molecule.yml @@ -0,0 +1 @@ +# Molecule scenario marker diff --git a/extensions/molecule/playbooks/create.yml b/extensions/molecule/playbooks/create.yml index 72b63af24..e8b60c328 100644 --- a/extensions/molecule/playbooks/create.yml +++ b/extensions/molecule/playbooks/create.yml @@ -1,34 +1,28 @@ - name: 'Create container instances' - hosts: 'localhost' + hosts: 'systems_under_test' gather_facts: false tasks: - name: 'Create containers from inventory' containers.podman.podman_container: - hostname: '{{ item }}' - name: '{{ item }}' - image: '{{ hostvars[item]["container_image"] }}' - command: '{{ hostvars[item]["container_command"] | default("sleep 1d") }}' - privileged: '{{ hostvars[item]["container_privileged"] | default(false) }}' - volumes: '{{ hostvars[item]["container_volumes"] | default(omit) }}' - capabilities: '{{ hostvars[item]["container_capabilities"] | default(omit) }}' - systemd: '{{ hostvars[item]["container_systemd"] | default(false) }}' - log_driver: '{{ hostvars[item]["container_log_driver"] | default("json-file") }}' + hostname: '{{ inventory_hostname }}' + name: '{{ inventory_hostname }}' + image: '{{ container_image }}' + command: '{{ container_command | default("sleep 1d") }}' + privileged: '{{ container_privileged | default(false) }}' + volumes: '{{ container_volumes | default(omit) }}' + capabilities: '{{ container_capabilities | default(omit) }}' + systemd: '{{ container_systemd | default(false) }}' + log_driver: '{{ container_log_driver | default("json-file") }}' state: 'started' - register: result - loop: '{{ groups["systems_under_test"] }}' + register: 'create_result' + delegate_to: 'localhost' + # Not using ansible.builtin.wait_for_connection here as it depends on a working Python + # installation inside the container; however, some container images are shipped with the + # bare minimum and thus do not contain a Python interpreter. - name: 'Verify containers are running' ansible.builtin.include_tasks: file: 'tasks/create-fail.yml' when: > - item.container.State.ExitCode != 0 or - not item.container.State.Running - loop: '{{ result.results }}' - loop_control: - label: '{{ item.container.Name }}' - - #- name: 'Wait for containers to be ready' - # ansible.builtin.wait_for_connection: - # timeout: 30 - # delegate_to: '{{ item }}' - # loop: '{{ groups["systems_under_test"] }}' + create_result.container.State.ExitCode != 0 or + not create_result.container.State.Running diff --git a/extensions/molecule/playbooks/destroy.yml b/extensions/molecule/playbooks/destroy.yml index 490af1ae8..1c45b6956 100644 --- a/extensions/molecule/playbooks/destroy.yml +++ b/extensions/molecule/playbooks/destroy.yml @@ -1,21 +1,16 @@ - name: 'Destroy container instances' - hosts: 'localhost' + hosts: 'systems_under_test' gather_facts: false tasks: - - name: 'Get info for all containers' - containers.podman.podman_container_info: - name: '{{ item }}' - loop: '{{ groups["systems_under_test"] }}' - register: 'podman_infos' + #- name: 'Get info for all containers' + # containers.podman.podman_container_info: + # name: '{{ inventory_hostname }}' + # delegate_to: 'localhost' + # register: 'container_info' - name: 'Kill container if running' containers.podman.podman_container: - name: '{{ item.item }}' + name: '{{ inventory_hostname }}' state: 'absent' #'stopped' timeout: 2 - loop: '{{ podman_infos.results }}' - loop_control: - label: '{{ item.item }}' - when: - - 'item.containers | length > 0' - - 'item.containers[0].State.Status == "running"' + delegate_to: 'localhost' diff --git a/extensions/molecule/playbooks/prepare.yml b/extensions/molecule/playbooks/prepare.yml index 6d6f31f07..761378648 100644 --- a/extensions/molecule/playbooks/prepare.yml +++ b/extensions/molecule/playbooks/prepare.yml @@ -2,7 +2,7 @@ hosts: 'systems_under_test' gather_facts: false tasks: - - name: 'Install Python using raw module' + - name: 'Install Python using raw module (in case it is missing from the container image)' ansible.builtin.raw: | sh -c ''' if command -v dnf > /dev/null 2>&1; then From 41d0a37e658a2248b865ba5334d57783f6998880 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 8 May 2026 17:07:39 +0200 Subject: [PATCH 05/36] feat(molecule): implement initial playbook testing based on setup_basic --- .../molecule/apps/install/inventory/hosts.yml | 18 +++++++-------- extensions/molecule/apps/molecule.yml | 9 -------- .../molecule/apps/remove/inventory/hosts.yml | 18 +++++++-------- extensions/molecule/config.yml | 5 ++-- extensions/molecule/default/molecule.yml | 1 + ...1_container.yml => debian11-container.yml} | 2 +- .../{debian11_vm.yml => debian11-vm.yml} | 0 ...2_container.yml => debian12-container.yml} | 2 +- .../{debian12_vm.yml => debian12-vm.yml} | 0 ...3_container.yml => debian13-container.yml} | 2 +- .../{debian13_vm.yml => debian13-vm.yml} | 0 ...10_container.yml => rocky10-container.yml} | 2 +- .../{rocky10_vm.yml => rocky10-vm.yml} | 0 ...ky8_container.yml => rocky8-container.yml} | 2 +- .../{rocky8_vm.yml => rocky8-vm.yml} | 0 ...ky9_container.yml => rocky9-container.yml} | 2 +- .../{rocky9_vm.yml => rocky9-vm.yml} | 0 ...container.yml => ubuntu2004-container.yml} | 2 +- .../{ubuntu2004_vm.yml => ubuntu2004-vm.yml} | 0 ...container.yml => ubuntu2204-container.yml} | 2 +- .../{ubuntu2204_vm.yml => ubuntu2204-vm.yml} | 0 ...container.yml => ubuntu2404-container.yml} | 2 +- .../{ubuntu2404_vm.yml => ubuntu2404-vm.yml} | 0 .../host_vars/ubuntu2604-container.yml | 6 +++++ .../inventory/host_vars/ubuntu2604-vm.yml | 17 ++++++++++++++ .../sysctl/inventory/hosts.yml | 12 +++------- extensions/molecule/playbooks/destroy.yml | 7 +----- extensions/molecule/setup_basic/converge.yml | 2 ++ .../group_vars/systems_under_test.yml | 5 ++++ .../inventory/host_vars/rocky10-vm.yml | 1 + .../molecule/setup_basic/inventory/hosts.yml | 23 +++++++++++++++++++ extensions/molecule/setup_basic/molecule.yml | 5 ++++ extensions/molecule/setup_basic/verify.yml | 0 33 files changed, 93 insertions(+), 54 deletions(-) delete mode 100644 extensions/molecule/apps/molecule.yml rename extensions/molecule/inventory/host_vars/{debian11_container.yml => debian11-container.yml} (81%) rename extensions/molecule/inventory/host_vars/{debian11_vm.yml => debian11-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{debian12_container.yml => debian12-container.yml} (81%) rename extensions/molecule/inventory/host_vars/{debian12_vm.yml => debian12-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{debian13_container.yml => debian13-container.yml} (81%) rename extensions/molecule/inventory/host_vars/{debian13_vm.yml => debian13-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{rocky10_container.yml => rocky10-container.yml} (82%) rename extensions/molecule/inventory/host_vars/{rocky10_vm.yml => rocky10-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{rocky8_container.yml => rocky8-container.yml} (82%) rename extensions/molecule/inventory/host_vars/{rocky8_vm.yml => rocky8-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{rocky9_container.yml => rocky9-container.yml} (82%) rename extensions/molecule/inventory/host_vars/{rocky9_vm.yml => rocky9-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{ubuntu2004_container.yml => ubuntu2004-container.yml} (80%) rename extensions/molecule/inventory/host_vars/{ubuntu2004_vm.yml => ubuntu2004-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{ubuntu2204_container.yml => ubuntu2204-container.yml} (80%) rename extensions/molecule/inventory/host_vars/{ubuntu2204_vm.yml => ubuntu2204-vm.yml} (100%) rename extensions/molecule/inventory/host_vars/{ubuntu2404_container.yml => ubuntu2404-container.yml} (80%) rename extensions/molecule/inventory/host_vars/{ubuntu2404_vm.yml => ubuntu2404-vm.yml} (100%) create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2604-container.yml create mode 100644 extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml create mode 100644 extensions/molecule/setup_basic/converge.yml create mode 100644 extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/setup_basic/inventory/host_vars/rocky10-vm.yml create mode 100644 extensions/molecule/setup_basic/inventory/hosts.yml create mode 100644 extensions/molecule/setup_basic/molecule.yml create mode 100644 extensions/molecule/setup_basic/verify.yml diff --git a/extensions/molecule/apps/install/inventory/hosts.yml b/extensions/molecule/apps/install/inventory/hosts.yml index 1f66fe92d..9068a5e66 100644 --- a/extensions/molecule/apps/install/inventory/hosts.yml +++ b/extensions/molecule/apps/install/inventory/hosts.yml @@ -1,11 +1,11 @@ systems_under_test: hosts: - debian11_container: - debian12_container: - debian13_container: - rocky8_container: - rocky9_container: - rocky10_container: - ubuntu2004_container: - ubuntu2204_container: - ubuntu2404_container: + debian11-container: + debian12-container: + debian13-container: + rocky8-container: + rocky9-container: + rocky10-container: + ubuntu2004-container: + ubuntu2204-container: + ubuntu2404-container: diff --git a/extensions/molecule/apps/molecule.yml b/extensions/molecule/apps/molecule.yml deleted file mode 100644 index 2cc4a7111..000000000 --- a/extensions/molecule/apps/molecule.yml +++ /dev/null @@ -1,9 +0,0 @@ -scenario: - test_sequence: - - 'create' - - 'prepare' - - 'converge' - - 'verify' - - 'idempotence' - - 'verify' - - 'destroy' diff --git a/extensions/molecule/apps/remove/inventory/hosts.yml b/extensions/molecule/apps/remove/inventory/hosts.yml index 1f66fe92d..9068a5e66 100644 --- a/extensions/molecule/apps/remove/inventory/hosts.yml +++ b/extensions/molecule/apps/remove/inventory/hosts.yml @@ -1,11 +1,11 @@ systems_under_test: hosts: - debian11_container: - debian12_container: - debian13_container: - rocky8_container: - rocky9_container: - rocky10_container: - ubuntu2004_container: - ubuntu2204_container: - ubuntu2404_container: + debian11-container: + debian12-container: + debian13-container: + rocky8-container: + rocky9-container: + rocky10-container: + ubuntu2004-container: + ubuntu2204-container: + ubuntu2404-container: diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index bd43d94bf..d27f71086 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -42,11 +42,11 @@ ansible: ansible_navigator: - '--inventory=${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/inventory/' - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' - - '--limit=${MOLECULE_TARGET}' + - '--limit=${LFOPS_TEST_TARGETS}' ansible_playbook: - '--inventory=${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/inventory/' - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' - - '--limit=${MOLECULE_TARGET}' + - '--limit=${LFOPS_TEST_TARGETS}' provisioner: playbooks: @@ -62,5 +62,4 @@ scenario: - 'verify' - 'idempotence' - 'verify' - - 'cleanup' - 'destroy' diff --git a/extensions/molecule/default/molecule.yml b/extensions/molecule/default/molecule.yml index e69de29bb..1e47cbff8 100644 --- a/extensions/molecule/default/molecule.yml +++ b/extensions/molecule/default/molecule.yml @@ -0,0 +1 @@ +# Molecule scenario marker diff --git a/extensions/molecule/inventory/host_vars/debian11_container.yml b/extensions/molecule/inventory/host_vars/debian11-container.yml similarity index 81% rename from extensions/molecule/inventory/host_vars/debian11_container.yml rename to extensions/molecule/inventory/host_vars/debian11-container.yml index dfb4a5c68..5815e39b9 100644 --- a/extensions/molecule/inventory/host_vars/debian11_container.yml +++ b/extensions/molecule/inventory/host_vars/debian11-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'debian11_container' +ansible_host: 'debian11-container' container_command: 'sleep 1d' container_image: 'docker.io/library/debian:11' diff --git a/extensions/molecule/inventory/host_vars/debian11_vm.yml b/extensions/molecule/inventory/host_vars/debian11-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/debian11_vm.yml rename to extensions/molecule/inventory/host_vars/debian11-vm.yml diff --git a/extensions/molecule/inventory/host_vars/debian12_container.yml b/extensions/molecule/inventory/host_vars/debian12-container.yml similarity index 81% rename from extensions/molecule/inventory/host_vars/debian12_container.yml rename to extensions/molecule/inventory/host_vars/debian12-container.yml index 9aaf69d1a..1cee9a253 100644 --- a/extensions/molecule/inventory/host_vars/debian12_container.yml +++ b/extensions/molecule/inventory/host_vars/debian12-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'debian12_container' +ansible_host: 'debian12-container' container_command: 'sleep 1d' container_image: 'docker.io/library/debian:12' diff --git a/extensions/molecule/inventory/host_vars/debian12_vm.yml b/extensions/molecule/inventory/host_vars/debian12-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/debian12_vm.yml rename to extensions/molecule/inventory/host_vars/debian12-vm.yml diff --git a/extensions/molecule/inventory/host_vars/debian13_container.yml b/extensions/molecule/inventory/host_vars/debian13-container.yml similarity index 81% rename from extensions/molecule/inventory/host_vars/debian13_container.yml rename to extensions/molecule/inventory/host_vars/debian13-container.yml index d70c7fcb8..48ef11937 100644 --- a/extensions/molecule/inventory/host_vars/debian13_container.yml +++ b/extensions/molecule/inventory/host_vars/debian13-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'debian13_container' +ansible_host: 'debian13-container' container_command: 'sleep 1d' container_image: 'docker.io/library/debian:13' diff --git a/extensions/molecule/inventory/host_vars/debian13_vm.yml b/extensions/molecule/inventory/host_vars/debian13-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/debian13_vm.yml rename to extensions/molecule/inventory/host_vars/debian13-vm.yml diff --git a/extensions/molecule/inventory/host_vars/rocky10_container.yml b/extensions/molecule/inventory/host_vars/rocky10-container.yml similarity index 82% rename from extensions/molecule/inventory/host_vars/rocky10_container.yml rename to extensions/molecule/inventory/host_vars/rocky10-container.yml index 7ec5284e5..1fb37d96e 100644 --- a/extensions/molecule/inventory/host_vars/rocky10_container.yml +++ b/extensions/molecule/inventory/host_vars/rocky10-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'rocky10_container' +ansible_host: 'rocky10-container' container_command: 'sleep 1d' container_image: 'docker.io/rockylinux/rockylinux:10' diff --git a/extensions/molecule/inventory/host_vars/rocky10_vm.yml b/extensions/molecule/inventory/host_vars/rocky10-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/rocky10_vm.yml rename to extensions/molecule/inventory/host_vars/rocky10-vm.yml diff --git a/extensions/molecule/inventory/host_vars/rocky8_container.yml b/extensions/molecule/inventory/host_vars/rocky8-container.yml similarity index 82% rename from extensions/molecule/inventory/host_vars/rocky8_container.yml rename to extensions/molecule/inventory/host_vars/rocky8-container.yml index 74093781b..71cd5d599 100644 --- a/extensions/molecule/inventory/host_vars/rocky8_container.yml +++ b/extensions/molecule/inventory/host_vars/rocky8-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'rocky8_container' +ansible_host: 'rocky8-container' container_command: 'sleep 1d' container_image: 'docker.io/rockylinux/rockylinux:8' diff --git a/extensions/molecule/inventory/host_vars/rocky8_vm.yml b/extensions/molecule/inventory/host_vars/rocky8-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/rocky8_vm.yml rename to extensions/molecule/inventory/host_vars/rocky8-vm.yml diff --git a/extensions/molecule/inventory/host_vars/rocky9_container.yml b/extensions/molecule/inventory/host_vars/rocky9-container.yml similarity index 82% rename from extensions/molecule/inventory/host_vars/rocky9_container.yml rename to extensions/molecule/inventory/host_vars/rocky9-container.yml index 154fc081e..615fe38c9 100644 --- a/extensions/molecule/inventory/host_vars/rocky9_container.yml +++ b/extensions/molecule/inventory/host_vars/rocky9-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'rocky9_container' +ansible_host: 'rocky9-container' container_command: 'sleep 1d' container_image: 'docker.io/rockylinux/rockylinux:9' diff --git a/extensions/molecule/inventory/host_vars/rocky9_vm.yml b/extensions/molecule/inventory/host_vars/rocky9-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/rocky9_vm.yml rename to extensions/molecule/inventory/host_vars/rocky9-vm.yml diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004_container.yml b/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml similarity index 80% rename from extensions/molecule/inventory/host_vars/ubuntu2004_container.yml rename to extensions/molecule/inventory/host_vars/ubuntu2004-container.yml index 61d8d2342..a1195cfb9 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2004_container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2004_container' +ansible_host: 'ubuntu2004-container' container_command: 'sleep 1d' container_image: 'docker.io/library/ubuntu:20.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004_vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2004-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/ubuntu2004_vm.yml rename to extensions/molecule/inventory/host_vars/ubuntu2004-vm.yml diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204_container.yml b/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml similarity index 80% rename from extensions/molecule/inventory/host_vars/ubuntu2204_container.yml rename to extensions/molecule/inventory/host_vars/ubuntu2204-container.yml index 8c74c4786..289026c02 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2204_container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2204_container' +ansible_host: 'ubuntu2204-container' container_command: 'sleep 1d' container_image: 'docker.io/library/ubuntu:22.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204_vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2204-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/ubuntu2204_vm.yml rename to extensions/molecule/inventory/host_vars/ubuntu2204-vm.yml diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404_container.yml b/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml similarity index 80% rename from extensions/molecule/inventory/host_vars/ubuntu2404_container.yml rename to extensions/molecule/inventory/host_vars/ubuntu2404-container.yml index 39815ab11..c2ba1ba29 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2404_container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2404_container' +ansible_host: 'ubuntu2404-container' container_command: 'sleep 1d' container_image: 'docker.io/library/ubuntu:24.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404_vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2404-vm.yml similarity index 100% rename from extensions/molecule/inventory/host_vars/ubuntu2404_vm.yml rename to extensions/molecule/inventory/host_vars/ubuntu2404-vm.yml diff --git a/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml new file mode 100644 index 000000000..379f6d141 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml @@ -0,0 +1,6 @@ +ansible_connection: 'containers.podman.podman' +ansible_host: 'ubuntu2604-container' + +container_command: 'sleep 1d' +container_image: 'docker.io/library/ubuntu:26.04' +container_privileged: false diff --git a/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml new file mode 100644 index 000000000..ff888065a --- /dev/null +++ b/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml @@ -0,0 +1,17 @@ +ansible_connection: 'ssh' +ansible_user: 'root' + +vm_image_url: 'https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-amd64.img' + +kvm_vm__autostart: false +kvm_vm__base_image: 'resolute-server-cloudimg-amd64.img' +kvm_vm__boot_disk_size: '20G' +kvm_vm__host: 'localhost' +kvm_vm__memory: 4096 +kvm_vm__network_connections: + - name: 'enp1s0' + network_name: 'default' + dhcp4: true +kvm_vm__osinfo: 'ubuntu26.04' +kvm_vm__packages: [] +kvm_vm__vcpus: 2 diff --git a/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml index 05a410225..042a6336e 100644 --- a/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml +++ b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml @@ -1,11 +1,5 @@ systems_under_test: hosts: - debian11_vm: - debian12_vm: - debian13_vm: - rocky8_vm: - rocky9_vm: - rocky10_vm: - ubuntu2004_vm: - ubuntu2204_vm: - ubuntu2404_vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: diff --git a/extensions/molecule/playbooks/destroy.yml b/extensions/molecule/playbooks/destroy.yml index 1c45b6956..6d7a1df97 100644 --- a/extensions/molecule/playbooks/destroy.yml +++ b/extensions/molecule/playbooks/destroy.yml @@ -2,15 +2,10 @@ hosts: 'systems_under_test' gather_facts: false tasks: - #- name: 'Get info for all containers' - # containers.podman.podman_container_info: - # name: '{{ inventory_hostname }}' - # delegate_to: 'localhost' - # register: 'container_info' - name: 'Kill container if running' containers.podman.podman_container: name: '{{ inventory_hostname }}' - state: 'absent' #'stopped' + state: 'absent' timeout: 2 delegate_to: 'localhost' diff --git a/extensions/molecule/setup_basic/converge.yml b/extensions/molecule/setup_basic/converge.yml new file mode 100644 index 000000000..e9e8f2cbe --- /dev/null +++ b/extensions/molecule/setup_basic/converge.yml @@ -0,0 +1,2 @@ +- name: 'Converge' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.setup_basic' diff --git a/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml b/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..10bf17814 --- /dev/null +++ b/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,5 @@ +setup_basic__skip_mailto_root: true +setup_basic__skip_monitoring_plugins: true +setup_basic__skip_postfix: true +setup_basic__skip_system_update: true +setup_basic__skip_duplicity: true diff --git a/extensions/molecule/setup_basic/inventory/host_vars/rocky10-vm.yml b/extensions/molecule/setup_basic/inventory/host_vars/rocky10-vm.yml new file mode 100644 index 000000000..8491746ef --- /dev/null +++ b/extensions/molecule/setup_basic/inventory/host_vars/rocky10-vm.yml @@ -0,0 +1 @@ +setup_basic__skip_glances: true diff --git a/extensions/molecule/setup_basic/inventory/hosts.yml b/extensions/molecule/setup_basic/inventory/hosts.yml new file mode 100644 index 000000000..91c369d11 --- /dev/null +++ b/extensions/molecule/setup_basic/inventory/hosts.yml @@ -0,0 +1,23 @@ +lfops_setup_basic: + hosts: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: + +systems_under_test: + hosts: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: diff --git a/extensions/molecule/setup_basic/molecule.yml b/extensions/molecule/setup_basic/molecule.yml new file mode 100644 index 000000000..4c0b6ce0a --- /dev/null +++ b/extensions/molecule/setup_basic/molecule.yml @@ -0,0 +1,5 @@ +provisioner: + playbooks: + create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-create.yml' + destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-destroy.yml' + prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-prepare.yml' diff --git a/extensions/molecule/setup_basic/verify.yml b/extensions/molecule/setup_basic/verify.yml new file mode 100644 index 000000000..e69de29bb From ede186a3262d94c6c732c4d7a4b162567b913a0f Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 14:26:35 +0200 Subject: [PATCH 06/36] fix(molecule): fix log extraction when container creation fails --- extensions/molecule/playbooks/tasks/create-fail.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/molecule/playbooks/tasks/create-fail.yml b/extensions/molecule/playbooks/tasks/create-fail.yml index f44eb21d4..00fc5bc72 100644 --- a/extensions/molecule/playbooks/tasks/create-fail.yml +++ b/extensions/molecule/playbooks/tasks/create-fail.yml @@ -1,13 +1,14 @@ - name: 'Retrieve container log' ansible.builtin.command: - cmd: 'podman logs {{ item.container.Name }}' + cmd: 'podman logs {{ create_result.container.Name }}' changed_when: false register: 'logfile_cmd' + delegate_to: 'localhost' - name: 'Display container log and fail' ansible.builtin.fail: msg: | - Container {{ item.container.Name }} failed to start properly. - Exit Code: {{ item.container.State.ExitCode }} - Running: {{ item.container.State.Running }} + Container {{ create_result.container.Name }} failed to start properly. + Exit Code: {{ create_result.container.State.ExitCode }} + Running: {{ create_result.container.State.Running }} Log output: {{ logfile_cmd.stdout | default('No logs available') }} From 07c515396544c1a749e6f24cc2df46efee3ac917 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 14:31:19 +0200 Subject: [PATCH 07/36] chore: clean up apps role test --- extensions/molecule/apps/install/converge.yml | 2 +- extensions/molecule/apps/install/verify.yml | 2 +- extensions/molecule/apps/remove/cleanup.yml | 2 -- extensions/molecule/apps/remove/converge.yml | 2 +- extensions/molecule/apps/remove/verify.yml | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 extensions/molecule/apps/remove/cleanup.yml diff --git a/extensions/molecule/apps/install/converge.yml b/extensions/molecule/apps/install/converge.yml index ac53e7b96..2af0ae4bd 100644 --- a/extensions/molecule/apps/install/converge.yml +++ b/extensions/molecule/apps/install/converge.yml @@ -1,4 +1,4 @@ -- name: 'Converge' +- name: 'Converge apps role' hosts: 'systems_under_test' roles: - role: 'linuxfabrik.lfops.apps' diff --git a/extensions/molecule/apps/install/verify.yml b/extensions/molecule/apps/install/verify.yml index cede6ccaf..ff47ba68c 100644 --- a/extensions/molecule/apps/install/verify.yml +++ b/extensions/molecule/apps/install/verify.yml @@ -1,4 +1,4 @@ -- name: 'Verify' +- name: 'Verify apps are installed' hosts: 'systems_under_test' gather_facts: false tasks: diff --git a/extensions/molecule/apps/remove/cleanup.yml b/extensions/molecule/apps/remove/cleanup.yml deleted file mode 100644 index a7be1cb80..000000000 --- a/extensions/molecule/apps/remove/cleanup.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: 'Converge' - hosts: 'systems_under_test' diff --git a/extensions/molecule/apps/remove/converge.yml b/extensions/molecule/apps/remove/converge.yml index ac53e7b96..2af0ae4bd 100644 --- a/extensions/molecule/apps/remove/converge.yml +++ b/extensions/molecule/apps/remove/converge.yml @@ -1,4 +1,4 @@ -- name: 'Converge' +- name: 'Converge apps role' hosts: 'systems_under_test' roles: - role: 'linuxfabrik.lfops.apps' diff --git a/extensions/molecule/apps/remove/verify.yml b/extensions/molecule/apps/remove/verify.yml index 69d846070..34616d7bc 100644 --- a/extensions/molecule/apps/remove/verify.yml +++ b/extensions/molecule/apps/remove/verify.yml @@ -1,4 +1,4 @@ -- name: 'Verify' +- name: 'Verify apps are not installed' hosts: 'systems_under_test' gather_facts: true tasks: From ac0c57e25d1750de4ee2d383c0cd99ea58d1e3d8 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 14:41:54 +0200 Subject: [PATCH 08/36] test(monitoring_plugins): added --- .../molecule/monitoring_plugins/converge.yml | 2 ++ .../group_vars/systems_under_test.yml | 1 + .../monitoring_plugins/inventory/hosts.yml | 36 +++++++++++++++++++ .../molecule/monitoring_plugins/molecule.yml | 1 + .../molecule/monitoring_plugins/verify.yml | 12 +++++++ 5 files changed, 52 insertions(+) create mode 100644 extensions/molecule/monitoring_plugins/converge.yml create mode 100644 extensions/molecule/monitoring_plugins/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/monitoring_plugins/inventory/hosts.yml create mode 100644 extensions/molecule/monitoring_plugins/molecule.yml create mode 100644 extensions/molecule/monitoring_plugins/verify.yml diff --git a/extensions/molecule/monitoring_plugins/converge.yml b/extensions/molecule/monitoring_plugins/converge.yml new file mode 100644 index 000000000..fc3b8a9d3 --- /dev/null +++ b/extensions/molecule/monitoring_plugins/converge.yml @@ -0,0 +1,2 @@ +- name: 'Converge monitoring_plugins playbook' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.monitoring_plugins' diff --git a/extensions/molecule/monitoring_plugins/inventory/group_vars/systems_under_test.yml b/extensions/molecule/monitoring_plugins/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..d59c92ea7 --- /dev/null +++ b/extensions/molecule/monitoring_plugins/inventory/group_vars/systems_under_test.yml @@ -0,0 +1 @@ +monitoring_plugins__version: '5.0.0' diff --git a/extensions/molecule/monitoring_plugins/inventory/hosts.yml b/extensions/molecule/monitoring_plugins/inventory/hosts.yml new file mode 100644 index 000000000..7f276c4e4 --- /dev/null +++ b/extensions/molecule/monitoring_plugins/inventory/hosts.yml @@ -0,0 +1,36 @@ +lfops_monitoring_plugins: + hosts: + debian11-container: + debian12-container: + debian13-container: + rocky8-container: + rocky9-container: + rocky10-container: + ubuntu2004-container: + ubuntu2204-container: + ubuntu2404-container: + +lfops_repo_monitoring_plugins: + hosts: + debian11-container: + debian12-container: + debian13-container: + rocky8-container: + rocky9-container: + rocky10-container: + ubuntu2004-container: + ubuntu2204-container: + ubuntu2404-container: + +systems_under_test: + hosts: + debian11-container: + debian12-container: + debian13-container: + rocky8-container: + rocky9-container: + rocky10-container: + ubuntu2004-container: + ubuntu2204-container: + ubuntu2404-container: + diff --git a/extensions/molecule/monitoring_plugins/molecule.yml b/extensions/molecule/monitoring_plugins/molecule.yml new file mode 100644 index 000000000..1e47cbff8 --- /dev/null +++ b/extensions/molecule/monitoring_plugins/molecule.yml @@ -0,0 +1 @@ +# Molecule scenario marker diff --git a/extensions/molecule/monitoring_plugins/verify.yml b/extensions/molecule/monitoring_plugins/verify.yml new file mode 100644 index 000000000..2b409f77d --- /dev/null +++ b/extensions/molecule/monitoring_plugins/verify.yml @@ -0,0 +1,12 @@ +- name: 'Verify monitoring plugins are installed' + hosts: 'systems_under_test' + gather_facts: false + tasks: + - name: 'stat /usr/lib64/nagios/plugins/about-me' + ansible.builtin.stat: + path: '/usr/lib64/nagios/plugins/about-me' + register: 'about_me_plugin_stat_result' + + - name: 'Assert that about-me monitoring plugin is installed' + ansible.builtin.assert: + that: 'about_me_plugin_stat_result.stat.exists' From 07ec7550e69d2b53a512f5c16224bba69d582837 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 14:59:20 +0200 Subject: [PATCH 09/36] test: switch default rocky container images to systemd variants --- .../inventory/host_vars/rocky10-container.yml | 5 +++-- .../inventory/host_vars/rocky8-container.yml | 5 +++-- .../inventory/host_vars/rocky9-container.yml | 5 +++-- extensions/molecule/playbooks/create.yml | 12 ++++++------ 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/extensions/molecule/inventory/host_vars/rocky10-container.yml b/extensions/molecule/inventory/host_vars/rocky10-container.yml index 1fb37d96e..d9235047d 100644 --- a/extensions/molecule/inventory/host_vars/rocky10-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky10-container.yml @@ -1,6 +1,7 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'rocky10-container' -container_command: 'sleep 1d' -container_image: 'docker.io/rockylinux/rockylinux:10' +container_command: '/usr/sbin/init' +container_image: 'docker.io/rockylinux/rockylinux:10-ubi-init' container_privileged: false +container_systemd: true diff --git a/extensions/molecule/inventory/host_vars/rocky8-container.yml b/extensions/molecule/inventory/host_vars/rocky8-container.yml index 71cd5d599..c43d3d546 100644 --- a/extensions/molecule/inventory/host_vars/rocky8-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky8-container.yml @@ -1,6 +1,7 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'rocky8-container' -container_command: 'sleep 1d' -container_image: 'docker.io/rockylinux/rockylinux:8' +container_command: '/usr/sbin/init' +container_image: 'docker.io/rockylinux/rockylinux:8-ubi-init' container_privileged: false +container_systemd: true diff --git a/extensions/molecule/inventory/host_vars/rocky9-container.yml b/extensions/molecule/inventory/host_vars/rocky9-container.yml index 615fe38c9..af07853f6 100644 --- a/extensions/molecule/inventory/host_vars/rocky9-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky9-container.yml @@ -1,6 +1,7 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'rocky9-container' -container_command: 'sleep 1d' -container_image: 'docker.io/rockylinux/rockylinux:9' +container_command: '/usr/sbin/init' +container_image: 'docker.io/rockylinux/rockylinux:9-ubi-init' container_privileged: false +container_systemd: true diff --git a/extensions/molecule/playbooks/create.yml b/extensions/molecule/playbooks/create.yml index e8b60c328..335e9361e 100644 --- a/extensions/molecule/playbooks/create.yml +++ b/extensions/molecule/playbooks/create.yml @@ -4,16 +4,16 @@ tasks: - name: 'Create containers from inventory' containers.podman.podman_container: + capabilities: '{{ container_capabilities | default(omit) }}' + command: '{{ container_command | default("sleep 1d") }}' hostname: '{{ inventory_hostname }}' - name: '{{ inventory_hostname }}' image: '{{ container_image }}' - command: '{{ container_command | default("sleep 1d") }}' - privileged: '{{ container_privileged | default(false) }}' - volumes: '{{ container_volumes | default(omit) }}' - capabilities: '{{ container_capabilities | default(omit) }}' - systemd: '{{ container_systemd | default(false) }}' log_driver: '{{ container_log_driver | default("json-file") }}' + name: '{{ inventory_hostname }}' + privileged: '{{ container_privileged | default(false) }}' state: 'started' + systemd: '{{ container_systemd | default(false) }}' + volumes: '{{ container_volumes | default(omit) }}' register: 'create_result' delegate_to: 'localhost' From b9efefbdd6b5d2f80f00f7e0c5e8473e58ae0fff Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 16:58:27 +0200 Subject: [PATCH 10/36] test: switch default strategy to use VMs instead of containers --- .../molecule/apps/install/inventory/hosts.yml | 22 +++++++++++-------- .../molecule/apps/remove/inventory/hosts.yml | 22 +++++++++++-------- extensions/molecule/config.yml | 6 ++--- extensions/molecule/inventory/hosts.yml | 1 + .../{create.yml => create-container.yml} | 2 +- .../{vm-create.yml => create-vm.yml} | 0 .../{destroy.yml => destroy-container.yml} | 0 .../{vm-destroy.yml => destroy-vm.yml} | 0 .../{prepare.yml => prepare-container.yml} | 0 .../{vm-prepare.yml => prepare-vm.yml} | 0 ...ate-fail.yml => create-container-fail.yml} | 0 11 files changed, 31 insertions(+), 22 deletions(-) rename extensions/molecule/playbooks/{create.yml => create-container.yml} (96%) rename extensions/molecule/playbooks/{vm-create.yml => create-vm.yml} (100%) rename extensions/molecule/playbooks/{destroy.yml => destroy-container.yml} (100%) rename extensions/molecule/playbooks/{vm-destroy.yml => destroy-vm.yml} (100%) rename extensions/molecule/playbooks/{prepare.yml => prepare-container.yml} (100%) rename extensions/molecule/playbooks/{vm-prepare.yml => prepare-vm.yml} (100%) rename extensions/molecule/playbooks/tasks/{create-fail.yml => create-container-fail.yml} (100%) diff --git a/extensions/molecule/apps/install/inventory/hosts.yml b/extensions/molecule/apps/install/inventory/hosts.yml index 9068a5e66..02ce0bb32 100644 --- a/extensions/molecule/apps/install/inventory/hosts.yml +++ b/extensions/molecule/apps/install/inventory/hosts.yml @@ -1,11 +1,15 @@ +lfops_apps: + children: + systems_under_test: + systems_under_test: hosts: - debian11-container: - debian12-container: - debian13-container: - rocky8-container: - rocky9-container: - rocky10-container: - ubuntu2004-container: - ubuntu2204-container: - ubuntu2404-container: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: diff --git a/extensions/molecule/apps/remove/inventory/hosts.yml b/extensions/molecule/apps/remove/inventory/hosts.yml index 9068a5e66..02ce0bb32 100644 --- a/extensions/molecule/apps/remove/inventory/hosts.yml +++ b/extensions/molecule/apps/remove/inventory/hosts.yml @@ -1,11 +1,15 @@ +lfops_apps: + children: + systems_under_test: + systems_under_test: hosts: - debian11-container: - debian12-container: - debian13-container: - rocky8-container: - rocky9-container: - rocky10-container: - ubuntu2004-container: - ubuntu2204-container: - ubuntu2404-container: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index d27f71086..2261ef4bb 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -50,9 +50,9 @@ ansible: provisioner: playbooks: - create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/create.yml' - destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/destroy.yml' - prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/prepare.yml' + create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/create-vm.yml' + destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/destroy-vm.yml' + prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/prepare-vm.yml' scenario: test_sequence: diff --git a/extensions/molecule/inventory/hosts.yml b/extensions/molecule/inventory/hosts.yml index e69de29bb..ef30ee4af 100644 --- a/extensions/molecule/inventory/hosts.yml +++ b/extensions/molecule/inventory/hosts.yml @@ -0,0 +1 @@ +# Required for this directory to be an Ansible inventory source diff --git a/extensions/molecule/playbooks/create.yml b/extensions/molecule/playbooks/create-container.yml similarity index 96% rename from extensions/molecule/playbooks/create.yml rename to extensions/molecule/playbooks/create-container.yml index 335e9361e..2436e31d0 100644 --- a/extensions/molecule/playbooks/create.yml +++ b/extensions/molecule/playbooks/create-container.yml @@ -22,7 +22,7 @@ # bare minimum and thus do not contain a Python interpreter. - name: 'Verify containers are running' ansible.builtin.include_tasks: - file: 'tasks/create-fail.yml' + file: 'tasks/create-container-fail.yml' when: > create_result.container.State.ExitCode != 0 or not create_result.container.State.Running diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/create-vm.yml similarity index 100% rename from extensions/molecule/playbooks/vm-create.yml rename to extensions/molecule/playbooks/create-vm.yml diff --git a/extensions/molecule/playbooks/destroy.yml b/extensions/molecule/playbooks/destroy-container.yml similarity index 100% rename from extensions/molecule/playbooks/destroy.yml rename to extensions/molecule/playbooks/destroy-container.yml diff --git a/extensions/molecule/playbooks/vm-destroy.yml b/extensions/molecule/playbooks/destroy-vm.yml similarity index 100% rename from extensions/molecule/playbooks/vm-destroy.yml rename to extensions/molecule/playbooks/destroy-vm.yml diff --git a/extensions/molecule/playbooks/prepare.yml b/extensions/molecule/playbooks/prepare-container.yml similarity index 100% rename from extensions/molecule/playbooks/prepare.yml rename to extensions/molecule/playbooks/prepare-container.yml diff --git a/extensions/molecule/playbooks/vm-prepare.yml b/extensions/molecule/playbooks/prepare-vm.yml similarity index 100% rename from extensions/molecule/playbooks/vm-prepare.yml rename to extensions/molecule/playbooks/prepare-vm.yml diff --git a/extensions/molecule/playbooks/tasks/create-fail.yml b/extensions/molecule/playbooks/tasks/create-container-fail.yml similarity index 100% rename from extensions/molecule/playbooks/tasks/create-fail.yml rename to extensions/molecule/playbooks/tasks/create-container-fail.yml From a544b61acba3b787ae190c7e0f220df3ecd23337 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 17:03:06 +0200 Subject: [PATCH 11/36] test(apps): test entire playbook instead of only the role --- extensions/molecule/apps/install/converge.yml | 6 ++---- extensions/molecule/apps/remove/converge.yml | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/extensions/molecule/apps/install/converge.yml b/extensions/molecule/apps/install/converge.yml index 2af0ae4bd..24ae2f236 100644 --- a/extensions/molecule/apps/install/converge.yml +++ b/extensions/molecule/apps/install/converge.yml @@ -1,4 +1,2 @@ -- name: 'Converge apps role' - hosts: 'systems_under_test' - roles: - - role: 'linuxfabrik.lfops.apps' +- name: 'Converge apps playbook' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.apps' diff --git a/extensions/molecule/apps/remove/converge.yml b/extensions/molecule/apps/remove/converge.yml index 2af0ae4bd..24ae2f236 100644 --- a/extensions/molecule/apps/remove/converge.yml +++ b/extensions/molecule/apps/remove/converge.yml @@ -1,4 +1,2 @@ -- name: 'Converge apps role' - hosts: 'systems_under_test' - roles: - - role: 'linuxfabrik.lfops.apps' +- name: 'Converge apps playbook' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.apps' From efcfa26cc76721c3bcf426ab0dbb26509616f5db Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 17:07:43 +0200 Subject: [PATCH 12/36] test(kernel_settings): test entire playbook instead of only the role --- extensions/molecule/kernel_settings/sysctl/converge.yml | 6 ++---- .../molecule/kernel_settings/sysctl/inventory/hosts.yml | 4 ++++ extensions/molecule/kernel_settings/sysctl/molecule.yml | 6 +----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/extensions/molecule/kernel_settings/sysctl/converge.yml b/extensions/molecule/kernel_settings/sysctl/converge.yml index 991e95dcd..b0cbd950c 100644 --- a/extensions/molecule/kernel_settings/sysctl/converge.yml +++ b/extensions/molecule/kernel_settings/sysctl/converge.yml @@ -1,4 +1,2 @@ -- name: 'Converge' - hosts: 'systems_under_test' - roles: - - role: 'linuxfabrik.lfops.kernel_settings' +- name: 'Converge kernel_settings playbook' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.kernel_settings' diff --git a/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml index 042a6336e..9134979a4 100644 --- a/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml +++ b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml @@ -1,3 +1,7 @@ +lfops_kernel_settings: + children: + systems_under_test: + systems_under_test: hosts: rocky8-vm: diff --git a/extensions/molecule/kernel_settings/sysctl/molecule.yml b/extensions/molecule/kernel_settings/sysctl/molecule.yml index 4c0b6ce0a..1e47cbff8 100644 --- a/extensions/molecule/kernel_settings/sysctl/molecule.yml +++ b/extensions/molecule/kernel_settings/sysctl/molecule.yml @@ -1,5 +1 @@ -provisioner: - playbooks: - create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-create.yml' - destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-destroy.yml' - prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-prepare.yml' +# Molecule scenario marker From c6f9860b30590ec79db03d6b5bb33e769ee0c3f4 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 17:39:23 +0200 Subject: [PATCH 13/36] test(setup_basic): also test mail, monitoring_plugins and system_update functionality --- .../inventory/group_vars/systems_under_test.yml | 14 ++++++++++---- .../molecule/setup_basic/inventory/hosts.yml | 12 ++---------- extensions/molecule/setup_basic/molecule.yml | 6 +----- extensions/molecule/setup_basic/verify.yml | 12 ++++++++++++ 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml b/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml index 10bf17814..c6f5ca51b 100644 --- a/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml +++ b/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml @@ -1,5 +1,11 @@ -setup_basic__skip_mailto_root: true -setup_basic__skip_monitoring_plugins: true -setup_basic__skip_postfix: true -setup_basic__skip_system_update: true +mailto_root__from: 'root@localhost' +mailto_root__to: + - 'root@localhost' + +monitoring_plugins__version: '5.0.0' + +postfix__relayhost: 'mail.example.com' + setup_basic__skip_duplicity: true +setup_basic__skip_repo_icinga: true +setup_basic__skip_icinga2_agent: true diff --git a/extensions/molecule/setup_basic/inventory/hosts.yml b/extensions/molecule/setup_basic/inventory/hosts.yml index 91c369d11..fcd38a4c0 100644 --- a/extensions/molecule/setup_basic/inventory/hosts.yml +++ b/extensions/molecule/setup_basic/inventory/hosts.yml @@ -1,14 +1,6 @@ lfops_setup_basic: - hosts: - debian11-vm: - debian12-vm: - debian13-vm: - rocky8-vm: - rocky9-vm: - rocky10-vm: - ubuntu2004-vm: - ubuntu2204-vm: - ubuntu2404-vm: + children: + systems_under_test: systems_under_test: hosts: diff --git a/extensions/molecule/setup_basic/molecule.yml b/extensions/molecule/setup_basic/molecule.yml index 4c0b6ce0a..1e47cbff8 100644 --- a/extensions/molecule/setup_basic/molecule.yml +++ b/extensions/molecule/setup_basic/molecule.yml @@ -1,5 +1 @@ -provisioner: - playbooks: - create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-create.yml' - destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-destroy.yml' - prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-prepare.yml' +# Molecule scenario marker diff --git a/extensions/molecule/setup_basic/verify.yml b/extensions/molecule/setup_basic/verify.yml index e69de29bb..3a86d0ea9 100644 --- a/extensions/molecule/setup_basic/verify.yml +++ b/extensions/molecule/setup_basic/verify.yml @@ -0,0 +1,12 @@ +- name: 'Verify monitoring plugins are installed' + hosts: 'systems_under_test' + gather_facts: false + tasks: + - name: 'stat /usr/lib64/nagios/plugins/about-me' + ansible.builtin.stat: + path: '/usr/lib64/nagios/plugins/about-me' + register: 'about_me_plugin_stat_result' + + - name: 'Assert that the about-me monitoring plugin is installed' + ansible.builtin.assert: + that: 'about_me_plugin_stat_result.stat.exists' From 2caaa45333ab9781d156db4336d9e0fa7e00c006 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 17:39:41 +0200 Subject: [PATCH 14/36] test(monitoring_plugins): cleanup inventory --- .../monitoring_plugins/inventory/hosts.yml | 42 ++++++------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/extensions/molecule/monitoring_plugins/inventory/hosts.yml b/extensions/molecule/monitoring_plugins/inventory/hosts.yml index 7f276c4e4..4fa064744 100644 --- a/extensions/molecule/monitoring_plugins/inventory/hosts.yml +++ b/extensions/molecule/monitoring_plugins/inventory/hosts.yml @@ -1,36 +1,20 @@ lfops_monitoring_plugins: - hosts: - debian11-container: - debian12-container: - debian13-container: - rocky8-container: - rocky9-container: - rocky10-container: - ubuntu2004-container: - ubuntu2204-container: - ubuntu2404-container: + children: + systems_under_test: lfops_repo_monitoring_plugins: - hosts: - debian11-container: - debian12-container: - debian13-container: - rocky8-container: - rocky9-container: - rocky10-container: - ubuntu2004-container: - ubuntu2204-container: - ubuntu2404-container: + children: + systems_under_test: systems_under_test: hosts: - debian11-container: - debian12-container: - debian13-container: - rocky8-container: - rocky9-container: - rocky10-container: - ubuntu2004-container: - ubuntu2204-container: - ubuntu2404-container: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: From 4df98399eaa972b8f59ae6d291fb039251567045 Mon Sep 17 00:00:00 2001 From: Marco Benedetti Date: Fri, 22 May 2026 17:46:47 +0200 Subject: [PATCH 15/36] docs(contributing): add section about testing with Molecule --- CONTRIBUTING.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71196ae8d..223ea8654 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -704,6 +704,63 @@ Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored * Drop when: LFOps raises its minimum ansible-core to >= 2.18; switch to `community.general.lvm_pv` and update `roles/lvm` accordingly. +### Testing + +Molecule is used as the framework to test the LFOps playbooks (*not roles*). The test scenarios and configurations live in `extension/molecule` and are structured as follows: + +``` +extensions +└── molecule + ├── apps -- test scenario, named after the playbook name + │ ├── install -- if needed, sub-scenario + │ │ ├── converge.yml + │ │ ├── inventory -- scenario-specific inventory with variables that are needed for the playbook under test and optionally additional hosts (e.g. for a cluster test setup). Overwrites the shared inventory (extensions/molecule/inventory) + │ │ │ ├── group_vars + │ │ │ │ └── systems_under_test.yml + │ │ │ └── hosts.yml + │ │ ├── molecule.yml -- scenario marker; required even if empty. Can also be used to overwrite, which playbooks are used by Molecule (e.g. to switch between VM and container provisioning playbooks) + │ │ └── verify.yml + │ └── remove -- additional sub-scenario + │ └── ... + ├── config.yml -- valid for all scenarios, can be overwritten in each scenario's molecule.yml (same content and structure) + ├── default -- we are not using the "default" scenario, but molecule needs this to run at all. could be used to share config (e.g. prepare.yml) across *all* scenarios + │ └── molecule.yml + ├── inventory -- shared inventory across all scenarios and therefore available in all scenarios. Contains a basic set of VMs/containers that are commonly used. + │ ├── hosts.yml -- Required, even if empty, that Ansible can detect this inventory + │ └── host_vars + │ ├── debian11-container.yml + │ ├── debian11-vm.yml + │ └── ... + ├── monitoring_plugins -- scenario with no sub-scenarios + │ ├── converge.yml + │ ├── inventory + │ │ └── ... + │ ├── molecule.yml + │ └── verify.yml + ├── playbooks -- shared playbooks used by Molecule for running the scenarios + │ ├── create-container.yml + │ ├── destroy-container.yml + │ └── ... + └── requirements.yml +``` + +Tests can be run against a subset of targets by providing them as a comma-separated list via the project-specific `LFOPS_TEST_TARGETS` environment variable: + +```shell +# for VMs (the hypervisor host needs to be included as well; here `localhost`) +LFOPS_TEST_TARGETS='localhost,rocky*' molecule test --scenario-name apps/install + +# for containers +LFOPS_TEST_TARGETS='rocky*' molecule test --scenario-name apps/install +``` + + +Known Limitations: + +* VM-based testing currently requires passwordless sudo on the Ansible controller. +* Ansible Navigator does not work out of the box. + + ### Credits * From 2dcf720f4832c0d9e9b00387a06caf6d0523f7f6 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 11:10:40 +0200 Subject: [PATCH 16/36] refactor(molecule): consistent variable naming and host var cleanup --- .../molecule/apps/install/inventory/hosts.yml | 1 + extensions/molecule/apps/install/verify.yml | 4 +- .../molecule/apps/remove/inventory/hosts.yml | 1 + extensions/molecule/apps/remove/verify.yml | 6 +-- extensions/molecule/config.yml | 8 ++-- .../host_vars/debian11-container.yml | 4 +- .../inventory/host_vars/debian11-vm.yml | 5 +-- .../host_vars/debian12-container.yml | 4 +- .../inventory/host_vars/debian12-vm.yml | 5 +-- .../host_vars/debian13-container.yml | 4 +- .../inventory/host_vars/debian13-vm.yml | 5 +-- .../inventory/host_vars/rocky10-container.yml | 7 ++- .../inventory/host_vars/rocky10-vm.yml | 5 +-- .../inventory/host_vars/rocky8-container.yml | 7 ++- .../inventory/host_vars/rocky8-vm.yml | 5 +-- .../inventory/host_vars/rocky9-container.yml | 7 ++- .../inventory/host_vars/rocky9-vm.yml | 5 +-- .../host_vars/ubuntu2004-container.yml | 4 +- .../inventory/host_vars/ubuntu2004-vm.yml | 5 +-- .../host_vars/ubuntu2204-container.yml | 4 +- .../inventory/host_vars/ubuntu2204-vm.yml | 5 +-- .../host_vars/ubuntu2404-container.yml | 4 +- .../inventory/host_vars/ubuntu2404-vm.yml | 5 +-- .../host_vars/ubuntu2604-container.yml | 4 +- .../inventory/host_vars/ubuntu2604-vm.yml | 5 +-- .../sysctl/inventory/hosts.yml | 1 + .../kernel_settings/sysctl/verify.yml | 8 ++-- .../monitoring_plugins/inventory/hosts.yml | 2 +- .../molecule/monitoring_plugins/verify.yml | 4 +- .../molecule/playbooks/create-container.yml | 21 ++++----- extensions/molecule/playbooks/create-vm.yml | 44 +++++++++---------- extensions/molecule/playbooks/destroy-vm.yml | 6 +-- .../playbooks/tasks/create-container-fail.yml | 12 ++--- extensions/molecule/setup_basic/converge.yml | 2 +- .../molecule/setup_basic/inventory/hosts.yml | 1 + extensions/molecule/setup_basic/verify.yml | 6 +-- 36 files changed, 102 insertions(+), 124 deletions(-) diff --git a/extensions/molecule/apps/install/inventory/hosts.yml b/extensions/molecule/apps/install/inventory/hosts.yml index 02ce0bb32..b417f539d 100644 --- a/extensions/molecule/apps/install/inventory/hosts.yml +++ b/extensions/molecule/apps/install/inventory/hosts.yml @@ -1,3 +1,4 @@ +# yamllint disable rule:empty-values lfops_apps: children: systems_under_test: diff --git a/extensions/molecule/apps/install/verify.yml b/extensions/molecule/apps/install/verify.yml index ff47ba68c..e45bdf41b 100644 --- a/extensions/molecule/apps/install/verify.yml +++ b/extensions/molecule/apps/install/verify.yml @@ -6,6 +6,6 @@ ansible.builtin.package_facts: manager: 'auto' - - name: 'Assert' + - name: 'Assert that zsh is installed' ansible.builtin.assert: - that: '"zsh" in ansible_facts.packages' + that: '"zsh" in ansible_facts["packages"]' diff --git a/extensions/molecule/apps/remove/inventory/hosts.yml b/extensions/molecule/apps/remove/inventory/hosts.yml index 02ce0bb32..b417f539d 100644 --- a/extensions/molecule/apps/remove/inventory/hosts.yml +++ b/extensions/molecule/apps/remove/inventory/hosts.yml @@ -1,3 +1,4 @@ +# yamllint disable rule:empty-values lfops_apps: children: systems_under_test: diff --git a/extensions/molecule/apps/remove/verify.yml b/extensions/molecule/apps/remove/verify.yml index 34616d7bc..d5c8a39e2 100644 --- a/extensions/molecule/apps/remove/verify.yml +++ b/extensions/molecule/apps/remove/verify.yml @@ -1,11 +1,11 @@ - name: 'Verify apps are not installed' hosts: 'systems_under_test' - gather_facts: true + gather_facts: false tasks: - name: 'Gather the package facts' ansible.builtin.package_facts: manager: 'auto' - - name: 'Assert' + - name: 'Assert that less is not installed' ansible.builtin.assert: - that: '"less" not in ansible_facts.packages' + that: '"less" not in ansible_facts["packages"]' diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 2261ef4bb..0ab7991b7 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -16,10 +16,6 @@ ansible: gathering: 'smart' host_key_checking: false inventory: 'hosts' - nocows: 1 - retry_files_enabled: true - timeout: 60 - log_path: '${MOLECULE_EPHEMERAL_DIRECTORY}/ansible.log' inventory_ignore_extensions: - '~' - '.orig' @@ -32,7 +28,11 @@ ansible: - '.csv' - '.md' inventory_ignore_patterns: '(host|group)_files' + log_path: '${MOLECULE_EPHEMERAL_DIRECTORY}/ansible.log' + nocows: 1 + retry_files_enabled: true stdout_callback: 'yaml' + timeout: 60 ssh_connections: pipelining: true ssh_args: '-o ControlMaster=auto -o ControlPersist=60s' diff --git a/extensions/molecule/inventory/host_vars/debian11-container.yml b/extensions/molecule/inventory/host_vars/debian11-container.yml index 5815e39b9..d981bf444 100644 --- a/extensions/molecule/inventory/host_vars/debian11-container.yml +++ b/extensions/molecule/inventory/host_vars/debian11-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'debian11-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/debian:11' -container_privileged: false +molecule__container_image: 'docker.io/library/debian:11' diff --git a/extensions/molecule/inventory/host_vars/debian11-vm.yml b/extensions/molecule/inventory/host_vars/debian11-vm.yml index 6e9479e51..903931f0a 100644 --- a/extensions/molecule/inventory/host_vars/debian11-vm.yml +++ b/extensions/molecule/inventory/host_vars/debian11-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-amd64.qcow2' - kvm_vm__autostart: false kvm_vm__base_image: 'debian-11-genericcloud-amd64.qcow2' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'debian11' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-amd64.qcow2' diff --git a/extensions/molecule/inventory/host_vars/debian12-container.yml b/extensions/molecule/inventory/host_vars/debian12-container.yml index 1cee9a253..364b53e2f 100644 --- a/extensions/molecule/inventory/host_vars/debian12-container.yml +++ b/extensions/molecule/inventory/host_vars/debian12-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'debian12-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/debian:12' -container_privileged: false +molecule__container_image: 'docker.io/library/debian:12' diff --git a/extensions/molecule/inventory/host_vars/debian12-vm.yml b/extensions/molecule/inventory/host_vars/debian12-vm.yml index 68ae91634..5b891589f 100644 --- a/extensions/molecule/inventory/host_vars/debian12-vm.yml +++ b/extensions/molecule/inventory/host_vars/debian12-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2' - kvm_vm__autostart: false kvm_vm__base_image: 'debian-12-genericcloud-amd64.qcow2' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'debian12' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2' diff --git a/extensions/molecule/inventory/host_vars/debian13-container.yml b/extensions/molecule/inventory/host_vars/debian13-container.yml index 48ef11937..b682c1ba3 100644 --- a/extensions/molecule/inventory/host_vars/debian13-container.yml +++ b/extensions/molecule/inventory/host_vars/debian13-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'debian13-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/debian:13' -container_privileged: false +molecule__container_image: 'docker.io/library/debian:13' diff --git a/extensions/molecule/inventory/host_vars/debian13-vm.yml b/extensions/molecule/inventory/host_vars/debian13-vm.yml index 6cf4fbf84..6f5779e31 100644 --- a/extensions/molecule/inventory/host_vars/debian13-vm.yml +++ b/extensions/molecule/inventory/host_vars/debian13-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2' - kvm_vm__autostart: false kvm_vm__base_image: 'debian-13-genericcloud-amd64.qcow2' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'debian13' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2' diff --git a/extensions/molecule/inventory/host_vars/rocky10-container.yml b/extensions/molecule/inventory/host_vars/rocky10-container.yml index d9235047d..489ff41a5 100644 --- a/extensions/molecule/inventory/host_vars/rocky10-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky10-container.yml @@ -1,7 +1,6 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'rocky10-container' -container_command: '/usr/sbin/init' -container_image: 'docker.io/rockylinux/rockylinux:10-ubi-init' -container_privileged: false -container_systemd: true +molecule__container_command: '/usr/sbin/init' +molecule__container_image: 'docker.io/rockylinux/rockylinux:10-ubi-init' +molecule__container_systemd: true diff --git a/extensions/molecule/inventory/host_vars/rocky10-vm.yml b/extensions/molecule/inventory/host_vars/rocky10-vm.yml index b2d9d39cb..97afc9d86 100644 --- a/extensions/molecule/inventory/host_vars/rocky10-vm.yml +++ b/extensions/molecule/inventory/host_vars/rocky10-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://dl.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2' - kvm_vm__autostart: false kvm_vm__base_image: 'Rocky-10-GenericCloud-Base.latest.x86_64.qcow2' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'rocky10' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://dl.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2' diff --git a/extensions/molecule/inventory/host_vars/rocky8-container.yml b/extensions/molecule/inventory/host_vars/rocky8-container.yml index c43d3d546..1bd38f258 100644 --- a/extensions/molecule/inventory/host_vars/rocky8-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky8-container.yml @@ -1,7 +1,6 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'rocky8-container' -container_command: '/usr/sbin/init' -container_image: 'docker.io/rockylinux/rockylinux:8-ubi-init' -container_privileged: false -container_systemd: true +molecule__container_command: '/usr/sbin/init' +molecule__container_image: 'docker.io/rockylinux/rockylinux:8-ubi-init' +molecule__container_systemd: true diff --git a/extensions/molecule/inventory/host_vars/rocky8-vm.yml b/extensions/molecule/inventory/host_vars/rocky8-vm.yml index 0520d860e..ed34d5da4 100644 --- a/extensions/molecule/inventory/host_vars/rocky8-vm.yml +++ b/extensions/molecule/inventory/host_vars/rocky8-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2' - kvm_vm__autostart: false kvm_vm__base_image: 'Rocky-8-GenericCloud-Base.latest.x86_64.qcow2' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'rocky8' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2' diff --git a/extensions/molecule/inventory/host_vars/rocky9-container.yml b/extensions/molecule/inventory/host_vars/rocky9-container.yml index af07853f6..e1a8a351e 100644 --- a/extensions/molecule/inventory/host_vars/rocky9-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky9-container.yml @@ -1,7 +1,6 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'rocky9-container' -container_command: '/usr/sbin/init' -container_image: 'docker.io/rockylinux/rockylinux:9-ubi-init' -container_privileged: false -container_systemd: true +molecule__container_command: '/usr/sbin/init' +molecule__container_image: 'docker.io/rockylinux/rockylinux:9-ubi-init' +molecule__container_systemd: true diff --git a/extensions/molecule/inventory/host_vars/rocky9-vm.yml b/extensions/molecule/inventory/host_vars/rocky9-vm.yml index 77072841a..63ad3768d 100644 --- a/extensions/molecule/inventory/host_vars/rocky9-vm.yml +++ b/extensions/molecule/inventory/host_vars/rocky9-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2' - kvm_vm__autostart: false kvm_vm__base_image: 'Rocky-9-GenericCloud-Base.latest.x86_64.qcow2' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'rocky9' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml index a1195cfb9..2725075e6 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'ubuntu2004-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/ubuntu:20.04' -container_privileged: false +molecule__container_image: 'docker.io/library/ubuntu:20.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004-vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2004-vm.yml index 8202ec9da..59fba93f2 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2004-vm.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2004-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img' - kvm_vm__autostart: false kvm_vm__base_image: 'focal-server-cloudimg-amd64.img' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'ubuntu20.04' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml index 289026c02..18fc8a873 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'ubuntu2204-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/ubuntu:22.04' -container_privileged: false +molecule__container_image: 'docker.io/library/ubuntu:22.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204-vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2204-vm.yml index 944045dbc..97e0235c1 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2204-vm.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2204-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img' - kvm_vm__autostart: false kvm_vm__base_image: 'jammy-server-cloudimg-amd64.img' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'ubuntu22.04' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml index c2ba1ba29..1d4a49097 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'ubuntu2404-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/ubuntu:24.04' -container_privileged: false +molecule__container_image: 'docker.io/library/ubuntu:24.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404-vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2404-vm.yml index c21fc4068..773f4ffe2 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2404-vm.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2404-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img' - kvm_vm__autostart: false kvm_vm__base_image: 'noble-server-cloudimg-amd64.img' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'ubuntu24.04' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml index 379f6d141..17d914c03 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml @@ -1,6 +1,4 @@ ansible_connection: 'containers.podman.podman' ansible_host: 'ubuntu2604-container' -container_command: 'sleep 1d' -container_image: 'docker.io/library/ubuntu:26.04' -container_privileged: false +molecule__container_image: 'docker.io/library/ubuntu:26.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml index ff888065a..617d0b58f 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml @@ -1,8 +1,5 @@ -ansible_connection: 'ssh' ansible_user: 'root' -vm_image_url: 'https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-amd64.img' - kvm_vm__autostart: false kvm_vm__base_image: 'resolute-server-cloudimg-amd64.img' kvm_vm__boot_disk_size: '20G' @@ -15,3 +12,5 @@ kvm_vm__network_connections: kvm_vm__osinfo: 'ubuntu26.04' kvm_vm__packages: [] kvm_vm__vcpus: 2 + +molecule__vm_image_url: 'https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-amd64.img' diff --git a/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml index 9134979a4..e5ead358c 100644 --- a/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml +++ b/extensions/molecule/kernel_settings/sysctl/inventory/hosts.yml @@ -1,3 +1,4 @@ +# yamllint disable rule:empty-values lfops_kernel_settings: children: systems_under_test: diff --git a/extensions/molecule/kernel_settings/sysctl/verify.yml b/extensions/molecule/kernel_settings/sysctl/verify.yml index e40812250..c832c6f51 100644 --- a/extensions/molecule/kernel_settings/sysctl/verify.yml +++ b/extensions/molecule/kernel_settings/sysctl/verify.yml @@ -1,12 +1,12 @@ -- name: 'Verify' +- name: 'Verify vm.overcommit_memory is set' hosts: 'systems_under_test' gather_facts: false tasks: - name: 'Read sysctl vm.overcommit_memory from procfs' ansible.builtin.slurp: src: '/proc/sys/vm/overcommit_memory' - register: 'sysctl_vm_overcommit_memory_result' + register: '__molecule__sysctl_vm_overcommit_memory_result' - - name: 'Assert' + - name: 'Assert that vm.overcommit_memory is set to 1' ansible.builtin.assert: - that: 'sysctl_vm_overcommit_memory_result.content | b64decode | int == 1' + that: '__molecule__sysctl_vm_overcommit_memory_result["content"] | b64decode | int == 1' diff --git a/extensions/molecule/monitoring_plugins/inventory/hosts.yml b/extensions/molecule/monitoring_plugins/inventory/hosts.yml index 4fa064744..b21a51c63 100644 --- a/extensions/molecule/monitoring_plugins/inventory/hosts.yml +++ b/extensions/molecule/monitoring_plugins/inventory/hosts.yml @@ -1,3 +1,4 @@ +# yamllint disable rule:empty-values lfops_monitoring_plugins: children: systems_under_test: @@ -17,4 +18,3 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: - diff --git a/extensions/molecule/monitoring_plugins/verify.yml b/extensions/molecule/monitoring_plugins/verify.yml index 2b409f77d..ea52c1729 100644 --- a/extensions/molecule/monitoring_plugins/verify.yml +++ b/extensions/molecule/monitoring_plugins/verify.yml @@ -5,8 +5,8 @@ - name: 'stat /usr/lib64/nagios/plugins/about-me' ansible.builtin.stat: path: '/usr/lib64/nagios/plugins/about-me' - register: 'about_me_plugin_stat_result' + register: '__molecule__about_me_plugin_stat_result' - name: 'Assert that about-me monitoring plugin is installed' ansible.builtin.assert: - that: 'about_me_plugin_stat_result.stat.exists' + that: '__molecule__about_me_plugin_stat_result["stat"]["exists"]' diff --git a/extensions/molecule/playbooks/create-container.yml b/extensions/molecule/playbooks/create-container.yml index 2436e31d0..3ab5989f8 100644 --- a/extensions/molecule/playbooks/create-container.yml +++ b/extensions/molecule/playbooks/create-container.yml @@ -2,19 +2,20 @@ hosts: 'systems_under_test' gather_facts: false tasks: + - name: 'Create containers from inventory' containers.podman.podman_container: - capabilities: '{{ container_capabilities | default(omit) }}' - command: '{{ container_command | default("sleep 1d") }}' + capabilities: '{{ molecule__container_capabilities | default(omit) }}' + command: '{{ molecule__container_command | default("sleep 1d") }}' hostname: '{{ inventory_hostname }}' - image: '{{ container_image }}' - log_driver: '{{ container_log_driver | default("json-file") }}' + image: '{{ molecule__container_image }}' + log_driver: '{{ molecule__container_log_driver | default("json-file") }}' name: '{{ inventory_hostname }}' - privileged: '{{ container_privileged | default(false) }}' + privileged: '{{ molecule__container_privileged | default(false) }}' state: 'started' - systemd: '{{ container_systemd | default(false) }}' - volumes: '{{ container_volumes | default(omit) }}' - register: 'create_result' + systemd: '{{ molecule__container_systemd | default(false) }}' + volumes: '{{ molecule__container_volumes | default(omit) }}' + register: '__molecule__container_result' delegate_to: 'localhost' # Not using ansible.builtin.wait_for_connection here as it depends on a working Python @@ -24,5 +25,5 @@ ansible.builtin.include_tasks: file: 'tasks/create-container-fail.yml' when: > - create_result.container.State.ExitCode != 0 or - not create_result.container.State.Running + __molecule__container_result['container']['State']['ExitCode'] != 0 or + not __molecule__container_result['container']['State']['Running'] diff --git a/extensions/molecule/playbooks/create-vm.yml b/extensions/molecule/playbooks/create-vm.yml index ecacb2212..1e5a83649 100644 --- a/extensions/molecule/playbooks/create-vm.yml +++ b/extensions/molecule/playbooks/create-vm.yml @@ -2,12 +2,12 @@ hosts: 'systems_under_test' gather_facts: false vars: - molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' + __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - name: 'Generate ephemeral SSH keypair' community.crypto.openssh_keypair: - path: '{{ molecule_ephemeral_dir }}/molecule_key' + path: '{{ __molecule__ephemeral_dir }}/molecule_key' type: 'ed25519' comment: 'molecule' delegate_to: 'localhost' @@ -15,14 +15,14 @@ - name: 'Read SSH public key' ansible.builtin.slurp: - src: '{{ molecule_ephemeral_dir }}/molecule_key.pub' + src: '{{ __molecule__ephemeral_dir }}/molecule_key.pub' delegate_to: 'localhost' - register: 'molecule_pubkey_result' + register: '__molecule__pubkey_result' run_once: true - name: 'Set SSH public key fact' ansible.builtin.set_fact: - molecule_ssh_pubkey: '{{ molecule_pubkey_result["content"] | b64decode | trim }}' + __molecule__ssh_pubkey: '{{ __molecule__pubkey_result["content"] | b64decode | trim }}' delegate_to: 'localhost' delegate_facts: true run_once: true @@ -31,23 +31,23 @@ community.libvirt.virt_pool: command: 'info' delegate_to: 'localhost' - register: 'molecule_pool_info' + register: '__molecule__pool_result' run_once: true - name: 'Download cloud images into storage pool' ansible.builtin.get_url: - url: '{{ vm_image_url }}' - dest: '{{ molecule_pool_info["pools"]["default"]["path"] }}/{{ kvm_vm__base_image }}' - mode: '0644' + url: '{{ molecule__vm_image_url }}' + dest: '{{ __molecule__pool_result["pools"]["default"]["path"] }}/{{ kvm_vm__base_image }}' + mode: 0o644 become: true delegate_to: 'localhost' - when: 'vm_image_url is defined' + when: 'molecule__vm_image_url is defined' - name: 'Ensure ephemeral inventory directory exists' ansible.builtin.file: - path: '{{ molecule_ephemeral_dir }}/inventory' + path: '{{ __molecule__ephemeral_dir }}/inventory' state: 'directory' - mode: '0755' + mode: 0o755 delegate_to: 'localhost' run_once: true @@ -60,7 +60,7 @@ - role: 'linuxfabrik.lfops.kvm_vm' vars: kvm_vm__ssh_authorized_keys: - - '{{ hostvars["localhost"]["molecule_ssh_pubkey"] }}' + - '{{ hostvars["localhost"]["__molecule__ssh_pubkey"] }}' kvm_vm__state: 'running' @@ -68,14 +68,14 @@ hosts: 'systems_under_test' gather_facts: false vars: - molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' + __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - name: 'Wait for VMs to obtain IP addresses' ansible.builtin.command: cmd: 'virsh domifaddr {{ inventory_hostname }} --source arp' - register: 'domifaddr_result' - until: 'domifaddr_result.stdout_lines | select("match", ".*ipv4.*") | list | length > 0' + register: '__molecule__vm_ipaddr_result' + until: '__molecule__vm_ipaddr_result["stdout_lines"] | select("match", ".*ipv4.*") | list | length > 0' retries: 30 delay: 5 changed_when: false @@ -83,9 +83,9 @@ - name: 'Parse VM IP addresses' ansible.builtin.set_fact: - molecule_vm_ip: >- + __molecule__vm_ipaddr: >- {{ - domifaddr_result.stdout_lines + __molecule__vm_ipaddr_result['stdout_lines'] | select('match', '.*ipv4.*') | first | regex_replace('.*\s+(\d+\.\d+\.\d+\.\d+)\/.*', '\1') @@ -98,11 +98,11 @@ hosts: {% for host in ansible_play_hosts %} {{ host }}: - ansible_host: '{{ hostvars[host]["molecule_vm_ip"] }}' - ansible_ssh_private_key_file: '{{ molecule_ephemeral_dir }}/molecule_key' + ansible_host: '{{ hostvars[host]["__molecule__vm_ipaddr"] }}' + ansible_ssh_private_key_file: '{{ __molecule__ephemeral_dir }}/molecule_key' ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' {% endfor %} - dest: '{{ molecule_ephemeral_dir }}/inventory/molecule_vm.yml' - mode: '0644' + dest: '{{ __molecule__ephemeral_dir }}/inventory/molecule_vm.yml' + mode: 0o644 delegate_to: 'localhost' run_once: true diff --git a/extensions/molecule/playbooks/destroy-vm.yml b/extensions/molecule/playbooks/destroy-vm.yml index a33533eb7..b98afabf5 100644 --- a/extensions/molecule/playbooks/destroy-vm.yml +++ b/extensions/molecule/playbooks/destroy-vm.yml @@ -12,12 +12,12 @@ hosts: 'localhost' gather_facts: false vars: - molecule_ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' + __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - name: 'Remove ephemeral SSH keypair' ansible.builtin.file: - path: '{{ molecule_ephemeral_dir }}/{{ item }}' + path: '{{ __molecule__ephemeral_dir }}/{{ item }}' state: 'absent' loop: - 'molecule_key' @@ -25,5 +25,5 @@ - name: 'Remove dynamic inventory' ansible.builtin.file: - path: '{{ molecule_ephemeral_dir }}/inventory' + path: '{{ __molecule__ephemeral_dir }}/inventory' state: 'absent' diff --git a/extensions/molecule/playbooks/tasks/create-container-fail.yml b/extensions/molecule/playbooks/tasks/create-container-fail.yml index 00fc5bc72..631aef6bd 100644 --- a/extensions/molecule/playbooks/tasks/create-container-fail.yml +++ b/extensions/molecule/playbooks/tasks/create-container-fail.yml @@ -1,14 +1,14 @@ - name: 'Retrieve container log' ansible.builtin.command: - cmd: 'podman logs {{ create_result.container.Name }}' + cmd: 'podman logs {{ __molecule__container_result["container"]["Name"] }}' changed_when: false - register: 'logfile_cmd' + register: '__molecule__container_log_result' delegate_to: 'localhost' - name: 'Display container log and fail' ansible.builtin.fail: msg: | - Container {{ create_result.container.Name }} failed to start properly. - Exit Code: {{ create_result.container.State.ExitCode }} - Running: {{ create_result.container.State.Running }} - Log output: {{ logfile_cmd.stdout | default('No logs available') }} + Container {{ __molecule__container_result['container']['Name'] }} failed to start properly. + Exit Code: {{ __molecule__container_result['container']['State']['ExitCode'] }} + Running: {{ __molecule__container_result['container']['State']['Running'] }} + Log output: {{ __molecule__container_log_result['stdout'] | default('No logs available') }} diff --git a/extensions/molecule/setup_basic/converge.yml b/extensions/molecule/setup_basic/converge.yml index e9e8f2cbe..046a2140c 100644 --- a/extensions/molecule/setup_basic/converge.yml +++ b/extensions/molecule/setup_basic/converge.yml @@ -1,2 +1,2 @@ -- name: 'Converge' +- name: 'Converge setup_basic playbook' ansible.builtin.import_playbook: 'linuxfabrik.lfops.setup_basic' diff --git a/extensions/molecule/setup_basic/inventory/hosts.yml b/extensions/molecule/setup_basic/inventory/hosts.yml index fcd38a4c0..94bf903aa 100644 --- a/extensions/molecule/setup_basic/inventory/hosts.yml +++ b/extensions/molecule/setup_basic/inventory/hosts.yml @@ -1,3 +1,4 @@ +# yamllint disable rule:empty-values lfops_setup_basic: children: systems_under_test: diff --git a/extensions/molecule/setup_basic/verify.yml b/extensions/molecule/setup_basic/verify.yml index 3a86d0ea9..84e0ed62a 100644 --- a/extensions/molecule/setup_basic/verify.yml +++ b/extensions/molecule/setup_basic/verify.yml @@ -1,12 +1,12 @@ -- name: 'Verify monitoring plugins are installed' +- name: 'Verify setup_basic installed the monitoring plugins' hosts: 'systems_under_test' gather_facts: false tasks: - name: 'stat /usr/lib64/nagios/plugins/about-me' ansible.builtin.stat: path: '/usr/lib64/nagios/plugins/about-me' - register: 'about_me_plugin_stat_result' + register: '__molecule__about_me_plugin_stat_result' - name: 'Assert that the about-me monitoring plugin is installed' ansible.builtin.assert: - that: 'about_me_plugin_stat_result.stat.exists' + that: '__molecule__about_me_plugin_stat_result["stat"]["exists"]' From 18bec93ea8fba6cf05b0e9a7bb8bd05ebd4f887e Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 11:11:48 +0200 Subject: [PATCH 17/36] fix(molecule): install required collections during 'molecule test' --- extensions/molecule/config.yml | 18 ++++++++++++++---- extensions/molecule/requirements.yml | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 0ab7991b7..1d2778346 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -1,7 +1,6 @@ -dependency: - name: 'galaxy' - options: - requirements-file: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/requirements.yml' +# Shared Molecule base config. It is merged into every scenario and is read whenever Molecule +# resolves a scenario (e.g. `molecule test`, `molecule converge`, `molecule create`). Individual +# scenarios can override any of these settings in their own molecule.yml. ansible: cfg: @@ -48,6 +47,16 @@ ansible: - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' - '--limit=${LFOPS_TEST_TARGETS}' +# The 'galaxy' dependency below installs the collections listed in requirements.yml via +# `ansible-galaxy collection install`. Molecule only runs the 'dependency' stage when it is part +# of the sequence being executed. Our custom test_sequence therefore lists +# 'dependency' explicitly; without it `molecule test` would skip the install and rely on the +# collections already being present on the controller. +dependency: + name: 'galaxy' + options: + requirements-file: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/requirements.yml' + provisioner: playbooks: create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/create-vm.yml' @@ -56,6 +65,7 @@ provisioner: scenario: test_sequence: + - 'dependency' # must stay in the list so requirements.yml is installed (see header note) - 'create' - 'prepare' - 'converge' diff --git a/extensions/molecule/requirements.yml b/extensions/molecule/requirements.yml index b2ef5cb74..fe829fefc 100644 --- a/extensions/molecule/requirements.yml +++ b/extensions/molecule/requirements.yml @@ -1,4 +1,5 @@ collections: + - name: 'community.crypto' + - name: 'community.libvirt' - name: 'containers.podman' version: '>=1.10.0' - - name: 'community.libvirt' From 1f799b3cbbaf3c370ccc837a1c953063fc8f4fc4 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 11:12:12 +0200 Subject: [PATCH 18/36] docs(contributing): refine the Molecule testing section --- CONTRIBUTING.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 223ea8654..f1e218cef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -706,32 +706,32 @@ Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored ### Testing -Molecule is used as the framework to test the LFOps playbooks (*not roles*). The test scenarios and configurations live in `extension/molecule` and are structured as follows: +Molecule is used as the framework to test the LFOps playbooks (and therefore indirectly the roles). The test scenarios and configurations live in `extensions/molecule` and are structured as follows: ``` extensions └── molecule ├── apps -- test scenario, named after the playbook name │ ├── install -- if needed, sub-scenario - │ │ ├── converge.yml - │ │ ├── inventory -- scenario-specific inventory with variables that are needed for the playbook under test and optionally additional hosts (e.g. for a cluster test setup). Overwrites the shared inventory (extensions/molecule/inventory) + │ │ ├── converge.yml -- the actual test phase. this is where the playbook under test runs against the hosts + │ │ ├── inventory -- scenario-specific inventory with variables that are needed for the playbook under test and optionally additional hosts (e.g. for a cluster test setup). overwrites the shared inventory (extensions/molecule/inventory) │ │ │ ├── group_vars - │ │ │ │ └── systems_under_test.yml - │ │ │ └── hosts.yml - │ │ ├── molecule.yml -- scenario marker; required even if empty. Can also be used to overwrite, which playbooks are used by Molecule (e.g. to switch between VM and container provisioning playbooks) - │ │ └── verify.yml + │ │ │ │ └── systems_under_test.yml -- by convention, the "systems_under_test" group contains all our hosts against which the tests are run + │ │ │ └── hosts.yml -- here we select against which hosts we want to run (most of the time the hosts come from the shared inventory) and put them into the correct group for the playbook, here "lfops_apps" + │ │ ├── molecule.yml -- scenario marker; the file is required even if empty. can also be used to overwrite settings from the extensions/molecule/config.yml, for example which playbooks are used by Molecule (e.g. to switch between VM and container provisioning playbooks) + │ │ └── verify.yml -- runs after the test phase and uses ansible to check if the result is as expected │ └── remove -- additional sub-scenario │ └── ... - ├── config.yml -- valid for all scenarios, can be overwritten in each scenario's molecule.yml (same content and structure) + ├── config.yml -- valid for all scenarios, can be overwritten in each scenario's molecule.yml (content and structure are the same) ├── default -- we are not using the "default" scenario, but molecule needs this to run at all. could be used to share config (e.g. prepare.yml) across *all* scenarios │ └── molecule.yml - ├── inventory -- shared inventory across all scenarios and therefore available in all scenarios. Contains a basic set of VMs/containers that are commonly used. - │ ├── hosts.yml -- Required, even if empty, that Ansible can detect this inventory + ├── inventory -- shared inventory across all scenarios and therefore available in all scenarios. contains a basic set of VMs/containers that are commonly used + │ ├── hosts.yml -- required, even if empty, that Ansible can detect this inventory │ └── host_vars │ ├── debian11-container.yml │ ├── debian11-vm.yml │ └── ... - ├── monitoring_plugins -- scenario with no sub-scenarios + ├── monitoring_plugins -- a scenario with no sub-scenarios │ ├── converge.yml │ ├── inventory │ │ └── ... From b11a764049339698dcef4ac332296647ee0f7f96 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 11:31:27 +0200 Subject: [PATCH 19/36] refactor(molecule): prefix test playbooks with vm/container for sorting --- CONTRIBUTING.md | 4 ++-- extensions/molecule/config.yml | 6 +++--- .../{create-container.yml => container-create.yml} | 2 +- .../{destroy-container.yml => container-destroy.yml} | 0 .../{prepare-container.yml => container-prepare.yml} | 2 +- ...{create-container-fail.yml => container-create-fail.yml} | 0 .../molecule/playbooks/{create-vm.yml => vm-create.yml} | 0 .../molecule/playbooks/{destroy-vm.yml => vm-destroy.yml} | 0 .../molecule/playbooks/{prepare-vm.yml => vm-prepare.yml} | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename extensions/molecule/playbooks/{create-container.yml => container-create.yml} (96%) rename extensions/molecule/playbooks/{destroy-container.yml => container-destroy.yml} (100%) rename extensions/molecule/playbooks/{prepare-container.yml => container-prepare.yml} (89%) rename extensions/molecule/playbooks/tasks/{create-container-fail.yml => container-create-fail.yml} (100%) rename extensions/molecule/playbooks/{create-vm.yml => vm-create.yml} (100%) rename extensions/molecule/playbooks/{destroy-vm.yml => vm-destroy.yml} (100%) rename extensions/molecule/playbooks/{prepare-vm.yml => vm-prepare.yml} (76%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1e218cef..328177fd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -738,8 +738,8 @@ extensions │ ├── molecule.yml │ └── verify.yml ├── playbooks -- shared playbooks used by Molecule for running the scenarios - │ ├── create-container.yml - │ ├── destroy-container.yml + │ ├── container-create.yml + │ ├── container-destroy.yml │ └── ... └── requirements.yml ``` diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 1d2778346..30bcad714 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -59,9 +59,9 @@ dependency: provisioner: playbooks: - create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/create-vm.yml' - destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/destroy-vm.yml' - prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/prepare-vm.yml' + create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-create.yml' + destroy: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-destroy.yml' + prepare: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-prepare.yml' scenario: test_sequence: diff --git a/extensions/molecule/playbooks/create-container.yml b/extensions/molecule/playbooks/container-create.yml similarity index 96% rename from extensions/molecule/playbooks/create-container.yml rename to extensions/molecule/playbooks/container-create.yml index 3ab5989f8..81b5c94e9 100644 --- a/extensions/molecule/playbooks/create-container.yml +++ b/extensions/molecule/playbooks/container-create.yml @@ -23,7 +23,7 @@ # bare minimum and thus do not contain a Python interpreter. - name: 'Verify containers are running' ansible.builtin.include_tasks: - file: 'tasks/create-container-fail.yml' + file: 'tasks/container-create-fail.yml' when: > __molecule__container_result['container']['State']['ExitCode'] != 0 or not __molecule__container_result['container']['State']['Running'] diff --git a/extensions/molecule/playbooks/destroy-container.yml b/extensions/molecule/playbooks/container-destroy.yml similarity index 100% rename from extensions/molecule/playbooks/destroy-container.yml rename to extensions/molecule/playbooks/container-destroy.yml diff --git a/extensions/molecule/playbooks/prepare-container.yml b/extensions/molecule/playbooks/container-prepare.yml similarity index 89% rename from extensions/molecule/playbooks/prepare-container.yml rename to extensions/molecule/playbooks/container-prepare.yml index 761378648..abab89865 100644 --- a/extensions/molecule/playbooks/prepare-container.yml +++ b/extensions/molecule/playbooks/container-prepare.yml @@ -16,4 +16,4 @@ changed_when: true - name: 'Gather facts after Python is available' - ansible.builtin.setup: + ansible.builtin.setup: # yamllint disable-line rule:empty-values diff --git a/extensions/molecule/playbooks/tasks/create-container-fail.yml b/extensions/molecule/playbooks/tasks/container-create-fail.yml similarity index 100% rename from extensions/molecule/playbooks/tasks/create-container-fail.yml rename to extensions/molecule/playbooks/tasks/container-create-fail.yml diff --git a/extensions/molecule/playbooks/create-vm.yml b/extensions/molecule/playbooks/vm-create.yml similarity index 100% rename from extensions/molecule/playbooks/create-vm.yml rename to extensions/molecule/playbooks/vm-create.yml diff --git a/extensions/molecule/playbooks/destroy-vm.yml b/extensions/molecule/playbooks/vm-destroy.yml similarity index 100% rename from extensions/molecule/playbooks/destroy-vm.yml rename to extensions/molecule/playbooks/vm-destroy.yml diff --git a/extensions/molecule/playbooks/prepare-vm.yml b/extensions/molecule/playbooks/vm-prepare.yml similarity index 76% rename from extensions/molecule/playbooks/prepare-vm.yml rename to extensions/molecule/playbooks/vm-prepare.yml index 9ecccf529..aec0cdaa6 100644 --- a/extensions/molecule/playbooks/prepare-vm.yml +++ b/extensions/molecule/playbooks/vm-prepare.yml @@ -7,4 +7,4 @@ timeout: 120 - name: 'Gather facts' - ansible.builtin.setup: + ansible.builtin.setup: # yamllint disable-line rule:empty-values From 98a6f9b781dbc71260add9ba73bc48a4809d60ea Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 11:31:49 +0200 Subject: [PATCH 20/36] refactor(molecule): align package_facts to the rest of the repo --- extensions/molecule/apps/install/verify.yml | 3 +-- extensions/molecule/apps/remove/verify.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/molecule/apps/install/verify.yml b/extensions/molecule/apps/install/verify.yml index e45bdf41b..b1eb7bd19 100644 --- a/extensions/molecule/apps/install/verify.yml +++ b/extensions/molecule/apps/install/verify.yml @@ -3,8 +3,7 @@ gather_facts: false tasks: - name: 'Gather the package facts' - ansible.builtin.package_facts: - manager: 'auto' + ansible.builtin.package_facts: # yamllint disable-line rule:empty-values - name: 'Assert that zsh is installed' ansible.builtin.assert: diff --git a/extensions/molecule/apps/remove/verify.yml b/extensions/molecule/apps/remove/verify.yml index d5c8a39e2..07c6a6e9b 100644 --- a/extensions/molecule/apps/remove/verify.yml +++ b/extensions/molecule/apps/remove/verify.yml @@ -3,8 +3,7 @@ gather_facts: false tasks: - name: 'Gather the package facts' - ansible.builtin.package_facts: - manager: 'auto' + ansible.builtin.package_facts: # yamllint disable-line rule:empty-values - name: 'Assert that less is not installed' ansible.builtin.assert: From dedf33f87dfa427cbbcc5318ea5c494ef81dd093 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 11:36:32 +0200 Subject: [PATCH 21/36] docs(molecule): add README for the test playbooks --- extensions/molecule/playbooks/README.md | 137 ++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 extensions/molecule/playbooks/README.md diff --git a/extensions/molecule/playbooks/README.md b/extensions/molecule/playbooks/README.md new file mode 100644 index 000000000..39b1bf955 --- /dev/null +++ b/extensions/molecule/playbooks/README.md @@ -0,0 +1,137 @@ +# Molecule Provisioning Playbooks + +These shared playbooks back the Molecule test scenarios under `extensions/molecule`. They provision the systems under test as either containers or VMs and are wired into Molecule through the `create`, `prepare`, and `destroy` hooks in `extensions/molecule/config.yml`. A scenario can switch backends by overriding those hooks in its own `molecule.yml`. + +This directory also serves as a reference for how the LFOps test setup spins instances up and down. The variables consumed here are set per host in the shared inventory (`extensions/molecule/inventory/host_vars`) or in a scenario-specific inventory. + +Two backends are provided: + +* libvirt/KVM virtual machines, provisioned through the [linuxfabrik.lfops.kvm_vm](https://github.com/Linuxfabrik/lfops/tree/main/roles/kvm_vm) role. +* Podman containers. + + +## Mandatory Requirements + +* The collections listed in `extensions/molecule/requirements.yml` (`community.crypto`, `community.libvirt`, `containers.podman`). Molecule installs them during the `dependency` stage. + + +## Optional Requirements + +Depending on the backend a scenario uses: + +* VM backend: + + * A libvirt/KVM hypervisor reachable as the create host (`kvm_vm__host`, `localhost` by default), with a `default` storage pool and a `default` network. + * `virsh` on the Ansible controller. + * Passwordless sudo on the Ansible controller. + +* Container backend: + + * Podman on the Ansible controller. + + +## Playbooks + +`vm-create.yml` + +* Generates an ephemeral SSH keypair, downloads the cloud image into the libvirt storage pool (when `molecule__vm_image_url` is set), creates the VMs through the `kvm_vm` role, waits for each to obtain an IP address, and writes a dynamic inventory so Molecule can reach them over SSH. + +`vm-prepare.yml` + +* Waits for SSH to become available on each VM and gathers facts. + +`vm-destroy.yml` + +* Removes the VMs through the `kvm_vm` role and deletes the ephemeral keypair and dynamic inventory. + +`container-create.yml` + +* Starts one Podman container per host. Fails early and prints the container log when a container does not come up (via `tasks/container-create-fail.yml`). + +`container-prepare.yml` + +* Installs Python inside the container when the image does not ship it, then gathers facts. + +`container-destroy.yml` + +* Removes the containers. + + +## Mandatory Variables + +`molecule__container_image` + +* Container backend only. The image to start the container from. +* Type: String. + +Example: +```yaml +# mandatory (container backend) +molecule__container_image: 'docker.io/rockylinux/rockylinux:9-ubi-init' +``` + + +## Optional Variables + +`molecule__container_capabilities` + +* Container backend only. Linux capabilities to add to the container. +* Type: List. +* Default: unset (Podman default). + +`molecule__container_command` + +* Container backend only. The command the container runs to stay alive. +* Type: String. +* Default: `'sleep 1d'` + +`molecule__container_log_driver` + +* Container backend only. The Podman log driver. +* Type: String. +* Default: `'json-file'` + +`molecule__container_privileged` + +* Container backend only. Run the container in privileged mode. +* Type: Bool. +* Default: `false` + +`molecule__container_systemd` + +* Container backend only. Run the container with systemd enabled. +* Type: Bool. +* Default: `false` + +`molecule__container_volumes` + +* Container backend only. Volumes to mount into the container. +* Type: List. +* Default: unset (no volumes). + +`molecule__vm_image_url` + +* VM backend only. URL of the cloud image to download into the libvirt storage pool. When unset, no image is downloaded and the base image is assumed to exist already. +* Type: String. + +The VM backend additionally consumes the `kvm_vm__*` variables, which are passed straight to the [linuxfabrik.lfops.kvm_vm](https://github.com/Linuxfabrik/lfops/tree/main/roles/kvm_vm) role; see that role's README for their meaning. The playbooks set `kvm_vm__ssh_authorized_keys` and `kvm_vm__state` themselves. + +Example: +```yaml +# optional (container backend) +molecule__container_command: '/usr/sbin/init' +molecule__container_systemd: true + +# optional (VM backend) +molecule__vm_image_url: 'https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) From f94e41f62b299b75adfdaf3060a4e2c280c9a6bb Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 12:19:04 +0200 Subject: [PATCH 22/36] test(molecule): add commented example reference scenario A fully commented, non-functional reference scenario (install + remove sub-scenarios) for the fictional example playbook. It demonstrates how to structure a Molecule test and what to assert: behaviour and end state (service running, the app using the configured value, API-managed state) rather than the role's own mechanics. Copy it as the starting point for a new playbook's test. --- CONTRIBUTING.md | 44 +++++++++++++++ .../molecule/example/install/converge.yml | 9 +++ .../group_vars/systems_under_test.yml | 27 +++++++++ .../example/install/inventory/hosts.yml | 27 +++++++++ .../molecule/example/install/molecule.yml | 12 ++++ .../molecule/example/install/verify.yml | 55 +++++++++++++++++++ .../molecule/example/remove/converge.yml | 5 ++ .../group_vars/systems_under_test.yml | 18 ++++++ .../example/remove/inventory/hosts.yml | 19 +++++++ .../molecule/example/remove/molecule.yml | 8 +++ extensions/molecule/example/remove/verify.yml | 31 +++++++++++ 11 files changed, 255 insertions(+) create mode 100644 extensions/molecule/example/install/converge.yml create mode 100644 extensions/molecule/example/install/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/example/install/inventory/hosts.yml create mode 100644 extensions/molecule/example/install/molecule.yml create mode 100644 extensions/molecule/example/install/verify.yml create mode 100644 extensions/molecule/example/remove/converge.yml create mode 100644 extensions/molecule/example/remove/inventory/group_vars/systems_under_test.yml create mode 100644 extensions/molecule/example/remove/inventory/hosts.yml create mode 100644 extensions/molecule/example/remove/molecule.yml create mode 100644 extensions/molecule/example/remove/verify.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 328177fd2..e7a126e06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -725,6 +725,11 @@ extensions ├── config.yml -- valid for all scenarios, can be overwritten in each scenario's molecule.yml (content and structure are the same) ├── default -- we are not using the "default" scenario, but molecule needs this to run at all. could be used to share config (e.g. prepare.yml) across *all* scenarios │ └── molecule.yml + ├── example -- fully commented reference scenario (install + remove sub-scenarios); copy it when adding a new test, like the example role + │ ├── install + │ │ └── ... + │ └── remove + │ └── ... ├── inventory -- shared inventory across all scenarios and therefore available in all scenarios. contains a basic set of VMs/containers that are commonly used │ ├── hosts.yml -- required, even if empty, that Ansible can detect this inventory │ └── host_vars @@ -744,6 +749,8 @@ extensions └── requirements.yml ``` +The `extensions/molecule/example` scenario mirrors the `example` role: it is a fully commented, non-functional reference that walks through every file of a scenario (a non-functional reference because the `example` playbook installs the fictional "Example" application). Copy it as the starting point when adding a test for a playbook. + Tests can be run against a subset of targets by providing them as a comma-separated list via the project-specific `LFOPS_TEST_TARGETS` environment variable: ```shell @@ -761,6 +768,43 @@ Known Limitations: * Ansible Navigator does not work out of the box. +#### How a scenario runs + +`molecule test --scenario-name ` runs the steps listed in the `test_sequence` of `config.yml`, in order: + +* `dependency`: installs the collections from `requirements.yml`. +* `create`: provisions the instances (libvirt/KVM VMs or Podman containers). +* `prepare`: waits until the instances are reachable and gathers facts. +* `converge`: runs the playbook under test (`converge.yml`). +* `verify`: runs `verify.yml` against the converged instances. +* `idempotence`: runs the playbook a second time and fails if it reports any change. +* `verify`: runs `verify.yml` again, now against the idempotent state. +* `destroy`: tears the instances down. + + +#### What to verify + +Verify the observable end result, not the steps the role took to get there. Ansible and the role already guarantee their own mechanics, so re-checking those only tests Ansible. The guiding question is "what can only be confirmed by looking at the running system?". + +Two guarantees come for free, so do not rebuild them in `verify.yml`: + +* If the playbook errors out, the `converge` step fails and `verify.yml` never runs. `verify.yml` is therefore only ever about the *result* of a successful run, not about whether the run crashed. +* Idempotence is enforced by the dedicated `idempotence` step. Never add tasks that check "running it a second time changes nothing". + +Do **not** assert: + +* That a templated file exists or contains a given line. If the `template` task ran, the file is there with the rendered content; asserting it only exercises Jinja and the `template` module. +* That a package was installed or a file was written, as the goal of the test. The module already reports `changed`/`ok` for that. A one-line "the package is installed" smoke check is fine as a floor, but it is not where the value of the test lies. + +Do assert what only the running system can confirm, that is, that the pieces actually work together: + +* The application is running and enabled (`ansible.builtin.service_facts`), and reachable on its port (`ansible.builtin.wait_for`, or a request that would fail if it were not). A service that starts proves the deployed config is at least valid, which the role's own tasks cannot tell you. +* The application actually *uses* the configured values. Ask the running application (an API or status endpoint via `ansible.builtin.uri`, or a CLI that prints the effective configuration) and assert it reports the value the scenario set in `group_vars`. This is the important one: it proves the whole chain, `group_vars` to template to the service reading the file to its behaviour, which is exactly what grepping the config file does not. +* End state managed outside the package and file layer (users, databases, API objects) is present, or absent in a removal scenario. + +A useful rule of thumb: if an assertion would still pass while the service is dead or running with the wrong configuration, it is testing the wrong thing. + + ### Credits * diff --git a/extensions/molecule/example/install/converge.yml b/extensions/molecule/example/install/converge.yml new file mode 100644 index 000000000..b3183fed7 --- /dev/null +++ b/extensions/molecule/example/install/converge.yml @@ -0,0 +1,9 @@ +# converge.yml is the test phase. Molecule runs it after 'create' and +# 'prepare' have provisioned and readied the systems under test. +# +# Import the playbook under test by its collection FQCN, so the test exercises +# the real playbook (and through it, the roles) instead of a copy that could +# drift. Keep this file to the single import; the test inputs live in the +# scenario inventory, and the checks live in verify.yml. +- name: 'Converge example playbook' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.example' diff --git a/extensions/molecule/example/install/inventory/group_vars/systems_under_test.yml b/extensions/molecule/example/install/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..569a66176 --- /dev/null +++ b/extensions/molecule/example/install/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,27 @@ +# Variables the playbook under test needs, applied to every system under test. +# This is the test's input: set the mandatory variables, plus any optional ones +# whose behaviour the scenario should exercise. + +# Mandatory: the example role requires a version. +example__version: '3.2.1' + +# Skip the fictional repo_example role the playbook pulls in. A test for a real +# playbook would normally let the repo role run so the package source is set up. +example__skip_repo_example: true + +# Optional simple values, set through the __group_var injection slot so the +# role's combined_var logic merges them over its defaults. +example__conf_log_level__group_var: 'debug' +example__conf_max_connections__group_var: 200 + +# Combined variables are lists of dicts with a state. The install scenario adds +# one plugin and one user so verify.yml has concrete state to assert on. The +# matching remove scenario flips these to state: 'absent'. +example__plugins__group_var: + - name: 'example-plugin-auth-ldap' + state: 'present' + +example__users__group_var: + - name: 'example-admin' + password: 'linuxfabrik' + state: 'present' diff --git a/extensions/molecule/example/install/inventory/hosts.yml b/extensions/molecule/example/install/inventory/hosts.yml new file mode 100644 index 000000000..eeb915364 --- /dev/null +++ b/extensions/molecule/example/install/inventory/hosts.yml @@ -0,0 +1,27 @@ +# yamllint disable rule:empty-values + +# Scenario inventory. It is layered on top of the shared inventory +# (extensions/molecule/inventory) through the two --inventory flags in +# config.yml, so the host_vars defined there (connection, image, kvm_vm__*) +# stay available; this file only adds the host-to-group mapping. + +# Put the systems under test into the group the playbook targets. The example +# playbook runs against 'lfops_example' (see playbooks/example.yml: hosts). +lfops_example: + children: + systems_under_test: + +# 'systems_under_test' is the conventional group holding the actual hosts. +# Select the platforms the role supports; here the standard VM set. Trim this +# (or use LFOPS_TEST_TARGETS at runtime) to test against fewer hosts. +systems_under_test: + hosts: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: diff --git a/extensions/molecule/example/install/molecule.yml b/extensions/molecule/example/install/molecule.yml new file mode 100644 index 000000000..175265265 --- /dev/null +++ b/extensions/molecule/example/install/molecule.yml @@ -0,0 +1,12 @@ +# Molecule scenario marker. Required even when empty, so that Molecule +# discovers the scenario. Settings from extensions/molecule/config.yml can be +# overridden here when a scenario needs it, for example to point the +# create/prepare/destroy hooks at the container playbooks instead of the VM +# ones. +# +# Like the 'example' role, this 'example' scenario is a non-functional +# reference: the 'example' playbook installs the fictional "Example" +# application, so the scenario does not converge against a real system. It +# exists to show how a Molecule test for an LFOps playbook is structured and +# commented. Copy it as the starting point when adding a test for a real +# playbook. diff --git a/extensions/molecule/example/install/verify.yml b/extensions/molecule/example/install/verify.yml new file mode 100644 index 000000000..f8612061b --- /dev/null +++ b/extensions/molecule/example/install/verify.yml @@ -0,0 +1,55 @@ +# verify.yml runs after converge, and again after the idempotence step (see the +# test_sequence in config.yml). It checks the observable end result with Ansible +# facts and read-only modules - never by re-running the role. +# +# The guiding question is "what can only be confirmed by looking at the running +# system?". We do not assert that /etc/example/example.conf exists or contains a +# certain line: if the template task ran, that is already guaranteed, so it would +# only test Jinja and the template module. We confirm instead that the service +# runs and that it actually uses the values this scenario set in group_vars. +- name: 'Verify example is installed and configured' + hosts: 'systems_under_test' + gather_facts: false + tasks: + + # Floor check: did the install happen at all? On its own this would still + # pass even if the service never starts, so it is the start, not the goal. + - name: 'Gather the package facts' + ansible.builtin.package_facts: # yamllint disable-line rule:empty-values + + - name: 'Assert that the example-server package is installed' + ansible.builtin.assert: + that: '"example-server" in ansible_facts["packages"]' + + # Real check: is the service up? This catches a config that deployed without + # error but makes the service crash on start - something the role's own + # tasks cannot detect. + - name: 'Gather the service facts' + ansible.builtin.service_facts: # yamllint disable-line rule:empty-values + + - name: 'Assert that example.service is running' + ansible.builtin.assert: + that: 'ansible_facts["services"]["example.service"]["state"] == "running"' + + # The important check: does the running application actually use the value we + # configured? Ask the application (here an illustrative status endpoint), not + # the config file. This scenario sets example__conf_max_connections to 200, + # so the live configuration must report 200. This proves the whole chain + # works: group_vars -> template -> service reads the file -> behaviour. + - name: 'curl http://127.0.0.1:8080/api/status' + ansible.builtin.uri: + url: 'http://127.0.0.1:8080/api/status' + return_content: true + register: '__molecule__example_status_result' + + - name: 'Assert the running application uses the configured max_connections' + ansible.builtin.assert: + that: '__molecule__example_status_result["json"]["max_connections"] == 200' + + # End state managed over the REST API is invisible to package/service facts, + # so query it directly. The install scenario creates this user, so a GET for + # it returns 200. + - name: 'curl http://127.0.0.1:8080/api/user/example-admin' + ansible.builtin.uri: + url: 'http://127.0.0.1:8080/api/user/example-admin' + status_code: 200 diff --git a/extensions/molecule/example/remove/converge.yml b/extensions/molecule/example/remove/converge.yml new file mode 100644 index 000000000..0ba8d4397 --- /dev/null +++ b/extensions/molecule/example/remove/converge.yml @@ -0,0 +1,5 @@ +# Same playbook as the install sub-scenario; only the scenario inventory +# differs. The remove sub-scenario flips the plugin and user to state: 'absent' +# (see its group_vars), which exercises the role's removal path. +- name: 'Converge example playbook' + ansible.builtin.import_playbook: 'linuxfabrik.lfops.example' diff --git a/extensions/molecule/example/remove/inventory/group_vars/systems_under_test.yml b/extensions/molecule/example/remove/inventory/group_vars/systems_under_test.yml new file mode 100644 index 000000000..a26d01b40 --- /dev/null +++ b/extensions/molecule/example/remove/inventory/group_vars/systems_under_test.yml @@ -0,0 +1,18 @@ +# The remove scenario runs the same playbook but flips the plugin and user to +# state: 'absent'. This exercises the list-with-state removal pattern the LFOps +# roles use, and gives verify.yml something concrete to assert is gone. +# +# In a real suite, run the install sub-scenario first to create the state, then +# this one to remove it. As a standalone run it still demonstrates that a second +# converge with state: 'absent' leaves the host without the plugin/user. + +example__version: '3.2.1' +example__skip_repo_example: true + +example__plugins__group_var: + - name: 'example-plugin-auth-ldap' + state: 'absent' + +example__users__group_var: + - name: 'example-admin' + state: 'absent' diff --git a/extensions/molecule/example/remove/inventory/hosts.yml b/extensions/molecule/example/remove/inventory/hosts.yml new file mode 100644 index 000000000..6c133c8be --- /dev/null +++ b/extensions/molecule/example/remove/inventory/hosts.yml @@ -0,0 +1,19 @@ +# yamllint disable rule:empty-values + +# Same host-to-group mapping as the install sub-scenario. Sub-scenarios do not +# share inventory with each other, so each repeats the hosts it runs against. +lfops_example: + children: + systems_under_test: + +systems_under_test: + hosts: + debian11-vm: + debian12-vm: + debian13-vm: + rocky8-vm: + rocky9-vm: + rocky10-vm: + ubuntu2004-vm: + ubuntu2204-vm: + ubuntu2404-vm: diff --git a/extensions/molecule/example/remove/molecule.yml b/extensions/molecule/example/remove/molecule.yml new file mode 100644 index 000000000..a683437dd --- /dev/null +++ b/extensions/molecule/example/remove/molecule.yml @@ -0,0 +1,8 @@ +# Molecule scenario marker for the 'remove' sub-scenario. A scenario can have +# several sub-scenarios (here 'install' and 'remove'); each is a directory with +# its own converge/inventory/verify, run with +# `molecule test --scenario-name example/remove`. +# +# Like the 'install' sub-scenario, this is a non-functional reference: the +# 'example' playbook targets the fictional "Example" application. It shows how +# to test the removal half of the role's list-with-state pattern. diff --git a/extensions/molecule/example/remove/verify.yml b/extensions/molecule/example/remove/verify.yml new file mode 100644 index 000000000..d0df5900c --- /dev/null +++ b/extensions/molecule/example/remove/verify.yml @@ -0,0 +1,31 @@ +# Verify the removal. Same mindset as install/verify.yml: check the observable +# result, and confirm the removal did not break the still-installed application. +- name: 'Verify the example plugin and user are removed' + hosts: 'systems_under_test' + gather_facts: false + tasks: + + # Removing a plugin and a user must not take the application down with them, + # so the most important check here is that the service is still running. + - name: 'Gather the service facts' + ansible.builtin.service_facts: # yamllint disable-line rule:empty-values + + - name: 'Assert that example.service is still running' + ansible.builtin.assert: + that: 'ansible_facts["services"]["example.service"]["state"] == "running"' + + # The plugin is an OS package, so its removal is observable in the package + # facts - the counterpart to install's "is present" check. + - name: 'Gather the package facts' + ansible.builtin.package_facts: # yamllint disable-line rule:empty-values + + - name: 'Assert that the example plugin package is absent' + ansible.builtin.assert: + that: '"example-plugin-auth-ldap" not in ansible_facts["packages"]' + + # The user is managed over the REST API, so assert its absence the same way + # install asserts its presence: a GET for it now returns 404. + - name: 'curl http://127.0.0.1:8080/api/user/example-admin' + ansible.builtin.uri: + url: 'http://127.0.0.1:8080/api/user/example-admin' + status_code: 404 From 3022eee15f747a1940807e69b1131dbde9308f15 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 14:57:00 +0200 Subject: [PATCH 23/36] style(molecule): drop redundant changed_when on the raw python install --- extensions/molecule/playbooks/container-prepare.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/molecule/playbooks/container-prepare.yml b/extensions/molecule/playbooks/container-prepare.yml index abab89865..8452ac710 100644 --- a/extensions/molecule/playbooks/container-prepare.yml +++ b/extensions/molecule/playbooks/container-prepare.yml @@ -13,7 +13,6 @@ apt-get update && apt-get install -y python3 fi ''' - changed_when: true - name: 'Gather facts after Python is available' ansible.builtin.setup: # yamllint disable-line rule:empty-values From 977ac7701734c089fcba32b449adbed2f7b58eff Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 17:18:39 +0200 Subject: [PATCH 24/36] test(molecule): point localhost at the python with libvirt bindings --- extensions/molecule/inventory/host_vars/localhost.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 extensions/molecule/inventory/host_vars/localhost.yml diff --git a/extensions/molecule/inventory/host_vars/localhost.yml b/extensions/molecule/inventory/host_vars/localhost.yml new file mode 100644 index 000000000..4f862fb41 --- /dev/null +++ b/extensions/molecule/inventory/host_vars/localhost.yml @@ -0,0 +1 @@ +ansible_python_interpreter: '/usr/bin/python3' # needs to point to the python3 version that has the libvirt module (`python3 -c 'import libvirt'` has to work) From dc00cadbd57c502bc1ecdc5ca42ad35278ceb8d8 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 17:19:04 +0200 Subject: [PATCH 25/36] fix(molecule): target system libvirt explicitly in VM IP discovery --- extensions/molecule/playbooks/vm-create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml index 1e5a83649..cc91e1215 100644 --- a/extensions/molecule/playbooks/vm-create.yml +++ b/extensions/molecule/playbooks/vm-create.yml @@ -73,7 +73,7 @@ - name: 'Wait for VMs to obtain IP addresses' ansible.builtin.command: - cmd: 'virsh domifaddr {{ inventory_hostname }} --source arp' + cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ inventory_hostname }} --source arp' register: '__molecule__vm_ipaddr_result' until: '__molecule__vm_ipaddr_result["stdout_lines"] | select("match", ".*ipv4.*") | list | length > 0' retries: 30 From d51b33edceff3ae9486a5bb4327d762393f83947 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 17:31:11 +0200 Subject: [PATCH 26/36] docs(CONTRIBUTING): add troubleshooting section for molecule --- CONTRIBUTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7a126e06..ee5447d57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -805,6 +805,15 @@ Do assert what only the running system can confirm, that is, that the pieces act A useful rule of thumb: if an assertion would still pass while the service is dead or running with the wrong configuration, it is testing the wrong thing. +#### Troubleshooting + +**`molecule test` aborts with `ansible_compat.errors.InvalidPrerequisiteError: Command ansible-galaxy collection install -vvv --force /path/to/lfops` during prerun while installing the local collection** + +* Before running a scenario, Molecule's prerun step tries to install the current repository as a collection with `ansible-galaxy collection install --force `. That build fails because `galaxy.yml` carries a non-semver `version` (`main`), which `ansible-galaxy` rejects. +* Option 1: disable the prerun so Molecule stops trying to build and install the local collection, by setting `prerun: false` as a top-level key in the `config.yml`. If you do this, you have to make sure that LFOps is installed yourself. +* Option 2: If you installed LFOps by symlinking it, make sure the link points to the **same** folder that you are running `molecule` in (`ln -sf "$(pwd)" ~/.ansible/collections/ansible_collections/linuxfabrik/lfops`). + + ### Credits * From 2d7fe540cabb2de3de717744cc9889eac5f5a8d0 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 18:28:06 +0200 Subject: [PATCH 27/36] refactor(molecule): streamline the VM and container provisioning playbooks --- extensions/molecule/config.yml | 11 +++ .../kernel_settings/sysctl/verify.yml | 2 +- .../molecule/playbooks/container-create.yml | 64 ++++++++----- .../molecule/playbooks/container-prepare.yml | 6 +- .../playbooks/tasks/container-create-fail.yml | 14 --- extensions/molecule/playbooks/vm-create.yml | 92 +++++++++---------- extensions/molecule/playbooks/vm-destroy.yml | 14 +-- 7 files changed, 104 insertions(+), 99 deletions(-) delete mode 100644 extensions/molecule/playbooks/tasks/container-create-fail.yml diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 30bcad714..97dc4d677 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -57,6 +57,17 @@ dependency: options: requirements-file: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/requirements.yml' +# Inventory model (why there is no 'platforms:' block and no managed-driver instance_config): +# We test playbooks, which are inventory-driven (hosts: lfops_*, group_vars, host_vars). The +# scenarios therefore use real inventory files (extensions/molecule/inventory plus each +# scenario's own inventory) so the test inventory mirrors a production one and converge runs the +# playbook the way an admin would. Molecule's managed driver only injects connection details +# (ansible_host, ansible_user, ...) for hosts listed under 'platforms:', which the inventory +# model does not use - so vm-create.yml discovers each VM's IP and writes the connection details +# into Molecule's ephemeral inventory directory itself (Molecule includes that directory +# automatically). Host key checking is turned off above (host_key_checking) because the VMs are +# throwaway and reuse IPs from the libvirt default network. + provisioner: playbooks: create: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/playbooks/vm-create.yml' diff --git a/extensions/molecule/kernel_settings/sysctl/verify.yml b/extensions/molecule/kernel_settings/sysctl/verify.yml index c832c6f51..82f1b5d41 100644 --- a/extensions/molecule/kernel_settings/sysctl/verify.yml +++ b/extensions/molecule/kernel_settings/sysctl/verify.yml @@ -9,4 +9,4 @@ - name: 'Assert that vm.overcommit_memory is set to 1' ansible.builtin.assert: - that: '__molecule__sysctl_vm_overcommit_memory_result["content"] | b64decode | int == 1' + that: '__molecule__sysctl_vm_overcommit_memory_result["content"] | ansible.builtin.b64decode | int == 1' diff --git a/extensions/molecule/playbooks/container-create.yml b/extensions/molecule/playbooks/container-create.yml index 81b5c94e9..225ea4c22 100644 --- a/extensions/molecule/playbooks/container-create.yml +++ b/extensions/molecule/playbooks/container-create.yml @@ -3,27 +3,45 @@ gather_facts: false tasks: - - name: 'Create containers from inventory' - containers.podman.podman_container: - capabilities: '{{ molecule__container_capabilities | default(omit) }}' - command: '{{ molecule__container_command | default("sleep 1d") }}' - hostname: '{{ inventory_hostname }}' - image: '{{ molecule__container_image }}' - log_driver: '{{ molecule__container_log_driver | default("json-file") }}' - name: '{{ inventory_hostname }}' - privileged: '{{ molecule__container_privileged | default(false) }}' - state: 'started' - systemd: '{{ molecule__container_systemd | default(false) }}' - volumes: '{{ molecule__container_volumes | default(omit) }}' - register: '__molecule__container_result' - delegate_to: 'localhost' + - name: 'Create the container and confirm it is running' + block: - # Not using ansible.builtin.wait_for_connection here as it depends on a working Python - # installation inside the container; however, some container images are shipped with the - # bare minimum and thus do not contain a Python interpreter. - - name: 'Verify containers are running' - ansible.builtin.include_tasks: - file: 'tasks/container-create-fail.yml' - when: > - __molecule__container_result['container']['State']['ExitCode'] != 0 or - not __molecule__container_result['container']['State']['Running'] + - name: 'Create the container from inventory' + containers.podman.podman_container: + capabilities: '{{ molecule__container_capabilities | default(omit) }}' + command: '{{ molecule__container_command | default("sleep 1d") }}' + hostname: '{{ inventory_hostname }}' + image: '{{ molecule__container_image }}' + log_driver: '{{ molecule__container_log_driver | default("json-file") }}' + name: '{{ inventory_hostname }}' + privileged: '{{ molecule__container_privileged | default(false) }}' + state: 'started' + systemd: '{{ molecule__container_systemd | default(false) }}' + volumes: '{{ molecule__container_volumes | default(omit) }}' + register: '__molecule__container_result' + delegate_to: 'localhost' + + # podman reports the container as 'started' even when the entrypoint exits immediately, + # so assert on the actual state. A failure here drops into the rescue, which surfaces the + # container log to explain why it did not come up. + - name: 'Assert the container is running' + ansible.builtin.assert: + that: + - '__molecule__container_result["container"]["State"]["ExitCode"] == 0' + - '__molecule__container_result["container"]["State"]["Running"]' + quiet: true + + rescue: + + - name: 'Retrieve the container log' + ansible.builtin.command: + cmd: 'podman logs {{ inventory_hostname }}' + changed_when: false + register: '__molecule__container_log_result' + delegate_to: 'localhost' + + - name: 'Fail with the container log' + ansible.builtin.fail: + msg: | + Container {{ inventory_hostname }} failed to start properly. + Log output: {{ __molecule__container_log_result["stdout"] | d("No logs available") }} diff --git a/extensions/molecule/playbooks/container-prepare.yml b/extensions/molecule/playbooks/container-prepare.yml index 8452ac710..8349b9d1c 100644 --- a/extensions/molecule/playbooks/container-prepare.yml +++ b/extensions/molecule/playbooks/container-prepare.yml @@ -2,17 +2,13 @@ hosts: 'systems_under_test' gather_facts: false tasks: - - name: 'Install Python using raw module (in case it is missing from the container image)' + - name: 'Install Python using the raw module (in case it is missing from the container image)' ansible.builtin.raw: | - sh -c ''' if command -v dnf > /dev/null 2>&1; then dnf install -y python3 - elif command -v yum > /dev/null 2>&1; then - yum install -y python3 elif command -v apt-get > /dev/null 2>&1; then apt-get update && apt-get install -y python3 fi - ''' - name: 'Gather facts after Python is available' ansible.builtin.setup: # yamllint disable-line rule:empty-values diff --git a/extensions/molecule/playbooks/tasks/container-create-fail.yml b/extensions/molecule/playbooks/tasks/container-create-fail.yml deleted file mode 100644 index 631aef6bd..000000000 --- a/extensions/molecule/playbooks/tasks/container-create-fail.yml +++ /dev/null @@ -1,14 +0,0 @@ -- name: 'Retrieve container log' - ansible.builtin.command: - cmd: 'podman logs {{ __molecule__container_result["container"]["Name"] }}' - changed_when: false - register: '__molecule__container_log_result' - delegate_to: 'localhost' - -- name: 'Display container log and fail' - ansible.builtin.fail: - msg: | - Container {{ __molecule__container_result['container']['Name'] }} failed to start properly. - Exit Code: {{ __molecule__container_result['container']['State']['ExitCode'] }} - Running: {{ __molecule__container_result['container']['State']['Running'] }} - Log output: {{ __molecule__container_log_result['stdout'] | default('No logs available') }} diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml index cc91e1215..fd9d4da93 100644 --- a/extensions/molecule/playbooks/vm-create.yml +++ b/extensions/molecule/playbooks/vm-create.yml @@ -1,97 +1,95 @@ -- name: 'Prepare for VM creation' - hosts: 'systems_under_test' +# Shared, controller-side setup. Runs once on localhost (the hypervisor), so there is no +# delegate_to / run_once juggling. The keypair and pool info become normal facts on localhost +# that the later plays read via hostvars["localhost"]. +- name: 'Prepare the controller for VM creation' + hosts: 'localhost' gather_facts: false vars: __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - - name: 'Generate ephemeral SSH keypair' + - name: 'Generate the ephemeral SSH keypair' community.crypto.openssh_keypair: path: '{{ __molecule__ephemeral_dir }}/molecule_key' type: 'ed25519' comment: 'molecule' - delegate_to: 'localhost' - run_once: true + register: '__molecule__keypair_result' - - name: 'Read SSH public key' - ansible.builtin.slurp: - src: '{{ __molecule__ephemeral_dir }}/molecule_key.pub' - delegate_to: 'localhost' - register: '__molecule__pubkey_result' - run_once: true - - - name: 'Set SSH public key fact' - ansible.builtin.set_fact: - __molecule__ssh_pubkey: '{{ __molecule__pubkey_result["content"] | b64decode | trim }}' - delegate_to: 'localhost' - delegate_facts: true - run_once: true - - - name: 'Get info on storage pools' + - name: 'Get the libvirt storage pool info' community.libvirt.virt_pool: command: 'info' - delegate_to: 'localhost' + uri: '{{ kvm_vm__connect_url | d("qemu:///system") }}' register: '__molecule__pool_result' - run_once: true - - name: 'Download cloud images into storage pool' - ansible.builtin.get_url: - url: '{{ molecule__vm_image_url }}' - dest: '{{ __molecule__pool_result["pools"]["default"]["path"] }}/{{ kvm_vm__base_image }}' - mode: 0o644 - become: true - delegate_to: 'localhost' - when: 'molecule__vm_image_url is defined' - - - name: 'Ensure ephemeral inventory directory exists' + - name: 'Ensure the ephemeral inventory directory exists' ansible.builtin.file: path: '{{ __molecule__ephemeral_dir }}/inventory' state: 'directory' mode: 0o755 - delegate_to: 'localhost' - run_once: true -- name: 'Create VM instances using kvm_vm role' - become: true +# One VM per host in systems_under_test. The kvm_vm role delegates to kvm_vm__host, so this play +# never connects to the (not yet existing) target; the play-level become is for the role's +# privileged libvirt operations and the image download into the root-owned pool. +- name: 'Create VM instances using the kvm_vm role' hosts: 'systems_under_test' + become: true gather_facts: false - roles: - - role: 'linuxfabrik.lfops.kvm_vm' vars: kvm_vm__ssh_authorized_keys: - - '{{ hostvars["localhost"]["__molecule__ssh_pubkey"] }}' + - '{{ hostvars["localhost"]["__molecule__keypair_result"]["public_key"] }}' kvm_vm__state: 'running' + pre_tasks: + + - name: 'Download the cloud image into the storage pool' + ansible.builtin.get_url: + url: '{{ molecule__vm_image_url }}' + dest: '{{ hostvars["localhost"]["__molecule__pool_result"]["pools"][kvm_vm__pool | d("default")]["path"] }}/{{ kvm_vm__base_image }}' + mode: 0o644 + delegate_to: 'localhost' + when: 'molecule__vm_image_url is defined' + + roles: + - role: 'linuxfabrik.lfops.kvm_vm' -- name: 'Discover VM IPs and write dynamic inventory' +# Discover each VM's address via the qemu guest agent and write it into Molecule's ephemeral +# inventory directory. Molecule includes that directory automatically, so converge and verify +# reach the VMs over SSH. See the inventory-model note in config.yml for why this is hand-written +# instead of using Molecule's instance_config. +- name: 'Discover VM addresses and write the dynamic inventory' hosts: 'systems_under_test' gather_facts: false vars: __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - - name: 'Wait for VMs to obtain IP addresses' + - name: 'Wait for the VM to report a non-loopback IPv4 address via the guest agent' ansible.builtin.command: - cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ inventory_hostname }} --source arp' + cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ inventory_hostname }} --source agent' register: '__molecule__vm_ipaddr_result' - until: '__molecule__vm_ipaddr_result["stdout_lines"] | select("match", ".*ipv4.*") | list | length > 0' + until: >- + __molecule__vm_ipaddr_result['stdout_lines'] + | select('ansible.builtin.search', 'ipv4') + | reject('ansible.builtin.search', '^\s*lo\s') + | list | length > 0 retries: 30 delay: 5 changed_when: false delegate_to: 'localhost' - - name: 'Parse VM IP addresses' + - name: 'Parse the VM IPv4 address' ansible.builtin.set_fact: __molecule__vm_ipaddr: >- {{ __molecule__vm_ipaddr_result['stdout_lines'] - | select('match', '.*ipv4.*') + | select('ansible.builtin.search', 'ipv4') + | reject('ansible.builtin.search', '^\s*lo\s') | first - | regex_replace('.*\s+(\d+\.\d+\.\d+\.\d+)\/.*', '\1') + | ansible.builtin.regex_search('([0-9]{1,3}\.){3}[0-9]{1,3}') }} - - name: 'Write dynamic VM inventory' + - name: 'Write the dynamic VM inventory' ansible.builtin.copy: content: | molecule: diff --git a/extensions/molecule/playbooks/vm-destroy.yml b/extensions/molecule/playbooks/vm-destroy.yml index b98afabf5..367ed4d19 100644 --- a/extensions/molecule/playbooks/vm-destroy.yml +++ b/extensions/molecule/playbooks/vm-destroy.yml @@ -1,6 +1,6 @@ -- name: 'Destroy VM instances using kvm_vm role' - become: true +- name: 'Destroy VM instances using the kvm_vm role' hosts: 'systems_under_test' + become: true gather_facts: false roles: - role: 'linuxfabrik.lfops.kvm_vm' @@ -8,22 +8,18 @@ kvm_vm__state: 'absent' -- name: 'Clean up Molecule artifacts' +- name: 'Clean up Molecule artifacts on the controller' hosts: 'localhost' gather_facts: false vars: __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - - name: 'Remove ephemeral SSH keypair' + - name: 'Remove the ephemeral SSH keypair and dynamic inventory' ansible.builtin.file: path: '{{ __molecule__ephemeral_dir }}/{{ item }}' state: 'absent' loop: - 'molecule_key' - 'molecule_key.pub' - - - name: 'Remove dynamic inventory' - ansible.builtin.file: - path: '{{ __molecule__ephemeral_dir }}/inventory' - state: 'absent' + - 'inventory/molecule_vm.yml' From 98ad88ad289f27b2713624ea7f400add48c9acea Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 18:36:04 +0200 Subject: [PATCH 28/36] test(molecule): silence the galaxy roles dependency warning --- extensions/molecule/config.yml | 4 ++++ extensions/molecule/requirements-roles.yml | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 extensions/molecule/requirements-roles.yml diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 97dc4d677..55cf01a34 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -52,10 +52,14 @@ ansible: # of the sequence being executed. Our custom test_sequence therefore lists # 'dependency' explicitly; without it `molecule test` would skip the install and rely on the # collections already being present on the controller. +# The galaxy dependency also runs a roles sub-step that ignores requirements-file and reads +# role-file instead. We have no Galaxy roles, so role-file points at an empty +# requirements-roles.yml purely to stop it warning about a missing roles requirements file. dependency: name: 'galaxy' options: requirements-file: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/requirements.yml' + role-file: '${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/requirements-roles.yml' # Inventory model (why there is no 'platforms:' block and no managed-driver instance_config): # We test playbooks, which are inventory-driven (hosts: lfops_*, group_vars, host_vars). The diff --git a/extensions/molecule/requirements-roles.yml b/extensions/molecule/requirements-roles.yml new file mode 100644 index 000000000..0b7d8cf85 --- /dev/null +++ b/extensions/molecule/requirements-roles.yml @@ -0,0 +1,5 @@ +# The galaxy dependency always runs both a roles and a collections sub-step. We have no Galaxy +# roles to install (everything is a local collection, see requirements.yml), but pointing the +# roles sub-step at this empty file gives it something to read so it does not warn "Missing roles +# requirements file: requirements.yml" on every run. +roles: [] From a6f06ae1c4be291cbeab2ea9158e7d4a54d4dfe5 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 18:46:19 +0200 Subject: [PATCH 29/36] refactor(molecule): prefix VM and container names with lfops-molecule- --- extensions/molecule/inventory/group_vars/all.yml | 5 +++++ .../molecule/inventory/host_vars/debian11-container.yml | 2 +- .../molecule/inventory/host_vars/debian12-container.yml | 2 +- .../molecule/inventory/host_vars/debian13-container.yml | 2 +- .../molecule/inventory/host_vars/rocky10-container.yml | 2 +- .../molecule/inventory/host_vars/rocky8-container.yml | 2 +- .../molecule/inventory/host_vars/rocky9-container.yml | 2 +- .../molecule/inventory/host_vars/ubuntu2004-container.yml | 2 +- .../molecule/inventory/host_vars/ubuntu2204-container.yml | 2 +- .../molecule/inventory/host_vars/ubuntu2404-container.yml | 2 +- .../molecule/inventory/host_vars/ubuntu2604-container.yml | 2 +- extensions/molecule/playbooks/container-create.yml | 8 ++++---- extensions/molecule/playbooks/container-destroy.yml | 2 +- extensions/molecule/playbooks/vm-create.yml | 3 ++- extensions/molecule/playbooks/vm-destroy.yml | 1 + 15 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 extensions/molecule/inventory/group_vars/all.yml diff --git a/extensions/molecule/inventory/group_vars/all.yml b/extensions/molecule/inventory/group_vars/all.yml new file mode 100644 index 000000000..312e60d96 --- /dev/null +++ b/extensions/molecule/inventory/group_vars/all.yml @@ -0,0 +1,5 @@ +# Prefix host-visible resource names (libvirt domains, podman containers, and the guest hostname +# the kvm_vm role derives from kvm_vm__name) so Molecule's throwaway instances are easy to spot +# in `virsh list` / `podman ps` and never clash with anything else on the host. inventory_hostname +# stays the short name that Ansible itself uses to identify the host. +__molecule__instance_name: 'lfops-molecule-{{ inventory_hostname }}' diff --git a/extensions/molecule/inventory/host_vars/debian11-container.yml b/extensions/molecule/inventory/host_vars/debian11-container.yml index d981bf444..7ffbd690f 100644 --- a/extensions/molecule/inventory/host_vars/debian11-container.yml +++ b/extensions/molecule/inventory/host_vars/debian11-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'debian11-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/debian:11' diff --git a/extensions/molecule/inventory/host_vars/debian12-container.yml b/extensions/molecule/inventory/host_vars/debian12-container.yml index 364b53e2f..db02979af 100644 --- a/extensions/molecule/inventory/host_vars/debian12-container.yml +++ b/extensions/molecule/inventory/host_vars/debian12-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'debian12-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/debian:12' diff --git a/extensions/molecule/inventory/host_vars/debian13-container.yml b/extensions/molecule/inventory/host_vars/debian13-container.yml index b682c1ba3..c209eccdd 100644 --- a/extensions/molecule/inventory/host_vars/debian13-container.yml +++ b/extensions/molecule/inventory/host_vars/debian13-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'debian13-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/debian:13' diff --git a/extensions/molecule/inventory/host_vars/rocky10-container.yml b/extensions/molecule/inventory/host_vars/rocky10-container.yml index 489ff41a5..555bbd3f0 100644 --- a/extensions/molecule/inventory/host_vars/rocky10-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky10-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'rocky10-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_command: '/usr/sbin/init' molecule__container_image: 'docker.io/rockylinux/rockylinux:10-ubi-init' diff --git a/extensions/molecule/inventory/host_vars/rocky8-container.yml b/extensions/molecule/inventory/host_vars/rocky8-container.yml index 1bd38f258..fcebbdd6b 100644 --- a/extensions/molecule/inventory/host_vars/rocky8-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky8-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'rocky8-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_command: '/usr/sbin/init' molecule__container_image: 'docker.io/rockylinux/rockylinux:8-ubi-init' diff --git a/extensions/molecule/inventory/host_vars/rocky9-container.yml b/extensions/molecule/inventory/host_vars/rocky9-container.yml index e1a8a351e..b89ffec64 100644 --- a/extensions/molecule/inventory/host_vars/rocky9-container.yml +++ b/extensions/molecule/inventory/host_vars/rocky9-container.yml @@ -1,5 +1,5 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'rocky9-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_command: '/usr/sbin/init' molecule__container_image: 'docker.io/rockylinux/rockylinux:9-ubi-init' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml index 2725075e6..cfc575532 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2004-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2004-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/ubuntu:20.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml index 18fc8a873..ea7cf7f45 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2204-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2204-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/ubuntu:22.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml index 1d4a49097..583526665 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2404-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2404-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/ubuntu:24.04' diff --git a/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml b/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml index 17d914c03..3ddaadd84 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2604-container.yml @@ -1,4 +1,4 @@ ansible_connection: 'containers.podman.podman' -ansible_host: 'ubuntu2604-container' +ansible_host: '{{ __molecule__instance_name }}' molecule__container_image: 'docker.io/library/ubuntu:26.04' diff --git a/extensions/molecule/playbooks/container-create.yml b/extensions/molecule/playbooks/container-create.yml index 225ea4c22..0c13fe50f 100644 --- a/extensions/molecule/playbooks/container-create.yml +++ b/extensions/molecule/playbooks/container-create.yml @@ -10,10 +10,10 @@ containers.podman.podman_container: capabilities: '{{ molecule__container_capabilities | default(omit) }}' command: '{{ molecule__container_command | default("sleep 1d") }}' - hostname: '{{ inventory_hostname }}' + hostname: '{{ __molecule__instance_name }}' image: '{{ molecule__container_image }}' log_driver: '{{ molecule__container_log_driver | default("json-file") }}' - name: '{{ inventory_hostname }}' + name: '{{ __molecule__instance_name }}' privileged: '{{ molecule__container_privileged | default(false) }}' state: 'started' systemd: '{{ molecule__container_systemd | default(false) }}' @@ -35,7 +35,7 @@ - name: 'Retrieve the container log' ansible.builtin.command: - cmd: 'podman logs {{ inventory_hostname }}' + cmd: 'podman logs {{ __molecule__instance_name }}' changed_when: false register: '__molecule__container_log_result' delegate_to: 'localhost' @@ -43,5 +43,5 @@ - name: 'Fail with the container log' ansible.builtin.fail: msg: | - Container {{ inventory_hostname }} failed to start properly. + Container {{ __molecule__instance_name }} failed to start properly. Log output: {{ __molecule__container_log_result["stdout"] | d("No logs available") }} diff --git a/extensions/molecule/playbooks/container-destroy.yml b/extensions/molecule/playbooks/container-destroy.yml index 6d7a1df97..fef6a3ae5 100644 --- a/extensions/molecule/playbooks/container-destroy.yml +++ b/extensions/molecule/playbooks/container-destroy.yml @@ -5,7 +5,7 @@ - name: 'Kill container if running' containers.podman.podman_container: - name: '{{ inventory_hostname }}' + name: '{{ __molecule__instance_name }}' state: 'absent' timeout: 2 delegate_to: 'localhost' diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml index fd9d4da93..db2bb606a 100644 --- a/extensions/molecule/playbooks/vm-create.yml +++ b/extensions/molecule/playbooks/vm-create.yml @@ -36,6 +36,7 @@ become: true gather_facts: false vars: + kvm_vm__name: '{{ __molecule__instance_name }}' kvm_vm__ssh_authorized_keys: - '{{ hostvars["localhost"]["__molecule__keypair_result"]["public_key"] }}' kvm_vm__state: 'running' @@ -66,7 +67,7 @@ - name: 'Wait for the VM to report a non-loopback IPv4 address via the guest agent' ansible.builtin.command: - cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ inventory_hostname }} --source agent' + cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ __molecule__instance_name }} --source agent' register: '__molecule__vm_ipaddr_result' until: >- __molecule__vm_ipaddr_result['stdout_lines'] diff --git a/extensions/molecule/playbooks/vm-destroy.yml b/extensions/molecule/playbooks/vm-destroy.yml index 367ed4d19..fbe762337 100644 --- a/extensions/molecule/playbooks/vm-destroy.yml +++ b/extensions/molecule/playbooks/vm-destroy.yml @@ -5,6 +5,7 @@ roles: - role: 'linuxfabrik.lfops.kvm_vm' vars: + kvm_vm__name: '{{ __molecule__instance_name }}' kvm_vm__state: 'absent' From 63f2c2d87b3da0a5170ad58b2598ed4c195521cb Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 18:51:30 +0200 Subject: [PATCH 30/36] refactor(molecule): auto-include localhost in the test target limit --- CONTRIBUTING.md | 8 ++++---- extensions/molecule/config.yml | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee5447d57..12c83cc55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -751,13 +751,13 @@ extensions The `extensions/molecule/example` scenario mirrors the `example` role: it is a fully commented, non-functional reference that walks through every file of a scenario (a non-functional reference because the `example` playbook installs the fictional "Example" application). Copy it as the starting point when adding a test for a playbook. -Tests can be run against a subset of targets by providing them as a comma-separated list via the project-specific `LFOPS_TEST_TARGETS` environment variable: +Tests can be run against a subset of targets by providing them as a comma-separated list via the project-specific `LFOPS_TEST_TARGETS` environment variable. The variable is optional: unset, every target in the scenario runs. `localhost` (the hypervisor) is included automatically, so you only ever pass the targets themselves: ```shell -# for VMs (the hypervisor host needs to be included as well; here `localhost`) -LFOPS_TEST_TARGETS='localhost,rocky*' molecule test --scenario-name apps/install +# all targets in the scenario +molecule test --scenario-name apps/install -# for containers +# a subset LFOPS_TEST_TARGETS='rocky*' molecule test --scenario-name apps/install ``` diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 55cf01a34..6d1b00188 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -37,15 +37,18 @@ ansible: ssh_args: '-o ControlMaster=auto -o ControlPersist=60s' executor: backend: 'ansible-playbook' # or 'ansible-navigator' + # The limit always includes localhost (vm-create.yml's controller-setup play targets it) and + # defaults LFOPS_TEST_TARGETS to 'all', so the variable is optional: unset runs every target, + # and when set you only pass the targets (e.g. 'rocky*'), never localhost. args: ansible_navigator: - '--inventory=${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/inventory/' - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' - - '--limit=${LFOPS_TEST_TARGETS}' + - '--limit=localhost,${LFOPS_TEST_TARGETS:-all}' ansible_playbook: - '--inventory=${MOLECULE_PROJECT_DIRECTORY}/extensions/molecule/inventory/' - '--inventory=${MOLECULE_SCENARIO_DIRECTORY}/inventory/' - - '--limit=${LFOPS_TEST_TARGETS}' + - '--limit=localhost,${LFOPS_TEST_TARGETS:-all}' # The 'galaxy' dependency below installs the collections listed in requirements.yml via # `ansible-galaxy collection install`. Molecule only runs the 'dependency' stage when it is part From 55204e9fba692fc49f0532a5fb231dbb3f45a7bd Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 5 Jun 2026 19:00:56 +0200 Subject: [PATCH 31/36] fix(molecule): discover VM IPs via DHCP lease instead of the guest agent --- extensions/molecule/playbooks/vm-create.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml index db2bb606a..23a240885 100644 --- a/extensions/molecule/playbooks/vm-create.yml +++ b/extensions/molecule/playbooks/vm-create.yml @@ -54,7 +54,7 @@ - role: 'linuxfabrik.lfops.kvm_vm' -# Discover each VM's address via the qemu guest agent and write it into Molecule's ephemeral +# Discover each VM's address via its libvirt DHCP lease and write it into Molecule's ephemeral # inventory directory. Molecule includes that directory automatically, so converge and verify # reach the VMs over SSH. See the inventory-model note in config.yml for why this is hand-written # instead of using Molecule's instance_config. @@ -65,17 +65,22 @@ __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - - name: 'Wait for the VM to report a non-loopback IPv4 address via the guest agent' + # Use --source lease (libvirt reads its own DHCP lease table on the host), not --source agent: + # the agent needs qemu-guest-agent running inside the guest, which only the Rocky cloud image + # ships preinstalled - the VMs install no extra packages (kvm_vm__packages is []), so on + # Debian/Ubuntu the agent never connects. --source lease works for every distro because they + # all get their address from the libvirt-managed 'default' network's DHCP. + - name: 'Wait for the VM to report a non-loopback IPv4 address via its DHCP lease' ansible.builtin.command: - cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ __molecule__instance_name }} --source agent' + cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ __molecule__instance_name }} --source lease' register: '__molecule__vm_ipaddr_result' until: >- __molecule__vm_ipaddr_result['stdout_lines'] | select('ansible.builtin.search', 'ipv4') | reject('ansible.builtin.search', '^\s*lo\s') | list | length > 0 - retries: 30 - delay: 5 + retries: 15 + delay: 2 changed_when: false delegate_to: 'localhost' From 3ba63dd3c5d38bfa67668c0aee011e9a1abddde9 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 11:48:04 +0200 Subject: [PATCH 32/36] docs(contributing): add explanation to molecule limitations --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b00332682..fbcbb0ff3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -948,8 +948,8 @@ LFOPS_TEST_TARGETS='rocky*' molecule test --scenario-name apps/install Known Limitations: -* VM-based testing currently requires passwordless sudo on the Ansible controller. -* Ansible Navigator does not work out of the box. +* VM-based testing requires passwordless sudo on the Ansible controller. The cloud image and per-VM disks are written and built directly in the root-owned libvirt pool directory (`get_url`, `qemu-img`, `virt-customize`), which is plain filesystem I/O and needs root. The read-only libvirt API calls already run unprivileged via the `libvirt` group; it is only the pool writes that require sudo. Trying to make the whole run rootless is not worth it: the only way to provision VMs without root-equivalent rights at all is the user session (`qemu:///session`), which the test cannot use because its DHCP-lease address discovery needs the libvirt-managed `default` network that only the system connection (`qemu:///system`) provides. Every other route still grants effective root: a member of the `libvirt` group (which the read-only calls already require) can define a domain backed by any host device and drive QEMU as root. Swapping the `sudo` for a user-owned `qemu:///system` pool therefore only trades an explicit, on-demand escalation for a standing root-equivalent privilege plus looser filesystem permissions, which is a worse posture, not a better one. +* Does not work inside an Ansible Execution Environment (Ansible Navigator). Provisioning runs as `localhost`, which inside an EE is the container, yet it has to act on the host's libvirt and podman. The disk-build tools (`qemu-img`, `virt-customize`, `virt-sysprep`) are filesystem-bound to the pool and have no libvirt-socket equivalent, so an EE would have to bind-mount the host libvirt/podman sockets and the pool directory and use host networking, which removes most of the isolation an EE exists to provide. #### How a scenario runs From a3549da0e14c1e583e08e34f7f0fc195a0a96891 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 15:38:02 +0200 Subject: [PATCH 33/36] fix(molecule): make the VM-based test path work end to end - discover VM IPs from the host ARP table (--source arp) instead of the DHCP lease table: a stable MAC across recreate cycles accumulates several unexpired leases, so --source lease returned stale addresses - raise the cloud-image download timeout to 120s; the 10s default times out when one play pulls all images at once over shared bandwidth - refresh the apt cache on Debian/Ubuntu during prepare; the cloud images ship an empty index, so a bare package install could not resolve anything - set a known root password on the throwaway VMs for console debugging --- CONTRIBUTING.md | 2 +- extensions/molecule/playbooks/vm-create.yml | 33 +++++++++++++++----- extensions/molecule/playbooks/vm-prepare.yml | 10 ++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbcbb0ff3..ae117b2ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -948,7 +948,7 @@ LFOPS_TEST_TARGETS='rocky*' molecule test --scenario-name apps/install Known Limitations: -* VM-based testing requires passwordless sudo on the Ansible controller. The cloud image and per-VM disks are written and built directly in the root-owned libvirt pool directory (`get_url`, `qemu-img`, `virt-customize`), which is plain filesystem I/O and needs root. The read-only libvirt API calls already run unprivileged via the `libvirt` group; it is only the pool writes that require sudo. Trying to make the whole run rootless is not worth it: the only way to provision VMs without root-equivalent rights at all is the user session (`qemu:///session`), which the test cannot use because its DHCP-lease address discovery needs the libvirt-managed `default` network that only the system connection (`qemu:///system`) provides. Every other route still grants effective root: a member of the `libvirt` group (which the read-only calls already require) can define a domain backed by any host device and drive QEMU as root. Swapping the `sudo` for a user-owned `qemu:///system` pool therefore only trades an explicit, on-demand escalation for a standing root-equivalent privilege plus looser filesystem permissions, which is a worse posture, not a better one. +* VM-based testing requires passwordless sudo on the Ansible controller. The cloud image and per-VM disks are written and built directly in the root-owned libvirt pool directory (`get_url`, `qemu-img`, `virt-customize`), which is plain filesystem I/O and needs root. The read-only libvirt API calls already run unprivileged via the `libvirt` group; it is only the pool writes that require sudo. Trying to make the whole run rootless is not worth it: the only way to provision VMs without root-equivalent rights at all is the user session (`qemu:///session`), which the test cannot use because its address discovery reads the host's ARP/neighbour table for the libvirt-managed `default` network that only the system connection (`qemu:///system`) provides. Every other route still grants effective root: a member of the `libvirt` group (which the read-only calls already require) can define a domain backed by any host device and drive QEMU as root. Swapping the `sudo` for a user-owned `qemu:///system` pool therefore only trades an explicit, on-demand escalation for a standing root-equivalent privilege plus looser filesystem permissions, which is a worse posture, not a better one. * Does not work inside an Ansible Execution Environment (Ansible Navigator). Provisioning runs as `localhost`, which inside an EE is the container, yet it has to act on the host's libvirt and podman. The disk-build tools (`qemu-img`, `virt-customize`, `virt-sysprep`) are filesystem-bound to the pool and have no libvirt-socket equivalent, so an EE would have to bind-mount the host libvirt/podman sockets and the pool directory and use host networking, which removes most of the isolation an EE exists to provide. diff --git a/extensions/molecule/playbooks/vm-create.yml b/extensions/molecule/playbooks/vm-create.yml index 23a240885..999a232c4 100644 --- a/extensions/molecule/playbooks/vm-create.yml +++ b/extensions/molecule/playbooks/vm-create.yml @@ -37,16 +37,24 @@ gather_facts: false vars: kvm_vm__name: '{{ __molecule__instance_name }}' + # Throwaway test VMs: set a known root password for console login during debugging. SSH still + # uses the ephemeral keypair below. + kvm_vm__root_password: 'linuxfabrik' kvm_vm__ssh_authorized_keys: - '{{ hostvars["localhost"]["__molecule__keypair_result"]["public_key"] }}' kvm_vm__state: 'running' pre_tasks: + # Raise timeout well above the 10s default: the cloud images are several hundred MB and one + # play downloads all of them at once (forks), so individual connections stall past 10s under + # the shared bandwidth and hit "read operation timed out". get_url defaults to force: false, so + # an image already present in the pool is left untouched and only a missing one is fetched. - name: 'Download the cloud image into the storage pool' ansible.builtin.get_url: url: '{{ molecule__vm_image_url }}' dest: '{{ hostvars["localhost"]["__molecule__pool_result"]["pools"][kvm_vm__pool | d("default")]["path"] }}/{{ kvm_vm__base_image }}' mode: 0o644 + timeout: 120 delegate_to: 'localhost' when: 'molecule__vm_image_url is defined' @@ -65,14 +73,23 @@ __molecule__ephemeral_dir: '{{ lookup("env", "MOLECULE_EPHEMERAL_DIRECTORY") }}' tasks: - # Use --source lease (libvirt reads its own DHCP lease table on the host), not --source agent: - # the agent needs qemu-guest-agent running inside the guest, which only the Rocky cloud image - # ships preinstalled - the VMs install no extra packages (kvm_vm__packages is []), so on - # Debian/Ubuntu the agent never connects. --source lease works for every distro because they - # all get their address from the libvirt-managed 'default' network's DHCP. - - name: 'Wait for the VM to report a non-loopback IPv4 address via its DHCP lease' + # Use --source arp: libvirt reads the host's neighbour cache (ip neigh on the bridge), so it + # only ever reports addresses currently live on the network. We rejected the two alternatives: + # --source lease reads libvirt's DHCP lease table, which keeps every still-unexpired lease. + # The VMs reuse a stable MAC across destroy/create cycles but regenerate their machine-id + # (hence DHCP client-id) on each rebuilt boot disk, so dnsmasq hands out a fresh IP every + # time and the old leases linger. domifaddr then returns several IPs for one MAC and there + # is no way to tell the live one from the stale ones without sorting by lease expiry. + # --source agent needs qemu-guest-agent running inside the guest, which only the Rocky cloud + # image ships preinstalled - the VMs install no extra packages (kvm_vm__packages is []), so + # on Debian/Ubuntu the agent never connects. + # --source arp sidesteps both: the neighbour cache holds only what is reachable now, so stale + # leases never appear, and it needs nothing inside the guest. The entry is populated once the + # VM emits traffic (its DHCP exchange does this within seconds), so the retry loop below covers + # the short boot window where it is not yet there. + - name: 'Wait for the VM to report a non-loopback IPv4 address via the host ARP table' ansible.builtin.command: - cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ __molecule__instance_name }} --source lease' + cmd: 'virsh --connect {{ kvm_vm__connect_url | d("qemu:///system") }} domifaddr {{ __molecule__instance_name }} --source arp' register: '__molecule__vm_ipaddr_result' until: >- __molecule__vm_ipaddr_result['stdout_lines'] @@ -109,4 +126,4 @@ dest: '{{ __molecule__ephemeral_dir }}/inventory/molecule_vm.yml' mode: 0o644 delegate_to: 'localhost' - run_once: true + run_once: true # single write that loops over ansible_play_hosts to emit every host at once diff --git a/extensions/molecule/playbooks/vm-prepare.yml b/extensions/molecule/playbooks/vm-prepare.yml index aec0cdaa6..d591dbad2 100644 --- a/extensions/molecule/playbooks/vm-prepare.yml +++ b/extensions/molecule/playbooks/vm-prepare.yml @@ -8,3 +8,13 @@ - name: 'Gather facts' ansible.builtin.setup: # yamllint disable-line rule:empty-values + + # Debian/Ubuntu genericcloud images ship with an empty apt index (/var/lib/apt/lists is cleaned + # at image-build time), so the first package install cannot resolve any package until the cache + # is refreshed. Scenarios that pull in a repo_* role get this for free; ones that install a bare + # package (e.g. apps) would otherwise fail with "No package matching '' is available". + - name: 'apt-get update' + ansible.builtin.apt: + update_cache: true + when: + - 'ansible_facts["os_family"] == "Debian"' From fd1b7b15979ad21151b5a1eba2f46cd1d6415620 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 15:38:15 +0200 Subject: [PATCH 34/36] test(molecule): add the ubuntu 26.04 VM target to the standard set Use the ubuntu-lts-latest osinfo alias since libosinfo has no dedicated 26.04 entry yet; it only seeds hardware defaults and tracks the latest LTS. --- extensions/molecule/apps/install/inventory/hosts.yml | 1 + extensions/molecule/apps/remove/inventory/hosts.yml | 1 + extensions/molecule/example/install/inventory/hosts.yml | 1 + extensions/molecule/example/remove/inventory/hosts.yml | 1 + extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml | 5 ++++- extensions/molecule/monitoring_plugins/inventory/hosts.yml | 1 + extensions/molecule/setup_basic/inventory/hosts.yml | 1 + 7 files changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/molecule/apps/install/inventory/hosts.yml b/extensions/molecule/apps/install/inventory/hosts.yml index b417f539d..129b4f61b 100644 --- a/extensions/molecule/apps/install/inventory/hosts.yml +++ b/extensions/molecule/apps/install/inventory/hosts.yml @@ -14,3 +14,4 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: + ubuntu2604-vm: diff --git a/extensions/molecule/apps/remove/inventory/hosts.yml b/extensions/molecule/apps/remove/inventory/hosts.yml index b417f539d..129b4f61b 100644 --- a/extensions/molecule/apps/remove/inventory/hosts.yml +++ b/extensions/molecule/apps/remove/inventory/hosts.yml @@ -14,3 +14,4 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: + ubuntu2604-vm: diff --git a/extensions/molecule/example/install/inventory/hosts.yml b/extensions/molecule/example/install/inventory/hosts.yml index eeb915364..ca03a844b 100644 --- a/extensions/molecule/example/install/inventory/hosts.yml +++ b/extensions/molecule/example/install/inventory/hosts.yml @@ -25,3 +25,4 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: + ubuntu2604-vm: diff --git a/extensions/molecule/example/remove/inventory/hosts.yml b/extensions/molecule/example/remove/inventory/hosts.yml index 6c133c8be..47abd4b32 100644 --- a/extensions/molecule/example/remove/inventory/hosts.yml +++ b/extensions/molecule/example/remove/inventory/hosts.yml @@ -17,3 +17,4 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: + ubuntu2604-vm: diff --git a/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml b/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml index 617d0b58f..20b484587 100644 --- a/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml +++ b/extensions/molecule/inventory/host_vars/ubuntu2604-vm.yml @@ -9,7 +9,10 @@ kvm_vm__network_connections: - name: 'enp1s0' network_name: 'default' dhcp4: true -kvm_vm__osinfo: 'ubuntu26.04' +# libosinfo does not ship a dedicated 'ubuntu26.04' entry yet, so use the LTS-latest alias. It +# only seeds hypervisor hardware defaults (identical for recent Ubuntu) and starts resolving to +# 26.04 once libosinfo learns it. +kvm_vm__osinfo: 'ubuntu-lts-latest' kvm_vm__packages: [] kvm_vm__vcpus: 2 diff --git a/extensions/molecule/monitoring_plugins/inventory/hosts.yml b/extensions/molecule/monitoring_plugins/inventory/hosts.yml index b21a51c63..8b8a4e59b 100644 --- a/extensions/molecule/monitoring_plugins/inventory/hosts.yml +++ b/extensions/molecule/monitoring_plugins/inventory/hosts.yml @@ -18,3 +18,4 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: + ubuntu2604-vm: diff --git a/extensions/molecule/setup_basic/inventory/hosts.yml b/extensions/molecule/setup_basic/inventory/hosts.yml index 94bf903aa..eb0eda385 100644 --- a/extensions/molecule/setup_basic/inventory/hosts.yml +++ b/extensions/molecule/setup_basic/inventory/hosts.yml @@ -14,3 +14,4 @@ systems_under_test: ubuntu2004-vm: ubuntu2204-vm: ubuntu2404-vm: + ubuntu2604-vm: From 3bc252cc50ee4b6bd2859cf19be5737a08ae56c4 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 15:38:22 +0200 Subject: [PATCH 35/36] refactor(molecule): review cleanups - drop obsolete accelerate_timeout from config.yml (accelerate plugin was removed from Ansible) - apply | bool to the container privileged/systemd params - fix the README reference to a non-existent container-create-fail.yml --- extensions/molecule/config.yml | 1 - extensions/molecule/playbooks/README.md | 2 +- extensions/molecule/playbooks/container-create.yml | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/molecule/config.yml b/extensions/molecule/config.yml index 6d1b00188..946379768 100644 --- a/extensions/molecule/config.yml +++ b/extensions/molecule/config.yml @@ -5,7 +5,6 @@ ansible: cfg: defaults: - accelerate_timeout: 5 ansible_managed: 'This file is managed by Ansible - do not edit' callbacks_enabled: 'profile_tasks' fact_caching: 'jsonfile' diff --git a/extensions/molecule/playbooks/README.md b/extensions/molecule/playbooks/README.md index 39b1bf955..d3f35f1bd 100644 --- a/extensions/molecule/playbooks/README.md +++ b/extensions/molecule/playbooks/README.md @@ -46,7 +46,7 @@ Depending on the backend a scenario uses: `container-create.yml` -* Starts one Podman container per host. Fails early and prints the container log when a container does not come up (via `tasks/container-create-fail.yml`). +* Starts one Podman container per host. Fails early and prints the container log when a container does not come up. `container-prepare.yml` diff --git a/extensions/molecule/playbooks/container-create.yml b/extensions/molecule/playbooks/container-create.yml index 0c13fe50f..b90c89ad3 100644 --- a/extensions/molecule/playbooks/container-create.yml +++ b/extensions/molecule/playbooks/container-create.yml @@ -14,9 +14,9 @@ image: '{{ molecule__container_image }}' log_driver: '{{ molecule__container_log_driver | default("json-file") }}' name: '{{ __molecule__instance_name }}' - privileged: '{{ molecule__container_privileged | default(false) }}' + privileged: '{{ molecule__container_privileged | default(false) | bool }}' state: 'started' - systemd: '{{ molecule__container_systemd | default(false) }}' + systemd: '{{ molecule__container_systemd | default(false) | bool }}' volumes: '{{ molecule__container_volumes | default(omit) }}' register: '__molecule__container_result' delegate_to: 'localhost' From 028ce3c3212c34339427fcc981db108660b6aa60 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 16:04:20 +0200 Subject: [PATCH 36/36] docs(CHANGELOG): note the Molecule test framework --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abc08a9ed..83beaa1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **testing**: Add a Molecule-based test framework that runs the playbooks (and through them the roles) against throwaway libvirt/KVM VMs or Podman containers. Scenarios live under `extensions/molecule`; see the Testing section in `CONTRIBUTING.md`. * **role:tmux**: Installs tmux and deploys a system-wide `/etc/tmux.conf` with sensible defaults, such as a larger scrollback buffer and mouse support. Selections are copied to the local clipboard over SSH via OSC 52 (where the terminal emulator supports it), and `prefix + P` dumps a pane's whole scrollback buffer to a file. * **role:graylog_server**: Make more HTTP, Elasticsearch, processing/output buffer and message journal settings configurable via `graylog_server__http_external_uri`, `graylog_server__http_enable_cors`, `graylog_server__elasticsearch_max_total_connections`, `graylog_server__elasticsearch_max_total_connections_per_route`, `graylog_server__output_batch_size`, `graylog_server__processbuffer_processors`, `graylog_server__outputbuffer_processors`, `graylog_server__ring_size`, `graylog_server__inputbuffer_ring_size`, `graylog_server__message_journal_max_age` and `graylog_server__message_journal_max_size`. * **role:mariadb_server**: Make `aria_pagecache_buffer_size`, `key_buffer_size` and `sort_buffer_size` configurable via the corresponding `mariadb_server__cnf_*` variables.