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>
This commit is contained in:
Fabio Scotto di Santolo
2026-09-08 10:36:29 +02:00
committed by GitHub
parent ae1ca44aa7
commit eab66b6d3d
20 changed files with 507 additions and 271 deletions

View File

@@ -1,6 +1,6 @@
# AGENTS.md # AGENTS.md
Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora IoT, WSL, and servers. 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 ## Source Of Truth
- Main orchestration: `ansible/site.yml` - Main orchestration: `ansible/site.yml`
@@ -15,7 +15,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Current laptop: `nymph = platform_fedora + 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` - Void desktop profile is also the base for other future/reference hosts via `platform_void + graphical_desktop`
- Workstation: `deadalus` is Windows + Fedora WSL. - Workstation: `deadalus` is Windows + Fedora WSL.
- Ubuntu server: `prometheus` - Rocky server: `prometheus` belongs to `rocky_server`.
- NAS: `atlas` (Rocky Linux 9, reached through SSH) - NAS: `atlas` (Rocky Linux 9, reached through SSH)
- Always-on LAN node: `aegis` (Fedora IoT on Raspberry Pi 4, 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. - Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions.
@@ -45,6 +45,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Fedora laptop work: `ansible-playbook ansible/site.yml --limit nymph --check --diff` - Fedora laptop work: `ansible-playbook ansible/site.yml --limit nymph --check --diff`
- WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff` - WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff`
- Server: `ansible-playbook ansible/site.yml --limit prometheus --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` - Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --check --diff`
- Aegis IoT: `ansible-playbook ansible/site.yml --limit aegis --check --diff` - Aegis IoT: `ansible-playbook ansible/site.yml --limit aegis --check --diff`
- Focused checks: - Focused checks:
@@ -83,7 +84,9 @@ The Void desktop package lists in `ansible/inventory/group_vars/void.yml` are ke
- `desktop_common_packages` — GUI infrastructure shared by the minimal desktop mode. - `desktop_common_packages` — GUI infrastructure shared by the minimal desktop mode.
- `desktop_minimal_packages` — applications, integration components, and the `emptty` display manager. - `desktop_minimal_packages` — applications, integration components, and the `emptty` display manager.
- `desktop_sway_packages` — binaries specific to the Sway session. - `desktop_sway_packages` — binaries specific to the Sway session.
`profile_packages` in the same file is cross-distro and is overridden by `group_vars/server.yml` and the workstation group vars; do not move desktop-specific Void entries through it. `profile_packages` remains the shared package bucket for Void and Fedora profiles. Rocky uses
`rocky_profile_packages` so RPM-specific names do not leak back into the other platforms; do not move
desktop-specific Void entries through either bucket.
The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-independent content and `desktop_minimal_dotfiles` carries Thunar, Udiskie, and MIME defaults. `desktop_void_dotfiles` remains reserved for files that need the Void runtime. 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 ## Workstation Notes
@@ -92,6 +95,16 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- Fedora WSL installs Mise from the official `jdxcode/mise` COPR and uses its pinned Temurin Java 11 JDK; update the declared Mise version deliberately. - 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. - Windows applications are installed manually and are not managed from the WSL profile.
## Rocky Server Notes
- `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 Docker CE, uses firewalld, preserves SELinux enforcement, and renders the
same server Compose stack. It does not transfer data, start containers, update DNS, or cut over traffic.
- `scripts/migrate_prometheus_data.sh` is the separate, source-host-run 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, Cockpit, and Syncthing stay selected through Atlas host variables
and must not leak into `rocky_server`.
## Atlas NAS Notes ## Atlas NAS Notes
- `atlas` is a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values in - `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>`; `host_vars/atlas.yml`. Bootstrap it once with `-e atlas_connection_username=<existing-admin>`;

View File

@@ -33,7 +33,6 @@ infra/
│ ├── common/ │ ├── common/
│ ├── desktop/ │ ├── desktop/
│ ├── fedora/ │ ├── fedora/
│ ├── ubuntu/
│ ├── server/ │ ├── server/
│ ├── workstation/ │ ├── workstation/
│ ├── workstation_dev_wsl/ │ ├── workstation_dev_wsl/
@@ -56,8 +55,8 @@ Il repository è diviso in due componenti principali:
# Macchine gestite # Macchine gestite
Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Ubuntu e Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Rocky
un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti: Linux 9 e un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti:
```text ```text
common user environment common user environment
@@ -74,8 +73,8 @@ Matrice target:
| ikaros | Fedora | Personal workstation | GNOME | | ikaros | Fedora | Personal workstation | GNOME |
| nymph | Fedora | Desktop laptop | GNOME | | nymph | Fedora | Desktop laptop | GNOME |
| deadalus | Fedora WSL | Workstation dev | — | | deadalus | Fedora WSL | Workstation dev | — |
| prometheus | Ubuntu | Server | — | | prometheus | Rocky Linux | Server | — |
| atlas | Rocky 9 | NAS | — | | atlas | Rocky Linux | NAS | — |
Regola operativa: Regola operativa:
@@ -93,7 +92,7 @@ Nota sullo stato attuale del playbook principale:
- `ansible/site.yml` applica oggi in automatico Fedora/GNOME su `ikaros` e `nymph` - `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 Fedora WSL alla workstation `deadalus`
- `ansible/site.yml` applica anche il profilo `ubuntu_server` con baseline apt, systemd, dotfiles server e firewall UFW - `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 - `ansible/site.yml` applica il profilo NAS Rocky su `atlas` tramite SSH remoto
## Desktop ## Desktop
@@ -164,7 +163,7 @@ Le applicazioni Windows sono installate e gestite manualmente; il profilo WSL no
Sistema operativo: Sistema operativo:
- Ubuntu LTS - Rocky Linux 9
Configurazione: Configurazione:
@@ -178,12 +177,19 @@ Profilo orientato a servizi server e gestione di dotfiles dedicati.
Lo stato attuale del profilo server include: Lo stato attuale del profilo server include:
- installazione pacchetti base Ubuntu via apt - installazione pacchetti Rocky via DNF, EPEL e CRB
- installazione e configurazione di Docker dal repository ufficiale - installazione e configurazione di Docker dal repository ufficiale
- abilitazione dei servizi systemd dichiarati in inventory/group vars - abilitazione dei servizi systemd dichiarati in inventory/group vars
- copia dei dotfiles server e rendering dei template server, incluso il `docker-compose.yml` dello stack servizi - copia dei dotfiles server e rendering dei template server, incluso il `docker-compose.yml` dello stack servizi
- attivazione del firewall UFW con regola SSH esplicita - attivazione di firewalld con servizio SSH esplicitamente abilitato
- apertura delle porte Syncthing `22000/tcp`, `22000/udp` e `21027/udp`, lasciando la GUI non esposta direttamente su UFW - Syncthing escluso dal profilo server Rocky
### 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 i dati PostgreSQL. Non avvia container, non
cancella dati e non esegue il cutover.
Utente del profilo server: Utente del profilo server:
@@ -274,7 +280,6 @@ I principali ruoli attualmente presenti sono:
| ------------------------- | ----------------------------------- | | ------------------------- | ----------------------------------- |
| base | configurazione base comune | | base | configurazione base comune |
| packages_void | installazione pacchetti su Void | | packages_void | installazione pacchetti su Void |
| packages_ubuntu | installazione pacchetti su Ubuntu |
| packages_fedora | installazione pacchetti su Fedora | | packages_fedora | installazione pacchetti su Fedora |
| packages_rocky | installazione pacchetti su Rocky Linux 9 | | packages_rocky | installazione pacchetti su Rocky Linux 9 |
| services_runit | gestione servizi runit | | services_runit | gestione servizi runit |
@@ -309,7 +314,7 @@ platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common workstation_dev_fedora -> profile_workstation_dev_common
workstation_dev_wsl -> profile_workstation_dev_wsl (dopo platform_fedora + workstation_dev_fedora) workstation_dev_wsl -> profile_workstation_dev_wsl (dopo platform_fedora + workstation_dev_fedora)
ubuntu_server -> packages_ubuntu + services_systemd + profile_server rocky_server -> dotfiles_common + profile_server (dopo platform_rocky)
``` ```
Questo significa che, allo stato attuale: Questo significa che, allo stato attuale:
@@ -318,9 +323,9 @@ Questo significa che, allo stato attuale:
- `nymph` riceve Fedora Workstation/GNOME come target laptop - `nymph` riceve Fedora Workstation/GNOME come target laptop
- il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri - il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri
- `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati - `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati
- il server Ubuntu (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewall - il server Rocky (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewalld
- il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives - il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives
- lo stack container server include `navidrome`, `postgres`, `gitea`, `nginx-proxy-manager` e `syncthing`, con GUI Syncthing raggiungibile tramite la rete Docker `web` - lo stack container server include `navidrome`, `postgres`, `gitea` e `nginx-proxy-manager`
# Dotfiles # Dotfiles
@@ -332,7 +337,6 @@ dotfiles/
├── desktop ├── desktop
├── server ├── server
├── fedora ├── fedora
├── ubuntu
├── workstation ├── workstation
├── workstation_dev_wsl ├── workstation_dev_wsl
└── nymph └── nymph
@@ -394,8 +398,7 @@ Allo stato attuale questo comando:
- per `platform_fedora & role_personal_workstation` applica il layer personale a `ikaros` - 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 `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 `workstation_dev_wsl` applica i tweak WSL dopo il layer Fedora a `deadalus`, escludendo Flatpak e Snap
- per gli host `ubuntu_server` applica pacchetti Ubuntu, servizi systemd, profilo server, UFW, dotfiles e template dedicati - 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`
- per `platform_rocky` applica pacchetti Rocky e servizi systemd ad `atlas`, quindi il profilo NAS dedicato
- non riavvia automaticamente il display manager - non riavvia automaticamente il display manager
- carica `secrets/vault.yml` solo se presente - carica `secrets/vault.yml` solo se presente
- carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza - carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza

View File

@@ -20,7 +20,6 @@ infra/
│ ├── common/ │ ├── common/
│ ├── desktop/ │ ├── desktop/
│ ├── fedora/ │ ├── fedora/
│ ├── ubuntu/
│ ├── server/ │ ├── server/
│ ├── workstation/ │ ├── workstation/
│ ├── workstation_dev_wsl/ │ ├── workstation_dev_wsl/
@@ -37,8 +36,8 @@ infra/
## Managed machines ## Managed machines
The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora IoT LAN The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora IoT LAN
node, an Ubuntu server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied to node, a Rocky Linux 9 server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied
host names: to host names:
```text ```text
common user environment common user environment
@@ -54,8 +53,8 @@ common user environment
| `nymph` | Fedora | Desktop laptop | GNOME | | `nymph` | Fedora | Desktop laptop | GNOME |
| `deadalus` | Fedora WSL | Development workstation | — | | `deadalus` | Fedora WSL | Development workstation | — |
| `aegis` | Fedora IoT | Always-on LAN node | — | | `aegis` | Fedora IoT | Always-on LAN node | — |
| `prometheus` | Ubuntu | Server | — | | `prometheus` | Rocky Linux | Server | — |
| `atlas` | Rocky 9 | NAS | — | | `atlas` | Rocky Linux | NAS | — |
```text ```text
ikaros must be boring ikaros must be boring
@@ -101,9 +100,14 @@ That gives it Fedora packages through DNF, Docker from the official repository,
## Server ## Server
`prometheus` is the Ubuntu LTS server. It has no graphical environment and gets server-specific dotfiles and templates. `prometheus` is the Rocky Linux 9 server. It has no graphical environment and gets server-specific
dotfiles and templates. The profile provisions configuration only: it does not transfer data, start
the Compose stack, update DNS, or perform a cutover.
The server profile installs Ubuntu packages, Docker from the official repository, declared systemd services, UFW rules, and the server Compose stack. Syncthing ports `22000/tcp`, `22000/udp`, and `21027/udp` are opened; the Syncthing GUI is not directly opened in UFW. The server profile installs platform-specific packages, Docker CE from the official repository,
declared systemd services, the server Compose stack, and firewalld. The Rocky server excludes
Syncthing. Rocky bind mounts use private SELinux relabeling for application data while host system
files remain unchanged.
Server identity comes from `server_username`, `server_user_group`, and `server_user_home` in `ansible/inventory/group_vars/server.yml`. `server_username` defaults to `username`, but it can be overridden, for example: Server identity comes from `server_username`, `server_user_group`, and `server_user_home` in `ansible/inventory/group_vars/server.yml`. `server_username` defaults to `username`, but it can be overridden, for example:
@@ -114,6 +118,30 @@ ansible-playbook ansible/site.yml --limit prometheus \
-e server_user_home=/srv/myuser -e server_user_home=/srv/myuser
``` ```
The target must already provide `server_username` with local sudo access.
### Data migration
Provision Rocky first, then run the migration script **on the retired Ubuntu source host**. It is
dry-run by default and requires an explicit source-stack stop before it can copy PostgreSQL data:
```bash
sudo ./scripts/migrate_prometheus_data.sh \
--destination rocky@179.237.102.172 \
--identity /root/.ssh/id_ed25519
sudo ./scripts/migrate_prometheus_data.sh \
--destination rocky@179.237.102.172 \
--identity /root/.ssh/id_ed25519 \
--quiesce-source --execute
```
The script copies Navidrome, music, Nginx Proxy Manager, PostgreSQL and Gitea data. It does not
delete data, move Syncthing, copy `/home/git/.ssh`, start containers, update DNS, or perform a
cutover. The destination SSH host key must already be trusted and the destination account needs
passwordless sudo for `rsync`. It preserves ACLs but not extended attributes, so source SELinux labels
are not transferred; the Rocky Compose bind mounts apply their own `:Z` labels when containers start.
## Aegis ## Aegis
`aegis` is a Raspberry Pi 4 running Fedora IoT. Generate Ignition from `aegis` is a Raspberry Pi 4 running Fedora IoT. Generate Ignition from
@@ -238,7 +266,6 @@ ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --di
| Role | What it does | | Role | What it does |
| --- | --- | | --- | --- |
| `packages_void` | Installs packages on Void. | | `packages_void` | Installs packages on Void. |
| `packages_ubuntu` | Installs packages on Ubuntu. |
| `packages_fedora` | Installs packages on Fedora. | | `packages_fedora` | Installs packages on Fedora. |
| `packages_rocky` | Installs packages on Rocky Linux 9. | | `packages_rocky` | Installs packages on Rocky Linux 9. |
| `services_runit` | Manages runit services. | | `services_runit` | Manages runit services. |
@@ -259,25 +286,25 @@ ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --di
## What `site.yml` runs ## What `site.yml` runs
```text ```text
all -> dotfiles_common all except platform_rocky -> dotfiles_common
platform_void -> packages_void + services_runit platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_fedora -> packages_fedora + services_systemd platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd platform_rocky -> packages_rocky + services_systemd
role_aegis -> profile_aegis role_aegis -> profile_aegis
atlas -> profile_atlas atlas -> profile_atlas
rocky_server -> dotfiles_common + profile_server (after platform_rocky)
platform_fedora & role_personal_workstation -> profile_personal_workstation platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common workstation_dev_fedora -> profile_workstation_dev_common
workstation_dev_wsl -> profile_workstation_dev_wsl (after platform_fedora + workstation_dev_fedora) workstation_dev_wsl -> profile_workstation_dev_wsl (after platform_fedora + workstation_dev_fedora)
ubuntu_server -> packages_ubuntu + services_systemd + profile_server
``` ```
So, in practice: So, in practice:
- `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`. - `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`.
- `deadalus` gets the Fedora development layer followed by the WSL layer. - `deadalus` gets the Fedora development layer followed by the WSL layer.
- `ubuntu_server` configures `prometheus`. - `rocky_server` configures the Rocky 9 server, `prometheus`.
- `atlas` receives the Rocky platform layer and the NAS profile through SSH. - `atlas` receives the Rocky platform layer and the NAS profile through SSH.
- `aegis` receives only the immutable Fedora IoT profile through SSH; it does not receive - `aegis` receives only the immutable Fedora IoT profile through SSH; it does not receive
mutable Fedora package or common dotfile roles. mutable Fedora package or common dotfile roles.

View File

@@ -16,17 +16,39 @@ rocky_syncthing_archive_url: >-
~ '.tar.gz' ~ '.tar.gz'
}} }}
rocky_manage_openzfs_repo: false
rocky_manage_syncthing_binary: false
rocky_manage_docker_repo: false
rocky_manage_github_cli_repo: false
rocky_docker_repo_url: https://download.docker.com/linux/rhel/docker-ce.repo
rocky_docker_repo_file: /etc/yum.repos.d/docker-ce.repo
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: enabled_services:
- firewalld - firewalld
rocky_common_packages:
- bash-completion
- bat
- fzf
- git
- jq
- tree
- unzip
- vim-enhanced
- wget
- zip
- zoxide
rocky_packages_base: rocky_packages_base:
- cockpit
- dnf-plugins-core - dnf-plugins-core
- firewalld - firewalld
- nfs-utils
- openssh-server - openssh-server
- policycoreutils-python-utils
- rsync - rsync
- samba
- sudo - sudo
- zfs
rocky_profile_packages: []
rocky_docker_packages: []
rocky_docker_conflicting_packages: []

