Compare commits

..

21 Commits

Author SHA1 Message Date
Damien
b5c8c35534 Add ipfabric to excluded hosts 2026-06-21 14:28:39 +02:00
Damien
5f2d368a97 Remove ipfabric from root_hosts and add pve01 to excluded 2026-06-14 16:33:56 +02:00
Damien
7916ec86b7 Add tags to gather facts task 2026-06-14 12:10:18 +02:00
Damien
c86df61152 Add host exclusion and improve UFW task reliability
Add `excluded` host group and skip offline hosts
Fix UFW service initialization command for Alpine Linux
2026-06-13 10:17:13 +02:00
Damien
77869f0e50 Add ipfabric group 2026-06-12 12:54:33 +02:00
Damien
e34cb4adb1 Add pve01 to static inventory groups 2026-06-12 12:49:41 +02:00
Damien
01cb285d54 Add damien_hosts group to static inventory 2026-06-12 12:32:32 +02:00
Damien
edfab0c61c Add damien_hosts group with Ansible config 2026-06-12 12:31:35 +02:00
f5fca9171a Merge pull request 'Add initial project structure and documentation' (#6) from chore/documentation into main
Reviewed-on: #6
2026-06-02 06:16:33 +00:00
75a84f7724 Merge pull request 'Rename async job variables for consistency' (#5) from fix/basic_tag_violation into main
Reviewed-on: #5
2026-06-02 06:16:01 +00:00
Damien
6b9fccb1ea Rename async job variables for consistency
Rename async job variables in UFW and upgrade roles to follow a
consistent
naming pattern (upgrade_<os>_job) for better maintainability.
2026-06-02 08:13:05 +02:00
Damien
462855f077 Add initial project structure and documentation 2026-06-01 11:31:31 +02:00
21019fa6ea Merge pull request 'feat/upgrade' (#4) from feat/upgrade into main
Reviewed-on: #4
2026-06-01 09:27:46 +00:00
Damien
5d567d1536 Add async pattern for package upgrades with unreachable tolerance 2026-06-01 11:27:27 +02:00
Damien
2639c5e872 Add upgrade tasks for Alpine and Debian systems 2026-06-01 10:55:15 +02:00
994337fc9a Merge pull request 'Set Alpine repositories to edge' (#3) from feat/update_repo into main
Reviewed-on: #3
2026-06-01 08:53:23 +00:00
Damien
217c378fb3 Set Alpine repositories to edge 2026-06-01 10:52:25 +02:00
6e33ac688c Merge pull request 'feat/ufw_implementation' (#2) from feat/ufw_implementation into main
Reviewed-on: #2
2026-06-01 08:49:12 +00:00
Damien
5768492bd3 Update UFW rc-update changed condition 2026-06-01 10:48:05 +02:00
Damien
38e73c9c96 Add UFW installation and configuration tasks 2026-06-01 10:45:59 +02:00
Damien
19c5371601 Update homelab playbook and UFW role
Change hosts target from online to managed
Add tags to UFW role tasks
Set default UFW policies
Allow traffic on Tailscale interface
2026-06-01 10:21:18 +02:00
9 changed files with 246 additions and 9 deletions

33
README.md Normal file
View File

@@ -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/`.

View File

@@ -0,0 +1,5 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/vars.json
---
ansible_user: damien
ansible_become: true
ansible_become_method: sudo

View File

@@ -1,11 +1,32 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/playbook.json
---
- name: Homelab alignment
hosts: online
hosts: managed:!excluded
become: true
gather_facts: false
environment:
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
pre_tasks:
- name: Test connectivity
ansible.builtin.ping:
ignore_unreachable: true
register: ping_result
- name: Skip offline hosts
ansible.builtin.meta: end_host
when: ping_result.unreachable | default(false)
- name: Gather facts
ansible.builtin.setup:
tags: always
roles:
- role: repos
- role: common
- role: ufw
tags: ufw
- role: repos
tags: repos
- role: upgrade
tags: upgrade

View File

@@ -5,25 +5,29 @@ all:
children:
root_hosts:
hosts:
ipfabric:
grafana:
gitea:
prometheus:
prometheus-pve-exporter:
gitea-runner:
openbao:
komodo:
pve01:
debian_hosts:
hosts:
proxy:
damien_hosts:
hosts:
komodo:
managed:
children:
root_hosts:
debian_hosts:
damien_hosts:
excluded:
children:
hosts:
pve01:
ipfabric:

View File

@@ -0,0 +1,13 @@
---
# roles/repos/tasks/main.yml
- name: Set Alpine repositories to edge
ansible.builtin.copy:
dest: /etc/apk/repositories
content: |
http://dl-cdn.alpinelinux.org/alpine/edge/main
http://dl-cdn.alpinelinux.org/alpine/edge/community
owner: root
group: root
mode: "0644"
when: ansible_facts['os_family'] == "Alpine"

View File

@@ -1,4 +1,5 @@
# roles/ufw/defaults/main.yml
---
ufw_default_incoming_policy: deny
ufw_default_outgoing_policy: allow
ufw_tailscale_interface: tailscale0
ufw_extra_rules: []

View File

@@ -0,0 +1,46 @@
---
# roles/ufw/tasks/main.yml
- name: Install UFW (Alpine)
community.general.apk:
name: ufw
state: present
update_cache: true
when: ansible_facts['os_family'] == "Alpine"
- name: Install UFW (Debian)
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
when: ansible_facts['os_family'] == "Debian"
- name: Add UFW to default runlevel (Alpine)
ansible.builtin.command:
cmd: /sbin/rc-update add ufw default
register: ufw_rc_update_result
changed_when: >- # " >- : transform multiple lines into one line"
'already' not in ufw_rc_update_result.stdout
and 'already' not in ufw_rc_update_result.stderr
failed_when: ufw_rc_update_result.rc != 0
when: ansible_facts['os_family'] == "Alpine"
- name: Set default incoming policy
community.general.ufw:
default: "{{ ufw_default_incoming_policy }}"
direction: incoming
- name: Set default outgoing policy
community.general.ufw:
default: "{{ ufw_default_outgoing_policy }}"
direction: outgoing
- name: Enable UFW
community.general.ufw:
state: enabled
- name: Allow all traffic on Tailscale interface
community.general.ufw:
rule: allow
interface: "{{ ufw_tailscale_interface }}"
direction: in

55
roles/upgrade/README.md Normal file
View File

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

View File

@@ -0,0 +1,59 @@
---
# roles/upgrade/tasks/main.yml
#
# Async pattern: Tailscale may restart during upgrade, dropping SSH.
# Fire and forget → poll status with unreachable tolerance + retries.
- name: Upgrade all packages (Alpine)
community.general.apk:
upgrade: true
update_cache: true
async: 3600
poll: 0
register: upgrade_alpine_job
# By default async job return change: true
# Force adapting the change condition for idempotency
changed_when: false
when: ansible_facts['os_family'] == "Alpine"
- name: Wait for Alpine upgrade
ansible.builtin.async_status:
jid: "{{ upgrade_alpine_job.ansible_job_id }}"
register: upgrade_alpine_result
until: upgrade_alpine_result.finished
retries: 60
delay: 10
# By default async job return change: true
# Force adapting the change condition for idempotency
changed_when: upgrade_alpine_result.changed | default(false)
ignore_unreachable: true
when:
- ansible_facts['os_family'] == "Alpine"
- upgrade_alpine_job.ansible_job_id is defined
- name: Upgrade all packages (Debian)
ansible.builtin.apt:
upgrade: full
update_cache: true
async: 3600
poll: 0
register: upgrade_debian_job
# By default async job return change: true
# Force adapting the change condition for idempotency
changed_when: false
when: ansible_facts['os_family'] == "Debian"
- name: Wait for Debian upgrade
ansible.builtin.async_status:
jid: "{{ upgrade_debian_job.ansible_job_id }}"
register: upgrade_debian_result
until: upgrade_debian_result.finished
retries: 60
delay: 10
# By default async job return change: true
# Force adapting the change condition for idempotency
changed_when: upgrade_debian_result.changed | default(false)
ignore_unreachable: true
when:
- ansible_facts['os_family'] == "Debian"
- upgrade_debian_job.ansible_job_id is defined