From 7a33a9628cb9a3730af185f5bc00a5d8ff1b78a6 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:47:52 +0200 Subject: [PATCH 1/3] =?UTF-8?q?NFS-Stabilisierung:=20roles/nfs=5Fstability?= =?UTF-8?q?/tasks/main.yml=20hinzuf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roles/nfs_stability/tasks/main.yml | 128 +++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 roles/nfs_stability/tasks/main.yml diff --git a/roles/nfs_stability/tasks/main.yml b/roles/nfs_stability/tasks/main.yml new file mode 100644 index 0000000..f8b86cc --- /dev/null +++ b/roles/nfs_stability/tasks/main.yml @@ -0,0 +1,128 @@ +--- +- name: Ensure this role only targets the three mergerfs media hosts + ansible.builtin.assert: + that: + - inventory_hostname in ['emby-sascha', 'emby-chris', 'arrapps'] + fail_msg: "nfs_stability must not run on {{ inventory_hostname }}" + +- name: Stop and disable the destructive legacy retry timer first + ansible.builtin.systemd: + name: remount-nfs.timer + state: stopped + enabled: false + failed_when: false + +- name: Stop a currently running destructive legacy retry service + ansible.builtin.systemd: + name: remount-nfs.service + state: stopped + failed_when: false + +- name: Mark mergerfs as a network-dependent mount without remounting it live + ansible.builtin.replace: + path: /etc/systemd/system/mnt-media.mount + regexp: '^Options=(?![^\n]*_netdev)(.*)$' + replace: 'Options=_netdev,\1' + backup: true + register: mergerfs_unit + +- name: Install non-destructive one-shot NFS recovery helper + ansible.builtin.copy: + dest: /usr/local/sbin/remount-nfs-safe + owner: root + group: root + mode: '0755' + content: | + #!/bin/bash + set -u + is_nfs() { + findmnt -rn --target "$1" -t nfs,nfs4 -o TARGET | grep -Fxq "$1" + } + is_media() { + findmnt -rn --target /mnt/media -t fuse.mergerfs -o TARGET | grep -Fxq /mnt/media + } + for attempt in 1 2 3; do + missing=0 + for entry in 'mnt-nas.mount:/mnt/nas' 'mnt-qnap1.mount:/mnt/qnap1' 'mnt-qnap2.mount:/mnt/qnap2'; do + unit="${entry%%:*}" + target="${entry#*:}" + if ! is_nfs "$target"; then + systemctl start "$unit" || true + fi + is_nfs "$target" || missing=1 + done + if [ "$missing" -eq 0 ]; then + if ! is_media; then + systemctl start mnt-media.mount || true + fi + is_media && exit 0 + fi + sleep 10 + done + echo 'NFS recovery failed without restarting any active mount' >&2 + exit 1 + +- name: Install safe one-shot NFS recovery service + ansible.builtin.copy: + dest: /etc/systemd/system/remount-nfs.service + owner: root + group: root + mode: '0644' + backup: true + content: | + [Unit] + Description=Safely start missing NFS mounts once after boot + After=network-online.target + Wants=network-online.target + + [Service] + Type=oneshot + ExecStart=/usr/local/sbin/remount-nfs-safe + +- name: Install once-per-boot NFS recovery timer + ansible.builtin.copy: + dest: /etc/systemd/system/remount-nfs.timer + owner: root + group: root + mode: '0644' + backup: true + content: | + [Unit] + Description=One-shot NFS mount validation after boot + + [Timer] + OnBootSec=45 + AccuracySec=5 + Persistent=false + Unit=remount-nfs.service + + [Install] + WantedBy=timers.target + +- name: Reload systemd and enable the one-shot boot timer + ansible.builtin.systemd: + daemon_reload: true + name: remount-nfs.timer + enabled: true + state: started + +- name: Verify systemd unit dependency graph + ansible.builtin.command: + argv: + - systemd-analyze + - verify + - /etc/systemd/system/mnt-media.mount + - /etc/systemd/system/remount-nfs.service + - /etc/systemd/system/remount-nfs.timer + changed_when: false + +- name: Verify all active storage mounts without restarting them + ansible.builtin.shell: | + set -e + for target in /mnt/nas /mnt/qnap1 /mnt/qnap2; do + findmnt -rn --target "$target" -t nfs,nfs4 -o TARGET | grep -Fxq "$target" + done + findmnt -rn --target /mnt/media -t fuse.mergerfs -o TARGET | grep -Fxq /mnt/media + args: + executable: /bin/bash + changed_when: false From c8485ac04c276d08dd53388d26a3ff2c0b2a2426 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:47:53 +0200 Subject: [PATCH 2/3] =?UTF-8?q?NFS-Stabilisierung:=20tests/test=5Fnfs=5Fst?= =?UTF-8?q?ability=5Frole.py=20hinzuf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_nfs_stability_role.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_nfs_stability_role.py diff --git a/tests/test_nfs_stability_role.py b/tests/test_nfs_stability_role.py new file mode 100644 index 0000000..820ddfd --- /dev/null +++ b/tests/test_nfs_stability_role.py @@ -0,0 +1,27 @@ +from pathlib import Path + +ROLE = Path("roles/nfs_stability/tasks/main.yml").read_text() +SITE = Path("site.yml").read_text() + + +def test_recovery_is_non_destructive(): + assert "systemctl restart" not in ROLE + assert "OnUnitActiveSec" not in ROLE + assert "systemctl start" in ROLE + + +def test_recovery_validates_real_nfs_and_mergerfs_mounts(): + assert "-t nfs,nfs4" in ROLE + assert "-t fuse.mergerfs" in ROLE + assert all(path in ROLE for path in ("/mnt/nas", "/mnt/qnap1", "/mnt/qnap2", "/mnt/media")) + + +def test_legacy_loop_is_stopped_before_replacement(): + stop_at = ROLE.index("Stop and disable the destructive legacy retry timer first") + install_at = ROLE.index("Install once-per-boot NFS recovery timer") + assert stop_at < install_at + + +def test_role_is_scoped_to_media_mount_hosts(): + assert "hosts: emby-sascha:emby-chris:arrapps" in SITE + assert "- nfs_stability" in SITE From 5e614727be01230d47ce7de000819f13e761a761 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:47:53 +0200 Subject: [PATCH 3/3] NFS-Stabilisierung: site.yml aktualisieren --- site.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/site.yml b/site.yml index f359ccd..6152040 100644 --- a/site.yml +++ b/site.yml @@ -1,4 +1,11 @@ --- +# Stabilize NFS/mergerfs only on hosts that use the shared media pool. +- name: NFS and mergerfs stability + hosts: emby-sascha:emby-chris:arrapps + become: yes + roles: + - nfs_stability + # Neue VM komplett einrichten - name: VM Setup hosts: all