diff --git a/AGENTS.md b/AGENTS.md index b44f523..e96f268 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # 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 - 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` - Void desktop profile is also the base for other future/reference hosts via `platform_void + graphical_desktop` - Workstation: `deadalus` is Windows + Fedora WSL. -- Ubuntu server: `prometheus` +- 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. @@ -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` - 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 --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: @@ -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_minimal_packages` — applications, integration components, and the `emptty` display manager. - `desktop_sway_packages` — binaries specific to the Sway session. -`profile_packages` in the same file is cross-distro and is overridden by `group_vars/server.yml` and the workstation group vars; do not move desktop-specific Void entries through it. +`profile_packages` remains the shared package bucket for Void and Fedora profiles. Rocky uses +`rocky_profile_packages` so RPM-specific names do not leak back into the other platforms; do not move +desktop-specific Void entries through either bucket. The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-independent content and `desktop_minimal_dotfiles` carries Thunar, Udiskie, and MIME defaults. `desktop_void_dotfiles` remains reserved for files that need the Void runtime. ## Workstation Notes @@ -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. - 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` 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=`; diff --git a/README.it.md b/README.it.md index 6597bee..3c4619e 100644 --- a/README.it.md +++ b/README.it.md @@ -33,7 +33,6 @@ infra/ │ ├── common/ │ ├── desktop/ │ ├── fedora/ -│ ├── ubuntu/ │ ├── server/ │ ├── workstation/ │ ├── workstation_dev_wsl/ @@ -56,8 +55,8 @@ Il repository è diviso in due componenti principali: # Macchine gestite -Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Ubuntu e -un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti: +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 @@ -74,8 +73,8 @@ Matrice target: | ikaros | Fedora | Personal workstation | GNOME | | nymph | Fedora | Desktop laptop | GNOME | | deadalus | Fedora WSL | Workstation dev | — | -| prometheus | Ubuntu | Server | — | -| atlas | Rocky 9 | NAS | — | +| prometheus | Rocky Linux | Server | — | +| atlas | Rocky Linux | NAS | — | 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 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 ## Desktop @@ -164,7 +163,7 @@ Le applicazioni Windows sono installate e gestite manualmente; il profilo WSL no Sistema operativo: -- Ubuntu LTS +- Rocky Linux 9 Configurazione: @@ -178,12 +177,19 @@ Profilo orientato a servizi server e gestione di dotfiles dedicati. 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 - abilitazione dei servizi systemd dichiarati in inventory/group vars - copia dei dotfiles server e rendering dei template server, incluso il `docker-compose.yml` dello stack servizi -- attivazione del firewall UFW con regola SSH esplicita -- apertura delle porte Syncthing `22000/tcp`, `22000/udp` e `21027/udp`, lasciando la GUI non esposta direttamente su UFW +- attivazione di firewalld con servizio SSH esplicitamente abilitato +- 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: @@ -274,7 +280,6 @@ I principali ruoli attualmente presenti sono: | ------------------------- | ----------------------------------- | | base | configurazione base comune | | packages_void | installazione pacchetti su Void | -| packages_ubuntu | installazione pacchetti su Ubuntu | | packages_fedora | installazione pacchetti su Fedora | | packages_rocky | installazione pacchetti su Rocky Linux 9 | | services_runit | gestione servizi runit | @@ -309,7 +314,7 @@ 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) -ubuntu_server -> packages_ubuntu + services_systemd + profile_server +rocky_server -> dotfiles_common + profile_server (dopo platform_rocky) ``` Questo significa che, allo stato attuale: @@ -318,9 +323,9 @@ Questo significa che, allo stato attuale: - `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 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 -- 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 @@ -332,7 +337,6 @@ dotfiles/ ├── desktop ├── server ├── fedora -├── ubuntu ├── workstation ├── workstation_dev_wsl └── 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 & 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 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`, quindi il profilo NAS dedicato +- 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 diff --git a/README.md b/README.md index 478ae0a..0257eaa 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ infra/ │ ├── common/ │ ├── desktop/ │ ├── fedora/ -│ ├── ubuntu/ │ ├── server/ │ ├── workstation/ │ ├── workstation_dev_wsl/ @@ -37,8 +36,8 @@ infra/ ## Managed machines 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 -host names: +node, a Rocky Linux 9 server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied +to host names: ```text common user environment @@ -54,8 +53,8 @@ common user environment | `nymph` | Fedora | Desktop laptop | GNOME | | `deadalus` | Fedora WSL | Development workstation | — | | `aegis` | Fedora IoT | Always-on LAN node | — | -| `prometheus` | Ubuntu | Server | — | -| `atlas` | Rocky 9 | NAS | — | +| `prometheus` | Rocky Linux | Server | — | +| `atlas` | Rocky Linux | NAS | — | ```text ikaros must be boring @@ -101,9 +100,14 @@ That gives it Fedora packages through DNF, Docker from the official repository, ## 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: @@ -114,6 +118,30 @@ ansible-playbook ansible/site.yml --limit prometheus \ -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` 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 | | --- | --- | | `packages_void` | Installs packages on Void. | -| `packages_ubuntu` | Installs packages on Ubuntu. | | `packages_fedora` | Installs packages on Fedora. | | `packages_rocky` | Installs packages on Rocky Linux 9. | | `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 ```text -all -> dotfiles_common +all except platform_rocky -> dotfiles_common platform_void -> packages_void + services_runit platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host platform_fedora -> packages_fedora + services_systemd platform_rocky -> packages_rocky + services_systemd role_aegis -> profile_aegis atlas -> profile_atlas +rocky_server -> dotfiles_common + profile_server (after platform_rocky) platform_fedora & role_personal_workstation -> profile_personal_workstation platform_fedora & desktop_gnome -> profile_desktop_gnome workstation_dev_fedora -> profile_workstation_dev_common workstation_dev_wsl -> profile_workstation_dev_wsl (after platform_fedora + workstation_dev_fedora) -ubuntu_server -> packages_ubuntu + services_systemd + profile_server ``` So, in practice: - `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`. - `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. - `aegis` receives only the immutable Fedora IoT profile through SSH; it does not receive mutable Fedora package or common dotfile roles. diff --git a/ansible/inventory/group_vars/platform_rocky.yml b/ansible/inventory/group_vars/platform_rocky.yml index 3ea84da..49ef00a 100644 --- a/ansible/inventory/group_vars/platform_rocky.yml +++ b/ansible/inventory/group_vars/platform_rocky.yml @@ -16,17 +16,39 @@ rocky_syncthing_archive_url: >- ~ '.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: - firewalld +rocky_common_packages: + - bash-completion + - bat + - fzf + - git + - jq + - tree + - unzip + - vim-enhanced + - wget + - zip + - zoxide + rocky_packages_base: - - cockpit - dnf-plugins-core - firewalld - - nfs-utils - openssh-server - - policycoreutils-python-utils - rsync - - samba - sudo - - zfs + +rocky_profile_packages: [] +rocky_docker_packages: [] +rocky_docker_conflicting_packages: [] diff --git a/ansible/inventory/group_vars/rocky_server.yml b/ansible/inventory/group_vars/rocky_server.yml new file mode 100644 index 0000000..6cd2c13 --- /dev/null +++ b/ansible/inventory/group_vars/rocky_server.yml @@ -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 diff --git a/ansible/inventory/group_vars/server.yml b/ansible/inventory/group_vars/server.yml index cbdeada..c5311a4 100644 --- a/ansible/inventory/group_vars/server.yml +++ b/ansible/inventory/group_vars/server.yml @@ -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" @@ -50,6 +53,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 }}" @@ -82,6 +89,17 @@ server_directories: owner: root group: root 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 owner: root group: root @@ -94,30 +112,6 @@ 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" diff --git a/ansible/inventory/group_vars/ubuntu.yml b/ansible/inventory/group_vars/ubuntu.yml deleted file mode 100644 index 56877ca..0000000 --- a/ansible/inventory/group_vars/ubuntu.yml +++ /dev/null @@ -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 diff --git a/ansible/inventory/host_vars/atlas.yml b/ansible/inventory/host_vars/atlas.yml index 7e6caec..6f3c86d 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -26,6 +26,16 @@ atlas_zfs_dataset_icloud_photos: icloud_photos atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT 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: - path: "{{ atlas_work_mountpoint }}" clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)" diff --git a/ansible/inventory/host_vars/prometheus.yml b/ansible/inventory/host_vars/prometheus.yml index fd1b67c..f9496dd 100644 --- a/ansible/inventory/host_vars/prometheus.yml +++ b/ansible/inventory/host_vars/prometheus.yml @@ -1,5 +1,8 @@ --- 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 diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml index 664d83d..92c65a0 100644 --- a/ansible/inventory/hosts.yml +++ b/ansible/inventory/hosts.yml @@ -20,6 +20,8 @@ all: platform_rocky: hosts: atlas: + children: + rocky_server: void: children: @@ -67,10 +69,6 @@ all: nymph: ansible_connection: local - ubuntu: - children: - ubuntu_server: - workstation: children: workstation_dev: @@ -82,7 +80,7 @@ all: server: children: - ubuntu_server: + rocky_server: workstation_dev_fedora: hosts: @@ -94,7 +92,6 @@ all: deadalus: ansible_connection: local - ubuntu_server: + rocky_server: hosts: prometheus: - ansible_connection: local diff --git a/ansible/roles/packages_rocky/tasks/main.yml b/ansible/roles/packages_rocky/tasks/main.yml index b6a456c..4bfa28d 100644 --- a/ansible/roles/packages_rocky/tasks/main.yml +++ b/ansible/roles/packages_rocky/tasks/main.yml @@ -32,6 +32,7 @@ ansible.builtin.dnf: name: "{{ rocky_openzfs_release_rpm }}" state: present + when: rocky_manage_openzfs_repo | bool - name: Disable OpenZFS DKMS repository tags: [packages, storage] @@ -39,6 +40,7 @@ name: - zfs state: disabled + when: rocky_manage_openzfs_repo | bool - name: Enable OpenZFS kmod repository tags: [packages, storage] @@ -46,6 +48,36 @@ name: - zfs-kmod 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 tags: [packages] @@ -58,20 +90,30 @@ name: >- {{ ( - (common_packages | default([])) + (rocky_common_packages | default([])) + (rocky_packages_base | default([])) - + (profile_packages | default([])) + + (rocky_profile_packages | default([])) + + (rocky_docker_packages | default([])) + (host_packages | default([])) ) | unique }} 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 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] @@ -83,6 +125,7 @@ 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] @@ -92,6 +135,7 @@ ('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] @@ -101,7 +145,9 @@ owner: root group: root mode: "0755" - when: rocky_syncthing_install_required | bool + when: + - rocky_manage_syncthing_binary | bool + - rocky_syncthing_install_required | bool - name: Download pinned Syncthing release tags: [packages, syncthing] @@ -112,7 +158,9 @@ owner: root group: root mode: "0644" - when: rocky_syncthing_install_required | bool + when: + - rocky_manage_syncthing_binary | bool + - rocky_syncthing_install_required | bool - name: Extract pinned Syncthing release tags: [packages, syncthing] @@ -120,7 +168,9 @@ src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz" dest: "/tmp/syncthing-{{ rocky_syncthing_version }}" 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 tags: [packages, syncthing] @@ -136,16 +186,20 @@ owner: root group: root mode: "0755" - when: rocky_syncthing_install_required | bool + 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 diff --git a/ansible/roles/packages_ubuntu/tasks/main.yml b/ansible/roles/packages_ubuntu/tasks/main.yml deleted file mode 100644 index 760b83e..0000000 --- a/ansible/roles/packages_ubuntu/tasks/main.yml +++ /dev/null @@ -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 diff --git a/ansible/roles/profile_server/handlers/main.yml b/ansible/roles/profile_server/handlers/main.yml index ab5516d..dd7e309 100644 --- a/ansible/roles/profile_server/handlers/main.yml +++ b/ansible/roles/profile_server/handlers/main.yml @@ -2,5 +2,5 @@ - name: Reload SSH service tags: [services] ansible.builtin.service: - name: ssh + name: "{{ server_sshd_service_name }}" state: reloaded diff --git a/ansible/roles/profile_server/tasks/main.yml b/ansible/roles/profile_server/tasks/main.yml index 8cb36e2..b302e6d 100644 --- a/ansible/roles/profile_server/tasks/main.yml +++ b/ansible/roles/profile_server/tasks/main.yml @@ -1,16 +1,12 @@ --- -- 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: Require supported server firewall backend + tags: [services, packages] + ansible.builtin.assert: + that: + - server_firewall_backend == 'firewalld' + fail_msg: >- + server_firewall_backend must be firewalld for the Rocky server profile. - name: Require server container secret variables tags: [dotfiles, dotfiles:server, services] @@ -30,10 +26,30 @@ owner: "{{ item.owner }}" group: "{{ item.group }}" 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: 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 tags: [dotfiles, dotfiles:server] ansible.builtin.template: @@ -68,33 +84,41 @@ notify: Reload SSH service when: (server_sshd_allow_users | default([])) | length > 0 -- name: Define effective server UFW rules +- name: Allow server services through firewalld tags: [services, packages] - ansible.builtin.set_fact: - server_ufw_rules_effective: "{{ server_ufw_rules | default([]) }}" - -- name: Apply server UFW rules - tags: [services, packages] - community.general.ufw: - rule: "{{ item.rule }}" - name: "{{ item.name | default(omit) }}" - port: "{{ item.port | default(omit) }}" - proto: "{{ item.proto | default(omit) }}" - from_ip: "{{ item.src | default(omit) }}" - to_ip: "{{ item.dest | default(omit) }}" - from_port: "{{ item.from_port | default(omit) }}" - direction: "{{ item.direction | default(omit) }}" - interface: "{{ item.interface | default(omit) }}" - interface_in: "{{ item.interface_in | default(omit) }}" - interface_out: "{{ item.interface_out | default(omit) }}" - route: "{{ item.route | default(omit) }}" - comment: "{{ item.comment | default(omit) }}" - loop: "{{ server_ufw_rules_effective }}" - loop_control: - label: "{{ item.name | default(item.port) }}" - -- name: Enable UFW firewall on server - tags: [services, packages] - community.general.ufw: + ansible.posix.firewalld: + service: "{{ item }}" + zone: "{{ server_firewalld_zone }}" state: enabled - when: (server_ufw_rules_effective | default([])) | length > 0 + permanent: true + immediate: true + loop: "{{ server_firewalld_services | default([]) }}" + loop_control: + label: "{{ item }}" + when: server_firewall_backend == 'firewalld' + +- name: Allow server ports through firewalld + tags: [services, packages] + ansible.posix.firewalld: + port: "{{ item }}" + zone: "{{ server_firewalld_zone }}" + state: enabled + permanent: true + immediate: true + loop: "{{ server_firewalld_ports | default([]) }}" + loop_control: + label: "{{ item }}" + when: server_firewall_backend == 'firewalld' + +- 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' diff --git a/ansible/site.yml b/ansible/site.yml index 8498440..3a25282 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -85,6 +85,14 @@ roles: - profile_atlas +- name: Configure Rocky Linux server + hosts: rocky_server + become: true + + roles: + - dotfiles_common + - profile_server + - name: Configure personal workstation role on Fedora hosts: platform_fedora:&role_personal_workstation become: true @@ -112,12 +120,3 @@ roles: - profile_workstation_dev_wsl - -- name: Configure Ubuntu server - hosts: ubuntu_server - become: true - - roles: - - packages_ubuntu - - services_systemd - - profile_server diff --git a/ansible/templates/server/docker-compose.yml.j2 b/ansible/templates/server/docker-compose.yml.j2 index f75cda7..d27ed62 100644 --- a/ansible/templates/server/docker-compose.yml.j2 +++ b/ansible/templates/server/docker-compose.yml.j2 @@ -1,4 +1,5 @@ --- +{% set selinux_volume_option = server_compose_selinux_mount_option | default('') %} version: "3.8" services: @@ -14,8 +15,8 @@ services: ND_ENABLETRANSCODING: "true" volumes: - - "/opt/navidrome/data:/data" - - "/opt/music:/music:ro" + - "/opt/navidrome/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" + - "/opt/music:/music:ro{{ ',' ~ selinux_volume_option if selinux_volume_option else '' }}" user: "1000:1000" networks: - web @@ -30,8 +31,8 @@ services: - "443:443" - "81:81" volumes: - - "/opt/npm/data:/data" - - "/opt/npm/letsencrypt:/etc/letsencrypt" + - "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" + - "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" networks: - web - gitea @@ -66,7 +67,7 @@ services: POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}" volumes: - - "/opt/postgres/data:/var/lib/postgresql/data" + - "/opt/postgres/data:/var/lib/postgresql/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" networks: - web gitea: @@ -79,14 +80,15 @@ services: networks: - gitea volumes: - - /opt/gitea/data:/data + - /opt/gitea/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }} - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro - - /home/git/.ssh:/data/git/.ssh + - /home/git/.ssh:/data/git/.ssh{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }} ports: - "3000:3000" - "127.0.0.1:222:22" +{% if server_syncthing_enabled | default(true) | bool %} syncthing: image: syncthing/syncthing:2 container_name: syncthing @@ -95,14 +97,15 @@ services: expose: - "8384" volumes: - - "/opt/syncthing/config:/var/syncthing" - - "/srv/syncthing/data:/data" + - "/opt/syncthing/config:/var/syncthing{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" + - "/srv/syncthing/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" ports: - "22000:22000/tcp" - "22000:22000/udp" - "21027:21027/udp" networks: - web +{% endif %} networks: web: diff --git a/dotfiles/common/.config/ai/knowledge/server.md b/dotfiles/common/.config/ai/knowledge/server.md index 0b6e5b5..9d6e4a3 100644 --- a/dotfiles/common/.config/ai/knowledge/server.md +++ b/dotfiles/common/.config/ai/knowledge/server.md @@ -1,5 +1,5 @@ # Server -- Ubuntu minimal server +- Rocky Linux 9 server - Self-hosting services - Low attack surface diff --git a/dotfiles/ubuntu/.bashrc.d/20-editor-ubuntu.sh b/dotfiles/server/.bashrc.d/20-editor-server.sh similarity index 100% rename from dotfiles/ubuntu/.bashrc.d/20-editor-ubuntu.sh rename to dotfiles/server/.bashrc.d/20-editor-server.sh diff --git a/dotfiles/ubuntu/.bashrc.d/30-gpg-agent-wsl.sh b/dotfiles/ubuntu/.bashrc.d/30-gpg-agent-wsl.sh deleted file mode 100644 index 60164ae..0000000 --- a/dotfiles/ubuntu/.bashrc.d/30-gpg-agent-wsl.sh +++ /dev/null @@ -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 diff --git a/scripts/migrate_prometheus_data.sh b/scripts/migrate_prometheus_data.sh new file mode 100644 index 0000000..7339829 --- /dev/null +++ b/scripts/migrate_prometheus_data.sh @@ -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