Compare commits

...

45 Commits

Author SHA1 Message Date
Fabio Scotto di Santolo
76f3e10ee4 Verify WireGuard handshakes and restore Podman networking 2026-09-15 22:38:56 +02:00
Fabio Scotto di Santolo
c899bb7192 Harden Rocky storage and sharing configuration 2026-09-15 22:05:48 +02:00
Fabio Scotto di Santolo
2ab5ba6818 Manage SSH authorized key fragments for infrastructure hosts 2026-09-13 15:16:40 +02:00
Fabio Scotto di Santolo
a347880d4c Manage Aegis SSH authorized key fragments 2026-09-13 14:46:08 +02:00
Fabio Scotto di Santolo
b05a2b4e74 Allow configurable Aegis SSH users and authorized keys 2026-09-13 14:35:43 +02:00
Fabio Scotto di Santolo
75d9081a11 Rotate Ansible Vault secrets 2026-09-13 13:53:03 +02:00
Fabio Scotto di Santolo
b4b3bd10b8 Harden Atlas network, SSH, firewall, and sharing 2026-09-13 00:01:48 +02:00
Fabio Scotto di Santolo
db10d1296e Enable Atlas NAS management and document bootstrap workflow 2026-09-12 18:53:21 +02:00
Fabio Scotto di Santolo
8c35ef63c9 Document Atlas backend phase one and WireGuard deployment 2026-09-12 17:19:20 +02:00
Fabio Scotto di Santolo
64aebe8c34 Add Atlas media and storage services 2026-09-10 22:27:33 +02:00
Fabio Scotto di Santolo
569e6ef24e Fix desktop dotfile idempotency 2026-09-08 23:24:53 +02:00
Fabio Scotto di Santolo
ab8c51a57d Migrate Prometheus stack to Podman 2026-09-08 23:12:41 +02:00
Fabio Scotto di Santolo
7203ac6e2d Install cockpit and enable socket on prometheus 2026-09-08 19:17:22 +02:00
Fabio Scotto di Santolo
fc8df74050 Keep podman on rocky server for future Docker migration 2026-09-08 19:17:20 +02:00
Fabio Scotto di Santolo
350fc7ac95 Rename SSH host alias vps to prometheus-ubuntu 2026-09-08 18:56:00 +02:00
Fabio Scotto di Santolo
ab0bbf30ab Document DNS Filter role for Aegis 2026-09-08 18:55:54 +02:00
Fabio Scotto di Santolo
24268938bd Hotfix/duckdns token (#8)
* fix: source DuckDNS token from Vault

* chore: rotate DuckDNS Vault token
2026-09-08 18:48:13 +02:00
Fabio Scotto di Santolo
54e2917062 chore: uninstall GitHub Copilot agent 2026-09-08 10:42:25 +02:00
Fabio Scotto di Santolo
eab66b6d3d Add dormant Rocky server profile (#2)
* Add dormant Rocky server profile

* feat: activate Rocky server profile for prometheus

* fix: complete Rocky server migration tooling

* Remove FreeBSD profile

* feat: add Aegis Fedora CoreOS profile

* docs: define Git Flow branch prefixes

* Add Aegis TPM-backed btrfs storage

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Preserve existing Aegis filesystem tables

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Remove unsupported Aegis TPM storage

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Configure Aegis pi Btrfs storage

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Mount Aegis Btrfs as root

Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>

* Handle Maven on Mise

* Feature/aegis coreos (#7)

* feat(aegis): add ARM image bootstrap workflow

* feat(aegis): manage firewall and SSH hardening

* feat(aegis): harden Fedora IoT services

* chore(aegis): remove local TLS configuration

* fix(aegis): restore supported Fedora IoT bootstrap

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-08 10:36:29 +02:00
Fabio Scotto di Santolo
ae1ca44aa7 Feature/aegis coreos (#7)
* feat(aegis): add ARM image bootstrap workflow

* feat(aegis): manage firewall and SSH hardening

* feat(aegis): harden Fedora IoT services

* chore(aegis): remove local TLS configuration

* fix(aegis): restore supported Fedora IoT bootstrap
2026-09-05 16:58:53 +02:00
Fabio Scotto di Santolo
295f1a62ad Handle Maven on Mise 2026-09-04 09:36:01 +02:00
Copilot
6da430b9e4 Merge pull request #5 from fscotto/copilot/featureadd-pi-user-and-encrypt-filesystem
Configure Aegis with a pi user and Btrfs root filesystem
2026-09-04 01:04:28 +02:00
copilot-swe-agent[bot]
e92ff1c729 Mount Aegis Btrfs as root
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-03 23:02:42 +00:00
copilot-swe-agent[bot]
8d3f67885c Configure Aegis pi Btrfs storage
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-03 22:51:57 +00:00
copilot-swe-agent[bot]
bc9b381525 Remove unsupported Aegis TPM storage
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-03 22:48:04 +00:00
copilot-swe-agent[bot]
2004b48cf3 Preserve existing Aegis filesystem tables
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-03 22:44:21 +00:00
copilot-swe-agent[bot]
54e1e88a4e Add Aegis TPM-backed btrfs storage
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-09-03 22:43:54 +00:00
Fabio Scotto di Santolo
cb88e95385 docs: define Git Flow branch prefixes 2026-09-01 22:44:14 +02:00
Fabio Scotto di Santolo
764d329a8f feat: add Aegis Fedora CoreOS profile 2026-09-01 22:38:49 +02:00
Fabio Scotto di Santolo
1517c92866 Merge pull request #3 from fscotto/remove-freebsd-profile
Remove FreeBSD profile
2026-08-31 21:39:44 +02:00
Fabio Scotto di Santolo
6296925f2d Remove FreeBSD profile 2026-08-31 21:38:13 +02:00
Fabio Scotto di Santolo
45340103d0 feat: manage Java 11 with Mise on Fedora WSL 2026-08-31 18:20:52 +02:00
Fabio Scotto di Santolo
67b576d9b3 Fix Git config on Fedora WSL 2026-08-31 17:49:48 +02:00
Fabio Scotto di Santolo
5914ae1557 feat: use BobIDE as diff/merge tool for workstation 2026-08-31 12:51:53 +02:00
Fabio Scotto di Santolo
671581d717 feat: manage coding agents independently 2026-08-31 10:02:32 +02:00
Fabio Scotto di Santolo
7f6a45f614 Fix GitHub Copilot CLI 2026-08-30 23:56:36 +02:00
Fabio Scotto di Santolo
aadebbebd0 Add Raspberry Pi Imager to Fedora desktops 2026-08-30 23:37:24 +02:00
Fabio Scotto di Santolo
bb907febf8 Update Atlas offsite backup plan 2026-08-30 23:30:16 +02:00
Fabio Scotto di Santolo
2c1f58ce8e Add Seahorse and GSConnect to GNOME desktops 2026-08-30 16:45:44 +02:00
Fabio Scotto di Santolo
10bf0ee685 Sync Ikaros app picker layout 2026-08-30 16:37:10 +02:00
Fabio Scotto di Santolo
17ffa58e19 Use native Evolution desktop client 2026-08-30 16:32:09 +02:00
Fabio Scotto di Santolo
097374f2c5 Replace LibreOffice with OnlyOffice Flatpak 2026-08-30 16:07:01 +02:00
Fabio Scotto di Santolo
8ed439317a Add Rocky 9 Atlas NAS profile 2026-08-30 14:40:01 +02:00
Fabio Scotto di Santolo
bc5a7572a7 Add English README 2026-08-28 11:51:23 +02:00
Fabio Scotto di Santolo
a6a58245ff Refactor workstation to Fedora WSL target 2026-08-28 11:41:14 +02:00
109 changed files with 4975 additions and 1904 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# DuckDNS runtime files contain a rendered Vault token and must stay on the host.
/dotfiles/server/duckdns/

156
AGENTS.md
View File

@@ -1,6 +1,6 @@
# AGENTS.md
Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBSD transition targets, WSL, and an Ubuntu server.
Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora IoT, WSL, a Rocky Linux 9 server, and an Atlas NAS.
## Source Of Truth
- Main orchestration: `ansible/site.yml`
@@ -14,8 +14,10 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBS
- Current personal desktop: `ikaros = platform_fedora + role_personal_workstation + graphical_desktop + desktop_gnome`
- Current laptop: `nymph = platform_fedora + graphical_desktop + desktop_gnome`
- Void desktop profile is also the base for other future/reference hosts via `platform_void + graphical_desktop`
- Workstation: `deadalus` is Windows + WSL; Ansible targets are `deadalus-wsl` (Ubuntu) and `deadalus-fedora-wsl` (Fedora)
- Ubuntu server: `prometheus`
- Workstation: `deadalus` is Windows + Fedora WSL.
- Rocky server: `prometheus` belongs to `rocky_server`.
- NAS: `atlas` (Rocky Linux 9, reached through SSH)
- Always-on LAN node: `aegis` (Fedora IoT on Raspberry Pi 4, reached through SSH)
- Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions.
- Inventory axes are independent: `platform_*`, `role_*`, and `desktop_*`. Legacy `void` and `desktop` remain compatibility parents.
@@ -23,7 +25,10 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBS
- Preserve layering `all -> platform -> role -> desktop -> host`.
- Keep `ansible/site.yml` small; orchestration belongs there, implementation belongs in roles.
- Prefer minimal, targeted edits. Preserve idempotency and existing ordering.
- All hosts use `ansible_connection: local`.
- Use Git Flow branch prefixes: `feature/` for new functionality, `bugfix/` for non-urgent fixes,
`hotfix/` for urgent production fixes, `release/` for release preparation, and `support/` for
maintained release lines. Do not use abbreviated prefixes such as `feat/`.
- Desktop and WSL hosts use `ansible_connection: local`; remote infrastructure hosts use SSH.
- Treat `secrets/` as sensitive. Never print secret values.
- Tmux plugins are bootstrapped by TPM on the host; the repo only keeps tmux config and custom helper scripts.
- Read the relevant role tasks, templates, vars, and deployed dotfiles before editing.
@@ -38,13 +43,25 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBS
- Host-focused dry runs:
- Fedora desktop work: `ansible-playbook ansible/site.yml --limit ikaros --check --diff`
- Fedora laptop work: `ansible-playbook ansible/site.yml --limit nymph --check --diff`
- Ubuntu WSL dev: `ansible-playbook ansible/site.yml --limit deadalus-wsl --check --diff`
- Fedora WSL dev: `ansible-playbook ansible/site.yml --limit deadalus-fedora-wsl --check --diff`
- WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff`
- Server: `ansible-playbook ansible/site.yml --limit prometheus --check --diff`
- Rocky server after activation: `ansible-playbook ansible/site.yml --limit <host> --check --diff`
- Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --check --diff`
- Aegis IoT: `ansible-playbook ansible/site.yml --limit aegis --check --diff`
- Focused checks:
- Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit <host> --tags emacs --check --diff -e emacs_enabled=true`
- 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`
- Server compose render: `docker compose -f /opt/docker/server/docker-compose.yml config`
- 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 network/share hardening:
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
- Atlas phase-one rootless services:
`ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff`
- Prometheus/Atlas WireGuard overlay:
`ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard --check --diff`
- DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff`
## Conventions
- Use FQCN Ansible modules.
@@ -76,19 +93,116 @@ The Void desktop package lists in `ansible/inventory/group_vars/void.yml` are ke
- `desktop_common_packages` — GUI infrastructure shared by the minimal desktop mode.
- `desktop_minimal_packages` — applications, integration components, and the `emptty` display manager.
- `desktop_sway_packages` — binaries specific to the Sway session.
`profile_packages` in the same file is cross-distro and is overridden by `group_vars/server.yml` and the workstation group vars; do not move desktop-specific Void entries through it.
`profile_packages` remains the shared package bucket for Void and Fedora profiles. Rocky uses
`rocky_profile_packages` so RPM-specific names do not leak back into the other platforms; do not move
desktop-specific Void entries through either bucket.
The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-independent content and `desktop_minimal_dotfiles` carries Thunar, Udiskie, and MIME defaults. `desktop_void_dotfiles` remains reserved for files that need the Void runtime.
## Workstation Notes
- `deadalus` is modeled as Windows + WSL; keep Linux dev automation on `deadalus-wsl` (Ubuntu) and `deadalus-fedora-wsl` (Fedora).
- `deadalus` is modeled as Windows + Fedora WSL and is the sole workstation target.
- Fedora WSL belongs to `platform_fedora`, `workstation_dev_fedora`, and the shared WSL layer. It must not receive Flatpak or Snap runtimes.
- Native Linux workstation groups remain available for future hosts but have no current host in the main inventory.
- Fedora WSL installs Mise from the official `jdxcode/mise` COPR and uses its pinned Temurin Java 11 JDK; update the declared Mise version deliberately.
- Windows applications are installed manually and are not managed from the WSL profile.
## Rocky Server Notes
- DuckDNS is rendered by `profile_server` from host-local `server_duckdns_domain` and
`vault_duckdns_token`. Keep the rotated token in encrypted Vault or untracked local vars, never in
dotfiles. The private `~/duckdns/duck.sh` keeps the existing entrypoint; rendering uses `no_log`
and disables diffs. Provisioning does not execute the updater or change its external schedule.
- `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 Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the
existing Nginx Proxy Manager/Gitea Compose stack with a `podman-compose-server` systemd unit. PostgreSQL and
Navidrome are no longer part of the desired Prometheus configuration. The role does not stop or remove legacy
containers, delete `/opt/postgres/data`, start the Compose stack, update DNS, or cut over traffic.
- 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.
Nextcloud remains disabled; do not provision `/srv/nextcloud` directories.
- `scripts/migrate_prometheus_data.sh` is the separate, source-host-run NPM/Gitea migration path. It dry-runs by
default and requires explicit source-stack quiescing before copying persistent Docker data with rsync.
- Atlas-only OpenZFS, NFS, Samba, and Syncthing stay selected through Atlas host variables and must not
leak into `rocky_server`. Cockpit plus its Navigator and Podman extensions are selected explicitly for
Prometheus through its host variables.
## Atlas NAS Notes
- `atlas` is a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values in
`host_vars/atlas.yml`. Bootstrap it once with `-e atlas_connection_username=<existing-admin>`;
subsequent runs use the dedicated Atlas account.
- The pool is normally pre-existing. A one-time bootstrap may create it only when `atlas_create_pool=true`
is explicitly supplied and `atlas_zpool_disks` contains exactly four real `/dev/disk/by-id/...` paths.
Never partition, force, destroy, roll back, or modify the vdev layout of an existing pool.
- `atlas_manage_storage`, `atlas_manage_sharing`, and `atlas_manage_firewall` are enabled in Atlas host vars as
the declared steady state; set one false only for a deliberate suspension. `atlas_manage_media_stack` remains false
until the future rootful Immich stack has its required Vault inputs and target validation.
- Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled,
`vault_atlas_samba_password`. The future rootful media stack also requires
`vault_atlas_immich_db_password`. Never print these values.
- Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `work`, `archive`,
`archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`,
`media/photobook`, `backups`, `backups/services`, and `backup_prometheus`. `backups/services` has a `500G`
refreservation. There is no separate legacy `zpool/syncthing` dataset.
- The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only
the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive
`/dev/dri`, while the Photobook external library is read-only at `/external/photobook`.
- Atlas applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, agent and remote forwarding
are disabled, while local forwarding remains available for private administrative tunnels. Photobook is exported only to the configured Aegis IP with all access squashed to UID/GID
`1100`. Targeted SELinux is enforced persistently; a required reboot is reported but never initiated automatically. The primary LAN interface is assigned explicitly to the managed firewalld zone, and firewall rules are applied before NFS or SMB are started; their service state and TCP listeners are then verified. SMB3 exposes `Archive` to Vault-backed authorized accounts on mandatory encrypted, signed SMB3 over TCP/445 only 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.
- `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome
`0.63.2` uses SQLite below `/data` and does not support `ND_DATABASE_URL` or an external PostgreSQL backend; do not
recreate the obsolete Prometheus `navidromedb` service. The role requires the storage role's `zpool/media/music`,
`zpool/archive/app_data`, `zpool/archive/app_data/navidrome`, and `zpool/archive/app_data/syncthing` datasets at
their exact paths. It never creates the pool.
- Keep `backend_phase1_start_services` false until the stopped Prometheus Navidrome data directory has been copied to
Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application
data; after cutover, set the flag true to enable and start Navidrome and Syncthing.
- Phase 1 must not change Prometheus' existing NPM deployment. NPM continues to be managed exactly by `profile_server`;
use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Syncthing does not use host networking:
its GUI, transfer, QUIC and discovery ports are explicitly published only on `10.0.0.2`; native transfer/discovery does not use the HTTP proxy.
- `wireguard_overlay` manages the required `wg0` path between Prometheus and Atlas, persists private keys only on their
respective hosts, exchanges only derived public keys, and verifies a real peer handshake. The initial run must
include both hosts. Prometheus
opens `51820/udp`; after creating the WireGuard firewalld zone, restore Prometheus' rootful Podman networking with
`podman network reload --all` so the existing proxy stack retains container DNS. The Atlas backend role admits
service ports only in the WireGuard firewalld zone.
## Atlas NAS TODO
- Provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host.
Before the first apply, confirm the pool, mountpoints, LAN subnet and firewalld zone. Keep
`atlas_manage_media_stack` disabled until `/dev/dri`, the container paths and the Immich database secret are validated.
- 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.
- Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files
through both NFS and SMB before considering multiprotocol access complete.
- Add Ansible-managed ZFS snapshot retention and scrub timers. Use Cockpit Scheduler for visibility
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
or API access for the selected clients.
- Validate the managed WireGuard path and its LAN/VPN-only firewalld rules before enabling remote services;
never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the Atlas-initiated least-privilege Prometheus backup pull: Prometheus exposes only prepared
read-only dumps through a dedicated account and Atlas retains the private SSH key, pinned host key,
atomic pull, verification, retention and systemd service/timer.
- Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity,
pin the host key, keep Borg repository credentials and encryption material in Vault, use
snapshot-consistent sources, and manage retries, logging, pruning, repository checks and restores.
- Add the UUID-bound offline USB backup with versioned rsync, locking, capacity checks, verification,
safe unmounting and a tested restore procedure; never trigger it for an arbitrary USB disk.
- Add monitoring and alerting for pool health, scrub/resilver, SMART data, temperatures, free space and
failed backup timers, plus a controlled Rocky kernel/OpenZFS update and reboot procedure.
- Document and test disaster recovery: rebuild Atlas with Ansible, import the existing pool, restore
from snapshot/USB/Hetzner, preserve Vault and Borg recovery material offline, and define RPO/RTO.
- Optionally design iCloud photo ingestion as a separate workflow after the storage and backup layers
are validated; do not make it a dependency of the Atlas baseline.
## Coding Agent Notes
- Shared agent packages live in `ai_agents_npm_packages` in `ansible/inventory/group_vars/all.yml`.
- Shared agent definitions and lifecycle flags live in `ai_agents` in `ansible/inventory/group_vars/all.yml`.
- Shared agent dotfiles live in `ai_agents_dotfiles`; rendered configs live in `ai_agents_templates`.
- Desktop, native workstation, and WSL profiles consume the shared agent package list; do not duplicate package entries in profile-specific vars.
- `dotfiles_common` copies common dotfiles plus `ai_agents_dotfiles`, then renders `ai_agents_templates`.
- Every `ai_agents.<agent>` entry has independent `install_enabled`, `deploy_enabled`, and `uninstall_enabled` flags. Installation and removal must not both be true for the same agent; the common pre-task fails before changes when they conflict.
- Fedora, Void desktop, and WSL workstation profiles consume the shared agent definitions; do not duplicate package entries in profile-specific vars. IBM Bob on the workstation follows its own flags.
- `dotfiles_common` deploys `ai_agents_dotfiles` and renders `ai_agents_templates` only when deployment is enabled.
- Removal is limited to the managed npm packages and `/usr/local/bin/bob`; never remove agent dotfiles, instructions, credentials, or user data.
- Keep `.config/ai/` as the common instruction source; update agent-specific entrypoints to reference it rather than duplicating instruction text.
## Tooling Notes
@@ -102,3 +216,19 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- Keep `README.md` and `AGENTS.md` aligned when workflows materially change.
- If you add a new operational area, also add the narrowest validation command for it.
- Call out checks you could not run and any follow-up verification needed.
## Aegis Fedora IoT Notes
- `aegis` is a remote Fedora IoT Raspberry Pi 4 node. Bootstrap it once with
`ansible/bootstrap/aegis.bu`; the remaining configuration is applied by `profile_aegis` over SSH.
- Fedora IoT is immutable. Do not add it to mutable Fedora package or shared dotfile roles.
- `profile_aegis` owns rootful Podman Quadlets, persistent container state under `/var/lib`, the
Podman auto-update timer, LAN-restricted firewalld rules, and SSH hardening. Keep
`aegis_lan_subnet` and `aegis_adguard_web_port` host-specific; SSH permits only the declared
key-authenticated users, never root or password authentication. Keep Apple IDs and other
credentials in Vault and use `no_log` for their rendering.
- `aegis_adguard_web_port` defaults to `80`. The initial AdGuard Home wizard port `3000` is intentionally unmanaged: open and close it manually only while
completing initial setup. Disable the local systemd-resolved stub through `profile_aegis` before
AdGuard binds port 53; keep
`/etc/resolv.conf` linked to `/run/systemd/resolve/resolv.conf` so Aegis retains router-provided DNS.
- iCloudPD requires post-deployment interactive MFA initialization; its cookie/configuration state is
persisted in `/var/lib/icloudpd/config`.

667
README.it.md Normal file
View File

@@ -0,0 +1,667 @@
# Infra — Personal Infrastructure as Code
> **English version:** [README.md](README.md)
Questo repository contiene la configurazione **Infrastructure as Code (IaC)** utilizzata per gestire e mantenere allineate diverse macchine personali tramite **Ansible**.
L'obiettivo è avere **una singola fonte di verità** per:
- configurazione delle macchine
- pacchetti installati
- servizi di sistema
- configurazioni utente (dotfiles)
Il repository consente di gestire più sistemi operativi e profili macchina mantenendo una struttura modulare, riproducibile e idempotente.
---
# Architettura del progetto
```text
infra/
├── ansible/
│ ├── ansible.cfg
│ ├── site.yml
│ ├── inventory/
│ │ ├── hosts.yml
│ │ ├── group_vars/
│ │ └── host_vars/
│ ├── templates/
│ └── roles/
│
├── dotfiles/
│ ├── common/
│ ├── desktop/
│ ├── fedora/
│ ├── server/
│ ├── workstation/
│ ├── workstation_dev_wsl/
│ └── nymph/
│
├── scripts/
├── secrets/
├── README.md
└── README.it.md
```
Il repository è diviso in due componenti principali:
| Componente | Scopo |
| ---------- | -------------------------------------- |
| ansible | provisioning e configurazione macchine |
| dotfiles | configurazioni utente versionate |
---
# Macchine gestite
Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Rocky
Linux 9 e un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti:
```text
common user environment
+ host-specific platform
+ role-specific software
+ independently selectable desktop
+ host hardware overrides
```
Matrice target:
| Host | Platform | Role | Desktop |
| ------------ | -------- | -------------------- | ------- |
| ikaros | Fedora | Personal workstation | GNOME |
| nymph | Fedora | Desktop laptop | GNOME |
| deadalus | Fedora WSL | Workstation dev | — |
| prometheus | Rocky Linux | Server | — |
| atlas | Rocky Linux | NAS | — |
Regola operativa:
```text
ikaros must be boring
nymph is allowed to break
```
`ikaros` usa Fedora Workstation/GNOME come desktop personale stabile; `nymph` usa lo stesso
target Fedora Workstation/GNOME come laptop. I gruppi legacy `void` e `desktop` restano alias di
compatibilita per eventuali host Void futuri mentre i nuovi assi sono
`platform_*`, `role_*` e `desktop_*`.
Nota sullo stato attuale del playbook principale:
- `ansible/site.yml` applica oggi in automatico Fedora/GNOME su `ikaros` e `nymph`
- `ansible/site.yml` applica il profilo Fedora WSL alla workstation `deadalus`
- `ansible/site.yml` applica il profilo server Rocky a `prometheus` con DNF, systemd, dotfiles server e firewalld
- `ansible/site.yml` applica il profilo NAS Rocky su `atlas` tramite SSH remoto
## Desktop
Target operativi:
- `ikaros`: Fedora Workstation + GNOME, desktop personale stabile/floating.
- `nymph`: Fedora Workstation + GNOME, laptop desktop con dotfiles desktop condivisi e GNOME lasciato al default Fedora.
Il profilo Void desktop resta disponibile come modello riutilizzabile per host
futuri e usa esclusivamente `desktop_environment: minimal`: Sway e il default,
mentre Niri si seleziona con il gruppo `desktop_niri`. GNOME e disponibile solo
sui target Fedora tramite `desktop_gnome`.
Lo stato attuale del profilo desktop include, tra le altre cose:
- dotfiles comuni e desktop
- sessioni Sway e Niri per eventuali host Void in modalita `minimal`
- `emptty` con default host-specific in modalita `minimal` e session file Wayland per `sway`
- pacchetti Void Linux e servizi runit; le liste pacchetti Void desktop sono separate per criterio:
- `void_packages_base` per il runtime sistema (init, kernel, audio core, networking, firewall, hw daemons)
- `desktop_common_packages` per l'infrastruttura condivisa
- `desktop_minimal_packages` per applicazioni GTK e `emptty`
- `desktop_sway_packages` per i binari specifici della sessione Sway
- `turnstile` per i servizi utente Void, incluso `ssh-agent`
- `ssh-agent` con socket stabile condiviso tra shell e SSH in `~/.local/state/ssh-agent/socket`
- Emacs usa una sola configurazione orientata a Org e authoring, condivisa da desktop Fedora/GNOME e workstation; Vim resta l'editor di sviluppo
- `tmux` con plugin gestiti da TPM al bootstrap del profilo desktop
- Flatpak con remoto Flathub
- GNOME Keyring e `udiskie` nella modalita minimale
- multi-monitor Void: sotto Sway è gestito da `kanshi`
---
## Workstation
La workstation `deadalus` usa Fedora in WSL sulla macchina Windows omonima, senza runtime Flatpak o Snap.
Il profilo è pensato per sviluppo e lavoro.
Nel modello Ansible usato qui, un singolo inventory host puo appartenere intenzionalmente a piu gruppi e quindi ricevere piu play nello stesso run: l'associazione non e `1 host = 1 play`, ma `host + gruppi = layering finale`.
Il profilo workstation e agganciato al playbook principale tramite:
- layer dev Fedora
- layer WSL dedicato per sviluppo con `systemd`
Lo stato attuale del profilo workstation include:
- installazione pacchetti base Fedora via dnf
- installazione e configurazione di Docker dal repository ufficiale
- installazione di Mise dal COPR ufficiale con JDK Eclipse Temurin Java 11 fissato
- gestione dei dotfiles workstation e rendering dei template dev condivisi
- preparazione di Fedora WSL con `systemd` per il toolchain di sviluppo
- attivazione del firewall `firewalld` sui target Fedora che dichiarano regole host-specifiche
Workflow WSL previsto:
1. avviare Fedora WSL almeno una volta e completare la creazione dell'utente Linux
2. installare Ansible dentro la distribuzione WSL
3. lanciare il playbook dalla distribuzione su `deadalus` per configurare l'ambiente dev locale
4. usare VS Code con le estensioni Remote (`WSL`, `SSH`, `Dev Containers`) dal lato Windows
Le applicazioni Windows sono installate e gestite manualmente; il profilo WSL non installa componenti di remoting Python per esse.
---
## Server
Sistema operativo:
- Rocky Linux 9
Configurazione:
- nessun ambiente grafico
Macchina:
- `prometheus`
Profilo orientato a servizi server e gestione di dotfiles dedicati.
Lo stato attuale del profilo server include:
- installazione pacchetti Rocky via DNF, EPEL e CRB
- installazione di Podman e podman-compose
- abilitazione dei servizi systemd dichiarati in inventory/group vars
- copia dei dotfiles server e rendering del `docker-compose.yml` per Nginx Proxy Manager e Gitea,
piu l'unita `podman-compose-server` (attivazione manuale)
- attivazione di firewalld con SSH, Cockpit (`9090/tcp`), HTTP e HTTPS abilitati
- Syncthing escluso dal profilo server Rocky
Il Compose desiderato su Prometheus non include piu Navidrome ne il database PostgreSQL obsoleto.
Navidrome e Syncthing appartengono ad Atlas; Navidrome ufficiale usa invece SQLite. Il profilo non
arresta o rimuove automaticamente eventuali container legacy e non elimina `/opt/postgres/data`.
Nginx Proxy Manager pubblica solo `80/tcp` e `443/tcp`; la sua interfaccia di amministrazione e
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`.
La fase 1 su Atlas non modifica questo deployment NPM ne i suoi dati persistenti. Dopo aver attivato
WireGuard e i servizi Atlas, configurare i proxy host NPM correnti con upstream Navidrome
`http://10.0.0.2:4533` e upstream per la GUI Syncthing `http://10.0.0.2:8384`. Solo la GUI web di
Syncthing usa NPM; il traffico di sincronizzazione resta sulle porte native pubblicate esplicitamente solo
sull'indirizzo WireGuard di Atlas. Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI.
### DuckDNS
`profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello
script e `duck.log`. Definire `server_duckdns_domain` negli host vars del server e salvare il
**nuovo token rigenerato** in `vault_duckdns_token`, nel Vault cifrato `secrets/vault.yml`
(`ansible-vault edit secrets/vault.yml`) oppure negli override non versionati `secrets/vault.local.yml`.
Non committare lo script generato e non passare il token sulla riga di comando. Il rendering
nasconde output e diff sensibili; lo script verifica TLS e passa il token a curl tramite stdin.
Il playbook non esegue lo script e non modifica la sua schedulazione esterna.
```bash
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns
```
La cancellazione dalla cronologia non revoca il token: rigenerarlo sul pannello DuckDNS.
Dopo la bonifica, riclonare gli altri checkout senza unire nuovamente la vecchia storia;
salvare separatamente eventuali modifiche non committate senza copiare segreti.
### Migrazione dati
Dopo il provisioning Rocky, eseguire `scripts/migrate_prometheus_data.sh` **sul server Ubuntu
sorgente**. Lo script usa rsync, e in dry-run di default; richiede `--quiesce-source --execute` per
fermare lo stack sorgente e copiare in modo consistente soltanto i dati di Nginx Proxy Manager e
Gitea. Non sposta Navidrome o Syncthing, non avvia container, non cancella dati e non esegue il
cutover.
Utente del profilo server:
- il profilo usa `server_username`, `server_user_group` e `server_user_home` definiti in `ansible/inventory/group_vars/server.yml`
- per default `server_username` eredita `username`, ma puo essere sovrascritto per tutti gli host server via inventory oppure a runtime con extra vars
- esempio override da CLI:
```bash
ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser
```
- se necessario puoi passare anche:
```bash
ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser -e server_user_group=mygroup -e server_user_home=/srv/myuser
```
---
## NAS
`atlas` e un NAS Rocky Linux 9 raggiunto tramite SSH. Normalmente il pool ZFS esiste gia e il profilo
gestisce solo i dataset figli. Un bootstrap RAIDZ2 una tantum e disponibile solo con conferma esplicita
(`atlas_create_pool=true`) e quattro percorsi reali e verificati `/dev/disk/by-id/...` in
`atlas_zpool_disks`. Non partiziona, forza, distrugge, esegue rollback o modifica il layout vdev di un
pool esistente. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN
configurata.
Per il primo avvio fornire `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`,
`vault_atlas_samba_password` e `vault_atlas_immich_db_password`. Eseguire il bootstrap tramite
l'amministratore esistente:
```bash
ansible-playbook ansible/site.yml --limit atlas \
-e atlas_connection_username=<existing-admin>
```
`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`. Atlas dichiara
abilitati storage, condivisioni e regole firewall LAN. Prima della prima applicazione verificare pool e
mountpoint esistenti, subnet LAN e zona firewalld attiva. `atlas_manage_media_stack` resta disabilitato
finche non saranno validati `/dev/dri`, i percorsi dei container e il segreto del database Immich.
Con la gestione storage attiva, Atlas crea l'intera gerarchia sotto il pool `zpool` esistente o creato esplicitamente:
`work`, `archive`, `archive/app_data`, i dataset applicativi separati
`archive/app_data/navidrome` e `archive/app_data/syncthing`, `media`, `media/music`,
`media/photobook`, `backups`, `backups/services` e `backup_prometheus`. I dataset applicativi e
di archivio usano `zstd`; media, Syncthing e backup dei servizi usano `lz4`;
`backups/services` mantiene inoltre una `refreservation` di `500G`.
Atlas impone SELinux targeted in modo persistente e segnala, senza avviarlo, l’eventuale reboot necessario per attivarlo. Assegna esplicitamente l’interfaccia LAN primaria alla zona firewalld gestita e applica hardening persistente del kernel di rete: rifiuta redirect e source-route, registra i martian, usa reverse-path filtering loose per WireGuard e disabilita il forwarding IPv4. SSH consente solo l’amministratore dichiarato tramite chiave pubblica; root, password, agent e forwarding
remoto sono disabilitati, mentre il forwarding locale resta disponibile per tunnel amministrativi privati. SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e
ammette la LAN configurata su SMB3 cifrato e firmato, esclusivamente su TCP/445. NFSv4 esporta soltanto
`media/photobook` all'IP configurato di Aegis su TCP/2049, 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 Photobook e montato in sola lettura su `/external/photobook`. NPM pubblica `80` e
`443`, mentre l'amministrazione resta vincolata a `127.0.0.1:81` per l'accesso tramite tunnel SSH.
La fase 1 e limitata ai Quadlet utente rootless di Navidrome e Syncthing su Atlas. E abilitata nella
configurazione host di Atlas e puo essere impostata a `false` solo per una sospensione intenzionale. Navidrome ufficiale `0.63.2` usa il database SQLite sotto `/data` e
non supporta `ND_DATABASE_URL` ne un backend PostgreSQL esterno. Il servizio obsoleto `navidromedb`
e quindi rimosso da Prometheus invece di essere replicato su Atlas. Il ruolo deriva i percorsi dal
pool `zpool`, montato in `/zpool`: musica in sola lettura da `/zpool/media/music`, stato
applicativo Navidrome e `navidrome.db` in `/zpool/archive/app_data/navidrome` e dati Syncthing in
`/zpool/archive/app_data/syncthing`. `profile_atlas` crea questi dataset quando
`atlas_manage_storage` e attivo; il ruolo backend verifica i mountpoint esatti prima di avviare i
container. Il ruolo backend non crea mai il pool. Il ruolo separato `wireguard_overlay`
gestisce `wg0` tra Prometheus (`10.0.0.1`) e Atlas (`10.0.0.2`), genera una sola volta le chiavi
private sui rispettivi host e scambia tramite Ansible soltanto quelle pubbliche. Solo Prometheus apre
pubblicamente `51820/udp`. Le porte backend sono ammesse esclusivamente nella zona firewalld WireGuard.
`backend_phase1_start_services` resta falso durante il trasferimento dello stato applicativo, quindi
la prima esecuzione reale del backend genera i Quadlet senza creare un database Atlas vuoto. Dopo aver
arrestato Navidrome su Prometheus, copiare l'intera directory `/opt/navidrome/data/` in
`/zpool/archive/app_data/navidrome/`, preservando `navidrome.db` e gli eventuali file SQLite laterali.
Impostare quindi questa variabile a vero e rieseguire il ruolo per abilitare e avviare Navidrome e
Syncthing. Il playbook non copia e non elimina mai i dati applicativi.
Validare e generare i servizi Atlas con:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags storage
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1
```
Per il cutover, arrestare il vecchio Navidrome prima di copiare la sua directory dati, verificare
l'ownership dell'account `admin` su Atlas e confermare la presenza del database SQLite copiato prima
di impostare `backend_phase1_start_services: true` in `host_vars/atlas.yml`. Conservare i dati sorgente
e il container legacy `navidromedb` fermo finche Navidrome su Atlas e una prova di restore non sono
stati validati.
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`.
---
# Composizione della configurazione
Emacs è abilitato sui profili Fedora/GNOME e workstation; la configurazione canonica è distribuita da `dotfiles_common`, con Org in `~/Org/`, template versionati e export PDF/HTML/Markdown/DOCX/ODT. Per abilitarlo temporaneamente su un altro profilo:
```bash
ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=true
```
La configurazione finale di una macchina è ottenuta combinando più livelli.
```text
common configuration
+ platform configuration
+ role configuration
+ desktop configuration
+ host overrides
```
Esempi correnti:
```text
ikaros -> common + platform_fedora + role_personal_workstation + graphical_desktop + desktop_gnome + ikaros
nymph -> common + platform_fedora + graphical_desktop + desktop_gnome + nymph
deadalus -> common + platform_fedora + workstation_dev_fedora + workstation_dev_wsl + deadalus
```
Questo approccio consente di:
- mantenere configurazioni condivise
- applicare override specifici per host
- evitare duplicazioni
- riutilizzare il profilo Void corrente su un host futuro assegnandolo a
`platform_void + graphical_desktop + desktop_sway`
---
# Ruoli Ansible
I principali ruoli attualmente presenti sono:
| Role | Descrizione |
| ------------------------- | ----------------------------------- |
| base | configurazione base comune |
| packages_void | installazione pacchetti su Void |
| packages_fedora | installazione pacchetti su Fedora |
| packages_rocky | installazione pacchetti su Rocky Linux 9 |
| services_runit | gestione servizi runit |
| services_systemd | gestione servizi systemd |
| profile_desktop_common | bootstrap desktop Void condiviso |
| profile_desktop_gnome | dotfiles desktop condivisi per Fedora/GNOME |
| profile_desktop_sway | sessione desktop sway / SwayFX (Wayland) |
| profile_desktop_niri | sessione desktop Niri su Void (Wayland) |
| profile_desktop_host | override desktop specifici per host |
| profile_personal_workstation | layer stabile per workstation personale |
| profile_workstation_dev_common | configurazione dev workstation condivisa |
| profile_workstation_dev_wsl | configurazione WSL condivisa per sviluppo |
| profile_server | configurazione server |
| profile_atlas | configurazione NAS Rocky Linux 9 |
| profile_backend_phase1 | Navidrome e Syncthing rootless su Atlas |
| wireguard_overlay | overlay WireGuard Prometheus/Atlas |
| dotfiles_common | distribuzione dotfiles comuni |
| dotfiles | distribuzione configurazioni utente |
---
# Stato attuale del playbook principale
Il playbook `ansible/site.yml` e attualmente composto da blocchi per asse:
```text
all -> dotfiles_common
platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd
wireguard_overlay -> wireguard_overlay (dopo platform_rocky)
atlas -> profile_atlas
role_backend_phase1 -> profile_backend_phase1 (dopo atlas)
platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common
workstation_dev_wsl -> profile_workstation_dev_wsl (dopo platform_fedora + workstation_dev_fedora)
rocky_server -> dotfiles_common + profile_server (dopo platform_rocky)
```
Questo significa che, allo stato attuale:
- `ikaros` riceve Fedora Workstation/GNOME come target desktop personale stabile
- `nymph` riceve Fedora Workstation/GNOME come target laptop
- il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri
- `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati
- 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
- lo stack Compose server include soltanto `gitea` e `nginx-proxy-manager`; Navidrome e Syncthing
della fase 1 sono Quadlet rootless su Atlas
# Dotfiles
La directory `dotfiles/` contiene le configurazioni utente versionate.
```text
dotfiles/
├── common
├── desktop
├── server
├── fedora
├── workstation
├── workstation_dev_wsl
└── nymph
```
Le configurazioni sono applicate tramite Ansible e organizzate per livelli:
| Livello | Scopo |
| ------- | -------------------------------- |
| common | configurazioni condivise |
| profile | configurazioni per tipo macchina |
| host | override specifici |
---
# Requisiti
Per utilizzare il repository sono necessari:
- Python 3
- Ansible
- `ansible-lint`
- `yamllint`
- `shellcheck`
- collection definite in `ansible/collections/requirements.yml`
- accesso locale o SSH alle macchine target, in base a come e definito l'inventory
Installazione base:
```bash
python3 -m pip install ansible ansible-lint yamllint shellcheck-py
ansible-galaxy collection install -r ansible/collections/requirements.yml
```
Gestione segreti:
- il repository supporta il caricamento opzionale di `secrets/vault.yml`
- il repository supporta anche `secrets/vault.local.yml` per override locali non versionati
- `secrets/vault.yml.example` funge da template/esempio
- se `secrets/vault.yml` non e presente, il playbook continua comunque senza caricare variabili locali opzionali
- se `secrets/.vault_pass.gpg` esiste viene usato automaticamente per sbloccare i vault tramite `gpg`; in alternativa resta supportato `secrets/.vault_pass` come fallback legacy locale; se nessuno dei due file esiste Ansible richiede la password in modo interattivo
---
# Utilizzo
Eseguire il playbook principale:
```bash
ansible-playbook ansible/site.yml
```
Allo stato attuale questo comando:
- distribuisce i dotfiles comuni a tutti gli host
- per `platform_void` applica pacchetti Void e servizi runit
- per `platform_void + graphical_desktop` applica bootstrap desktop condiviso, sessioni Sway/Niri e override specifici per host
- per `platform_fedora` applica pacchetti Fedora e servizi systemd a `ikaros`, `nymph` e `deadalus`
- per `platform_fedora & role_personal_workstation` applica il layer personale a `ikaros`
- per `platform_fedora & desktop_gnome` applica il profilo GNOME a `ikaros` e `nymph`
- per `workstation_dev_wsl` applica i tweak WSL dopo il layer Fedora a `deadalus`, escludendo Flatpak e Snap
- per `platform_rocky` applica pacchetti Rocky e servizi systemd ad `atlas` e `prometheus`; quindi applica il profilo NAS ad `atlas` e il profilo server a `prometheus`
- non riavvia automaticamente il display manager
- carica `secrets/vault.yml` solo se presente
- carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza
Per validare prima di applicare:
```bash
ansible-playbook ansible/site.yml --syntax-check
ansible-playbook ansible/site.yml --limit ikaros,nymph --check --diff
ansible-playbook ansible/site.yml --limit ikaros --check --diff
ansible-playbook ansible/site.yml --limit nymph --check --diff
ansible-playbook ansible/site.yml --limit deadalus --check --diff
ansible-playbook ansible/site.yml --limit prometheus --check --diff
ansible-playbook ansible/site.yml --limit atlas --check --diff
ansible-lint ansible/site.yml
ansible-lint ansible/roles
yamllint ansible/
```
Per testare un override dell'utente server senza modificare l'inventory:
```bash
ansible-playbook ansible/site.yml --limit prometheus --check --diff -e server_username=myuser
```
Per validazioni piu mirate:
```bash
ansible-playbook ansible/site.yml --limit <host> --tags <tag1>,<tag2> --check --diff
ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" --check --diff
ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
```
## Tag supportati dal playbook
Per vedere l'elenco reale aggiornato dei tag disponibili:
```bash
ansible-playbook ansible/site.yml --list-tags
```
Allo stato attuale `ansible/site.yml` espone questi tag:
| Tag | Scopo | Ambito principale |
| --- | --- | --- |
| `always` | pre-task sempre eseguiti, inclusi caricamento vault e validazioni preliminari | common |
| `ai_agents` | installazione agenti AI condivisi | Fedora, WSL |
| `atlas` | account, storage, condivisioni e container Atlas | NAS Atlas |
| `backend_phase1` | Quadlet rootless Navidrome e Syncthing | NAS Atlas |
| `containers` | Quadlet rootful Atlas | NAS Atlas |
| `dotfiles` | distribuzione/configurazione dotfiles | tutti i profili |
| `dotfiles:common` | dotfiles comuni condivisi | common, workstation, server |
| `dotfiles:desktop` | dotfiles desktop | desktop Void, Fedora/GNOME |
| `dotfiles:host` | override host-specifici desktop | desktop Void |
| `dotfiles:server` | dotfiles dedicati al profilo server | server |
| `dotfiles:workstation` | dotfiles dedicati alle workstation | personal workstation, WSL |
| `emptty` | gestione display manager `emptty` | desktop Void |
| `display-manager` | gestione del display manager `emptty` | desktop Void |
| `emacs` | configurazione Emacs condivisa e dipendenze di authoring | desktop Fedora/GNOME e workstation |
| `fonts` | installazione font | Fedora |
| `fzf` | configurazione FZF | dotfiles comuni |
| `git` | configurazione Git e GPG desktop | Fedora/GNOME, desktop Void |
| `gnome` | configurazione host GNOME | Fedora/GNOME desktop |
| `immich` | account e Quadlet Immich | NAS Atlas |
| `sway` | sessione/configurazione sway / SwayFX (Wayland) | desktop Void |
| `niri` | sessione/configurazione Niri (Wayland) | desktop Void |
| `npm` | installazione pacchetti npm globali | Fedora/GNOME, desktop Void, WSL |
| `nvidia` | componenti NVIDIA desktop | desktop Void |
| `packages` | installazione e aggiornamento pacchetti | tutti i profili |
| `podman` | integrazione Podman Compose e Quadlet rootless | server |
| `portal` | configurazione xdg-desktop-portal | desktop Void |
| `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 |
| `tmux` | configurazione e plugin tmux | desktop Fedora/Void, WSL |
| `vim` | configurazione Vim | dotfiles comuni |
| `wireguard` | overlay WireGuard Prometheus/Atlas | Prometheus, NAS Atlas |
| `wsl` | bootstrap e configurazione WSL | WSL |
Esempi pratici:
```bash
ansible-playbook ansible/site.yml --limit nymph --tags dotfiles:desktop,gnome --check --diff
ansible-playbook ansible/site.yml --limit ikaros --tags gnome --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags services,dotfiles:server --check --diff
```
---
# Bootstrap di una nuova macchina
Una nuova macchina può essere inizializzata con i seguenti passaggi:
```bash
git clone <repo>
cd <repo-dir>
ansible-galaxy collection install -r ansible/collections/requirements.yml
ansible-playbook ansible/site.yml
```
Dopo l'esecuzione del playbook la macchina verra configurata secondo il profilo definito e i ruoli attualmente orchestrati.
Per aggiungere un nuovo host Void che riusa il profilo desktop preservato:
1. aggiungere l'host a `platform_void`;
2. aggiungerlo a `graphical_desktop`;
3. usare Sway, oppure aggiungerlo a `desktop_niri` per selezionare Niri;
4. lasciare eventuali dettagli hardware in `host_vars/<host>.yml`.
I gruppi legacy `void` e `desktop` sono parent di compatibilita, quindi un host
in `platform_void` e `graphical_desktop` continua a ricevere anche le variabili
Void e desktop esistenti.
Per il flusso mail desktop esiste inoltre uno script dedicato:
```bash
scripts/bootstrap_mail.sh
```
Lo script si occupa del bootstrap dei secret nel keyring, del primo sync con `mbsync` e dell'inizializzazione di `mu` usando la configurazione mail generata dai template.
Se modifichi questo script, valida almeno con:
```bash
sh -n scripts/bootstrap_mail.sh
shellcheck scripts/bootstrap_mail.sh
```
---
# Filosofia del progetto
Il repository segue alcuni principi chiave:
- Infrastructure as Code
- configurazione dichiarativa
- idempotenza
- ambienti riproducibili
- separazione tra configurazione sistema e configurazione utente
Questo consente di ricreare qualsiasi macchina partendo esclusivamente dal repository.
---
# Roadmap
Possibili evoluzioni future:
- hardening sicurezza server
- configurazione backup
- testing automatico playbook
- integrazione CI
- supporto ad altre distribuzioni Linux
---
# Licenza
Questo progetto è distribuito sotto licenza **LGPL-3.0**.

752
README.md
View File

@@ -1,24 +1,14 @@
# Infra — Personal Infrastructure as Code
Questo repository contiene la configurazione **Infrastructure as Code (IaC)** utilizzata per gestire e mantenere allineate diverse macchine personali tramite **Ansible**.
> **Italian version:** [README.it.md](README.it.md)
L'obiettivo è avere **una singola fonte di verità** per:
This is my Ansible repo for keeping my personal machines and dotfiles in sync. It is the source of truth for packages, services, and user configuration. The setup is meant to stay modular, reproducible, and idempotent without getting too clever.
- configurazione delle macchine
- pacchetti installati
- servizi di sistema
- configurazioni utente (dotfiles)
Il repository consente di gestire più sistemi operativi e profili macchina mantenendo una struttura modulare, riproducibile e idempotente.
---
# Architettura del progetto
## Layout
```text
infra/
├── ansible/
│ ├── ansible.cfg
│ ├── site.yml
│ ├── inventory/
│ │ ├── hosts.yml
@@ -26,207 +16,296 @@ infra/
│ │ └── host_vars/
│ ├── templates/
│ └── roles/
│
├── dotfiles/
│ ├── common/
│ ├── desktop/
│ ├── fedora/
│ ├── ubuntu/
│ ├── server/
│ ├── workstation/
│ ├── workstation_host_linux/
│ ├── workstation_dev_wsl/
│ └── nymph/
│
├── scripts/
├── secrets/
└── README.md
├── README.md
└── README.it.md
```
Il repository è diviso in due componenti principali:
- `ansible/` holds provisioning and host configuration.
- `dotfiles/` holds versioned user configuration.
| Componente | Scopo |
| ---------- | -------------------------------------- |
| ansible | provisioning e configurazione macchine |
| dotfiles | configurazioni utente versionate |
## Managed machines
---
# Macchine gestite
Il repository modella attualmente host Fedora/GNOME, ambienti WSL di sviluppo Ubuntu e Fedora e un server Ubuntu.
La composizione resta separata in assi indipendenti:
The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora IoT LAN
node, a Rocky Linux 9 server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied
to host names:
```text
common user environment
+ host-specific platform
+ platform-specific setup
+ role-specific software
+ independently selectable desktop
+ host hardware overrides
+ independently selected desktop
+ host overrides
```
Matrice target:
| Host | Platform | Role | Desktop |
| ------------ | -------- | -------------------- | ------- |
| ikaros | Fedora | Personal workstation | GNOME |
| nymph | Fedora | Desktop laptop | GNOME |
| deadalus-wsl | Ubuntu | Workstation dev | — |
| deadalus-fedora-wsl | Fedora | Workstation dev WSL | — |
| prometheus | Ubuntu | Server | — |
Regola operativa:
| Host | Platform | Role | Desktop |
| --- | --- | --- | --- |
| `ikaros` | Fedora | Personal workstation | GNOME |
| `nymph` | Fedora | Desktop laptop | GNOME |
| `deadalus` | Fedora WSL | Development workstation | — |
| `aegis` | Fedora IoT | Always-on LAN node | — |
| `prometheus` | Rocky Linux | Server | — |
| `atlas` | Rocky Linux | NAS | — |
```text
ikaros must be boring
nymph is allowed to break
```
`ikaros` usa Fedora Workstation/GNOME come desktop personale stabile; `nymph` usa lo stesso
target Fedora Workstation/GNOME come laptop. I gruppi legacy `void` e `desktop` restano alias di
compatibilita per eventuali host Void futuri mentre i nuovi assi sono
`platform_*`, `role_*` e `desktop_*`.
`ikaros` is the stable personal Fedora/GNOME desktop. `nymph` is the laptop and gets the same shared desktop dotfiles while GNOME itself stays close to the Fedora defaults. The legacy `void` and `desktop` groups are compatibility parents; the main axes are `platform_*`, `role_*`, and `desktop_*`.
Nota sullo stato attuale del playbook principale:
## Desktop profiles
- `ansible/site.yml` applica oggi in automatico Fedora/GNOME su `ikaros` e `nymph`
- `ansible/site.yml` applica i rami WSL Ubuntu e Fedora per il modello dev in WSL
- `ansible/site.yml` applica anche il profilo `ubuntu_server` con baseline apt, systemd, dotfiles server e firewall UFW
- `ikaros`: stable Fedora Workstation + GNOME desktop.
- `nymph`: Fedora Workstation + GNOME laptop.
- Void desktops stay available as reusable future profiles through `platform_void + graphical_desktop`.
## Desktop
Void uses `desktop_environment: minimal`. Sway is the normal session; add a host to `desktop_niri` to select Niri. GNOME is only handled on Fedora through `desktop_gnome`.
Target operativi:
The desktop setup includes shared desktop dotfiles, Sway/Niri support for future Void hosts, `emptty`, `turnstile` user services, a stable ssh-agent socket at `~/.local/state/ssh-agent/socket`, Emacs authoring config, tmux bootstrapped through TPM, Flatpak, GNOME Keyring, Udiskie, and `kanshi` for Sway multi-monitor setups.
- `ikaros`: Fedora Workstation + GNOME, desktop personale stabile/floating.
- `nymph`: Fedora Workstation + GNOME, laptop desktop con dotfiles desktop condivisi e GNOME lasciato al default Fedora.
Void package buckets stay separate on purpose:
Il profilo Void desktop resta disponibile come modello riutilizzabile per host
futuri e usa esclusivamente `desktop_environment: minimal`: Sway e il default,
mentre Niri si seleziona con il gruppo `desktop_niri`. GNOME e disponibile solo
sui target Fedora tramite `desktop_gnome`.
Lo stato attuale del profilo desktop include, tra le altre cose:
- dotfiles comuni e desktop
- sessioni Sway e Niri per eventuali host Void in modalita `minimal`
- `emptty` con default host-specific in modalita `minimal` e session file Wayland per `sway`
- pacchetti Void Linux e servizi runit; le liste pacchetti Void desktop sono separate per criterio:
- `void_packages_base` per il runtime sistema (init, kernel, audio core, networking, firewall, hw daemons)
- `desktop_common_packages` per l'infrastruttura condivisa
- `desktop_minimal_packages` per applicazioni GTK e `emptty`
- `desktop_sway_packages` per i binari specifici della sessione Sway
- `turnstile` per i servizi utente Void, incluso `ssh-agent`
- `ssh-agent` con socket stabile condiviso tra shell e SSH in `~/.local/state/ssh-agent/socket`
- Emacs usa una sola configurazione orientata a Org e authoring, condivisa da desktop Fedora/GNOME e workstation; Vim resta l'editor di sviluppo
- `tmux` con plugin gestiti da TPM al bootstrap del profilo desktop
- Flatpak con remoto Flathub
- GNOME Keyring e `udiskie` nella modalita minimale
- multi-monitor Void: sotto Sway è gestito da `kanshi`
---
- `void_packages_base`: system runtime and services.
- `desktop_common_packages`: shared GUI infrastructure.
- `desktop_minimal_packages`: GTK applications and `emptty`.
- `desktop_sway_packages`: Sway-only binaries.
## Workstation
Sistemi operativi supportati:
`deadalus` is the only workstation target. It is Fedora running in WSL on the Windows machine with the same name. Flatpak and Snap are explicitly kept out of this profile.
- Ubuntu WSL, usato da `deadalus-wsl`
- Fedora WSL, usato da `deadalus-fedora-wsl`
- Fedora Workstation nativa, disponibile per host futuri tramite gruppi dedicati
The workstation receives two layers:
Desktop environment host Linux, per eventuali workstation native:
- Fedora development setup through `workstation_dev_fedora`.
- WSL setup with `systemd` through `workstation_dev_wsl`.
- GNOME
That gives it Fedora packages through DNF, Docker from the official repository, Mise from its official COPR repository with a pinned Eclipse Temurin Java 11 JDK, shared workstation dotfiles and templates, tmux helpers, and WSL systemd configuration. Windows applications are installed manually; the WSL profile does not manage Python remoting components for them.
Macchine attuali:
### WSL workflow
- `deadalus-wsl` come ambiente dev Ubuntu in WSL sulla workstation Windows `deadalus`
- `deadalus-fedora-wsl` come clone dev Fedora in WSL sulla workstation Windows `deadalus`, senza runtime Flatpak o Snap
Questo profilo è pensato per sviluppo e lavoro, con separazione tra layer host e layer dev.
Nel modello Ansible usato qui, un singolo inventory host puo appartenere intenzionalmente a piu gruppi e quindi ricevere piu play nello stesso run: l'associazione non e `1 host = 1 play`, ma `host + gruppi = layering finale`.
Il profilo workstation e agganciato al playbook principale e ora distingue:
- layer dev condiviso tra WSL e workstation Fedora future
- layer dev Fedora nativo disponibile per host futuri
- layer host Linux GNOME disponibile per host futuri
- layer WSL dedicato per sviluppo con `systemd`
Per esempio, lo stesso host Linux puo stare in `workstation_host_linux` e in `workstation_dev_fedora`, a seconda del layering che vuoi comporre.
Lo stato attuale del profilo workstation include:
- installazione pacchetti base Ubuntu via apt
- installazione pacchetti base Fedora via dnf per eventuali workstation native
- installazione e configurazione di Docker dal repository ufficiale
- gestione dei dotfiles workstation e rendering dei template dev condivisi
- installazione opzionale di Google Chrome, VS Code, IntelliJ IDEA Ultimate e applicazioni Flatpak per eventuali workstation Fedora native
- estensioni GNOME per eventuali host Linux nativi
- preparazione dei rami WSL Ubuntu e Fedora con `systemd` per il toolchain di sviluppo
- attivazione del firewall `firewalld` sui target Fedora che dichiarano regole host-specifiche
Workflow WSL previsto:
1. avviare la distribuzione WSL scelta almeno una volta e completare la creazione dell'utente Linux
2. installare Ansible dentro la distribuzione WSL
3. lanciare il playbook dalla distribuzione su `deadalus-wsl` oppure `deadalus-fedora-wsl` per configurare l'ambiente dev locale
4. usare VS Code con le estensioni Remote (`WSL`, `SSH`, `Dev Containers`) dal lato Windows
---
1. Start Fedora WSL once and finish creating the Linux user.
2. Install Ansible inside Fedora WSL.
3. Run the playbook from that distribution with `--limit deadalus`.
4. Use Windows-side VS Code with Remote WSL, Remote SSH, and Dev Containers if wanted.
## Server
Sistema operativo:
`prometheus` is the Rocky Linux 9 server. It has no graphical environment and gets server-specific
dotfiles and templates. The profile provisions configuration only: it does not transfer data, start
the Compose stack, update DNS, or perform a cutover.
- Ubuntu LTS
The server profile installs platform-specific packages, Podman and podman-compose, declared systemd
services, and firewalld. The manually activated `podman-compose-server` unit contains the existing
Nginx Proxy Manager and Gitea services. The desired Compose file no longer includes Navidrome,
Syncthing, or the obsolete Navidrome PostgreSQL database. Navidrome and Syncthing belong to Atlas;
official Navidrome uses SQLite instead. Applying the profile does not stop or remove legacy
containers and does not delete `/opt/postgres/data`.
Configurazione:
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
from Ikaros or Nymph with the `npm-tunnel` Bash alias. Nextcloud remains disabled and the profile
does not provision any `/srv/nextcloud` directories.
- nessun ambiente grafico
The Atlas phase-one work does not change this NPM deployment or its persistent data. Once WireGuard
and the Atlas services are active, configure the current NPM proxy hosts with Navidrome upstream
`http://10.0.0.2:4533` and Syncthing GUI upstream `http://10.0.0.2:8384`. Only the Syncthing web GUI
uses NPM; synchronization traffic remains on explicitly published native ports bound only to the Atlas
WireGuard address. Configure both Syncthing authentication and an appropriate NPM access policy before
publishing its GUI.
Macchina:
- `prometheus`
Profilo orientato a servizi server e gestione di dotfiles dedicati.
Lo stato attuale del profilo server include:
- installazione pacchetti base Ubuntu via apt
- installazione e configurazione di Docker dal repository ufficiale
- 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
- attivazione del firewall UFW con regola SSH esplicita
- apertura delle porte Syncthing `22000/tcp`, `22000/udp` e `21027/udp`, lasciando la GUI non esposta direttamente su UFW
Utente del profilo server:
- il profilo usa `server_username`, `server_user_group` e `server_user_home` definiti in `ansible/inventory/group_vars/server.yml`
- per default `server_username` eredita `username`, ma puo essere sovrascritto per tutti gli host server via inventory oppure a runtime con extra vars
- esempio override da CLI:
Server identity comes from `server_username`, `server_user_group`, and `server_user_home` in `ansible/inventory/group_vars/server.yml`. `server_username` defaults to `username`, but it can be overridden, for example:
```bash
ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser
ansible-playbook ansible/site.yml --limit prometheus \
-e server_username=myuser -e server_user_group=mygroup \
-e server_user_home=/srv/myuser
```
- se necessario puoi passare anche:
The target must already provide `server_username` with local sudo access.
Prometheus authorizes its declared SSH public keys through separate files below
`~/.ssh/authorized_keys.d/`, while `sshd` is configured to read those files directly.
### DuckDNS
`profile_server` renders `~/duckdns/duck.sh` with mode `0700`, keeping the existing updater path
and `duck.log`. Set `server_duckdns_domain` in the server's host vars and store the **rotated**
`vault_duckdns_token` in encrypted `secrets/vault.yml` (using `ansible-vault edit secrets/vault.yml`)
or untracked `secrets/vault.local.yml`. Never commit the rendered script or put the token on a
command line. Rendering hides secret output/diffs; the updater verifies TLS and passes the token
to curl through stdin. The playbook neither runs the updater nor changes its external schedule.
```bash
ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser -e server_user_group=mygroup -e server_user_home=/srv/myuser
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns
```
---
An exposed token must be revoked/regenerated on DuckDNS: deleting it from Git history does not
revoke it. After a history cleanup, re-clone other checkouts rather than merging the old history
back in; preserve any uncommitted work separately without copying secrets.
# Composizione della configurazione
### Data migration
Emacs è abilitato sui profili Fedora/GNOME e workstation; la configurazione canonica è distribuita da `dotfiles_common`, con Org in `~/Org/`, template versionati e export PDF/HTML/Markdown/DOCX/ODT. Per abilitarlo temporaneamente su un altro profilo:
Provision Rocky first, then run the migration script **on the retired Ubuntu source host**. It is
dry-run by default and requires an explicit source-stack stop before it can copy application data:
```bash
ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=true
sudo ./scripts/migrate_prometheus_data.sh \
--destination rocky@179.237.102.172 \
--identity /root/.ssh/id_ed25519
sudo ./scripts/migrate_prometheus_data.sh \
--destination rocky@179.237.102.172 \
--identity /root/.ssh/id_ed25519 \
--quiesce-source --execute
```
La configurazione finale di una macchina è ottenuta combinando più livelli.
The script copies only Nginx Proxy Manager and Gitea data. It does not delete data, move
Navidrome/Syncthing, copy `/home/git/.ssh`, start containers, update DNS, or perform a cutover. The
destination SSH host key must already be trusted and the destination account needs passwordless sudo
for `rsync`. It preserves ACLs but not extended attributes, so source SELinux labels are not
transferred; the Rocky Compose bind mounts apply their own `:Z` labels when containers start.
## DNS Filter
`aegis` is a Raspberry Pi 4 running Fedora IoT. Generate Ignition from
`ansible/bootstrap/aegis.bu` with the included Podman/Butane helper, then write the SD card with
`arm-image-installer`:
```bash
ansible/bootstrap/generate-aegis-ign.sh --write IMAGE DEVICE
```
The controller manages it remotely as `pi@aegis`; unlike local desktop profiles, Aegis is
intentionally an SSH inventory target. `profile_aegis` manages rootful Podman Quadlets for AdGuard
Home and iCloudPD, persistent data under `/var/lib`, the Podman auto-update timer, LAN-restricted
firewalld rules, SSH key-only access for `pi`, and `wake-ikaros`. Set the host-local
`aegis_lan_subnet` and `aegis_adguard_web_port` values before applying it. The playbook permits
AdGuard Home HTTP on port `80`; the initial wizard port `3000` is intentionally unmanaged and must be
opened and closed manually during initial setup. The profile disables the local systemd-resolved DNS
stub and points `/etc/resolv.conf` to its full resolver data, freeing port 53
for AdGuard while retaining DNS learned from the router. Define
`vault_aegis_icloudpd_apple_id` in Vault before applying it. iCloudPD still requires interactive MFA
initialization after its first deployment.
New Aegis images create the `admin` account in Butane. Before configuring a newly imaged node, run its
first playbook execution with `-e ansible_user=admin`; the SSH hardening role then permits that same
account. Keep the inventory on `pi` until the existing node has been replaced.
Validate the profile before deployment:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit aegis --check --diff --ask-become-pass
```
## NAS
`atlas` is a Rocky Linux 9 NAS reached through SSH. Normally its pool already exists and the profile
only manages child datasets. A one-time RAIDZ2 bootstrap is available only with explicit confirmation
(`atlas_create_pool=true`) and exactly four verified `/dev/disk/by-id/...` paths in `atlas_zpool_disks`.
It never partitions, forces, destroys, rolls back, or changes the vdev layout of an existing pool. Linux
clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the configured LAN.
For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and
`vault_atlas_immich_db_password`. Bootstrap the host through its
existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then
include both WireGuard peers in the same idempotent playbook run:
```bash
ansible-playbook ansible/site.yml --limit prometheus,atlas \
-e atlas_connection_username=<existing-admin> \
-e atlas_create_pool=true
```
The explicit pool gate is safe to repeat: the role creates the RAIDZ2 pool only when it is absent.
WireGuard waits for a real peer handshake before the play continues.
`vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text
Cockpit password. Subsequent runs use `atlas_admin_username`. Atlas declares storage, sharing, and its
LAN firewall rules enabled. Before the first apply, check the existing pool and mountpoints, LAN subnet,
and active firewalld zone. `atlas_manage_media_stack` remains disabled until `/dev/dri`, the container
paths, and the Immich database secret are validated. Atlas reads its declared SSH public keys from
separate files below `~/.ssh/authorized_keys.d/`.
With storage management enabled, Atlas creates the complete dataset hierarchy below the existing or
explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and
`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`,
`backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`,
while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G`
refreservation. Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords,
agent and remote forwarding are disabled, while local forwarding remains available for private administrative tunnels. SMB3 exposes `Archive` only to the configured Vault-backed
Samba accounts on encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific
exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using
`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 Photobook is mounted read-only at `/external/photobook`. NPM publishes ports `80` and `443`; its
administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access.
Phase 1 is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. It is enabled in the
Atlas host configuration and can be set to `false` only for a deliberate suspension. Official Navidrome `0.63.2` uses its SQLite database below `/data`; it does
not support `ND_DATABASE_URL` or an external PostgreSQL backend. The obsolete `navidromedb` service
was therefore removed from Prometheus instead of being reproduced on Atlas. The role derives all
storage paths from the `zpool` mounted at `/zpool`: music is read-only at
`/zpool/media/music`, Navidrome application state and `navidrome.db` are stored at
`/zpool/archive/app_data/navidrome`, and Syncthing persists at
`/zpool/archive/app_data/syncthing`. `profile_atlas` creates these datasets when
`atlas_manage_storage` is enabled; the backend role verifies their exact mountpoints before starting
containers. The backend role never creates the pool. The separate `wireguard_overlay` role manages `wg0`
between Prometheus (`10.0.0.1`) and Atlas (`10.0.0.2`), generating private keys once
on their respective hosts and exchanging only public keys through Ansible. Prometheus alone opens
`51820/udp` publicly. When the WireGuard zone is created, Ansible reloads firewalld and immediately
reloads Prometheus' rootful Podman networks so the existing proxy stack retains container DNS and
connectivity. Backend ports are admitted only in the WireGuard firewalld zone.
`backend_phase1_start_services` stays false during the application-state transfer, so the first real
backend run renders the Quadlets without creating an empty Atlas database. After stopping Navidrome
on Prometheus, copy the complete `/opt/navidrome/data/` directory into
`/zpool/archive/app_data/navidrome/`, preserving `navidrome.db` and any SQLite sidecar files. Then set
this variable to true and rerun the role to enable and start Navidrome and Syncthing. The playbook
never copies or deletes application data.
Validate and render the Atlas services with:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags storage
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1
```
For the cutover, stop the old Navidrome writer before copying its data directory, verify ownership by
the Atlas `admin` account and confirm that the copied SQLite database is present before changing
`backend_phase1_start_services: true` in `host_vars/atlas.yml`. Keep the source data and the stopped
legacy `navidromedb` container until Navidrome on Atlas and a restore test have been validated.
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
A host can intentionally belong to more than one inventory group. The final configuration is the combination of the host and its groups, not a one-host/one-play mapping.
```text
common configuration
@@ -236,245 +315,201 @@ common configuration
+ host overrides
```
Esempi correnti:
Current examples:
```text
ikaros -> common + platform_fedora + role_personal_workstation + graphical_desktop + desktop_gnome + ikaros
nymph -> common + platform_fedora + graphical_desktop + desktop_gnome + nymph
deadalus-fedora-wsl -> common + platform_fedora + workstation_dev_fedora + workstation_dev_wsl + deadalus-fedora-wsl
ikaros -> common + platform_fedora + role_personal_workstation + graphical_desktop + desktop_gnome + ikaros
nymph -> common + platform_fedora + graphical_desktop + desktop_gnome + nymph
deadalus -> common + platform_fedora + workstation_dev_fedora + workstation_dev_wsl + deadalus
```
Questo approccio consente di:
This keeps shared configuration reusable, lets host overrides stay small, and leaves the Void desktop profile ready for a future host using `platform_void + graphical_desktop + desktop_sway`.
- mantenere configurazioni condivise
- applicare override specifici per host
- evitare duplicazioni
- riutilizzare il profilo Void corrente su un host futuro assegnandolo a
`platform_void + graphical_desktop + desktop_sway`
Emacs is enabled on Fedora/GNOME and workstation profiles. `dotfiles_common` deploys the canonical authoring setup, including `~/Org/`, versioned templates, and PDF/HTML/Markdown/DOCX/ODT export support. To turn it on temporarily elsewhere:
---
```bash
ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=true
```
# Ruoli Ansible
## AI coding agents
I principali ruoli attualmente presenti sono:
The shared npm-managed agents are OpenCode, Claude Code, Codex, Gemini CLI, and
GitHub Copilot; IBM Bob is also managed on `deadalus`. Each agent has its own
lifecycle flags in `ansible/inventory/group_vars/all.yml`, so one agent can be
installed, configured, or removed without affecting the others:
| Role | Descrizione |
| ------------------------- | ----------------------------------- |
| base | configurazione base comune |
| packages_void | installazione pacchetti su Void |
| packages_freebsd | installazione pacchetti su FreeBSD via pkg |
| packages_ubuntu | installazione pacchetti su Ubuntu |
| packages_fedora | installazione pacchetti su Fedora |
| services_runit | gestione servizi runit |
| services_systemd | gestione servizi systemd |
| services_freebsd | gestione servizi FreeBSD dichiarati per host |
| profile_desktop_common | bootstrap desktop Void condiviso |
| profile_desktop_gnome | dotfiles desktop condivisi per Fedora/GNOME |
| profile_desktop_sway | sessione desktop sway / SwayFX (Wayland) |
| profile_desktop_niri | sessione desktop Niri su Void (Wayland) |
| profile_desktop_host | override desktop specifici per host |
| profile_personal_workstation | layer stabile per workstation personale |
| profile_workstation_dev_common | configurazione dev workstation condivisa |
| profile_workstation_gnome | configurazione host workstation GNOME |
| profile_workstation_dev_wsl | configurazione WSL condivisa per sviluppo |
| profile_server | configurazione server |
| dotfiles_common | distribuzione dotfiles comuni |
| dotfiles | distribuzione configurazioni utente |
```yaml
ai_agents:
opencode:
npm_package: opencode-ai
install_enabled: true
deploy_enabled: true
uninstall_enabled: false
```
---
Installation uses the npm `latest` state; deployment copies/renders only the
configuration belonging to each enabled agent. Removal deletes only the selected
managed npm package or, for IBM Bob, `/usr/local/bin/bob`; it preserves dotfiles,
instructions, credentials, and user data. Installation and removal are mutually
exclusive per agent: the playbook fails before making changes when both flags are
true for the same agent. Servers set `ai_agents: {}` and therefore manage none.
# Stato attuale del playbook principale
Run a focused dry run with:
Il playbook `ansible/site.yml` e attualmente composto da blocchi per asse:
```bash
ansible-playbook ansible/site.yml --limit ikaros --tags ai_agents --check --diff
ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --diff
```
To preview removal, set `install_enabled: false` and `uninstall_enabled: true`
only in the entry for the agent being removed, then run:
```bash
ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --diff
```
## Main roles
| Role | What it does |
| --- | --- |
| `packages_void` | Installs packages on Void. |
| `packages_fedora` | Installs packages on Fedora. |
| `packages_rocky` | Installs packages on Rocky Linux 9. |
| `services_runit` | Manages runit services. |
| `services_systemd` | Manages systemd services. |
| `profile_desktop_common` | Shared Void desktop bootstrap. |
| `profile_desktop_gnome` | Shared Fedora/GNOME desktop dotfiles. |
| `profile_desktop_sway` | Sway / SwayFX Wayland session. |
| `profile_desktop_niri` | Niri Wayland session on Void. |
| `profile_desktop_host` | Host-specific desktop overrides. |
| `profile_personal_workstation` | Stable personal-workstation layer. |
| `profile_workstation_dev_common` | Shared workstation development setup. |
| `profile_workstation_dev_wsl` | WSL development setup. |
| `profile_server` | Server setup. |
| `profile_atlas` | Rocky Linux 9 NAS setup. |
| `profile_backend_phase1` | Rootless Navidrome and Syncthing on Atlas. |
| `wireguard_overlay` | Prometheus/Atlas WireGuard overlay. |
| `profile_aegis` | Fedora IoT always-on LAN node. |
| `dotfiles_common` | Shared user dotfiles. |
## What `site.yml` runs
```text
all -> dotfiles_common
all except platform_rocky -> dotfiles_common
platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_freebsd -> packages_freebsd + services_freebsd
platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd
wireguard_overlay -> wireguard_overlay (after platform_rocky)
role_aegis -> profile_aegis
atlas -> profile_atlas
role_backend_phase1 -> profile_backend_phase1 (after atlas)
rocky_server -> dotfiles_common + profile_server (after platform_rocky)
platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common
workstation_host_linux -> profile_workstation_gnome
workstation_dev_wsl_ubuntu -> packages_ubuntu + services_systemd + profile_workstation_dev_common + profile_workstation_dev_wsl
workstation_dev_wsl_fedora -> profile_workstation_dev_wsl (dopo platform_fedora + workstation_dev_fedora)
ubuntu_server -> packages_ubuntu + services_systemd + profile_server
workstation_dev_wsl -> profile_workstation_dev_wsl (after platform_fedora + workstation_dev_fedora)
```
Questo significa che, allo stato attuale:
So, in practice:
- `ikaros` riceve Fedora Workstation/GNOME come target desktop personale stabile
- `nymph` riceve Fedora Workstation/GNOME come target laptop
- il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri
- i rami WSL Ubuntu (`deadalus-wsl`) e Fedora (`deadalus-fedora-wsl`) sono predisposti con play dev dedicati
- il server Ubuntu (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewall
- lo stack container server include `navidrome`, `postgres`, `gitea`, `nginx-proxy-manager` e `syncthing`, con GUI Syncthing raggiungibile tramite la rete Docker `web`
- `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`.
- `deadalus` gets the Fedora development layer followed by the WSL layer.
- `rocky_server` configures the Rocky 9 server, `prometheus`.
- `atlas` receives the Rocky platform layer and the NAS profile through SSH.
- `aegis` receives only the immutable Fedora IoT profile through SSH; it does not receive
mutable Fedora package or common dotfile roles.
- Empty `platform_void` groups do nothing until they get a host.
- The playbook never restarts the display manager during a run.
- `secrets/vault.yml` and then `secrets/vault.local.yml` are loaded only when present.
# Dotfiles
## Requirements
La directory `dotfiles/` contiene le configurazioni utente versionate.
```text
dotfiles/
├── common
├── desktop
├── server
├── fedora
├── ubuntu
├── workstation
├── workstation_host_linux
├── workstation_dev_wsl
└── nymph
```
Le configurazioni sono applicate tramite Ansible e organizzate per livelli:
| Livello | Scopo |
| ------- | -------------------------------- |
| common | configurazioni condivise |
| profile | configurazioni per tipo macchina |
| host | override specifici |
---
# Requisiti
Per utilizzare il repository sono necessari:
- Python 3
- Ansible
- `ansible-lint`
- `yamllint`
- `shellcheck`
- collection definite in `ansible/collections/requirements.yml`
- accesso locale o SSH alle macchine target, in base a come e definito l'inventory
Installazione base:
You will need Python 3, Ansible, `ansible-lint`, `yamllint`, `shellcheck`, and the collections in `ansible/collections/requirements.yml`.
```bash
python3 -m pip install ansible ansible-lint yamllint shellcheck-py
ansible-galaxy collection install -r ansible/collections/requirements.yml
```
Gestione segreti:
Secrets are optional:
- il repository supporta il caricamento opzionale di `secrets/vault.yml`
- il repository supporta anche `secrets/vault.local.yml` per override locali non versionati
- `secrets/vault.yml.example` funge da template/esempio
- se `secrets/vault.yml` non e presente, il playbook continua comunque senza caricare variabili locali opzionali
- se `secrets/.vault_pass.gpg` esiste viene usato automaticamente per sbloccare i vault tramite `gpg`; in alternativa resta supportato `secrets/.vault_pass` come fallback legacy locale; se nessuno dei due file esiste Ansible richiede la password in modo interattivo
- `secrets/vault.yml` can hold shared local vault values.
- `secrets/vault.local.yml` can hold untracked local overrides.
- `secrets/vault.yml.example` is the example template.
- If no vault file exists, the playbook still runs without those optional values.
- `secrets/.vault_pass.gpg` is used when available; `secrets/.vault_pass` is a legacy local fallback. Without either one, Ansible asks for the password interactively.
---
## Running it
# Utilizzo
Eseguire il playbook principale:
Run the whole playbook:
```bash
ansible-playbook ansible/site.yml
```
Allo stato attuale questo comando:
- distribuisce i dotfiles comuni a tutti gli host
- per `platform_void` applica pacchetti Void e servizi runit
- per `platform_void + graphical_desktop` applica bootstrap desktop condiviso, sessioni Sway/Niri e override specifici per host
- per `platform_freebsd` e `workstation_host_linux` non applica nulla finche quei gruppi restano senza host
- per `platform_fedora` applica pacchetti Fedora e servizi systemd a `ikaros`, `nymph` e `deadalus-fedora-wsl`
- per `platform_fedora & role_personal_workstation` applica il layer personale a `ikaros`
- per `platform_fedora & desktop_gnome` applica il profilo GNOME a `ikaros` e `nymph`
- per `workstation_dev_wsl_ubuntu` applica pacchetti Ubuntu, servizi systemd, profilo dev comune e tweak WSL a `deadalus-wsl`
- per `workstation_dev_wsl_fedora` applica i tweak WSL dopo il layer Fedora a `deadalus-fedora-wsl`, escludendo Flatpak e Snap
- per gli host `ubuntu_server` applica pacchetti Ubuntu, servizi systemd, profilo server, UFW, dotfiles e template dedicati
- non riavvia automaticamente il display manager
- carica `secrets/vault.yml` solo se presente
- carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza
Per validare prima di applicare:
Useful checks before applying changes:
```bash
ansible-playbook ansible/site.yml --syntax-check
ansible-playbook ansible/site.yml --limit ikaros,nymph --check --diff
ansible-playbook ansible/site.yml --limit ikaros --check --diff
ansible-playbook ansible/site.yml --limit nymph --check --diff
ansible-playbook ansible/site.yml --limit deadalus-wsl --check --diff
ansible-playbook ansible/site.yml --limit deadalus-fedora-wsl --check --diff
ansible-playbook ansible/site.yml --limit deadalus --check --diff
ansible-playbook ansible/site.yml --limit prometheus --check --diff
ansible-playbook ansible/site.yml --limit atlas --check --diff
ansible-playbook ansible/site.yml --limit aegis --check --diff
ansible-lint ansible/site.yml
ansible-lint ansible/roles
yamllint ansible/
```
Per testare un override dell'utente server senza modificare l'inventory:
```bash
ansible-playbook ansible/site.yml --limit prometheus --check --diff -e server_username=myuser
```
Per validazioni piu mirate:
For focused checks:
```bash
ansible-playbook ansible/site.yml --limit <host> --tags <tag1>,<tag2> --check --diff
ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" --check --diff
ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml
docker 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
```
## Tag supportati dal playbook
## Tags
Per vedere l'elenco reale aggiornato dei tag disponibili:
Use Ansible as the source of truth for the current tag list:
```bash
ansible-playbook ansible/site.yml --list-tags
```
Allo stato attuale `ansible/site.yml` espone questi tag:
| Tag | Main scope |
| --- | --- |
| `always` | Common pre-tasks, including optional vault loading. |
| `ai_agents` | AI coding-agent install, configuration deployment, and managed-binary removal. |
| `atlas` | Atlas NAS account, storage, sharing, and container configuration. |
| `backend_phase1` | Rootless Atlas Navidrome and Syncthing Quadlets. |
| `containers` | Rootful Atlas Quadlets. |
| `dotfiles` | User configuration across all profiles. |
| `dotfiles:common` | Shared dotfiles. |
| `dotfiles:desktop` | Void and Fedora/GNOME desktop dotfiles. |
| `dotfiles:host` | Host-specific Void desktop overrides. |
| `dotfiles:server` | Server dotfiles. |
| `dotfiles:workstation` | Personal workstation and WSL dotfiles. |
| `emacs` | Shared Emacs setup and authoring dependencies. |
| `gnome` | Fedora/GNOME desktop configuration. |
| `immich` | Atlas Immich account and Quadlets. |
| `npm` | Global npm packages. |
| `packages` | Package installation and updates. |
| `podman` | Podman Compose and rootless Quadlet integration. |
| `services` | runit and systemd services. |
| `sharing` | Atlas NFSv4 and SMB3 configuration. |
| `storage` | Atlas child ZFS datasets. |
| `tmux` | tmux configuration and plugins. |
| `wireguard` | Prometheus/Atlas WireGuard overlay. |
| `wsl` | WSL bootstrap and configuration. |
| Tag | Scopo | Ambito principale |
| --- | --- | --- |
| `always` | pre-task sempre eseguiti, inclusi caricamento vault e validazioni preliminari | common |
| `ai_agents` | installazione agenti AI condivisi | Fedora, WSL |
| `dotfiles` | distribuzione/configurazione dotfiles | tutti i profili |
| `dotfiles:common` | dotfiles comuni condivisi | common, workstation, server |
| `dotfiles:desktop` | dotfiles desktop | desktop Void, Fedora/GNOME |
| `dotfiles:host` | override host-specifici desktop | desktop Void |
| `dotfiles:server` | dotfiles dedicati al profilo server | server |
| `dotfiles:workstation` | dotfiles dedicati alle workstation | personal workstation, WSL, workstation Linux future |
| `emptty` | gestione display manager `emptty` | desktop Void |
| `display-manager` | gestione del display manager `emptty` | desktop Void |
| `emacs` | configurazione Emacs condivisa e dipendenze di authoring | desktop Fedora/GNOME e workstation |
| `fonts` | installazione font | Fedora |
| `fzf` | configurazione FZF | dotfiles comuni |
| `git` | configurazione Git e GPG desktop | Fedora/GNOME, desktop Void |
| `gnome` | configurazione host GNOME | Fedora/GNOME desktop, workstation host Linux future |
| `sway` | sessione/configurazione sway / SwayFX (Wayland) | desktop Void |
| `niri` | sessione/configurazione Niri (Wayland) | desktop Void |
| `npm` | installazione pacchetti npm globali | Fedora/GNOME, desktop Void, workstation Linux, WSL |
| `nvidia` | componenti NVIDIA desktop | desktop Void |
| `packages` | installazione e aggiornamento pacchetti | tutti i profili |
| `portal` | configurazione xdg-desktop-portal | desktop Void |
| `services` | gestione servizi runit/systemd | tutti i profili |
| `theme` | configurazione del tema GTK/Qt | desktop Void |
| `tmux` | configurazione e plugin tmux | desktop Fedora/Void, WSL |
| `vim` | configurazione Vim | dotfiles comuni |
| `vscode` | installazione/configurazione VS Code | workstation Fedora future, host Linux |
| `wsl` | bootstrap e configurazione WSL | WSL |
Esempi pratici:
```bash
ansible-playbook ansible/site.yml --limit nymph --tags dotfiles:desktop,gnome --check --diff
ansible-playbook ansible/site.yml --limit ikaros --tags gnome --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags services,dotfiles:server --check --diff
```
---
# Bootstrap di una nuova macchina
Una nuova macchina può essere inizializzata con i seguenti passaggi:
## Bootstrapping a new machine
```bash
git clone <repo>
@@ -483,66 +518,11 @@ ansible-galaxy collection install -r ansible/collections/requirements.yml
ansible-playbook ansible/site.yml
```
Dopo l'esecuzione del playbook la macchina verra configurata secondo il profilo definito e i ruoli attualmente orchestrati.
For a future Void desktop host:
Per aggiungere un nuovo host Void che riusa il profilo desktop preservato:
1. Add it to `platform_void`.
2. Add it to `graphical_desktop`.
3. Use Sway, or add it to `desktop_niri` for Niri.
4. Put hardware-specific details in `host_vars/<host>.yml`.
1. aggiungere l'host a `platform_void`;
2. aggiungerlo a `graphical_desktop`;
3. usare Sway, oppure aggiungerlo a `desktop_niri` per selezionare Niri;
4. lasciare eventuali dettagli hardware in `host_vars/<host>.yml`.
I gruppi legacy `void` e `desktop` sono parent di compatibilita, quindi un host
in `platform_void` e `graphical_desktop` continua a ricevere anche le variabili
Void e desktop esistenti.
Per prove in VM sono disponibili gruppi di esempio in
`ansible/inventory/examples/platform-test-hosts.yml`, da passare esplicitamente
con `-i` insieme all'inventory principale.
Per il flusso mail desktop esiste inoltre uno script dedicato:
```bash
scripts/bootstrap_mail.sh
```
Lo script si occupa del bootstrap dei secret nel keyring, del primo sync con `mbsync` e dell'inizializzazione di `mu` usando la configurazione mail generata dai template.
Se modifichi questo script, valida almeno con:
```bash
sh -n scripts/bootstrap_mail.sh
shellcheck scripts/bootstrap_mail.sh
```
---
# Filosofia del progetto
Il repository segue alcuni principi chiave:
- Infrastructure as Code
- configurazione dichiarativa
- idempotenza
- ambienti riproducibili
- separazione tra configurazione sistema e configurazione utente
Questo consente di ricreare qualsiasi macchina partendo esclusivamente dal repository.
---
# Roadmap
Possibili evoluzioni future:
- hardening sicurezza server
- configurazione backup
- testing automatico playbook
- integrazione CI
- supporto ad altre distribuzioni Linux
---
# Licenza
Questo progetto è distribuito sotto licenza **LGPL-3.0**.
The legacy `void` and `desktop` groups remain compatibility parents, so hosts in `platform_void` and `graphical_desktop` still receive the existing Void and desktop variables.

View File

@@ -0,0 +1,57 @@
# One-time bootstrap for Fedora IoT on Aegis (Raspberry Pi 4).
# Generate only: ./generate-aegis-ign.sh
# Generate and write an SD card: ./generate-aegis-ign.sh --write IMAGE DEVICE
# The write mode uses arm-image-installer with the RPi4 target and embeds
# config.ign; it prompts for Wi-Fi credentials unless supplied through its
# WIFI_SSID and WIFI_PASS environment variables.
# For WiFi, the UEFI 'System Table Selection' must be DeviceTree (Esc at boot).
variant: fiot
version: 1.0.0
passwd:
users:
- name: admin
password_hash: "$6$bNDsU0XC5BxNJS5U$ENDGpdOUPJM3wcXBgNESCQkOqqQ9gN72/xEQoJsAI6QdsaY6mD4G5DBVIv7nHwHQOP35IH1oGRl.Uy3R2iUYQ1"
groups:
- wheel
ssh_authorized_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
systemd:
units:
- name: sshd.service
enabled: true
- name: rpm-ostree-install-wifi.service
enabled: true
contents: |
[Unit]
Description=Layer WiFi packages (driver, firmware, NetworkManager-wifi)
Wants=network-online.target
After=network-online.target
Before=zincati.service
ConditionPathExists=!/var/lib/%N.stamp
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/rpm-ostree install -y --allow-inactive NetworkManager-wifi NetworkManager-wwan wpa_supplicant wireless-regdb iw brcmfmac-firmware
ExecStart=/bin/touch /var/lib/%N.stamp
ExecStart=/bin/systemctl --no-block reboot
[Install]
WantedBy=multi-user.target
storage:
files:
- path: /etc/hostname
mode: 0644
contents:
inline: |
aegis
- path: /etc/sysctl.d/99-ip-forward.conf
mode: 0644
contents:
inline: |
net.ipv4.ip_forward = 1
links:
- path: /etc/localtime
target: ../usr/share/zoneinfo/Europe/Rome

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
BUTANE_IMAGE=${BUTANE_IMAGE:-quay.io/coreos/butane:release}
BUTANE_SOURCE=${BUTANE_SOURCE:-"$SCRIPT_DIR/aegis.bu"}
IGNITION_OUTPUT=${IGNITION_OUTPUT:-"$SCRIPT_DIR/config.ign"}
SSH_PUBLIC_KEY=${SSH_PUBLIC_KEY:-"$HOME/.ssh/id_ed25519.pub"}
WIFI_SECURITY=${WIFI_SECURITY:-wpa-psk}
usage() {
cat <<'USAGE'
Usage:
generate-aegis-ign.sh
generate-aegis-ign.sh --write IMAGE DEVICE
Environment overrides:
BUTANE_IMAGE Butane container image (default: quay.io/coreos/butane:release)
BUTANE_SOURCE Butane source path (default: aegis.bu beside this script)
IGNITION_OUTPUT Ignition output path (default: config.ign beside this script)
SSH_PUBLIC_KEY SSH public key passed to arm-image-installer
WIFI_SSID Wi-Fi SSID; prompted if unset in --write mode
WIFI_PASS Wi-Fi password; prompted if unset in --write mode
WIFI_SECURITY Wi-Fi security type (default: wpa-psk)
USAGE
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'Error: required command not found: %s\n' "$1" >&2
exit 1
fi
}
read_required() {
prompt=$1
value=$2
if [ -z "$value" ]; then
printf '%s' "$prompt" >&2
IFS= read -r value
fi
if [ -z "$value" ]; then
printf '%s\n' 'Error: a value is required.' >&2
exit 1
fi
printf '%s' "$value"
}
read_secret() {
value=$1
if [ -z "$value" ]; then
printf '%s' 'Wi-Fi password: ' >&2
stty -echo
IFS= read -r value
stty echo
printf '\n' >&2
fi
if [ -z "$value" ]; then
printf '%s\n' 'Error: a value is required.' >&2
exit 1
fi
printf '%s' "$value"
}
write_image=false
case $# in
0)
;;
3)
if [ "$1" != '--write' ]; then
usage >&2
exit 2
fi
write_image=true
IMAGE=$2
DEVICE=$3
;;
*)
usage >&2
exit 2
;;
esac
require_command podman
if [ ! -f "$BUTANE_SOURCE" ]; then
printf 'Error: Butane source not found: %s\n' "$BUTANE_SOURCE" >&2
exit 1
fi
OUTPUT_DIR=$(dirname "$IGNITION_OUTPUT")
if [ ! -d "$OUTPUT_DIR" ]; then
printf 'Error: output directory not found: %s\n' "$OUTPUT_DIR" >&2
exit 1
fi
umask 077
TEMP_OUTPUT=$(mktemp "$OUTPUT_DIR/.config.ign.XXXXXX")
trap 'rm -f "$TEMP_OUTPUT"' EXIT HUP INT TERM
podman run --rm -i "$BUTANE_IMAGE" --strict < "$BUTANE_SOURCE" > "$TEMP_OUTPUT"
mv "$TEMP_OUTPUT" "$IGNITION_OUTPUT"
trap - EXIT HUP INT TERM
printf 'Generated Ignition config: %s\n' "$IGNITION_OUTPUT"
if [ "$write_image" = false ]; then
exit 0
fi
if [ ! -f "$IMAGE" ]; then
printf 'Error: image not found: %s\n' "$IMAGE" >&2
exit 1
fi
if [ ! -b "$DEVICE" ]; then
printf 'Error: target is not a block device: %s\n' "$DEVICE" >&2
exit 1
fi
if [ ! -f "$SSH_PUBLIC_KEY" ]; then
printf 'Error: SSH public key not found: %s\n' "$SSH_PUBLIC_KEY" >&2
exit 1
fi
require_command arm-image-installer
WIFI_SSID=$(read_required 'Wi-Fi SSID: ' "${WIFI_SSID:-}")
WIFI_PASS=$(read_secret "${WIFI_PASS:-}")
printf 'Writing %s to %s.\n' "$IMAGE" "$DEVICE" >&2
sudo arm-image-installer \
--image="$IMAGE" \
--target=rpi4 \
--media="$DEVICE" \
--ignition="$IGNITION_OUTPUT" \
--addkey="$SSH_PUBLIC_KEY" \
--resizefs \
--wifi-ssid="$WIFI_SSID" \
--wifi-pass="$WIFI_PASS" \
--wifi-security="$WIFI_SECURITY"

View File

@@ -1,8 +0,0 @@
---
# Optional inventory overlay for VM tests. It is not loaded by ansible.cfg.
all:
children:
platform_freebsd:
hosts:
freebsdtest:
ansible_connection: local

View File

@@ -90,40 +90,75 @@ common_dotfiles:
dest: .config/bat/
mode: preserve
ai_agents_npm_packages:
- name: "opencode-ai"
state: latest
- name: "@anthropic-ai/claude-code"
state: latest
- name: "@openai/codex"
state: latest
- name: "@google/gemini-cli"
state: latest
- name: "@githubnext/github-copilot-cli"
state: latest
# AI coding-agent lifecycle controls are independent for every agent.
# Installation and removal are mutually exclusive per agent; the common
# pre-task enforces this before any changes are made.
ai_agents:
opencode:
npm_package: opencode-ai
install_enabled: true
deploy_enabled: true
uninstall_enabled: false
claude_code:
npm_package: "@anthropic-ai/claude-code"
install_enabled: false
deploy_enabled: false
uninstall_enabled: true
codex:
npm_package: "@openai/codex"
install_enabled: true
deploy_enabled: true
uninstall_enabled: false
gemini_cli:
npm_package: "@google/gemini-cli"
install_enabled: true
deploy_enabled: true
uninstall_enabled: false
github_copilot:
npm_package: "@github/copilot"
install_enabled: false
deploy_enabled: false
uninstall_enabled: true
ibm_bob:
install_enabled: true
deploy_enabled: false
uninstall_enabled: false
ai_agents_enabled: true
ai_agents_deploy_enabled_names: >-
{{ ai_agents | dict2items | selectattr('value.deploy_enabled') | map(attribute='key') | list }}
ai_agents_dotfiles:
- name: AI common config
src: .config/ai/
dest: .config/ai/
mode: preserve
agents:
- opencode
- codex
- gemini_cli
- name: Gemini CLI config
src: .gemini/
dest: .gemini/
mode: preserve
agents:
- gemini_cli
- name: OpenCode config
src: .config/opencode/
dest: .config/opencode/
mode: preserve
agents:
- opencode
- name: Claude Code memory
src: .claude/
dest: .claude/
mode: preserve
agents:
- claude_code
ai_agents_templates:
- name: Codex config
src: .codex/config.toml.j2
dest: .codex/config.toml
mode: "0644"
agents:
- codex

View File

@@ -31,8 +31,7 @@ desktop_binary_tools: []
desktop_npm_packages: >-
{{
ai_agents_npm_packages
+ [
[
{'name': '@mermaid-js/mermaid-cli', 'state': 'latest'},
{'name': 'vscode-langservers-extracted', 'state': 'latest'}
]
@@ -225,8 +224,8 @@ desktop_flatpak_packages:
- com.github.tchx84.Flatseal
- com.spotify.Client
- io.github.flattool.Warehouse
- org.onlyoffice.desktopeditors
- org.telegram.desktop
- org.mozilla.Thunderbird
desktop_flatpak_extensions:
- org.gtk.Gtk3theme.Yaru-Blue-dark//stable

View File

@@ -11,6 +11,7 @@ fedora_desktop_packages:
# Fedora equivalents of the development/tooling packages previously pulled by
# the Void desktop profile.
- 7zip
- arm-image-installer
- nodejs-bash-language-server
- bluez
- bridge-utils
@@ -21,6 +22,7 @@ fedora_desktop_packages:
- croc
- delve
- dnsmasq
- evolution
- fastfetch
- poppler-glib
- poppler-utils
@@ -30,9 +32,9 @@ fedora_desktop_packages:
- golang
- golangci-lint
- gopls
- gnome-shell-extension-gsconnect
- hunspell
- libvirt
- libreoffice
- liberation-fonts
- libasan
- libubsan
@@ -50,10 +52,12 @@ fedora_desktop_packages:
- qemu-tools
- rclone
- remmina
- rpi-imager
- ruff
- rustup
- rsync
- shfmt
- seahorse
- syncthing
- tealdeer
- terminus-fonts
@@ -80,7 +84,5 @@ fedora_flatpak_packages:
- be.alexandervanhee.gradia
- ch.protonmail.protonmail-bridge
- com.spotify.Client
- org.mozilla.thunderbird
- org.onlyoffice.desktopeditors
- org.telegram.desktop
fedora_npm_packages: "{{ ai_agents_npm_packages }}"

View File

@@ -12,12 +12,6 @@ fedora_packages_base:
- ripgrep
fedora_manage_docker_repo: true
fedora_manage_google_chrome_repo: true
fedora_manage_intellij_repo: true
fedora_manage_vscode_repo: true
fedora_intellij_copr_owner: coder966
fedora_intellij_copr_project: intellij-idea-ultimate
fedora_intellij_package_name: intellij-idea-ultimate
fedora_emacs_packages:
- emacs-pgtk
@@ -53,7 +47,7 @@ fedora_dotfiles:
workstation_firewall_backend: firewalld
fedora_workstation_dev_packages:
fedora_dev_packages:
- croc
- distrobox
- gnupg
@@ -62,35 +56,5 @@ fedora_workstation_dev_packages:
- openssh-clients
- shellcheck
workstation_host_linux_packages_fedora:
- code
- croc
- firewall-config
- gnome-extensions-app
- gnome-shell-extension-appindicator
- gnome-shell-extension-just-perfection
- gnome-shell-extension-no-overview
- gnome-tweaks
- libreoffice
- meld
- NetworkManager-openconnect-gnome
- pcsc-lite
- pinentry-gnome3
- podman
- podman-compose
- rclone
- yubikey-manager
fedora_flatpak_remote_name: flathub
fedora_flatpak_remote_url: https://dl.flathub.org/repo/flathub.flatpakrepo
workstation_flatpak_remote_name: "{{ fedora_flatpak_remote_name }}"
workstation_flatpak_remote_url: "{{ fedora_flatpak_remote_url }}"
workstation_flatpak_packages:
- com.getpostman.Postman
- com.spotify.Client
- com.github.jeromerobert.pdfarranger
- com.github.xournalpp.xournalpp
- io.dbeaver.DBeaverCommunity
- org.mozilla.Thunderbird
- org.telegram.desktop

View File

@@ -0,0 +1,2 @@
---
# Fedora IoT is immutable: do not attach the mutable Fedora package roles.

View File

@@ -1,55 +0,0 @@
---
platform_profile: freebsd
platform_family: freebsd
platform_package_manager: pkg
platform_service_manager: rc
ansible_python_interpreter: /usr/local/bin/python3
vim_plugin_distro_packages:
- fzf
vim_plugin_source_plugins:
- name: fzf
repo: https://github.com/junegunn/fzf.git
version: v0.72.0
- name: fzf.vim
repo: https://github.com/junegunn/fzf.vim.git
version: 34a564c81f36047f50e593c1656f4580ff75ccca
- name: vim-fugitive
repo: https://github.com/tpope/vim-fugitive.git
version: 3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0
effective_user_group: "{{ user_group }}"
effective_user_home: "/home/{{ effective_username }}"
user_home: "/home/{{ username }}"
freebsd_packages_base:
- bash
- bash-completion
- bat
- curl
- fzf
- git
- jq
- python3
- ripgrep
- rsync
- tmux
- tree
- unzip
- vim
- wget
- zip
- zoxide
freebsd_development_packages:
- clang
- cmake
- gdb
- go
- gmake
- node
- npm
- pkgconf
- py311-pip
- rust
- shellcheck

View File

@@ -0,0 +1,55 @@
---
platform_profile: rocky
platform_family: redhat
platform_package_manager: dnf
platform_service_manager: systemd
rocky_openzfs_release_rpm: https://zfsonlinux.org/epel/zfs-release-3-0.el9.noarch.rpm
rocky_openzfs_gpg_key_url: >-
https://raw.githubusercontent.com/zfsonlinux/zfsonlinux.github.com/master/zfs-release/RPM-GPG-KEY-openzfs-key2
rocky_openzfs_gpg_key_fingerprint: 7DC7 299D CF7C 7FD9 CD87 701B A599 FD5E 9DB8 4141
rocky_syncthing_version: 2.1.3
rocky_syncthing_archive_checksum: sha256:f929eb8e5b72a85543eeeefb2c38f34a68e0c530e70758a2905b78840c76602c
rocky_syncthing_archive_url: >-
{{
'https://github.com/syncthing/syncthing/releases/download/v'
~ rocky_syncthing_version
~ '/syncthing-linux-amd64-v'
~ rocky_syncthing_version
~ '.tar.gz'
}}
rocky_manage_openzfs_repo: false
rocky_manage_syncthing_binary: false
rocky_manage_podman: false
rocky_manage_github_cli_repo: false
rocky_github_cli_repo_url: https://cli.github.com/packages/rpm/gh-cli.repo
rocky_github_cli_repo_file: /etc/yum.repos.d/gh-cli.repo
enabled_services:
- firewalld
rocky_common_packages:
- bash-completion
- bat
- fzf
- git
- jq
- tree
- unzip
- vim-enhanced
- wget
- zip
- zoxide
rocky_packages_base:
- dnf-plugins-core
- firewalld
- openssh-server
- rsync
- selinux-policy-targeted
- sudo
rocky_profile_packages: []
rocky_podman_packages: []

View File

@@ -0,0 +1,43 @@
---
rocky_manage_podman: true
rocky_manage_github_cli_repo: true
rocky_profile_packages:
- avahi
- curl
- dmidecode
- dosfstools
- gcc
- gcc-c++
- git-delta
- gh
- gnupg2
- htop
- make
- nmap-ncat
- nodejs
- parted
- pciutils
- pinentry
- ranger
- ripgrep
rocky_podman_packages:
- podman
- podman-compose
enabled_services:
- firewalld
- podman.socket
server_firewall_backend: firewalld
server_firewalld_zone: public
server_firewalld_services:
- ssh
- cockpit
- http
- https
server_firewalld_ports: []
server_sshd_service_name: sshd
server_compose_selinux_mount_option: Z
server_syncthing_enabled: false

View File

@@ -13,12 +13,4 @@ personal_workstation_directories:
- path: "{{ user_home }}/Remotes"
mode: "0755"
personal_workstation_dotfiles:
- src: .gitignore_global
dest: .gitignore_global
mode: "0644"
- src: .themes.gitignore
dest: .themes.gitignore
mode: "0644"
personal_workstation_flatpak_packages: []

View File

@@ -6,7 +6,7 @@ effective_username: "{{ server_username }}"
effective_user_group: "{{ server_user_group }}"
effective_user_home: "{{ server_user_home }}"
server_container_stack_dir: /opt/docker/server
ai_agents_enabled: false
ai_agents: {}
vim_plugins_enabled: false
profile_packages:
@@ -25,6 +25,9 @@ profile_packages:
- rsync
server_dotfiles:
- src: .bashrc.d/20-editor-server.sh
dest: .bashrc.d/20-editor-server.sh
mode: "0644"
- src: .gnupg/gpg-agent.conf
dest: .gnupg/gpg-agent.conf
mode: "0600"
@@ -34,9 +37,6 @@ server_dotfiles:
- src: .themes.gitignore
dest: .themes.gitignore
mode: "0644"
- src: duckdns/
dest: duckdns/
mode: preserve
server_templates:
- src: server/.gitconfig.j2
@@ -50,6 +50,10 @@ server_templates:
no_log: true
server_directories:
- path: "{{ server_user_home }}/.bashrc.d"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: "{{ server_user_home }}/.gnupg"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
@@ -58,14 +62,6 @@ server_directories:
owner: root
group: root
mode: "0755"
- path: /opt/navidrome/data
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: /opt/music
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: /opt/npm/data
owner: root
group: root
@@ -74,14 +70,13 @@ server_directories:
owner: root
group: root
mode: "0755"
- path: /opt/postgres/data
owner: root
group: root
mode: "0755"
- path: /opt/gitea/data
owner: root
group: root
mode: "0755"
server_syncthing_enabled: true
server_syncthing_directories:
- path: /opt/syncthing/config
owner: root
group: root
@@ -94,33 +89,11 @@ server_directories:
owner: "1000"
group: "1000"
mode: "0755"
- path: /srv/nextcloud
owner: root
group: root
mode: "0755"
- path: /srv/nextcloud/data
owner: root
group: root
mode: "0755"
server_ufw_rules:
- rule: allow
name: OpenSSH
- rule: allow
port: "22000"
proto: tcp
comment: Syncthing sync traffic
- rule: allow
port: "22000"
proto: udp
comment: Syncthing QUIC sync traffic
- rule: allow
port: "21027"
proto: udp
comment: Syncthing local discovery
server_sshd_settings:
PermitRootLogin: "no"
server_sshd_allow_users:
- "{{ server_username }}"
server_ssh_authorized_keys: []
server_ssh_authorized_key_directory: "{{ server_user_home }}/.ssh/authorized_keys.d"

View File

@@ -1,37 +0,0 @@
---
ubuntu_packages_base:
- curl
- ufw
- htop
- build-essential
- nodejs
- git-delta
- jq
- ripgrep
ubuntu_manage_docker_repo: true
ubuntu_emacs_packages:
- emacs
- pandoc
- latexmk
- texlive-luatex
- texlive-latex-base
- texlive-latex-recommended
- texlive-fonts-recommended
ubuntu_docker_packages:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
enabled_services:
- ufw
- docker
ubuntu_dotfiles:
- src: .bashrc.d/
dest: .bashrc.d/
mode: preserve

View File

@@ -183,6 +183,7 @@ profile_packages:
- deluge-gtk
- delve
- dnsmasq
- evolution
- poppler-glib
- poppler-utils
- fontconfig-devel
@@ -193,7 +194,6 @@ profile_packages:
- hunspell
- libvirt
- libspa-bluetooth
- libreoffice
- liberation-fonts-ttf
- libX11-devel
- libXft-devel

View File

@@ -1,5 +1,3 @@
---
workstation_manage_opencode: true
workstation_manage_ibm_bob: true
workstation_ibm_bob_install_url: "https://bob.ibm.com/download/bobshell.sh"
workstation_npm_packages: "{{ ai_agents_npm_packages + [] }}"

View File

@@ -1,14 +1,6 @@
---
emacs_enabled: true
workstation_dev_packages:
- distrobox
- gnupg
- gpg-agent
- libfido2-1
- openssh-client
- shellcheck
workstation_user_directories:
- path: "{{ user_home }}/.config"
mode: "0755"

View File

@@ -3,27 +3,25 @@ enabled_services:
- docker
workstation_dev_wsl_packages:
- lazygit
- mise
- pinentry-curses
- python3-pip
- tmux
workstation_dev_wsl_excluded_packages:
- alacritty
- pcscd
- pinentry-gnome3
- podman
- podman-compose
- ufw
- yubikey-manager
# Java 11 and Maven are managed by Mise on this Fedora WSL profile. Keep their
# versions pinned; update them deliberately.
workstation_mise_java_version: temurin-11.0.31+11
workstation_mise_maven_version: 3.9.16
workstation_is_wsl: true
workstation_manage_google_chrome: false
workstation_manage_croc: true
workstation_croc_version: v11.0.3
workstation_removed_snap_packages: []
workstation_snap_packages: []
workstation_dev_wsl_python_packages: []
workstation_wsl_systemd_enabled: true
workstation_dev_wsl_dotfiles:
- src: .bashrc.d/30-mise.sh
dest: .bashrc.d/30-mise.sh
mode: "0644"
- src: .config/mise/
dest: .config/mise/
mode: preserve
- src: .gnupg/gpg-agent.conf
dest: .gnupg/gpg-agent.conf
mode: "0600"

View File

@@ -1,47 +0,0 @@
---
enabled_services:
- docker
workstation_dev_wsl_packages:
- lazygit
- pinentry-curses
- python3-pip
workstation_dev_wsl_excluded_packages:
- alacritty
- pcscd
- pinentry-gnome3
- podman
- podman-compose
- ufw
- yubikey-manager
workstation_is_wsl: true
workstation_manage_google_chrome: false
<<<<<<< HEAD
workstation_removed_snap_packages: []
workstation_snap_packages:
- name: glab
classic: true
workstation_dev_wsl_python_packages: []
||||||| parent of cb83ed1 (Update gitconfig on WSL)
workstation_removed_snap_packages: []
workstation_snap_packages:
- name: glab
classic: true
workstation_dev_wsl_python_packages:
- pypsrp
- pyspnego
=======
workstation_manage_glab: true
workstation_glab_version: "1.103.0"
workstation_removed_snap_packages:
- glab
workstation_snap_packages: []
workstation_dev_wsl_python_packages:
- pypsrp
- pyspnego
>>>>>>> cb83ed1 (Update gitconfig on WSL)
workstation_wsl_systemd_enabled: true
workstation_dev_wsl_dotfiles:
- src: .gnupg/gpg-agent.conf
dest: .gnupg/gpg-agent.conf
mode: "0600"

View File

@@ -1,11 +0,0 @@
---
# Fedora WSL mirrors the Ubuntu WSL development profile without installing
# alternative application runtimes.
fedora_excluded_packages:
- flatpak
- lazygit
fedora_removed_packages:
- flatpak
- snapd
fedora_manage_lazygit: true
fedora_lazygit_version: 0.64.1

View File

@@ -1,75 +0,0 @@
---
emacs_enabled: true
workstation_host_linux_packages:
- croc
- gnome-shell-extension-manager
- gnome-shell-extensions
- gnome-tweaks
- gufw
- libreoffice
- meld
- network-manager-openconnect-gnome
- pcscd
- pinentry-gnome3
- podman
- podman-compose
- yubikey-manager
workstation_host_linux_dotfiles:
- src: .gnupg/gpg-agent.conf
dest: .gnupg/gpg-agent.conf
mode: "0600"
workstation_firewall_backend: ufw
workstation_manage_google_chrome: true
workstation_removed_snap_packages:
- firefox
workstation_snap_packages:
- name: intellij-idea-ultimate
classic: true
channel: latest/stable
- name: postman
classic: false
channel: latest/stable
- name: thunderbird
classic: false
channel: latest/stable
- name: code
classic: true
channel: latest/stable
- name: xournalpp
classic: false
channel: latest/stable
- name: pdfarranger
classic: false
channel: latest/stable
- name: spotify
classic: false
channel: latest/stable
- name: telegram-desktop
classic: false
channel: latest/stable
- name: dbeaver-ce
classic: true
channel: latest/stable
workstation_gnome_extensions:
- id: 9308
uuid: bluetooth-battery-monitor@v8v88v8v88.com
version_tag: 68559
enabled: true
- id: 1401
uuid: bluetooth-quick-connect@bjarosze.gmail.com
version_tag: 65323
enabled: true
- id: 6099
uuid: paperwm@paperwm.github.com
version_tag: 68525
enabled: true
workstation_disabled_gnome_extensions:
- tiling-assistant@ubuntu.com
- ubuntu-dock@ubuntu.com

View File

@@ -0,0 +1,20 @@
---
ansible_host: aegis
ansible_connection: ssh
ansible_user: pi
ansible_become: true
ansible_python_interpreter: /usr/bin/python3
# Avoid PTY framing around module JSON on this remote Fedora IoT host.
ansible_ssh_use_tty: false
aegis_lan_subnet: 192.168.178.0/24
aegis_adguard_web_port: 80
aegis_ssh_authorized_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
aegis_icloudpd_apple_id: "{{ vault_aegis_icloudpd_apple_id | default('') }}"

View File

@@ -0,0 +1,116 @@
---
# Atlas declares its intended steady state; set a feature flag false only for a deliberate suspension.
hostname: atlas
ansible_host: 192.168.178.55
ansible_user: "{{ atlas_connection_username }}"
ansible_port: 22
ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
atlas_admin_username: admin
atlas_connection_username: "{{ atlas_admin_username }}"
atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000
atlas_admin_gid: 1000
atlas_admin_ssh_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_ssh_allow_tcp_forwarding: local
atlas_lan_subnet: 192.168.178.0/24
atlas_aegis_ip: 192.168.178.54
atlas_manage_firewall: true
atlas_firewalld_zone: public
atlas_zfs_pool: zpool
# Populate only for the first pool bootstrap with four real persistent disk paths.
# Confirmed empty 4 TB IronWolf data disks; the NVMe system disk is intentionally excluded.
atlas_zpool_disks:
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1XM
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A73T2
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM
atlas_create_pool: false
atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_app_data: archive/app_data
atlas_zfs_dataset_navidrome: archive/app_data/navidrome
atlas_zfs_dataset_syncthing: archive/app_data/syncthing
atlas_zfs_dataset_media: media
atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services
atlas_mount_root: /zpool
atlas_manage_storage: true
atlas_manage_sharing: true
atlas_manage_media_stack: false
wireguard_overlay_enabled: true
wireguard_address: 10.0.0.2/24
wireguard_peers:
- name: prometheus
host: prometheus
endpoint: "{{ hostvars['prometheus']['ansible_host'] }}:{{ hostvars['prometheus']['wireguard_listen_port'] }}"
allowed_ips:
- 10.0.0.0/24
persistent_keepalive: 25
backend_phase1_enabled: true
backend_phase1_start_services: false
backend_phase1_wireguard_address: 10.0.0.2
rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: false
rocky_manage_podman: true
rocky_podman_packages:
- podman
host_packages:
- cockpit
- nfs-utils
- policycoreutils
- policycoreutils-python-utils
- python3-libselinux
- samba
- samba-client
- samba-common-tools
- zfs
atlas_nfs_exports:
- path: "{{ atlas_photobook_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_encryption: required
# Append any pre-existing laptop Unix account here and keep its Samba password in Vault.
atlas_samba_accounts:
- 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:
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="22" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="9090" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" port port="2049" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="445" protocol="tcp" accept'
host_enabled_services:
- sshd
- cockpit.socket
- zfs.target

View File

@@ -1,6 +0,0 @@
---
hostname: deadalus-fedora-wsl
workstation_wsl_windows_user_home: /mnt/c/Users/FABIOSCOTTODISANTOLO
host_packages: []
host_enabled_services: []

View File

@@ -1,6 +0,0 @@
---
hostname: deadalus-wsl
workstation_wsl_windows_user_home: /mnt/c/Users/FABIOSCOTTODISANTOLO
host_packages: []
host_enabled_services: []

View File

@@ -0,0 +1,13 @@
---
hostname: deadalus
workstation_wsl_windows_user_home: /mnt/c/Users/FABIOSCOTTODISANTOLO
host_packages: []
host_enabled_services: []
# Fedora WSL must not receive alternative application runtimes.
fedora_excluded_packages:
- flatpak
fedora_removed_packages:
- flatpak
- snapd

View File

@@ -229,7 +229,7 @@ gnome_gsettings_settings:
- schema: org.gnome.shell
key: app-picker-layout
value: >-
[{'org.fedoraproject.MediaWriter.desktop': <{'position': <0>}>, 'org.gnome.Contacts.desktop': <{'position': <1>}>, 'org.gnome.Weather.desktop': <{'position': <2>}>, 'org.gnome.clocks.desktop': <{'position': <3>}>, 'org.gnome.Maps.desktop': <{'position': <4>}>, 'org.gnome.SimpleScan.desktop': <{'position': <5>}>, 'org.gnome.Settings.desktop': <{'position': <6>}>, 'org.gnome.Boxes.desktop': <{'position': <7>}>, 'org.gnome.Showtime.desktop': <{'position': <8>}>, 'org.gnome.Snapshot.desktop': <{'position': <9>}>, 'org.gnome.Characters.desktop': <{'position': <10>}>, 'Utilities': <{'position': <11>}>, 'System': <{'position': <12>}>, 'org.gnome.Tour.desktop': <{'position': <13>}>, 'org.gnome.Yelp.desktop': <{'position': <14>}>, 'htop.desktop': <{'position': <15>}>, 'libreoffice-writer.desktop': <{'position': <16>}>, 'libreoffice-calc.desktop': <{'position': <17>}>, 'libreoffice-impress.desktop': <{'position': <18>}>, 'libreoffice-draw.desktop': <{'position': <19>}>, 'libreoffice-math.desktop': <{'position': <20>}>, 'libreoffice-base.desktop': <{'position': <21>}>, 'org.gnome.Meld.desktop': <{'position': <22>}>, 'mpv.desktop': <{'position': <23>}>}, {'com.github.jeromerobert.pdfarranger.desktop': <{'position': <0>}>, 'org.remmina.Remmina.desktop': <{'position': <1>}>, 'ch.protonmail.protonmail-bridge.desktop': <{'position': <2>}>, 'org.gnome.TextEditor.desktop': <{'position': <3>}>, 'org.gnome.Software.desktop': <{'position': <4>}>, 'org.gnome.Calculator.desktop': <{'position': <5>}>, 'org.gnome.Calendar.desktop': <{'position': <6>}>, 'com.mattjakeman.ExtensionManager.desktop': <{'position': <7>}>, 'be.alexandervanhee.gradia.desktop': <{'position': <8>}>, 'syncthing-start.desktop': <{'position': <9>}>, 'syncthing-ui.desktop': <{'position': <10>}>}]
[{'System': <{'position': <0>}>, 'Utilities': <{'position': <1>}>, 'org.fedoraproject.MediaWriter.desktop': <{'position': <2>}>, 'org.gnome.Contacts.desktop': <{'position': <3>}>, 'org.gnome.Calendar.desktop': <{'position': <4>}>, 'org.gnome.Calculator.desktop': <{'position': <5>}>, 'org.gnome.Weather.desktop': <{'position': <6>}>, 'org.gnome.clocks.desktop': <{'position': <7>}>, 'org.gnome.Maps.desktop': <{'position': <8>}>, 'org.gnome.SimpleScan.desktop': <{'position': <9>}>, 'org.gnome.TextEditor.desktop': <{'position': <10>}>, 'org.gnome.Settings.desktop': <{'position': <11>}>, 'org.gnome.Boxes.desktop': <{'position': <12>}>, 'org.gnome.Showtime.desktop': <{'position': <13>}>, 'org.gnome.Snapshot.desktop': <{'position': <14>}>, 'org.gnome.Characters.desktop': <{'position': <15>}>, 'org.gnome.Tour.desktop': <{'position': <16>}>, 'org.gnome.Yelp.desktop': <{'position': <17>}>, 'org.gnome.Software.desktop': <{'position': <18>}>, 'be.alexandervanhee.gradia.desktop': <{'position': <19>}>, 'com.github.jeromerobert.pdfarranger.desktop': <{'position': <20>}>, 'org.onlyoffice.desktopeditors.desktop': <{'position': <21>}>, 'org.gnome.Meld.desktop': <{'position': <22>}>, 'com.mattjakeman.ExtensionManager.desktop': <{'position': <23>}>}, {'org.remmina.Remmina.desktop': <{'position': <0>}>, 'ch.protonmail.protonmail-bridge.desktop': <{'position': <1>}>, 'syncthing-start.desktop': <{'position': <2>}>, 'syncthing-ui.desktop': <{'position': <3>}>, 'emacs.desktop': <{'position': <4>}>, 'emacs-client.desktop': <{'position': <5>}>, 'org.gnome.gitlab.somas.Apostrophe.desktop': <{'position': <6>}>, 'mpv.desktop': <{'position': <7>}>, 'htop.desktop': <{'position': <8>}>, 'org.mozilla.thunderbird.desktop': <{'position': <9>}>}]
- schema: org.gnome.shell
key: disabled-extensions
value: >-
@@ -241,7 +241,7 @@ gnome_gsettings_settings:
- schema: org.gnome.shell
key: favorite-apps
value: >-
['org.gnome.Nautilus.desktop', 'org.mozilla.firefox.desktop', 'com.spotify.Client.desktop', 'org.mozilla.thunderbird.desktop', 'org.telegram.desktop.desktop', 'org.gnome.Ptyxis.desktop', 'virt-manager.desktop']
['org.gnome.Nautilus.desktop', 'org.mozilla.firefox.desktop', 'com.spotify.Client.desktop', 'org.gnome.Evolution.desktop', 'org.telegram.desktop.desktop', 'org.gnome.Ptyxis.desktop', 'virt-manager.desktop']
- schema: org.gnome.shell
key: remember-mount-password
value: "true"

View File

@@ -1,5 +1,35 @@
---
hostname: prometheus
ansible_host: 179.237.102.172
ansible_user: rocky
ansible_port: 22
ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
host_packages: []
host_enabled_services: []
server_username: rocky
server_duckdns_domain: fscotto
server_ssh_authorized_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
wireguard_overlay_enabled: true
wireguard_address: 10.0.0.1/24
wireguard_listen_port: 51820
wireguard_enable_ipv4_forwarding: true
wireguard_reload_rootful_podman_networks: true
wireguard_peers:
- name: atlas
host: atlas
allowed_ips:
- 10.0.0.2/32
host_packages:
- cockpit
- cockpit-navigator
- cockpit-podman
host_enabled_services:
- cockpit.socket

View File

@@ -10,11 +10,18 @@ all:
ansible_connection: local
nymph:
ansible_connection: local
deadalus-fedora-wsl:
deadalus:
ansible_connection: local
platform_freebsd:
hosts: {}
platform_fedora_iot:
hosts:
aegis:
platform_rocky:
hosts:
atlas:
children:
rocky_server:
void:
children:
@@ -23,6 +30,11 @@ all:
fedora:
children:
platform_fedora:
platform_fedora_iot:
rocky:
children:
platform_rocky:
graphical_desktop:
hosts:
@@ -40,6 +52,10 @@ all:
ikaros:
ansible_connection: local
role_aegis:
hosts:
aegis:
desktop_sway:
hosts: {}
@@ -53,20 +69,10 @@ all:
nymph:
ansible_connection: local
ubuntu:
children:
workstation_dev_wsl_ubuntu:
ubuntu_server:
workstation:
children:
workstation_host:
workstation_dev:
workstation_host:
children:
workstation_host_linux:
workstation_dev:
children:
workstation_dev_fedora:
@@ -74,34 +80,27 @@ all:
server:
children:
ubuntu_server:
fedora_workstation:
hosts: {}
workstation_host_linux:
hosts: {}
rocky_server:
workstation_dev_fedora:
children:
workstation_dev_wsl_fedora:
hosts:
deadalus:
ansible_connection: local
workstation_dev_wsl:
children:
workstation_dev_wsl_ubuntu:
workstation_dev_wsl_fedora:
workstation_dev_wsl_ubuntu:
hosts:
deadalus-wsl:
deadalus:
ansible_connection: local
workstation_dev_wsl_fedora:
hosts:
deadalus-fedora-wsl:
ansible_connection: local
ubuntu_server:
rocky_server:
hosts:
prometheus:
ansible_connection: local
wireguard_overlay:
hosts:
prometheus:
atlas:
role_backend_phase1:
hosts:
atlas:

View File

@@ -23,6 +23,7 @@
dest: "{{ effective_user_home }}/Templates"
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
creates: "{{ effective_user_home }}/Templates/code/main.c"
when: "'desktop' in group_names"
- name: Ensure Emacs authoring directories exist
@@ -86,13 +87,25 @@
group: "{{ effective_user_group }}"
mode: "{{ item.mode }}"
loop: >-
{{
(common_dotfiles | default([]))
+ ((ai_agents_dotfiles | default([])) if (ai_agents_enabled | default(false)) else [])
}}
{{ common_dotfiles | default([]) }}
loop_control:
label: "{{ item.dest }}"
- name: Deploy AI coding-agent dotfiles
tags: [dotfiles, dotfiles:common, ai_agents]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/common/{{ item.src }}"
dest: "{{ effective_user_home }}/{{ item.dest }}"
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
mode: "{{ item.mode }}"
loop: "{{ ai_agents_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when:
- (item.agents | intersect(ai_agents_deploy_enabled_names)) | length > 0
- (ai_agents_dotfiles | default([])) | length > 0
- name: Install distro packages for Vim plugins
tags: [packages, vim, fzf]
ansible.builtin.package:
@@ -141,7 +154,7 @@
- (vim_plugin_source_plugins | default([])) | length > 0
- name: Ensure AI config directories exist
tags: [dotfiles, dotfiles:common]
tags: [dotfiles, dotfiles:common, ai_agents]
ansible.builtin.file:
path: "{{ effective_user_home }}/{{ item }}"
state: directory
@@ -151,11 +164,11 @@
loop:
- .codex
when:
- ai_agents_enabled | default(false)
- "'codex' in ai_agents_deploy_enabled_names"
- (ai_agents_templates | default([])) | length > 0
- name: Render AI agent templates
tags: [dotfiles, dotfiles:common]
- name: Render AI coding-agent templates
tags: [dotfiles, dotfiles:common, ai_agents]
ansible.builtin.template:
src: "{{ playbook_dir }}/../dotfiles/common/{{ item.src }}"
dest: "{{ effective_user_home }}/{{ item.dest }}"
@@ -165,7 +178,9 @@
loop: "{{ ai_agents_templates | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when: ai_agents_enabled | default(false)
when:
- (item.agents | intersect(ai_agents_deploy_enabled_names)) | length > 0
- (ai_agents_templates | default([])) | length > 0
- name: Refresh bat cache
tags: [dotfiles, dotfiles:common]

View File

@@ -25,46 +25,19 @@
- "'workstation_dev_fedora' in group_names"
- fedora_manage_docker_repo | default(false)
- name: Configure Google Chrome repository on Fedora
tags: [packages]
ansible.builtin.yum_repository:
name: google-chrome
description: Google Chrome
baseurl: "https://dl.google.com/linux/chrome/rpm/stable/$basearch"
enabled: true
gpgcheck: true
gpgkey: https://dl.google.com/linux/linux_signing_key.pub
- name: Install COPR support on Fedora WSL
tags: [packages, mise]
ansible.builtin.dnf:
name: dnf-plugins-core
state: present
when:
- workstation_manage_google_chrome | default(false)
- fedora_manage_google_chrome_repo | default(false)
when: "'workstation_dev_wsl' in group_names"
- name: Configure Visual Studio Code repository on Fedora
tags: [packages, vscode]
ansible.builtin.yum_repository:
name: vscode
description: Visual Studio Code
baseurl: https://packages.microsoft.com/yumrepos/vscode
enabled: true
gpgcheck: true
gpgkey: https://packages.microsoft.com/keys/microsoft.asc
state: present
when:
- "'workstation_host_linux' in group_names"
- fedora_manage_vscode_repo | default(false)
- name: Configure IntelliJ IDEA COPR repository on Fedora
tags: [packages]
ansible.builtin.get_url:
url: >-
https://copr.fedorainfracloud.org/coprs/{{ fedora_intellij_copr_owner }}/{{ fedora_intellij_copr_project }}/repo/fedora-{{ ansible_facts['distribution_major_version'] }}/{{ fedora_intellij_copr_owner }}-{{ fedora_intellij_copr_project }}-fedora-{{ ansible_facts['distribution_major_version'] }}.repo
dest: /etc/yum.repos.d/{{ fedora_intellij_copr_owner }}-{{ fedora_intellij_copr_project }}-fedora-{{ ansible_facts['distribution_major_version'] }}.repo
owner: root
group: root
mode: "0644"
when:
- "'workstation_host_linux' in group_names"
- fedora_manage_intellij_repo | default(false)
- name: Configure Mise COPR repository on Fedora WSL
tags: [packages, mise]
community.general.copr:
name: jdxcode/mise
state: enabled
when: "'workstation_dev_wsl' in group_names"
- name: Refresh dnf package metadata
tags: [packages]
@@ -84,11 +57,6 @@
if 'workstation_dev_fedora' in group_names
else []
)
+ (
['google-chrome-stable']
if workstation_manage_google_chrome | default(false)
else []
)
+ (profile_packages | default([]))
+ (
(fedora_desktop_packages | default([]))
@@ -103,7 +71,7 @@
else []
)
+ (
(fedora_workstation_dev_packages | default(workstation_dev_packages | default([])))
(fedora_dev_packages | default([]))
if 'workstation_dev_fedora' in group_names
else []
)
@@ -112,19 +80,6 @@
if 'workstation_dev_wsl' in group_names
else []
)
+ (
(workstation_host_linux_packages_fedora | default(workstation_host_linux_packages | default([])))
if 'workstation_host_linux' in group_names
else []
)
+ (
[fedora_intellij_package_name]
if (
'workstation_host_linux' in group_names
and fedora_manage_intellij_repo | default(false)
)
else []
)
+ (host_packages | default([]))
)
| difference(fedora_excluded_packages | default([]))
@@ -139,100 +94,6 @@
state: absent
when: (fedora_removed_packages | default([])) | length > 0
- name: Ensure architecture is supported for LazyGit release
tags: [packages]
ansible.builtin.fail:
msg: "Unsupported architecture {{ ansible_facts['architecture'] }} for LazyGit release"
when:
- fedora_manage_lazygit | default(false)
- ansible_facts['architecture'] not in ['x86_64', 'aarch64', 'arm64']
- name: Read installed LazyGit version
tags: [packages]
ansible.builtin.command: /usr/local/bin/lazygit --version
register: fedora_lazygit_version_check
changed_when: false
failed_when: false
check_mode: false
when: fedora_manage_lazygit | default(false)
- name: Ensure temporary LazyGit extraction directory is absent
tags: [packages]
ansible.builtin.file:
path: "/tmp/lazygit_{{ fedora_lazygit_version }}"
state: absent
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Create temporary LazyGit extraction directory
tags: [packages]
ansible.builtin.file:
path: "/tmp/lazygit_{{ fedora_lazygit_version }}"
state: directory
owner: root
group: root
mode: "0755"
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Download LazyGit release archive
tags: [packages]
ansible.builtin.get_url:
url: >-
https://github.com/jesseduffield/lazygit/releases/download/v{{ fedora_lazygit_version }}/lazygit_{{
fedora_lazygit_version }}_Linux_{{
'x86_64' if ansible_facts['architecture'] == 'x86_64'
else 'arm64'
}}.tar.gz
dest: "/tmp/lazygit_{{ fedora_lazygit_version }}.tar.gz"
mode: "0644"
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Extract LazyGit release archive
tags: [packages]
ansible.builtin.unarchive:
src: "/tmp/lazygit_{{ fedora_lazygit_version }}.tar.gz"
dest: "/tmp/lazygit_{{ fedora_lazygit_version }}"
remote_src: true
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Install LazyGit binary
tags: [packages]
ansible.builtin.copy:
src: "/tmp/lazygit_{{ fedora_lazygit_version }}/lazygit"
dest: /usr/local/bin/lazygit
owner: root
group: root
mode: "0755"
remote_src: true
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Remove downloaded LazyGit archive
tags: [packages]
ansible.builtin.file:
path: "/tmp/lazygit_{{ fedora_lazygit_version }}.tar.gz"
state: absent
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Remove temporary LazyGit extraction directory
tags: [packages]
ansible.builtin.file:
path: "/tmp/lazygit_{{ fedora_lazygit_version }}"
state: absent
when:
- fedora_manage_lazygit | default(false)
- fedora_lazygit_version not in (fedora_lazygit_version_check.stdout | default(''))
- name: Add user to docker group
tags: [packages]
ansible.builtin.user:
@@ -243,16 +104,31 @@
- "'workstation_dev_fedora' in group_names"
- (fedora_docker_packages | default([])) | length > 0
- name: Install Fedora npm packages
- name: Install shared AI coding agents on Fedora
tags: [packages, npm, ai_agents]
community.general.npm:
name: "{{ item.name }}"
name: "{{ item.value.npm_package }}"
global: true
state: "{{ item.state | default('present') }}"
loop: "{{ fedora_npm_packages | default([]) }}"
when: (fedora_npm_packages | default([])) | length > 0
state: latest
loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}"
when:
- item.value.install_enabled | bool
- "'workstation_dev_fedora' not in group_names"
loop_control:
label: "{{ item.name }}"
label: "{{ item.key }}"
- name: Uninstall shared AI coding agents on Fedora
tags: [packages, npm, ai_agents]
community.general.npm:
name: "{{ item.value.npm_package }}"
global: true
state: absent
loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}"
when:
- item.value.uninstall_enabled | bool
- "'workstation_dev_fedora' not in group_names"
loop_control:
label: "{{ item.key }}"
- name: Define Fedora Flatpak applications
tags: [packages]
@@ -262,11 +138,6 @@
(
(fedora_flatpak_packages | default([]))
+ (host_flatpak_packages | default([]))
+ (
(workstation_flatpak_packages | default([]))
if 'workstation_host_linux' in group_names
else []
)
)
| unique
}}

View File

@@ -1,14 +0,0 @@
---
- name: Install packages on FreeBSD
tags: [packages]
community.general.pkgng:
name: >-
{{
(
(freebsd_packages_base | default([]))
+ (freebsd_development_packages | default([]))
+ (host_packages | default([]))
)
| unique
}}
state: present

View File

@@ -0,0 +1,196 @@
---
- name: Require Rocky Linux 9
tags: [packages]
ansible.builtin.assert:
that:
- ansible_facts['distribution'] == 'Rocky'
- ansible_facts['distribution_major_version'] == '9'
fail_msg: This platform role supports Rocky Linux 9 only.
- name: Install DNF configuration plugin
tags: [packages]
ansible.builtin.dnf:
name: dnf-plugins-core
state: present
- name: Enable Rocky CodeReady Builder repository
tags: [packages]
community.general.dnf_config_manager:
name:
- crb
state: enabled
when: rocky_enable_crb | default(true) | bool
- name: Install EPEL repository package
tags: [packages]
ansible.builtin.dnf:
name: epel-release
state: present
- name: Import official OpenZFS EL9+ signing key
tags: [packages, storage]
ansible.builtin.rpm_key:
state: present
key: "{{ rocky_openzfs_gpg_key_url }}"
fingerprint: "{{ rocky_openzfs_gpg_key_fingerprint }}"
when: rocky_manage_openzfs_repo | bool
- name: Install official OpenZFS repository package
tags: [packages, storage]
ansible.builtin.dnf:
name: "{{ rocky_openzfs_release_rpm }}"
state: present
when: rocky_manage_openzfs_repo | bool
- name: Disable OpenZFS DKMS repository
tags: [packages, storage]
community.general.dnf_config_manager:
name:
- zfs
state: disabled
when: rocky_manage_openzfs_repo | bool
- name: Enable OpenZFS kmod repository
tags: [packages, storage]
community.general.dnf_config_manager:
name:
- zfs-kmod
state: enabled
when: rocky_manage_openzfs_repo | bool
- name: Configure official GitHub CLI RPM repository
tags: [packages]
ansible.builtin.get_url:
url: "{{ rocky_github_cli_repo_url }}"
dest: "{{ rocky_github_cli_repo_file }}"
owner: root
group: root
mode: "0644"
when: rocky_manage_github_cli_repo | bool
- name: Refresh Rocky package metadata
tags: [packages]
ansible.builtin.dnf:
update_cache: true
- name: Install packages on Rocky Linux
tags: [packages]
ansible.builtin.dnf:
name: >-
{{
(
(rocky_common_packages | default([]))
+ (rocky_packages_base | default([]))
+ (rocky_profile_packages | default([]))
+ (rocky_podman_packages | default([]))
+ (host_packages | default([]))
) | unique
}}
state: present
- name: Ensure Podman runtime socket directory exists
tags: [packages, podman]
ansible.builtin.file:
path: /run/podman
state: directory
owner: root
group: root
mode: "0700"
when: rocky_manage_podman | bool
- name: Require supported architecture for Syncthing
tags: [packages, syncthing]
ansible.builtin.assert:
that:
- ansible_facts['architecture'] == 'x86_64'
fail_msg: The pinned Atlas Syncthing binary currently supports x86_64 only.
when: rocky_manage_syncthing_binary | bool
- name: Read installed Syncthing version
tags: [packages, syncthing]
ansible.builtin.command:
argv:
- /usr/local/bin/syncthing
- --version
register: rocky_syncthing_version_check
changed_when: false
failed_when: false
check_mode: false
when: rocky_manage_syncthing_binary | bool
- name: Determine whether Syncthing must be installed
tags: [packages, syncthing]
ansible.builtin.set_fact:
rocky_syncthing_install_required: >-
{{
('syncthing v' ~ rocky_syncthing_version ~ ' ')
not in (rocky_syncthing_version_check.stdout | default(''))
}}
when: rocky_manage_syncthing_binary | bool
- name: Create temporary Syncthing extraction directory
tags: [packages, syncthing]
ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}"
state: directory
owner: root
group: root
mode: "0755"
when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Download pinned Syncthing release
tags: [packages, syncthing]
ansible.builtin.get_url:
url: "{{ rocky_syncthing_archive_url }}"
dest: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
checksum: "{{ rocky_syncthing_archive_checksum }}"
owner: root
group: root
mode: "0644"
when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Extract pinned Syncthing release
tags: [packages, syncthing]
ansible.builtin.unarchive:
src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
dest: "/tmp/syncthing-{{ rocky_syncthing_version }}"
remote_src: true
when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Install pinned Syncthing binary
tags: [packages, syncthing]
ansible.builtin.copy:
src: >-
{{
'/tmp/syncthing-' ~ rocky_syncthing_version
~ '/syncthing-linux-amd64-v' ~ rocky_syncthing_version
~ '/syncthing'
}}
dest: /usr/local/bin/syncthing
remote_src: true
owner: root
group: root
mode: "0755"
when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Remove Syncthing release archive
tags: [packages, syncthing]
ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
state: absent
when: rocky_manage_syncthing_binary | bool
- name: Remove Syncthing extraction directory
tags: [packages, syncthing]
ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}"
state: absent
when: rocky_manage_syncthing_binary | bool

View File

@@ -1,297 +0,0 @@
---
- name: Gather installed package facts
tags: [packages]
ansible.builtin.package_facts:
manager: auto
- name: Copy Ubuntu dotfiles
tags: [dotfiles, dotfiles:common]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/ubuntu/{{ item.src }}"
dest: "{{ effective_user_home }}/{{ item.dest }}"
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
mode: "{{ item.mode }}"
loop: "{{ ubuntu_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Ensure architecture is supported for Google Chrome
tags: [packages]
ansible.builtin.fail:
msg: "Unsupported architecture {{ ansible_facts['architecture'] }} for Google Chrome stable package"
when:
- workstation_manage_google_chrome | default(false)
- ansible_facts['architecture'] != 'x86_64'
- name: Download Google Chrome Debian package
tags: [packages]
ansible.builtin.get_url:
url: https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
dest: /tmp/google-chrome-stable_current_amd64.deb
mode: "0644"
when:
- workstation_manage_google_chrome | default(false)
- "'google-chrome-stable' not in (ansible_facts.packages | default({}))"
- name: Install Google Chrome from downloaded Debian package
tags: [packages]
ansible.builtin.apt:
deb: /tmp/google-chrome-stable_current_amd64.deb
state: present
when:
- workstation_manage_google_chrome | default(false)
- "'google-chrome-stable' not in (ansible_facts.packages | default({}))"
- name: Remove downloaded Google Chrome Debian package
tags: [packages]
ansible.builtin.file:
path: /tmp/google-chrome-stable_current_amd64.deb
state: absent
when:
- workstation_manage_google_chrome | default(false)
- "'google-chrome-stable' not in (ansible_facts.packages | default({}))"
- name: Download glab Debian package
tags: [packages]
ansible.builtin.get_url:
url: >-
https://gitlab.com/gitlab-org/cli/-/releases/v{{ workstation_glab_version }}/downloads/glab_{{ workstation_glab_version }}_linux_{{
'amd64' if ansible_facts['architecture'] == 'x86_64'
else 'arm64' if ansible_facts['architecture'] in ['aarch64', 'arm64']
else ansible_facts['architecture']
}}.deb
dest: "/tmp/glab_{{ workstation_glab_version }}.deb"
mode: "0644"
when:
- workstation_manage_glab | default(false)
- ansible_facts.packages['glab'] is not defined or ansible_facts.packages['glab'][0].version != workstation_glab_version
- name: Install glab from downloaded Debian package
tags: [packages]
ansible.builtin.apt:
deb: "/tmp/glab_{{ workstation_glab_version }}.deb"
state: present
when:
- workstation_manage_glab | default(false)
- ansible_facts.packages['glab'] is not defined or ansible_facts.packages['glab'][0].version != workstation_glab_version
- name: Remove downloaded glab Debian package
tags: [packages]
ansible.builtin.file:
path: "/tmp/glab_{{ workstation_glab_version }}.deb"
state: absent
when:
- workstation_manage_glab | default(false)
- ansible_facts.packages['glab'] is not defined or ansible_facts.packages['glab'][0].version != workstation_glab_version
- name: Ensure Docker apt keyrings directory exists
tags: [packages]
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: "0755"
when: ubuntu_manage_docker_repo | default(false)
- name: Download Docker apt repository signing key
tags: [packages]
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
owner: root
group: root
mode: "0644"
when: ubuntu_manage_docker_repo | default(false)
- name: Configure Docker apt repository
tags: [packages]
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ 'amd64' if ansible_facts['architecture'] == 'x86_64'
else 'arm64' if ansible_facts['architecture'] in ['aarch64', 'arm64']
else ansible_facts['architecture'] }}
signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu
{{ ansible_facts['distribution_release'] }} stable
filename: docker
state: present
update_cache: true
when: ubuntu_manage_docker_repo | default(false)
- name: Refresh apt package cache
tags: [packages]
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install packages on Ubuntu
tags: [packages]
ansible.builtin.apt:
name: >-
{{
(
(common_packages | default([]))
+ (ubuntu_packages_base | default([]))
+ (ubuntu_docker_packages | default([]))
+ (profile_packages | default([]))
+ (
(ubuntu_emacs_packages | default([]))
if (emacs_enabled | default(false) | bool)
else []
)
+ (workstation_dev_packages | default([]))
+ (
(workstation_host_linux_packages | default([]))
if 'workstation_host_linux' in group_names
else []
)
+ (
(workstation_dev_wsl_packages | default([]))
if 'workstation_dev_wsl' in group_names
else []
)
+ (desktop_common_packages | default([]))
+ (host_packages | default([]))
)
| difference(
(workstation_dev_wsl_excluded_packages | default([]))
if 'workstation_dev_wsl' in group_names
else []
)
| unique
}}
state: present
- name: Add user to docker group
tags: [packages]
ansible.builtin.user:
name: "{{ effective_username }}"
groups: docker
append: true
when: (ubuntu_docker_packages | default([])) | length > 0
- name: Ensure architecture is supported for croc GitHub release
tags: [packages]
ansible.builtin.fail:
msg: "Unsupported architecture {{ ansible_facts['architecture'] }} for croc GitHub release"
when:
- workstation_manage_croc | default(false)
- ansible_facts['architecture'] not in ['x86_64', 'aarch64', 'arm64']
- name: Read installed croc version
tags: [packages]
ansible.builtin.command: /usr/local/bin/croc --version
register: croc_version_check
changed_when: false
failed_when: false
check_mode: false
when: workstation_manage_croc | default(false)
- name: Ensure temporary croc extraction directory is absent
tags: [packages]
ansible.builtin.file:
path: "/tmp/croc_{{ workstation_croc_version }}"
state: absent
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Create temporary croc extraction directory
tags: [packages]
ansible.builtin.file:
path: "/tmp/croc_{{ workstation_croc_version }}"
state: directory
owner: root
group: root
mode: "0755"
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Download croc GitHub release archive
tags: [packages]
ansible.builtin.get_url:
url: >-
{{
'https://github.com/schollz/croc/releases/download/'
~ workstation_croc_version
~ '/croc_'
~ workstation_croc_version
~ '_Linux-'
~ (
'64bit' if ansible_facts['architecture'] == 'x86_64'
else 'ARM64' if ansible_facts['architecture'] in ['aarch64', 'arm64']
else ansible_facts['architecture']
)
~ '.tar.gz'
}}
dest: "/tmp/croc_{{ workstation_croc_version }}.tar.gz"
mode: "0644"
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Extract croc GitHub release archive
tags: [packages]
ansible.builtin.unarchive:
src: "/tmp/croc_{{ workstation_croc_version }}.tar.gz"
dest: "/tmp/croc_{{ workstation_croc_version }}"
remote_src: true
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Install croc binary
tags: [packages]
ansible.builtin.copy:
src: "/tmp/croc_{{ workstation_croc_version }}/croc"
dest: /usr/local/bin/croc
owner: root
group: root
mode: "0755"
remote_src: true
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Remove downloaded croc archive
tags: [packages]
ansible.builtin.file:
path: "/tmp/croc_{{ workstation_croc_version }}.tar.gz"
state: absent
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Remove temporary croc extraction directory
tags: [packages]
ansible.builtin.file:
path: "/tmp/croc_{{ workstation_croc_version }}"
state: absent
when:
- workstation_manage_croc | default(false)
- workstation_croc_version not in (croc_version_check.stdout | default(''))
- name: Remove workstation snap packages
tags: [packages]
community.general.snap:
name: "{{ item }}"
state: absent
loop: "{{ workstation_removed_snap_packages | default([]) }}"
loop_control:
label: "{{ item }}"
when: (workstation_removed_snap_packages | default([])) | length > 0
- name: Install workstation snap packages
tags: [packages]
community.general.snap:
name: "{{ item.name }}"
classic: "{{ item.classic | default(false) }}"
channel: "{{ item.channel | default(omit) }}"
state: present
loop: "{{ workstation_snap_packages | default([]) }}"
loop_control:
label: "{{ item.name }}"
when: (workstation_snap_packages | default([])) | length > 0

View File

@@ -0,0 +1,17 @@
---
aegis_hostname: aegis
aegis_adguard_image: docker.io/adguard/adguardhome:latest
aegis_icloudpd_image: docker.io/boredazfcuk/icloudpd:latest
aegis_icloudpd_folder_structure: '{:%Y/%m/%d}'
aegis_icloudpd_synchronisation_interval: 86400
aegis_icloudpd_apple_id: ""
aegis_ikaros_mac_address: aa:bb:cc:dd:ee:ff
aegis_wol_port: 9
aegis_lan_subnet: CHANGEME_LAN_SUBNET
aegis_firewalld_zone: public
aegis_adguard_web_port: 80
aegis_ssh_allowed_users:
- "{{ ansible_user }}"
aegis_ssh_user_home: "/var/home/{{ ansible_user }}"
aegis_ssh_authorized_keys: []

View File

@@ -0,0 +1,21 @@
---
- name: Restart Aegis systemd-resolved
ansible.builtin.systemd:
name: systemd-resolved.service
state: restarted
- name: Reload Aegis SSH
ansible.builtin.systemd:
name: sshd.service
state: reloaded
- name: Restart Aegis Quadlet services
ansible.builtin.systemd:
name: "{{ item }}"
state: restarted
daemon_reload: true
loop:
- adguardhome.service
- icloudpd.service
loop_control:
label: "{{ item }}"

View File

@@ -0,0 +1,291 @@
---
- name: Require Aegis iCloudPD Apple ID
tags: [aegis, icloudpd]
ansible.builtin.assert:
that:
- aegis_icloudpd_apple_id | length > 0
fail_msg: Define vault_aegis_icloudpd_apple_id before applying the Aegis profile.
no_log: true
- name: Require completed Aegis network placeholders
tags: [aegis, firewall, services]
ansible.builtin.assert:
that:
- aegis_lan_subnet != 'CHANGEME_LAN_SUBNET'
- aegis_firewalld_zone | length > 0
- aegis_adguard_web_port | int > 0
- aegis_adguard_web_port | int < 65536
- aegis_ssh_allowed_users | length > 0
fail_msg: Define the Aegis LAN subnet, firewalld zone, AdGuard web port, and SSH users.
- name: Set Aegis hostname
tags: [aegis, services]
ansible.builtin.hostname:
name: "{{ aegis_hostname }}"
- name: Create Aegis persistent container directories
tags: [aegis, containers]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- /var/lib/adguard/work
- /var/lib/adguard/conf
- /var/lib/icloudpd/data
- /var/lib/icloudpd/config
- name: Create Quadlet configuration directory
tags: [aegis, containers]
ansible.builtin.file:
path: /etc/containers/systemd
state: directory
owner: root
group: root
mode: "0755"
- name: Render Aegis Quadlet units
tags: [aegis, containers]
ansible.builtin.template:
src: "{{ item.src }}"
dest: "/etc/containers/systemd/{{ item.dest }}"
owner: root
group: root
mode: "0644"
loop:
- src: adguardhome.container.j2
dest: adguardhome.container
- src: icloudpd.container.j2
dest: icloudpd.container
loop_control:
label: "{{ item.dest }}"
no_log: "{{ item.dest == 'icloudpd.container' }}"
notify: Restart Aegis Quadlet services
- name: Create Aegis systemd-resolved configuration directory
tags: [aegis, adguard, dns, services]
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
state: directory
owner: root
group: root
mode: "0755"
- name: Disable Aegis systemd-resolved DNS stub listener
tags: [aegis, adguard, dns, services]
ansible.builtin.template:
src: 10-adguard-dns.conf.j2
dest: /etc/systemd/resolved.conf.d/10-adguard-dns.conf
owner: root
group: root
mode: "0644"
notify:
- Restart Aegis systemd-resolved
- Restart Aegis Quadlet services
- name: Point Aegis resolver at the full systemd-resolved configuration
tags: [aegis, adguard, dns, services]
ansible.builtin.file:
src: ../run/systemd/resolve/resolv.conf
dest: /etc/resolv.conf
state: link
force: true
notify: Restart Aegis systemd-resolved
- name: Enable Aegis firewalld
tags: [aegis, firewall, services]
ansible.builtin.systemd:
name: firewalld.service
enabled: true
state: started
- name: Render Aegis AdGuard web firewalld service
tags: [aegis, firewall]
ansible.builtin.template:
src: aegis-adguard-web.xml.j2
dest: /etc/firewalld/services/aegis-adguard-web.xml
owner: root
group: root
mode: "0644"
- name: Reload firewalld custom services
tags: [aegis, firewall]
ansible.builtin.command:
argv:
- firewall-cmd
- --reload
changed_when: false
when: not ansible_check_mode
- name: Remove unrestricted Aegis services from firewalld zone
tags: [aegis, firewall]
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: disabled
zone: "{{ aegis_firewalld_zone }}"
loop:
- ssh
- dns
- http
- aegis-adguard-web
loop_control:
label: "{{ item }}"
- name: Remove retired Aegis TLS access rule
tags: [aegis, firewall]
ansible.posix.firewalld:
rich_rule: 'rule family="ipv4" source address="{{ aegis_lan_subnet }}" port port="853" protocol="tcp" accept'
permanent: true
immediate: true
state: disabled
zone: "{{ aegis_firewalld_zone }}"
- name: Allow Aegis services from the LAN only
tags: [aegis, firewall]
ansible.posix.firewalld:
rich_rule: "{{ item }}"
permanent: true
immediate: true
state: enabled
zone: "{{ aegis_firewalld_zone }}"
loop:
- 'rule family="ipv4" source address="{{ aegis_lan_subnet }}" service name="ssh" accept'
- 'rule family="ipv4" source address="{{ aegis_lan_subnet }}" service name="dns" accept'
- 'rule family="ipv4" source address="{{ aegis_lan_subnet }}" service name="aegis-adguard-web" accept'
loop_control:
label: "{{ item }}"
- name: Check the standard Aegis SSH authorized keys file
tags: [aegis, ssh, services]
ansible.builtin.stat:
path: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys"
register: aegis_authorized_keys
- name: Ensure Aegis SSH authorized key fragments directory exists
tags: [aegis, ssh, services]
ansible.builtin.file:
path: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys.d"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0700"
when: aegis_ssh_authorized_keys | length > 0
- name: Manage Aegis SSH authorized key fragments
tags: [aegis, ssh, services]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys.d/{{ item.name }}"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0600"
loop: "{{ aegis_ssh_authorized_keys }}"
loop_control:
label: "{{ item.name }}"
when: aegis_ssh_authorized_keys | length > 0
- name: Find Aegis SSH authorized key fragments
tags: [aegis, ssh, services]
ansible.builtin.find:
paths: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys.d"
file_type: file
recurse: false
register: aegis_authorized_key_fragments
- name: Require an Aegis SSH authorized key before hardening
tags: [aegis, ssh, services]
ansible.builtin.assert:
that:
- >-
(aegis_ssh_authorized_keys | length > 0)
or (aegis_authorized_keys.stat.exists and aegis_authorized_keys.stat.size | int > 0)
or aegis_authorized_key_fragments.matched | int > 0
fail_msg: Add a public key for the Ansible SSH user before disabling password authentication.
- name: Ensure Aegis SSH configuration drop-in directory exists
tags: [aegis, ssh, services]
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
- name: Ensure Aegis SSH drop-ins are loaded before other settings
tags: [aegis, ssh, services]
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*Include\s+/etc/ssh/sshd_config\.d/\*\.conf\s*$'
line: Include /etc/ssh/sshd_config.d/*.conf
insertbefore: BOF
state: present
validate: "sshd -t -f %s"
- name: Render Aegis SSH hardening drop-in
tags: [aegis, ssh, services]
ansible.builtin.template:
src: 00-aegis-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/00-aegis-hardening.conf
owner: root
group: root
mode: "0600"
notify: Reload Aegis SSH
- name: Validate Aegis SSH configuration
tags: [aegis, ssh, services]
ansible.builtin.command:
argv:
- sshd
- -t
changed_when: false
when: not ansible_check_mode
- name: Read effective Aegis SSH daemon configuration
tags: [aegis, ssh, services]
ansible.builtin.command:
argv:
- sshd
- -T
- -C
- "user={{ ansible_user }},host={{ aegis_hostname }},addr=127.0.0.1"
register: aegis_sshd_effective_configuration
changed_when: false
when: not ansible_check_mode
- name: Verify effective Aegis SSH hardening
tags: [aegis, ssh, services]
ansible.builtin.assert:
that:
- "'permitrootlogin no' in aegis_sshd_effective_configuration.stdout_lines"
- "'pubkeyauthentication yes' in aegis_sshd_effective_configuration.stdout_lines"
- "'passwordauthentication no' in aegis_sshd_effective_configuration.stdout_lines"
- "'kbdinteractiveauthentication no' in aegis_sshd_effective_configuration.stdout_lines"
- "'allowusers ' + (aegis_ssh_allowed_users | join(' ')) in aegis_sshd_effective_configuration.stdout_lines"
when: not ansible_check_mode
- name: Install Wake-on-LAN helper for Ikaros
tags: [aegis, wol]
ansible.builtin.template:
src: wake-ikaros.j2
dest: /usr/local/bin/wake-ikaros
owner: root
group: root
mode: "0755"
- name: Enable Aegis Quadlet services and automatic updates
tags: [aegis, containers, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
daemon_reload: true
loop:
- adguardhome.service
- icloudpd.service
- podman-auto-update.timer
loop_control:
label: "{{ item }}"

View File

@@ -0,0 +1,6 @@
# Managed by Ansible. Do not edit manually.
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers {{ aegis_ssh_allowed_users | join(' ') }}

View File

@@ -0,0 +1,3 @@
# Managed by Ansible. Do not edit manually.
[Resolve]
DNSStubListener=no

View File

@@ -0,0 +1,18 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=AdGuard Home DNS sinkhole
Wants=network-online.target
After=network-online.target
[Container]
Image={{ aegis_adguard_image }}
Network=host
Volume=/var/lib/adguard/work:/opt/adguardhome/work:Z
Volume=/var/lib/adguard/conf:/opt/adguardhome/conf:Z
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Aegis AdGuard Home web interface</short>
<description>AdGuard Home administrative web interface on Aegis.</description>
<port protocol="tcp" port="{{ aegis_adguard_web_port }}"/>
</service>

View File

@@ -0,0 +1,20 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=iCloud Photos Downloader
Wants=network-online.target
After=network-online.target
[Container]
Image={{ aegis_icloudpd_image }}
Environment=apple_id={{ aegis_icloudpd_apple_id }}
Environment=folder_structure={{ aegis_icloudpd_folder_structure }}
Environment=synchronisation_interval={{ aegis_icloudpd_synchronisation_interval }}
Volume=/var/lib/icloudpd/data:/home/root/iCloud:Z
Volume=/var/lib/icloudpd/config:/config:Z
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Managed by Ansible. Do not edit manually.
set -euo pipefail
mac='{{ aegis_ikaros_mac_address }}'
port='{{ aegis_wol_port }}'
mac_hex="${mac//:/}"
if [[ ! $mac_hex =~ ^[[:xdigit:]]{12}$ ]]; then
printf 'Indirizzo MAC non valido: %s\n' "$mac" >&2
exit 1
fi
packet='\xFF\xFF\xFF\xFF\xFF\xFF'
for _ in {1..16}; do
packet+="$(printf '\\x%s\\x%s\\x%s\\x%s\\x%s\\x%s' \
"${mac_hex:0:2}" "${mac_hex:2:2}" "${mac_hex:4:2}" \
"${mac_hex:6:2}" "${mac_hex:8:2}" "${mac_hex:10:2}")"
done
printf '%b' "$packet" > "/dev/udp/255.255.255.255/${port}"

View File

@@ -0,0 +1,142 @@
---
atlas_manage_storage: false
atlas_manage_sharing: false
# Destructive first-boot action; normally false once the pool exists.
atlas_create_pool: false
atlas_zpool_disks: []
atlas_admin_username: CHANGEME_ATLAS_ADMIN
atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000
atlas_admin_gid: 1000
atlas_admin_ssh_keys: []
atlas_admin_ssh_key_directory: "{{ atlas_admin_home }}/.ssh/authorized_keys.d"
atlas_admin_password_hash: "!"
# Local forwarding permits an administrator to tunnel a private service without allowing remote forwards.
atlas_ssh_allow_tcp_forwarding: local
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_aegis_ip: CHANGEME_AEGIS_IP
atlas_manage_firewall: false
atlas_firewalld_zone: public
atlas_hardening_sysctls:
net.ipv4.conf.all.accept_redirects: "0"
net.ipv4.conf.default.accept_redirects: "0"
net.ipv4.conf.all.send_redirects: "0"
net.ipv4.conf.default.send_redirects: "0"
net.ipv4.conf.all.accept_source_route: "0"
net.ipv4.conf.default.accept_source_route: "0"
net.ipv4.conf.all.log_martians: "1"
net.ipv4.conf.default.log_martians: "1"
net.ipv4.conf.all.rp_filter: "2"
net.ipv4.conf.default.rp_filter: "2"
net.ipv4.tcp_syncookies: "1"
net.ipv4.icmp_echo_ignore_broadcasts: "1"
net.ipv4.icmp_ignore_bogus_error_responses: "1"
net.ipv4.ip_forward: "0"
net.ipv6.conf.all.accept_redirects: "0"
net.ipv6.conf.default.accept_redirects: "0"
net.ipv6.conf.all.accept_source_route: "0"
net.ipv6.conf.default.accept_source_route: "0"
atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_app_data: archive/app_data
atlas_zfs_dataset_navidrome: archive/app_data/navidrome
atlas_zfs_dataset_syncthing: archive/app_data/syncthing
atlas_zfs_dataset_media: media
atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services
atlas_zfs_service_backups_refreservation: 500G
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
atlas_app_data_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}"
atlas_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}"
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}"
atlas_photobook_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_photobook }}"
atlas_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backups }}"
atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}"
atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo
atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo
atlas_45drives_packages:
- cockpit-file-sharing
- cockpit-navigator
- cockpit-identities
- cockpit-zfs
- cockpit-scheduler
atlas_nfs_exports:
- path: "{{ atlas_photobook_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
# Keep encryption mandatory unless a verified client-compatibility exception is explicitly required.
atlas_samba_encryption: required
atlas_samba_valid_users: []
atlas_samba_accounts: []
atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords
atlas_firewalld_rich_rules: []
atlas_firewalld_restricted_services:
- ssh
- cockpit
- nfs
- samba
- http
- https
atlas_selinux_booleans:
- samba_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

@@ -0,0 +1,41 @@
---
- name: Reload SSH service
ansible.builtin.systemd:
name: sshd
state: reloaded
- name: Reload NFS exports
ansible.builtin.command:
argv:
- exportfs
- -ra
changed_when: true
when: not ansible_check_mode
- name: Restart NFS server
ansible.builtin.systemd:
name: nfs-server
state: restarted
- name: Restart NFS mount daemon
ansible.builtin.systemd:
name: nfs-mountd
state: restarted
daemon_reload: true
- name: Restart Samba service
ansible.builtin.systemd:
name: smb
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

@@ -0,0 +1,180 @@
---
- name: Reject incomplete Atlas account configuration
tags: [atlas, services]
ansible.builtin.assert:
that:
- atlas_admin_username != 'CHANGEME_ATLAS_ADMIN'
- (atlas_admin_ssh_keys | default([])) | length > 0
- atlas_admin_password_hash not in ['', '!', '*']
- atlas_ssh_allow_tcp_forwarding in ['no', 'yes', 'local', 'remote', 'all']
- "'wheel' not in atlas_immich_supplementary_groups"
fail_msg: >-
Define atlas_admin_username, atlas_admin_ssh_keys and vault_atlas_admin_password_hash
before applying the Atlas profile.
no_log: true
- name: Create Atlas administrator group
tags: [atlas, services]
ansible.builtin.group:
name: "{{ atlas_admin_group }}"
gid: "{{ atlas_admin_gid }}"
state: present
- name: Create Atlas administrator account
tags: [atlas, services]
ansible.builtin.user:
name: "{{ atlas_admin_username }}"
uid: "{{ atlas_admin_uid }}"
group: "{{ atlas_admin_group }}"
home: "{{ atlas_admin_home }}"
shell: /bin/bash
password: "{{ atlas_admin_password_hash }}"
create_home: true
state: present
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
tags: [atlas, services]
ansible.builtin.copy:
content: "{{ atlas_admin_username }} ALL=(ALL) NOPASSWD: ALL\n"
dest: "/etc/sudoers.d/{{ atlas_admin_username }}"
owner: root
group: root
mode: "0440"
validate: "visudo -cf %s"
- name: Ensure Atlas administrator SSH authorized key fragments directory exists
tags: [atlas, services]
ansible.builtin.file:
path: "{{ atlas_admin_ssh_key_directory }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0700"
- name: Manage Atlas administrator SSH authorized key fragments
tags: [atlas, services]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ atlas_admin_ssh_key_directory }}/{{ item.name }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0600"
loop: "{{ atlas_admin_ssh_keys }}"
loop_control:
label: "{{ item.name }}"
- name: Check whether the Atlas SSH host key exists
tags: [atlas, services]
ansible.builtin.stat:
path: /etc/ssh/ssh_host_ed25519_key
register: atlas_ssh_host_ed25519_key
- name: Generate missing Atlas SSH host keys
tags: [atlas, services]
ansible.builtin.command:
argv:
- ssh-keygen
- -A
changed_when: true
when: not atlas_ssh_host_ed25519_key.stat.exists
- name: Ensure Atlas SSH configuration drop-in directory exists
tags: [atlas, services]
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
- name: Ensure Atlas SSH drop-ins are loaded before other settings
tags: [atlas, services]
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*Include\s+/etc/ssh/sshd_config\.d/\*\.conf\s*$'
line: Include /etc/ssh/sshd_config.d/*.conf
insertbefore: BOF
state: present
validate: "sshd -t -f %s"
- name: Render Atlas SSH hardening drop-in
tags: [atlas, services]
ansible.builtin.template:
src: 00-atlas-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/00-atlas-hardening.conf
owner: root
group: root
mode: "0600"
notify: Reload SSH service
- name: Read effective Atlas SSH daemon configuration
tags: [atlas, services]
ansible.builtin.command:
argv:
- sshd
- -T
- -C
- "user={{ atlas_admin_username }},host=atlas,addr=127.0.0.1"
register: atlas_sshd_effective_configuration
changed_when: false
when: not ansible_check_mode
- name: Verify effective Atlas SSH hardening
tags: [atlas, services]
ansible.builtin.assert:
that:
- "'permitrootlogin no' in atlas_sshd_effective_configuration.stdout_lines"
- "'pubkeyauthentication yes' in atlas_sshd_effective_configuration.stdout_lines"
- "'passwordauthentication no' in atlas_sshd_effective_configuration.stdout_lines"
- "'kbdinteractiveauthentication no' in atlas_sshd_effective_configuration.stdout_lines"
- "'authenticationmethods publickey' in atlas_sshd_effective_configuration.stdout_lines"
- "'x11forwarding no' in atlas_sshd_effective_configuration.stdout_lines"
- "('allowtcpforwarding ' ~ atlas_ssh_allow_tcp_forwarding) in atlas_sshd_effective_configuration.stdout_lines"
- "'allowagentforwarding no' in atlas_sshd_effective_configuration.stdout_lines"
- "'gatewayports no' in atlas_sshd_effective_configuration.stdout_lines"
- "'permittunnel no' in atlas_sshd_effective_configuration.stdout_lines"
- "'permituserenvironment no' in atlas_sshd_effective_configuration.stdout_lines"
- "'maxauthtries 3' in atlas_sshd_effective_configuration.stdout_lines"
- "'logingracetime 30' in atlas_sshd_effective_configuration.stdout_lines"
- "'clientaliveinterval 300' in atlas_sshd_effective_configuration.stdout_lines"
- "'clientalivecountmax 2' in atlas_sshd_effective_configuration.stdout_lines"
- "'loglevel VERBOSE' in atlas_sshd_effective_configuration.stdout_lines"
- "('allowusers ' ~ atlas_admin_username) in atlas_sshd_effective_configuration.stdout_lines"
fail_msg: The effective Atlas SSH configuration does not match the required hardening.
when: not ansible_check_mode

View File

@@ -0,0 +1,15 @@
---
- name: Configure the official 45Drives repository
tags: [atlas, packages]
ansible.builtin.get_url:
url: "{{ atlas_45drives_repo_url }}"
dest: "{{ atlas_45drives_repo_file }}"
owner: root
group: root
mode: "0644"
- name: Install 45Drives Cockpit plugins
tags: [atlas, packages]
ansible.builtin.dnf:
name: "{{ atlas_45drives_packages }}"
state: present

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

@@ -0,0 +1,45 @@
---
- name: Enforce targeted SELinux on Atlas
tags: [atlas, hardening, security]
ansible.posix.selinux:
policy: targeted
state: enforcing
update_kernel_param: true
register: atlas_selinux_enforcement
- name: Report when Atlas requires a reboot for SELinux enforcement
tags: [atlas, hardening, security]
ansible.builtin.debug:
msg: Reboot Atlas before relying on SELinux enforcement.
when: atlas_selinux_enforcement.reboot_required | default(false)
- name: Apply Atlas network-kernel hardening
tags: [atlas, hardening, security]
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
state: present
sysctl_set: true
reload: true
loop: "{{ atlas_hardening_sysctls | dict2items }}"
loop_control:
label: "{{ item.key }}={{ item.value }}"
- name: Read active Atlas SELinux mode
tags: [atlas, hardening, security]
ansible.builtin.command:
argv:
- getenforce
register: atlas_selinux_mode
changed_when: false
when: not ansible_check_mode
- name: Require active SELinux enforcement on Atlas
tags: [atlas, hardening, security]
ansible.builtin.assert:
that:
- atlas_selinux_mode.stdout == 'Enforcing'
fail_msg: >-
Atlas SELinux is not enforcing. Reboot if requested by the preceding
SELinux task, then rerun the hardening role before enabling services.
when: not ansible_check_mode

View File

@@ -0,0 +1,21 @@
---
- name: Import Atlas account tasks
ansible.builtin.import_tasks: account.yml
- name: Import Atlas 45Drives tasks
ansible.builtin.import_tasks: cockpit.yml
- name: Import Atlas network hardening tasks
ansible.builtin.import_tasks: hardening.yml
- name: Import Atlas pool bootstrap tasks
ansible.builtin.import_tasks: pool.yml
- name: Import Atlas storage tasks
ansible.builtin.import_tasks: storage.yml
- name: Import Atlas file sharing tasks
ansible.builtin.import_tasks: sharing.yml
- name: Import Atlas media-container tasks
ansible.builtin.import_tasks: containers.yml

View File

@@ -0,0 +1,57 @@
---
- name: Bootstrap Atlas ZFS pool
tags: [atlas, storage, pool]
when: atlas_create_pool | bool
block:
- name: Validate Atlas pool bootstrap inputs
ansible.builtin.assert:
that:
- atlas_zpool_disks | length == 4
- atlas_zpool_disks | unique | length == 4
- atlas_zpool_disks | select('match', '^/dev/disk/by-id/') | list | length == 4
fail_msg: >-
Set exactly four distinct persistent /dev/disk/by-id paths in
atlas_zpool_disks before creating the RAIDZ2 pool.
- name: Inspect declared Atlas pool disks
ansible.builtin.stat:
path: "{{ item }}"
follow: true
loop: "{{ atlas_zpool_disks }}"
loop_control:
label: "{{ item }}"
register: atlas_zpool_disk_stats
- name: Require every declared Atlas pool disk
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.isblk | default(false)
fail_msg: "Declared Atlas pool disk is unavailable or is not a block device: {{ item.item }}"
loop: "{{ atlas_zpool_disk_stats.results }}"
loop_control:
label: "{{ item.item }}"
- name: Check whether the Atlas ZFS pool already exists
ansible.builtin.command:
argv:
- zpool
- list
- -H
- -o
- name
- "{{ atlas_zfs_pool }}"
register: atlas_zpool_bootstrap_check
changed_when: false
failed_when: atlas_zpool_bootstrap_check.rc not in [0, 1]
- name: Create the Atlas RAIDZ2 pool when absent
community.general.zpool:
name: "{{ atlas_zfs_pool }}"
state: present
mountpoint: "{{ atlas_mount_root }}"
force: false
vdevs:
- type: raidz2
disks: "{{ atlas_zpool_disks }}"
when: atlas_zpool_bootstrap_check.rc == 1

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

@@ -0,0 +1,271 @@
- name: Require completed Atlas firewall placeholders
tags: [atlas, sharing, services]
ansible.builtin.assert:
that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP'
- atlas_firewalld_zone | length > 0
- ansible_facts.default_ipv4.interface | default('') | length > 0
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules
tags: [atlas, sharing, services]
ansible.posix.firewalld:
rich_rule: "{{ item }}"
permanent: true
immediate: true
state: enabled
zone: "{{ atlas_firewalld_zone }}"
loop: "{{ atlas_firewalld_rich_rules }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_firewall | bool
- name: Assign primary Atlas LAN interface to managed firewalld zone
tags: [atlas, sharing, services]
ansible.posix.firewalld:
interface: "{{ ansible_facts.default_ipv4.interface }}"
zone: "{{ atlas_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
when: atlas_manage_firewall | bool
- name: Remove unrestricted Atlas services from firewalld zone
tags: [atlas, sharing, services]
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: disabled
zone: "{{ atlas_firewalld_zone }}"
loop: "{{ atlas_firewalld_restricted_services }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_firewall | bool
- name: Render Atlas NFS exports
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas.exports.j2
dest: /etc/exports.d/atlas.exports
owner: root
group: root
mode: "0644"
notify: Reload NFS exports
when: atlas_manage_sharing | bool
- name: Ensure Atlas NFS configuration drop-in directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: /etc/nfs.conf.d
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_sharing | bool
- name: Configure Atlas NFSv4-only service
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas-nfs.conf.j2
dest: /etc/nfs.conf.d/atlas.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS server
when: atlas_manage_sharing | bool
- name: Mask Atlas NFSv3 RPC services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
masked: true
loop:
- rpc-statd.service
- rpcbind.service
- rpcbind.socket
loop_control:
label: "{{ item }}"
when: atlas_manage_sharing | bool
- name: Ensure Atlas NFS mount daemon drop-in directory exists
tags: [atlas, sharing, services]
ansible.builtin.file:
path: /etc/systemd/system/nfs-mountd.service.d
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_sharing | bool
- name: Disable Atlas NFSv3 mount daemon listeners
tags: [atlas, sharing, services]
ansible.builtin.template:
src: nfs-mountd-v4only.conf.j2
dest: /etc/systemd/system/nfs-mountd.service.d/v4only.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS mount daemon
when: atlas_manage_sharing | bool
- name: Enable SELinux access for Atlas file sharing
tags: [atlas, sharing, services]
ansible.posix.seboolean:
name: "{{ item }}"
state: true
persistent: true
loop: "{{ atlas_selinux_booleans }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_sharing | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Atlas Samba configuration
tags: [atlas, sharing]
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
owner: root
group: root
mode: "0644"
validate: "testparm --suppress-prompt %s"
notify: Restart Samba service
when: atlas_manage_sharing | bool
- name: Require Vault-backed Atlas Samba accounts
tags: [atlas, sharing]
ansible.builtin.assert:
that:
- atlas_samba_encryption in ['required', 'desired']
- atlas_samba_accounts | length > 0
- >-
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
when: atlas_manage_sharing | bool
- name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: "{{ atlas_samba_password_marker_dir }}"
state: directory
owner: root
group: root
mode: "0700"
when: atlas_manage_sharing | bool
- name: Manage Vault-backed Atlas Samba credentials
tags: [atlas, sharing]
ansible.builtin.include_tasks: samba_account.yml
loop: "{{ atlas_samba_accounts }}"
loop_control:
loop_var: atlas_samba_account
label: "{{ atlas_samba_account.username }}"
no_log: true
when: atlas_manage_sharing | 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_sharing | bool
- name: Gather effective Atlas firewalld zone configuration
tags: [atlas, sharing, services, security]
ansible.posix.firewalld_info:
zones:
- "{{ atlas_firewalld_zone }}"
register: atlas_firewalld_info
when:
- atlas_manage_firewall | bool
- not ansible_check_mode
- name: Verify effective Atlas firewalld restrictions
tags: [atlas, sharing, services, security]
ansible.builtin.assert:
that:
- atlas_firewalld_zone in atlas_firewalld_info.firewalld_info.zones
- >-
ansible_facts.default_ipv4.interface
in atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].interfaces
- >-
atlas_firewalld_restricted_services
| intersect(atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].services)
| length == 0
fail_msg: >-
The effective Atlas firewalld zone does not restrict the primary LAN
interface and unmanaged service exposure as declared.
when:
- atlas_manage_firewall | bool
- not ansible_check_mode
- name: Apply pending Atlas sharing handlers before verification
tags: [atlas, sharing, services, security]
ansible.builtin.meta: flush_handlers
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Check active Atlas file-sharing services
tags: [atlas, sharing, services, security]
ansible.builtin.command:
argv:
- systemctl
- is-active
- --quiet
- "{{ item }}"
loop:
- nfs-server.service
- smb.service
loop_control:
label: "{{ item }}"
register: atlas_file_sharing_service_activity
changed_when: false
failed_when: false
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Require active Atlas file-sharing services
tags: [atlas, sharing, services, security]
ansible.builtin.assert:
that:
- atlas_file_sharing_service_activity.results | map(attribute='rc') | list == [0, 0]
fail_msg: Atlas NFSv4 or SMB3 did not start after its managed configuration was applied.
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Require Atlas file-sharing TCP listeners
tags: [atlas, sharing, services, security]
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ item }}"
state: started
timeout: 15
loop:
- 2049
- 445
loop_control:
label: "{{ item }}/tcp"
when:
- atlas_manage_sharing | bool
- not ansible_check_mode

View File

@@ -0,0 +1,144 @@
---
- name: Define Atlas ZFS datasets
tags: [atlas, storage]
ansible.builtin.set_fact:
atlas_zfs_datasets:
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}"
mountpoint: "{{ atlas_work_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
mountpoint: "{{ atlas_work_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
mountpoint: "{{ atlas_archive_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
mountpoint: "{{ atlas_app_data_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_app_data_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_navidrome }}"
mountpoint: "{{ atlas_navidrome_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_navidrome_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
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_media }}"
mountpoint: "{{ atlas_media_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_media_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_backup_prometheus }}"
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_photobook }}"
mountpoint: "{{ atlas_photobook_mountpoint }}"
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_photobook_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backups }}"
mountpoint: "{{ atlas_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_backups_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}"
mountpoint: "{{ atlas_service_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_service_backups_mountpoint }}"
refreservation: "{{ atlas_zfs_service_backups_refreservation }}"
- name: Require completed Atlas storage placeholders
tags: [atlas, storage]
ansible.builtin.assert:
that:
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
- atlas_mount_root != '/CHANGEME_ATLAS_MOUNT_ROOT'
- atlas_mount_root.startswith('/')
- (atlas_zfs_datasets | map(attribute='name') | unique | list | length) == (atlas_zfs_datasets | length)
- (atlas_zfs_datasets | map(attribute='mountpoint') | unique | list | length) == (atlas_zfs_datasets | length)
fail_msg: >-
Replace the Atlas ZFS pool and mount-root placeholders, then set
atlas_manage_storage to true before managing datasets.
when: atlas_manage_storage | bool
- name: Check whether the configured Atlas ZFS pool exists
tags: [atlas, storage]
ansible.builtin.command:
argv:
- zpool
- list
- -H
- -o
- name
- "{{ atlas_zfs_pool }}"
register: atlas_zfs_pool_check
changed_when: false
failed_when: false
when: atlas_manage_storage | bool
- name: Require the configured Atlas ZFS pool
tags: [atlas, storage]
ansible.builtin.assert:
that:
- atlas_zfs_pool_check.rc == 0
fail_msg: >-
The configured Atlas ZFS pool does not exist. The Atlas profile only
manages child datasets and never creates pools.
when: atlas_manage_storage | bool
- name: Manage Atlas ZFS datasets declaratively
tags: [atlas, storage]
community.general.zfs:
name: "{{ item.name }}"
state: present
extra_zfs_properties: "{{ item.properties }}"
loop: "{{ atlas_zfs_datasets }}"
loop_control:
label: "{{ item.name }}"
when: atlas_manage_storage | bool
- name: Set Atlas dataset ownership
tags: [atlas, storage]
ansible.builtin.file:
path: "{{ item.mountpoint }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "0770"
loop: "{{ atlas_zfs_datasets }}"
loop_control:
label: "{{ item.mountpoint }}"
when: atlas_manage_storage | bool

View File

@@ -0,0 +1,24 @@
# Managed by Ansible. Do not edit manually.
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
AuthorizedKeysFile {{
atlas_admin_ssh_keys
| map(attribute='name')
| map('regex_replace', '^', '%h/.ssh/authorized_keys.d/')
| join(' ')
}}
X11Forwarding no
AllowTcpForwarding {{ atlas_ssh_allow_tcp_forwarding }}
AllowAgentForwarding no
GatewayPorts no
PermitTunnel no
PermitUserEnvironment no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE
AllowUsers {{ atlas_admin_username }}

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_photobook_mountpoint }}:/external/photobook: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,3 @@
# Managed by Ansible. Do not edit manually.
[nfsd]
vers3 = n

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

@@ -0,0 +1,4 @@
# Managed by Ansible. Do not edit manually.
{% for export in atlas_nfs_exports %}
{{ export.path }} {{ export.client }}({{ export.options | join(',') }})
{% 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

@@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=/usr/sbin/rpc.mountd --no-tcp --no-udp

View File

@@ -0,0 +1,21 @@
# Managed by Ansible. Do not edit manually.
[global]
workgroup = {{ atlas_samba_workgroup }}
security = user
map to guest = Never
server min protocol = SMB3
server signing = mandatory
smb encrypt = {{ atlas_samba_encryption }}
disable netbios = yes
smb ports = 445
hosts allow = {{ atlas_lan_subnet }}
include = registry
[{{ atlas_samba_share_name }}]
path = {{ atlas_archive_mountpoint }}
browseable = yes
read only = no
valid users = {{ atlas_samba_valid_users | join(' ') }}
force group = {{ atlas_admin_group }}
create mask = 0660
directory mask = 0770

View File

@@ -0,0 +1,28 @@
---
backend_phase1_enabled: false
backend_phase1_start_services: false
backend_phase1_username: "{{ atlas_admin_username }}"
backend_phase1_user_group: "{{ atlas_admin_group }}"
backend_phase1_user_home: "{{ atlas_admin_home }}"
backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd"
backend_phase1_wireguard_interface: wg0
backend_phase1_wireguard_address: CHANGEME_ATLAS_WIREGUARD_ADDRESS
backend_phase1_wireguard_firewalld_zone: wireguard
backend_phase1_music_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_app_data_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}"
backend_phase1_navidrome_data_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
backend_phase1_syncthing_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
backend_phase1_music_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_app_data_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
backend_phase1_navidrome_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_navidrome }}"
backend_phase1_syncthing_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
backend_phase1_navidrome_image: docker.io/deluan/navidrome:0.63.2
backend_phase1_syncthing_image: docker.io/syncthing/syncthing:2.1.3
backend_phase1_navidrome_port: 4533
backend_phase1_syncthing_gui_port: 8384
backend_phase1_syncthing_transfer_port: 22000
backend_phase1_syncthing_discovery_port: 21027
backend_phase1_timezone: Europe/Rome
backend_phase1_services:
- atlas-navidrome.service
- atlas-syncthing.service

View File

@@ -0,0 +1,32 @@
---
- name: Restart Atlas Navidrome Quadlet
tags: [backend_phase1, navidrome, podman, services]
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: atlas-navidrome.service
scope: user
state: restarted
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_enabled | bool
- backend_phase1_start_services | bool
- not ansible_check_mode
- name: Restart Atlas Syncthing Quadlet
tags: [backend_phase1, syncthing, podman, services]
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: atlas-syncthing.service
scope: user
state: restarted
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_enabled | bool
- backend_phase1_start_services | bool
- not ansible_check_mode

View File

@@ -0,0 +1,163 @@
---
- name: Configure Atlas phase-one backend services
tags: [backend_phase1, podman]
when: backend_phase1_enabled | bool
block:
- name: Validate phase-one backend inputs
ansible.builtin.assert:
that:
- not (atlas_manage_media_stack | bool)
- backend_phase1_wireguard_interface in ansible_facts.interfaces
- backend_phase1_wireguard_address != 'CHANGEME_ATLAS_WIREGUARD_ADDRESS'
- backend_phase1_music_dir.startswith('/')
- 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 + '/')
fail_msg: >-
Disable the rootful media-stack gate and provide the active
WireGuard interface/address and absolute ZFS-backed paths before
enabling phase one. This role does not manage Prometheus or migrate
Navidrome application data.
- name: Read the rootless service account
ansible.builtin.getent:
database: passwd
key: "{{ backend_phase1_username }}"
- 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] }}"
- name: Read system service state before starting rootless Syncthing
ansible.builtin.service_facts:
- name: Refuse to overlap a system-level Atlas Syncthing service
ansible.builtin.assert:
that:
- >-
ansible_facts.services['atlas-syncthing.service'] is not defined
or ansible_facts.services['atlas-syncthing.service'].state != 'running'
fail_msg: >-
Remove or stop the system-level atlas-syncthing.service before
enabling the rootless Syncthing Quadlet.
- name: Inspect required ZFS datasets
community.general.zfs_facts:
name: "{{ item.dataset }}"
properties: name,mounted,mountpoint
loop:
- dataset: "{{ backend_phase1_music_dataset }}"
mountpoint: "{{ backend_phase1_music_dir }}"
- dataset: "{{ backend_phase1_app_data_dataset }}"
mountpoint: "{{ backend_phase1_app_data_root }}"
- dataset: "{{ backend_phase1_navidrome_dataset }}"
mountpoint: "{{ backend_phase1_navidrome_data_dir }}"
- dataset: "{{ backend_phase1_syncthing_dataset }}"
mountpoint: "{{ backend_phase1_syncthing_root }}"
loop_control:
label: "{{ item.dataset }}"
register: backend_phase1_zfs_facts
- name: Require mounted datasets at the declared paths
ansible.builtin.assert:
that:
- item.ansible_facts.ansible_zfs_datasets | length == 1
- item.ansible_facts.ansible_zfs_datasets[0].mounted == 'yes'
- item.ansible_facts.ansible_zfs_datasets[0].mountpoint == item.item.mountpoint
fail_msg: >-
{{ item.item.dataset }} must already be mounted at
{{ item.item.mountpoint }}. The phase-one role never creates or
imports the pool.
loop: "{{ backend_phase1_zfs_facts.results }}"
loop_control:
label: "{{ item.item.dataset }}"
- name: Enable lingering for the rootless service account
ansible.builtin.command:
argv:
- loginctl
- enable-linger
- "{{ backend_phase1_username }}"
creates: "/var/lib/systemd/linger/{{ backend_phase1_username }}"
- name: Start the rootless user systemd manager
ansible.builtin.systemd:
name: "user@{{ backend_phase1_uid }}.service"
state: started
when: not ansible_check_mode
- name: Create rootless Quadlet and application directories
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "{{ item.mode }}"
loop:
- path: "{{ backend_phase1_quadlet_dir }}"
mode: "0700"
- path: "{{ backend_phase1_navidrome_data_dir }}"
mode: "0750"
- path: "{{ backend_phase1_syncthing_root }}"
mode: "0750"
loop_control:
label: "{{ item.path }}"
- name: Render the rootless Navidrome Quadlet
ansible.builtin.template:
src: atlas-navidrome.container.j2
dest: "{{ backend_phase1_quadlet_dir }}/atlas-navidrome.container"
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0644"
notify: Restart Atlas Navidrome Quadlet
- name: Render the rootless Syncthing Quadlet
ansible.builtin.template:
src: atlas-syncthing.container.j2
dest: "{{ backend_phase1_quadlet_dir }}/atlas-syncthing.container"
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0644"
notify: Restart Atlas Syncthing Quadlet
- name: Reload the rootless user systemd manager
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
scope: user
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: not ansible_check_mode
- name: Permit phase-one services only through the WireGuard zone
ansible.posix.firewalld:
port: "{{ item }}"
zone: "{{ backend_phase1_wireguard_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
loop:
- "{{ backend_phase1_navidrome_port }}/tcp"
- "{{ backend_phase1_syncthing_gui_port }}/tcp"
- "{{ backend_phase1_syncthing_transfer_port }}/tcp"
- "{{ backend_phase1_syncthing_transfer_port }}/udp"
- "{{ backend_phase1_syncthing_discovery_port }}/udp"
- name: Start rootless phase-one Quadlets
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: "{{ item }}"
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"
loop: "{{ backend_phase1_services }}"
when:
- backend_phase1_start_services | bool
- not ansible_check_mode

View File

@@ -0,0 +1,28 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas rootless Navidrome backend
[Container]
ContainerName=atlas-navidrome
Image={{ backend_phase1_navidrome_image }}
UserNS=keep-id
User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_navidrome_port }}:4533
Environment=ND_LOGLEVEL=info
Environment=ND_SCANSCHEDULE=1h
Environment=ND_SESSIONTIMEOUT=24h
Environment=ND_ENABLETRANSCODING=true
Environment=TZ={{ backend_phase1_timezone }}
Volume={{ backend_phase1_navidrome_data_dir }}:/data:Z
Volume={{ backend_phase1_music_dir }}:/music:ro,z
NoNewPrivileges=true
DropCapability=all
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,30 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas rootless Syncthing backend
[Container]
ContainerName=atlas-syncthing
Image={{ backend_phase1_syncthing_image }}
HostName=atlas-syncthing
UserNS=keep-id
User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_gui_port }}:{{ backend_phase1_syncthing_gui_port }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_transfer_port }}:{{ backend_phase1_syncthing_transfer_port }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_transfer_port }}:{{ backend_phase1_syncthing_transfer_port }}/udp
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_discovery_port }}:{{ backend_phase1_syncthing_discovery_port }}/udp
Environment=HOME=/var/syncthing
Environment=STHOMEDIR=/var/syncthing/config
Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }}
Environment=TZ={{ backend_phase1_timezone }}
Volume={{ backend_phase1_syncthing_root }}:/var/syncthing:Z
NoNewPrivileges=true
DropCapability=all
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -556,6 +556,30 @@
loop_control:
label: "{{ item.name }}"
- name: Install shared AI coding agents on Void desktop
tags: [packages, npm, ai_agents]
community.general.npm:
name: "{{ item.value.npm_package }}"
global: true
state: latest
loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}"
when:
- item.value.install_enabled | bool
loop_control:
label: "{{ item.key }}"
- name: Uninstall shared AI coding agents on Void desktop
tags: [packages, npm, ai_agents]
community.general.npm:
name: "{{ item.value.npm_package }}"
global: true
state: absent
loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}"
when:
- item.value.uninstall_enabled | bool
loop_control:
label: "{{ item.key }}"
- name: Install desktop Python tools with uv
tags: [packages]
ansible.builtin.command:

View File

@@ -10,15 +10,3 @@
loop: "{{ personal_workstation_directories | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Copy personal workstation dotfiles
tags: [dotfiles, dotfiles:workstation]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/workstation/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ personal_workstation_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"

View File

@@ -2,5 +2,10 @@
- name: Reload SSH service
tags: [services]
ansible.builtin.service:
name: ssh
name: "{{ server_sshd_service_name }}"
state: reloaded
- name: Reload systemd for Podman Compose
tags: [services]
ansible.builtin.systemd:
daemon_reload: true

View File

@@ -0,0 +1,33 @@
---
- name: Require DuckDNS domain and Vault token before deployment
ansible.builtin.assert:
that:
- >-
server_duckdns_domain | default('') is
regex('[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?', match_type='fullmatch')
- >-
vault_duckdns_token | default('') is
regex('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', match_type='fullmatch')
fail_msg: >-
Define server_duckdns_domain in host_vars and the rotated vault_duckdns_token
in encrypted Vault or an untracked local vars file before deploying DuckDNS.
no_log: true
- name: Ensure private DuckDNS directory exists
ansible.builtin.file:
path: "{{ server_user_home }}/duckdns"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
- name: Render DuckDNS updater with the Vault token
ansible.builtin.template:
src: duck.sh.j2
dest: "{{ server_user_home }}/duckdns/duck.sh"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
validate: /bin/sh -n %s
no_log: true
diff: false

View File

@@ -1,5 +1,37 @@
---
- name: Require supported server firewall backend
tags: [services, packages]
ansible.builtin.assert:
that:
- server_firewall_backend == 'firewalld'
fail_msg: >-
server_firewall_backend must be firewalld for the Rocky server profile.
- name: Configure DuckDNS updater
tags: [dotfiles, dotfiles:server, duckdns]
ansible.builtin.import_tasks: duckdns.yml
- name: Ensure server directories exist
tags: [dotfiles, services]
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop: >-
{{
(server_directories | default([]))
+ (
server_syncthing_directories | default([])
if server_syncthing_enabled | default(true) | bool
else []
)
}}
loop_control:
label: "{{ item.path }}"
- name: Copy server dotfiles
tags: [dotfiles, dotfiles:server]
ansible.builtin.copy:
@@ -12,28 +44,6 @@
loop_control:
label: "{{ item.dest }}"
- name: Require server container secret variables
tags: [dotfiles, dotfiles:server, services]
ansible.builtin.assert:
that:
- (vault_navidrome_db_password | default('')) | length > 0
- (vault_postgres_root_password | default('')) | length > 0
fail_msg: >-
Server container secrets are missing. Define vault_navidrome_db_password and
vault_postgres_root_password in secrets/vault.yml or another vars source.
- name: Ensure server directories exist
tags: [dotfiles, services]
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop: "{{ server_directories | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Render server templates
tags: [dotfiles, dotfiles:server]
ansible.builtin.template:
@@ -47,6 +57,46 @@
label: "{{ item.dest }}"
no_log: "{{ item.no_log | default(false) }}"
- name: Manage Podman Compose stack
tags: [services, podman]
ansible.builtin.include_tasks: podman-compose.yml
- name: Ensure server SSH authorized key fragments directory exists
tags: [services, ssh]
ansible.builtin.file:
path: "{{ server_ssh_authorized_key_directory }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_ssh_authorized_keys | length > 0
- name: Manage server SSH authorized key fragments
tags: [services, ssh]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ server_ssh_authorized_key_directory }}/{{ item.name }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
loop: "{{ server_ssh_authorized_keys }}"
loop_control:
label: "{{ item.name }}"
when: server_ssh_authorized_keys | length > 0
- name: Configure server SSH authorized key fragments
tags: [services, ssh]
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*AuthorizedKeysFile\s+'
line: >-
AuthorizedKeysFile {{ server_ssh_authorized_keys | map(attribute='name')
| map('regex_replace', '^', '%h/.ssh/authorized_keys.d/') | join(' ') }}
state: present
validate: "sshd -t -f %s"
notify: Reload SSH service
when: server_ssh_authorized_keys | length > 0
- name: Disable SSH root login on server
tags: [services]
ansible.builtin.lineinfile:
@@ -68,33 +118,28 @@
notify: Reload SSH service
when: (server_sshd_allow_users | default([])) | length > 0
- name: Define effective server UFW rules
- name: Allow server services through firewalld
tags: [services, packages]
ansible.builtin.set_fact:
server_ufw_rules_effective: "{{ server_ufw_rules | default([]) }}"
- name: Apply server UFW rules
tags: [services, packages]
community.general.ufw:
rule: "{{ item.rule }}"
name: "{{ item.name | default(omit) }}"
port: "{{ item.port | default(omit) }}"
proto: "{{ item.proto | default(omit) }}"
from_ip: "{{ item.src | default(omit) }}"
to_ip: "{{ item.dest | default(omit) }}"
from_port: "{{ item.from_port | default(omit) }}"
direction: "{{ item.direction | default(omit) }}"
interface: "{{ item.interface | default(omit) }}"
interface_in: "{{ item.interface_in | default(omit) }}"
interface_out: "{{ item.interface_out | default(omit) }}"
route: "{{ item.route | default(omit) }}"
comment: "{{ item.comment | default(omit) }}"
loop: "{{ server_ufw_rules_effective }}"
loop_control:
label: "{{ item.name | default(item.port) }}"
- name: Enable UFW firewall on server
tags: [services, packages]
community.general.ufw:
ansible.posix.firewalld:
service: "{{ item }}"
zone: "{{ server_firewalld_zone }}"
state: enabled
when: (server_ufw_rules_effective | default([])) | length > 0
permanent: true
immediate: true
loop: "{{ server_firewalld_services | default([]) }}"
loop_control:
label: "{{ item }}"
when: server_firewall_backend == 'firewalld'
- name: Allow server ports through firewalld
tags: [services, packages]
ansible.posix.firewalld:
port: "{{ item }}"
zone: "{{ server_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
loop: "{{ server_firewalld_ports | default([]) }}"
loop_control:
label: "{{ item }}"
when: server_firewall_backend == 'firewalld'

View File

@@ -0,0 +1,10 @@
---
- name: Render Podman Compose systemd unit
tags: [services, podman]
ansible.builtin.template:
src: podman-compose-server.service.j2
dest: /etc/systemd/system/podman-compose-server.service
owner: root
group: root
mode: "0644"
notify: Reload systemd for Podman Compose

View File

@@ -0,0 +1,24 @@
#!/bin/sh
# Managed by Ansible. Contains a Vault token; never copy this file into Git.
set -eu
umask 077
log_file={{ (server_user_home ~ '/duckdns/duck.log') | quote }}
# Keep the token out of process arguments and verify the HTTPS certificate.
if ! response=$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 --config - <<'DUCKDNS_CONFIG'
url = "https://www.duckdns.org/update?domains={{ server_duckdns_domain }}&token={{ vault_duckdns_token }}&ip="
DUCKDNS_CONFIG
); then
printf 'ERROR\n' > "$log_file"
exit 1
fi
case "$response" in
OK) printf 'OK\n' > "$log_file" ;;
*)
printf 'KO\n' > "$log_file"
printf 'DuckDNS update failed.\n' >&2
exit 1
;;
esac

View File

@@ -0,0 +1,15 @@
[Unit]
Description=Podman Compose stack for {{ server_username }}
Requires=network-online.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory={{ server_container_stack_dir }}
ExecStart=/usr/bin/podman-compose up -d
ExecStop=/usr/bin/podman-compose down
ExecReload=/usr/bin/podman-compose up -d
[Install]
WantedBy=multi-user.target

View File

@@ -35,19 +35,31 @@
loop_control:
label: "{{ item.dest }}"
- name: Install workstation npm packages
tags: [packages, npm]
- name: Install shared AI coding agents on workstation
tags: [packages, npm, ai_agents]
community.general.npm:
name: "{{ item.name }}"
name: "{{ item.value.npm_package }}"
global: true
state: "{{ item.state | default('present') }}"
state: latest
become: true
loop: "{{ workstation_npm_packages | default([]) }}"
loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}"
when:
- workstation_manage_opencode | default(false)
- workstation_npm_packages | length > 0
- item.value.install_enabled | bool
loop_control:
label: "{{ item.name }}"
label: "{{ item.key }}"
- name: Uninstall shared AI coding agents on workstation
tags: [packages, npm, ai_agents]
community.general.npm:
name: "{{ item.value.npm_package }}"
global: true
state: absent
become: true
loop: "{{ ai_agents | dict2items | selectattr('value.npm_package', 'defined') | list }}"
when:
- item.value.uninstall_enabled | bool
loop_control:
label: "{{ item.key }}"
- name: Install IBM Bob coding agent
tags: [packages, ai_agents]
@@ -59,4 +71,15 @@
become: true
when:
- workstation_manage_ibm_bob | default(false)
- ai_agents.ibm_bob.install_enabled | bool
- workstation_ibm_bob_install_url | length > 0
- name: Uninstall IBM Bob coding agent binary
tags: [packages, ai_agents]
ansible.builtin.file:
path: /usr/local/bin/bob
state: absent
become: true
when:
- workstation_manage_ibm_bob | default(false)
- ai_agents.ibm_bob.uninstall_enabled | bool

View File

@@ -55,6 +55,39 @@
loop_control:
label: "{{ item.dest }}"
- name: Check whether the pinned Java version is installed with Mise
tags: [packages, mise, java, wsl]
ansible.builtin.command:
cmd: "mise where java@{{ workstation_mise_java_version }}"
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
register: workstation_mise_java_where
changed_when: false
failed_when: false
- name: Check whether the pinned Maven version is installed with Mise
tags: [packages, mise, maven, wsl]
ansible.builtin.command:
cmd: "mise where maven@{{ workstation_mise_maven_version }}"
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
register: workstation_mise_maven_where
changed_when: false
failed_when: false
- name: Install the pinned Mise tools
tags: [packages, mise, java, maven, wsl]
ansible.builtin.command:
cmd: mise install
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
when: >-
workstation_mise_java_where.rc != 0 or
workstation_mise_maven_where.rc != 0
- name: Ensure WSL boot configuration file exists
tags: [packages, services]
ansible.builtin.file:
@@ -76,15 +109,6 @@
register: workstation_wsl_systemd_config
when: workstation_wsl_systemd_enabled | default(false)
- name: Install WSL Python packages for Windows remoting
tags: [packages]
ansible.builtin.pip:
name: "{{ workstation_dev_wsl_python_packages | default([]) }}"
executable: pip3
extra_args: --break-system-packages
state: present
when: (workstation_dev_wsl_python_packages | default([])) | length > 0
- name: Note when WSL must be restarted
tags: [packages, services]
ansible.builtin.debug:

View File

@@ -1,262 +0,0 @@
---
- name: Copy workstation host Linux dotfiles
tags: [dotfiles, dotfiles:workstation, gnome]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/workstation_host_linux/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ workstation_host_linux_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Ensure GNOME extension directories exist
tags: [packages, gnome]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
loop:
- "{{ user_home }}/.cache/gnome-shell/extensions"
- "{{ user_home }}/.local/share/gnome-shell/extensions"
- name: Gather workstation user account data
tags: [packages, gnome]
ansible.builtin.getent:
database: passwd
key: "{{ username }}"
- name: Set workstation GNOME session environment
tags: [packages, gnome]
ansible.builtin.set_fact:
workstation_user_uid: "{{ ansible_facts.getent_passwd[username][1] }}"
workstation_gnome_extension_dir: "{{ user_home }}/.cache/gnome-shell/extensions"
workstation_gnome_environment:
HOME: "{{ user_home }}"
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts.getent_passwd[username][1] }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ ansible_facts.getent_passwd[username][1] }}/bus"
- name: Read current workstation GNOME managed settings
tags: [gnome]
ansible.builtin.command:
argv:
- gsettings
- get
- "{{ item.schema }}{{ ':' ~ item.path if item.path is defined else '' }}"
- "{{ item.key }}"
become_user: "{{ username }}"
environment: "{{ workstation_gnome_environment }}"
loop: "{{ workstation_gnome_managed_settings | default([]) }}"
loop_control:
label: >-
{{ item.schema }}{{ ':' ~ item.path if item.path is defined else '' }} {{ item.key }}
register: workstation_gnome_managed_settings_current
changed_when: false
- name: Apply workstation GNOME managed settings
tags: [gnome]
ansible.builtin.command:
argv:
- gsettings
- set
- "{{ item.item.schema }}{{ ':' ~ item.item.path if item.item.path is defined else '' }}"
- "{{ item.item.key }}"
- "{{ item.item.value }}"
become_user: "{{ username }}"
environment: "{{ workstation_gnome_environment }}"
loop: "{{ workstation_gnome_managed_settings_current.results | default([]) }}"
loop_control:
label: >-
{{ item.item.schema }}{{ ':' ~ item.item.path if item.item.path is defined else '' }} {{ item.item.key }}
changed_when: true
when: item.stdout | trim != item.item.value
- name: Apply workstation GNOME extension dconf settings
tags: [gnome]
ansible.builtin.command:
argv:
- dconf
- write
- "{{ item.path }}{{ item.key }}"
- "{{ item.value }}"
become_user: "{{ username }}"
loop: "{{ workstation_gnome_extension_dconf_settings | default([]) }}"
loop_control:
label: "{{ item.path }}{{ item.key }}"
changed_when: true
- name: Check whether VS Code CLI is available on workstation host
tags: [packages, vscode]
ansible.builtin.command:
argv:
- code
- --version
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
register: workstation_vscode_cli
changed_when: false
failed_when: false
when: (workstation_host_vscode_extensions | default([])) | length > 0
- name: Ensure VS Code CLI is available before managing extensions
tags: [packages, vscode]
ansible.builtin.fail:
msg: Ensure Visual Studio Code is installed before managing workstation VS Code extensions.
when:
- (workstation_host_vscode_extensions | default([])) | length > 0
- workstation_vscode_cli.rc != 0
- name: Read installed VS Code extensions on workstation host
tags: [packages, vscode]
ansible.builtin.command:
argv:
- code
- --list-extensions
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
register: workstation_vscode_extensions_current
changed_when: false
when:
- (workstation_host_vscode_extensions | default([])) | length > 0
- workstation_vscode_cli.rc == 0
- name: Install VS Code extensions on workstation host
tags: [packages, vscode]
ansible.builtin.command:
argv:
- code
- --install-extension
- "{{ item }}"
- --force
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
loop: "{{ workstation_host_vscode_extensions | default([]) }}"
loop_control:
label: "{{ item }}"
when:
- workstation_vscode_cli.rc == 0
- item not in (workstation_vscode_extensions_current.stdout_lines | default([]))
- name: Download workstation GNOME extension archives
tags: [packages, gnome]
ansible.builtin.get_url:
url: >-
https://extensions.gnome.org/download-extension/{{ item.uuid }}.shell-extension.zip?version_tag={{ item.version_tag }}
dest: "{{ workstation_gnome_extension_dir }}/{{ item.uuid }}.zip"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0644"
loop: "{{ workstation_gnome_extensions | default([]) }}"
loop_control:
label: "{{ item.uuid }}"
- name: Check installed workstation GNOME extensions
tags: [packages, gnome]
ansible.builtin.stat:
path: "{{ user_home }}/.local/share/gnome-shell/extensions/{{ item.uuid }}/metadata.json"
loop: "{{ workstation_gnome_extensions | default([]) }}"
loop_control:
label: "{{ item.uuid }}"
register: workstation_gnome_extension_install_state
- name: Install workstation GNOME extensions from downloaded archives
tags: [packages, gnome]
ansible.builtin.command:
cmd: "gnome-extensions install --force {{ workstation_gnome_extension_dir }}/{{ item.uuid }}.zip"
become_user: "{{ username }}"
environment: "{{ workstation_gnome_environment }}"
loop: "{{ workstation_gnome_extensions | default([]) }}"
loop_control:
label: "{{ item.uuid }}"
when: >-
not (
workstation_gnome_extension_install_state.results
| selectattr('item.uuid', 'equalto', item.uuid)
| map(attribute='stat.exists')
| first
| default(false)
)
- name: Read current workstation GNOME enabled extensions
tags: [gnome]
ansible.builtin.command:
cmd: gsettings get org.gnome.shell enabled-extensions
become_user: "{{ username }}"
environment: "{{ workstation_gnome_environment }}"
register: workstation_enabled_gnome_extensions_current
changed_when: false
- name: Compute desired workstation GNOME enabled extensions
tags: [gnome]
ansible.builtin.set_fact:
workstation_enabled_gnome_extensions_current_list: >-
{{
(
workstation_enabled_gnome_extensions_current.stdout
| default('')
| regex_search('\[.*\]')
| default('[]', true)
)
| from_yaml
}}
- name: Build desired workstation GNOME enabled extensions list
tags: [gnome]
ansible.builtin.set_fact:
workstation_enabled_gnome_extensions_desired: >-
{{
(
workstation_enabled_gnome_extensions_current_list
+ (
workstation_gnome_extensions
| default([])
| selectattr('enabled', 'defined')
| selectattr('enabled')
| map(attribute='uuid')
| list
)
)
| difference(workstation_disabled_gnome_extensions | default([]))
| unique
| sort
}}
- name: Build workstation GNOME extensions gsettings payload
tags: [gnome]
ansible.builtin.set_fact:
workstation_enabled_gnome_extensions_desired_gsettings: >-
[{% for extension_uuid in workstation_enabled_gnome_extensions_desired -%}
'{{ extension_uuid | replace("'", "\\'") }}'{% if not loop.last %}, {% endif %}
{%- endfor %}]
- name: Determine whether workstation GNOME enabled extensions must change
tags: [gnome]
ansible.builtin.set_fact:
workstation_gnome_extensions_state_changed: >-
{{ (workstation_enabled_gnome_extensions_current_list | sort) != workstation_enabled_gnome_extensions_desired }}
- name: Apply workstation GNOME enabled extensions list
tags: [gnome]
ansible.builtin.command:
argv:
- gsettings
- set
- org.gnome.shell
- enabled-extensions
- "{{ workstation_enabled_gnome_extensions_desired_gsettings }}"
become_user: "{{ username }}"
environment: "{{ workstation_gnome_environment }}"
changed_when: workstation_gnome_extensions_state_changed
when: workstation_gnome_extensions_state_changed
- name: Enable UFW firewall on workstation
tags: [services, packages]
community.general.ufw:
state: enabled
when: workstation_firewall_backend | default('ufw') == 'ufw'

View File

@@ -1,10 +0,0 @@
---
- name: Enable FreeBSD rc services
tags: [services, packages]
ansible.builtin.service:
name: "{{ item }}"
enabled: true
state: started
loop: "{{ host_enabled_services | default([]) }}"
loop_control:
label: "{{ item }}"

View File

@@ -0,0 +1,18 @@
---
wireguard_overlay_enabled: false
wireguard_interface: wg0
wireguard_config_dir: /etc/wireguard
wireguard_private_key_path: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.key"
wireguard_config_path: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.conf"
wireguard_address: CHANGEME_WIREGUARD_ADDRESS
wireguard_listen_port: 0
wireguard_mtu: 1420
wireguard_firewalld_zone: wireguard
wireguard_public_firewalld_zone: public
wireguard_enable_ipv4_forwarding: false
wireguard_reload_rootful_podman_networks: false
wireguard_handshake_retries: 12
wireguard_handshake_delay: 5
wireguard_peers: []
wireguard_packages:
- wireguard-tools

View File

@@ -0,0 +1,10 @@
---
- name: Restart WireGuard interface
tags: [wireguard, services]
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}.service"
state: restarted
daemon_reload: true
when:
- wireguard_overlay_enabled | bool
- not ansible_check_mode

View File

@@ -0,0 +1,192 @@
---
- name: Configure WireGuard overlay
tags: [wireguard, services]
when: wireguard_overlay_enabled | bool
block:
- name: Validate WireGuard host configuration
ansible.builtin.assert:
that:
- wireguard_address != 'CHANGEME_WIREGUARD_ADDRESS'
- wireguard_address is match('^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$')
- wireguard_peers | length > 0
- wireguard_peers | map(attribute='host') | difference(ansible_play_hosts_all) | length == 0
fail_msg: >-
Configure this host's WireGuard address and peers, and run the first
key bootstrap against every peer in the same play.
- name: Install WireGuard userspace tools
ansible.builtin.dnf:
name: "{{ wireguard_packages }}"
state: present
- name: Create private WireGuard configuration directory
ansible.builtin.file:
path: "{{ wireguard_config_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Check for an existing WireGuard private key
ansible.builtin.stat:
path: "{{ wireguard_private_key_path }}"
register: wireguard_private_key_stat
- name: Generate a missing WireGuard private key
ansible.builtin.command:
argv:
- wg
- genkey
register: wireguard_generated_private_key
changed_when: true
no_log: true
when:
- not wireguard_private_key_stat.stat.exists
- not ansible_check_mode
- name: Persist the generated WireGuard private key
ansible.builtin.copy:
content: "{{ wireguard_generated_private_key.stdout }}\n"
dest: "{{ wireguard_private_key_path }}"
owner: root
group: root
mode: "0600"
no_log: true
when:
- not wireguard_private_key_stat.stat.exists
- not ansible_check_mode
- name: Require a private key during check mode
ansible.builtin.assert:
that:
- wireguard_private_key_stat.stat.exists
fail_msg: >-
The initial WireGuard key generation cannot be simulated safely in
check mode. Run the gated WireGuard play once without --check.
when: ansible_check_mode
- name: Read the persisted WireGuard private key
ansible.builtin.slurp:
src: "{{ wireguard_private_key_path }}"
register: wireguard_private_key_material
no_log: true
- name: Derive this host's WireGuard public key
ansible.builtin.command:
argv:
- wg
- pubkey
stdin: "{{ wireguard_private_key_material.content | b64decode | trim }}"
register: wireguard_derived_public_key
changed_when: false
no_log: true
- name: Publish this host's WireGuard public key
ansible.builtin.set_fact:
wireguard_public_key: "{{ wireguard_derived_public_key.stdout }}"
- name: Require every peer's generated public key
ansible.builtin.assert:
that:
- hostvars[item.host].wireguard_public_key is defined
- hostvars[item.host].wireguard_public_key | length > 0
fail_msg: >-
The public key for {{ item.host }} is unavailable. The first
WireGuard run must include every overlay host.
loop: "{{ wireguard_peers }}"
loop_control:
label: "{{ item.name }}"
- name: Render the private WireGuard interface configuration
ansible.builtin.template:
src: wg.conf.j2
dest: "{{ wireguard_config_path }}"
owner: root
group: root
mode: "0600"
diff: false
no_log: true
notify: Restart WireGuard interface
- name: Enable IPv4 forwarding for the ingress host
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: "1"
state: present
sysctl_set: true
reload: true
when: wireguard_enable_ipv4_forwarding | bool
- name: Create the WireGuard firewalld zone
ansible.posix.firewalld:
zone: "{{ wireguard_firewalld_zone }}"
state: present
permanent: true
register: wireguard_firewalld_zone_result
- name: Reload firewalld after creating the WireGuard zone
ansible.builtin.systemd:
name: firewalld.service
state: reloaded
when:
- wireguard_firewalld_zone_result is changed
- not ansible_check_mode
- name: Restore rootful Podman networking after firewalld reload
ansible.builtin.command:
argv:
- podman
- network
- reload
- --all
register: wireguard_podman_network_reload
changed_when: wireguard_podman_network_reload.stdout_lines | length > 0
when:
- wireguard_firewalld_zone_result is changed
- wireguard_reload_rootful_podman_networks | bool
- not ansible_check_mode
- name: Assign the WireGuard interface to its firewalld zone
ansible.posix.firewalld:
interface: "{{ wireguard_interface }}"
zone: "{{ wireguard_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
- name: Permit this host's public WireGuard listener
ansible.posix.firewalld:
port: "{{ wireguard_listen_port }}/udp"
zone: "{{ wireguard_public_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
when: wireguard_listen_port | int > 0
- name: Enable the WireGuard interface
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}.service"
enabled: true
state: started
daemon_reload: true
when: not ansible_check_mode
- name: Apply pending WireGuard handlers before verification
ansible.builtin.meta: flush_handlers
when: not ansible_check_mode
- name: Wait for every WireGuard peer handshake
ansible.builtin.command:
argv:
- wg
- show
- "{{ wireguard_interface }}"
- latest-handshakes
register: wireguard_latest_handshakes
changed_when: false
retries: "{{ wireguard_handshake_retries }}"
delay: "{{ wireguard_handshake_delay }}"
until:
- wireguard_latest_handshakes.stdout_lines | length == wireguard_peers | length
- wireguard_latest_handshakes.stdout_lines | select('search', '\t0$') | list | length == 0
when: not ansible_check_mode

View File

@@ -0,0 +1,25 @@
# Managed by Ansible. Do not edit manually.
[Interface]
Address = {{ wireguard_address }}
PrivateKey = {{ wireguard_private_key_material.content | b64decode | trim }}
MTU = {{ wireguard_mtu }}
SaveConfig = false
{% if wireguard_listen_port | int > 0 %}
ListenPort = {{ wireguard_listen_port }}
{% endif %}
{% for peer in wireguard_peers %}
[Peer]
# {{ peer.name }}
PublicKey = {{ hostvars[peer.host].wireguard_public_key }}
AllowedIPs = {{ peer.allowed_ips | join(', ') }}
{% if peer.endpoint is defined %}
Endpoint = {{ peer.endpoint }}
{% endif %}
{% if peer.persistent_keepalive | default(0) | int > 0 %}
PersistentKeepalive = {{ peer.persistent_keepalive }}
{% endif %}
{% if not loop.last %}
{% endif %}
{% endfor %}

View File

@@ -7,6 +7,7 @@
tags: [always]
ansible.builtin.include_vars:
file: "{{ playbook_dir }}/../secrets/vault.yml"
no_log: true
when: >-
lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.yml',
errors='ignore', wantlist=True) | length > 0
@@ -15,12 +16,28 @@
tags: [always]
ansible.builtin.include_vars:
file: "{{ playbook_dir }}/../secrets/vault.local.yml"
no_log: true
when: >-
lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.local.yml',
errors='ignore', wantlist=True) | length > 0
- name: Reject conflicting AI coding-agent lifecycle flags
tags: [always, ai_agents]
ansible.builtin.assert:
that:
- not (item.value.install_enabled | bool and item.value.uninstall_enabled | bool)
fail_msg: >-
{{ item.key }} has both install_enabled and uninstall_enabled set to true.
Choose either installation or removal before running the playbook.
loop: "{{ ai_agents | dict2items }}"
loop_control:
label: "{{ item.key }}"
roles:
- dotfiles_common
- role: dotfiles_common
when:
- "'platform_rocky' not in group_names"
- "'platform_fedora_iot' not in group_names"
- name: Configure Void platform
hosts: platform_void
@@ -40,14 +57,6 @@
- profile_desktop_niri
- profile_desktop_host
- name: Configure FreeBSD platform
hosts: platform_freebsd
become: true
roles:
- packages_freebsd
- services_freebsd
- name: Configure Fedora platform
hosts: platform_fedora
become: true
@@ -56,6 +65,50 @@
- packages_fedora
- services_systemd
- name: Configure Rocky Linux platform
hosts: platform_rocky
become: true
roles:
- packages_rocky
- services_systemd
- name: Configure WireGuard overlay
hosts: wireguard_overlay
become: true
roles:
- wireguard_overlay
- name: Configure Aegis Fedora IoT profile
hosts: role_aegis
become: true
roles:
- profile_aegis
- name: Configure Atlas NAS profile
hosts: atlas
become: true
roles:
- profile_atlas
- name: Configure Atlas phase-one backend services
hosts: role_backend_phase1
become: true
roles:
- profile_backend_phase1
- name: Configure Rocky Linux server
hosts: rocky_server
become: true
roles:
- dotfiles_common
- profile_server
- name: Configure personal workstation role on Fedora
hosts: platform_fedora:&role_personal_workstation
become: true
@@ -78,34 +131,8 @@
- profile_workstation_dev_common
- name: Configure Fedora WSL workstation development layer
hosts: workstation_dev_wsl_fedora
hosts: workstation_dev_wsl
become: true
roles:
- profile_workstation_dev_wsl
- name: Configure native Linux workstation host layer
hosts: workstation_host_linux
become: true
roles:
- profile_workstation_gnome
- name: Configure WSL workstation development layer
hosts: workstation_dev_wsl_ubuntu
become: true
roles:
- packages_ubuntu
- services_systemd
- profile_workstation_dev_common
- profile_workstation_dev_wsl
- name: Configure Ubuntu server
hosts: ubuntu_server
become: true
roles:
- packages_ubuntu
- services_systemd
- profile_server

View File

@@ -1,40 +1,23 @@
---
version: "3.8"
{% set selinux_volume_option = server_compose_selinux_mount_option | default('') %}
name: server
services:
navidrome:
image: 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"
- "/opt/music:/music:ro"
user: "1000:1000"
networks:
- web
depends_on:
- navidromedb
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
image: docker.io/jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
- "127.0.0.1:81:81"
extra_hosts:
- "host.containers.internal:host-gateway"
volumes:
- "/opt/npm/data:/data"
- "/opt/npm/letsencrypt:/etc/letsencrypt"
- "/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 '' }}"
networks:
- web
- gitea
# Disabled: prometheus does not have enough resources to run Nextcloud AIO.
# nextcloud-aio-mastercontainer:
@@ -55,20 +38,6 @@ services:
# networks:
# - web
navidromedb:
image: postgres:13
container_name: navidromedb
restart: unless-stopped
mem_limit: 2048m
environment:
POSTGRES_DB: "navidrome_db"
POSTGRES_USER: "navidrome"
POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}"
volumes:
- "/opt/postgres/data:/var/lib/postgresql/data"
networks:
- web
gitea:
image: docker.gitea.com/gitea:1.25.2
container_name: gitea
@@ -77,39 +46,39 @@ services:
- USER_GID=1100
restart: always
networks:
- gitea
- web
volumes:
- /opt/gitea/data:/data
- /opt/gitea/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- /home/git/.ssh:/data/git/.ssh
- /home/git/.ssh:/data/git/.ssh{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}
ports:
- "3000:3000"
- "127.0.0.1:222:22"
{% if server_syncthing_enabled | default(true) | bool %}
syncthing:
image: syncthing/syncthing:2
image: docker.io/syncthing/syncthing:2
container_name: syncthing
hostname: syncthing
restart: unless-stopped
expose:
- "8384"
volumes:
- "/opt/syncthing/config:/var/syncthing"
- "/srv/syncthing/data:/data"
- "/opt/syncthing/config:/var/syncthing{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/srv/syncthing/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
ports:
- "22000:22000/tcp"
- "22000:22000/udp"
- "21027:21027/udp"
networks:
- web
{% endif %}
networks:
web:
name: server_web
external: false
gitea:
external: false
# volumes:
# nextcloud_aio_mastercontainer:

View File

@@ -24,19 +24,19 @@
[merge]
conflictstyle = diff3
tool = vscode
tool = bobide
[mergetool]
prompt = false
[mergetool "vscode"]
cmd = code --reuse-window --wait $MERGED
[mergetool "bobide"]
cmd = bobide --reuse-window --wait $MERGED
[diff]
tool = vscode
tool = bobide
[difftool "vscode"]
cmd = code --reuse-window --wait --diff $LOCAL $REMOTE
[difftool "bobide"]
cmd = bobide --reuse-window --wait --diff $LOCAL $REMOTE
[color]
ui = always
@@ -89,11 +89,11 @@
side-by-side = true
[http "https://gitlab.ecaas.datacenter.comune.roma"]
sslBackend = gnutls
sslBackend = openssl
sslCAInfo = ~/.local/share/certs/ecaas.datacenter.comune.roma.crt
[http "https://gitlab.datacenter.comune.roma"]
sslBackend = gnutls
sslBackend = openssl
sslCAInfo = ~/.local/share/certs/datacenter.comune.roma.crt
[credential]

View File

@@ -1,20 +1,10 @@
# =========================
# Portable Bash config
# Target: Void Linux + FreeBSD
# =========================
# Exit if not interactive
[[ $- != *i* ]] && return
# --- environment detection
case "$(uname -s)" in
Linux) PLATFORM="linux" ;;
FreeBSD) PLATFORM="freebsd" ;;
*) PLATFORM="other" ;;
esac
export PLATFORM
# --- history
HISTSIZE=10000
HISTFILESIZE=20000
@@ -438,15 +428,10 @@ extract() {
}
# =========================
# OS-specific small touches
# Small touches
# =========================
if [ "$PLATFORM" = "freebsd" ]; then
alias df='df -h'
alias du='du -h'
elif [ "$PLATFORM" = "linux" ]; then
alias df='df -h'
alias du='du -h'
fi
alias df='df -h'
alias du='du -h'
[ -r "$HOME/.bashrc.aliases" ] && . "$HOME/.bashrc.aliases"

View File

@@ -1,2 +1,5 @@
# Additional Bash aliases.
# Keep this file as the extension point for shared aliases.
# Nginx Proxy Manager administration tunnel on Prometheus.
alias npm-tunnel='ssh -N -o ExitOnForwardFailure=yes -L 8181:127.0.0.1:81 rocky@179.237.102.172'

View File

@@ -1,5 +1,5 @@
# Server
- Ubuntu minimal server
- Rocky Linux 9 server
- Self-hosting services
- Low attack surface

View File

@@ -61,49 +61,49 @@ video/x-msvideo=mpv.desktop
video/x-ogm+ogg=mpv.desktop
video/x-theora=mpv.desktop
video/x-theora+ogg=mpv.desktop
application/msword=libreoffice-writer.desktop
application/rtf=libreoffice-writer.desktop
application/vnd.ms-word=libreoffice-writer.desktop
application/vnd.ms-word.document.macroEnabled.12=libreoffice-writer.desktop
application/vnd.ms-word.template.macroEnabled.12=libreoffice-writer.desktop
application/vnd.oasis.opendocument.text=libreoffice-writer.desktop
application/vnd.oasis.opendocument.text-template=libreoffice-writer.desktop
application/vnd.openxmlformats-officedocument.wordprocessingml.document=libreoffice-writer.desktop
application/vnd.openxmlformats-officedocument.wordprocessingml.template=libreoffice-writer.desktop
application/x-doc=libreoffice-writer.desktop
text/rtf=libreoffice-writer.desktop
application/csv=libreoffice-calc.desktop
application/excel=libreoffice-calc.desktop
application/msexcel=libreoffice-calc.desktop
application/tab-separated-values=libreoffice-calc.desktop
application/vnd.ms-excel=libreoffice-calc.desktop
application/vnd.ms-excel.sheet.binary.macroEnabled.12=libreoffice-calc.desktop
application/vnd.ms-excel.sheet.macroEnabled.12=libreoffice-calc.desktop
application/vnd.ms-excel.template.macroEnabled.12=libreoffice-calc.desktop
application/vnd.oasis.opendocument.spreadsheet=libreoffice-calc.desktop
application/vnd.oasis.opendocument.spreadsheet-template=libreoffice-calc.desktop
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=libreoffice-calc.desktop
application/vnd.openxmlformats-officedocument.spreadsheetml.template=libreoffice-calc.desktop
application/x-dos_ms_excel=libreoffice-calc.desktop
application/x-excel=libreoffice-calc.desktop
application/x-ms-excel=libreoffice-calc.desktop
application/x-msexcel=libreoffice-calc.desktop
text/comma-separated-values=libreoffice-calc.desktop
text/csv=libreoffice-calc.desktop
text/tab-separated-values=libreoffice-calc.desktop
text/x-comma-separated-values=libreoffice-calc.desktop
text/x-csv=libreoffice-calc.desktop
application/mspowerpoint=libreoffice-impress.desktop
application/vnd.ms-powerpoint=libreoffice-impress.desktop
application/vnd.ms-powerpoint.presentation.macroEnabled.12=libreoffice-impress.desktop
application/vnd.ms-powerpoint.slideshow.macroEnabled.12=libreoffice-impress.desktop
application/vnd.ms-powerpoint.template.macroEnabled.12=libreoffice-impress.desktop
application/vnd.oasis.opendocument.presentation=libreoffice-impress.desktop
application/vnd.oasis.opendocument.presentation-template=libreoffice-impress.desktop
application/vnd.openxmlformats-officedocument.presentationml.presentation=libreoffice-impress.desktop
application/vnd.openxmlformats-officedocument.presentationml.slide=libreoffice-impress.desktop
application/vnd.openxmlformats-officedocument.presentationml.slideshow=libreoffice-impress.desktop
application/vnd.openxmlformats-officedocument.presentationml.template=libreoffice-impress.desktop
application/msword=org.onlyoffice.desktopeditors.desktop
application/rtf=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-word=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-word.document.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-word.template.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.oasis.opendocument.text=org.onlyoffice.desktopeditors.desktop
application/vnd.oasis.opendocument.text-template=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.wordprocessingml.document=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.wordprocessingml.template=org.onlyoffice.desktopeditors.desktop
application/x-doc=org.onlyoffice.desktopeditors.desktop
text/rtf=org.onlyoffice.desktopeditors.desktop
application/csv=org.onlyoffice.desktopeditors.desktop
application/excel=org.onlyoffice.desktopeditors.desktop
application/msexcel=org.onlyoffice.desktopeditors.desktop
application/tab-separated-values=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-excel=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-excel.sheet.binary.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-excel.sheet.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-excel.template.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.oasis.opendocument.spreadsheet=org.onlyoffice.desktopeditors.desktop
application/vnd.oasis.opendocument.spreadsheet-template=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.spreadsheetml.template=org.onlyoffice.desktopeditors.desktop
application/x-dos_ms_excel=org.onlyoffice.desktopeditors.desktop
application/x-excel=org.onlyoffice.desktopeditors.desktop
application/x-ms-excel=org.onlyoffice.desktopeditors.desktop
application/x-msexcel=org.onlyoffice.desktopeditors.desktop
text/comma-separated-values=org.onlyoffice.desktopeditors.desktop
text/csv=org.onlyoffice.desktopeditors.desktop
text/tab-separated-values=org.onlyoffice.desktopeditors.desktop
text/x-comma-separated-values=org.onlyoffice.desktopeditors.desktop
text/x-csv=org.onlyoffice.desktopeditors.desktop
application/mspowerpoint=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-powerpoint=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-powerpoint.presentation.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-powerpoint.slideshow.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.ms-powerpoint.template.macroEnabled.12=org.onlyoffice.desktopeditors.desktop
application/vnd.oasis.opendocument.presentation=org.onlyoffice.desktopeditors.desktop
application/vnd.oasis.opendocument.presentation-template=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.presentationml.presentation=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.presentationml.slide=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.presentationml.slideshow=org.onlyoffice.desktopeditors.desktop
application/vnd.openxmlformats-officedocument.presentationml.template=org.onlyoffice.desktopeditors.desktop
application/gzip=xarchiver.desktop
application/vnd.debian.binary-package=xarchiver.desktop
application/vnd.ms-cab-compressed=xarchiver.desktop
@@ -213,49 +213,49 @@ video/x-msvideo=mpv.desktop;
video/x-ogm+ogg=mpv.desktop;
video/x-theora=mpv.desktop;
video/x-theora+ogg=mpv.desktop;
application/msword=libreoffice-writer.desktop;
application/rtf=libreoffice-writer.desktop;
application/vnd.ms-word=libreoffice-writer.desktop;
application/vnd.ms-word.document.macroEnabled.12=libreoffice-writer.desktop;
application/vnd.ms-word.template.macroEnabled.12=libreoffice-writer.desktop;
application/vnd.oasis.opendocument.text=libreoffice-writer.desktop;
application/vnd.oasis.opendocument.text-template=libreoffice-writer.desktop;
application/vnd.openxmlformats-officedocument.wordprocessingml.document=libreoffice-writer.desktop;
application/vnd.openxmlformats-officedocument.wordprocessingml.template=libreoffice-writer.desktop;
application/x-doc=libreoffice-writer.desktop;
text/rtf=libreoffice-writer.desktop;
application/csv=libreoffice-calc.desktop;
application/excel=libreoffice-calc.desktop;
application/msexcel=libreoffice-calc.desktop;
application/tab-separated-values=libreoffice-calc.desktop;
application/vnd.ms-excel=libreoffice-calc.desktop;
application/vnd.ms-excel.sheet.binary.macroEnabled.12=libreoffice-calc.desktop;
application/vnd.ms-excel.sheet.macroEnabled.12=libreoffice-calc.desktop;
application/vnd.ms-excel.template.macroEnabled.12=libreoffice-calc.desktop;
application/vnd.oasis.opendocument.spreadsheet=libreoffice-calc.desktop;
application/vnd.oasis.opendocument.spreadsheet-template=libreoffice-calc.desktop;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=libreoffice-calc.desktop;
application/vnd.openxmlformats-officedocument.spreadsheetml.template=libreoffice-calc.desktop;
application/x-dos_ms_excel=libreoffice-calc.desktop;
application/x-excel=libreoffice-calc.desktop;
application/x-ms-excel=libreoffice-calc.desktop;
application/x-msexcel=libreoffice-calc.desktop;
text/comma-separated-values=libreoffice-calc.desktop;
text/csv=libreoffice-calc.desktop;
text/tab-separated-values=libreoffice-calc.desktop;
text/x-comma-separated-values=libreoffice-calc.desktop;
text/x-csv=libreoffice-calc.desktop;
application/mspowerpoint=libreoffice-impress.desktop;
application/vnd.ms-powerpoint=libreoffice-impress.desktop;
application/vnd.ms-powerpoint.presentation.macroEnabled.12=libreoffice-impress.desktop;
application/vnd.ms-powerpoint.slideshow.macroEnabled.12=libreoffice-impress.desktop;
application/vnd.ms-powerpoint.template.macroEnabled.12=libreoffice-impress.desktop;
application/vnd.oasis.opendocument.presentation=libreoffice-impress.desktop;
application/vnd.oasis.opendocument.presentation-template=libreoffice-impress.desktop;
application/vnd.openxmlformats-officedocument.presentationml.presentation=libreoffice-impress.desktop;
application/vnd.openxmlformats-officedocument.presentationml.slide=libreoffice-impress.desktop;
application/vnd.openxmlformats-officedocument.presentationml.slideshow=libreoffice-impress.desktop;
application/vnd.openxmlformats-officedocument.presentationml.template=libreoffice-impress.desktop;
application/msword=org.onlyoffice.desktopeditors.desktop;
application/rtf=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-word=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-word.document.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-word.template.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.oasis.opendocument.text=org.onlyoffice.desktopeditors.desktop;
application/vnd.oasis.opendocument.text-template=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.wordprocessingml.document=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.wordprocessingml.template=org.onlyoffice.desktopeditors.desktop;
application/x-doc=org.onlyoffice.desktopeditors.desktop;
text/rtf=org.onlyoffice.desktopeditors.desktop;
application/csv=org.onlyoffice.desktopeditors.desktop;
application/excel=org.onlyoffice.desktopeditors.desktop;
application/msexcel=org.onlyoffice.desktopeditors.desktop;
application/tab-separated-values=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-excel=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-excel.sheet.binary.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-excel.sheet.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-excel.template.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.oasis.opendocument.spreadsheet=org.onlyoffice.desktopeditors.desktop;
application/vnd.oasis.opendocument.spreadsheet-template=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.spreadsheetml.template=org.onlyoffice.desktopeditors.desktop;
application/x-dos_ms_excel=org.onlyoffice.desktopeditors.desktop;
application/x-excel=org.onlyoffice.desktopeditors.desktop;
application/x-ms-excel=org.onlyoffice.desktopeditors.desktop;
application/x-msexcel=org.onlyoffice.desktopeditors.desktop;
text/comma-separated-values=org.onlyoffice.desktopeditors.desktop;
text/csv=org.onlyoffice.desktopeditors.desktop;
text/tab-separated-values=org.onlyoffice.desktopeditors.desktop;
text/x-comma-separated-values=org.onlyoffice.desktopeditors.desktop;
text/x-csv=org.onlyoffice.desktopeditors.desktop;
application/mspowerpoint=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-powerpoint=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-powerpoint.presentation.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-powerpoint.slideshow.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.ms-powerpoint.template.macroEnabled.12=org.onlyoffice.desktopeditors.desktop;
application/vnd.oasis.opendocument.presentation=org.onlyoffice.desktopeditors.desktop;
application/vnd.oasis.opendocument.presentation-template=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.presentationml.presentation=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.presentationml.slide=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.presentationml.slideshow=org.onlyoffice.desktopeditors.desktop;
application/vnd.openxmlformats-officedocument.presentationml.template=org.onlyoffice.desktopeditors.desktop;
application/gzip=xarchiver.desktop;
application/vnd.debian.binary-package=xarchiver.desktop;
application/vnd.ms-cab-compressed=xarchiver.desktop;

View File

@@ -1,4 +1,4 @@
Host vps
Host prometheus-ubuntu
IdentityFile ~/.ssh/id_rsa_vps
Host *

Some files were not shown because too many files have changed in this diff Show More