From add75d74e9f04c56c9d21de5c342b6fce3d53caf Mon Sep 17 00:00:00 2001 From: Fabio Scotto di Santolo Date: Thu, 17 Sep 2026 00:06:46 +0200 Subject: [PATCH] refactor(atlas): separate service and host backup datasets --- AGENTS.md | 12 ++--- README.md | 16 +++---- ansible/inventory/host_vars/atlas.yml | 15 ++++--- ansible/roles/profile_atlas/defaults/main.yml | 22 +++++----- ansible/roles/profile_atlas/tasks/storage.yml | 44 ++++++++++--------- 5 files changed, 56 insertions(+), 53 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4ccb350..276f0e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,10 +139,10 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i - Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled, `vault_atlas_samba_password`. The future rootful media stack also requires `vault_atlas_immich_db_password`. Never print these values. -- Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `work`, `archive`, - `archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`, - `media/photobook`, `backups`, `backups/services`, and `backup_prometheus`. `backups/services` has a `500G` - refreservation. There is no separate legacy `zpool/syncthing` dataset. +- Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `archive`, + `services`, `services/data`, `services/data/navidrome`, `services/data/syncthing`, `media`, `media/music`, + `media/photobook`, `backup`, `backup/hosts`, and `backup/hosts/prometheus`. `backup` has a `500G` + reservation covering its descendants. `archive` is the SMB-shared raw-data namespace; container state is never beneath it. - The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive `/dev/dri`, while the Photobook external library is read-only at `/external/photobook`. @@ -154,7 +154,7 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i - `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome `0.63.2` uses SQLite below `/data` and does not support `ND_DATABASE_URL` or an external PostgreSQL backend; do not recreate the obsolete Prometheus `navidromedb` service. The role requires the storage role's `zpool/media/music`, - `zpool/archive/app_data`, `zpool/archive/app_data/navidrome`, and `zpool/archive/app_data/syncthing` datasets at + `zpool/services/data`, `zpool/services/data/navidrome`, and `zpool/services/data/syncthing` datasets at their exact paths. It never creates the pool. - Keep `backend_phase1_start_services` false until the stopped Prometheus Navidrome data directory has been copied to Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application @@ -176,7 +176,7 @@ the Prometheus--Atlas WireGuard path are operational. Aegis has validated NFSv4. and `all_squash` mapping to UID/GID `1100` end-to-end. - Complete the Phase 1 Navidrome cutover: stop the Prometheus writer, copy and verify its complete `/opt/navidrome/data/` directory (including SQLite sidecars) under - `/zpool/archive/app_data/navidrome/`, then set `backend_phase1_start_services: true` and validate + `/zpool/services/data/navidrome/`, then set `backend_phase1_start_services: true` and validate Navidrome on Atlas through WireGuard. Do not delete the source until a restore test succeeds. - Start and validate the rendered Syncthing Quadlet only after its device IDs, star topology, folders, folder modes, ignore rules, and GUI/API protection are declared. Validate its GUI and native transfer diff --git a/README.md b/README.md index 652beda..9de413b 100644 --- a/README.md +++ b/README.md @@ -263,11 +263,11 @@ paths, and the Immich database secret are validated. Atlas reads its declared SS separate files below `~/.ssh/authorized_keys.d/`. With storage management enabled, Atlas creates the complete dataset hierarchy below the existing or -explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and -`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`, -`backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`, -while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G` -refreservation. Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, +explicitly bootstrapped `zpool`: SMB-shared `archive`, private `services/data` with separate +`services/data/navidrome` and `services/data/syncthing` application datasets, `media`, `media/music`, +`media/photobook`, and `backup/hosts/prometheus`. Application/archive datasets use `zstd`, while media, +Syncthing, and host-backup datasets use `lz4`; `backup` has a `500G` reservation covering its descendants. +Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, agent and remote forwarding are disabled, while local forwarding remains available for private administrative tunnels. SMB3 exposes `Archive` only to the configured Vault-backed Samba accounts on encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using @@ -285,8 +285,8 @@ not support `ND_DATABASE_URL` or an external PostgreSQL backend. The obsolete `n was therefore removed from Prometheus instead of being reproduced on Atlas. The role derives all storage paths from the `zpool` mounted at `/zpool`: music is read-only at `/zpool/media/music`, Navidrome application state and `navidrome.db` are stored at -`/zpool/archive/app_data/navidrome`, and Syncthing persists at -`/zpool/archive/app_data/syncthing`. `profile_atlas` creates these datasets when +`/zpool/services/data/navidrome`, and Syncthing persists at +`/zpool/services/data/syncthing`. `profile_atlas` creates these datasets when `atlas_manage_storage` is enabled; the backend role verifies their exact mountpoints before starting containers. The backend role never creates the pool. The separate `wireguard_overlay` role manages `wg0` between Prometheus (`10.0.0.1`) and Atlas (`10.0.0.2`), generating private keys once @@ -298,7 +298,7 @@ connectivity. Backend ports are admitted only in the WireGuard firewalld zone. `backend_phase1_start_services` stays false during the application-state transfer, so the first real backend run renders the Quadlets without creating an empty Atlas database. After stopping Navidrome on Prometheus, copy the complete `/opt/navidrome/data/` directory into -`/zpool/archive/app_data/navidrome/`, preserving `navidrome.db` and any SQLite sidecar files. Then set +`/zpool/services/data/navidrome/`, preserving `navidrome.db` and any SQLite sidecar files. Then set this variable to true and rerun the role to enable and start Navidrome and Syncthing. The playbook never copies or deletes application data. diff --git a/ansible/inventory/host_vars/atlas.yml b/ansible/inventory/host_vars/atlas.yml index 0b3b129..3e7c443 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -35,17 +35,18 @@ atlas_zpool_disks: - /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK - /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM atlas_create_pool: false -atlas_zfs_dataset_work: work atlas_zfs_dataset_archive: archive -atlas_zfs_dataset_app_data: archive/app_data -atlas_zfs_dataset_navidrome: archive/app_data/navidrome -atlas_zfs_dataset_syncthing: archive/app_data/syncthing +atlas_zfs_dataset_services: services +atlas_zfs_dataset_app_data: services/data +atlas_zfs_dataset_navidrome: services/data/navidrome +atlas_zfs_dataset_syncthing: services/data/syncthing atlas_zfs_dataset_media: media atlas_zfs_dataset_music: media/music -atlas_zfs_dataset_backup_prometheus: backup_prometheus +atlas_zfs_dataset_backup: backup +atlas_zfs_dataset_host_backups: backup/hosts +atlas_zfs_dataset_backup_prometheus: backup/hosts/prometheus +atlas_zfs_backup_reservation: 500G atlas_zfs_dataset_photobook: media/photobook -atlas_zfs_dataset_backups: backups -atlas_zfs_dataset_service_backups: backups/services atlas_mount_root: /zpool atlas_manage_storage: true atlas_manage_sharing: true diff --git a/ansible/roles/profile_atlas/defaults/main.yml b/ansible/roles/profile_atlas/defaults/main.yml index a2cbc8e..2f1f4c7 100644 --- a/ansible/roles/profile_atlas/defaults/main.yml +++ b/ansible/roles/profile_atlas/defaults/main.yml @@ -47,31 +47,31 @@ atlas_hardening_sysctls: net.ipv6.conf.all.accept_source_route: "0" net.ipv6.conf.default.accept_source_route: "0" atlas_zfs_pool: CHANGEME_ZFS_POOL -atlas_zfs_dataset_work: work atlas_zfs_dataset_archive: archive -atlas_zfs_dataset_app_data: archive/app_data -atlas_zfs_dataset_navidrome: archive/app_data/navidrome -atlas_zfs_dataset_syncthing: archive/app_data/syncthing +atlas_zfs_dataset_services: services +atlas_zfs_dataset_app_data: services/data +atlas_zfs_dataset_navidrome: services/data/navidrome +atlas_zfs_dataset_syncthing: services/data/syncthing atlas_zfs_dataset_media: media atlas_zfs_dataset_music: media/music -atlas_zfs_dataset_backup_prometheus: backup_prometheus +atlas_zfs_dataset_backup: backup +atlas_zfs_dataset_host_backups: backup/hosts +atlas_zfs_dataset_backup_prometheus: backup/hosts/prometheus +atlas_zfs_backup_reservation: 500G atlas_zfs_dataset_photobook: media/photobook -atlas_zfs_dataset_backups: backups -atlas_zfs_dataset_service_backups: backups/services -atlas_zfs_service_backups_refreservation: 500G atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}" +atlas_services_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_services }}" atlas_app_data_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}" atlas_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}" atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}" atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}" atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}" -atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}" +atlas_backup_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup }}" +atlas_host_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_host_backups }}" atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}" atlas_photobook_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_photobook }}" -atlas_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backups }}" -atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}" atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo diff --git a/ansible/roles/profile_atlas/tasks/storage.yml b/ansible/roles/profile_atlas/tasks/storage.yml index 760e23a..195eb58 100644 --- a/ansible/roles/profile_atlas/tasks/storage.yml +++ b/ansible/roles/profile_atlas/tasks/storage.yml @@ -3,12 +3,6 @@ tags: [atlas, storage] ansible.builtin.set_fact: atlas_zfs_datasets: - - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}" - mountpoint: "{{ atlas_work_mountpoint }}" - owner: "{{ atlas_admin_username }}" - group: "{{ atlas_admin_group }}" - properties: - mountpoint: "{{ atlas_work_mountpoint }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}" mountpoint: "{{ atlas_archive_mountpoint }}" owner: "{{ atlas_admin_username }}" @@ -16,6 +10,13 @@ properties: compression: zstd mountpoint: "{{ atlas_archive_mountpoint }}" + - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_services }}" + mountpoint: "{{ atlas_services_mountpoint }}" + owner: "{{ atlas_admin_username }}" + group: "{{ atlas_admin_group }}" + properties: + compression: zstd + mountpoint: "{{ atlas_services_mountpoint }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}" mountpoint: "{{ atlas_app_data_mountpoint }}" owner: "{{ atlas_admin_username }}" @@ -51,11 +52,27 @@ properties: compression: lz4 mountpoint: "{{ atlas_music_mountpoint }}" + - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup }}" + mountpoint: "{{ atlas_backup_mountpoint }}" + owner: "{{ atlas_admin_username }}" + group: "{{ atlas_admin_group }}" + properties: + compression: lz4 + mountpoint: "{{ atlas_backup_mountpoint }}" + reservation: "{{ atlas_zfs_backup_reservation }}" + - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_host_backups }}" + mountpoint: "{{ atlas_host_backups_mountpoint }}" + owner: "{{ atlas_admin_username }}" + group: "{{ atlas_admin_group }}" + properties: + compression: lz4 + mountpoint: "{{ atlas_host_backups_mountpoint }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}" mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" owner: "{{ atlas_admin_username }}" group: "{{ atlas_admin_group }}" properties: + compression: lz4 mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_photobook }}" mountpoint: "{{ atlas_photobook_mountpoint }}" @@ -64,21 +81,6 @@ properties: compression: lz4 mountpoint: "{{ atlas_photobook_mountpoint }}" - - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backups }}" - mountpoint: "{{ atlas_backups_mountpoint }}" - owner: "{{ atlas_admin_username }}" - group: "{{ atlas_admin_group }}" - properties: - compression: lz4 - mountpoint: "{{ atlas_backups_mountpoint }}" - - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}" - mountpoint: "{{ atlas_service_backups_mountpoint }}" - owner: "{{ atlas_admin_username }}" - group: "{{ atlas_admin_group }}" - properties: - compression: lz4 - mountpoint: "{{ atlas_service_backups_mountpoint }}" - refreservation: "{{ atlas_zfs_service_backups_refreservation }}" - name: Require completed Atlas storage placeholders tags: [atlas, storage]