Merge main and reconcile Atlas checklist

This commit is contained in:
Fabio Scotto di Santolo
2026-10-02 17:54:33 +02:00
8 changed files with 138 additions and 5 deletions

View File

@@ -70,6 +70,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Gitea cutover network configuration before activation:
`ansible-playbook ansible/site.yml --limit prometheus --tags gitea_cutover,prometheus_backup --check --diff -e server_gitea_on_atlas=true`
and `ansible-playbook ansible/site.yml --limit atlas --tags gitea --check --diff`
- Atlas daily Navidrome music copy:
`ansible-playbook ansible/site.yml --limit atlas --tags music_sync --check --diff`
- Atlas network/share hardening:
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
- Atlas ZFS snapshot retention and scrub timers:
@@ -177,7 +179,9 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- `profile_backend_phase1` temporarily runs rootless Navidrome and Syncthing on Atlas until Uranus replaces
them. It binds only to Atlas' LAN IP, never `wg0`; Navidrome and the Syncthing GUI admit only Aegis as
the source-NAT gateway, while native Syncthing ports admit the configured LAN. It initializes fresh
state only and never migrates or deletes source application data.
state only and never migrates or deletes source application data. The enabled rootless
`atlas-music-sync.timer` copies `/zpool/archive/Music` to `/zpool/media/music` daily at 00:45
Europe/Rome without deleting destination files; it requires both datasets to be mounted.
- `wireguard_overlay` manages `wg0` between Prometheus (`10.0.0.1`) and Aegis (`10.0.0.2`). It persists private
keys only on their respective hosts, exchanges only derived public keys through Ansible, and verifies a real peer
handshake. Prometheus opens `51820/udp`; Aegis is the LAN gateway. Its persistent IPv4 forwarding, narrowly scoped
@@ -266,13 +270,24 @@ successfully. The first monthly scrub remains a runtime check.
files are deployed; live read-only SSH, shell denial, and write denial were verified. On 2026-09-30
a manual export, Atlas pull, checksum verification, and temporary restore passed; both SQLite
databases passed integrity checks and a restored Git repository passed `git fsck`. Both daily
timers are enabled for 02:00/03:00 Europe/Rome; their first scheduled results remain unverified.
timers are enabled for 02:00/03:00 Europe/Rome. On 2026-10-01 their first scheduled export and
pull succeeded: Atlas verified the payload checksum and published `20261001T000001Z` as `latest`.
- [x] Decide whether a common SMB/NFS namespace is required: no. `Archive` (SMB) and `photobook` (NFS)
remain intentionally distinct; `docs/atlas-sharing-decision.md` records the decision. No ACL or export
change is authorized by this decision.
### Priority 3 - Service expansion
- [ ] After data protection and recovery are validated, populate `/zpool/media/music` and validate Navidrome.
- [x] Populate `/zpool/media/music` and validate Navidrome. On 2026-09-30, 21,158 files
(93,937,810,350 regular-file bytes) were copied from `/zpool/archive/Music` using a temporary
ZFS snapshot; a checksum-based rsync dry run found no differences or extra files. Navidrome saw
all files through its read-only mount, completed a scan, indexed 18,168 tracks, and responded
over HTTP. Some imported playlists still reference obsolete Windows paths. The source was left
intact and the temporary snapshot was removed.
- [x] Schedule a daily, non-deleting copy from `Archive/Music` to the separate Navidrome music
dataset. The rootless `atlas-music-sync.timer` is enabled for 00:45 Europe/Rome; a manual
idempotent service run succeeded on 2026-10-01. The first scheduled run triggered at
00:45 CEST on 2026-10-02 and exited successfully (`Result=success`, status 0); the next
run is scheduled for 2026-10-03 00:45 CEST.
- [x] Design the staged Prometheus-to-Atlas Gitea migration in `docs/atlas-gitea-migration.md`.
The approved topology keeps NPM on Prometheus and moves HTTPS and public SSH (TCP/2222) together;
Gitea runs as an `admin`-owned rootless user Quadlet on Atlas with an internal `gitea` user.

View File

@@ -311,7 +311,12 @@ l'interfaccia amministrativa resta su `127.0.0.1:81`, raggiungibile via tunnel S
Atlas ospita temporaneamente Navidrome e Syncthing rootless fino alla sostituzione con Uranus. I
servizi sono inizializzati **ex novo**, senza migrare lo stato precedente, rispettivamente sotto
`/zpool/services/data/navidrome` e `/zpool/services/data/syncthing`; la musica in
`/zpool/media/music` viene popolata separatamente. Sono vincolati all'indirizzo LAN di Atlas
`/zpool/media/music` è stata popolata separatamente da `/zpool/archive/Music` il 2026-09-30;
Navidrome ha completato la scansione. Il timer rootless `atlas-music-sync.timer` copia i file nuovi
o modificati ogni giorno alle 00:45 Europe/Rome, senza eliminare quelli presenti solo nella
destinazione; entrambi i dataset ZFS devono essere montati. La prima esecuzione schedulata è
riuscita il 2026-10-02. Alcune playlist originali contengono
ancora vecchi percorsi Windows. I servizi sono vincolati all'indirizzo LAN di Atlas
(`192.168.178.55`), mai a WireGuard. `wireguard_overlay` collega invece Prometheus (`10.0.0.1`)
e Aegis (`10.0.0.2`): le chiavi private restano sui rispettivi host e Ansible scambia solo le pubbliche.
Prometheus apre `51820/udp`; Aegis inoltra soltanto il traffico overlay→LAN dichiarato e applica
@@ -740,6 +745,7 @@ yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
ansible-playbook ansible/site.yml --limit atlas --tags music_sync --check --diff
```
## Tag supportati dal playbook

View File

@@ -296,7 +296,11 @@ Atlas temporarily hosts rootless Navidrome and Syncthing until Uranus replaces t
Atlas' LAN address (`192.168.178.55`); WireGuard remains exclusively between Prometheus (`10.0.0.1`)
and Aegis (`10.0.0.2`). Their state is initialized ex novo in `/zpool/services/data/navidrome` and
`/zpool/services/data/syncthing`; no source application state is migrated. The music library at
`/zpool/media/music` is populated separately.
`/zpool/media/music` was populated separately from `/zpool/archive/Music` on 2026-09-30;
Navidrome completed its library scan. The rootless `atlas-music-sync.timer` copies new and changed
files daily at 00:45 Europe/Rome, without deleting destination-only files. Both ZFS datasets must
be mounted. Its first scheduled run succeeded on 2026-10-02. Some source playlists still contain
obsolete Windows paths.
The Gitea move from Prometheus to Atlas is tracked in
[`docs/atlas-gitea-migration.md`](docs/atlas-gitea-migration.md). The final consistent copy runs in
@@ -731,6 +735,7 @@ ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff
ansible-playbook ansible/site.yml --limit atlas --tags music_sync --check --diff
```
## Tags

View File

@@ -151,6 +151,7 @@ backend_phase1_bind_address: "{{ ansible_host }}"
backend_phase1_firewalld_zone: "{{ atlas_firewalld_zone }}"
backend_phase1_npm_source_ip: "{{ atlas_aegis_ip }}"
backend_phase1_syncthing_native_subnet: "{{ atlas_lan_subnet }}"
backend_phase1_music_sync_enabled: true
rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: false

View File

@@ -30,3 +30,7 @@ backend_phase1_timezone: Europe/Rome
backend_phase1_services:
- atlas-navidrome.service
- atlas-syncthing.service
backend_phase1_music_sync_enabled: false
backend_phase1_music_source_dir: "{{ backend_phase1_archive_dir }}/Music"
backend_phase1_music_sync_calendar: "*-*-* 00:45:00 Europe/Rome"
backend_phase1_user_systemd_dir: "{{ backend_phase1_user_home }}/.config/systemd/user"

View File

