Compare commits

..

3 commits

6 changed files with 157 additions and 0 deletions

30
ansible/README.md Normal file
View file

@ -0,0 +1,30 @@
# Ansible
Run from David's Linux PC over SSH — nothing is installed on the managed hosts.
Run all commands from inside this `ansible/` directory so `ansible.cfg` is picked up.
## Prerequisites
- `sudo pacman -S ansible`
- One SSH key per client device (`~/.ssh/id_ed25519_homelab`), copied to each host with `ssh-copy-id`.
- A `~/.ssh/config` entry per host (`User`, `IdentityFile`, `IdentitiesOnly yes`). The inventory
only lists host names, so these must match the `Host` lines exactly.
## Hosts
| Group | Host | Notes |
|---|---|---|
| `octoprint` | `octopi.home.staffenberger.at` | RPi4, IoT VLAN — the learning guinea pig |
## Usage
```bash
ansible octoprint -m ping # connectivity check
ansible-playbook playbooks/update.yml --list-hosts # which hosts would this touch?
ansible-playbook playbooks/update.yml --check --diff # dry run
ansible-playbook playbooks/update.yml # apt upgrade + reboot if required
ansible-playbook playbooks/octoprint.yml --check --diff # dry run of the Pi's configuration
ansible-playbook playbooks/octoprint.yml # apply the Pi's configuration (after changes or a fresh flash)
```
Add `-K` if a play fails with "Missing sudo password".

6
ansible/ansible.cfg Normal file
View file

@ -0,0 +1,6 @@
[defaults]
inventory = inventory.yml
# Use whatever Python the target has without printing a discovery warning each run
interpreter_python = auto_silent
# Opt in to the future default now: facts only via ansible_facts['...'], not ansible_* vars
inject_facts_as_vars = False

7
ansible/inventory.yml Normal file
View file

@ -0,0 +1,7 @@
# Host names match the `Host` entries in ~/.ssh/config, which supply the user
# and key — so no connection details (or secrets) live in this repo.
all:
children:
octoprint:
hosts:
octopi.home.staffenberger.at:

View file

@ -0,0 +1,63 @@
# OS-level configuration of the OctoPrint Pi, on top of a stock OctoPi image.
# OctoPrint's own settings/plugins/profiles come from its Backup & Restore zip,
# not from here (OctoPrint rewrites its own config.yaml) — see docs/todo.md.
- name: Configure the OctoPrint Pi
hosts: octoprint
become: true
vars:
timezone: Europe/Vienna
tasks:
- name: Set the timezone
community.general.timezone:
name: "{{ timezone }}"
# ModemManager probes new serial devices for modems — a printer on /dev/ttyACM0
# is one, and the probing can disturb the connection. There's no modem here.
- name: Disable and mask ModemManager
ansible.builtin.systemd_service:
name: ModemManager
state: stopped
enabled: false
masked: true
- name: Disable the Bluetooth services
ansible.builtin.systemd_service:
name: "{{ item }}"
state: stopped
enabled: false
loop:
- bluetooth
- hciuart
- name: Turn off the Bluetooth hardware (applies after reboot)
ansible.builtin.lineinfile:
path: /boot/firmware/config.txt
regexp: '^dtoverlay=disable-bt$'
line: dtoverlay=disable-bt
insertafter: '^\[all\]$'
notify: Reboot
# Key login only. sshd uses the first value it reads, and files in
# sshd_config.d are read (alphabetically) before the main sshd_config.
- name: Disable SSH password login
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/10-no-passwords.conf
content: |
# Managed by Ansible (Infrastructure repo) — key login only
PasswordAuthentication no
KbdInteractiveAuthentication no
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart ssh
handlers:
- name: Restart ssh
ansible.builtin.systemd_service:
name: ssh
state: restarted
- name: Reboot
ansible.builtin.reboot:

View file

@ -0,0 +1,34 @@
# Upgrade all apt packages and reboot only if the upgrade asks for it.
# Don't run against the OctoPrint Pi while a print is running — a reboot kills it.
# OctoPrint itself lives in its own Python venv and is updated from its web UI, not apt.
- name: Update Debian-based hosts
hosts: octoprint
become: true
tasks:
- name: Upgrade all packages
ansible.builtin.apt:
update_cache: true
upgrade: dist
autoremove: true
# Raspberry Pi OS doesn't create /var/run/reboot-required after kernel updates,
# so compare the running kernel (from facts, gathered before the upgrade) with the
# newest installed kernel of the same flavour (e.g. rpi-v8).
- name: Find the newest installed kernel of the running flavour
ansible.builtin.shell: |
set -o pipefail
ls /lib/modules | grep -- '-{{ ansible_facts['kernel'] | regex_replace("^[^-]*-", "") }}$' | sort -V | tail -n 1
args:
executable: /bin/bash
register: newest_kernel
changed_when: false
check_mode: false # read-only, so safe to run in --check mode too
- name: Check whether a package asked for a reboot
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_flag
- name: Reboot if needed
ansible.builtin.reboot:
when: reboot_flag.stat.exists or newest_kernel.stdout != ansible_facts['kernel']

View file

