Compare commits

1 Commits

Author SHA1 Message Date
Fabio Scotto di Santolo
64aebe8c34 Add Atlas media and storage services 2026-09-10 22:27:33 +02:00
34 changed files with 1127 additions and 209 deletions

View File

@@ -53,6 +53,10 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff` - AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff`
- Mail bootstrap: `sh -n scripts/bootstrap_mail.sh` and `shellcheck scripts/bootstrap_mail.sh` - Mail bootstrap: `sh -n scripts/bootstrap_mail.sh` and `shellcheck scripts/bootstrap_mail.sh`
- Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server` - 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`
- Prometheus media mount:
`ansible-playbook ansible/site.yml --limit prometheus --tags rclone,navidrome --check --diff`
- DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff` - DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff`
## Conventions ## Conventions
@@ -104,8 +108,12 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- `rocky_server` is a child of both `platform_rocky` and `server`; `prometheus` is its active target. - `rocky_server` is a child of both `platform_rocky` and `server`; `prometheus` is its active target.
- The target must already provide `server_username` with local sudo access before the profile runs. - The target must already provide `server_username` with local sudo access before the profile runs.
- The Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the - The Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the
same server Compose stack with a `podman-compose-server` systemd unit. It does not start, enable, transfer data, Nginx Proxy Manager/Gitea/Navidrome-PostgreSQL Compose stack with a `podman-compose-server` systemd unit. It does not
update DNS, or cut over traffic; activating the stack is a manual step. start or enable that Compose stack, transfer data, update DNS, or cut over traffic; activating it remains manual.
- Prometheus has a gated system `rclone-music.service` and rootless Navidrome Quadlet. They remain disabled until the
Atlas WireGuard address, pinned SSH host key and Vault-backed SFTP private key are configured. The rclone mount is
read-only at `/mnt/music_atlas`; Navidrome must not start against the underlying empty mountpoint or while the legacy
rootful Navidrome container is still running. The role never removes that legacy container or its data.
- Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes `80/tcp` and - Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes `80/tcp` and
`443/tcp`; bind its administration interface only to `127.0.0.1:81` and use `npm-tunnel` from Ikaros or Nymph. `443/tcp`; bind its administration interface only to `127.0.0.1:81` and use `npm-tunnel` from Ikaros or Nymph.
Nextcloud remains disabled; do not provision `/srv/nextcloud` directories. Nextcloud remains disabled; do not provision `/srv/nextcloud` directories.
@@ -121,17 +129,28 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
subsequent runs use the dedicated Atlas account. subsequent runs use the dedicated Atlas account.
- The pool is pre-existing: never add pool creation, disk partitioning, RAIDZ creation, rollback, - The pool is pre-existing: never add pool creation, disk partitioning, RAIDZ creation, rollback,
or destruction to the Atlas profile. or destruction to the Atlas profile.
- `atlas_manage_storage` and `atlas_manage_firewall` remain false until their placeholders are - `atlas_manage_storage`, `atlas_manage_firewall`, and `atlas_manage_media_stack` remain false until their placeholders
replaced; only then may the profile manage datasets, shares and LAN-restricted firewall rules. and Vault inputs are replaced; only then may the profile manage datasets, shares, LAN-restricted firewall rules, and
rootful media Quadlets.
- Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit - Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit
and, when storage is enabled, `vault_atlas_samba_password`. Never print these values. and, when the relevant gates are enabled, `vault_atlas_samba_password` and `vault_atlas_immich_db_password`. Never
- Atlas uses NFSv4 for Linux and SMB for Windows/WSL, restricted to the configured LAN. Snapshot, print these values.
Borg/Hetzner offsite backup, Prometheus pull and USB backup automation are intentionally deferred. - Atlas creates `archive`, `media/music`, `media/icloud_photos`, and `backups/services` only under the verified
pre-existing pool; `backups/services` has a `500G` refreservation. Existing Work, Syncthing, and
Prometheus-backup datasets remain managed and separate.
- 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 iCloud Photos external library is read-only.
- Atlas exports iCloud Photos only to the configured Aegis IP with all access squashed to UID/GID `1100`. SMB3 exposes
`Archive` to Vault-backed authorized accounts and admits the configured LAN without host-specific exclusions.
- Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains
bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet.
## Atlas NAS TODO ## Atlas NAS TODO
- Replace every Atlas `CHANGEME` value, provide the required Vault variables and validate the first - Replace every Atlas `CHANGEME` value, provide the required Vault variables and validate the first
remote bootstrap on the real Rocky Linux 9 host. Enable `atlas_manage_storage` first and remote bootstrap on the real Rocky Linux 9 host. Enable `atlas_manage_storage` first and
`atlas_manage_firewall` only after confirming the pool, mountpoints, LAN subnet and firewalld zone. `atlas_manage_firewall` only after confirming the pool, mountpoints, LAN subnet and firewalld zone. Enable
`atlas_manage_media_stack` last, after validating `/dev/dri`, the container paths and the Immich database secret.
- Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset - Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset
mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing. mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing.
- Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files - Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files
@@ -140,8 +159,8 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
or manual operations, not as the only source of configuration, and never automate snapshot rollback. or manual operations, not as the only source of configuration, and never automate snapshot rollback.
- Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI - Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI
or API access for the selected clients. or API access for the selected clients.
- Add Tailscale or WireGuard and corresponding LAN/VPN-only firewalld rules before enabling remote - Validate the existing WireGuard path and add its LAN/VPN-only firewalld rules before enabling remote services;
services; never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding. never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity, - Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity,
pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer. pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer.
- Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity, - Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity,

View File

