Compare commits

...

70 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
Fabio Scotto di Santolo
3f0654a4bd Merge branch 'feature/fedora-wsl' 2026-08-27 22:41:10 +02:00
Fabio Scotto di Santolo
8771fe79c8 Install LazyGit from official release on Fedora WSL 2026-08-27 18:57:11 +02:00
Fabio Scotto di Santolo
3badf14c60 Avoid package facts on Fedora WSL 2026-08-27 18:27:51 +02:00
Fabio Scotto di Santolo
31bc8d07de Add profile for Fedora WSL 2026-08-27 18:11:41 +02:00
Fabio Scotto di Santolo
304479a3f9 Make Emacs default font platform-aware (Linux/WSL) and drop Claude CLI agent files 2026-08-27 17:49:07 +02:00
Fabio Scotto di Santolo
6ba0040192 Merge pull request #1 from fscotto/copilot/infra 2026-08-22 13:42:21 +02:00
copilot-swe-agent[bot]
49105a6f91 Use official GitHub Copilot CLI npm package
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-08-22 10:49:51 +00:00
copilot-swe-agent[bot]
d59913e837 Add GitHub Copilot CLI npm package
Co-authored-by: fscotto <17803710+fscotto@users.noreply.github.com>
2026-08-22 10:49:20 +00:00
Fabio Scotto di Santolo
51ba397c6a Deploy WSL Emacs config to Windows home 2026-08-20 15:46:28 +02:00
Fabio Scotto di Santolo
474f0082bb Refactor Emacs for authoring workflows 2026-08-20 15:35:38 +02:00
Fabio Scotto di Santolo
6589b25f95 Set GPT-5.6 Terra as Codex default 2026-08-19 16:20:30 +02:00
Fabio Scotto di Santolo
f2bc1cf768 Keep Maximize New Windows enabled 2026-08-19 14:48:01 +02:00
Fabio Scotto di Santolo
e5a330dab0 Remove Hyprland and FreeBSD desktop profiles 2026-08-19 14:21:20 +02:00
Fabio Scotto di Santolo
6e27569dc3 Remove obsolete desktop profiles 2026-08-19 13:52:15 +02:00
Fabio Scotto di Santolo
66575e6d80 Install croc separately on WSL 2026-08-12 10:10:06 +02:00
Fabio Scotto di Santolo
9315f0e8bc Configure Fedora GTK dark preference 2026-08-11 23:16:54 +02:00
Fabio Scotto di Santolo
cfedfbc676 Enable Fedora Wayland Emacs 2026-08-11 20:19:59 +02:00
Fabio Scotto di Santolo
13991fb2b4 Install croc on desktops and workstations 2026-08-11 20:08:01 +02:00
Fabio Scotto di Santolo
e8e1cd3cbc Set Vim default theme to zaibatsu 2026-08-11 20:00:12 +02:00
Fabio Scotto di Santolo
886098dc0d Add sanitizer development tools 2026-08-01 21:41:32 +02:00
Fabio Scotto di Santolo
38bca45f38 Manage tmux plugins for desktop profiles 2026-07-31 14:31:40 +02:00
Fabio Scotto di Santolo
5f39674092 Share desktop tmux config with WSL 2026-07-28 15:07:37 +02:00
Fabio Scotto di Santolo
23a062a2e4 Update tmux clipboard and terminal styling 2026-07-27 23:08:57 +02:00
Fabio Scotto di Santolo
fd9968f205 Add JetBrainsMono and Iosevka Nerd Fonts 2026-07-27 19:47:19 +02:00
Fabio Scotto di Santolo
a04b871068 Manage ikaros GNOME gsettings 2026-07-27 19:46:41 +02:00
326 changed files with 6175 additions and 7429 deletions

View File

@@ -1,19 +0,0 @@
{
"permissions": {
"allow": [
"Bash(xargs ls -la)",
"Bash(command -v pacman)",
"Bash(command -v paclist)",
"Bash(command -v pacinfo)",
"Bash(git -C /home/fscotto/AnsiblePlaybook add ansible/inventory/group_vars/arch.yml ansible/roles/services_systemd/tasks/main.yml)",
"Bash(git -C /home/fscotto/AnsiblePlaybook commit -m ' *)",
"Bash(git -C /home/fscotto/AnsiblePlaybook diff --stat)",
"Bash(git -C /home/fscotto/AnsiblePlaybook status --short)",
"Bash(git -C /home/fscotto/AnsiblePlaybook log --oneline --all -- ansible/inventory/host_vars/nymph.yml ansible/roles/profile_desktop_host/tasks/nymph.yml 'dotfiles/nymph/*')",
"Bash(git *)",
"Bash(wlogout --help)",
"Bash(xbps-query *)",
"Bash(xbps-install --dry-run gtklock)"
]
}
}

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/

174
AGENTS.md
View File

@@ -1,6 +1,6 @@
# AGENTS.md
Ansible-driven personal infrastructure repo for Fedora and Void desktops, Linux Mint and 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, Linux
- 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 target is `deadalus-wsl`
- 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, Linux
- 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,12 +43,25 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Linux
- 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`
- WSL dev: `ansible-playbook ansible/site.yml --limit deadalus-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.
@@ -54,41 +72,137 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Linux
- Use `no_log: true` for secret-bearing task inputs or outputs.
## Desktop Notes
- `desktop_profile` names independently selectable desktop groups such as `desktop_gnome`, `desktop_hyprland`, `desktop_sway`, `desktop_niri`, and `desktop_cinnamon`. Keep platform-specific session bootstrap in platform-specific roles.
- `desktop_environment` selects the mutually exclusive `minimal` (default), `gnome`, `xfce`, or `kde` desktop mode. `profile_desktop_common` owns shared Void bootstrap; the minimal mode uses `profile_desktop_sway`, `profile_desktop_hyprland`, and `profile_desktop_niri`, `profile_desktop_xfce` owns the reproducible XFCE setup, `profile_desktop_kde` owns KDE-specific defaults and cleanup, and `profile_desktop_gnome` copies shared desktop dotfiles for Fedora/GNOME without managing GNOME settings. `desktop_sessions_enabled` and `desktop_default_session` apply only to the minimal mode.
- Emacs packages and `.emacs.d` deploy are disabled by default via `emacs_enabled: false`; keep the dotfiles in the repo for reversible opt-in only.
- `desktop_profile` names independently selectable desktop groups such as `desktop_gnome`, `desktop_sway`, and `desktop_niri`. Keep platform-specific session bootstrap in platform-specific roles.
- `desktop_environment` is fixed to `minimal` for Void desktops. `profile_desktop_common` owns shared Void bootstrap; `profile_desktop_sway` and `profile_desktop_niri` manage the enabled sessions, while `profile_desktop_gnome` copies shared desktop dotfiles for Fedora/GNOME without managing GNOME settings. `desktop_sessions_enabled` and `desktop_default_session` apply to the minimal mode.
- Emacs has one authoring-oriented `.emacs.d`, deployed by `dotfiles_common` when `emacs_enabled` is true. Fedora/GNOME desktops and workstation profiles enable it; keep platform dependencies in package group vars rather than branching in Emacs Lisp.
- NTFS filesystem support is provided by `ntfs-3g` in `ansible/inventory/group_vars/void.yml`.
- Void user services are managed by `turnstile` and live under `dotfiles/desktop/.config/service/`.
- `ssh-agent` keeps the stable socket `~/.local/state/ssh-agent/socket`.
- Critical session entrypoints:
- `dotfiles/desktop/.config/sway/config` plus `host.conf` and `session-env` deployed via `host_sway_dotfiles` (sway / Wayland)
- `dotfiles/desktop/.config/hypr/hyprland.conf` plus `host.conf` and `session-env` deployed via `host_hyprland_dotfiles` (Hyprland / Wayland)
- `dotfiles/desktop/.config/niri/config.kdl` and `session-env` deployed via `desktop_niri_dotfiles` (Niri / Wayland)
- Void Niri lives in `profile_desktop_niri`, gated on `'niri' in desktop_sessions_enabled`; it installs the `emptty` `niri.desktop` session, the `/usr/local/bin/start-niri` launcher, and the xdg-desktop-portal config, mirroring `profile_desktop_sway`.
- Fedora GNOME (`desktop_gnome`) assumes GNOME comes from the Fedora Workstation base install; Ansible deploys shared desktop dotfiles and git/GPG config for `ikaros` and `nymph`, not GNOME settings.
- FreeBSD Niri (dormant; `platform_freebsd` currently has no hosts) must keep FreeBSD-specific package, rc, DBus, seat, portal, and launcher behavior in `profile_desktop_niri_freebsd` or FreeBSD platform vars.
- Do not switch or restart a display manager during a playbook run from an active graphical session. Changing among `emptty`, LightDM, and SDDM requires an explicit run from another TTY/SSH session with `desktop_allow_display_manager_switch=true`. Apply managed XFCE XML while logged out of XFCE so `xfconfd` cannot overwrite it from its in-memory state.
- Do not switch or restart the display manager during a playbook run from an active graphical session.
- `nymph` is the Fedora/GNOME laptop target; keep GNOME settings unmanaged for now and add host-specific tuning only after real use.
## Void Package And Dotfile Bucket Rules
`platform_void` is the reusable Void platform selection. The legacy `void` group remains a compatibility parent so existing `group_vars/void.yml` and `when: "'void' in group_names"` checks keep working during the transition.
The Void desktop package lists in `ansible/inventory/group_vars/void.yml` are kept disjoint by role:
- `void_packages_base` — system runtime only (init/services, kernel, audio core, networking, filesystem, firewall, hardware daemons, runit logging).
- `desktop_common_packages` — GUI infrastructure shared by all desktop modes.
- `desktop_minimal_packages` / `desktop_xfce_packages` / `desktop_kde_packages` — mutually exclusive applications, integration components, and display managers. Packages required by more than one exclusive mode may be repeated; cleanup subtracts the selected mode before removing inactive packages.
- `desktop_sway_packages` / `desktop_hyprland_packages` — binaries specific to each session. Cross-WM packages used by both (such as `dunst`, `rofi`, and `foot`) are intentionally duplicated in the two lists.
`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.
The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-independent content, `desktop_minimal_dotfiles` carries Thunar, Udiskie, and minimal MIME defaults, and `desktop_xfce_dotfiles` carries XFCE state. KDE uses its own MIME defaults. `desktop_void_dotfiles` remains reserved for files that need the Void runtime.
- `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` 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`.
- Native Linux workstation groups remain available for future hosts but have no current host in the main inventory.
- `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.
- 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`.

