From 1f3856a1122674e7239a79c9e2543618beac3405 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Wed, 3 Jun 2026 11:12:19 +0200 Subject: [PATCH 01/12] feat(prepare): exclude loop and virtual devices from host LVM scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set an LVM global_filter in /etc/lvm/lvm.conf in the prepare playbooks so the host LVM does not scan or activate volume groups backed by LINSTOR/DRBD volumes — or located inside loop-mounted images. Filters /dev/drbd*, /dev/dm-*, /dev/zd* and /dev/loop*, mirroring the Talos machine config. Applied to the ubuntu, rhel and suse prepare playbooks via lineinfile so the distro's default lvm.conf is preserved. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- CHANGELOG.rst | 6 ++++++ examples/rhel/prepare-rhel.yml | 12 ++++++++++++ examples/suse/prepare-suse.yml | 12 ++++++++++++ examples/ubuntu/prepare-ubuntu.yml | 12 ++++++++++++ 4 files changed, 42 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a65571a..d730904 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,12 @@ Unreleased IP addresses for ingress-nginx Service ``externalIPs``. Required on ``isp-full-generic`` platform variant when nodes lack a native load balancer (cloud VMs, bare metal). +- Prepare playbooks now set an LVM ``global_filter`` in + ``/etc/lvm/lvm.conf`` excluding ``/dev/drbd*``, ``/dev/dm-*``, + ``/dev/zd*`` and ``/dev/loop*`` so the host LVM does not scan or + activate volume groups backed by LINSTOR/DRBD volumes or located + inside loop-mounted images. Mirrors the global_filter shipped in the + Talos machine config. Unreleased ========== diff --git a/examples/rhel/prepare-rhel.yml b/examples/rhel/prepare-rhel.yml index 479d48f..e5b6096 100644 --- a/examples/rhel/prepare-rhel.yml +++ b/examples/rhel/prepare-rhel.yml @@ -281,6 +281,18 @@ } notify: Restart multipathd + # Exclude DRBD, device-mapper, zvol and loop devices from the host's + # LVM device scanning. Without this the host LVM may scan and activate + # volume groups backed by LINSTOR/DRBD volumes — or located inside + # loop-mounted images — making them unavailable to the satellite. + # Mirrors the global_filter shipped in the Talos machine config. + - name: Exclude virtual and loop devices from host LVM scanning + ansible.builtin.lineinfile: + path: /etc/lvm/lvm.conf + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^devices {' + line: ' global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]' + - name: Configure sysctl parameters ansible.posix.sysctl: name: "{{ item.name }}" diff --git a/examples/suse/prepare-suse.yml b/examples/suse/prepare-suse.yml index 9d91722..a3bf1be 100644 --- a/examples/suse/prepare-suse.yml +++ b/examples/suse/prepare-suse.yml @@ -276,6 +276,18 @@ } notify: Restart multipathd + # Exclude DRBD, device-mapper, zvol and loop devices from the host's + # LVM device scanning. Without this the host LVM may scan and activate + # volume groups backed by LINSTOR/DRBD volumes — or located inside + # loop-mounted images — making them unavailable to the satellite. + # Mirrors the global_filter shipped in the Talos machine config. + - name: Exclude virtual and loop devices from host LVM scanning + ansible.builtin.lineinfile: + path: /etc/lvm/lvm.conf + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^devices {' + line: ' global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]' + - name: Configure sysctl parameters ansible.posix.sysctl: name: "{{ item.name }}" diff --git a/examples/ubuntu/prepare-ubuntu.yml b/examples/ubuntu/prepare-ubuntu.yml index cb803db..44ee475 100644 --- a/examples/ubuntu/prepare-ubuntu.yml +++ b/examples/ubuntu/prepare-ubuntu.yml @@ -322,6 +322,18 @@ } notify: Restart multipathd + # Exclude DRBD, device-mapper, zvol and loop devices from the host's + # LVM device scanning. Without this the host LVM may scan and activate + # volume groups backed by LINSTOR/DRBD volumes — or located inside + # loop-mounted images — making them unavailable to the satellite. + # Mirrors the global_filter shipped in the Talos machine config. + - name: Exclude virtual and loop devices from host LVM scanning + ansible.builtin.lineinfile: + path: /etc/lvm/lvm.conf + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^devices {' + line: ' global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]' + - name: Configure sysctl parameters ansible.posix.sysctl: name: "{{ item.name }}" From 0657c427a7eefcd362d8ea1f1e2cc81498d73aec Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 12:55:00 +0300 Subject: [PATCH 02/12] feat(prepare): make host LVM global_filter configurable and robust Expose the host LVM global_filter as cozystack_lvm_global_filter so operators whose own PVs live on device-mapper devices (LVM-on-LUKS, multipath) can override the default instead of having those PVs filtered out. The default list is unchanged. Relax the insertafter anchor to '^\s*devices\s*{' so the setting still lands inside the devices{} block when lvm.conf uses leading whitespace or compact bracing; with the stricter '^devices {' a non-match made lineinfile append global_filter at EOF, where LVM ignores it. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- examples/rhel/prepare-rhel.yml | 9 +++++++-- examples/suse/prepare-suse.yml | 9 +++++++-- examples/ubuntu/prepare-ubuntu.yml | 9 +++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/examples/rhel/prepare-rhel.yml b/examples/rhel/prepare-rhel.yml index e5b6096..f71ef58 100644 --- a/examples/rhel/prepare-rhel.yml +++ b/examples/rhel/prepare-rhel.yml @@ -286,12 +286,17 @@ # volume groups backed by LINSTOR/DRBD volumes — or located inside # loop-mounted images — making them unavailable to the satellite. # Mirrors the global_filter shipped in the Talos machine config. + # Override cozystack_lvm_global_filter from inventory on hosts whose + # own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so + # they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry. - name: Exclude virtual and loop devices from host LVM scanning ansible.builtin.lineinfile: path: /etc/lvm/lvm.conf regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^devices {' - line: ' global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]' + insertafter: '^\s*devices\s*{' + line: ' global_filter = {{ cozystack_lvm_global_filter | + default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", + "r|^/dev/loop.*|"]) | to_json }}' - name: Configure sysctl parameters ansible.posix.sysctl: diff --git a/examples/suse/prepare-suse.yml b/examples/suse/prepare-suse.yml index a3bf1be..033b983 100644 --- a/examples/suse/prepare-suse.yml +++ b/examples/suse/prepare-suse.yml @@ -281,12 +281,17 @@ # volume groups backed by LINSTOR/DRBD volumes — or located inside # loop-mounted images — making them unavailable to the satellite. # Mirrors the global_filter shipped in the Talos machine config. + # Override cozystack_lvm_global_filter from inventory on hosts whose + # own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so + # they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry. - name: Exclude virtual and loop devices from host LVM scanning ansible.builtin.lineinfile: path: /etc/lvm/lvm.conf regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^devices {' - line: ' global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]' + insertafter: '^\s*devices\s*{' + line: ' global_filter = {{ cozystack_lvm_global_filter | + default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", + "r|^/dev/loop.*|"]) | to_json }}' - name: Configure sysctl parameters ansible.posix.sysctl: diff --git a/examples/ubuntu/prepare-ubuntu.yml b/examples/ubuntu/prepare-ubuntu.yml index 44ee475..e6bbba1 100644 --- a/examples/ubuntu/prepare-ubuntu.yml +++ b/examples/ubuntu/prepare-ubuntu.yml @@ -327,12 +327,17 @@ # volume groups backed by LINSTOR/DRBD volumes — or located inside # loop-mounted images — making them unavailable to the satellite. # Mirrors the global_filter shipped in the Talos machine config. + # Override cozystack_lvm_global_filter from inventory on hosts whose + # own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so + # they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry. - name: Exclude virtual and loop devices from host LVM scanning ansible.builtin.lineinfile: path: /etc/lvm/lvm.conf regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^devices {' - line: ' global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]' + insertafter: '^\s*devices\s*{' + line: ' global_filter = {{ cozystack_lvm_global_filter | + default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", + "r|^/dev/loop.*|"]) | to_json }}' - name: Configure sysctl parameters ansible.posix.sysctl: From 61a60677e369c2bd8504e116d70cc7ed735e7b0e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 13:01:57 +0300 Subject: [PATCH 03/12] docs(prepare): document host LVM global_filter and override variable Add a 'Required: Host LVM global_filter' section next to the multipath DRBD blacklist describing the silent-failure trap it prevents, a row for cozystack_lvm_global_filter in the example-playbook variables table, and a CHANGELOG note that the filter is overridable from inventory. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- CHANGELOG.rst | 4 +++- README.md | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d730904..fe87a80 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,7 +23,9 @@ Unreleased ``/dev/zd*`` and ``/dev/loop*`` so the host LVM does not scan or activate volume groups backed by LINSTOR/DRBD volumes or located inside loop-mounted images. Mirrors the global_filter shipped in the - Talos machine config. + Talos machine config. The filter is overridable from inventory via + ``cozystack_lvm_global_filter`` for hosts whose own PVs sit on + device-mapper devices (LVM-on-LUKS, multipath). Unreleased ========== diff --git a/README.md b/README.md index d3449d4..1f5b80f 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,18 @@ blacklist { } ``` +#### Required: Host LVM global_filter + +> **Silent failure if omitted.** Without a `global_filter` the host's LVM can scan and auto-activate volume groups that live on DRBD/LINSTOR volumes, ZFS zvols, device-mapper targets, or loop-mounted images. Once the host activates a VG backed by a LINSTOR volume the Piraeus satellite can no longer manage it, and stacked volumes become unavailable after the next reboot. + +The prepare playbooks set a `global_filter` in `/etc/lvm/lvm.conf` that rejects virtual and loop devices, mirroring the filter shipped in the Talos machine config: + +```text +global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ] +``` + +The list is exposed as `cozystack_lvm_global_filter` (see [Example playbook variables](#example-playbook-variables)). Override it from inventory on hosts whose own physical volumes sit on device-mapper devices — LVM-on-LUKS or LVM-on-multipath, where the PVs are `/dev/dm-*` — so they are not filtered out. Dedicated storage nodes use the default unchanged. + #### Required: Containerd + Kubernetes kernel modules Required for containerd's overlay storage driver and standard Kubernetes bridge networking. Loaded via `/etc/modules-load.d/cozystack.conf`: @@ -362,6 +374,7 @@ These variables are consumed only by the example prepare playbooks in `examples/ | `cozystack_enable_kubevirt` | `true` | Example playbooks: load KubeVirt kernel modules **and** install the containerd `device_ownership_from_security_context` drop-in for CDI block imports. Set `false` to skip both. | | `cozystack_k3s_containerd_dropin_dir` | `/var/lib/rancher/k3s/agent/etc/containerd/config-v3.toml.d` | Example playbooks: directory for the containerd CRI drop-in (gated on `cozystack_enable_kubevirt`). Only relocates the file — the drop-in content is hardcoded for containerd 2.x (config v3); a containerd 1.x cluster needs a hand-written `config.toml.d` drop-in instead. | | `cozystack_flush_iptables` | `false` | Example playbooks: flush the iptables INPUT chain before k3s installs. Set `true` on Ubuntu/Debian cloud images (OCI/AWS/GCP) where the default INPUT chain ends with `REJECT icmp-host-prohibited` and blocks k3s inter-node ports 2380/6443. | +| `cozystack_lvm_global_filter` | `["r\|^/dev/drbd.*\|", "r\|^/dev/dm-.*\|", "r\|^/dev/zd.*\|", "r\|^/dev/loop.*\|"]` | Example playbooks: LVM `global_filter` written into `/etc/lvm/lvm.conf` so the host does not scan or activate volume groups backed by DRBD/LINSTOR volumes, ZFS zvols, device-mapper targets, or loop images. Override on hosts whose own PVs live on device-mapper devices (LVM-on-LUKS, multipath) — e.g. drop the `r\|^/dev/dm-.*\|` entry. | | `cozystack_zfs_release_rpm_extra` | `{}` | `examples/rhel/` only: merged on top of the built-in `cozystack_zfs_release_rpm_by_major` dict, so you can add (or override) a single EL-major → OpenZFS release RPM entry from inventory without wiping the base dict. Example: `{"10": "https://zfsonlinux.org/epel/zfs-release-X-Y.el10.noarch.rpm"}` once upstream ships one. | | `cozystack_enable_drbd_dkms` | `true` | `examples/ubuntu/` only: install `drbd-dkms` from the LINBIT PPA on Ubuntu LTS 22.04 / 24.04 hosts so DRBD's kernel module is signed via dkms+shim under Secure Boot. Set `false` on Talos hosts (Talos ships pre-signed DRBD modules in extensions) or where Secure Boot is disabled and the in-cluster compile path is preferred. The toggle stops *future* installs but does NOT undo a prior install — manually `apt purge drbd-dkms` and remove the LINBIT entry from `/etc/apt/sources.list.d/` if you flipped to `false` after a successful run. | | `cozystack_drbd_ppa` | `ppa:linbit/linbit-drbd9-stack` | `examples/ubuntu/` only: override to point at a Launchpad PPA mirror of the LINBIT archive. `ansible.builtin.apt_repository` resolves the signing key for `ppa:` URIs by querying Launchpad's REST API directly (no extra packages required). Non-Launchpad URIs (`deb http://internal-mirror/...`) work but you must manage the apt signing key separately — drop a keyring under `/etc/apt/keyrings/` and add `signed-by=` to the repo line. | From f3964eda65ad2ac6005853788dd43bea7d3f3823 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 13:19:39 +0300 Subject: [PATCH 04/12] fix(changelog): collapse duplicate Unreleased section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Unreleased entries had drifted into two headers — one without an RST underline (rendering as body text) and a second underlined block with a Bugfixes subsection. Merge them into a single underlined Unreleased section with flat bullets matching the released-version style, so the release workflow's Unreleased-to-version rename targets one unambiguous section. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- CHANGELOG.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe87a80..3baa267 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,7 @@ cozystack.installer Release Notes ================================= Unreleased +========== - CI: new ``hack/check-versions.sh`` invariant check runs in the ``Lint`` job and fails the build if version strings drift across the three @@ -26,13 +27,6 @@ Unreleased Talos machine config. The filter is overridable from inventory via ``cozystack_lvm_global_filter`` for hosts whose own PVs sit on device-mapper devices (LVM-on-LUKS, multipath). - -Unreleased -========== - -Bugfixes --------- - - Prepare playbooks now enable ``device_ownership_from_security_context`` on the containerd CRI plugin (k3s drop-in From 7539c429bb2803e18e4f16669f59855dd8f67d64 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 13:19:46 +0300 Subject: [PATCH 05/12] docs(prepare): note LVM global_filter task replaces existing entries Clarify that the lineinfile task overwrites any existing global_filter (commented or active) in lvm.conf, so operators with custom filter rules must set cozystack_lvm_global_filter to the full desired list rather than only the entries they want to add. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f5b80f..322066b 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ The prepare playbooks set a `global_filter` in `/etc/lvm/lvm.conf` that rejects global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ] ``` -The list is exposed as `cozystack_lvm_global_filter` (see [Example playbook variables](#example-playbook-variables)). Override it from inventory on hosts whose own physical volumes sit on device-mapper devices — LVM-on-LUKS or LVM-on-multipath, where the PVs are `/dev/dm-*` — so they are not filtered out. Dedicated storage nodes use the default unchanged. +The list is exposed as `cozystack_lvm_global_filter` (see [Example playbook variables](#example-playbook-variables)). Override it from inventory on hosts whose own physical volumes sit on device-mapper devices — LVM-on-LUKS or LVM-on-multipath, where the PVs are `/dev/dm-*` — so they are not filtered out. Dedicated storage nodes use the default unchanged. The task replaces whatever `global_filter` is already in `lvm.conf` (commented or active), so if a host already has custom filter rules set `cozystack_lvm_global_filter` to the full list you want, not just the entries to add. #### Required: Containerd + Kubernetes kernel modules From 961c1db535bf7c3e8ea2ac544e4ebf1ebe765cee Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 13:19:53 +0300 Subject: [PATCH 06/12] test(prepare): cover host LVM global_filter rendering and lineinfile Add tests/test-lvm-global-filter.yml asserting the templated default renders to valid LVM syntax, the lineinfile replaces a commented global_filter in place inside the devices block, is idempotent, falls back to the insertafter anchor (including an indented devices header) when no filter line exists, and honours a cozystack_lvm_global_filter override. Wire it into the Test workflow as its own job. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .github/workflows/test.yml | 18 +++ tests/test-lvm-global-filter.yml | 218 +++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 tests/test-lvm-global-filter.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48f2185..7ea429f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -184,6 +184,24 @@ jobs: echo "OK: Invalid IPs correctly rejected" + lvm-global-filter: + name: LVM global_filter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: Install Ansible + run: pip install ansible-core + + - name: Test host LVM global_filter rendering and lineinfile behaviour + run: ansible-playbook tests/test-lvm-global-filter.yml + e2e: name: E2E runs-on: ubuntu-latest diff --git a/tests/test-lvm-global-filter.yml b/tests/test-lvm-global-filter.yml new file mode 100644 index 0000000..9cf1b65 --- /dev/null +++ b/tests/test-lvm-global-filter.yml @@ -0,0 +1,218 @@ +--- +# Verify the host LVM global_filter task used by the example prepare +# playbooks (examples/*/prepare-*.yml). The lineinfile regexp/insertafter +# pair and the templated `line` value are mirrored here verbatim; keep them +# in sync with the playbooks. The task is exercised against throwaway +# lvm.conf fixtures in /tmp so no root or real LVM is required. +- name: Test host LVM global_filter task behaviour + hosts: localhost + gather_facts: false + vars: + # Expected rendered line for the shipped default (to_json output). + _lvm_expected_default_line: ' global_filter = ["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]' + # The exact templated value from the prepare playbooks. Re-rendered on + # every use, so a task-level cozystack_lvm_global_filter override is + # picked up automatically. + _lvm_filter_line: ' global_filter = {{ cozystack_lvm_global_filter | + default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", + "r|^/dev/loop.*|"]) | to_json }}' + _lvm_fixture_stock: /tmp/cozystack-test-lvm-stock.conf + _lvm_fixture_nofilter: /tmp/cozystack-test-lvm-nofilter.conf + _lvm_fixture_override: /tmp/cozystack-test-lvm-override.conf + _lvm_fixture_indented: /tmp/cozystack-test-lvm-indented.conf + tasks: + - name: Render the shipped default and assert valid LVM global_filter syntax + ansible.builtin.assert: + that: + - _lvm_filter_line == _lvm_expected_default_line + fail_msg: >- + Default global_filter drifted from the playbooks. Rendered + '{{ _lvm_filter_line }}', expected '{{ _lvm_expected_default_line }}'. + + # --- Stock lvm.conf: a commented global_filter already lives inside the + # devices block (as shipped by upstream lvm2). The regexp must replace it + # in place rather than appending a second copy at EOF. + - name: Create stock-like lvm.conf fixture + ansible.builtin.copy: + dest: "{{ _lvm_fixture_stock }}" + mode: "0644" + content: | + # Sample lvm.conf + devices { + dir = "/dev" + scan = [ "/dev" ] + # global_filter = [ "a|.*/|" ] + } + global { + umask = 63 + } + + - name: Apply global_filter to stock fixture (first run) + ansible.builtin.lineinfile: + path: "{{ _lvm_fixture_stock }}" + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^\s*devices\s*{' + line: "{{ _lvm_filter_line }}" + register: _stock_first + + - name: Apply global_filter to stock fixture (second run) + ansible.builtin.lineinfile: + path: "{{ _lvm_fixture_stock }}" + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^\s*devices\s*{' + line: "{{ _lvm_filter_line }}" + register: _stock_second + + - name: Read back the stock fixture + ansible.builtin.slurp: + src: "{{ _lvm_fixture_stock }}" + register: _stock_slurp + + - name: Assert global_filter replaced in place inside the devices block + ansible.builtin.assert: + that: + - _stock_first is changed + - _stock_second is not changed + - _lvm_expected_default_line in _lines + - "' # global_filter = [ \"a|.*/|\" ]' not in _lines" + - _lines.index(_lvm_expected_default_line) < _lines.index('}') + fail_msg: >- + global_filter was not replaced in place inside devices {} on the + stock fixture. + vars: + _lines: "{{ (_stock_slurp.content | b64decode).split('\n') }}" + + # --- Fresh lvm.conf with no global_filter at all: the insertafter anchor + # must drop the line immediately after the `devices {` header. + - name: Create lvm.conf fixture without any global_filter + ansible.builtin.copy: + dest: "{{ _lvm_fixture_nofilter }}" + mode: "0644" + content: | + devices { + dir = "/dev" + } + global { + umask = 63 + } + + - name: Apply global_filter to no-filter fixture (first run) + ansible.builtin.lineinfile: + path: "{{ _lvm_fixture_nofilter }}" + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^\s*devices\s*{' + line: "{{ _lvm_filter_line }}" + register: _nofilter_first + + - name: Apply global_filter to no-filter fixture (second run) + ansible.builtin.lineinfile: + path: "{{ _lvm_fixture_nofilter }}" + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^\s*devices\s*{' + line: "{{ _lvm_filter_line }}" + register: _nofilter_second + + - name: Read back the no-filter fixture + ansible.builtin.slurp: + src: "{{ _lvm_fixture_nofilter }}" + register: _nofilter_slurp + + - name: Assert global_filter inserted right after the devices header + ansible.builtin.assert: + that: + - _nofilter_first is changed + - _nofilter_second is not changed + - _lines[(_lines.index('devices {') | int) + 1] == _lvm_expected_default_line + fail_msg: >- + global_filter was not inserted immediately after 'devices {' on the + no-filter fixture. + vars: + _lines: "{{ (_nofilter_slurp.content | b64decode).split('\n') }}" + + # --- Override: dropping the dm- entry from cozystack_lvm_global_filter + # (the LVM-on-LUKS / multipath escape hatch) must render the shorter list. + - name: Create fixture for override case + ansible.builtin.copy: + dest: "{{ _lvm_fixture_override }}" + mode: "0644" + content: | + devices { + dir = "/dev" + } + + - name: Apply an overridden global_filter (dm- dropped) + ansible.builtin.lineinfile: + path: "{{ _lvm_fixture_override }}" + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^\s*devices\s*{' + line: "{{ _lvm_filter_line }}" + vars: + cozystack_lvm_global_filter: + - "r|^/dev/drbd.*|" + - "r|^/dev/loop.*|" + register: _override_run + + - name: Read back the override fixture + ansible.builtin.slurp: + src: "{{ _lvm_fixture_override }}" + register: _override_slurp + + - name: Assert the overridden filter rendered without the dm- entry + ansible.builtin.assert: + that: + - _override_run is changed + - ''' global_filter = ["r|^/dev/drbd.*|", "r|^/dev/loop.*|"]'' in _content' + - "'/dev/dm-' not in _content" + fail_msg: >- + Overridden cozystack_lvm_global_filter did not render as expected. + vars: + _content: "{{ _override_slurp.content | b64decode }}" + + # --- Robustness: an indented `devices {` header (leading whitespace or + # compact bracing) must still anchor the insert. The stricter `^devices {` + # would miss this and append global_filter at EOF, outside the block. + - name: Create lvm.conf fixture with an indented devices header + ansible.builtin.copy: + dest: "{{ _lvm_fixture_indented }}" + mode: "0644" + content: | + config { + checks = 1 + } + devices { + dir = "/dev" + } + + - name: Apply global_filter to the indented fixture + ansible.builtin.lineinfile: + path: "{{ _lvm_fixture_indented }}" + regexp: '^\s*#?\s*global_filter\s*=' + insertafter: '^\s*devices\s*{' + line: "{{ _lvm_filter_line }}" + register: _indented_run + + - name: Read back the indented fixture + ansible.builtin.slurp: + src: "{{ _lvm_fixture_indented }}" + register: _indented_slurp + + - name: Assert global_filter anchored to the indented devices header + ansible.builtin.assert: + that: + - _indented_run is changed + - _lines[(_lines.index(' devices {') | int) + 1] == _lvm_expected_default_line + fail_msg: >- + global_filter was not anchored to the indented 'devices {' header; + the insertafter regex is not whitespace-tolerant. + vars: + _lines: "{{ (_indented_slurp.content | b64decode).split('\n') }}" + + - name: Remove fixtures + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - "{{ _lvm_fixture_stock }}" + - "{{ _lvm_fixture_nofilter }}" + - "{{ _lvm_fixture_override }}" + - "{{ _lvm_fixture_indented }}" From ffc62f468b2df120e82be18d3b1ea0e3f1a3ee3e Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 13:30:22 +0300 Subject: [PATCH 07/12] test(prepare): pin regexp and insertafter across prepare playbooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the global_filter test with a drift guard that loads the rhel, suse and ubuntu prepare playbooks and asserts each one's regexp, insertafter and raw line template match the canonical values exercised here. Editing the task in one playbook without the others — or without this test — now fails CI. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- tests/test-lvm-global-filter.yml | 63 +++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/tests/test-lvm-global-filter.yml b/tests/test-lvm-global-filter.yml index 9cf1b65..9996a35 100644 --- a/tests/test-lvm-global-filter.yml +++ b/tests/test-lvm-global-filter.yml @@ -1,13 +1,17 @@ --- # Verify the host LVM global_filter task used by the example prepare # playbooks (examples/*/prepare-*.yml). The lineinfile regexp/insertafter -# pair and the templated `line` value are mirrored here verbatim; keep them -# in sync with the playbooks. The task is exercised against throwaway -# lvm.conf fixtures in /tmp so no root or real LVM is required. +# pair and the templated `line` value are defined once below and asserted +# against all three playbooks (drift guard), then exercised against +# throwaway lvm.conf fixtures in /tmp so no root or real LVM is required. - name: Test host LVM global_filter task behaviour hosts: localhost gather_facts: false vars: + # The canonical task parameters. The drift guard below proves the three + # prepare playbooks still use exactly these values. + _lvm_regexp: '^\s*#?\s*global_filter\s*=' + _lvm_insertafter: '^\s*devices\s*{' # Expected rendered line for the shipped default (to_json output). _lvm_expected_default_line: ' global_filter = ["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]' # The exact templated value from the prepare playbooks. Re-rendered on @@ -16,6 +20,18 @@ _lvm_filter_line: ' global_filter = {{ cozystack_lvm_global_filter | default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]) | to_json }}' + # Same template as a raw (un-rendered) literal — strings parsed out of a + # playbook with from_yaml are NOT re-templated, so the drift guard below + # compares the playbooks' `line` against this raw form, not the rendered + # one. + _lvm_raw_line: '{% raw %} global_filter = {{ cozystack_lvm_global_filter | + default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", + "r|^/dev/loop.*|"]) | to_json }}{% endraw %}' + _lvm_task_name: Exclude virtual and loop devices from host LVM scanning + _lvm_prepare_playbooks: + - "{{ playbook_dir }}/../examples/rhel/prepare-rhel.yml" + - "{{ playbook_dir }}/../examples/suse/prepare-suse.yml" + - "{{ playbook_dir }}/../examples/ubuntu/prepare-ubuntu.yml" _lvm_fixture_stock: /tmp/cozystack-test-lvm-stock.conf _lvm_fixture_nofilter: /tmp/cozystack-test-lvm-nofilter.conf _lvm_fixture_override: /tmp/cozystack-test-lvm-override.conf @@ -26,8 +42,21 @@ that: - _lvm_filter_line == _lvm_expected_default_line fail_msg: >- - Default global_filter drifted from the playbooks. Rendered - '{{ _lvm_filter_line }}', expected '{{ _lvm_expected_default_line }}'. + Default global_filter drifted. Rendered '{{ _lvm_filter_line }}', + expected '{{ _lvm_expected_default_line }}'. + + - name: Assert all prepare playbooks use the canonical task parameters + ansible.builtin.assert: + that: + - _pb_task.regexp == _lvm_regexp + - _pb_task.insertafter == _lvm_insertafter + - _pb_task.line == _lvm_raw_line + fail_msg: >- + {{ item }} global_filter task drifted from the canonical + regexp/insertafter/line tested here. + loop: "{{ _lvm_prepare_playbooks }}" + vars: + _pb_task: "{{ ((lookup('file', item) | from_yaml)[0].tasks | selectattr('name', 'eq', _lvm_task_name) | first)['ansible.builtin.lineinfile'] }}" # --- Stock lvm.conf: a commented global_filter already lives inside the # devices block (as shipped by upstream lvm2). The regexp must replace it @@ -50,16 +79,16 @@ - name: Apply global_filter to stock fixture (first run) ansible.builtin.lineinfile: path: "{{ _lvm_fixture_stock }}" - regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^\s*devices\s*{' + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" line: "{{ _lvm_filter_line }}" register: _stock_first - name: Apply global_filter to stock fixture (second run) ansible.builtin.lineinfile: path: "{{ _lvm_fixture_stock }}" - regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^\s*devices\s*{' + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" line: "{{ _lvm_filter_line }}" register: _stock_second @@ -99,16 +128,16 @@ - name: Apply global_filter to no-filter fixture (first run) ansible.builtin.lineinfile: path: "{{ _lvm_fixture_nofilter }}" - regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^\s*devices\s*{' + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" line: "{{ _lvm_filter_line }}" register: _nofilter_first - name: Apply global_filter to no-filter fixture (second run) ansible.builtin.lineinfile: path: "{{ _lvm_fixture_nofilter }}" - regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^\s*devices\s*{' + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" line: "{{ _lvm_filter_line }}" register: _nofilter_second @@ -143,8 +172,8 @@ - name: Apply an overridden global_filter (dm- dropped) ansible.builtin.lineinfile: path: "{{ _lvm_fixture_override }}" - regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^\s*devices\s*{' + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" line: "{{ _lvm_filter_line }}" vars: cozystack_lvm_global_filter: @@ -186,8 +215,8 @@ - name: Apply global_filter to the indented fixture ansible.builtin.lineinfile: path: "{{ _lvm_fixture_indented }}" - regexp: '^\s*#?\s*global_filter\s*=' - insertafter: '^\s*devices\s*{' + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" line: "{{ _lvm_filter_line }}" register: _indented_run From a870f429f54d38b25598fe64ce2d7edff7581d14 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 14:11:14 +0300 Subject: [PATCH 08/12] feat(prepare): verify host LVM global_filter took effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After writing global_filter, query LVM (lvmconfig devices/global_filter) and fail the play if the setting is not reported as active — for example when lvm.conf has no devices {} section and lineinfile appended the line at EOF, outside every block, where LVM silently ignores it. Turns that silent no-op into an actionable failure at prep time. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- examples/rhel/prepare-rhel.yml | 22 ++++++++++++++++++++++ examples/suse/prepare-suse.yml | 22 ++++++++++++++++++++++ examples/ubuntu/prepare-ubuntu.yml | 22 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/examples/rhel/prepare-rhel.yml b/examples/rhel/prepare-rhel.yml index f71ef58..3cb059d 100644 --- a/examples/rhel/prepare-rhel.yml +++ b/examples/rhel/prepare-rhel.yml @@ -298,6 +298,28 @@ default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]) | to_json }}' + # Confirm LVM honours the filter. `lvmconfig devices/global_filter` + # reports the setting only when it sits inside the devices {} block, so a + # filter that landed elsewhere (an lvm.conf with no devices section) is + # caught here instead of silently leaving the host LVM unfiltered. + - name: Read back the effective host LVM global_filter + ansible.builtin.command: + cmd: lvmconfig devices/global_filter + register: _cozystack_lvm_filter_check + changed_when: false + failed_when: false + + - name: Fail if the host LVM does not honour the configured global_filter + ansible.builtin.assert: + that: + - _cozystack_lvm_filter_check.rc == 0 + fail_msg: >- + LVM does not report devices/global_filter as active + (rc={{ _cozystack_lvm_filter_check.rc }}); the setting likely landed + outside the devices {} block. Ensure /etc/lvm/lvm.conf contains a + 'devices {' section. + success_msg: Host LVM global_filter is active. + - name: Configure sysctl parameters ansible.posix.sysctl: name: "{{ item.name }}" diff --git a/examples/suse/prepare-suse.yml b/examples/suse/prepare-suse.yml index 033b983..fcde481 100644 --- a/examples/suse/prepare-suse.yml +++ b/examples/suse/prepare-suse.yml @@ -293,6 +293,28 @@ default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]) | to_json }}' + # Confirm LVM honours the filter. `lvmconfig devices/global_filter` + # reports the setting only when it sits inside the devices {} block, so a + # filter that landed elsewhere (an lvm.conf with no devices section) is + # caught here instead of silently leaving the host LVM unfiltered. + - name: Read back the effective host LVM global_filter + ansible.builtin.command: + cmd: lvmconfig devices/global_filter + register: _cozystack_lvm_filter_check + changed_when: false + failed_when: false + + - name: Fail if the host LVM does not honour the configured global_filter + ansible.builtin.assert: + that: + - _cozystack_lvm_filter_check.rc == 0 + fail_msg: >- + LVM does not report devices/global_filter as active + (rc={{ _cozystack_lvm_filter_check.rc }}); the setting likely landed + outside the devices {} block. Ensure /etc/lvm/lvm.conf contains a + 'devices {' section. + success_msg: Host LVM global_filter is active. + - name: Configure sysctl parameters ansible.posix.sysctl: name: "{{ item.name }}" diff --git a/examples/ubuntu/prepare-ubuntu.yml b/examples/ubuntu/prepare-ubuntu.yml index e6bbba1..0dab175 100644 --- a/examples/ubuntu/prepare-ubuntu.yml +++ b/examples/ubuntu/prepare-ubuntu.yml @@ -339,6 +339,28 @@ default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]) | to_json }}' + # Confirm LVM honours the filter. `lvmconfig devices/global_filter` + # reports the setting only when it sits inside the devices {} block, so a + # filter that landed elsewhere (an lvm.conf with no devices section) is + # caught here instead of silently leaving the host LVM unfiltered. + - name: Read back the effective host LVM global_filter + ansible.builtin.command: + cmd: lvmconfig devices/global_filter + register: _cozystack_lvm_filter_check + changed_when: false + failed_when: false + + - name: Fail if the host LVM does not honour the configured global_filter + ansible.builtin.assert: + that: + - _cozystack_lvm_filter_check.rc == 0 + fail_msg: >- + LVM does not report devices/global_filter as active + (rc={{ _cozystack_lvm_filter_check.rc }}); the setting likely landed + outside the devices {} block. Ensure /etc/lvm/lvm.conf contains a + 'devices {' section. + success_msg: Host LVM global_filter is active. + - name: Configure sysctl parameters ansible.posix.sysctl: name: "{{ item.name }}" From c06237f383c0ecb735ca80ac85cd93d20f7c6406 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 14:11:26 +0300 Subject: [PATCH 09/12] test(prepare): verify global_filter effectiveness with lvmconfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drive each lvm.conf shape (commented-in-block, no-filter, indented header, override, and no-devices-block) through the prepare playbooks' lineinfile, then ask LVM via lvmconfig + LVM_SYSTEM_DIR whether the filter is effective — asserting it is for in-block cases and is NOT for the no-devices-block case (the silent failure the post-write check catches). Install lvm2 in the Test workflow for lvmconfig. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .github/workflows/test.yml | 5 +- tests/tasks-lvm-global-filter-case.yml | 63 ++++++ tests/test-lvm-global-filter.yml | 263 +++++++------------------ 3 files changed, 140 insertions(+), 191 deletions(-) create mode 100644 tests/tasks-lvm-global-filter-case.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ea429f..81c78bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -199,7 +199,10 @@ jobs: - name: Install Ansible run: pip install ansible-core - - name: Test host LVM global_filter rendering and lineinfile behaviour + - name: Install lvm2 (provides lvmconfig) + run: sudo apt-get update && sudo apt-get install --yes lvm2 + + - name: Test host LVM global_filter rendering and effectiveness run: ansible-playbook tests/test-lvm-global-filter.yml e2e: diff --git a/tests/tasks-lvm-global-filter-case.yml b/tests/tasks-lvm-global-filter-case.yml new file mode 100644 index 0000000..9d80771 --- /dev/null +++ b/tests/tasks-lvm-global-filter-case.yml @@ -0,0 +1,63 @@ +--- +# One global_filter scenario, included once per entry in _lvm_cases. Builds a +# throwaway lvm.conf, runs the same lineinfile the prepare playbooks use, then +# asks LVM itself (lvmconfig + LVM_SYSTEM_DIR) whether the filter is effective +# — exactly the post-write check the playbooks run. Expects these play vars: +# _lvm_base, _lvm_regexp, _lvm_insertafter, _lvm_filter_line, _lvm_default_list. +- name: Create the {{ item.name }} fixture directory + ansible.builtin.file: + path: "{{ _lvm_base }}/{{ item.name }}" + state: directory + mode: "0755" + +- name: Write the {{ item.name }} lvm.conf fixture + ansible.builtin.copy: + dest: "{{ _lvm_base }}/{{ item.name }}/lvm.conf" + mode: "0644" + content: "{{ item.content }}" + +- name: Apply global_filter to the {{ item.name }} fixture (first run) + ansible.builtin.lineinfile: + path: "{{ _lvm_base }}/{{ item.name }}/lvm.conf" + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" + line: "{{ _lvm_filter_line }}" + vars: + cozystack_lvm_global_filter: "{{ item.override | default(_lvm_default_list) }}" + register: _case_first + +- name: Apply global_filter to the {{ item.name }} fixture (second run) + ansible.builtin.lineinfile: + path: "{{ _lvm_base }}/{{ item.name }}/lvm.conf" + regexp: "{{ _lvm_regexp }}" + insertafter: "{{ _lvm_insertafter }}" + line: "{{ _lvm_filter_line }}" + vars: + cozystack_lvm_global_filter: "{{ item.override | default(_lvm_default_list) }}" + register: _case_second + +- name: Ask LVM whether the {{ item.name }} global_filter is effective + ansible.builtin.command: + cmd: lvmconfig devices/global_filter + environment: + LVM_SYSTEM_DIR: "{{ _lvm_base }}/{{ item.name }}" + register: _case_lvm + changed_when: false + failed_when: false + +- name: Assert the {{ item.name }} case behaves as expected + ansible.builtin.assert: + that: + # The task is idempotent regardless of where the line landed. + - _case_first is changed + - _case_second is not changed + # lvmconfig returns rc 0 only when global_filter sits inside devices {}. + - item.effective == (_case_lvm.rc == 0) + # When effective, LVM must report exactly the configured list. + - (not item.effective) or + ((_case_lvm.stdout | regex_replace('^global_filter=', '') | from_json | sort) + == ((item.override | default(_lvm_default_list)) | sort)) + fail_msg: >- + Case {{ item.name }}: expected effective={{ item.effective }}, got + rc={{ _case_lvm.rc }} stdout='{{ _case_lvm.stdout }}'. + success_msg: Case {{ item.name }} behaves as expected. diff --git a/tests/test-lvm-global-filter.yml b/tests/test-lvm-global-filter.yml index 9996a35..7c499b1 100644 --- a/tests/test-lvm-global-filter.yml +++ b/tests/test-lvm-global-filter.yml @@ -1,17 +1,24 @@ --- # Verify the host LVM global_filter task used by the example prepare -# playbooks (examples/*/prepare-*.yml). The lineinfile regexp/insertafter -# pair and the templated `line` value are defined once below and asserted -# against all three playbooks (drift guard), then exercised against -# throwaway lvm.conf fixtures in /tmp so no root or real LVM is required. +# playbooks (examples/*/prepare-*.yml). The canonical task parameters are +# defined once and asserted against all three playbooks (drift guard), then +# each scenario builds a throwaway lvm.conf and asks LVM itself +# (`lvmconfig devices/global_filter` via LVM_SYSTEM_DIR) whether the filter is +# effective — the same post-write check the playbooks run. Requires the lvm2 +# package (lvmconfig); the Test workflow installs it. - name: Test host LVM global_filter task behaviour hosts: localhost gather_facts: false vars: - # The canonical task parameters. The drift guard below proves the three - # prepare playbooks still use exactly these values. + # The canonical task parameters. The drift guard proves the three prepare + # playbooks still use exactly these values. _lvm_regexp: '^\s*#?\s*global_filter\s*=' _lvm_insertafter: '^\s*devices\s*{' + _lvm_default_list: + - "r|^/dev/drbd.*|" + - "r|^/dev/dm-.*|" + - "r|^/dev/zd.*|" + - "r|^/dev/loop.*|" # Expected rendered line for the shipped default (to_json output). _lvm_expected_default_line: ' global_filter = ["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]' # The exact templated value from the prepare playbooks. Re-rendered on @@ -21,9 +28,8 @@ default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]) | to_json }}' # Same template as a raw (un-rendered) literal — strings parsed out of a - # playbook with from_yaml are NOT re-templated, so the drift guard below - # compares the playbooks' `line` against this raw form, not the rendered - # one. + # playbook with from_yaml are NOT re-templated, so the drift guard compares + # the playbooks' `line` against this raw form, not the rendered one. _lvm_raw_line: '{% raw %} global_filter = {{ cozystack_lvm_global_filter | default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]) | to_json }}{% endraw %}' @@ -32,39 +38,14 @@ - "{{ playbook_dir }}/../examples/rhel/prepare-rhel.yml" - "{{ playbook_dir }}/../examples/suse/prepare-suse.yml" - "{{ playbook_dir }}/../examples/ubuntu/prepare-ubuntu.yml" - _lvm_fixture_stock: /tmp/cozystack-test-lvm-stock.conf - _lvm_fixture_nofilter: /tmp/cozystack-test-lvm-nofilter.conf - _lvm_fixture_override: /tmp/cozystack-test-lvm-override.conf - _lvm_fixture_indented: /tmp/cozystack-test-lvm-indented.conf - tasks: - - name: Render the shipped default and assert valid LVM global_filter syntax - ansible.builtin.assert: - that: - - _lvm_filter_line == _lvm_expected_default_line - fail_msg: >- - Default global_filter drifted. Rendered '{{ _lvm_filter_line }}', - expected '{{ _lvm_expected_default_line }}'. - - - name: Assert all prepare playbooks use the canonical task parameters - ansible.builtin.assert: - that: - - _pb_task.regexp == _lvm_regexp - - _pb_task.insertafter == _lvm_insertafter - - _pb_task.line == _lvm_raw_line - fail_msg: >- - {{ item }} global_filter task drifted from the canonical - regexp/insertafter/line tested here. - loop: "{{ _lvm_prepare_playbooks }}" - vars: - _pb_task: "{{ ((lookup('file', item) | from_yaml)[0].tasks | selectattr('name', 'eq', _lvm_task_name) | first)['ansible.builtin.lineinfile'] }}" - - # --- Stock lvm.conf: a commented global_filter already lives inside the - # devices block (as shipped by upstream lvm2). The regexp must replace it - # in place rather than appending a second copy at EOF. - - name: Create stock-like lvm.conf fixture - ansible.builtin.copy: - dest: "{{ _lvm_fixture_stock }}" - mode: "0644" + _lvm_base: /tmp/cozystack-lvmtest + # One scenario per lvm.conf shape. `effective` is whether LVM should honour + # the filter afterwards (i.e. it landed inside a devices {} block). + _lvm_cases: + # Upstream-style: a commented global_filter already inside devices {}. + # The regexp replaces it in place. + - name: stock + effective: true content: | # Sample lvm.conf devices { @@ -75,48 +56,9 @@ global { umask = 63 } - - - name: Apply global_filter to stock fixture (first run) - ansible.builtin.lineinfile: - path: "{{ _lvm_fixture_stock }}" - regexp: "{{ _lvm_regexp }}" - insertafter: "{{ _lvm_insertafter }}" - line: "{{ _lvm_filter_line }}" - register: _stock_first - - - name: Apply global_filter to stock fixture (second run) - ansible.builtin.lineinfile: - path: "{{ _lvm_fixture_stock }}" - regexp: "{{ _lvm_regexp }}" - insertafter: "{{ _lvm_insertafter }}" - line: "{{ _lvm_filter_line }}" - register: _stock_second - - - name: Read back the stock fixture - ansible.builtin.slurp: - src: "{{ _lvm_fixture_stock }}" - register: _stock_slurp - - - name: Assert global_filter replaced in place inside the devices block - ansible.builtin.assert: - that: - - _stock_first is changed - - _stock_second is not changed - - _lvm_expected_default_line in _lines - - "' # global_filter = [ \"a|.*/|\" ]' not in _lines" - - _lines.index(_lvm_expected_default_line) < _lines.index('}') - fail_msg: >- - global_filter was not replaced in place inside devices {} on the - stock fixture. - vars: - _lines: "{{ (_stock_slurp.content | b64decode).split('\n') }}" - - # --- Fresh lvm.conf with no global_filter at all: the insertafter anchor - # must drop the line immediately after the `devices {` header. - - name: Create lvm.conf fixture without any global_filter - ansible.builtin.copy: - dest: "{{ _lvm_fixture_nofilter }}" - mode: "0644" + # No global_filter yet: insertafter drops it after the devices header. + - name: nofilter + effective: true content: | devices { dir = "/dev" @@ -124,86 +66,10 @@ global { umask = 63 } - - - name: Apply global_filter to no-filter fixture (first run) - ansible.builtin.lineinfile: - path: "{{ _lvm_fixture_nofilter }}" - regexp: "{{ _lvm_regexp }}" - insertafter: "{{ _lvm_insertafter }}" - line: "{{ _lvm_filter_line }}" - register: _nofilter_first - - - name: Apply global_filter to no-filter fixture (second run) - ansible.builtin.lineinfile: - path: "{{ _lvm_fixture_nofilter }}" - regexp: "{{ _lvm_regexp }}" - insertafter: "{{ _lvm_insertafter }}" - line: "{{ _lvm_filter_line }}" - register: _nofilter_second - - - name: Read back the no-filter fixture - ansible.builtin.slurp: - src: "{{ _lvm_fixture_nofilter }}" - register: _nofilter_slurp - - - name: Assert global_filter inserted right after the devices header - ansible.builtin.assert: - that: - - _nofilter_first is changed - - _nofilter_second is not changed - - _lines[(_lines.index('devices {') | int) + 1] == _lvm_expected_default_line - fail_msg: >- - global_filter was not inserted immediately after 'devices {' on the - no-filter fixture. - vars: - _lines: "{{ (_nofilter_slurp.content | b64decode).split('\n') }}" - - # --- Override: dropping the dm- entry from cozystack_lvm_global_filter - # (the LVM-on-LUKS / multipath escape hatch) must render the shorter list. - - name: Create fixture for override case - ansible.builtin.copy: - dest: "{{ _lvm_fixture_override }}" - mode: "0644" - content: | - devices { - dir = "/dev" - } - - - name: Apply an overridden global_filter (dm- dropped) - ansible.builtin.lineinfile: - path: "{{ _lvm_fixture_override }}" - regexp: "{{ _lvm_regexp }}" - insertafter: "{{ _lvm_insertafter }}" - line: "{{ _lvm_filter_line }}" - vars: - cozystack_lvm_global_filter: - - "r|^/dev/drbd.*|" - - "r|^/dev/loop.*|" - register: _override_run - - - name: Read back the override fixture - ansible.builtin.slurp: - src: "{{ _lvm_fixture_override }}" - register: _override_slurp - - - name: Assert the overridden filter rendered without the dm- entry - ansible.builtin.assert: - that: - - _override_run is changed - - ''' global_filter = ["r|^/dev/drbd.*|", "r|^/dev/loop.*|"]'' in _content' - - "'/dev/dm-' not in _content" - fail_msg: >- - Overridden cozystack_lvm_global_filter did not render as expected. - vars: - _content: "{{ _override_slurp.content | b64decode }}" - - # --- Robustness: an indented `devices {` header (leading whitespace or - # compact bracing) must still anchor the insert. The stricter `^devices {` - # would miss this and append global_filter at EOF, outside the block. - - name: Create lvm.conf fixture with an indented devices header - ansible.builtin.copy: - dest: "{{ _lvm_fixture_indented }}" - mode: "0644" + # Indented devices header: the whitespace-tolerant anchor must still + # place the filter inside the block (the strict '^devices {' would not). + - name: indented + effective: true content: | config { checks = 1 @@ -211,37 +77,54 @@ devices { dir = "/dev" } + # Override drops the dm- entry (the LVM-on-LUKS / multipath escape hatch). + - name: override + effective: true + override: + - "r|^/dev/drbd.*|" + - "r|^/dev/loop.*|" + content: | + devices { + dir = "/dev" + } + # No devices {} block at all: the line lands at EOF, outside every + # section, so LVM ignores it. The playbooks' post-write check fails loud + # on this; here we assert LVM reports the filter as NOT effective. + - name: noblock + effective: false + content: | + global { + umask = 63 + } + tasks: + - name: Render the shipped default and assert valid LVM global_filter syntax + ansible.builtin.assert: + that: + - _lvm_filter_line == _lvm_expected_default_line + fail_msg: >- + Default global_filter drifted. Rendered '{{ _lvm_filter_line }}', + expected '{{ _lvm_expected_default_line }}'. - - name: Apply global_filter to the indented fixture - ansible.builtin.lineinfile: - path: "{{ _lvm_fixture_indented }}" - regexp: "{{ _lvm_regexp }}" - insertafter: "{{ _lvm_insertafter }}" - line: "{{ _lvm_filter_line }}" - register: _indented_run - - - name: Read back the indented fixture - ansible.builtin.slurp: - src: "{{ _lvm_fixture_indented }}" - register: _indented_slurp - - - name: Assert global_filter anchored to the indented devices header + - name: Assert all prepare playbooks use the canonical task parameters ansible.builtin.assert: that: - - _indented_run is changed - - _lines[(_lines.index(' devices {') | int) + 1] == _lvm_expected_default_line + - _pb_task.regexp == _lvm_regexp + - _pb_task.insertafter == _lvm_insertafter + - _pb_task.line == _lvm_raw_line fail_msg: >- - global_filter was not anchored to the indented 'devices {' header; - the insertafter regex is not whitespace-tolerant. + {{ item }} global_filter task drifted from the canonical + regexp/insertafter/line tested here. + loop: "{{ _lvm_prepare_playbooks }}" vars: - _lines: "{{ (_indented_slurp.content | b64decode).split('\n') }}" + _pb_task: "{{ ((lookup('file', item) | from_yaml)[0].tasks | selectattr('name', 'eq', _lvm_task_name) | first)['ansible.builtin.lineinfile'] }}" + + - name: Exercise every lvm.conf scenario + ansible.builtin.include_tasks: "{{ playbook_dir }}/tasks-lvm-global-filter-case.yml" + loop: "{{ _lvm_cases }}" + loop_control: + label: "{{ item.name }}" - - name: Remove fixtures + - name: Remove fixture tree ansible.builtin.file: - path: "{{ item }}" + path: "{{ _lvm_base }}" state: absent - loop: - - "{{ _lvm_fixture_stock }}" - - "{{ _lvm_fixture_nofilter }}" - - "{{ _lvm_fixture_override }}" - - "{{ _lvm_fixture_indented }}" From 91cc24b60f835aacb7eea7615118a4cc0ecfd2ed Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 14:11:26 +0300 Subject: [PATCH 10/12] docs(prepare): document the global_filter effectiveness check Note in the README and CHANGELOG that the prepare playbooks verify the global_filter with lvmconfig after writing it and fail loudly when it did not take effect (e.g. an lvm.conf with no devices section). Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- CHANGELOG.rst | 5 ++++- README.md | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3baa267..8e7f4d6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,7 +26,10 @@ Unreleased inside loop-mounted images. Mirrors the global_filter shipped in the Talos machine config. The filter is overridable from inventory via ``cozystack_lvm_global_filter`` for hosts whose own PVs sit on - device-mapper devices (LVM-on-LUKS, multipath). + device-mapper devices (LVM-on-LUKS, multipath). After writing it the + playbook verifies the filter with ``lvmconfig`` and fails loudly if it + did not take effect (for example on an ``lvm.conf`` with no ``devices`` + section), instead of leaving the host silently unfiltered. - Prepare playbooks now enable ``device_ownership_from_security_context`` on the containerd CRI plugin (k3s drop-in diff --git a/README.md b/README.md index 322066b..cb696e8 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,8 @@ global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev The list is exposed as `cozystack_lvm_global_filter` (see [Example playbook variables](#example-playbook-variables)). Override it from inventory on hosts whose own physical volumes sit on device-mapper devices — LVM-on-LUKS or LVM-on-multipath, where the PVs are `/dev/dm-*` — so they are not filtered out. Dedicated storage nodes use the default unchanged. The task replaces whatever `global_filter` is already in `lvm.conf` (commented or active), so if a host already has custom filter rules set `cozystack_lvm_global_filter` to the full list you want, not just the entries to add. +After writing the filter the playbook asks LVM itself (`lvmconfig devices/global_filter`) whether it is effective and fails loudly if it is not — for example when `lvm.conf` has no `devices {` section and the setting would land outside any block. This turns a silent no-op into an actionable failure at prep time instead of an unfiltered host after reboot. + #### Required: Containerd + Kubernetes kernel modules Required for containerd's overlay storage driver and standard Kubernetes bridge networking. Loaded via `/etc/modules-load.d/cozystack.conf`: From 023de1959dac902ae143dd257373f02900438fb8 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 14:28:16 +0300 Subject: [PATCH 11/12] feat(prepare): verify the effective global_filter value, not just success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare the list lvmconfig reports for devices/global_filter against the configured cozystack_lvm_global_filter, not only its exit status. When lvm.conf holds more than one global_filter line, lineinfile replaces the last match — which may sit outside devices {} — while a stale in-block filter keeps winning; lvmconfig then returns 0 with the old value. The value comparison rejects that. The default list is hoisted to a single _cozystack_lvm_default_filter fact shared by the write and the check. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- examples/rhel/prepare-rhel.yml | 36 ++++++++++++++++++++---------- examples/suse/prepare-suse.yml | 36 ++++++++++++++++++++---------- examples/ubuntu/prepare-ubuntu.yml | 36 ++++++++++++++++++++---------- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/examples/rhel/prepare-rhel.yml b/examples/rhel/prepare-rhel.yml index 3cb059d..7905530 100644 --- a/examples/rhel/prepare-rhel.yml +++ b/examples/rhel/prepare-rhel.yml @@ -289,19 +289,27 @@ # Override cozystack_lvm_global_filter from inventory on hosts whose # own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so # they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry. + - name: Set the default host LVM global_filter + ansible.builtin.set_fact: + _cozystack_lvm_default_filter: + - "r|^/dev/drbd.*|" + - "r|^/dev/dm-.*|" + - "r|^/dev/zd.*|" + - "r|^/dev/loop.*|" + - name: Exclude virtual and loop devices from host LVM scanning ansible.builtin.lineinfile: path: /etc/lvm/lvm.conf regexp: '^\s*#?\s*global_filter\s*=' insertafter: '^\s*devices\s*{' - line: ' global_filter = {{ cozystack_lvm_global_filter | - default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", - "r|^/dev/loop.*|"]) | to_json }}' - - # Confirm LVM honours the filter. `lvmconfig devices/global_filter` - # reports the setting only when it sits inside the devices {} block, so a - # filter that landed elsewhere (an lvm.conf with no devices section) is - # caught here instead of silently leaving the host LVM unfiltered. + line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}' + + # Confirm LVM honours the filter and applied the requested list. + # `lvmconfig devices/global_filter` reports the value only when it sits + # inside the devices {} block, so a filter that landed elsewhere (an + # lvm.conf with no devices section, or a stale duplicate outside the block + # that won the lineinfile match) is caught here instead of silently + # leaving the host LVM unfiltered or running an old filter. - name: Read back the effective host LVM global_filter ansible.builtin.command: cmd: lvmconfig devices/global_filter @@ -313,11 +321,15 @@ ansible.builtin.assert: that: - _cozystack_lvm_filter_check.rc == 0 + - _cozystack_lvm_filter_check.rc != 0 or + ((_cozystack_lvm_filter_check.stdout | regex_replace('^global_filter=', '') | from_json | sort) + == ((cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter)) | sort)) fail_msg: >- - LVM does not report devices/global_filter as active - (rc={{ _cozystack_lvm_filter_check.rc }}); the setting likely landed - outside the devices {} block. Ensure /etc/lvm/lvm.conf contains a - 'devices {' section. + LVM does not report the configured global_filter as active + (rc={{ _cozystack_lvm_filter_check.rc }}, value + '{{ _cozystack_lvm_filter_check.stdout }}'). It likely landed outside + the devices {} block or a stale entry won — ensure /etc/lvm/lvm.conf + has a 'devices {' section and no conflicting global_filter. success_msg: Host LVM global_filter is active. - name: Configure sysctl parameters diff --git a/examples/suse/prepare-suse.yml b/examples/suse/prepare-suse.yml index fcde481..22b6cd4 100644 --- a/examples/suse/prepare-suse.yml +++ b/examples/suse/prepare-suse.yml @@ -284,19 +284,27 @@ # Override cozystack_lvm_global_filter from inventory on hosts whose # own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so # they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry. + - name: Set the default host LVM global_filter + ansible.builtin.set_fact: + _cozystack_lvm_default_filter: + - "r|^/dev/drbd.*|" + - "r|^/dev/dm-.*|" + - "r|^/dev/zd.*|" + - "r|^/dev/loop.*|" + - name: Exclude virtual and loop devices from host LVM scanning ansible.builtin.lineinfile: path: /etc/lvm/lvm.conf regexp: '^\s*#?\s*global_filter\s*=' insertafter: '^\s*devices\s*{' - line: ' global_filter = {{ cozystack_lvm_global_filter | - default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", - "r|^/dev/loop.*|"]) | to_json }}' - - # Confirm LVM honours the filter. `lvmconfig devices/global_filter` - # reports the setting only when it sits inside the devices {} block, so a - # filter that landed elsewhere (an lvm.conf with no devices section) is - # caught here instead of silently leaving the host LVM unfiltered. + line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}' + + # Confirm LVM honours the filter and applied the requested list. + # `lvmconfig devices/global_filter` reports the value only when it sits + # inside the devices {} block, so a filter that landed elsewhere (an + # lvm.conf with no devices section, or a stale duplicate outside the block + # that won the lineinfile match) is caught here instead of silently + # leaving the host LVM unfiltered or running an old filter. - name: Read back the effective host LVM global_filter ansible.builtin.command: cmd: lvmconfig devices/global_filter @@ -308,11 +316,15 @@ ansible.builtin.assert: that: - _cozystack_lvm_filter_check.rc == 0 + - _cozystack_lvm_filter_check.rc != 0 or + ((_cozystack_lvm_filter_check.stdout | regex_replace('^global_filter=', '') | from_json | sort) + == ((cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter)) | sort)) fail_msg: >- - LVM does not report devices/global_filter as active - (rc={{ _cozystack_lvm_filter_check.rc }}); the setting likely landed - outside the devices {} block. Ensure /etc/lvm/lvm.conf contains a - 'devices {' section. + LVM does not report the configured global_filter as active + (rc={{ _cozystack_lvm_filter_check.rc }}, value + '{{ _cozystack_lvm_filter_check.stdout }}'). It likely landed outside + the devices {} block or a stale entry won — ensure /etc/lvm/lvm.conf + has a 'devices {' section and no conflicting global_filter. success_msg: Host LVM global_filter is active. - name: Configure sysctl parameters diff --git a/examples/ubuntu/prepare-ubuntu.yml b/examples/ubuntu/prepare-ubuntu.yml index 0dab175..c78b1a1 100644 --- a/examples/ubuntu/prepare-ubuntu.yml +++ b/examples/ubuntu/prepare-ubuntu.yml @@ -330,19 +330,27 @@ # Override cozystack_lvm_global_filter from inventory on hosts whose # own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so # they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry. + - name: Set the default host LVM global_filter + ansible.builtin.set_fact: + _cozystack_lvm_default_filter: + - "r|^/dev/drbd.*|" + - "r|^/dev/dm-.*|" + - "r|^/dev/zd.*|" + - "r|^/dev/loop.*|" + - name: Exclude virtual and loop devices from host LVM scanning ansible.builtin.lineinfile: path: /etc/lvm/lvm.conf regexp: '^\s*#?\s*global_filter\s*=' insertafter: '^\s*devices\s*{' - line: ' global_filter = {{ cozystack_lvm_global_filter | - default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", - "r|^/dev/loop.*|"]) | to_json }}' - - # Confirm LVM honours the filter. `lvmconfig devices/global_filter` - # reports the setting only when it sits inside the devices {} block, so a - # filter that landed elsewhere (an lvm.conf with no devices section) is - # caught here instead of silently leaving the host LVM unfiltered. + line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}' + + # Confirm LVM honours the filter and applied the requested list. + # `lvmconfig devices/global_filter` reports the value only when it sits + # inside the devices {} block, so a filter that landed elsewhere (an + # lvm.conf with no devices section, or a stale duplicate outside the block + # that won the lineinfile match) is caught here instead of silently + # leaving the host LVM unfiltered or running an old filter. - name: Read back the effective host LVM global_filter ansible.builtin.command: cmd: lvmconfig devices/global_filter @@ -354,11 +362,15 @@ ansible.builtin.assert: that: - _cozystack_lvm_filter_check.rc == 0 + - _cozystack_lvm_filter_check.rc != 0 or + ((_cozystack_lvm_filter_check.stdout | regex_replace('^global_filter=', '') | from_json | sort) + == ((cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter)) | sort)) fail_msg: >- - LVM does not report devices/global_filter as active - (rc={{ _cozystack_lvm_filter_check.rc }}); the setting likely landed - outside the devices {} block. Ensure /etc/lvm/lvm.conf contains a - 'devices {' section. + LVM does not report the configured global_filter as active + (rc={{ _cozystack_lvm_filter_check.rc }}, value + '{{ _cozystack_lvm_filter_check.stdout }}'). It likely landed outside + the devices {} block or a stale entry won — ensure /etc/lvm/lvm.conf + has a 'devices {' section and no conflicting global_filter. success_msg: Host LVM global_filter is active. - name: Configure sysctl parameters From 189287e7fc2a8af38d820315bf2298c410c03325 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Wed, 3 Jun 2026 14:28:16 +0300 Subject: [PATCH 12/12] test(prepare): cover the stale-duplicate global_filter case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a scenario where a stale global_filter inside devices {} plus a second match after it make lineinfile leave the stale value effective; assert lvmconfig reports a list that does NOT equal the configured one — which is what the playbooks' value comparison rejects. Mirror the single-source _cozystack_lvm_default_filter the playbooks now use. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- tests/tasks-lvm-global-filter-case.yml | 8 ++++++-- tests/test-lvm-global-filter.yml | 25 +++++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/tasks-lvm-global-filter-case.yml b/tests/tasks-lvm-global-filter-case.yml index 9d80771..41c6797 100644 --- a/tests/tasks-lvm-global-filter-case.yml +++ b/tests/tasks-lvm-global-filter-case.yml @@ -53,10 +53,14 @@ - _case_second is not changed # lvmconfig returns rc 0 only when global_filter sits inside devices {}. - item.effective == (_case_lvm.rc == 0) - # When effective, LVM must report exactly the configured list. + # When effective, LVM must report exactly the configured list — except + # the stale-duplicate case, where a pre-existing in-block filter wins the + # lineinfile race and the configured list must NOT match. That mismatch + # is exactly what the playbooks' value comparison rejects. - (not item.effective) or - ((_case_lvm.stdout | regex_replace('^global_filter=', '') | from_json | sort) + (((_case_lvm.stdout | regex_replace('^global_filter=', '') | from_json | sort) == ((item.override | default(_lvm_default_list)) | sort)) + != (item.stale | default(false))) fail_msg: >- Case {{ item.name }}: expected effective={{ item.effective }}, got rc={{ _case_lvm.rc }} stdout='{{ _case_lvm.stdout }}'. diff --git a/tests/test-lvm-global-filter.yml b/tests/test-lvm-global-filter.yml index 7c499b1..4fe5925 100644 --- a/tests/test-lvm-global-filter.yml +++ b/tests/test-lvm-global-filter.yml @@ -19,20 +19,19 @@ - "r|^/dev/dm-.*|" - "r|^/dev/zd.*|" - "r|^/dev/loop.*|" + # Mirrors the set_fact of the same name in the prepare playbooks; the + # templated line below references it exactly as the playbooks do. + _cozystack_lvm_default_filter: "{{ _lvm_default_list }}" # Expected rendered line for the shipped default (to_json output). _lvm_expected_default_line: ' global_filter = ["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|"]' # The exact templated value from the prepare playbooks. Re-rendered on # every use, so a task-level cozystack_lvm_global_filter override is # picked up automatically. - _lvm_filter_line: ' global_filter = {{ cozystack_lvm_global_filter | - default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", - "r|^/dev/loop.*|"]) | to_json }}' + _lvm_filter_line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}' # Same template as a raw (un-rendered) literal — strings parsed out of a # playbook with from_yaml are NOT re-templated, so the drift guard compares # the playbooks' `line` against this raw form, not the rendered one. - _lvm_raw_line: '{% raw %} global_filter = {{ cozystack_lvm_global_filter | - default(["r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", - "r|^/dev/loop.*|"]) | to_json }}{% endraw %}' + _lvm_raw_line: '{% raw %} global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}{% endraw %}' _lvm_task_name: Exclude virtual and loop devices from host LVM scanning _lvm_prepare_playbooks: - "{{ playbook_dir }}/../examples/rhel/prepare-rhel.yml" @@ -96,6 +95,20 @@ global { umask = 63 } + # A stale global_filter already lives inside devices {} and a second + # match sits after the block. lineinfile replaces the LAST match (the + # one outside), so the stale in-block value keeps winning. lvmconfig + # returns rc 0 but reports the stale list, not ours — the playbooks' + # value comparison rejects exactly this. + - name: stale_duplicate + effective: true + stale: true + content: | + devices { + dir = "/dev" + global_filter = [ "r|^/dev/sda.*|" ] + } + # global_filter = [ "a|.*/|" ] tasks: - name: Render the shipped default and assert valid LVM global_filter syntax ansible.builtin.assert: