diff --git a/AGENTS.md b/AGENTS.md index 3cd5ba4..66a10f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.it.md b/README.it.md index 0614098..fc4fc70 100644 --- a/README.it.md +++ b/README.it.md @@ -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 diff --git a/README.md b/README.md index 474e235..56dcf22 100644 --- a/README.md +++ b/README.md @@ -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/ 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 diff --git a/ansible/inventory/host_vars/atlas.yml b/ansible/inventory/host_vars/atlas.yml index f2e0f2d..1a59d92 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -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 diff --git a/ansible/roles/profile_backend_phase1/defaults/main.yml b/ansible/roles/profile_backend_phase1/defaults/main.yml index 6933ef1..6c13dbb 100644 --- a/ansible/roles/profile_backend_phase1/defaults/main.yml +++ b/ansible/roles/profile_backend_phase1/defaults/main.yml @@ -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" diff --git a/ansible/roles/profile_backend_phase1/tasks/main.yml b/ansible/roles/profile_backend_phase1/tasks/main.yml index 3160934..b3b14e9 100644 --- a/ansible/roles/profile_backend_phase1/tasks/main.yml +++ b/ansible/roles/profile_backend_phase1/tasks/main.yml @@ -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] diff --git a/ansible/roles/profile_backend_phase1/templates/atlas-music-sync.service.j2 b/ansible/roles/profile_backend_phase1/templates/atlas-music-sync.service.j2 new file mode 100644 index 0000000..30f7470 --- /dev/null +++ b/ansible/roles/profile_backend_phase1/templates/atlas-music-sync.service.j2 @@ -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 }}/ diff --git a/ansible/roles/profile_backend_phase1/templates/atlas-music-sync.timer.j2 b/ansible/roles/profile_backend_phase1/templates/atlas-music-sync.timer.j2 new file mode 100644 index 0000000..3f37c83 --- /dev/null +++ b/ansible/roles/profile_backend_phase1/templates/atlas-music-sync.timer.j2 @@ -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