View File

@@ -1,70 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this repo is
Ansible-driven personal infrastructure as code for Fedora desktops (`ikaros`, `nymph`), WSL development (`deadalus-wsl`), and an Ubuntu server (`prometheus`). See `AGENTS.md` for topology and `README.md` for full profile descriptions.
## Validation commands
```bash
# Minimum before any change
ansible-playbook ansible/site.yml --syntax-check
# Dry-run per host
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 prometheus --check --diff
# Linting
ansible-lint ansible/site.yml
ansible-lint ansible/roles
yamllint ansible/
# Tag-scoped dry-run
ansible-playbook ansible/site.yml --limit ikaros --tags <tag> --check --diff
# Shell scripts
sh -n scripts/bootstrap_mail.sh && shellcheck scripts/bootstrap_mail.sh
```
## Architecture
Configuration is composed in layers: `all → OS → profile → host`. All hosts use `ansible_connection: local`.
`ansible/site.yml` is the sole orchestration entry point — keep it thin. Implementation belongs in roles under `ansible/roles/`.
Variable layering:
- `ansible/inventory/group_vars/` — OS and profile defaults
- `ansible/inventory/host_vars/` — host-specific overrides (never put host overrides in group_vars)
Dotfiles under `dotfiles/` mirror the same layer split: `common/`, `desktop/`, `server/`, `workstation/`, then per-host directories (`ikaros/`, `nymph/`).
## Key conventions
- FQCN for all Ansible modules.
- Prefer declarative modules over `command`/`shell`; when shell is unavoidable, make idempotency explicit.
- YAML: start with `---`, 2-space indent, file modes quoted as `"0644"`, booleans as booleans.
- `no_log: true` on any task that handles secrets.
- Do not restart `emptty` in playbook tasks on active desktop hosts — do it manually via SSH/TTY.
## Void desktop package buckets (keep disjoint)
- `void_packages_base` — system runtime (init, kernel, audio core, networking, firewall, hw daemons)
- `desktop_common_packages` — WM-agnostic GUI infra (GTK theme, polkit, keyring, portals, flatpak, printing)
- `desktop_sway_packages` / `desktop_hyprland_packages` — session-specific binaries; cross-WM packages used by both (`dunst`, `rofi`, `alacritty`) are intentionally duplicated
- `profile_packages` — cross-distro; do not move desktop-only Void entries into it
Dotfile var split follows the same logic: `desktop_common_dotfiles` (WM-agnostic), `desktop_void_dotfiles` (Void runtime — turnstile services, bash fragments).
## Secrets
- `secrets/vault.yml` and `secrets/vault.local.yml` are loaded conditionally if present.
- `secrets/.vault_pass.gpg` is used automatically if present; falls back to `secrets/.vault_pass`.
- Never print vault values; always use `no_log: true` where secrets flow through tasks.
## AI agent dotfiles
Shared agent instructions live in `dotfiles/common/.config/ai/`. Agent-specific entrypoints reference that path rather than duplicating content. Shared npm packages are in `ai_agents_npm_packages` in `ansible/inventory/group_vars/all.yml`.

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**.

1086
README.md

File diff suppressed because it is too large Load Diff

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,34 +0,0 @@
---
# Optional inventory overlay for VM tests. It is not loaded by ansible.cfg.
all:
children:
platform_mint:
hosts:
minttest:
ansible_connection: local
platform_freebsd:
hosts:
freebsdtest:
ansible_connection: local
graphical_desktop:
hosts:
minttest:
freebsdtest:
role_personal_workstation:
hosts:
minttest:
role_lab:
hosts:
freebsdtest:
desktop_cinnamon:
hosts:
minttest:
desktop_hyprland:
hosts:
freebsdtest:

View File

@@ -9,6 +9,29 @@ effective_user_group: "{{ user_group }}"
effective_user_home: "{{ user_home }}"
emacs_enabled: false
emacs_dotfiles:
- name: Bash Emacs client fragment
src: .bashrc.d/25-emacs-client-desktop.sh
dest: .bashrc.d/25-emacs-client-desktop.sh
mode: "0644"
- name: Emacs authoring configuration
src: .emacs.d/
dest: .emacs.d/
mode: preserve
emacs_authoring_directories:
- .bashrc.d
- Org
- Org/documentation
- Org/meetings
- Org/projects
emacs_authoring_files:
- name: Org HTML stylesheet
src: .emacs.d/templates/org/org.css
dest: Org/org.css
mode: "0644"
xdg_user_directories:
- Desktop
- Documents
@@ -67,38 +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
# 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

@@ -1,17 +1,10 @@
---
desktop_environment: minimal
desktop_allow_display_manager_switch: false
desktop_display_manager_by_environment:
minimal: emptty
xfce: lightdm
kde: sddm
desktop_sessions_enabled:
- sway
- hyprland
desktop_default_session: hyprland
desktop_default_session: sway
desktop_default_session_env: wayland
desktop_prompt_for_session: true
@@ -38,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'}
]
@@ -156,7 +148,6 @@ desktop_minimal_managed_paths:
- .config/flameshot
- .config/foot
- .config/gtklock
- .config/hypr
- .config/kanshi
- .config/rofi
- .config/sway
@@ -165,16 +156,6 @@ desktop_minimal_managed_paths:
- .config/wlogout
- .local/bin/udiskie-password
desktop_emacs_dotfiles:
- name: Bash Emacs client fragment
src: .bashrc.d/25-emacs-client-desktop.sh
dest: .bashrc.d/25-emacs-client-desktop.sh
mode: "0644"
- name: Emacs config
src: .emacs.d/
dest: .emacs.d/
mode: preserve
desktop_sway_dotfiles:
- name: alacritty config
src: .config/alacritty/
@@ -213,32 +194,6 @@ desktop_sway_dotfiles:
dest: .config/gtklock/
mode: preserve
desktop_hyprland_dotfiles:
- name: alacritty config
src: .config/alacritty/
dest: .config/alacritty/
mode: preserve
- name: dunst config
src: .config/dunst/
dest: .config/dunst/
mode: preserve
- name: rofi config
src: .config/rofi/
dest: .config/rofi/
mode: preserve
- name: hyprland config
src: .config/hypr/
dest: .config/hypr/
mode: preserve
- name: waybar config
src: .config/waybar/
dest: .config/waybar/
mode: preserve
- name: foot config
src: .config/foot/
dest: .config/foot/
mode: preserve
desktop_niri_dotfiles:
- name: foot config
src: .config/foot/
@@ -261,30 +216,6 @@ desktop_niri_dotfiles:
dest: .config/niri/
mode: preserve
desktop_xfce_dotfiles:
- name: XFCE configuration
src: .config/xfce4/
dest: .config/xfce4/
mode: preserve
- name: XFCE Thunar custom actions
src: .config/Thunar/uca.xfce.xml
dest: .config/Thunar/uca.xml
mode: "0644"
- name: XFCE dark desktop background
src: .local/share/backgrounds/xfce-dark.svg
dest: .local/share/backgrounds/xfce-dark.svg
mode: "0644"
desktop_xfce_managed_paths:
- .config/xfce4
- .local/share/backgrounds/xfce-dark.svg
xfce_theme_gtk: Yaru-blue-dark
xfce_theme_icons: Papirus-Dark
xfce_theme_wm: Greybird-dark
xfce_font_ui: Liberation Sans 10
xfce_font_monospace: Liberation Mono 11
desktop_flatpak_packages:
- be.alexandervanhee.gradia
- ch.protonmail.protonmail-bridge
@@ -293,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

@@ -1,29 +0,0 @@
---
desktop_profile: cinnamon
desktop_default_session: cinnamon
cinnamon_theme_gtk: Mint-Y-Dark-Aqua
cinnamon_theme_icons: Mint-Y-Aqua
cinnamon_font_ui: Sans 10
cinnamon_font_monospace: Monospace 10
# WM-agnostic desktop dotfiles from desktop_common_dotfiles that do NOT apply to
# Cinnamon: the autostart entry is the (removed) Protonmail mail bridge, and the
# GTK theme script conflicts with Cinnamon's own dconf-managed theming.
cinnamon_desktop_dotfiles_exclude:
- XDG autostart entries
- GTK theme setup script
cinnamon_dconf_settings:
- key: /org/cinnamon/desktop/interface/gtk-theme
value: "'{{ cinnamon_theme_gtk }}'"
- key: /org/cinnamon/desktop/interface/icon-theme
value: "'{{ cinnamon_theme_icons }}'"
- key: /org/cinnamon/desktop/interface/font-name
value: "'{{ cinnamon_font_ui }}'"
- key: /org/cinnamon/desktop/interface/monospace-font-name
value: "'{{ cinnamon_font_monospace }}'"
- key: /org/cinnamon/desktop/interface/clock-use-24h
value: "true"
- key: /org/cinnamon/desktop/wm/preferences/button-layout
value: "'menu:minimize,maximize,close'"