@@ -18,21 +18,28 @@
- backend_phase1_app_data_root.startswith('/')
- backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/')
- backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/')
- >-
not (backend_phase1_music_sync_enabled | bool) or
(backend_phase1_music_source_dir.startswith(backend_phase1_archive_dir + '/')
and backend_phase1_music_sync_calendar | length > 0)
fail_msg: >-
Disable the rootful media-stack gate and provide the Atlas LAN bind
address, firewall sources, and absolute ZFS-backed paths before
enabling phase one. This role does not manage Prometheus or migrate
application data.
tags: [music_sync]
- name: Read the rootless service account
ansible.builtin.getent:
database: passwd
key: "{{ backend_phase1_username }}"
tags: [music_sync]
- name: Record rootless service account IDs
ansible.builtin.set_fact:
backend_phase1_uid: "{{ ansible_facts['getent_passwd'][backend_phase1_username][1] }}"
backend_phase1_gid: "{{ ansible_facts['getent_passwd'][backend_phase1_username][2] }}"
tags: [music_sync]
- name: Read system service state before starting rootless Syncthing
ansible.builtin.service_facts:
@@ -65,6 +72,7 @@
loop_control:
label: "{{ item.dataset }}"
register: backend_phase1_zfs_facts
tags: [music_sync]
- name: Require mounted datasets at the declared paths
ansible.builtin.assert:
@@ -79,6 +87,23 @@
loop: "{{ backend_phase1_zfs_facts.results }}"
loop_control:
label: "{{ item.item.dataset }}"
tags: [music_sync]
- name: Inspect the music copy source
ansible.builtin.stat:
path: "{{ backend_phase1_music_source_dir }}"
register: backend_phase1_music_source_stat
when: backend_phase1_music_sync_enabled | bool
tags: [music_sync]
- name: Require an existing music source directory
ansible.builtin.assert:
that:
- backend_phase1_music_source_stat.stat.isdir | default(false)
fail_msg: >-
{{ backend_phase1_music_source_dir }} must exist before enabling the daily music copy.
when: backend_phase1_music_sync_enabled | bool
tags: [music_sync]
- name: Enable lingering for the rootless service account
ansible.builtin.command:
@@ -87,12 +112,14 @@
- enable-linger
- "{{ backend_phase1_username }}"
creates: "/var/lib/systemd/linger/{{ backend_phase1_username }}"
tags: [music_sync]
- name: Start the rootless user systemd manager
ansible.builtin.systemd:
name: "user@{{ backend_phase1_uid }}.service"
state: started
when: not ansible_check_mode
tags: [music_sync]
- name: Create rootless Quadlet and application directories
ansible.builtin.file:
@@ -113,6 +140,43 @@
loop_control:
label: "{{ item.path }}"
- name: Install rsync for the daily music copy
ansible.builtin.dnf:
name: rsync
state: present
when: backend_phase1_music_sync_enabled | bool
tags: [music_sync]
- name: Create the rootless user systemd directory
ansible.builtin.file:
path: "{{ backend_phase1_user_systemd_dir }}"
state: directory
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0700"
when: backend_phase1_music_sync_enabled | bool
tags: [music_sync]
- name: Install the daily music copy service
ansible.builtin.template:
src: atlas-music-sync.service.j2
dest: "{{ backend_phase1_user_systemd_dir }}/atlas-music-sync.service"
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0644"
when: backend_phase1_music_sync_enabled | bool
tags: [music_sync]
- name: Install the daily music copy timer
ansible.builtin.template:
src: atlas-music-sync.timer.j2
dest: "{{ backend_phase1_user_systemd_dir }}/atlas-music-sync.timer"
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0644"
when: backend_phase1_music_sync_enabled | bool
tags: [music_sync]
- name: Render the rootless Navidrome Quadlet
ansible.builtin.template:
src: atlas-navidrome.container.j2
@@ -140,6 +204,7 @@
XDG_RUNTIME_DIR: "/run/user/{{ backend_phase1_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when: not ansible_check_mode
tags: [music_sync]
- name: Permit NPM access to phase-one web interfaces through Aegis
ansible.posix.firewalld:
@@ -186,3 +251,19 @@
when:
- backend_phase1_start_services | bool
- not ansible_check_mode
- name: Enable the daily music copy timer
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: atlas-music-sync.timer
scope: user
state: started
enabled: true
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ backend_phase1_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when:
- backend_phase1_music_sync_enabled | bool
- not ansible_check_mode
tags: [music_sync]

View File

@@ -0,0 +1,10 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Copy Atlas Archive music to the Navidrome library
[Service]
Type=oneshot
ExecStartPre=/usr/bin/mountpoint -q {{ backend_phase1_archive_dir }}
ExecStartPre=/usr/bin/mountpoint -q {{ backend_phase1_music_dir }}
ExecStartPre=/usr/bin/test -d {{ backend_phase1_music_source_dir }}
ExecStart=/usr/bin/rsync -aH --no-perms --no-owner --no-group --delay-updates --stats -- {{ backend_phase1_music_source_dir }}/ {{ backend_phase1_music_dir }}/

View File

@@ -0,0 +1,11 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Schedule the daily Atlas Navidrome music copy
[Timer]
OnCalendar={{ backend_phase1_music_sync_calendar }}
Persistent=true
Unit=atlas-music-sync.service
[Install]
WantedBy=timers.target