Skip to content

Commit 222f3e8

Browse files
kvapsclaudelexfrei
authored
feat(prepare): exclude loop and virtual devices from host LVM scanning (#49)
* feat(prepare): exclude loop and virtual devices from host LVM scanning 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 <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com> * 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * fix(changelog): collapse duplicate Unreleased section 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * test(prepare): pin regexp and insertafter across prepare playbooks 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * feat(prepare): verify host LVM global_filter took effect 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * test(prepare): verify global_filter effectiveness with lvmconfig 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * feat(prepare): verify the effective global_filter value, not just success 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> * test(prepare): cover the stale-duplicate global_filter case 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 <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> --------- Signed-off-by: Andrei Kvapil <kvapss@gmail.com> Signed-off-by: Aleksei Sviridkin <f@lex.la> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Aleksei Sviridkin <f@lex.la>
1 parent b32c277 commit 222f3e8

8 files changed

Lines changed: 411 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,27 @@ jobs:
184184
185185
echo "OK: Invalid IPs correctly rejected"
186186
187+
lvm-global-filter:
188+
name: LVM global_filter
189+
runs-on: ubuntu-latest
190+
steps:
191+
- name: Checkout
192+
uses: actions/checkout@v6
193+
194+
- name: Set up Python
195+
uses: actions/setup-python@v6
196+
with:
197+
python-version: "3.14"
198+
199+
- name: Install Ansible
200+
run: pip install ansible-core
201+
202+
- name: Install lvm2 (provides lvmconfig)
203+
run: sudo apt-get update && sudo apt-get install --yes lvm2
204+
205+
- name: Test host LVM global_filter rendering and effectiveness
206+
run: ansible-playbook tests/test-lvm-global-filter.yml
207+
187208
e2e:
188209
name: E2E
189210
runs-on: ubuntu-latest

CHANGELOG.rst

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ cozystack.installer Release Notes
33
=================================
44

55
Unreleased
6+
==========
67

78
- CI: new ``hack/check-versions.sh`` invariant check runs in the ``Lint``
89
job and fails the build if version strings drift across the three
@@ -18,13 +19,17 @@ Unreleased
1819
IP addresses for ingress-nginx Service ``externalIPs``. Required on
1920
``isp-full-generic`` platform variant when nodes lack a native load
2021
balancer (cloud VMs, bare metal).
21-
22-
Unreleased
23-
==========
24-
25-
Bugfixes
26-
--------
27-
22+
- Prepare playbooks now set an LVM ``global_filter`` in
23+
``/etc/lvm/lvm.conf`` excluding ``/dev/drbd*``, ``/dev/dm-*``,
24+
``/dev/zd*`` and ``/dev/loop*`` so the host LVM does not scan or
25+
activate volume groups backed by LINSTOR/DRBD volumes or located
26+
inside loop-mounted images. Mirrors the global_filter shipped in the
27+
Talos machine config. The filter is overridable from inventory via
28+
``cozystack_lvm_global_filter`` for hosts whose own PVs sit on
29+
device-mapper devices (LVM-on-LUKS, multipath). After writing it the
30+
playbook verifies the filter with ``lvmconfig`` and fails loudly if it
31+
did not take effect (for example on an ``lvm.conf`` with no ``devices``
32+
section), instead of leaving the host silently unfiltered.
2833
- Prepare playbooks now enable
2934
``device_ownership_from_security_context`` on the containerd CRI
3035
plugin (k3s drop-in

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ blacklist {
101101
}
102102
```
103103

104+
#### Required: Host LVM global_filter
105+
106+
> **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.
107+
108+
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:
109+
110+
```text
111+
global_filter = [ "r|^/dev/drbd.*|", "r|^/dev/dm-.*|", "r|^/dev/zd.*|", "r|^/dev/loop.*|" ]
112+
```
113+
114+
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.
115+
116+
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.
117+
104118
#### Required: Containerd + Kubernetes kernel modules
105119

106120
Required for containerd's overlay storage driver and standard Kubernetes bridge networking. Loaded via `/etc/modules-load.d/cozystack.conf`:
@@ -362,6 +376,7 @@ These variables are consumed only by the example prepare playbooks in `examples/
362376
| `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. |
363377
| `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. |
364378
| `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. |
379+
| `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. |
365380
| `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. |
366381
| `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. |
367382
| `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. |

examples/rhel/prepare-rhel.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,57 @@
281281
}
282282
notify: Restart multipathd
283283

284+
# Exclude DRBD, device-mapper, zvol and loop devices from the host's
285+
# LVM device scanning. Without this the host LVM may scan and activate
286+
# volume groups backed by LINSTOR/DRBD volumes — or located inside
287+
# loop-mounted images — making them unavailable to the satellite.
288+
# Mirrors the global_filter shipped in the Talos machine config.
289+
# Override cozystack_lvm_global_filter from inventory on hosts whose
290+
# own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so
291+
# they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry.
292+
- name: Set the default host LVM global_filter
293+
ansible.builtin.set_fact:
294+
_cozystack_lvm_default_filter:
295+
- "r|^/dev/drbd.*|"
296+
- "r|^/dev/dm-.*|"
297+
- "r|^/dev/zd.*|"
298+
- "r|^/dev/loop.*|"
299+
300+
- name: Exclude virtual and loop devices from host LVM scanning
301+
ansible.builtin.lineinfile:
302+
path: /etc/lvm/lvm.conf
303+
regexp: '^\s*#?\s*global_filter\s*='
304+
insertafter: '^\s*devices\s*{'
305+
line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}'
306+
307+
# Confirm LVM honours the filter and applied the requested list.
308+
# `lvmconfig devices/global_filter` reports the value only when it sits
309+
# inside the devices {} block, so a filter that landed elsewhere (an
310+
# lvm.conf with no devices section, or a stale duplicate outside the block
311+
# that won the lineinfile match) is caught here instead of silently
312+
# leaving the host LVM unfiltered or running an old filter.
313+
- name: Read back the effective host LVM global_filter
314+
ansible.builtin.command:
315+
cmd: lvmconfig devices/global_filter
316+
register: _cozystack_lvm_filter_check
317+
changed_when: false
318+
failed_when: false
319+
320+
- name: Fail if the host LVM does not honour the configured global_filter
321+
ansible.builtin.assert:
322+
that:
323+
- _cozystack_lvm_filter_check.rc == 0
324+
- _cozystack_lvm_filter_check.rc != 0 or
325+
((_cozystack_lvm_filter_check.stdout | regex_replace('^global_filter=', '') | from_json | sort)
326+
== ((cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter)) | sort))
327+
fail_msg: >-
328+
LVM does not report the configured global_filter as active
329+
(rc={{ _cozystack_lvm_filter_check.rc }}, value
330+
'{{ _cozystack_lvm_filter_check.stdout }}'). It likely landed outside
331+
the devices {} block or a stale entry won — ensure /etc/lvm/lvm.conf
332+
has a 'devices {' section and no conflicting global_filter.
333+
success_msg: Host LVM global_filter is active.
334+
284335
- name: Configure sysctl parameters
285336
ansible.posix.sysctl:
286337
name: "{{ item.name }}"

examples/suse/prepare-suse.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,57 @@
276276
}
277277
notify: Restart multipathd
278278

279+
# Exclude DRBD, device-mapper, zvol and loop devices from the host's
280+
# LVM device scanning. Without this the host LVM may scan and activate
281+
# volume groups backed by LINSTOR/DRBD volumes — or located inside
282+
# loop-mounted images — making them unavailable to the satellite.
283+
# Mirrors the global_filter shipped in the Talos machine config.
284+
# Override cozystack_lvm_global_filter from inventory on hosts whose
285+
# own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so
286+
# they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry.
287+
- name: Set the default host LVM global_filter
288+
ansible.builtin.set_fact:
289+
_cozystack_lvm_default_filter:
290+
- "r|^/dev/drbd.*|"
291+
- "r|^/dev/dm-.*|"
292+
- "r|^/dev/zd.*|"
293+
- "r|^/dev/loop.*|"
294+
295+
- name: Exclude virtual and loop devices from host LVM scanning
296+
ansible.builtin.lineinfile:
297+
path: /etc/lvm/lvm.conf
298+
regexp: '^\s*#?\s*global_filter\s*='
299+
insertafter: '^\s*devices\s*{'
300+
line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}'
301+
302+
# Confirm LVM honours the filter and applied the requested list.
303+
# `lvmconfig devices/global_filter` reports the value only when it sits
304+
# inside the devices {} block, so a filter that landed elsewhere (an
305+
# lvm.conf with no devices section, or a stale duplicate outside the block
306+
# that won the lineinfile match) is caught here instead of silently
307+
# leaving the host LVM unfiltered or running an old filter.
308+
- name: Read back the effective host LVM global_filter
309+
ansible.builtin.command:
310+
cmd: lvmconfig devices/global_filter
311+
register: _cozystack_lvm_filter_check
312+
changed_when: false
313+
failed_when: false
314+
315+
- name: Fail if the host LVM does not honour the configured global_filter
316+
ansible.builtin.assert:
317+
that:
318+
- _cozystack_lvm_filter_check.rc == 0
319+
- _cozystack_lvm_filter_check.rc != 0 or
320+
((_cozystack_lvm_filter_check.stdout | regex_replace('^global_filter=', '') | from_json | sort)
321+
== ((cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter)) | sort))
322+
fail_msg: >-
323+
LVM does not report the configured global_filter as active
324+
(rc={{ _cozystack_lvm_filter_check.rc }}, value
325+
'{{ _cozystack_lvm_filter_check.stdout }}'). It likely landed outside
326+
the devices {} block or a stale entry won — ensure /etc/lvm/lvm.conf
327+
has a 'devices {' section and no conflicting global_filter.
328+
success_msg: Host LVM global_filter is active.
329+
279330
- name: Configure sysctl parameters
280331
ansible.posix.sysctl:
281332
name: "{{ item.name }}"

examples/ubuntu/prepare-ubuntu.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,57 @@
322322
}
323323
notify: Restart multipathd
324324

325+
# Exclude DRBD, device-mapper, zvol and loop devices from the host's
326+
# LVM device scanning. Without this the host LVM may scan and activate
327+
# volume groups backed by LINSTOR/DRBD volumes — or located inside
328+
# loop-mounted images — making them unavailable to the satellite.
329+
# Mirrors the global_filter shipped in the Talos machine config.
330+
# Override cozystack_lvm_global_filter from inventory on hosts whose
331+
# own PVs sit on device-mapper devices (LVM-on-LUKS, multipath) so
332+
# they are not filtered out — e.g. drop the "r|^/dev/dm-.*|" entry.
333+
- name: Set the default host LVM global_filter
334+
ansible.builtin.set_fact:
335+
_cozystack_lvm_default_filter:
336+
- "r|^/dev/drbd.*|"
337+
- "r|^/dev/dm-.*|"
338+
- "r|^/dev/zd.*|"
339+
- "r|^/dev/loop.*|"
340+
341+
- name: Exclude virtual and loop devices from host LVM scanning
342+
ansible.builtin.lineinfile:
343+
path: /etc/lvm/lvm.conf
344+
regexp: '^\s*#?\s*global_filter\s*='
345+
insertafter: '^\s*devices\s*{'
346+
line: ' global_filter = {{ cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter) | to_json }}'
347+
348+
# Confirm LVM honours the filter and applied the requested list.
349+
# `lvmconfig devices/global_filter` reports the value only when it sits
350+
# inside the devices {} block, so a filter that landed elsewhere (an
351+
# lvm.conf with no devices section, or a stale duplicate outside the block
352+
# that won the lineinfile match) is caught here instead of silently
353+
# leaving the host LVM unfiltered or running an old filter.
354+
- name: Read back the effective host LVM global_filter
355+
ansible.builtin.command:
356+
cmd: lvmconfig devices/global_filter
357+
register: _cozystack_lvm_filter_check
358+
changed_when: false
359+
failed_when: false
360+
361+
- name: Fail if the host LVM does not honour the configured global_filter
362+
ansible.builtin.assert:
363+
that:
364+
- _cozystack_lvm_filter_check.rc == 0
365+
- _cozystack_lvm_filter_check.rc != 0 or
366+
((_cozystack_lvm_filter_check.stdout | regex_replace('^global_filter=', '') | from_json | sort)
367+
== ((cozystack_lvm_global_filter | default(_cozystack_lvm_default_filter)) | sort))
368+
fail_msg: >-
369+
LVM does not report the configured global_filter as active
370+
(rc={{ _cozystack_lvm_filter_check.rc }}, value
371+
'{{ _cozystack_lvm_filter_check.stdout }}'). It likely landed outside
372+
the devices {} block or a stale entry won — ensure /etc/lvm/lvm.conf
373+
has a 'devices {' section and no conflicting global_filter.
374+
success_msg: Host LVM global_filter is active.
375+
325376
- name: Configure sysctl parameters
326377
ansible.posix.sysctl:
327378
name: "{{ item.name }}"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
# One global_filter scenario, included once per entry in _lvm_cases. Builds a
3+
# throwaway lvm.conf, runs the same lineinfile the prepare playbooks use, then
4+
# asks LVM itself (lvmconfig + LVM_SYSTEM_DIR) whether the filter is effective
5+
# — exactly the post-write check the playbooks run. Expects these play vars:
6+
# _lvm_base, _lvm_regexp, _lvm_insertafter, _lvm_filter_line, _lvm_default_list.
7+
- name: Create the {{ item.name }} fixture directory
8+
ansible.builtin.file:
9+
path: "{{ _lvm_base }}/{{ item.name }}"
10+
state: directory
11+
mode: "0755"
12+
13+
- name: Write the {{ item.name }} lvm.conf fixture
14+
ansible.builtin.copy:
15+
dest: "{{ _lvm_base }}/{{ item.name }}/lvm.conf"
16+
mode: "0644"
17+
content: "{{ item.content }}"
18+
19+
- name: Apply global_filter to the {{ item.name }} fixture (first run)
20+
ansible.builtin.lineinfile:
21+
path: "{{ _lvm_base }}/{{ item.name }}/lvm.conf"
22+
regexp: "{{ _lvm_regexp }}"
23+
insertafter: "{{ _lvm_insertafter }}"
24+
line: "{{ _lvm_filter_line }}"
25+
vars:
26+
cozystack_lvm_global_filter: "{{ item.override | default(_lvm_default_list) }}"
27+
register: _case_first
28+
29+
- name: Apply global_filter to the {{ item.name }} fixture (second run)
30+
ansible.builtin.lineinfile:
31+
path: "{{ _lvm_base }}/{{ item.name }}/lvm.conf"
32+
regexp: "{{ _lvm_regexp }}"
33+
insertafter: "{{ _lvm_insertafter }}"
34+
line: "{{ _lvm_filter_line }}"
35+
vars:
36+
cozystack_lvm_global_filter: "{{ item.override | default(_lvm_default_list) }}"
37+
register: _case_second
38+
39+
- name: Ask LVM whether the {{ item.name }} global_filter is effective
40+
ansible.builtin.command:
41+
cmd: lvmconfig devices/global_filter
42+
environment:
43+
LVM_SYSTEM_DIR: "{{ _lvm_base }}/{{ item.name }}"
44+
register: _case_lvm
45+
changed_when: false
46+
failed_when: false
47+
48+
- name: Assert the {{ item.name }} case behaves as expected
49+
ansible.builtin.assert:
50+
that:
51+
# The task is idempotent regardless of where the line landed.
52+
- _case_first is changed
53+
- _case_second is not changed
54+
# lvmconfig returns rc 0 only when global_filter sits inside devices {}.
55+
- item.effective == (_case_lvm.rc == 0)
56+
# When effective, LVM must report exactly the configured list — except
57+
# the stale-duplicate case, where a pre-existing in-block filter wins the
58+
# lineinfile race and the configured list must NOT match. That mismatch
59+
# is exactly what the playbooks' value comparison rejects.
60+
- (not item.effective) or
61+
(((_case_lvm.stdout | regex_replace('^global_filter=', '') | from_json | sort)
62+
== ((item.override | default(_lvm_default_list)) | sort))
63+
!= (item.stale | default(false)))
64+
fail_msg: >-
65+
Case {{ item.name }}: expected effective={{ item.effective }}, got
66+
rc={{ _case_lvm.rc }} stdout='{{ _case_lvm.stdout }}'.
67+
success_msg: Case {{ item.name }} behaves as expected.

0 commit comments

Comments
 (0)