View File

@@ -3,6 +3,7 @@ desktop_profile: gnome
desktop_environment: gnome
desktop_sessions_enabled: []
desktop_default_session: gnome
emacs_enabled: true
# Fedora Workstation/GNOME desktop applications. Keep this profile separate from
# workstation_* so nymph can remain a desktop laptop rather than a dev workstation.
@@ -10,14 +11,18 @@ 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
- clang-tools-extra
- cmake
- cscope
- ctags
- croc
- delve
- dnsmasq
- evolution
- fastfetch
- poppler-glib
- poppler-utils
@@ -27,12 +32,12 @@ fedora_desktop_packages:
- golang
- golangci-lint
- gopls
- gnome-shell-extension-gsconnect
- hunspell
- libvirt
- libreoffice
- liberation-fonts
- libasan
- libvterm-devel
- libubsan
- libX11-devel
- libXft-devel
- meld
@@ -47,15 +52,17 @@ fedora_desktop_packages:
- qemu-tools
- rclone
- remmina
- rpi-imager
- ruff
- rustup
- rsync
- shfmt
- seahorse
- syncthing
- tealdeer
- terminus-fonts
- texlive
- tmux
- wl-clipboard
- ty
- uv
- valgrind
@@ -68,6 +75,8 @@ fedora_desktop_packages:
fedora_nerd_fonts_version: v3.3.0
fedora_nerd_fonts:
- Hack
- JetBrainsMono
- Iosevka
- UbuntuSans
- NerdFontsSymbolsOnly
@@ -75,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

@@ -1,3 +0,0 @@
---
desktop_profile: hyprland
desktop_default_session: hyprland

View File

@@ -12,12 +12,14 @@ 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
- pandoc
- latexmk
- texlive-latex
- texlive-luatex
- texlive-collection-latexrecommended
fedora_docker_packages:
- docker-ce
@@ -33,46 +35,26 @@ fedora_dotfiles:
- src: .bashrc.d/
dest: .bashrc.d/
mode: preserve
- src: .gtkrc-2.0
dest: .gtkrc-2.0
mode: "0644"
- src: .config/gtk-3.0/
dest: .config/gtk-3.0/
mode: preserve
- src: .config/gtk-4.0/
dest: .config/gtk-4.0/
mode: preserve
workstation_firewall_backend: firewalld
fedora_workstation_dev_packages:
fedora_dev_packages:
- croc
- distrobox
- gnupg
- gnupg2-gpg-agent
- libfido2
- openssh-clients
- pandoc
- shellcheck
workstation_host_linux_packages_fedora:
- code
- 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,121 +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
freebsd_desktop_common_packages:
- dbus
- dconf
- firefox
- fontconfig
- gnome-keyring
- libsecret
- mpv
- pinentry-gnome
- pipewire
- polkit
- thunderbird
- wireplumber
- xdg-desktop-portal
freebsd_hyprland_packages:
- alacritty
- cliphist
- dunst
- foot
- grim
- hypridle
- hyprland
- hyprlock
- hyprpaper
- rofi-wayland
- seatd
- slurp
- waybar
- wl-clipboard
- xdg-desktop-portal-gtk
- xdg-desktop-portal-hyprland
- xwayland
freebsd_niri_packages:
- cliphist
- dunst
- foot
- grim
- niri
- rofi-wayland
- seatd
- slurp
- waybar
- wl-clipboard
- xdg-desktop-portal-gtk
freebsd_lab_packages:
- lldb
- llvm
- podman
- qemu
- smartmontools
freebsd_rc_conf:
- name: dbus_enable
value: "YES"
- name: seatd_enable
value: "YES"
freebsd_enabled_services:
- dbus
- seatd
freebsd_xdg_config_dir: /usr/local/etc/xdg

View File

@@ -1,100 +0,0 @@
---
platform_profile: mint
platform_family: debian
platform_package_manager: apt
platform_service_manager: systemd
# syncthing runs per-user via the packaged system unit template
# (Debian/Mint ship syncthing@.service, parametrised by username).
enabled_services:
- "syncthing@{{ username }}.service"
mint_manage_flatpak: true
mint_flatpak_remote_name: flathub
mint_flatpak_remote_url: https://dl.flathub.org/repo/flathub.flatpakrepo
mint_packages_base:
- ca-certificates
- curl
- flatpak
- gpg
- htop
- make
- python3-pip
- ripgrep
- rsync
- shellcheck
mint_development_packages:
- build-essential
- clang
- clang-tools
- cmake
- gdb
- golang
- nodejs
- npm
- pkg-config
- python3-venv
mint_desktop_common_packages:
- dconf-cli
- deja-dup
- firefox
- fonts-liberation
- fonts-noto-color-emoji
- git-delta
- gnome-keyring
- libsecret-tools
- mpv
- pinentry-gnome3
- pinentry-gtk2
- rclone
- syncthing
- thunderbird
- tmux
- xdg-user-dirs
- yt-dlp
mint_emacs_packages:
- autoconf
- automake
- emacs
- hunspell
- hunspell-it
- libfreetype-dev
- libpoppler-glib-dev
- libpoppler-private-dev
- libtool-bin
- libvterm-dev
- libx11-dev
mint_cinnamon_packages:
- cinnamon
- cinnamon-control-center
- cinnamon-screensaver
- lightdm
- slick-greeter
- xdg-desktop-portal
- xdg-desktop-portal-gtk
mint_personal_workstation_packages:
- libreoffice
- meld
- pdfarranger
- vim
mint_flatpak_packages:
- ch.protonmail.protonmail-bridge
- com.calibre_ebook.calibre
- com.spotify.Client
- org.telegram.desktop
# Nerd Fonts referenced by the desktop configs deployed on Mint. Debian has no
# equivalent of Void's nerd-fonts-ttf package, so they are fetched from the
# upstream ryanoasis/nerd-fonts release.
mint_nerd_fonts_version: v3.3.0
mint_nerd_fonts:
- Hack
- UbuntuSans
- NerdFontsSymbolsOnly

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

@@ -1,11 +0,0 @@
---
machine_role: lab
role_profile: lab
role_stability: experimental
experimental_profile: true
lab_workspace_directories:
- path: "{{ user_home }}/Lab"
mode: "0755"
- path: "{{ user_home }}/Scratch"
mode: "0755"

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,28 +0,0 @@
---
ubuntu_packages_base:
- curl
- ufw
- htop
- build-essential
- nodejs
- git-delta
- jq
- ripgrep
ubuntu_manage_docker_repo: true
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

@@ -1,8 +1,4 @@
---
void_xbps_repositories:
- name: hyprland-void
url: https://raw.githubusercontent.com/sofijacom/hyprland-void/repository-x86_64-glibc
void_packages_base:
- 7zip
- NetworkManager
@@ -98,6 +94,7 @@ desktop_common_packages:
- cups
- cups-filters
- cups-pk-helper
- croc
- flatpak
- libsecret
- sane
@@ -140,45 +137,6 @@ desktop_minimal_packages:
- xfce-polkit
- xournalpp
desktop_kde_packages:
- ark
- dolphin
- ffmpegthumbs
- gwenview
- kate
- kde-plasma
- kleopatra
- konsole
- okular
- polkit-kde-agent
- print-manager
- sddm
- skanpage
- spectacle
desktop_xfce_packages:
- blueman
- gnome-keyring
- greybird-themes
- gvfs
- gvfs-cdda
- gvfs-mtp
- gvfs-smb
- lightdm
- lightdm-gtk-greeter
- network-manager-applet
- papirus-icon-theme
- seahorse
- thunar-archive-plugin
- xarchiver
- xdg-desktop-portal-gtk
- xfce4
- xfce4-clipman-plugin
- xfce4-pulseaudio-plugin
- xfce4-screenshooter
- xfce4-whiskermenu-plugin
- xorg
desktop_sway_packages:
- cliphist
- dunst
@@ -200,27 +158,6 @@ desktop_sway_packages:
- xdg-desktop-portal-wlr
- xorg-server-xwayland
desktop_hyprland_packages:
- cliphist
- dunst
- foot
- foot-terminfo
- grim
- hypridle
- hyprland
- hyprland-guiutils
- hyprlock
- hyprpolkitagent
- hyprpaper
- hyprsysteminfo
- rofi
- slurp
- SwayOSD
- Waybar
- wl-clipboard
- xdg-desktop-portal-hyprland
- xorg-server-xwayland
desktop_niri_packages:
- cliphist
- dunst
@@ -237,12 +174,6 @@ desktop_niri_packages:
- xdg-desktop-portal-gtk
- xorg-server-xwayland
void_lab_packages:
- llvm
- podman
- qemu
- smartmontools
profile_packages:
- bash-language-server
- bluez
@@ -252,6 +183,7 @@ profile_packages:
- deluge-gtk
- delve
- dnsmasq
- evolution
- poppler-glib
- poppler-utils
- fontconfig-devel
@@ -262,9 +194,7 @@ profile_packages:
- hunspell
- libvirt
- libspa-bluetooth
- libreoffice
- liberation-fonts-ttf
- libvterm-devel
- libX11-devel
- libXft-devel
- meld

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,12 +1,5 @@
---
workstation_dev_packages:
- distrobox
- gnupg
- gpg-agent
- libfido2-1
- openssh-client
- pandoc
- shellcheck
emacs_enabled: true
workstation_user_directories:
- path: "{{ user_home }}/.config"

View File

@@ -3,28 +3,33 @@ 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_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"
- src: .tmux.conf
dest: .tmux.conf
mode: "0644"
source_dir: desktop
- src: .tmux/bin/
dest: .tmux/bin/
mode: preserve
source_dir: desktop

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,72 +0,0 @@
---
workstation_host_linux_packages:
- 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,5 +0,0 @@
---
hostname: deadalus-wsl
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

@@ -22,3 +22,256 @@ host_authorized_ssh_keys: "{{ vault_ikaros_authorized_ssh_keys | default([]) }}"
host_enabled_services:
- sshd
# Host-local GNOME preferences captured from ikaros' live session. Keep these
# here rather than in desktop_gnome so nymph remains unmanaged for GNOME
# settings until it has its own real-use tuning.
gnome_gsettings_settings:
- schema: org.gnome.GWeather4
key: temperature-unit
value: "'centigrade'"
- schema: org.gnome.Ptyxis
key: cursor-shape
value: "'block'"
- schema: org.gnome.Ptyxis
key: default-columns
value: "uint32 140"
- schema: org.gnome.Ptyxis
key: default-profile-uuid
value: "'caa3c49ba73a208b4c4b64b56a5cd0af'"
- schema: org.gnome.Ptyxis
key: default-rows
value: "uint32 35"
- schema: org.gnome.Ptyxis
key: font-name
value: "'JetBrainsMono Nerd Font 14'"
- schema: org.gnome.Ptyxis
key: new-tab-position
value: "'last'"
- schema: org.gnome.Ptyxis
key: profile-uuids
value: "['caa3c49ba73a208b4c4b64b56a5cd0af']"
- schema: org.gnome.Ptyxis
key: restore-session
value: "false"
- schema: org.gnome.Ptyxis
key: restore-window-size
value: "false"
- schema: org.gnome.Ptyxis
key: use-system-font
value: "false"
- schema: org.gnome.Ptyxis.Profile
path: /org/gnome/Ptyxis/Profiles/caa3c49ba73a208b4c4b64b56a5cd0af/
key: bold-is-bright
value: "true"
- schema: org.gnome.Ptyxis.Profile
path: /org/gnome/Ptyxis/Profiles/caa3c49ba73a208b4c4b64b56a5cd0af/
key: label
value: "'Default'"
- schema: org.gnome.Ptyxis.Profile
path: /org/gnome/Ptyxis/Profiles/caa3c49ba73a208b4c4b64b56a5cd0af/
key: palette
value: "'linux'"
- schema: org.gnome.Ptyxis.Profile
path: /org/gnome/Ptyxis/Profiles/caa3c49ba73a208b4c4b64b56a5cd0af/
key: scroll-on-output
value: "true"
- schema: org.gnome.TextEditor
key: highlight-current-line
value: "true"
- schema: org.gnome.TextEditor
key: indent-style
value: "'space'"
- schema: org.gnome.TextEditor
key: indent-width
value: "-1"
- schema: org.gnome.TextEditor
key: right-margin-position
value: "uint32 120"
- schema: org.gnome.TextEditor
key: show-line-numbers
value: "true"
- schema: org.gnome.TextEditor
key: show-right-margin
value: "true"
- schema: org.gnome.TextEditor
key: tab-width
value: "uint32 4"
- schema: org.gnome.desktop.background
key: picture-options
value: "'zoom'"
- schema: org.gnome.desktop.background
key: picture-uri
value: "'file:///usr/share/backgrounds/gnome/morphogenesis-l.svg'"
- schema: org.gnome.desktop.background
key: picture-uri-dark
value: "'file:///usr/share/backgrounds/gnome/morphogenesis-d.svg'"
- schema: org.gnome.desktop.datetime
key: automatic-timezone
value: "true"
- schema: org.gnome.desktop.input-sources
key: sources
value: "[('xkb', 'us+alt-intl')]"
- schema: org.gnome.desktop.interface
key: accent-color
value: "'blue'"
- schema: org.gnome.desktop.interface
key: color-scheme
value: "'prefer-dark'"
- schema: org.gnome.desktop.peripherals.touchpad
key: two-finger-scrolling-enabled
value: "true"
- schema: org.gnome.desktop.privacy
key: remove-old-temp-files
value: "true"
- schema: org.gnome.desktop.privacy
key: remove-old-trash-files
value: "true"
- schema: org.gnome.desktop.privacy
key: report-technical-problems
value: "false"
- schema: org.gnome.desktop.screensaver
key: picture-options
value: "'zoom'"
- schema: org.gnome.desktop.screensaver
key: picture-uri
value: "'file:///usr/share/backgrounds/gnome/morphogenesis-l.svg'"
- schema: org.gnome.desktop.search-providers
key: sort-order
value: "['org.gnome.Settings.desktop', 'org.gnome.Contacts.desktop', 'org.gnome.Nautilus.desktop']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-1
value: "['<Super>1']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-2
value: "['<Super>2']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-3
value: "['<Super>3']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-4
value: "['<Super>4']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-5
value: "['<Super>5']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-6
value: "['<Super>6']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-7
value: "['<Super>7']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-8
value: "['<Super>8']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-9
value: "['<Super>9']"
- schema: org.gnome.desktop.wm.keybindings
key: switch-to-workspace-10
value: "['<Super>0']"
- schema: org.gnome.meld
key: highlight-current-line
value: "true"
- schema: org.gnome.meld
key: highlight-syntax
value: "true"
- schema: org.gnome.meld
key: indent-width
value: "4"
- schema: org.gnome.meld
key: insert-spaces-instead-of-tabs
value: "true"
- schema: org.gnome.meld
key: prefer-dark-theme
value: "true"
- schema: org.gnome.meld
key: show-line-numbers
value: "true"
- schema: org.gnome.meld
key: style-scheme
value: "'meld-dark'"
- schema: org.gnome.meld
key: wrap-mode
value: "'none'"
- schema: org.gnome.nautilus.list-view
key: default-zoom-level
value: "'large'"
- schema: org.gnome.nautilus.list-view
key: use-tree-view
value: "false"
- schema: org.gnome.nautilus.preferences
key: default-folder-viewer
value: "'list-view'"
- schema: org.gnome.nautilus.preferences
key: show-create-link
value: "true"
- schema: org.gnome.nautilus.preferences
key: show-delete-permanently
value: "true"
- schema: org.gnome.settings-daemon.plugins.color
key: night-light-enabled
value: "true"
- schema: org.gnome.settings-daemon.plugins.media-keys
key: custom-keybindings
value: "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/']"
- schema: org.gnome.settings-daemon.plugins.media-keys.custom-keybinding
path: /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/
key: binding
value: "'<Super>Return'"
- schema: org.gnome.settings-daemon.plugins.media-keys.custom-keybinding
path: /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/
key: command
value: "'ptyxis'"
- schema: org.gnome.settings-daemon.plugins.media-keys.custom-keybinding
path: /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/
key: name
value: "'Launch Terminal'"
- schema: org.gnome.shell
key: app-picker-layout
value: >-
[{'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: >-
['window-list@gnome-shell-extensions.gcampax.github.com', 'background-logo@fedorahosted.org', 'apps-menu@gnome-shell-extensions.gcampax.github.com', 'places-menu@gnome-shell-extensions.gcampax.github.com']
- schema: org.gnome.shell
key: enabled-extensions
value: >-
['status-icons@gnome-shell-extensions.gcampax.github.com', 'gnome-ui-tune@itstime.tech', 'appindicatorsupport@rgcjonas.gmail.com', 'bluetooth-battery-monitor@v8v88v8v88.com', 'clipboard-indicator@tudmotu.com', 'status-area-horizontal-spacing@mathematical.coffee.gmail.com', 'MaximizeWindowIntoNewWorkspace@kyleross.com', 'maximize-new-windows@VitalyOstanin']
- schema: org.gnome.shell
key: favorite-apps
value: >-
['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"
- schema: org.gnome.shell.keybindings
key: switch-to-application-1
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-2
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-3
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-4
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-5
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-6
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-7
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-8
value: "@as []"
- schema: org.gnome.shell.keybindings
key: switch-to-application-9
value: "@as []"
- schema: org.gnome.system.location
key: enabled
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,12 +10,18 @@ all:
ansible_connection: local
nymph:
ansible_connection: local
deadalus:
ansible_connection: local
platform_mint:
hosts: {}
platform_fedora_iot:
hosts:
aegis:
platform_freebsd:
hosts: {}
platform_rocky:
hosts:
atlas:
children:
rocky_server:
void:
children:
@@ -24,6 +30,11 @@ all:
fedora:
children:
platform_fedora:
platform_fedora_iot:
rocky:
children:
platform_rocky:
graphical_desktop:
hosts:
@@ -41,15 +52,13 @@ all:
ikaros:
ansible_connection: local
role_lab:
hosts: {}
role_aegis:
hosts:
aegis:
desktop_sway:
hosts: {}
desktop_hyprland:
hosts: {}
desktop_niri:
hosts: {}
@@ -60,23 +69,10 @@ all:
nymph:
ansible_connection: local
desktop_cinnamon:
hosts: {}
ubuntu:
children:
workstation_dev_wsl:
ubuntu_server:
workstation:
children:
workstation_host:
workstation_dev:
workstation_host:
children:
workstation_host_linux:
workstation_dev:
children:
workstation_dev_fedora:
@@ -84,23 +80,27 @@ all:
server:
children:
ubuntu_server:
fedora_workstation:
hosts: {}
workstation_host_linux:
hosts: {}
rocky_server:
workstation_dev_fedora:
hosts: {}
hosts:
deadalus:
ansible_connection: local
workstation_dev_wsl:
hosts:
deadalus-wsl:
deadalus:
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,8 +23,52 @@
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
tags: [dotfiles, dotfiles:common, emacs]
ansible.builtin.file:
path: "{{ effective_user_home }}/{{ item }}"
state: directory
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
mode: "0755"
loop: "{{ emacs_authoring_directories | default([]) }}"
when:
- emacs_enabled | default(false) | bool
- not (workstation_is_wsl | default(false) | bool)
- name: Deploy the shared Emacs authoring configuration
tags: [dotfiles, dotfiles:common, emacs]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ effective_user_home }}/{{ item.dest }}"
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
mode: "{{ item.mode }}"
loop: "{{ emacs_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when:
- emacs_enabled | default(false) | bool
- not (workstation_is_wsl | default(false) | bool)
- name: Deploy Emacs authoring assets
tags: [dotfiles, dotfiles:common, emacs]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ effective_user_home }}/{{ item.dest }}"
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
mode: "{{ item.mode }}"
loop: "{{ emacs_authoring_files | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when:
- emacs_enabled | default(false) | bool
- not (workstation_is_wsl | default(false) | bool)
- name: Ensure SSH socket directory exists
tags: [dotfiles, dotfiles:common]
ansible.builtin.file:
@@ -43,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:
@@ -98,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
@@ -108,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 }}"
@@ -122,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

