Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,27 @@ 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: 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:
name: E2E
runs-on: ubuntu-latest
Expand Down
19 changes: 12 additions & 7 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,13 +19,17 @@ 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).

Unreleased
==========

Bugfixes
--------

- 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. 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). 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
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ 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. 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`:
Expand Down Expand Up @@ -362,6 +376,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. |
Expand Down
51 changes: 51 additions & 0 deletions examples/rhel/prepare-rhel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,57 @@
}
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.
# 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(_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
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
- _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 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
ansible.posix.sysctl:
name: "{{ item.name }}"
Expand Down
51 changes: 51 additions & 0 deletions examples/suse/prepare-suse.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,57 @@
}
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.
# 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(_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
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
- _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 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
ansible.posix.sysctl:
name: "{{ item.name }}"
Expand Down
51 changes: 51 additions & 0 deletions examples/ubuntu/prepare-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,57 @@
}
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.
# 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(_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
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
- _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 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
ansible.posix.sysctl:
name: "{{ item.name }}"
Expand Down
67 changes: 67 additions & 0 deletions tests/tasks-lvm-global-filter-case.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
# 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 — 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)
== ((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 }}'.
success_msg: Case {{ item.name }} behaves as expected.
Loading