View File

@@ -0,0 +1,59 @@
---
rocky_manage_docker_repo: 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_docker_packages:
- containerd.io
- docker-buildx-plugin
- docker-ce
- docker-ce-cli
- docker-compose-plugin
rocky_docker_conflicting_packages:
- docker
- docker-client
- docker-client-latest
- docker-common
- docker-engine
- docker-latest
- docker-latest-logrotate
- docker-logrotate
- podman
- runc
enabled_services:
- firewalld
- docker
server_firewall_backend: firewalld
server_firewalld_zone: public
server_firewalld_services:
- ssh
server_firewalld_ports: []
server_firewalld_absent_ports:
- 22000/tcp
- 22000/udp
- 21027/udp
server_sshd_service_name: sshd
server_compose_selinux_mount_option: Z
server_syncthing_enabled: false

View File

@@ -25,6 +25,9 @@ profile_packages:
- rsync - rsync
server_dotfiles: server_dotfiles:
- src: .bashrc.d/20-editor-server.sh
dest: .bashrc.d/20-editor-server.sh
mode: "0644"
- src: .gnupg/gpg-agent.conf - src: .gnupg/gpg-agent.conf
dest: .gnupg/gpg-agent.conf dest: .gnupg/gpg-agent.conf
mode: "0600" mode: "0600"
@@ -50,6 +53,10 @@ server_templates:
no_log: true no_log: true
server_directories: server_directories:
- path: "{{ server_user_home }}/.bashrc.d"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: "{{ server_user_home }}/.gnupg" - path: "{{ server_user_home }}/.gnupg"
owner: "{{ server_username }}" owner: "{{ server_username }}"
group: "{{ server_user_group }}" group: "{{ server_user_group }}"
@@ -82,6 +89,17 @@ server_directories:
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- path: /srv/nextcloud
owner: root
group: root
mode: "0755"
- path: /srv/nextcloud/data
owner: root
group: root
mode: "0755"
server_syncthing_enabled: true
server_syncthing_directories:
- path: /opt/syncthing/config - path: /opt/syncthing/config
owner: root owner: root
group: root group: root
@@ -94,30 +112,6 @@ server_directories:
owner: "1000" owner: "1000"
group: "1000" group: "1000"
mode: "0755" 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: server_sshd_settings:
PermitRootLogin: "no" PermitRootLogin: "no"