@@ -1,9 +1,4 @@
---
- name: Gather installed package facts
tags: [packages]
ansible.builtin.package_facts:
manager: auto
- name: Copy Fedora dotfiles
tags: [dotfiles, dotfiles:common]
ansible.builtin.copy:
@@ -30,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]
@@ -89,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([]))
@@ -101,29 +64,36 @@
else []
)
+ (
(fedora_workstation_dev_packages | default(workstation_dev_packages | default([])))
(fedora_emacs_packages | default([]))
if (
emacs_enabled | default(false) | bool
)
else []
)
+ (
(fedora_dev_packages | default([]))
if 'workstation_dev_fedora' 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)
)
(workstation_dev_wsl_packages | default([]))
if 'workstation_dev_wsl' in group_names
else []
)
+ (host_packages | default([]))
)
| difference(fedora_excluded_packages | default([]))
| unique
}}
state: present
- name: Remove excluded Fedora packages
tags: [packages]
ansible.builtin.dnf:
name: "{{ fedora_removed_packages | default([]) }}"
state: absent
when: (fedora_removed_packages | default([])) | length > 0
- name: Add user to docker group
tags: [packages]
ansible.builtin.user:
@@ -134,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]
@@ -153,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,34 +0,0 @@
---
- name: Install packages on FreeBSD
tags: [packages]
community.general.pkgng:
name: >-
{{
(
(freebsd_packages_base | default([]))
+ (freebsd_development_packages | default([]))
+ (
(freebsd_desktop_common_packages | default([]))
if 'graphical_desktop' in group_names
else []
)
+ (
(freebsd_hyprland_packages | default([]))
if 'desktop_hyprland' in group_names
else []
)
+ (
(freebsd_niri_packages | default([]))
if 'desktop_niri' in group_names
else []
)
+ (
(freebsd_lab_packages | default([]))
if 'role_lab' in group_names
else []
)
+ (host_packages | default([]))
)
| unique
}}
state: present

View File

@@ -1,88 +0,0 @@
---
- name: Refresh apt package cache on Linux Mint
tags: [packages]
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install packages on Linux Mint
tags: [packages]
ansible.builtin.apt:
name: >-
{{
(
(common_packages | default([]))
+ (mint_packages_base | default([]))
+ (mint_development_packages | default([]))
+ (
(mint_desktop_common_packages | default([]))
if 'graphical_desktop' in group_names
else []
)
+ (
(mint_emacs_packages | default([]))
if (
'graphical_desktop' in group_names
and (emacs_enabled | default(false) | bool)
)
else []
)
+ (
(mint_cinnamon_packages | default([]))
if 'desktop_cinnamon' in group_names
else []
)
+ (
(mint_personal_workstation_packages | default([]))
if 'role_personal_workstation' in group_names
else []
)
+ (host_packages | default([]))
)
| unique
}}
state: present
- name: Ensure Flathub remote is configured on Linux Mint
tags: [packages]
community.general.flatpak_remote:
name: "{{ mint_flatpak_remote_name | default('flathub') }}"
state: present
flatpakrepo_url: "{{ mint_flatpak_remote_url | default('https://dl.flathub.org/repo/flathub.flatpakrepo') }}"
when:
- mint_manage_flatpak | default(true)
- >-
(
(mint_flatpak_packages | default([]))
+ (personal_workstation_flatpak_packages | default([]))
) | length > 0
- name: Install Linux Mint Flatpak applications
tags: [packages]
community.general.flatpak:
name: >-
{{
(
(mint_flatpak_packages | default([]))
+ (
(personal_workstation_flatpak_packages | default([]))
if 'role_personal_workstation' in group_names
else []
)
)
| unique
}}
state: present
remote: "{{ mint_flatpak_remote_name | default('flathub') }}"
method: system
when:
- mint_manage_flatpak | default(true)
- >-
(
(mint_flatpak_packages | default([]))
+ (
(personal_workstation_flatpak_packages | default([]))
if 'role_personal_workstation' in group_names
else []
)
) | length > 0

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,190 +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([]))
+ (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: 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

@@ -51,15 +51,7 @@
ansible.builtin.set_fact:
packages_void_selected_desktop_packages: >-
{{
(
(desktop_minimal_packages | default([]))
if (desktop_environment | default('minimal')) == 'minimal'
else (
(desktop_xfce_packages | default([]))
if (desktop_environment | default('minimal')) == 'xfce'
else (desktop_kde_packages | default([]))
)
)
(desktop_minimal_packages | default([]))
+ (
(desktop_sway_packages | default([]))
if (
@@ -68,14 +60,6 @@
)
else []
)
+ (
(desktop_hyprland_packages | default([]))
if (
(desktop_environment | default('minimal')) == 'minimal'
and 'hyprland' in (desktop_sessions_enabled | default([]))
)
else []
)
+ (
(desktop_niri_packages | default([]))
if (
@@ -95,14 +79,11 @@
(
(desktop_minimal_packages | default([]))
+ (desktop_sway_packages | default([]))
+ (desktop_hyprland_packages | default([]))
+ (desktop_niri_packages | default([]))
+ (desktop_xfce_packages | default([]))
+ (desktop_kde_packages | default([]))
)
| unique
| difference(packages_void_selected_desktop_packages)
| difference(desktop_display_manager_by_environment.values() | list)
| difference(['emptty'])
}}
- name: Install packages on Void Linux
@@ -118,7 +99,6 @@
+ (desktop_common_packages | default([]))
+ ((desktop_emacs_packages | default([])) if (emacs_enabled | default(false) | bool) else [])
+ (packages_void_selected_desktop_packages | default([]))
+ ((void_lab_packages | default([])) if 'role_lab' in group_names else [])
+ (host_packages | default([]))
)
| unique
@@ -129,7 +109,7 @@
- name: Check whether the selected display manager is active
tags: [packages, display-manager]
ansible.builtin.stat:
path: "/var/service/{{ desktop_display_manager_by_environment[desktop_environment | default('minimal')] }}"
path: /var/service/emptty
register: packages_void_selected_display_manager
- name: Remove packages managed by the inactive desktop environment
@@ -140,9 +120,7 @@
update_cache: false
when:
- (packages_void_inactive_desktop_packages | default([])) | length > 0
- >-
packages_void_selected_display_manager.stat.exists
or (desktop_allow_display_manager_switch | default(false))
- packages_void_selected_display_manager.stat.exists
- name: Remove host-absent packages on Void Linux
tags: [packages]

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

@@ -1,229 +0,0 @@
---
- name: Ensure Cinnamon user config directories exist
tags: [dotfiles, dotfiles:desktop, cinnamon]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
loop:
- "{{ user_home }}/.config"
- "{{ user_home }}/.local"
- "{{ user_home }}/.local/share"
- "{{ user_home }}/.local/bin"
- "{{ user_home }}/.local/state"
- "{{ user_home }}/.bashrc.d"
- "{{ user_home }}/.tmux"
- "{{ user_home }}/.tmux/bin"
- "{{ user_home }}/.tmux/plugins"
- name: Apply Cinnamon dconf settings
tags: [cinnamon]
community.general.dconf:
key: "{{ item.key }}"
value: "{{ item.value }}"
state: present
become: true
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
loop: "{{ cinnamon_dconf_settings | default([]) }}"
loop_control:
label: "{{ item.key }}"
- name: Ensure Emacs config directory exists
tags: [dotfiles, dotfiles:desktop, emacs]
ansible.builtin.file:
path: "{{ user_home }}/.emacs.d"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
when: emacs_enabled | default(false) | bool
- name: Ensure GnuPG directory exists
tags: [dotfiles, dotfiles:desktop, git]
ansible.builtin.file:
path: "{{ user_home }}/.gnupg"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0700"
- name: Ensure private user directories exist
tags: [dotfiles, dotfiles:desktop]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0700"
loop:
- "{{ user_home }}/.ssh"
- "{{ user_home }}/.local/state/ssh/sockets"
- name: Copy Emacs desktop dotfiles
tags: [dotfiles, dotfiles:desktop, emacs]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_emacs_dotfiles | default([]) }}"
when: emacs_enabled | default(false) | bool
loop_control:
label: "{{ item.dest }}"
- name: Copy WM-agnostic common desktop dotfiles
tags: [dotfiles, dotfiles:desktop]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: >-
{{
desktop_common_dotfiles
| default([])
| rejectattr('name', 'in', cinnamon_desktop_dotfiles_exclude | default([]))
| list
}}
loop_control:
label: "{{ item.dest }}"
- name: Copy gpg-agent configuration
tags: [dotfiles, dotfiles:desktop, git]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/.gnupg/gpg-agent.conf"
dest: "{{ user_home }}/.gnupg/gpg-agent.conf"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0600"
- name: Render git configuration with private values
tags: [dotfiles, dotfiles:desktop, git]
ansible.builtin.template:
src: desktop/.gitconfig.j2
dest: "{{ user_home }}/.gitconfig"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0644"
- name: Bootstrap tmux plugin manager checkout
tags: [dotfiles, dotfiles:desktop, tmux]
ansible.builtin.git:
repo: https://github.com/tmux-plugins/tpm
dest: "{{ user_home }}/.tmux/plugins/tpm"
version: master
update: true
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
- name: Install tmux plugins through TPM
tags: [dotfiles, dotfiles:desktop, tmux]
ansible.builtin.command:
cmd: "{{ user_home }}/.tmux/plugins/tpm/bin/install_plugins"
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
register: cinnamon_tmux_plugin_install
changed_when: >-
(cinnamon_tmux_plugin_install.stdout | default('')) is search('download success') or
(cinnamon_tmux_plugin_install.stderr | default('')) is search('download success')
- name: Install desktop npm packages
tags: [packages, npm]
community.general.npm:
name: "{{ item.name }}"
global: true
state: "{{ item.state | default('present') }}"
become: true
loop: "{{ desktop_npm_packages | default([]) }}"
when: desktop_npm_packages | length > 0
loop_control:
label: "{{ item.name }}"
- name: Ensure /usr/src is writable for source builds
tags: [packages]
ansible.builtin.file:
path: /usr/src
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
- name: Build and install the gf debugger frontend from source
tags: [packages]
ansible.builtin.include_tasks:
file: "{{ role_path }}/../profile_desktop_common/tasks/source_tool.yml"
apply:
tags: [packages]
loop: >-
{{
desktop_source_tools
| default([])
| selectattr('name', 'equalto', 'gf')
| list
}}
loop_control:
loop_var: source_tool
label: "{{ source_tool.name }}"
- name: Ensure Nerd Fonts base directory exists
tags: [packages, fonts]
ansible.builtin.file:
path: /usr/local/share/fonts/nerd-fonts
state: directory
owner: root
group: root
mode: "0755"
become: true
- name: Ensure per-font Nerd Fonts directories exist
tags: [packages, fonts]
ansible.builtin.file:
path: "/usr/local/share/fonts/nerd-fonts/{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
become: true
loop: "{{ mint_nerd_fonts | default([]) }}"
- name: Download Nerd Font archives
tags: [packages, fonts]
ansible.builtin.get_url:
url: "https://github.com/ryanoasis/nerd-fonts/releases/download/{{ mint_nerd_fonts_version }}/{{ item }}.zip"
dest: "/usr/local/share/fonts/nerd-fonts/{{ item }}.zip"
owner: root
group: root
mode: "0644"
become: true
loop: "{{ mint_nerd_fonts | default([]) }}"
loop_control:
label: "{{ item }}"
- name: Extract Nerd Font archives
tags: [packages, fonts]
ansible.builtin.unarchive:
src: "/usr/local/share/fonts/nerd-fonts/{{ item }}.zip"
dest: "/usr/local/share/fonts/nerd-fonts/{{ item }}"
remote_src: true
owner: root
group: root
become: true
loop: "{{ mint_nerd_fonts | default([]) }}"
loop_control:
label: "{{ item }}"
register: cinnamon_nerd_fonts_extract
- name: Refresh system font cache after installing Nerd Fonts
tags: [packages, fonts]
ansible.builtin.command: fc-cache -f
become: true
changed_when: false
when: cinnamon_nerd_fonts_extract is changed