@@ -180,7 +180,10 @@ Lo stato attuale del profilo server include:
- installazione pacchetti Rocky via DNF, EPEL e CRB - installazione pacchetti Rocky via DNF, EPEL e CRB
- installazione di Podman e podman-compose - installazione di Podman e podman-compose
- abilitazione dei servizi systemd dichiarati in inventory/group vars - abilitazione dei servizi systemd dichiarati in inventory/group vars
- copia dei dotfiles server e rendering dei template server, incluso il `docker-compose.yml` dello stack servizi e dell'unit `podman-compose-server` (attivazione manuale) - copia dei dotfiles server e rendering del `docker-compose.yml` per Nginx Proxy Manager, Gitea e il
database PostgreSQL esistente di Navidrome, piu l'unita `podman-compose-server` (attivazione manuale)
- mount di `/pool/media/music` da Atlas su `/mnt/music_atlas` tramite il servizio di sistema
`rclone-music.service`, e Navidrome tramite Quadlet utente rootless
- attivazione di firewalld con SSH, Cockpit (`9090/tcp`), HTTP e HTTPS abilitati - attivazione di firewalld con SSH, Cockpit (`9090/tcp`), HTTP e HTTPS abilitati
- Syncthing escluso dal profilo server Rocky - Syncthing escluso dal profilo server Rocky
@@ -188,6 +191,18 @@ Nginx Proxy Manager pubblica solo `80/tcp` e `443/tcp`; la sua interfaccia di am
associata a `127.0.0.1:81` ed e raggiungibile da Ikaros o Nymph con l'alias Bash `npm-tunnel`. associata a `127.0.0.1:81` ed e raggiungibile da Ikaros o Nymph con l'alias Bash `npm-tunnel`.
Nextcloud resta disabilitato e il profilo non crea directory `/srv/nextcloud`. Nextcloud resta disabilitato e il profilo non crea directory `/srv/nextcloud`.
Il mount musicale e protetto da `server_atlas_music_enabled`. Prima di abilitarlo, sostituire
l'indirizzo WireGuard e la chiave host SSH fissata in `host_vars/prometheus.yml`, quindi fornire
`vault_prometheus_atlas_sftp_private_key` tramite Vault cifrato o variabili locali non tracciate. La
chiave pubblica corrispondente deve essere gia presente nelle chiavi autorizzate gestite su Atlas.
Rclone usa il percorso remoto esatto `/pool/media/music` in sola lettura e una cache VFS completa da
`15G`; systemd lingering mantiene disponibile il manager utente per il Quadlet rootless.
Configurare il proxy host NPM di Prometheus per Navidrome come `host.containers.internal:4533`; la
porta Navidrome non viene aperta in firewalld.
Prima della prima attivazione, arrestare il vecchio container rootful `navidrome`. Il ruolo rifiuta
di avviare il sostituto rootless mentre il container precedente e in esecuzione e non rimuove mai
automaticamente il container o i dati esistenti.
### DuckDNS ### DuckDNS
`profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello `profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello
@@ -239,9 +254,10 @@ solo i dataset figli e non deve mai creare, partizionare, distruggere, fare roll
pool. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN pool. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN
configurata. configurata.
Per il primo avvio sostituire i placeholder Atlas e fornire Per il primo avvio sostituire i placeholder di host, pool, mount root, LAN e IP di Aegis e
`vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` e fornire `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`,
`vault_atlas_samba_password`. Eseguire il bootstrap tramite l'amministratore esistente: `vault_atlas_samba_password` e `vault_atlas_immich_db_password`. Eseguire il bootstrap tramite
l'amministratore esistente:
```bash ```bash
ansible-playbook ansible/site.yml --limit atlas \ ansible-playbook ansible/site.yml --limit atlas \
@@ -251,11 +267,26 @@ ansible-playbook ansible/site.yml --limit atlas \
`vault_atlas_admin_password_hash` deve essere un hash compatibile con `/etc/shadow`, non una `vault_atlas_admin_password_hash` deve essere un hash compatibile con `/etc/shadow`, non una
password Cockpit in chiaro. Le esecuzioni successive usano `atlas_admin_username`. Abilitare password Cockpit in chiaro. Le esecuzioni successive usano `atlas_admin_username`. Abilitare
`atlas_manage_storage` solo dopo aver verificato pool e mountpoint esistenti; abilitare `atlas_manage_storage` solo dopo aver verificato pool e mountpoint esistenti; abilitare
`atlas_manage_firewall` solo dopo aver verificato subnet LAN e zona firewalld attiva. `atlas_manage_firewall` solo dopo aver verificato subnet LAN e zona firewalld attiva. Abilitare
`atlas_manage_media_stack` per ultimo, dopo aver verificato `/dev/dri`, i percorsi dei container e il
segreto del database Immich.
Restano da implementare retention delle snapshot, topologia Syncthing, VPN, pull da Prometheus, Con la gestione storage attiva, Atlas crea `archive` (`zstd`), `media/music` (`lz4`),
backup cifrati con Borg su una Hetzner Storage Box, backup USB, monitoraggio e test di disaster `media/icloud_photos` (`lz4`) e `backups/services` (`lz4`, `refreservation=500G`) sotto il pool
recovery. Il backlog operativo dettagliato e in `AGENTS.md`. preesistente. I dataset esistenti Work, Syncthing e backup Prometheus restano gestiti e separati.
SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e ammette la LAN
configurata senza esclusioni specifiche per host. NFSv4 esporta soltanto
`media/icloud_photos` all'IP configurato di Aegis con `all_squash` verso UID/GID anonimi `1100`.
L'account di sistema `immich` usa UID/GID `1100`, shell senza login, nessuna appartenenza a `wheel` e
i gruppi supplementari `video` e `render`. I Quadlet rootful di Immich Server, ML, cache compatibile
Redis, PostgreSQL e NPM condividono una rete Podman. Immich viene eseguito come `1100:1100`; Server e
ML ricevono `/dev/dri` e la libreria iCloud Photos e montata in sola lettura. NPM pubblica `80` e
`443`, mentre l'amministrazione resta vincolata a `127.0.0.1:81` per l'accesso tramite tunnel SSH.
Restano da completare retention delle snapshot, topologia Syncthing, validazione WireGuard/firewall,
pull di backup da Prometheus, backup cifrati con Borg su una Hetzner Storage Box, backup USB,
monitoraggio e test di disaster recovery. Il backlog operativo dettagliato e in `AGENTS.md`.
--- ---
@@ -348,7 +379,8 @@ Questo significa che, allo stato attuale:
- `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati - `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati
- il server Rocky (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewalld - il server Rocky (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewalld
- il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives - il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives
- lo stack container server include `navidrome`, `postgres`, `gitea` e `nginx-proxy-manager` - lo stack Compose server include `gitea`, `nginx-proxy-manager` e il database PostgreSQL di
Navidrome; Navidrome usa un Quadlet rootless separato e legge il mount rclone di Atlas
# Dotfiles # Dotfiles
@@ -455,6 +487,8 @@ ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" -
ansible-lint ansible/roles/<role> ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config 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 prometheus --tags rclone,navidrome --check --diff
``` ```
## Tag supportati dal playbook ## Tag supportati dal playbook
@@ -471,6 +505,8 @@ Allo stato attuale `ansible/site.yml` espone questi tag:
| --- | --- | --- | | --- | --- | --- |
| `always` | pre-task sempre eseguiti, inclusi caricamento vault e validazioni preliminari | common | | `always` | pre-task sempre eseguiti, inclusi caricamento vault e validazioni preliminari | common |
| `ai_agents` | installazione agenti AI condivisi | Fedora, WSL | | `ai_agents` | installazione agenti AI condivisi | Fedora, WSL |
| `atlas` | account, storage, condivisioni e container Atlas | NAS Atlas |
| `containers` | Quadlet rootful Atlas | NAS Atlas |
| `dotfiles` | distribuzione/configurazione dotfiles | tutti i profili | | `dotfiles` | distribuzione/configurazione dotfiles | tutti i profili |
| `dotfiles:common` | dotfiles comuni condivisi | common, workstation, server | | `dotfiles:common` | dotfiles comuni condivisi | common, workstation, server |
| `dotfiles:desktop` | dotfiles desktop | desktop Void, Fedora/GNOME | | `dotfiles:desktop` | dotfiles desktop | desktop Void, Fedora/GNOME |
@@ -484,13 +520,19 @@ Allo stato attuale `ansible/site.yml` espone questi tag:
| `fzf` | configurazione FZF | dotfiles comuni | | `fzf` | configurazione FZF | dotfiles comuni |
| `git` | configurazione Git e GPG desktop | Fedora/GNOME, desktop Void | | `git` | configurazione Git e GPG desktop | Fedora/GNOME, desktop Void |
| `gnome` | configurazione host GNOME | Fedora/GNOME desktop | | `gnome` | configurazione host GNOME | Fedora/GNOME desktop |
| `immich` | account e Quadlet Immich | NAS Atlas |
| `navidrome` | mount rclone e Quadlet Navidrome rootless | Prometheus |
| `sway` | sessione/configurazione sway / SwayFX (Wayland) | desktop Void | | `sway` | sessione/configurazione sway / SwayFX (Wayland) | desktop Void |
| `niri` | sessione/configurazione Niri (Wayland) | desktop Void | | `niri` | sessione/configurazione Niri (Wayland) | desktop Void |
| `npm` | installazione pacchetti npm globali | Fedora/GNOME, desktop Void, WSL | | `npm` | installazione pacchetti npm globali | Fedora/GNOME, desktop Void, WSL |
| `nvidia` | componenti NVIDIA desktop | desktop Void | | `nvidia` | componenti NVIDIA desktop | desktop Void |
| `packages` | installazione e aggiornamento pacchetti | tutti i profili | | `packages` | installazione e aggiornamento pacchetti | tutti i profili |
| `podman` | integrazione Podman Compose e Quadlet rootless | server |
| `rclone` | mount musica Atlas | Prometheus |
| `portal` | configurazione xdg-desktop-portal | desktop Void | | `portal` | configurazione xdg-desktop-portal | desktop Void |
| `services` | gestione servizi runit/systemd | tutti i profili | | `services` | gestione servizi runit/systemd | tutti i profili |
| `sharing` | condivisioni NFSv4 e SMB3 | NAS Atlas |
| `storage` | dataset ZFS figli | NAS Atlas |
| `theme` | configurazione del tema GTK/Qt | desktop Void | | `theme` | configurazione del tema GTK/Qt | desktop Void |
| `tmux` | configurazione e plugin tmux | desktop Fedora/Void, WSL | | `tmux` | configurazione e plugin tmux | desktop Fedora/Void, WSL |
| `vim` | configurazione Vim | dotfiles comuni | | `vim` | configurazione Vim | dotfiles comuni |

View File

@@ -104,10 +104,24 @@ That gives it Fedora packages through DNF, Docker from the official repository,
dotfiles and templates. The profile provisions configuration only: it does not transfer data, start dotfiles and templates. The profile provisions configuration only: it does not transfer data, start
the Compose stack, update DNS, or perform a cutover. the Compose stack, update DNS, or perform a cutover.
The server profile installs platform-specific packages, Podman and podman-compose, The server profile installs platform-specific packages, Podman and podman-compose, declared systemd
declared systemd services, the server Compose stack behind the `podman-compose-server` systemd unit, and firewalld. The Rocky server excludes services, and firewalld. The manually activated `podman-compose-server` unit now contains Nginx Proxy
Syncthing. Rocky bind mounts use private SELinux relabeling for application data while host system Manager, Gitea, and the existing Navidrome PostgreSQL database. Navidrome itself runs as a rootless
files remain unchanged. user Quadlet and reads the Atlas music dataset from the system `rclone-music.service` mount at
`/mnt/music_atlas`. The Rocky server excludes Syncthing.
Rocky bind mounts use private SELinux relabeling where supported; the read-only FUSE music mount is
passed to Navidrome without relabeling.
The Atlas music path is gated by `server_atlas_music_enabled`. Before enabling it, replace the
WireGuard address and pinned SSH host-key placeholders in `host_vars/prometheus.yml`, and provide
`vault_prometheus_atlas_sftp_private_key` through encrypted Vault or untracked local vars. The SFTP
key's public half must already be present in Atlas' managed authorized keys. Rclone mounts the exact
remote path `/pool/media/music` read-only and uses a `15G` full VFS cache; the rootless user manager is
kept alive through systemd lingering.
Configure the Prometheus NPM proxy host for Navidrome as `host.containers.internal:4533`; the
Navidrome port is not opened through firewalld.
Before the first enablement, stop the legacy rootful `navidrome` container. The role refuses to start
the rootless replacement while that container is running and never removes the old container or data.
Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only
`80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached `80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached
@@ -201,9 +215,10 @@ manages child datasets and must never create, partition, destroy, roll back, or
pool itself. Linux clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the pool itself. Linux clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the
configured LAN. configured LAN.
For the first run, replace the Atlas placeholders and provide For the first run, replace the Atlas host, pool, mount-root, LAN, and Aegis-IP
`vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, and placeholders and provide `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`,
`vault_atlas_samba_password`. Bootstrap the host through its existing administrator: `vault_atlas_samba_password`, and `vault_atlas_immich_db_password`. Bootstrap the host through its
existing administrator:
```bash ```bash
ansible-playbook ansible/site.yml --limit atlas \ ansible-playbook ansible/site.yml --limit atlas \
@@ -211,13 +226,27 @@ ansible-playbook ansible/site.yml --limit atlas \
``` ```
`vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text `vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text
Cockpit password. Subsequent runs use `atlas_admin_username`. Enable Cockpit password. Subsequent runs use `atlas_admin_username`. Enable `atlas_manage_storage` only after
`atlas_manage_storage` only after checking the existing pool and mountpoints; enable checking the existing pool and mountpoints; enable `atlas_manage_firewall` only after checking the LAN
`atlas_manage_firewall` only after checking the LAN subnet and active firewalld zone. subnet and active firewalld zone. Enable `atlas_manage_media_stack` last, after validating `/dev/dri`,
the container paths and the Immich database secret.
Snapshot retention, Syncthing topology, VPN access, Prometheus pulls, encrypted Borg backups to a With storage management enabled, Atlas creates `archive` (`zstd`), `media/music` (`lz4`),
Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests remain follow-up work. The `media/icloud_photos` (`lz4`), and `backups/services` (`lz4`, `refreservation=500G`) beneath the
detailed operational backlog is kept in `AGENTS.md`. pre-existing pool. The existing Work, Syncthing, and Prometheus-backup datasets remain managed and
separate. SMB3 exposes `Archive` only to the configured Vault-backed Samba accounts and admits the
configured LAN without host-specific exclusions. NFSv4 exports only
`media/icloud_photos` to the configured Aegis IP, using `all_squash` with anonymous UID/GID `1100`.
The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and
receives `video` and `render` access. The rootful Immich Server, ML, Redis-compatible cache, PostgreSQL,
and NPM Quadlets share one Podman network. Immich runs as `1100:1100`; Server and ML receive `/dev/dri`,
and iCloud Photos is mounted read-only as an external library. NPM publishes ports `80` and `443`; its
administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access.
Snapshot retention, Syncthing topology, WireGuard/firewall validation, Prometheus backup pulls,
encrypted Borg backups to a Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests
remain follow-up work. The detailed operational backlog is kept in `AGENTS.md`.
## How layering works ## How layering works
@@ -384,6 +413,8 @@ ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" -
ansible-lint ansible/roles/<role> ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config 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 prometheus --tags rclone,navidrome --check --diff
``` ```
## Tags ## Tags
@@ -398,6 +429,8 @@ ansible-playbook ansible/site.yml --list-tags
| --- | --- | | --- | --- |
| `always` | Common pre-tasks, including optional vault loading. | | `always` | Common pre-tasks, including optional vault loading. |
| `ai_agents` | AI coding-agent install, configuration deployment, and managed-binary removal. | | `ai_agents` | AI coding-agent install, configuration deployment, and managed-binary removal. |
| `atlas` | Atlas NAS account, storage, sharing, and container configuration. |
| `containers` | Rootful Atlas Quadlets. |
| `dotfiles` | User configuration across all profiles. | | `dotfiles` | User configuration across all profiles. |
| `dotfiles:common` | Shared dotfiles. | | `dotfiles:common` | Shared dotfiles. |
| `dotfiles:desktop` | Void and Fedora/GNOME desktop dotfiles. | | `dotfiles:desktop` | Void and Fedora/GNOME desktop dotfiles. |
@@ -406,9 +439,15 @@ ansible-playbook ansible/site.yml --list-tags
| `dotfiles:workstation` | Personal workstation and WSL dotfiles. | | `dotfiles:workstation` | Personal workstation and WSL dotfiles. |
| `emacs` | Shared Emacs setup and authoring dependencies. | | `emacs` | Shared Emacs setup and authoring dependencies. |
| `gnome` | Fedora/GNOME desktop configuration. | | `gnome` | Fedora/GNOME desktop configuration. |
| `immich` | Atlas Immich account and Quadlets. |
| `navidrome` | Prometheus rclone mount and rootless Navidrome Quadlet. |
| `npm` | Global npm packages. | | `npm` | Global npm packages. |
| `packages` | Package installation and updates. | | `packages` | Package installation and updates. |
| `podman` | Podman Compose and rootless Quadlet integration. |
| `rclone` | Prometheus Atlas music mount. |
| `services` | runit and systemd services. | | `services` | runit and systemd services. |
| `sharing` | Atlas NFSv4 and SMB3 configuration. |
| `storage` | Atlas child ZFS datasets. |
| `tmux` | tmux configuration and plugins. | | `tmux` | tmux configuration and plugins. |
| `wsl` | WSL bootstrap and configuration. | | `wsl` | WSL bootstrap and configuration. |

View File

@@ -6,6 +6,29 @@ effective_username: "{{ server_username }}"
effective_user_group: "{{ server_user_group }}" effective_user_group: "{{ server_user_group }}"
effective_user_home: "{{ server_user_home }}" effective_user_home: "{{ server_user_home }}"
server_container_stack_dir: /opt/docker/server server_container_stack_dir: /opt/docker/server
server_atlas_music_enabled: false
server_atlas_sftp_remote_name: atlas
server_atlas_sftp_host: CHANGEME_ATLAS_WIREGUARD_IP
server_atlas_sftp_port: 22
server_atlas_sftp_username: admin
server_atlas_sftp_remote_path: /pool/media/music
server_atlas_sftp_private_key: "{{ vault_prometheus_atlas_sftp_private_key | default('') }}"
server_atlas_sftp_private_key_file: /etc/rclone/atlas_ed25519
server_atlas_sftp_known_host: ""
server_atlas_sftp_known_hosts_file: /etc/rclone/known_hosts
server_atlas_wireguard_unit: ""
server_rclone_config_dir: /etc/rclone
server_rclone_config_file: /etc/rclone/rclone.conf
server_rclone_music_service: rclone-music.service
server_rclone_music_mountpoint: /mnt/music_atlas
server_rclone_music_cache_dir: /var/cache/rclone-music
server_navidrome_data_dir: /opt/navidrome/data
server_navidrome_quadlet_dir: "{{ server_user_home }}/.config/containers/systemd"
server_navidrome_env_dir: "{{ server_user_home }}/.config/navidrome"
server_navidrome_env_file: "{{ server_navidrome_env_dir }}/navidrome.env"
server_navidrome_image: docker.io/deluan/navidrome:latest
server_navidrome_port: 4533
server_navidrome_db_password: "{{ vault_navidrome_db_password | default('') }}"
ai_agents: {} ai_agents: {}
vim_plugins_enabled: false vim_plugins_enabled: false
@@ -62,11 +85,7 @@ server_directories:
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- path: /opt/navidrome/data - path: "{{ server_navidrome_data_dir }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: /opt/music
owner: "{{ server_username }}" owner: "{{ server_username }}"
group: "{{ server_user_group }}" group: "{{ server_user_group }}"
mode: "0755" mode: "0755"

View File

@@ -1,10 +1,10 @@
--- ---
# Replace every CHANGEME value before enabling Atlas storage management. # Keep Atlas management gates disabled until the NAS and required Vault inputs are ready.
hostname: atlas hostname: atlas
ansible_host: CHANGEME_ATLAS_HOST ansible_host: 192.168.178.55
ansible_user: "{{ atlas_connection_username }}" ansible_user: "{{ atlas_connection_username }}"
ansible_port: 22 ansible_port: 22
ansible_ssh_private_key_file: CHANGEME_ATLAS_SSH_PRIVATE_KEY_PATH ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
atlas_admin_username: admin atlas_admin_username: admin
atlas_connection_username: "{{ atlas_admin_username }}" atlas_connection_username: "{{ atlas_admin_username }}"
@@ -15,46 +15,67 @@ atlas_admin_gid: 1000
atlas_admin_ssh_keys: "{{ vault_atlas_authorized_ssh_keys | default([]) }}" atlas_admin_ssh_keys: "{{ vault_atlas_authorized_ssh_keys | default([]) }}"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}" atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_lan_subnet: CHANGEME_LAN_SUBNET atlas_lan_subnet: 192.168.178.0/24
atlas_aegis_ip: 192.168.178.54
atlas_manage_firewall: false atlas_manage_firewall: false
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL atlas_zfs_pool: zpool
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_syncthing: syncthing atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos atlas_zfs_dataset_icloud_photos: media/icloud_photos
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_zfs_dataset_service_backups: backups/services
atlas_mount_root: /zpool
atlas_manage_storage: false atlas_manage_storage: false
atlas_manage_media_stack: false
rocky_manage_openzfs_repo: true rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: true rocky_manage_syncthing_binary: true
rocky_manage_podman: true
rocky_podman_packages:
- podman
host_packages: host_packages:
- cockpit - cockpit
- nfs-utils - nfs-utils
- policycoreutils-python-utils - policycoreutils-python-utils
- samba - samba
- samba-client
- samba-common-tools
- zfs - zfs
atlas_nfs_exports: atlas_nfs_exports:
- path: "{{ atlas_work_mountpoint }}" - path: "{{ atlas_icloud_photos_mountpoint }}"
clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)" client: "{{ atlas_aegis_ip }}"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: work atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users: # Append any pre-existing laptop Unix account here and keep its Samba password in Vault.
- "{{ atlas_admin_username }}" atlas_samba_accounts:
atlas_samba_password: "{{ vault_atlas_samba_password | default('') }}" - username: "{{ atlas_admin_username }}"
password: "{{ vault_atlas_samba_password | default('') }}"
atlas_samba_valid_users: "{{ atlas_samba_accounts | map(attribute='username') | list }}"
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_firewalld_rich_rules: atlas_firewalld_rich_rules:
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="2049" protocol="tcp" accept' - 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" service name="nfs" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="http" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="https" accept'
host_enabled_services: host_enabled_services:
- sshd - sshd
- cockpit.socket - cockpit.socket
- nfs-server
- smb
- zfs.target - zfs.target

View File

@@ -7,11 +7,19 @@ ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
server_username: rocky server_username: rocky
server_duckdns_domain: fscotto server_duckdns_domain: fscotto
server_atlas_music_enabled: false
server_atlas_sftp_host: CHANGEME_ATLAS_WIREGUARD_IP
server_atlas_sftp_username: admin
server_atlas_sftp_known_host: CHANGEME_ATLAS_SSH_HOST_KEY
server_atlas_wireguard_unit: wg-quick@wg0.service
server_atlas_sftp_private_key: "{{ vault_prometheus_atlas_sftp_private_key | default('') }}"
host_packages: host_packages:
- cockpit - cockpit
- cockpit-navigator - cockpit-navigator
- cockpit-podman - cockpit-podman
- fuse3
- rclone
host_enabled_services: host_enabled_services:
- cockpit.socket - cockpit.socket

View File

@@ -8,20 +8,36 @@ atlas_admin_gid: 1000
atlas_admin_ssh_keys: [] atlas_admin_ssh_keys: []
atlas_admin_password_hash: "!" atlas_admin_password_hash: "!"
atlas_immich_username: immich
atlas_immich_group: immich
atlas_immich_uid: 1100
atlas_immich_gid: 1100
atlas_immich_supplementary_groups:
- video
- render
atlas_lan_subnet: CHANGEME_LAN_SUBNET atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_aegis_ip: CHANGEME_AEGIS_IP
atlas_manage_firewall: false atlas_manage_firewall: false
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_syncthing: syncthing atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos atlas_zfs_dataset_icloud_photos: media/icloud_photos
atlas_zfs_dataset_service_backups: backups/services
atlas_zfs_service_backups_refreservation: 500G
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}" atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}" atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}" atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}"
atlas_icloud_photos_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_icloud_photos }}" atlas_icloud_photos_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_icloud_photos }}"
atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}"
atlas_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing" atlas_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing"
atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync" atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync"
@@ -34,18 +50,59 @@ atlas_45drives_packages:
- cockpit-zfs - cockpit-zfs
- cockpit-scheduler - cockpit-scheduler
atlas_nfs_exports: [] atlas_nfs_exports:
atlas_samba_share_name: work - path: "{{ atlas_icloud_photos_mountpoint }}"
client: "{{ atlas_aegis_ip }}"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users: [] atlas_samba_valid_users: []
atlas_samba_password: "" atlas_samba_accounts: []
atlas_samba_password_marker: /var/lib/samba/private/.atlas-password.sha256 atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords
atlas_firewalld_rich_rules: [] atlas_firewalld_rich_rules: []
atlas_firewalld_restricted_services: atlas_firewalld_restricted_services:
- ssh - ssh
- cockpit - cockpit
- nfs - nfs
- samba - samba
- http
- https
atlas_selinux_booleans: atlas_selinux_booleans:
- samba_export_all_rw - samba_export_all_rw
- nfs_export_all_rw - nfs_export_all_rw
atlas_manage_media_stack: false
atlas_container_quadlet_dir: /etc/containers/systemd
atlas_container_data_root: /var/lib/atlas-containers
atlas_npm_data_dir: "{{ atlas_container_data_root }}/npm/data"
atlas_npm_letsencrypt_dir: "{{ atlas_container_data_root }}/npm/letsencrypt"
atlas_npm_admin_bind_address: 127.0.0.1
atlas_timezone: Europe/Rome
atlas_immich_upload_dir: "{{ atlas_container_data_root }}/immich/upload"
atlas_immich_model_cache_dir: "{{ atlas_container_data_root }}/immich/model-cache"
atlas_immich_machine_learning_config_dir: "{{ atlas_container_data_root }}/immich/machine-learning-config"
atlas_immich_machine_learning_cache_dir: "{{ atlas_container_data_root }}/immich/machine-learning-cache"
atlas_immich_redis_data_dir: "{{ atlas_container_data_root }}/immich/redis"
atlas_immich_postgres_data_dir: "{{ atlas_container_data_root }}/immich/postgres"
atlas_npm_image: docker.io/jc21/nginx-proxy-manager:latest
atlas_immich_version: release
atlas_immich_server_image: "ghcr.io/immich-app/immich-server:{{ atlas_immich_version }}"
atlas_immich_machine_learning_image: "ghcr.io/immich-app/immich-machine-learning:{{ atlas_immich_version }}-openvino"
atlas_immich_redis_image: docker.io/valkey/valkey:9
atlas_immich_postgres_image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
atlas_immich_db_username: postgres
atlas_immich_db_name: immich
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_media_quadlet_services:
- atlas-immich-redis.service
- atlas-immich-postgres.service
- atlas-immich-machine-learning.service
- atlas-immich-server.service
- atlas-npm.service

View File

@@ -32,3 +32,15 @@
ansible.builtin.systemd: ansible.builtin.systemd:
name: atlas-syncthing name: atlas-syncthing
state: restarted state: restarted
- name: Restart Atlas media Quadlets
ansible.builtin.systemd:
name: "{{ item }}"
state: restarted
daemon_reload: true
loop: "{{ atlas_media_quadlet_services }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_media_stack | bool
- not ansible_check_mode

View File

@@ -6,6 +6,7 @@
- atlas_admin_username != 'CHANGEME_ATLAS_ADMIN' - atlas_admin_username != 'CHANGEME_ATLAS_ADMIN'
- (atlas_admin_ssh_keys | default([])) | length > 0 - (atlas_admin_ssh_keys | default([])) | length > 0
- atlas_admin_password_hash not in ['', '!', '*'] - atlas_admin_password_hash not in ['', '!', '*']
- "'wheel' not in atlas_immich_supplementary_groups"
fail_msg: >- fail_msg: >-
Define atlas_admin_username, vault_atlas_authorized_ssh_keys and Define atlas_admin_username, vault_atlas_authorized_ssh_keys and
vault_atlas_admin_password_hash before applying the Atlas profile. vault_atlas_admin_password_hash before applying the Atlas profile.
@@ -31,6 +32,41 @@
state: present state: present
no_log: true no_log: true
- name: Create Immich primary group
tags: [atlas, accounts, containers, immich]
ansible.builtin.group:
name: "{{ atlas_immich_group }}"
gid: "{{ atlas_immich_gid }}"
state: present
- name: Ensure Immich hardware-access groups exist
tags: [atlas, accounts, containers, immich]
ansible.builtin.group:
name: "{{ item }}"
state: present
loop: "{{ atlas_immich_supplementary_groups }}"
loop_control:
label: "{{ item }}"
- name: Create unprivileged Immich account
tags: [atlas, accounts, containers, immich]
ansible.builtin.user:
name: "{{ atlas_immich_username }}"
uid: "{{ atlas_immich_uid }}"
group: "{{ atlas_immich_group }}"
groups: "{{ atlas_immich_supplementary_groups }}"
append: false
home: /nonexistent
create_home: false
shell: /sbin/nologin
system: true
state: present
- name: Read Immich hardware-access group IDs
tags: [atlas, accounts, containers, immich]
ansible.builtin.getent:
database: group
- name: Grant Atlas administrator passwordless sudo - name: Grant Atlas administrator passwordless sudo
tags: [atlas, services] tags: [atlas, services]
ansible.builtin.copy: ansible.builtin.copy:

View File

@@ -0,0 +1,140 @@
---
- name: Require completed Atlas media-stack configuration
tags: [atlas, containers, immich]
ansible.builtin.assert:
that:
- atlas_manage_storage | bool
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
- atlas_mount_root != '/CHANGEME_ATLAS_MOUNT_ROOT'
- atlas_immich_db_password | length > 0
- atlas_npm_admin_bind_address == '127.0.0.1'
fail_msg: >-
Enable and configure Atlas storage, provide vault_atlas_immich_db_password,
and keep the NPM administration port bound to loopback before enabling the media stack.
no_log: true
when: atlas_manage_media_stack | bool
- name: Check Atlas Intel graphics device
tags: [atlas, containers, immich]
ansible.builtin.stat:
path: /dev/dri
register: atlas_dri_device
when: atlas_manage_media_stack | bool
- name: Require Atlas Intel graphics device
tags: [atlas, containers, immich]
ansible.builtin.assert:
that:
- atlas_dri_device.stat.isdir | default(false)
fail_msg: /dev/dri is required for Immich QuickSync and OpenVINO acceleration.
when: atlas_manage_media_stack | bool
- name: Create Atlas rootful Quadlet directory
tags: [atlas, containers]
ansible.builtin.file:
path: "{{ atlas_container_quadlet_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_media_stack | bool
- name: Create Atlas Immich configuration directory
tags: [atlas, containers, immich]
ansible.builtin.file:
path: /etc/immich
state: directory
owner: root
group: "{{ atlas_immich_group }}"
mode: "0750"
when: atlas_manage_media_stack | bool
- name: Create Atlas Immich persistent directories
tags: [atlas, containers, immich]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
mode: "0750"
loop:
- "{{ atlas_immich_upload_dir }}"
- "{{ atlas_immich_model_cache_dir }}"
- "{{ atlas_immich_machine_learning_config_dir }}"
- "{{ atlas_immich_machine_learning_cache_dir }}"
- "{{ atlas_immich_redis_data_dir }}"
- "{{ atlas_immich_postgres_data_dir }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_media_stack | bool
- name: Create Atlas NPM persistent directories
tags: [atlas, containers, npm]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0750"
loop:
- "{{ atlas_npm_data_dir }}"
- "{{ atlas_npm_letsencrypt_dir }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_media_stack | bool
- name: Allow confined containers to use Atlas graphics devices
tags: [atlas, containers, immich]
ansible.posix.seboolean:
name: container_use_devices
state: true
persistent: true
when:
- atlas_manage_media_stack | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Vault-backed Immich environment
tags: [atlas, containers, immich]
ansible.builtin.template:
src: immich.env.j2
dest: /etc/immich/immich.env
owner: root
group: "{{ atlas_immich_group }}"
mode: "0640"
no_log: true
diff: false
notify: Restart Atlas media Quadlets
when: atlas_manage_media_stack | bool
- name: Render Atlas media Quadlets
tags: [atlas, containers]
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "{{ atlas_container_quadlet_dir }}/{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- atlas-media.network
- atlas-immich-redis.container
- atlas-immich-postgres.container
- atlas-immich-machine-learning.container
- atlas-immich-server.container
- atlas-npm.container
loop_control:
label: "{{ item }}"
notify: Restart Atlas media Quadlets
when: atlas_manage_media_stack | bool
- name: Start Atlas media Quadlet services
tags: [atlas, containers, services]
ansible.builtin.systemd:
name: "{{ item }}"
state: started
daemon_reload: true
loop: "{{ atlas_media_quadlet_services }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_media_stack | bool
- not ansible_check_mode

View File

@@ -11,5 +11,8 @@
- name: Import Atlas file sharing tasks - name: Import Atlas file sharing tasks
ansible.builtin.import_tasks: sharing.yml ansible.builtin.import_tasks: sharing.yml
- name: Import Atlas media-container tasks
ansible.builtin.import_tasks: containers.yml
- name: Import Atlas Syncthing tasks - name: Import Atlas Syncthing tasks
ansible.builtin.import_tasks: syncthing.yml ansible.builtin.import_tasks: syncthing.yml

View File

@@ -0,0 +1,64 @@
---
- name: Require an existing Unix account for Atlas Samba
ansible.builtin.getent:
database: passwd
key: "{{ atlas_samba_account.username }}"
- name: Read Atlas Samba account
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_samba_account.username }}"
register: atlas_samba_account_check
changed_when: false
failed_when: false
- name: Check Atlas Samba password marker
ansible.builtin.stat:
path: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
register: atlas_samba_password_marker_stat
- name: Read Atlas Samba password marker
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
register: atlas_samba_password_marker_content
no_log: true
when: atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_account.password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account_check.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_account.password | hash('sha256'))
}}
no_log: true
- name: Set Atlas Samba account password
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_samba_account.username }}"
stdin: "{{ atlas_samba_account.password }}\n{{ atlas_samba_account.password }}"
changed_when: true
no_log: true
when: atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
owner: root
group: root
mode: "0600"
no_log: true
when: atlas_samba_password_update_required | bool

View File

@@ -10,6 +10,19 @@
notify: Reload NFS exports notify: Reload NFS exports
when: atlas_manage_storage | bool when: atlas_manage_storage | bool
- name: Enable Atlas file-sharing services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- nfs-server.service
- smb.service
loop_control:
label: "{{ item }}"
when: atlas_manage_storage | bool
- name: Configure Atlas NFSv4-only service - name: Configure Atlas NFSv4-only service
tags: [atlas, sharing] tags: [atlas, sharing]
ansible.builtin.template: ansible.builtin.template:
@@ -82,106 +95,50 @@
notify: Restart Samba service notify: Restart Samba service
when: atlas_manage_storage | bool when: atlas_manage_storage | bool
- name: Require Atlas Samba password - name: Require Vault-backed Atlas Samba accounts
tags: [atlas, sharing] tags: [atlas, sharing]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- atlas_samba_password | length > 0 - atlas_samba_accounts | length > 0
fail_msg: Define vault_atlas_samba_password before enabling Atlas storage. - >-
atlas_samba_accounts | map(attribute='username') | list
| difference(atlas_samba_valid_users) | length == 0
- >-
atlas_samba_valid_users
| difference(atlas_samba_accounts | map(attribute='username') | list) | length == 0
- atlas_samba_accounts | selectattr('password', 'equalto', '') | list | length == 0
fail_msg: Define every authorized Samba account and its Vault-backed password.
no_log: true no_log: true
when: atlas_manage_storage | bool when: atlas_manage_storage | bool
- name: Read Atlas Samba account
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_admin_username }}"
register: atlas_samba_account
changed_when: false
failed_when: false
when: atlas_manage_storage | bool
- name: Ensure Atlas Samba private state directory exists - name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing] tags: [atlas, sharing]
ansible.builtin.file: ansible.builtin.file:
path: "{{ atlas_samba_password_marker | dirname }}" path: "{{ atlas_samba_password_marker_dir }}"
state: directory state: directory
owner: root owner: root
group: root group: root
mode: "0700" mode: "0700"
when: atlas_manage_storage | bool when: atlas_manage_storage | bool
- name: Check Atlas Samba password marker - name: Manage Vault-backed Atlas Samba credentials
tags: [atlas, sharing] tags: [atlas, sharing]
ansible.builtin.stat: ansible.builtin.include_tasks: samba_account.yml
path: "{{ atlas_samba_password_marker }}" loop: "{{ atlas_samba_accounts }}"
register: atlas_samba_password_marker_stat loop_control:
when: atlas_manage_storage | bool loop_var: atlas_samba_account
label: "{{ atlas_samba_account.username }}"
- name: Read Atlas Samba password marker
tags: [atlas, sharing]
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_content
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
tags: [atlas, sharing]
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_password | hash('sha256'))
}}
no_log: true no_log: true
when: atlas_manage_storage | bool when: atlas_manage_storage | bool
- name: Set Atlas Samba account password
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_admin_username }}"
stdin: "{{ atlas_samba_password }}\n{{ atlas_samba_password }}"
changed_when: true
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
tags: [atlas, sharing]
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker }}"
owner: root
group: root
mode: "0600"
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Require completed Atlas firewall placeholders - name: Require completed Atlas firewall placeholders
tags: [atlas, sharing, services] tags: [atlas, sharing, services]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET' - atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP'
- atlas_firewalld_zone | length > 0 - atlas_firewalld_zone | length > 0
fail_msg: Replace the Atlas LAN subnet and firewall zone placeholders. fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules - name: Apply Atlas firewalld rich rules

View File

@@ -5,12 +5,52 @@
atlas_zfs_datasets: atlas_zfs_datasets:
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}"
mountpoint: "{{ atlas_work_mountpoint }}" 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 }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
mountpoint: "{{ atlas_music_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_music_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
mountpoint: "{{ atlas_syncthing_mountpoint }}" mountpoint: "{{ atlas_syncthing_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_syncthing_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}"
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}"
mountpoint: "{{ atlas_icloud_photos_mountpoint }}" mountpoint: "{{ atlas_icloud_photos_mountpoint }}"
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_icloud_photos_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 - name: Require completed Atlas storage placeholders
tags: [atlas, storage] tags: [atlas, storage]
@@ -56,8 +96,7 @@
community.general.zfs: community.general.zfs:
name: "{{ item.name }}" name: "{{ item.name }}"
state: present state: present
extra_zfs_properties: extra_zfs_properties: "{{ item.properties }}"
mountpoint: "{{ item.mountpoint }}"
loop: "{{ atlas_zfs_datasets }}" loop: "{{ atlas_zfs_datasets }}"
loop_control: loop_control:
label: "{{ item.name }}" label: "{{ item.name }}"
@@ -68,8 +107,8 @@
ansible.builtin.file: ansible.builtin.file:
path: "{{ item.mountpoint }}" path: "{{ item.mountpoint }}"
state: directory state: directory
owner: "{{ atlas_admin_username }}" owner: "{{ item.owner }}"
group: "{{ atlas_admin_group }}" group: "{{ item.group }}"
mode: "0770" mode: "0770"
loop: "{{ atlas_zfs_datasets }}" loop: "{{ atlas_zfs_datasets }}"
loop_control: loop_control:

View File

@@ -0,0 +1,27 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich machine learning
[Container]
ContainerName=atlas-immich-machine-learning
Image={{ atlas_immich_machine_learning_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
{% for group_name in atlas_immich_supplementary_groups %}
GroupAdd={{ ansible_facts['getent_group'][group_name][1] }}
{% endfor %}
AddDevice=/dev/dri
Network=atlas-media.network
NetworkAlias=atlas-immich-machine-learning
Environment=TZ={{ atlas_timezone }}
Volume={{ atlas_immich_model_cache_dir }}:/cache:Z
Volume={{ atlas_immich_machine_learning_config_dir }}:/.config:Z
Volume={{ atlas_immich_machine_learning_cache_dir }}:/.cache:Z
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,22 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich PostgreSQL database
[Container]
ContainerName=atlas-immich-postgres
Image={{ atlas_immich_postgres_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
Network=atlas-media.network
NetworkAlias=atlas-immich-postgres
EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_postgres_data_dir }}:/var/lib/postgresql/data:Z
ShmSize=128m
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,20 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich Redis-compatible cache
[Container]
ContainerName=atlas-immich-redis
Image={{ atlas_immich_redis_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
Network=atlas-media.network
NetworkAlias=atlas-immich-redis
Volume={{ atlas_immich_redis_data_dir }}:/data:Z
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,29 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich server
Requires=atlas-immich-redis.service atlas-immich-postgres.service atlas-immich-machine-learning.service
After=atlas-immich-redis.service atlas-immich-postgres.service atlas-immich-machine-learning.service
[Container]
ContainerName=atlas-immich-server
Image={{ atlas_immich_server_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
{% for group_name in atlas_immich_supplementary_groups %}
GroupAdd={{ ansible_facts['getent_group'][group_name][1] }}
{% endfor %}
AddDevice=/dev/dri
Network=atlas-media.network
NetworkAlias=atlas-immich-server
EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_upload_dir }}:/data:Z
Volume={{ atlas_icloud_photos_mountpoint }}:/external/icloud_photos:ro,Z
Volume=/etc/localtime:/etc/localtime:ro
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,3 @@
# Managed by Ansible. Do not edit manually.
[Network]
NetworkName=atlas-media

View File

@@ -0,0 +1,21 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Nginx Proxy Manager
[Container]
ContainerName=atlas-npm
Image={{ atlas_npm_image }}
Network=atlas-media.network
NetworkAlias=atlas-npm
PublishPort=80:80
PublishPort=443:443
PublishPort={{ atlas_npm_admin_bind_address }}:81:81
Volume={{ atlas_npm_data_dir }}:/data:Z
Volume={{ atlas_npm_letsencrypt_dir }}:/etc/letsencrypt:Z
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -1,4 +1,4 @@
# Managed by Ansible. Do not edit manually. # Managed by Ansible. Do not edit manually.
{% for export in atlas_nfs_exports %} {% for export in atlas_nfs_exports %}
{{ export.path }} {{ export.clients }} {{ export.path }} {{ export.client }}({{ export.options | join(',') }})
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,13 @@
# Managed by Ansible. Contains a Vault secret; do not edit or copy.
TZ={{ atlas_timezone | to_json }}
DB_HOSTNAME="atlas-immich-postgres"
DB_USERNAME={{ atlas_immich_db_username | to_json }}
DB_PASSWORD={{ atlas_immich_db_password | to_json }}
DB_DATABASE_NAME={{ atlas_immich_db_name | to_json }}
DB_VECTOR_EXTENSION="vectorchord"
POSTGRES_USER={{ atlas_immich_db_username | to_json }}
POSTGRES_PASSWORD={{ atlas_immich_db_password | to_json }}
POSTGRES_DB={{ atlas_immich_db_name | to_json }}
POSTGRES_INITDB_ARGS="--data-checksums"
REDIS_HOSTNAME="atlas-immich-redis"
IMMICH_MACHINE_LEARNING_URL="http://atlas-immich-machine-learning:3003"

View File

@@ -3,11 +3,12 @@
workgroup = {{ atlas_samba_workgroup }} workgroup = {{ atlas_samba_workgroup }}
security = user security = user
map to guest = Never map to guest = Never
server min protocol = SMB2 server min protocol = SMB3
hosts allow = {{ atlas_lan_subnet }}
include = registry include = registry
[{{ atlas_samba_share_name }}] [{{ atlas_samba_share_name }}]
path = {{ atlas_work_mountpoint }} path = {{ atlas_archive_mountpoint }}
browseable = yes browseable = yes
read only = no read only = no
valid users = {{ atlas_samba_valid_users | join(' ') }} valid users = {{ atlas_samba_valid_users | join(' ') }}

View File

@@ -9,3 +9,29 @@
tags: [services] tags: [services]
ansible.builtin.systemd: ansible.builtin.systemd:
daemon_reload: true daemon_reload: true
- name: Restart rclone music mount
tags: [services, rclone, navidrome]
ansible.builtin.systemd:
name: "{{ server_rclone_music_service }}"
state: restarted
daemon_reload: true
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode
- name: Restart rootless Navidrome
tags: [services, podman, rclone, navidrome]
become_user: "{{ server_username }}"
ansible.builtin.systemd:
name: navidrome.service
scope: user
state: restarted
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}"
DBUS_SESSION_BUS_ADDRESS: >-
unix:path=/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}/bus
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -8,15 +8,16 @@
fail_msg: >- fail_msg: >-
server_firewall_backend must be firewalld for the Rocky server profile. server_firewall_backend must be firewalld for the Rocky server profile.
- name: Require server container secret variables - name: Require server database secret variables
tags: [dotfiles, dotfiles:server, services] tags: [dotfiles, dotfiles:server, services, navidrome]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- (vault_navidrome_db_password | default('')) | length > 0 - server_navidrome_db_password | length > 0
- (vault_postgres_root_password | default('')) | length > 0 - (vault_postgres_root_password | default('')) | length > 0
fail_msg: >- fail_msg: >-
Server container secrets are missing. Define vault_navidrome_db_password and Define vault_navidrome_db_password and vault_postgres_root_password in Vault
vault_postgres_root_password in secrets/vault.yml or another vars source. before rendering the Navidrome database configuration.
no_log: true
- name: Configure DuckDNS updater - name: Configure DuckDNS updater
tags: [dotfiles, dotfiles:server, duckdns] tags: [dotfiles, dotfiles:server, duckdns]
@@ -71,6 +72,12 @@
tags: [services, podman] tags: [services, podman]
ansible.builtin.include_tasks: podman-compose.yml ansible.builtin.include_tasks: podman-compose.yml
- name: Configure Atlas music mount
ansible.builtin.import_tasks: rclone-music.yml
- name: Configure rootless Navidrome
ansible.builtin.import_tasks: navidrome.yml
- name: Disable SSH root login on server - name: Disable SSH root login on server
tags: [services] tags: [services]
ansible.builtin.lineinfile: ansible.builtin.lineinfile:

View File

@@ -0,0 +1,114 @@
---
- name: Read server account details for rootless Navidrome
tags: [services, podman, rclone, navidrome]
ansible.builtin.getent:
database: passwd
key: "{{ server_username }}"
when: server_atlas_music_enabled | bool
- name: Check for a running legacy rootful Navidrome container
tags: [services, podman, navidrome]
ansible.builtin.command:
argv:
- podman
- container
- inspect
- --format
- "{{ '{{.State.Running}}' }}"
- navidrome
register: server_legacy_navidrome_container
changed_when: false
failed_when: false
check_mode: false
when: server_atlas_music_enabled | bool
- name: Refuse concurrent legacy and rootless Navidrome
tags: [services, podman, navidrome]
ansible.builtin.assert:
that:
- >-
server_legacy_navidrome_container.rc != 0
or server_legacy_navidrome_container.stdout | trim != 'true'
fail_msg: >-
Stop the legacy rootful Navidrome container before enabling the rootless Quadlet.
The playbook does not remove the old container or its data automatically.
when: server_atlas_music_enabled | bool
- name: Enable lingering for rootless Navidrome
tags: [services, podman, navidrome]
ansible.builtin.command:
argv:
- loginctl
- enable-linger
- "{{ server_username }}"
creates: "/var/lib/systemd/linger/{{ server_username }}"
when: server_atlas_music_enabled | bool
- name: Start server user systemd manager
tags: [services, podman, navidrome]
ansible.builtin.systemd:
name: "user@{{ ansible_facts['getent_passwd'][server_username][1] }}.service"
state: started
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode
- name: Create rootless Navidrome Quadlet directory
tags: [services, podman, navidrome]
ansible.builtin.file:
path: "{{ server_navidrome_quadlet_dir }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_atlas_music_enabled | bool
- name: Create private rootless Navidrome configuration directory
tags: [services, podman, navidrome]
ansible.builtin.file:
path: "{{ server_navidrome_env_dir }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_atlas_music_enabled | bool
- name: Render Vault-backed rootless Navidrome environment
tags: [services, podman, navidrome]
ansible.builtin.template:
src: navidrome.env.j2
dest: "{{ server_navidrome_env_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
no_log: true
diff: false
notify: Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Render rootless Navidrome Quadlet
tags: [services, podman, navidrome]
ansible.builtin.template:
src: navidrome.container.j2
dest: "{{ server_navidrome_quadlet_dir }}/navidrome.container"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0644"
notify: Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Start rootless Navidrome Quadlet
tags: [services, podman, navidrome]
become_user: "{{ server_username }}"
ansible.builtin.systemd:
name: navidrome.service
scope: user
state: started
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}"
DBUS_SESSION_BUS_ADDRESS: >-
unix:path=/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}/bus
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -0,0 +1,105 @@
---
- name: Require completed Atlas SFTP mount configuration
tags: [services, rclone, navidrome]
ansible.builtin.assert:
that:
- server_atlas_sftp_host != 'CHANGEME_ATLAS_WIREGUARD_IP'
- server_atlas_sftp_known_host != 'CHANGEME_ATLAS_SSH_HOST_KEY'
- server_atlas_sftp_known_host | length > 0
- server_atlas_sftp_private_key | length > 0
- server_atlas_sftp_remote_path == '/pool/media/music'
fail_msg: >-
Define the Atlas WireGuard address, pinned SSH host key and Vault-backed
SFTP private key before enabling the music mount.
no_log: true
when: server_atlas_music_enabled | bool
- name: Create private rclone configuration directory
tags: [services, rclone, navidrome]
ansible.builtin.file:
path: "{{ server_rclone_config_dir }}"
state: directory
owner: root
group: "{{ server_user_group }}"
mode: "0750"
when: server_atlas_music_enabled | bool
- name: Install Vault-backed Atlas SFTP private key
tags: [services, rclone, navidrome]
ansible.builtin.copy:
content: "{{ server_atlas_sftp_private_key | trim }}\n"
dest: "{{ server_atlas_sftp_private_key_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
no_log: true
diff: false
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Install pinned Atlas SSH host key
tags: [services, rclone, navidrome]
ansible.builtin.copy:
content: "{{ server_atlas_sftp_known_host | trim }}\n"
dest: "{{ server_atlas_sftp_known_hosts_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0640"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Render Atlas SFTP rclone configuration
tags: [services, rclone, navidrome]
ansible.builtin.template:
src: rclone.conf.j2
dest: "{{ server_rclone_config_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Create rclone music directories
tags: [services, rclone, navidrome]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0750"
loop:
- "{{ server_rclone_music_mountpoint }}"
- "{{ server_rclone_music_cache_dir }}"
loop_control:
label: "{{ item }}"
when: server_atlas_music_enabled | bool
- name: Render rclone music system service
tags: [services, rclone, navidrome]
ansible.builtin.template:
src: rclone-music.service.j2
dest: "/etc/systemd/system/{{ server_rclone_music_service }}"
owner: root
group: root
mode: "0644"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Enable and start rclone music mount
tags: [services, rclone, navidrome]
ansible.builtin.systemd:
name: "{{ server_rclone_music_service }}"
enabled: true
state: started
daemon_reload: true
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -0,0 +1,21 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Rootless Navidrome music server
[Container]
ContainerName=navidrome
Image={{ server_navidrome_image }}
Network=host
EnvironmentFile={{ server_navidrome_env_file }}
Volume={{ server_navidrome_data_dir }}:/data
Volume={{ server_rclone_music_mountpoint }}:/music:ro
SecurityLabelDisable=true
[Service]
ExecStartPre=/usr/bin/mountpoint -q {{ server_rclone_music_mountpoint }}
Restart=always
RestartSec=10s
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,7 @@
# Managed by Ansible. Contains a Vault secret; do not edit or copy.
ND_DATABASE_URL={{ ('postgres://navidrome:' ~ server_navidrome_db_password ~ '@127.0.0.1:5432/navidrome_db?sslmode=disable') | to_json }}
ND_PORT={{ server_navidrome_port }}
ND_SCANSCHEDULE="1h"
ND_SESSIONTIMEOUT="24h"
ND_ENABLETRANSCODING="true"
ND_LOGLEVEL="info"

View File

@@ -0,0 +1,29 @@
[Unit]
Description=Read-only Atlas music mount via rclone SFTP
Wants=network-online.target
After=network-online.target
{% if server_atlas_wireguard_unit | length > 0 %}
Wants={{ server_atlas_wireguard_unit }}
After={{ server_atlas_wireguard_unit }}
{% endif %}
[Service]
Type=notify
User={{ server_username }}
Group={{ server_user_group }}
ExecStart=/usr/bin/rclone mount \
{{ server_atlas_sftp_remote_name }}:{{ server_atlas_sftp_remote_path }} \
{{ server_rclone_music_mountpoint }} \
--config {{ server_rclone_config_file }} \
--cache-dir {{ server_rclone_music_cache_dir }} \
--read-only \
--vfs-cache-mode full \
--vfs-cache-max-size 15G \
--vfs-read-chunk-size 5M \
--dir-cache-time 24h
ExecStop=-/usr/bin/fusermount3 -uz {{ server_rclone_music_mountpoint }}
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,9 @@
# Managed by Ansible. Authentication uses the separately deployed Vault-backed key.
[{{ server_atlas_sftp_remote_name }}]
type = sftp
host = {{ server_atlas_sftp_host }}
user = {{ server_atlas_sftp_username }}
port = {{ server_atlas_sftp_port }}
key_file = {{ server_atlas_sftp_private_key_file }}
known_hosts_file = {{ server_atlas_sftp_known_hosts_file }}
shell_type = unix

View File

@@ -3,25 +3,6 @@
name: server name: server
services: services:
navidrome:
image: docker.io/deluan/navidrome:latest
container_name: navidrome
restart: unless-stopped
expose:
- "4533"
environment:
ND_DATABASE_URL: "postgres://navidrome:{{ vault_navidrome_db_password }}@navidromedb:5432/navidrome_db?sslmode=disable"
ND_SESSIONTIMEOUT: 24h
ND_ENABLETRANSCODING: "true"
volumes:
- "/opt/navidrome/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/music:/music:ro{{ ',' ~ selinux_volume_option if selinux_volume_option else '' }}"
user: "1000:1000"
networks:
- web
depends_on:
- navidromedb
nginx-proxy-manager: nginx-proxy-manager:
image: docker.io/jc21/nginx-proxy-manager:latest image: docker.io/jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager container_name: nginx-proxy-manager
@@ -30,6 +11,8 @@ services:
- "80:80" - "80:80"
- "443:443" - "443:443"
- "127.0.0.1:81:81" - "127.0.0.1:81:81"
extra_hosts:
- "host.containers.internal:host-gateway"
volumes: volumes:
- "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
@@ -64,11 +47,13 @@ services:
POSTGRES_DB: "navidrome_db" POSTGRES_DB: "navidrome_db"
POSTGRES_USER: "navidrome" POSTGRES_USER: "navidrome"
POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}" POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}"
volumes: volumes:
- "/opt/postgres/data:/var/lib/postgresql/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/postgres/data:/var/lib/postgresql/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
ports:
- "127.0.0.1:5432:5432"
networks: networks:
- web - web
gitea: gitea:
image: docker.gitea.com/gitea:1.25.2 image: docker.gitea.com/gitea:1.25.2
container_name: gitea container_name: gitea

View File

@@ -1,45 +1,61 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
62633966363435396136666635303362663932366464323231363563373462383134326361346533 38633239386338323332633564613266393665666562353139646234343261303131336663316634
3465353461326565356435633866393230383838356235340a333036383038653134323066663031 6535313265356434373464326664303937353132643636630a373966646565663564613463643732
37373135643331356366373831343163363835656462343962636538623937356231376337616330 35646136346564636565616134303064313636643263343765396138653830646664633366656365
3831626261343761310a306133633264613430613165343265303664383439333230663665333231 3137643164396265650a663338346161333832643732653539366462323039313563383735626661
30393934323438333066383737663665316634613932353232343334626261636661393434646163 62626461656636366634353731613932653235323463336263363866336466356534636435313831
63316537383866353338353261373161623562343037383032633932383630666339633234326639 64373235383930663232613932623935346465643633666233386461346635353663323638633964
64303735663532376433393464616230616236353134383734623132333061356664613039323265 61366664323631313631663662643437386365616162333161636166666635636633333061323739
35373434336261313438633866643765323330623336343732346465613264613666616631303633 38386330326534303966613165656363633838313366666164656133386263646561306139383032
31333437623437626135623032306434306538323461623161336431353630336539646531313365 66646133643239396436653564323837623031393439303938323138326330636633613864623565
30356330356438353037663262643463363636386162393764323133373261376263663338636333 62616335633563333962653561633665643336303230623934356565363531323237643433306436
33336537383061643234356365653137333639623863303530666665373663376166316533623737 33363238306438333765323034636261333830653836656538636662303363363733356462353764
31616262363132326133666230396636663035343432633830323430336332326261613932633837 34636136666563333138383663643931363039303737313763373765356336656564633665666530
62303232306331663936366435666164383335313335326662663363643563643938623739386663 32346262636530646365636565656139623639346262373963346462626638316162616666373339
65356431353362623634333437346566363166366161313132653131613861386437306562633934 33353031313365356537646237356139613834343737343361636133343837613638643432346134
38306135636161366530303461636665386639396639356265323361623639616265323462333534 64313232646638633035383234303934666139616162356431306161353866316136663830343737
61626439616631356532353437653539333631636265346238653439313130326239633035613130 38646564393936383939373435313363653338653737346333643165626437386366346339303764
65366161356231356638313633393735356465393339343034356138316366656431336435386437 34623735613130643636623733353161393034303964656635366466396362393435376565313832
36366339366463396638303062366263356436666232393437613766623631313264646565316534 32623936646534656166383930666133663132663363623435393434663734363533653036663230
32373534366363373737616563616161346265323936336665653934656365323731633637376331 30663130626237303938613236306465613534346466663333663432316539653836323030643636
31336262356632313533393366346565373434373134653630303431643037363561613830663339 64646564643435373938363430303531333461613438633435356231656666373261636135396534
39313837356362643164386432653030306132643234303162363463376130666239653461623736 66343435663835646161376636633662353337386238316131623737353364383130653334643864
65333734303532356462636538326430353831336562666563336333323764346234643037373064 63353836303230303263343939373436646262346565313039323037383966636636656638366231
63343533386564356232633661346232666135393365323133323365663637373261396264333432 31343431373632363031346635663834393036623166346262393736346361323837303462346130
64616463643931653162373666323731653635616633386231653863623830666637623638353062 66643563306633323433616461343364326162336435363534326237366665316638353365376138
61363339613364343239363336626266386561343032333262396436663736353835303863643865 61633263643637323031613462303138303133623136323637633862623032623465616538643331
35616136363830326661383066316537633537616565343361633036373163316566653638343638 31343134353166383964666431353231636339636366626164346461386339356437386261336365
33323162376537353961383965666165663631396663356533643432356630396435356636366636 36373936356534623035663531353132633837333366663732393661613162316234373137333861
32363264303633656335333335356166323164386338363437663435336635643334646532616539 34656465343231623532343231646565316664373662626134346530653164393966333738656130
66646336393834613337336661336638613463626230336132386265396165656461623533333738 62663238373135643033396334386236313531646131313765323039306431323938386163316434
33353063306336623463396130386335383338383861376330616566643638333230303862356462 64616530343431303865336431666662343766303931653336376131313633306431346165623335
63356464313263666538306230636363643665643933323236363864303634343539326239363134 64383262626332336430373337313462613766613931613337393335393237616463646631623562
65376231623836363730376631303032393330326362383438373434383633656134373539653461 35343938323933663835313334306536343231663866616566653434366564313134393831343362
38306632636463346365396131316234623736626335633361663861316437326635633237346262 63396630366338626133383232666265383266396166306536633233306139666531613031643262
38643730666130656335663232366362333164353433613731363131366138633766623433363031 37356262363466303032336264333864646632373533396239393565653538383161653762313432
37613337383961386661393733623238393062663065663931313231366366626261666336656134 37313230666237626530316133393134303133613463363362323132653932333663313236623639
63646237643864643462653337306462646631663433663461343764306163646466326334323335 65326665626432386132323131353838346439393061343963616565306464636264626630396230
61396162353032633937333066313438333461376161666166336337356430373737313266363238 31373963326134383762373737626238353964383565336132623836333535313635316637653966
63633365613331636234333765633536653639343234396262653438353132623239303431316134 39386563396661663439343463353262656237616633646466323638346362376335306330373161
35663930323535373830313766353630636237343330346232316637336531336466326564306630 37343431316163626365386130383862643461363132636138646634663832383933303937643934
62356639356330396465353432333135313130653366303931616165663638633461613961643766 34326131353262643165336138643835333066663432353263333939633534383137393136633034
62633564666566616665366336396436386330323536356633323965343566396232303434666534 35386538616633336230353332306631353566343831376662306264386330623631653437393666
33333636313136326535303136643265393834613733343362346333336339636437353236326161 30646438666663383334333431336230373635663162626661373539336434646131346238623435
65633230656531633238623830343432663365313532343435623133636239333763633930316666 32303361366565353161646136386237366639343664616637303232376232316334366437636630
34363535313731666432 37633866376433633537373166613436303335663766303539336332366231353435646163386538
66323963303034393237323035366634303736653735623937323763336234376563663563333132
39646237306334326635353563326536633764306534313535383836633033373832316336383530
33316432356366363934666264306661373037663563333165633132643834353262376661636463
63326662663739616139393438393734353833376239363531376261303639376363646532666465
66356533393331393535393161653131343536353535373432323463346331646663353439383237
37386262633765326331383335613263653331303561343636626337333265313633373533363331
32373564646464626633323833663432663562363639373931623535306534646664346165356161
33306330363239386439646563353865646339323633646535316336363239303564373937636233
37396434353830386130316237386532333939363330306165633630336662333537663265363736
61636437623832383032643539626338313932303335333461363566363532663633353631353933
62376531303638303838663737373462323339633632626439353439396139656639373331656636
61303531383433343961666336363231353638656137666364323133326233623964363536303065
33623130653564363261653333343732653539386565326133386165313536613732366463303537
63616538383536636232636562653863323334633839656366373761626561656462316161323630
33396566333463326136626231653536336134653232306536396434636438366335633137623433
31636362366431396364

View File

@@ -13,3 +13,10 @@ vault_navidrome_db_password: "REPLACE_ME"
vault_postgres_root_password: "REPLACE_ME" vault_postgres_root_password: "REPLACE_ME"
vault_ikaros_authorized_ssh_keys: vault_ikaros_authorized_ssh_keys:
- "ssh-ed25519 REPLACE_ME" - "ssh-ed25519 REPLACE_ME"
vault_atlas_authorized_ssh_keys:
- "ssh-ed25519 REPLACE_ME atlas-admin"
vault_atlas_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH"
vault_atlas_samba_password: "REPLACE_ME"
vault_atlas_immich_db_password: "REPLACE_ME"
vault_prometheus_atlas_sftp_private_key: |
REPLACE_WITH_A_DEDICATED_ATLAS_SFTP_PRIVATE_KEY