@ -28,6 +28,23 @@ Living checklist. See [status.md](status.md) for the full decisions/context behi
- [ ] **Real (non-self-signed) certificate for the NAS's own DSM access** — not crucial, DSM's cert warning is just cosmetic for local access. Leaning toward **`mkcert`** (local CA, install its root cert as trusted on your own devices once, zero external credentials/services involved) over Let's Encrypt DNS-01 through INWX — the latter would need either a scoped INWX API key (check if INWX offers one) or `acme.sh`'s manual mode (no credentials, but manual renewal every ~90 days); full INWX account credentials handed to a script was correctly ruled out as too broad a permission grant for this. - [ ] **Real (non-self-signed) certificate for the NAS's own DSM access** — not crucial, DSM's cert warning is just cosmetic for local access. Leaning toward **`mkcert`** (local CA, install its root cert as trusted on your own devices once, zero external credentials/services involved) over Let's Encrypt DNS-01 through INWX — the latter would need either a scoped INWX API key (check if INWX offers one) or `acme.sh`'s manual mode (no credentials, but manual renewal every ~90 days); full INWX account credentials handed to a script was correctly ruled out as too broad a permission grant for this.
## Ansible — learning, started 2026-09-27
Setup lives in [`../ansible/`](../ansible/README.md). OctoPrint Pi is the guinea pig; the NUC is the real goal (status.md: Ansible from David's PC, no footprint on the server).
- [x] SSH key auth to the Pi: one key per *client device* (`id_ed25519_homelab`), not per server; `~/.ssh/config` entry with `IdentitiesOnly yes`.
- [x] `ansible octoprint -m ping` works; `update.yml` run for real on 2026-09-27 (248 packages, kernel → 6.12.109). Gotchas: David's sudo needs a password → run with `-K`; Raspberry Pi OS does **not** create `/var/run/reboot-required` after kernel updates, so the playbook compares the running kernel with the newest installed one of the same flavour.
- [ ] **Audit the Pi's hand-made changes** so a rebuild can reproduce them: `apt-mark showmanual`, enabled services, crontabs, `/boot/firmware/config.txt`, installed OctoPrint plugins. **Removed by hand 2026-09-27** (one-off cleanups don't belong in the config playbook — it describes what *should* be there, and an "ensure absent" task would fight future experiments): **Docker** (installed Sep 2026 for a Spoolman test, Spoolman already gone — packages, `docker.list` repo + `docker.asc` key, `/var/lib/docker`, `/var/lib/containerd`, `docker` group) and **nginx** (only the default site, never listening — `haproxy` holds 80/443 and fronts OctoPrint on `127.0.0.1:5000`). Rule going forward: experiment by hand, then either add it to the playbook or remove it; spool-manager-type services belong on the NUC anyway. The Pi 4 (**1 GB RAM**) boots the 32-bit `rpi-v7` kernel instead of the Bookworm default (64-bit `v8` kernel, 32-bit userland) — `config.txt` has an explicit `arm_64bit=0`, which appears to come with the OctoPi image (apt history shows the Pi is essentially a stock OctoPi flash, not years of in-place upgrades). **Decided: leave it.** With 1 GB there's no RAM to gain, and 32-bit userland uses slightly less memory; switching only the kernel isn't worth it before the rebuild. For the rebuild: a **32-bit** OctoPi image is fine on this Pi, using whatever kernel it boots by default. Still wanted from the audit: `config.txt`, enabled services, apt history.
- [x] First *configuration* playbook `playbooks/octoprint.yml` (run 2026-09-27; verified: password SSH refused, timezone set, no Bluetooth device): timezone, ModemManager masked (probes the printer's serial port), Bluetooth off (services + `dtoverlay=disable-bt`), SSH password login off. Wi-Fi stays on — the Pi is on the IoT **Wi-Fi** now, no longer on the cable. nginx removal checked: webcam uses MJPEG (`webcamd`), not the HLS stream (`ffmpeg_hls`, which needed nginx).
- [ ] **Rebuild procedure** (document once tested): flash current 32-bit OctoPi with Raspberry Pi Imager's OS customisation — hostname, user `david`, IoT Wi-Fi, SSH **public-key only** with `id_ed25519_homelab.pub` (Wi-Fi password stays out of the repo) → `ansible-playbook playbooks/octoprint.yml -K` → restore the OctoPrint backup. Test on a spare SD card.
- [ ] **Automated OctoPrint backups** (deferred — basics first). Rebuild plan: flash card → run playbook → restore OctoPrint backup; OctoPrint's own state (settings, plugins, profiles) comes from its Backup & Restore zip, not Ansible (OctoPrint rewrites its own `config.yaml`, so templating it would fight the UI). Plan:
1. Ansible deploys a nightly timer on the Pi running the backup CLI (`octoprint plugins backup:backup` — verify syntax/exclude flags first), excluding uploads/timelapses, keeping the last few. Alternative: the "Backup Scheduler" plugin, but that's UI config outside the repo.
2. Off-device copy must be **pulled** — the `IoT → !IoT` ACL means the Pi can't push to the NAS (keep it that way). Interim: `update.yml` takes a backup and fetches it to David's PC before upgrading. Later: nightly pull from the NUC (Servers → IoT allowed) with a **dedicated key restricted via `rrsync`** to read-only access to the backup folder.
3. Backup zips contain user hashes + API keys — store outside this repo (decide location, e.g. NAS share).
- [ ] Split into roles (`common`, `updates`) + `group_vars`; `ansible-vault` for any secrets (e.g. OctoPrint API key for a "skip while printing" check).
- [ ] Add the two personal Linux machines (`community.general.pacman` for Arch-based ones — not fully unattended, Arch updates occasionally need manual steps).
- [ ] Provision the NUC with it (common role + Docker + Compose stacks).
## Backlog / lower priority ## Backlog / lower priority
- [ ] **Change the Omada Controller auto-backup from daily to weekly** — daily is deliberate for now because so much config is changing; revisit once the switch/AP/VLAN work has settled and changes become infrequent. - [ ] **Change the Omada Controller auto-backup from daily to weekly** — daily is deliberate for now because so much config is changing; revisit once the switch/AP/VLAN work has settled and changes become infrequent.