From 1c7e0ec61f3c4f7c486d68ce7688649ec1f69dcb Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 8 Aug 2026 23:08:20 +0200 Subject: [PATCH 01/12] fix: verbleibende Sysctl-Doppeldefinitionen entfernen --- roles/sysctl/tasks/main.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/roles/sysctl/tasks/main.yml b/roles/sysctl/tasks/main.yml index 60a17d6..2b95113 100644 --- a/roles/sysctl/tasks/main.yml +++ b/roles/sysctl/tasks/main.yml @@ -16,23 +16,19 @@ loop: - /etc/sysctl.conf - /etc/sysctl.d/99-streaming.conf + - /etc/sysctl.d/99-proxmox-tuning.conf + - /etc/sysctl.d/99-zzz-cloudflare-warp-connector.conf register: legacy_sysctl_files -- name: Verwaltete Werte aus Legacy sysctl.conf entfernen +- name: Verwaltete Werte aus konkurrierenden Sysctl-Quellen entfernen ansible.builtin.lineinfile: - path: /etc/sysctl.conf - regexp: "^[ \\t]*{{ item.key | regex_escape }}[ \\t]*=" + path: "{{ item.0.item }}" + regexp: "^[ \\t]*{{ item.1.key | regex_escape }}[ \\t]*=" state: absent - loop: "{{ sysctl_params }}" - when: legacy_sysctl_files.results[0].stat.exists - -- name: Verwaltete Werte aus Legacy 99-streaming.conf entfernen - ansible.builtin.lineinfile: - path: /etc/sysctl.d/99-streaming.conf - regexp: "^[ \\t]*{{ item.key | regex_escape }}[ \\t]*=" - state: absent - loop: "{{ sysctl_params }}" - when: legacy_sysctl_files.results[1].stat.exists + loop: "{{ legacy_sysctl_files.results | product(sysctl_params) | list }}" + loop_control: + label: "{{ item.0.item }}: {{ item.1.key }}" + when: item.0.stat.exists - name: Sysctl Parameter kanonisch setzen ansible.posix.sysctl: From 184476e7726a38b20359aa9b3fbef5a7e421a745 Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 8 Aug 2026 23:08:21 +0200 Subject: [PATCH 02/12] fix: verbleibende Sysctl-Doppeldefinitionen entfernen --- roles/sysctl_proxmox/tasks/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/sysctl_proxmox/tasks/main.yml b/roles/sysctl_proxmox/tasks/main.yml index 6b9bbf8..89f046d 100644 --- a/roles/sysctl_proxmox/tasks/main.yml +++ b/roles/sysctl_proxmox/tasks/main.yml @@ -38,7 +38,6 @@ state: present loop: - { key: vm.overcommit_memory, value: "1" } - - { key: vm.swappiness, value: "1" } - { key: fs.file-max, value: "9999999" } - { key: fs.inotify.max_user_watches, value: "524288" } - { key: fs.inotify.max_user_instances, value: "512" } From 11b6273f428fb661b5201faa0e8978141d5db4a7 Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 8 Aug 2026 23:08:22 +0200 Subject: [PATCH 03/12] fix: verbleibende Sysctl-Doppeldefinitionen entfernen --- tests/test_sysctl_role.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_sysctl_role.py b/tests/test_sysctl_role.py index 3be0742..b93a0f1 100644 --- a/tests/test_sysctl_role.py +++ b/tests/test_sysctl_role.py @@ -1,4 +1,5 @@ from pathlib import Path +import re import unittest ROOT = Path(__file__).parents[1] @@ -17,6 +18,8 @@ class SysctlRoleContract(unittest.TestCase): text = (ROOT / "roles/sysctl/tasks/main.yml").read_text() self.assertIn("/etc/sysctl.conf", text) self.assertIn("/etc/sysctl.d/99-streaming.conf", text) + self.assertIn("/etc/sysctl.d/99-proxmox-tuning.conf", text) + self.assertIn("/etc/sysctl.d/99-zzz-cloudflare-warp-connector.conf", text) self.assertIn("state: absent", text) self.assertIn("regex_escape", text) @@ -25,9 +28,11 @@ class SysctlRoleContract(unittest.TestCase): self.assertLess(play.index("- sysctl\n"), play.index("- sysctl_proxmox")) def test_proxmox_role_does_not_duplicate_canonical_network_keys(self): - text = (ROOT / "roles/sysctl_proxmox/tasks/main.yml").read_text() - for key in ("net.core.rmem_max", "net.core.wmem_max", "net.ipv4.tcp_rmem", "net.ipv4.tcp_wmem", "net.ipv4.tcp_mtu_probing"): - self.assertNotIn(key, text) + canonical = (ROOT / "roles/sysctl/defaults/main.yml").read_text() + proxmox = (ROOT / "roles/sysctl_proxmox/tasks/main.yml").read_text() + canonical_keys = set(re.findall(r"key:\s*([a-z0-9_.-]+)", canonical)) + proxmox_keys = set(re.findall(r"key:\s*([a-z0-9_.-]+)", proxmox)) + self.assertEqual(canonical_keys & proxmox_keys, set()) if __name__ == "__main__": From 85e161f9926554d260d256ab579f76f205f09950 Mon Sep 17 00:00:00 2001 From: sascha Date: Thu, 13 Aug 2026 17:24:59 +0200 Subject: [PATCH 04/12] Update roles/sysctl/defaults/main.yml for streaming path tuning --- roles/sysctl/defaults/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roles/sysctl/defaults/main.yml b/roles/sysctl/defaults/main.yml index ac69319..87f88e3 100644 --- a/roles/sysctl/defaults/main.yml +++ b/roles/sysctl/defaults/main.yml @@ -16,8 +16,9 @@ sysctl_params: - { key: net.ipv4.tcp_fastopen, value: "3" } - { key: net.core.netdev_max_backlog, value: "16384" } - { key: net.core.somaxconn, value: "4096" } + - { key: net.core.optmem_max, value: "2097152" } - { key: net.ipv4.tcp_notsent_lowat, value: "16384" } - - { key: net.ipv4.tcp_fin_timeout, value: "15" } + - { key: net.ipv4.tcp_fin_timeout, value: "30" } - { key: net.ipv4.tcp_tw_reuse, value: "1" } - { key: vm.swappiness, value: "1" } - { key: vm.dirty_ratio, value: "15" } From 8e37300c928578c70767250f2370eb89aa7e70cd Mon Sep 17 00:00:00 2001 From: sascha Date: Thu, 13 Aug 2026 17:24:59 +0200 Subject: [PATCH 05/12] Update pfannkuchen.ini for streaming path tuning --- pfannkuchen.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pfannkuchen.ini b/pfannkuchen.ini index 6cd6201..9d72c0b 100644 --- a/pfannkuchen.ini +++ b/pfannkuchen.ini @@ -52,6 +52,7 @@ butler ansible_host=10.5.85.2 [hetzner] pfannkuchen ansible_host=46.225.230.72 ansible_user=root +guck-vps ansible_host=141.94.237.199 ansible_user=debian [nvidia] tdarr From 7a33a9628cb9a3730af185f5bc00a5d8ff1b78a6 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:47:52 +0200 Subject: [PATCH 06/12] =?UTF-8?q?NFS-Stabilisierung:=20roles/nfs=5Fstabili?= =?UTF-8?q?ty/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 07/12] =?UTF-8?q?NFS-Stabilisierung:=20tests/test=5Fnfs=5F?= =?UTF-8?q?stability=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 08/12] 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 From 69e46939e6630463aecd263e3c31f01a6a474c0b Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:49:13 +0200 Subject: [PATCH 09/12] NFS-Kommando: pfannkuchen.sh aktualisieren --- pfannkuchen.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pfannkuchen.sh b/pfannkuchen.sh index 6e91824..7f0955b 100755 --- a/pfannkuchen.sh +++ b/pfannkuchen.sh @@ -23,6 +23,7 @@ usage() { echo -e " ${C}wstunnel${N} [host] wstunnel + WireGuard deployen" echo -e " ${C}sshfs${N} [host] SSHFS Mounts einrichten" echo -e " ${C}tune${N} Sysctl Netzwerk-Tuning" + echo -e " ${C}nfs${N} NFS-/mergerfs-Stabilisierung ausrollen" echo -e " ${C}pvetune${N} [host] Proxmox Host Tuning (sysctl, resolv, hosts)" echo -e " ${C}tc${N} [host] tc per-flow Rate-Limit (50 Mbit/s pro Connection)" echo -e " ${C}watchdog${N} Network Watchdog deployen (Whitelist aus Ansible)" @@ -103,6 +104,10 @@ case "$CMD" in [ -z "$HOST" ] && echo -e "${R}Fehler: Host angeben${N}" && exit 1 run sysctl.yaml -l "$HOST" ;; + nfs) + [ -z "$HOST" ] && echo -e "${R}Fehler: Host angeben${N}" && exit 1 + run nfs-stability.yml -l "$HOST" + ;; pvetune) if [ -n "$HOST" ]; then run sysctl-proxmox.yaml -l "$HOST" From 89dd3dc203f927278d7ee1f910fa70314d391f56 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:49:14 +0200 Subject: [PATCH 10/12] =?UTF-8?q?NFS-Kommando:=20nfs-stability.yml=20hinzu?= =?UTF-8?q?f=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nfs-stability.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 nfs-stability.yml diff --git a/nfs-stability.yml b/nfs-stability.yml new file mode 100644 index 0000000..c5c3014 --- /dev/null +++ b/nfs-stability.yml @@ -0,0 +1,6 @@ +--- +- name: NFS and mergerfs stability + hosts: emby-sascha:emby-chris:arrapps + become: yes + roles: + - nfs_stability From 4fa6a9b0a05d4fe1cba47bf60c300f0e4e459560 Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 06:53:34 +0200 Subject: [PATCH 11/12] =?UTF-8?q?NFS-Recovery=20vor=20Verifikation=20synch?= =?UTF-8?q?ron=20ausf=C3=BChren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- roles/nfs_stability/tasks/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/roles/nfs_stability/tasks/main.yml b/roles/nfs_stability/tasks/main.yml index f8b86cc..8073ba6 100644 --- a/roles/nfs_stability/tasks/main.yml +++ b/roles/nfs_stability/tasks/main.yml @@ -106,6 +106,12 @@ enabled: true state: started +- name: Run the safe helper once to recover mounts left missing by the legacy loop + ansible.builtin.command: + argv: + - /usr/local/sbin/remount-nfs-safe + changed_when: false + - name: Verify systemd unit dependency graph ansible.builtin.command: argv: From 707f7959312d520e372e784817c90873ccca3afa Mon Sep 17 00:00:00 2001 From: sascha Date: Fri, 14 Aug 2026 10:40:20 +0200 Subject: [PATCH 12/12] Stillgelegten Outline-Host aus Inventory entfernen --- pfannkuchen.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfannkuchen.ini b/pfannkuchen.ini index 9d72c0b..943d6b7 100644 --- a/pfannkuchen.ini +++ b/pfannkuchen.ini @@ -40,7 +40,7 @@ n8n ansible_host=10.4.1.113 hermes ansible_host=10.4.1.100 monitoring ansible_host=10.1.1.111 automation1 ansible_host=10.5.85.5 -outline ansible_host=10.1.1.100 + satisfactory ansible_host=10.3.1.120 [communication]