diff --git a/AGENTS.md b/AGENTS.md index 6ece0cd..d4569f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora - Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server` - Atlas media stack: `ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --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: @@ -164,7 +166,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 @@ -264,7 +268,9 @@ successfully. The first monthly scrub remains a runtime check. 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. + intact and the temporary snapshot was removed. A daily, non-deleting rsync timer now keeps the + separate Navidrome music dataset updated from `Archive/Music`. A manual idempotent service run + succeeded on 2026-10-01; the first scheduled run remains to be verified. - [ ] Design and deploy Nextcloud as another explicitly temporary Atlas service before Uranus. Give it separate persistent application, database, and cache storage; keep credentials in Vault; publish it only through NPM over the Prometheus--Aegis gateway; and define backup, upgrade, and eventual Uranus-migration diff --git a/README.it.md b/README.it.md index 494a83c..ab086e1 100644 --- a/README.it.md +++ b/README.it.md @@ -312,8 +312,10 @@ Atlas ospita temporaneamente Navidrome e Syncthing rootless fino alla sostituzio 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` è stata popolata separatamente da `/zpool/archive/Music` il 2026-09-30; -Navidrome ha completato la scansione. Alcune playlist originali contengono ancora vecchi percorsi -Windows. I servizi sono vincolati all'indirizzo LAN di Atlas +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. 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 @@ -734,6 +736,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 d5313d0..1a0c52b 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,9 @@ Atlas' LAN address (`192.168.178.55`); WireGuard remains exclusively between Pro 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` was populated separately from `/zpool/archive/Music` on 2026-09-30; -Navidrome completed its library scan. Some source playlists still contain obsolete Windows paths. +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. Some source playlists still contain obsolete Windows paths. The separate `wireguard_overlay` role manages `wg0` between Prometheus (`10.0.0.1`) and Aegis (`10.0.0.2`), generating private keys once on their respective hosts and exchanging only public keys @@ -723,6 +725,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 1e06140..e8fd2ee 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -148,6 +148,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