View File

@@ -3,18 +3,12 @@
tags: [always]
ansible.builtin.assert:
that:
- (desktop_environment | default('minimal')) in ['minimal', 'xfce', 'kde']
- >-
(desktop_environment | default('minimal')) != 'minimal'
or (desktop_sessions_enabled | default([]) | length > 0)
- >-
(desktop_environment | default('minimal')) != 'minimal'
or (desktop_sessions_enabled | default([]) | difference(['sway', 'hyprland', 'niri']) | length == 0)
- >-
(desktop_environment | default('minimal')) != 'minimal'
or desktop_default_session in (desktop_sessions_enabled | default([]))
- (desktop_environment | default('minimal')) == 'minimal'
- (desktop_sessions_enabled | default([]) | length > 0)
- (desktop_sessions_enabled | default([]) | difference(['sway', 'niri']) | length == 0)
- desktop_default_session in (desktop_sessions_enabled | default([]))
fail_msg: >-
desktop_environment must be minimal, xfce, or kde. Minimal desktops also require
desktop_environment must be minimal. Minimal desktops also require
desktop_default_session to be included in desktop_sessions_enabled.
- name: Configure elogind to suspend on lid close
@@ -321,19 +315,6 @@
loop_control:
label: "{{ item.dest }}"
- name: Copy Emacs desktop dotfiles
tags: [dotfiles, dotfiles:desktop, emacs]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_emacs_dotfiles | default([]) }}"
when: emacs_enabled | default(false) | bool
loop_control:
label: "{{ item.dest }}"
- name: Render git configuration with private values
tags: [dotfiles, dotfiles:desktop, git]
ansible.builtin.template:
@@ -352,6 +333,7 @@
- tmux-autoreload
- tmux-resurrect
- tmux-continuum
- tmux-yank
- name: Check whether tmux plugin directories are git checkouts
tags: [dotfiles, dotfiles:desktop, tmux]
@@ -386,8 +368,10 @@
- name: Install tmux plugins through TPM
tags: [dotfiles, dotfiles:desktop, tmux]
ansible.builtin.command:
cmd: "{{ user_home }}/.tmux/plugins/tpm/bin/install_plugins"
ansible.builtin.shell: |
set -e
tmux start-server \; set-environment -g TMUX_PLUGIN_MANAGER_PATH "{{ user_home }}/.tmux/plugins/"
"{{ user_home }}/.tmux/plugins/tpm/bin/install_plugins"
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
@@ -463,14 +447,7 @@
owner: root
group: root
mode: "0644"
when: (desktop_environment | default('minimal')) in ['minimal', 'xfce']
- name: Remove minimal Qt theme override for KDE
tags: [theme]
ansible.builtin.file:
path: /etc/profile.d/10-qt-theme.sh
state: absent
when: (desktop_environment | default('minimal')) == 'kde'
when: (desktop_environment | default('minimal')) == 'minimal'
- name: Ensure flathub remote is configured
tags: [packages]
@@ -579,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

@@ -45,19 +45,6 @@
loop_control:
label: "{{ item.dest }}"
- name: Copy GNOME Emacs desktop dotfiles
tags: [dotfiles, dotfiles:desktop, emacs, gnome]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_emacs_dotfiles | default([]) }}"
when: emacs_enabled | default(false) | bool
loop_control:
label: "{{ item.dest }}"
- name: Configure GNOME gpg-agent for Fedora
tags: [dotfiles, dotfiles:desktop, git, gnome]
ansible.builtin.copy:
@@ -81,6 +68,114 @@
group: "{{ user_group }}"
mode: "0644"
- name: Define GNOME managed tmux plugin directories
tags: [dotfiles, dotfiles:desktop, tmux, gnome]
ansible.builtin.set_fact:
gnome_tmux_managed_plugin_names:
- tpm
- tmux-sensible
- tmux-autoreload
- tmux-resurrect
- tmux-continuum
- tmux-yank
- name: Check whether GNOME tmux plugin directories are git checkouts
tags: [dotfiles, dotfiles:desktop, tmux, gnome]
ansible.builtin.stat:
path: "{{ user_home }}/.tmux/plugins/{{ item }}/.git"
loop: "{{ gnome_tmux_managed_plugin_names }}"
loop_control:
label: "{{ item }}"
register: gnome_tmux_plugin_git_state
- name: Remove stale vendored GNOME tmux plugin directories
tags: [dotfiles, dotfiles:desktop, tmux, gnome]
ansible.builtin.file:
path: "{{ user_home }}/.tmux/plugins/{{ item.item }}"
state: absent
loop: "{{ gnome_tmux_plugin_git_state.results }}"
loop_control:
label: "{{ item.item }}"
when:
- item.stat.exists == false
- name: Bootstrap GNOME tmux plugin manager checkout
tags: [dotfiles, dotfiles:desktop, tmux, gnome]
ansible.builtin.git:
repo: https://github.com/tmux-plugins/tpm
dest: "{{ user_home }}/.tmux/plugins/tpm"
version: master
update: true
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
- name: Install GNOME tmux plugins through TPM
tags: [dotfiles, dotfiles:desktop, tmux, gnome]
ansible.builtin.shell: |
set -e
tmux start-server \; set-environment -g TMUX_PLUGIN_MANAGER_PATH "{{ user_home }}/.tmux/plugins/"
"{{ user_home }}/.tmux/plugins/tpm/bin/install_plugins"
become_user: "{{ username }}"
environment:
HOME: "{{ user_home }}"
register: gnome_tmux_plugin_install
changed_when: >-
(gnome_tmux_plugin_install.stdout | default('')) is search('download success') or
(gnome_tmux_plugin_install.stderr | default('')) is search('download success')
- name: Gather GNOME desktop user account data
tags: [gnome]
ansible.builtin.getent:
database: passwd
key: "{{ username }}"
when: (gnome_gsettings_settings | default([])) | length > 0
- name: Set GNOME desktop session environment
tags: [gnome]
ansible.builtin.set_fact:
gnome_user_uid: "{{ ansible_facts.getent_passwd[username][1] }}"
gnome_gsettings_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"
when: (gnome_gsettings_settings | default([])) | length > 0
- name: Read current 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: "{{ gnome_gsettings_environment }}"
loop: "{{ gnome_gsettings_settings | default([]) }}"
loop_control:
label: >-
{{ item.schema }}{{ ':' ~ item.path if item.path is defined else '' }} {{ item.key }}
register: gnome_gsettings_current
changed_when: false
- name: Apply 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: "{{ gnome_gsettings_environment }}"
loop: "{{ gnome_gsettings_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: Check whether SSH host ed25519 key exists on GNOME desktop
tags: [services, gnome]
ansible.builtin.stat:

View File

@@ -19,20 +19,3 @@
- (desktop_environment | default('minimal')) == 'minimal'
- "'sway' in (desktop_sessions_enabled | default([]))"
- (host_sway_dotfiles | default([])) | length > 0
- name: Copy host-specific Hyprland dotfiles
tags: [dotfiles, dotfiles:desktop, dotfiles:host, hyprland]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/{{ hostname }}/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ host_hyprland_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- (host_hyprland_dotfiles | default([])) | length > 0

View File

@@ -1,4 +0,0 @@
[preferred]
default=gtk
org.freedesktop.impl.portal.Screenshot=hyprland
org.freedesktop.impl.portal.ScreenCast=hyprland

View File

@@ -1,6 +0,0 @@
[Desktop Entry]
Name=Hyprland
Comment=Hyprland (Wayland)
Exec=/usr/local/bin/start-hyprland-session
Type=Application
DesktopNames=Hyprland

View File

@@ -1,92 +0,0 @@
---
- name: Ensure Hyprland config directories exist
tags: [dotfiles, dotfiles:desktop, hyprland]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
loop:
- "{{ user_home }}/.config/alacritty"
- "{{ user_home }}/.config/dunst"
- "{{ user_home }}/.config/flameshot"
- "{{ user_home }}/.config/foot"
- "{{ user_home }}/.config/hypr"
- "{{ user_home }}/.config/rofi"
- "{{ user_home }}/.config/waybar"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- name: Install allowed emptty Hyprland Wayland session
tags: [packages, services, emptty, hyprland]
ansible.builtin.copy:
src: hyprland.desktop
dest: /etc/emptty/wayland-sessions/hyprland.desktop
owner: root
group: root
mode: "0644"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- name: Remove legacy Hyprland session launcher shadowing package binary
tags: [packages, services, hyprland]
ansible.builtin.file:
path: /usr/local/bin/start-hyprland
state: absent
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- name: Install Hyprland session launcher in /usr/local/bin
tags: [packages, services, hyprland]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/.local/bin/start-hyprland-session"
dest: /usr/local/bin/start-hyprland-session
owner: root
group: root
mode: "0755"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- name: Ensure /etc/xdg/xdg-desktop-portal exists for Hyprland
tags: [packages, services, hyprland, portal]
ansible.builtin.file:
path: /etc/xdg/xdg-desktop-portal
state: directory
owner: root
group: root
mode: "0755"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- name: Configure xdg-desktop-portal backend preferences for Hyprland
tags: [packages, services, hyprland, portal]
ansible.builtin.copy:
src: hyprland-portals.conf
dest: /etc/xdg/xdg-desktop-portal/hyprland-portals.conf
owner: root
group: root
mode: "0644"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"
- name: Copy Hyprland desktop dotfiles
tags: [dotfiles, dotfiles:desktop, hyprland]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_hyprland_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when:
- (desktop_environment | default('minimal')) == 'minimal'
- "'hyprland' in (desktop_sessions_enabled | default([]))"

View File

@@ -1,4 +0,0 @@
[preferred]
default=gtk
org.freedesktop.impl.portal.Screenshot=hyprland
org.freedesktop.impl.portal.ScreenCast=hyprland

View File

@@ -1,58 +0,0 @@
---
- name: Ensure FreeBSD Hyprland user config directories exist
tags: [dotfiles, dotfiles:desktop, hyprland]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
loop:
- "{{ user_home }}/.config"
- "{{ user_home }}/.config/alacritty"
- "{{ user_home }}/.config/dunst"
- "{{ user_home }}/.config/foot"
- "{{ user_home }}/.config/hypr"
- "{{ user_home }}/.config/rofi"
- "{{ user_home }}/.config/waybar"
- "{{ user_home }}/.local"
- "{{ user_home }}/.local/bin"
- name: Copy common Hyprland desktop dotfiles on FreeBSD
tags: [dotfiles, dotfiles:desktop, hyprland]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_hyprland_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Render FreeBSD Hyprland session launcher
tags: [packages, services, hyprland]
ansible.builtin.template:
src: start-hyprland-session.j2
dest: /usr/local/bin/start-hyprland-session
owner: root
group: wheel
mode: "0755"
- name: Ensure FreeBSD xdg-desktop-portal config directory exists
tags: [packages, services, hyprland, portal]
ansible.builtin.file:
path: "{{ freebsd_xdg_config_dir | default('/usr/local/etc/xdg') }}/xdg-desktop-portal"
state: directory
owner: root
group: wheel
mode: "0755"
- name: Configure xdg-desktop-portal backend preferences for FreeBSD Hyprland
tags: [packages, services, hyprland, portal]
ansible.builtin.copy:
src: hyprland-portals.conf
dest: "{{ freebsd_xdg_config_dir | default('/usr/local/etc/xdg') }}/xdg-desktop-portal/hyprland-portals.conf"
owner: root
group: wheel
mode: "0644"

View File

@@ -1,24 +0,0 @@
#!/bin/sh
[ -r /etc/profile ] && . /etc/profile
[ -r "$HOME/.profile" ] && . "$HOME/.profile"
export XDG_CURRENT_DESKTOP=Hyprland
export XDG_SESSION_DESKTOP=Hyprland
export XDG_SESSION_TYPE=wayland
export MOZ_ENABLE_WAYLAND=1
export QT_QPA_PLATFORM='wayland;xcb'
export _JAVA_AWT_WM_NONREPARENTING=1
export SDL_VIDEODRIVER=wayland
export GDK_BACKEND='wayland,x11'
export SSH_AUTH_SOCK="$HOME/.local/state/ssh-agent/socket"
[ -r "$HOME/.config/hypr/session-env" ] && . "$HOME/.config/hypr/session-env"
if command -v dbus-run-session >/dev/null 2>&1; then
exec dbus-run-session Hyprland
fi
exec Hyprland

View File

@@ -1,60 +0,0 @@
---
- name: Check whether SDDM is the active runit display manager
tags: [packages, services, kde, display-manager]
ansible.builtin.stat:
path: /var/service/sddm
register: profile_desktop_kde_sddm_service
when: (desktop_environment | default('minimal')) == 'kde'
- name: Copy KDE MIME application defaults
tags: [dotfiles, dotfiles:desktop, kde]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/.config/mimeapps.kde.list"
dest: "{{ user_home }}/.config/mimeapps.list"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0644"
when: (desktop_environment | default('minimal')) == 'kde'
- name: Remove minimal desktop user configuration managed by Ansible
tags: [dotfiles, dotfiles:desktop, kde]
ansible.builtin.file:
path: "{{ user_home }}/{{ item }}"
state: absent
loop: >-
{{
(desktop_minimal_managed_paths | default([]))
+ (desktop_xfce_managed_paths | default([]))
}}
loop_control:
label: "{{ item }}"
when:
- (desktop_environment | default('minimal')) == 'kde'
- profile_desktop_kde_sddm_service.stat.exists
- name: Remove minimal desktop portal preferences
tags: [packages, services, kde, portal]
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/xdg/xdg-desktop-portal/hyprland-portals.conf
- /etc/xdg/xdg-desktop-portal/sway-portals.conf
- /etc/xdg/xdg-desktop-portal/xfce-portals.conf
when:
- (desktop_environment | default('minimal')) == 'kde'
- profile_desktop_kde_sddm_service.stat.exists
- name: Remove inactive emptty and minimal session launchers
tags: [packages, services, kde, display-manager]
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/emptty
- /etc/lightdm/lightdm.conf.d/50-xfce.conf
- /usr/local/bin/start-hyprland-session
- /usr/local/bin/start-sway
when:
- (desktop_environment | default('minimal')) == 'kde'
- profile_desktop_kde_sddm_service.stat.exists

View File

@@ -1,41 +0,0 @@
---
- name: Ensure FreeBSD Niri user config directories exist
tags: [dotfiles, dotfiles:desktop, niri]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
loop:
- "{{ user_home }}/.config"
- "{{ user_home }}/.config/dunst"
- "{{ user_home }}/.config/foot"
- "{{ user_home }}/.config/niri"
- "{{ user_home }}/.config/rofi"
- "{{ user_home }}/.config/waybar"
- "{{ user_home }}/.local"
- "{{ user_home }}/.local/bin"
- "{{ user_home }}/.local/state"
- "{{ user_home }}/.local/state/ssh-agent"
- name: Copy common Niri desktop dotfiles on FreeBSD
tags: [dotfiles, dotfiles:desktop, niri]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_niri_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Render FreeBSD Niri session launcher
tags: [packages, services, niri]
ansible.builtin.template:
src: start-niri-session.j2
dest: /usr/local/bin/start-niri-session
owner: root
group: wheel
mode: "0755"

View File

