From 673cedb6b339d0a2dc66cf1cddc14860be068cfe Mon Sep 17 00:00:00 2001 From: David Date: Thu, 29 Oct 2026 22:56:02 +0000 Subject: [PATCH] Add CLAUDE.md with repo guidance Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9bacbf5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Ansible playbooks that deploy and configure MeshCore packet-capture monitoring +nodes running on Raspberry Pi Zero W / Zero 2 W hardware (hostnames +`dm-baldock`, `dm-ashwell`, `dm-edworth`). All work happens under `ansible/`. + +## Commands + +```bash +cd ansible + +# Deploy a single host (recommended for first run / testing) +ansible-playbook -i inventory.yml site.yml --limit dm-edworth + +# Deploy all nodes +ansible-playbook -i inventory.yml site.yml + +# Dry run +ansible-playbook -i inventory.yml site.yml --limit dm-edworth --check + +# If sudo needs a password +ansible-playbook -i inventory.yml site.yml --limit dm-edworth --ask-become-pass +``` + +`ansible.cfg` sets `inventory.yml` and `../.vault_pass` as defaults, so +`-i`/`--vault-password-file` are usually implicit. The play prompts for a +Tailscale auth key each run — leave blank if the node is already +authenticated. + +Bootstrapping a brand-new node needs manual, out-of-band steps first (flashing +the SD card, `ssh-copy-id`, running `sudo tailscale up`, running the +meshcore-packet-capture installer interactively over SSH). See the full +checklist in README.md before assuming `site.yml` alone can bring up a new +host — it can't, on the first pass. + +## Architecture + +**Inventory**: `inventory.yml` defines group `meshcore` (all nodes), with +subgroups `zero_w` / `zero2_w` by hardware. `site.yml` applies the `meshcore` +group with `become: true` at the play level. + +**Vars layering**: `group_vars/all/vars.yml` (shared defaults, e.g. SSH +pubkeys, wifi networks every node joins) → `group_vars/meshcore.yml` +(MQTT/packet-capture config, still plaintext) → `host_vars//vars.yml` +(per-host `serial_port` for the MeshCore USB device, `wifi_ssid_location`). +Secrets are split into parallel `vault.yml` files (Ansible Vault, password in +gitignored `../.vault_pass`) at both the `group_vars/all/` and per-host +levels — `*/vault.yml.example` documents the expected keys. + +**Roles run in this order** (`site.yml`): `wifi` → `base` → `meshcore_cli` → +`meshcore_capture` → `scripts`. + +- `wifi` — NetworkManager (`community.general.nmcli`) connections for + `dotnetwork` (home), `dotmobile` (phone hotspot fallback), and the + host-specific deployed-location network. +- `base` — authorizes SSH keys, installs base apt packages, sets MOTD, + installs Tailscale (always) and authenticates it only if an auth key was + supplied. +- `meshcore_cli` — installs `meshcore-cli` via pipx. +- `meshcore_capture` — wraps the upstream `agessaman/meshcore-packet-capture` + installer. This is the trickiest role in the repo; see below. +- `scripts` — deploys small helper scripts (`voltage.sh`, `bandwidth.sh`). + +### `meshcore_capture` role — two installer generations + +The upstream installer changed its layout between versions, and this repo +must support both because two nodes were bootstrapped under the old one: + +- `meshcore_capture_layout: system` (default, set in + `group_vars/all/vars.yml`) — current upstream (v2.0.0+): dedicated + `meshcore-capture` system user, `/opt` + `/etc/meshcore-packet-capture`, + `config.d/*.toml`, service `meshcore-packet-capture.service`. +- `meshcore_capture_layout: legacy` — how `dm-baldock`/`dm-ashwell` were + originally set up: flat `~/.meshcore-packet-capture`, `.env.local`, service + `meshcore-capture.service`. Only kept so the role stays a safe no-op on + those two hosts; do not use it for new nodes. + +Which config template, service name, and update-script flags get used all +branch on this variable — check `host_vars//vars.yml` for an override +before assuming the `system` defaults apply. + +**The installer itself cannot run non-interactively on a fresh install** — it +opens `/dev/tty` directly for prompts and hangs forever over plain SSH/ansible +with no human to answer. Ansible's `creates:` guard on that task means it only +ever runs once; a human must SSH in and run it manually the first time (see +README.md step 9), after which reruns of `site.yml` just rewrite config and +restart the service. Do not attempt to script answers into it via +`script`/pty tricks — this was already tried and abandoned as too fragile. + +### Other non-obvious constraints + +- `ansible_become` as a *connection* variable takes precedence over a task's + own `become: false`, silently running supposedly-unprivileged tasks as + root. Because of this, `become` is set once at the play level in + `site.yml`, never in `group_vars`/`host_vars` — see the comment at the top + of `group_vars/all/vars.yml`. +- `armv6l` hosts (Pi Zero W, i.e. `dm-baldock`) have no prebuilt `cffi` wheel + and need `libffi-dev`/`pkg-config` to compile it; that apt task in the + `base` role is conditioned on `ansible_architecture`. +- Flashing SD cards with Raspberry Pi Imager 1.9.6 (the current Flathub + build) silently fails to write OS customisation on Trixie images — grab + `rpi-imager_2.0.10_amd64.deb`+ from GitHub releases instead. Full detail in + README.md ("Imager gotcha").