View File

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

View File

@@ -26,6 +26,16 @@ atlas_zfs_dataset_icloud_photos: icloud_photos
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_manage_storage: false atlas_manage_storage: false
rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: true
host_packages:
- cockpit
- nfs-utils
- policycoreutils-python-utils
- samba
- zfs
atlas_nfs_exports: atlas_nfs_exports:
- path: "{{ atlas_work_mountpoint }}" - path: "{{ atlas_work_mountpoint }}"
clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)" clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)"

View File

@@ -1,5 +1,8 @@
--- ---
hostname: prometheus 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: [] server_username: rocky
host_enabled_services: []

View File

@@ -20,6 +20,8 @@ all:
platform_rocky: platform_rocky:
hosts: hosts:
atlas: atlas:
children:
rocky_server:
void: void:
children: children:
@@ -67,10 +69,6 @@ all:
nymph: nymph:
ansible_connection: local ansible_connection: local
ubuntu:
children:
ubuntu_server:
workstation: workstation:
children: children:
workstation_dev: workstation_dev:
@@ -82,7 +80,7 @@ all:
server: server:
children: children:
ubuntu_server: rocky_server:
workstation_dev_fedora: workstation_dev_fedora:
hosts: hosts:
@@ -94,7 +92,6 @@ all:
deadalus: deadalus:
ansible_connection: local ansible_connection: local
ubuntu_server: rocky_server:
hosts: hosts:
prometheus: prometheus:
ansible_connection: local

View File

@@ -32,6 +32,7 @@
ansible.builtin.dnf: ansible.builtin.dnf:
name: "{{ rocky_openzfs_release_rpm }}" name: "{{ rocky_openzfs_release_rpm }}"
state: present state: present
when: rocky_manage_openzfs_repo | bool
- name: Disable OpenZFS DKMS repository - name: Disable OpenZFS DKMS repository
tags: [packages, storage] tags: [packages, storage]
@@ -39,6 +40,7 @@
name: name:
- zfs - zfs
state: disabled state: disabled
when: rocky_manage_openzfs_repo | bool
- name: Enable OpenZFS kmod repository - name: Enable OpenZFS kmod repository
tags: [packages, storage] tags: [packages, storage]
@@ -46,6 +48,36 @@
name: name:
- zfs-kmod - zfs-kmod
state: enabled state: enabled
when: rocky_manage_openzfs_repo | bool
- name: Configure official Docker RPM repository
tags: [packages, docker]
ansible.builtin.get_url:
url: "{{ rocky_docker_repo_url }}"
dest: "{{ rocky_docker_repo_file }}"
owner: root
group: root
mode: "0644"
when: rocky_manage_docker_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: Remove packages conflicting with Docker CE
tags: [packages, docker]
ansible.builtin.dnf:
name: "{{ rocky_docker_conflicting_packages }}"
state: absent
when:
- rocky_manage_docker_repo | bool
- rocky_docker_conflicting_packages | length > 0
- name: Refresh Rocky package metadata - name: Refresh Rocky package metadata
tags: [packages] tags: [packages]
@@ -58,20 +90,30 @@
name: >- name: >-
{{ {{
( (
(common_packages | default([])) (rocky_common_packages | default([]))
+ (rocky_packages_base | default([])) + (rocky_packages_base | default([]))
+ (profile_packages | default([])) + (rocky_profile_packages | default([]))
+ (rocky_docker_packages | default([]))
+ (host_packages | default([])) + (host_packages | default([]))
) | unique ) | unique
}} }}
state: present state: present
- name: Add Rocky user to docker group
tags: [packages, docker]
ansible.builtin.user:
name: "{{ effective_username }}"
groups: docker
append: true
when: rocky_docker_packages | length > 0
- name: Require supported architecture for Syncthing - name: Require supported architecture for Syncthing
tags: [packages, syncthing] tags: [packages, syncthing]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- ansible_facts['architecture'] == 'x86_64' - ansible_facts['architecture'] == 'x86_64'
fail_msg: The pinned Atlas Syncthing binary currently supports x86_64 only. fail_msg: The pinned Atlas Syncthing binary currently supports x86_64 only.
when: rocky_manage_syncthing_binary | bool
- name: Read installed Syncthing version - name: Read installed Syncthing version
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -83,6 +125,7 @@
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false check_mode: false
when: rocky_manage_syncthing_binary | bool
- name: Determine whether Syncthing must be installed - name: Determine whether Syncthing must be installed
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -92,6 +135,7 @@
('syncthing v' ~ rocky_syncthing_version ~ ' ') ('syncthing v' ~ rocky_syncthing_version ~ ' ')
not in (rocky_syncthing_version_check.stdout | default('')) not in (rocky_syncthing_version_check.stdout | default(''))
}} }}
when: rocky_manage_syncthing_binary | bool
- name: Create temporary Syncthing extraction directory - name: Create temporary Syncthing extraction directory
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -101,7 +145,9 @@
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
when: rocky_syncthing_install_required | bool when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Download pinned Syncthing release - name: Download pinned Syncthing release
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -112,7 +158,9 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: rocky_syncthing_install_required | bool when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Extract pinned Syncthing release - name: Extract pinned Syncthing release
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -120,7 +168,9 @@
src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz" src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
dest: "/tmp/syncthing-{{ rocky_syncthing_version }}" dest: "/tmp/syncthing-{{ rocky_syncthing_version }}"
remote_src: true remote_src: true
when: rocky_syncthing_install_required | bool when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Install pinned Syncthing binary - name: Install pinned Syncthing binary
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -136,16 +186,20 @@
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
when: rocky_syncthing_install_required | bool when:
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Remove Syncthing release archive - name: Remove Syncthing release archive
tags: [packages, syncthing] tags: [packages, syncthing]
ansible.builtin.file: ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz" path: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
state: absent state: absent
when: rocky_manage_syncthing_binary | bool
- name: Remove Syncthing extraction directory - name: Remove Syncthing extraction directory
tags: [packages, syncthing] tags: [packages, syncthing]
ansible.builtin.file: ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}" path: "/tmp/syncthing-{{ rocky_syncthing_version }}"
state: absent state: absent
when: rocky_manage_syncthing_binary | bool

View File

@@ -1,82 +0,0 @@
---
- 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 Docker apt keyrings directory exists
tags: [packages]
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: "0755"
when: ubuntu_manage_docker_repo | default(false)
- name: Download Docker apt repository signing key
tags: [packages]
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
owner: root
group: root
mode: "0644"
when: ubuntu_manage_docker_repo | default(false)
- name: Configure Docker apt repository
tags: [packages]
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ 'amd64' if ansible_facts['architecture'] == 'x86_64'
else 'arm64' if ansible_facts['architecture'] in ['aarch64', 'arm64']
else ansible_facts['architecture'] }}
signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu
{{ ansible_facts['distribution_release'] }} stable
filename: docker
state: present
update_cache: true
when: ubuntu_manage_docker_repo | default(false)
- name: Refresh apt package cache
tags: [packages]
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install packages on Ubuntu
tags: [packages]
ansible.builtin.apt:
name: >-
{{
(
(common_packages | default([]))
+ (ubuntu_packages_base | default([]))
+ (ubuntu_docker_packages | default([]))
+ (profile_packages | default([]))
+ (
(ubuntu_emacs_packages | default([]))
if (emacs_enabled | default(false) | bool)
else []
)
+ (host_packages | default([]))
)
| 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

View File

@@ -2,5 +2,5 @@
- name: Reload SSH service - name: Reload SSH service
tags: [services] tags: [services]
ansible.builtin.service: ansible.builtin.service:
name: ssh name: "{{ server_sshd_service_name }}"
state: reloaded state: reloaded

View File

@@ -1,16 +1,12 @@
--- ---
- name: Copy server dotfiles - name: Require supported server firewall backend
tags: [dotfiles, dotfiles:server] tags: [services, packages]
ansible.builtin.copy: ansible.builtin.assert:
src: "{{ playbook_dir }}/../dotfiles/server/{{ item.src }}" that:
dest: "{{ server_user_home }}/{{ item.dest }}" - server_firewall_backend == 'firewalld'
owner: "{{ server_username }}" fail_msg: >-
group: "{{ server_user_group }}" server_firewall_backend must be firewalld for the Rocky server profile.
mode: "{{ item.mode }}"
loop: "{{ server_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Require server container secret variables - name: Require server container secret variables
tags: [dotfiles, dotfiles:server, services] tags: [dotfiles, dotfiles:server, services]
@@ -30,10 +26,30 @@
owner: "{{ item.owner }}" owner: "{{ item.owner }}"
group: "{{ item.group }}" group: "{{ item.group }}"
mode: "{{ item.mode }}" mode: "{{ item.mode }}"
loop: "{{ server_directories | default([]) }}" loop: >-
{{
(server_directories | default([]))
+ (
server_syncthing_directories | default([])
if server_syncthing_enabled | default(true) | bool
else []
)
}}
loop_control: loop_control:
label: "{{ item.path }}" label: "{{ item.path }}"
- name: Copy server dotfiles
tags: [dotfiles, dotfiles:server]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/server/{{ item.src }}"
dest: "{{ server_user_home }}/{{ item.dest }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "{{ item.mode }}"
loop: "{{ server_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Render server templates - name: Render server templates
tags: [dotfiles, dotfiles:server] tags: [dotfiles, dotfiles:server]
ansible.builtin.template: ansible.builtin.template:
@@ -68,33 +84,41 @@
notify: Reload SSH service notify: Reload SSH service
when: (server_sshd_allow_users | default([])) | length > 0 when: (server_sshd_allow_users | default([])) | length > 0
- name: Define effective server UFW rules - name: Allow server services through firewalld
tags: [services, packages] tags: [services, packages]
ansible.builtin.set_fact: ansible.posix.firewalld:
server_ufw_rules_effective: "{{ server_ufw_rules | default([]) }}" service: "{{ item }}"
zone: "{{ server_firewalld_zone }}"
- name: Apply server UFW rules
tags: [services, packages]
community.general.ufw:
rule: "{{ item.rule }}"
name: "{{ item.name | default(omit) }}"
port: "{{ item.port | default(omit) }}"
proto: "{{ item.proto | default(omit) }}"
from_ip: "{{ item.src | default(omit) }}"
to_ip: "{{ item.dest | default(omit) }}"
from_port: "{{ item.from_port | default(omit) }}"
direction: "{{ item.direction | default(omit) }}"
interface: "{{ item.interface | default(omit) }}"
interface_in: "{{ item.interface_in | default(omit) }}"
interface_out: "{{ item.interface_out | default(omit) }}"
route: "{{ item.route | default(omit) }}"
comment: "{{ item.comment | default(omit) }}"
loop: "{{ server_ufw_rules_effective }}"
loop_control:
label: "{{ item.name | default(item.port) }}"
- name: Enable UFW firewall on server
tags: [services, packages]
community.general.ufw:
state: enabled state: enabled
when: (server_ufw_rules_effective | default([])) | length > 0 permanent: true
immediate: true
loop: "{{ server_firewalld_services | default([]) }}"
loop_control:
label: "{{ item }}"
when: server_firewall_backend == 'firewalld'
- name: Allow server ports through firewalld
tags: [services, packages]
ansible.posix.firewalld:
port: "{{ item }}"
zone: "{{ server_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
loop: "{{ server_firewalld_ports | default([]) }}"
loop_control:
label: "{{ item }}"
when: server_firewall_backend == 'firewalld'
- name: Remove disabled server ports from firewalld
tags: [services, packages]
ansible.posix.firewalld:
port: "{{ item }}"
zone: "{{ server_firewalld_zone }}"
state: disabled
permanent: true
immediate: true
loop: "{{ server_firewalld_absent_ports | default([]) }}"
loop_control:
label: "{{ item }}"
when: server_firewall_backend == 'firewalld'

View File

@@ -85,6 +85,14 @@
roles: roles:
- profile_atlas - profile_atlas
- name: Configure Rocky Linux server
hosts: rocky_server
become: true
roles:
- dotfiles_common
- profile_server
- name: Configure personal workstation role on Fedora - name: Configure personal workstation role on Fedora
hosts: platform_fedora:&role_personal_workstation hosts: platform_fedora:&role_personal_workstation
become: true become: true
@@ -112,12 +120,3 @@
roles: roles:
- profile_workstation_dev_wsl - profile_workstation_dev_wsl
- name: Configure Ubuntu server
hosts: ubuntu_server
become: true
roles:
- packages_ubuntu
- services_systemd
- profile_server

View File

@@ -1,4 +1,5 @@
--- ---
{% set selinux_volume_option = server_compose_selinux_mount_option | default('') %}
version: "3.8" version: "3.8"
services: services:
@@ -14,8 +15,8 @@ services:
ND_ENABLETRANSCODING: "true" ND_ENABLETRANSCODING: "true"
volumes: volumes:
- "/opt/navidrome/data:/data" - "/opt/navidrome/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/music:/music:ro" - "/opt/music:/music:ro{{ ',' ~ selinux_volume_option if selinux_volume_option else '' }}"
user: "1000:1000" user: "1000:1000"
networks: networks:
- web - web
@@ -30,8 +31,8 @@ services:
- "443:443" - "443:443"
- "81:81" - "81:81"
volumes: volumes:
- "/opt/npm/data:/data" - "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/npm/letsencrypt:/etc/letsencrypt" - "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
networks: networks:
- web - web
- gitea - gitea
@@ -66,7 +67,7 @@ services:
POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}" POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}"
volumes: volumes:
- "/opt/postgres/data:/var/lib/postgresql/data" - "/opt/postgres/data:/var/lib/postgresql/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
networks: networks:
- web - web
gitea: gitea:
@@ -79,14 +80,15 @@ services:
networks: networks:
- gitea - gitea
volumes: volumes:
- /opt/gitea/data:/data - /opt/gitea/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /home/git/.ssh:/data/git/.ssh - /home/git/.ssh:/data/git/.ssh{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}
ports: ports:
- "3000:3000" - "3000:3000"
- "127.0.0.1:222:22" - "127.0.0.1:222:22"
{% if server_syncthing_enabled | default(true) | bool %}
syncthing: syncthing:
image: syncthing/syncthing:2 image: syncthing/syncthing:2
container_name: syncthing container_name: syncthing
@@ -95,14 +97,15 @@ services:
expose: expose:
- "8384" - "8384"
volumes: volumes:
- "/opt/syncthing/config:/var/syncthing" - "/opt/syncthing/config:/var/syncthing{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/srv/syncthing/data:/data" - "/srv/syncthing/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
ports: ports:
- "22000:22000/tcp" - "22000:22000/tcp"
- "22000:22000/udp" - "22000:22000/udp"
- "21027:21027/udp" - "21027:21027/udp"
networks: networks:
- web - web
{% endif %}
networks: networks:
web: web:

View File

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

View File

@@ -1,17 +0,0 @@
case "$(uname -r 2>/dev/null)" in
*[Mm]icrosoft*) ;;
*) return ;;
esac
command -v gpgconf >/dev/null 2>&1 || return
if tty -s; then
export GPG_TTY="$(tty)"
fi
gpgconf --launch gpg-agent >/dev/null 2>&1
export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
if [ -n "${GPG_TTY-}" ]; then
gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1
fi

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env sh
# Copy the persistent Docker data from the retired Ubuntu server to the Rocky
# replacement. Run this script on the Ubuntu source as root. It is a dry run
# unless --execute and --quiesce-source are both supplied. Extended attributes
# are deliberately not copied: Rocky must assign its own SELinux labels.
set -eu
SOURCE_COMPOSE_FILE=/opt/docker/server/docker-compose.yml
DESTINATION=
IDENTITY_FILE=
EXECUTE=false
QUIESCE_SOURCE=false
DATA_PATHS='
/opt/navidrome/data
/opt/music
/opt/npm/data
/opt/npm/letsencrypt
/opt/postgres/data
/opt/gitea/data
'
usage() {
cat <<'EOF'
Usage: sudo ./scripts/migrate_prometheus_data.sh --destination USER@HOST [options]
Copies persistent Navidrome, Nginx Proxy Manager, PostgreSQL and Gitea data to
the Rocky server with rsync. The destination Docker containers must be stopped.
Options:
--destination USER@HOST Rocky SSH destination (required).
--identity PATH SSH private key readable by root on the source host.
--source-compose PATH Source Compose file (default: /opt/docker/server/docker-compose.yml).
--quiesce-source Stop the source Compose stack before copying.
--execute Perform the transfer; otherwise only show changes.
-h, --help Show this help.
The script never deletes source data, destination-only files, containers, or
volumes. It intentionally excludes Syncthing and /home/git/.ssh.
EOF
}
fail() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
}
while [ "$#" -gt 0 ]; do
case "$1" in
--destination)
[ "$#" -ge 2 ] || fail '--destination requires USER@HOST'
DESTINATION=$2
shift 2
;;
--identity)
[ "$#" -ge 2 ] || fail '--identity requires a path'
IDENTITY_FILE=$2
shift 2
;;
--source-compose)
[ "$#" -ge 2 ] || fail '--source-compose requires a path'
SOURCE_COMPOSE_FILE=$2
shift 2
;;
--quiesce-source)
QUIESCE_SOURCE=true
shift
;;
--execute)
EXECUTE=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "unknown option: $1"
;;
esac
done
[ "$(id -u)" -eq 0 ] || fail 'run this script with sudo on the Ubuntu source host'
[ -n "$DESTINATION" ] || fail '--destination is required'
if [ -n "$IDENTITY_FILE" ]; then
[ -r "$IDENTITY_FILE" ] || fail "SSH identity is not readable: $IDENTITY_FILE"
case "$IDENTITY_FILE" in
*' '*|*"$(printf '\t')"*) fail 'SSH identity paths must not contain whitespace' ;;
esac
fi
if [ "$EXECUTE" = true ] && [ "$QUIESCE_SOURCE" != true ]; then
fail '--execute requires --quiesce-source to keep PostgreSQL data consistent'
fi
require_command rsync
require_command ssh
SSH_COMMAND='ssh -o BatchMode=yes'
if [ -n "$IDENTITY_FILE" ]; then
SSH_COMMAND="$SSH_COMMAND -i $IDENTITY_FILE"
fi
run_ssh() {
# shellcheck disable=SC2086
$SSH_COMMAND "$DESTINATION" "$@"
}
printf 'Destination: %s\n' "$DESTINATION"
printf 'Mode: %s\n' "$( [ "$EXECUTE" = true ] && printf execute || printf dry-run )"
printf 'Data paths:\n%s\n' "$DATA_PATHS"
run_ssh 'sudo -n true' || fail 'destination sudo must be passwordless for this transfer'
run_ssh 'sudo -n docker info >/dev/null' \
|| fail 'destination Docker daemon is unavailable'
if run_ssh 'sudo -n docker ps -q | grep -q .'; then
fail 'destination Docker containers must be stopped before migration'
fi
for path in $DATA_PATHS; do
[ -d "$path" ] || fail "source directory is missing: $path"
run_ssh "sudo -n test -d $path" || fail "destination directory is missing: $path"
done
if [ "$QUIESCE_SOURCE" = true ]; then
require_command docker
[ -f "$SOURCE_COMPOSE_FILE" ] || fail "source Compose file is missing: $SOURCE_COMPOSE_FILE"
if [ "$EXECUTE" = true ]; then
printf 'Stopping source Compose stack...\n'
docker compose -f "$SOURCE_COMPOSE_FILE" stop
else
printf 'Dry-run: source Compose stack would be stopped.\n'
fi
fi
for path in $DATA_PATHS; do
printf '\nSyncing %s\n' "$path"
if [ "$EXECUTE" = true ]; then
rsync -aHA --numeric-ids --itemize-changes --human-readable --partial \
--rsync-path='sudo -n rsync' -e "$SSH_COMMAND" "$path/" "$DESTINATION:$path/"
else
rsync -aHA --numeric-ids --itemize-changes --human-readable --partial --dry-run \
--rsync-path='sudo -n rsync' -e "$SSH_COMMAND" "$path/" "$DESTINATION:$path/"
fi
done
if [ "$EXECUTE" = true ]; then
printf '\nVerifying source-to-destination parity...\n'
for path in $DATA_PATHS; do
rsync -aHA --numeric-ids --itemize-changes --dry-run \
--rsync-path='sudo -n rsync' -e "$SSH_COMMAND" "$path/" "$DESTINATION:$path/"
done
printf '\nTransfer completed. Keep the source stack stopped until application validation on Rocky succeeds.\n'
else
printf '\nDry-run completed. Re-run with --quiesce-source --execute after reviewing the changes.\n'
fi