diff --git a/README.md b/README.md new file mode 100644 index 0000000..db5958c --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# homelab-ansible + +Ansible playbook to align my homelab VMs — Alpine & Debian. + +## Roles + +| # | Role | Purpose | +| --- | --------- | ------------------------------------------------------------- | +| 1 | `ufw` | Install UFW, deny incoming by default, allow only Tailscale | +| 2 | `repos` | Set Alpine repos to `edge` | +| 3 | `upgrade` | Upgrade all packages (handles Tailscale SSH drops gracefully) | + +## Usage + +```sh +# Full playbook +ansible-playbook homelab.yml + +# Single role +ansible-playbook homelab.yml --tags ufw + +# Dry-run +ansible-playbook homelab.yml --check --diff +``` + +## Inventory + +- `inventory/tailscale_inventory.py` — dynamic inventory from local `tailscale status`, filtered by `tag:homelab` +- `inventory/static_groups.yml` — static group assignments (Alpine vs Debian) + +## Vars + +Per-role defaults live in `roles/*/defaults/main.yml`. Per-group overrides in `group_vars/`. diff --git a/roles/upgrade/README.md b/roles/upgrade/README.md new file mode 100644 index 0000000..5f54fc9 --- /dev/null +++ b/roles/upgrade/README.md @@ -0,0 +1,55 @@ +# Upgrade role + +Upgrades all packages using the native package manager (`apk` or `apt`). + +## Problem + +Tailscale may be updated during the upgrade. When its service restarts, the SSH tunnel drops and Ansible loses connectivity, causing a fatal `UNREACHABLE`. + +## Solution — async + poll + +Instead of a blocking upgrade, we fire the job in the background and poll for completion, tolerating temporary unreachability. + +```mermaid +sequenceDiagram + participant A as Ansible + participant T as Target + + A->>T: apk upgrade (async, poll: 0) + T-->>A: job ID: 42 + Note over T: 🔄 upgrade en cours... + A->>T: async_status jid=42 + T-->>A: finished: false + Note over A: FAILED-RETRYING (normal) + A-->>A: sleep 10s + A->>T: async_status jid=42 + T-->>A: finished: false + Note over A: FAILED-RETRYING (normal) + A-->>A: sleep 10s + A->>T: async_status jid=42 + T-->>A: finished: true ✅ + Note over A: ok +``` + +- **`async: 3600` + `poll: 0`** — fire and forget. Ansible gets a job ID and moves on. +- **`async_status` with `until: finished`** — polls every 10s for up to 60 retries (~10 min). +- **`ignore_unreachable: true`** — if Tailscale dropped SSH, Ansible doesn't crash. It retries until the host is back. +- The `FAILED - RETRYING` messages are normal — they just mean the `until` condition isn't met yet. + +## Idempotency + +A task with `async` + `poll: 0` always reports `changed: true` by default, even when nothing happened. To fix this: + +```mermaid +graph LR + A["Fire: changed_when: false"] --> B["Package manager runs in background"] + B --> C["Wait: polls every 10s"] + C -->|finished| D["async_status reads real changed value"] + D -->|"packages updated"| E["changed ✅"] + D -->|"nothing to do"| F["ok"] +``` + +- The **fire** task has `changed_when: false` — it never claims change. +- The **wait** task reads `changed_when: async_result.changed` — it propagates the real status from the package manager once the job finishes. + +Second run with nothing to upgrade → `changed=0` on all hosts.