@@ -1,24 +0,0 @@
#!/bin/sh
[ -r /etc/profile ] && . /etc/profile
[ -r "$HOME/.profile" ] && . "$HOME/.profile"
export XDG_CURRENT_DESKTOP=niri
export XDG_SESSION_DESKTOP=niri
export XDG_SESSION_TYPE=wayland
export MOZ_ENABLE_WAYLAND=1
export QT_QPA_PLATFORM='wayland;xcb'
export _JAVA_AWT_WM_NONREPARENTING=1
export SDL_VIDEODRIVER=wayland
export GDK_BACKEND='wayland,x11'
export SSH_AUTH_SOCK="$HOME/.local/state/ssh-agent/socket"
[ -r "$HOME/.config/niri/session-env" ] && . "$HOME/.config/niri/session-env"
if command -v dbus-run-session >/dev/null 2>&1; then
exec dbus-run-session niri --session
fi
exec niri --session

View File

@@ -1,223 +0,0 @@
---
- name: Inspect configured desktop display manager for XFCE cleanup gates
tags: [always, packages, services, xfce, display-manager]
ansible.builtin.stat:
path: "/var/service/{{ desktop_display_manager_by_environment[desktop_environment | default('minimal')] }}"
register: profile_desktop_xfce_selected_display_manager
- name: Ensure XFCE user configuration directories exist
tags: [dotfiles, dotfiles:desktop, xfce]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0755"
loop:
- "{{ user_home }}/.config/xfce4"
- "{{ user_home }}/.config/xfce4/terminal"
- "{{ user_home }}/.config/xfce4/xfconf"
- "{{ user_home }}/.config/xfce4/xfconf/xfce-perchannel-xml"
- "{{ user_home }}/.config/Thunar"
- "{{ user_home }}/.local/share/backgrounds"
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Copy XFCE desktop dotfiles
tags: [dotfiles, dotfiles:desktop, xfce]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ desktop_xfce_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Render XFCE themed configuration
tags: [dotfiles, dotfiles:desktop, xfce]
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0644"
loop:
- src: xsettings.xml.j2
dest: .config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml
- src: xfwm4.xml.j2
dest: .config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml
- src: xfce4-desktop.xml.j2
dest: .config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml
- src: terminalrc.j2
dest: .config/xfce4/terminal/terminalrc
loop_control:
label: "{{ item.dest }}"
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Copy XFCE MIME application defaults
tags: [dotfiles, dotfiles:desktop, xfce]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/desktop/.config/mimeapps.list"
dest: "{{ user_home }}/.config/mimeapps.list"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0644"
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Make Thunderbird the XFCE mail handler
tags: [dotfiles, dotfiles:desktop, xfce]
community.general.ini_file:
path: "{{ user_home }}/.config/mimeapps.list"
section: "{{ item.section }}"
option: x-scheme-handler/mailto
value: "{{ item.value }}"
no_extra_spaces: true
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "0644"
loop:
- section: Default Applications
value: org.mozilla.Thunderbird.desktop
- section: Added Associations
value: org.mozilla.Thunderbird.desktop;
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Ensure LightDM configuration directories exist
tags: [packages, services, xfce, display-manager]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- /etc/lightdm
- /etc/lightdm/lightdm.conf.d
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Configure LightDM for the XFCE session
tags: [packages, services, xfce, display-manager]
ansible.builtin.template:
src: lightdm-xfce.conf.j2
dest: /etc/lightdm/lightdm.conf.d/50-xfce.conf
owner: root
group: root
mode: "0644"
register: profile_desktop_xfce_lightdm_config
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Configure the LightDM GTK greeter
tags: [packages, services, xfce, display-manager]
ansible.builtin.template:
src: lightdm-gtk-greeter.conf.j2
dest: /etc/lightdm/lightdm-gtk-greeter.conf
owner: root
group: root
mode: "0644"
register: profile_desktop_xfce_lightdm_greeter_config
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Report deferred LightDM configuration activation
tags: [packages, services, xfce, display-manager]
ansible.builtin.debug:
msg: >-
LightDM configuration changed. Apply the display-manager switch or restart
LightDM from a TTY or SSH session; it is not restarted automatically.
when:
- (desktop_environment | default('minimal')) == 'xfce'
- >-
profile_desktop_xfce_lightdm_config is changed
or profile_desktop_xfce_lightdm_greeter_config is changed
- name: Manage gnome-keyring auth hook for LightDM
tags: [packages, services, xfce]
ansible.builtin.lineinfile:
path: /etc/pam.d/lightdm
insertafter: EOF
line: "auth optional pam_gnome_keyring.so"
state: present
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Manage gnome-keyring session hook for LightDM
tags: [packages, services, xfce]
ansible.builtin.lineinfile:
path: /etc/pam.d/lightdm
insertafter: EOF
line: "session optional pam_gnome_keyring.so auto_start"
state: present
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Ensure XFCE portal configuration directory exists
tags: [packages, services, xfce, portal]
ansible.builtin.file:
path: /etc/xdg/xdg-desktop-portal
state: directory
owner: root
group: root
mode: "0755"
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Configure xdg-desktop-portal preferences for XFCE
tags: [packages, services, xfce, portal]
ansible.builtin.copy:
dest: /etc/xdg/xdg-desktop-portal/xfce-portals.conf
content: |
[preferred]
default=gtk
owner: root
group: root
mode: "0644"
when: (desktop_environment | default('minimal')) == 'xfce'
- name: Remove minimal desktop user configuration after switching to XFCE
tags: [dotfiles, dotfiles:desktop, xfce]
ansible.builtin.file:
path: "{{ user_home }}/{{ item }}"
state: absent
loop: "{{ desktop_minimal_managed_paths | default([]) | difference(['.config/Thunar']) }}"
loop_control:
label: "{{ item }}"
when:
- (desktop_environment | default('minimal')) == 'xfce'
- profile_desktop_xfce_selected_display_manager.stat.exists
- name: Remove minimal portal preferences and session launchers after switching to XFCE
tags: [packages, services, xfce, portal, display-manager]
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/xdg/xdg-desktop-portal/hyprland-portals.conf
- /etc/xdg/xdg-desktop-portal/sway-portals.conf
- /etc/emptty
- /usr/local/bin/start-hyprland-session
- /usr/local/bin/start-sway
when:
- (desktop_environment | default('minimal')) == 'xfce'
- profile_desktop_xfce_selected_display_manager.stat.exists
- name: Remove XFCE user configuration after switching to another profile
tags: [dotfiles, dotfiles:desktop, xfce]
ansible.builtin.file:
path: "{{ user_home }}/{{ item }}"
state: absent
loop: "{{ desktop_xfce_managed_paths | default([]) }}"
loop_control:
label: "{{ item }}"
when:
- (desktop_environment | default('minimal')) != 'xfce'
- profile_desktop_xfce_selected_display_manager.stat.exists
- name: Remove inactive XFCE portal and LightDM profile configuration
tags: [packages, services, xfce, portal, display-manager]
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/xdg/xdg-desktop-portal/xfce-portals.conf
- /etc/lightdm/lightdm.conf.d/50-xfce.conf
when:
- (desktop_environment | default('minimal')) != 'xfce'
- profile_desktop_xfce_selected_display_manager.stat.exists

View File

@@ -1,7 +0,0 @@
[greeter]
theme-name={{ xfce_theme_gtk }}
icon-theme-name={{ xfce_theme_icons }}
font-name={{ xfce_font_ui }}
background=#050505
clock-format=%a %d %b, %H:%M
indicators=~host;~spacer;~clock;~spacer;~session;~power

View File

@@ -1,3 +0,0 @@
[Seat:*]
greeter-session=lightdm-gtk-greeter
user-session=xfce

View File

@@ -1,15 +0,0 @@
[Configuration]
FontName={{ xfce_font_monospace }}
MiscUseSystemFont=FALSE
MiscMenubarDefault=FALSE
MiscToolbarDefault=FALSE
MiscBordersDefault=TRUE
ScrollingLines=10000
BackgroundMode=TERMINAL_BACKGROUND_SOLID
ColorForeground=#d8d8d8
ColorBackground=#050505
ColorCursor=#d8d8d8
ColorSelection=#d8d8d8
ColorSelectionUseDefault=FALSE
ColorSelectionBackground=#3a5f7f
ColorPalette=#050505;#8f3f3f;#5f7f5f;#8f7f3f;#3a5f7f;#6f4f7f;#3f7f7f;#d8d8d8;#777777;#bf5f5f;#7f9f7f;#af9f5f;#5f7f9f;#8f6f9f;#5f9f9f;#ffffff

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-desktop" version="1.0">
<property name="backdrop" type="empty">
<property name="screen0" type="empty">
<property name="monitor0" type="empty">
<property name="workspace0" type="empty">
<property name="color-style" type="int" value="0"/>
<property name="image-style" type="int" value="5"/>
<property name="last-image" type="string" value="{{ user_home }}/.local/share/backgrounds/xfce-dark.svg"/>
</property>
</property>
</property>
</property>
<property name="desktop-icons" type="empty">
<property name="style" type="int" value="0"/>
</property>
</channel>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfwm4" version="1.0">
<property name="general" type="empty">
<property name="theme" type="string" value="{{ xfce_theme_wm }}"/>
<property name="title_font" type="string" value="{{ xfce_font_ui }}"/>
<property name="workspace_count" type="int" value="6"/>
<property name="workspace_names" type="array">
<value type="string" value="term"/>
<value type="string" value="web"/>
<value type="string" value="code"/>
<value type="string" value="mail"/>
<value type="string" value="ops"/>
<value type="string" value="misc"/>
</property>
<property name="use_compositing" type="bool" value="false"/>
<property name="click_to_focus" type="bool" value="true"/>
<property name="focus_delay" type="int" value="250"/>
<property name="raise_delay" type="int" value="250"/>
</property>
</channel>

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