From 483d8d9802173e866fae85e2d23aa4604db80df0 Mon Sep 17 00:00:00 2001 From: mrwhiski Date: Sun, 27 Sep 2026 22:13:35 +0200 Subject: [PATCH 1/2] Adds ansible update and configuration file for octoprint raspberry --- ansible/README.md | 28 +++++++++++++++ ansible/ansible.cfg | 6 ++++ ansible/inventory.yml | 7 ++++ ansible/playbooks/octoprint.yml | 63 +++++++++++++++++++++++++++++++++ ansible/playbooks/update.yml | 34 ++++++++++++++++++ docs/todo.md | 17 +++++++++ 6 files changed, 155 insertions(+) create mode 100644 ansible/README.md create mode 100644 ansible/ansible.cfg create mode 100644 ansible/inventory.yml create mode 100644 ansible/playbooks/octoprint.yml create mode 100644 ansible/playbooks/update.yml diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..4db4c7d --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,28 @@ +# 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 +``` + +Add `-K` if a play fails with "Missing sudo password". diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..cff3473 --- /dev/null +++ b/ansible/ansible.cfg @@ -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 diff --git a/ansible/inventory.yml b/ansible/inventory.yml new file mode 100644 index 0000000..f0eb5e3 --- /dev/null +++ b/ansible/inventory.yml @@ -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: diff --git a/ansible/playbooks/octoprint.yml b/ansible/playbooks/octoprint.yml new file mode 100644 index 0000000..1ce4061 --- /dev/null +++ b/ansible/playbooks/octoprint.yml @@ -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: diff --git a/ansible/playbooks/update.yml b/ansible/playbooks/update.yml new file mode 100644 index 0000000..f8d55b1 --- /dev/null +++ b/ansible/playbooks/update.yml @@ -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'] diff --git a/docs/todo.md b/docs/todo.md index d6ddca2..6400e33 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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. +## 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 - [ ] **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. From 69e9a76dcd908ebff423e8c21dfc81e6b3bc7a86 Mon Sep 17 00:00:00 2001 From: mrwhiski Date: Sun, 27 Sep 2026 22:16:24 +0200 Subject: [PATCH 2/2] Adds ansible commands to readme --- ansible/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ansible/README.md b/ansible/README.md index 4db4c7d..db798cc 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -23,6 +23,8 @@ 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".