Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ stage0 stage1 stage2 ← upstream pi-gen (base OS, boot files, networ
stage-airplanes/ ← fork-specific
00-prep build deps + chroot hygiene (policy-rc.d, systemctl shim) + SSH posture + cloud-init/first-boot-wizard mask
01-install-feed setup-only: airplanes-feed service account + group + state dir (the feeder readsb, mlat-client venv, feed scripts, apl-feed CLI, and feed/mlat units now arrive via the overlay at stage 02)
02-install-runtime-overlay downloads + verifies the signed runtime-overlay release tarball (decode stack: readsb + dump978 + tar1090 + graphs1090 + render-status; webconfig binary + helpers + wifi libs; feeder readsb + mlat-client venv + feed scripts) and lays it at /opt/airplanes-runtime/releases/vX.Y.Z, flips `current`, and installs the managed_paths entries (symlinks + copy-mode drop-ins)
02-install-runtime-overlay downloads + verifies the signed runtime-overlay release tarball (decode stack: readsb + dump978 + tar1090 + graphs1090 + render-status; webconfig binary + helpers + wifi libs; feeder readsb + mlat-client venv + feed scripts) and lays it at /opt/airplanes/releases/vX.Y.Z, flips `current`, and installs the managed_paths entries (symlinks + copy-mode drop-ins)
05-install-webconfig setup-only: webconfig system user, state dirs, lighttpd mod_proxy + conf-enabled activation, /run tmpfiles spec (the webconfig binary/helpers/units/sudoers now arrive via the overlay at stage 02, not a clone here)
06-firstboot airplanes-first-run script + claim service/timer + boot config template
06a-run-tmpfs resize /run tmpfs + dedicated /run/collectd mount
Expand All @@ -72,7 +72,7 @@ Each substage has `00-run.sh` (host-side: clone, copy files into rootfs) and/or

Pi boots → cloud-init runs (handles user-data / WiFi / hostname injected by rpi-imager) → `airplanes-first-run.service` runs every boot, gated by file presence rather than a rootfs marker. If `/boot/firmware/airplanes-config.txt` exists: parse + apply (HOSTNAME / WiFi keyfile / FEED_HOST translations + feed.env merge), then **rename source → `airplanes-config.applied.txt`** on full success, or write a sibling `airplanes-config.error.txt` and leave the source for retry on failure. → `airplanes-grant-sudo.service` runs after `cloud-final.service` and writes per-user `/etc/sudoers.d/099_airplanes-sudo-<name>` NOPASSWD grants for each rpi-imager-created human user (closes the gap where rpi-imager's user-data emitter omits the sudo grant on non-rpi-os images — see `stage-airplanes/06c-grant-sudo/`). → `airplanes-feed.service` connects to `feed.airplanes.live` → `airplanes-claim.timer` periodically polls the claim endpoint until claimed. cloud-init does not read `airplanes-config.txt`; that file is exclusively `airplanes-first-run`'s input.

The state machine on FAT visible to a user pulling the SD card: `airplanes-config.txt` only = pending or failed; `airplanes-config.txt` + `airplanes-config.error.txt` = failed (read .error.txt to see what to fix); `airplanes-config.applied.txt` only = consumed successfully. The unit is sandboxed with `ProtectSystem=true` + `ReadWritePaths=/boot/firmware /usr/local/share/airplanes` + `RuntimeDirectory=airplanes` — chroot tests bypass that sandbox, so a static lint at `test/test_first_run_unit.bats` asserts the directives stay aligned with what the script actually writes.
The state machine on FAT visible to a user pulling the SD card: `airplanes-config.txt` only = pending or failed; `airplanes-config.txt` + `airplanes-config.error.txt` = failed (read .error.txt to see what to fix); `airplanes-config.applied.txt` only = consumed successfully. The unit is sandboxed with `ProtectSystem=true` + `ReadWritePaths=/boot/firmware` + `RuntimeDirectory=airplanes` — chroot tests bypass that sandbox, so a static lint at `test/test_first_run_unit.bats` asserts the directives stay aligned with what the script actually writes.

`airplanes-config.txt` keys (allowlist enforced by `parse_boot_config`): `HOSTNAME`, `WIFI_SSID`, `WIFI_PASS`, `WIFI_COUNTRY`, `FEED_HOST`, `WEBSITE_URL`, `SSH_PASSWORD`, `SSH_PUBKEY`. Bootstrap-only — hostname for mDNS discovery, WiFi creds for network join, FEED_HOST for the ingest endpoint (ADS-B + MLAT), WEBSITE_URL for the website-API endpoint (claim, diagnostics, remote-config-sync). `FEED_HOST` expands to `MLATSERVER` + `TARGET` in feed.env (synthetic; `FEED_HOST` itself doesn't leak). `WEBSITE_URL` is renamed to `APL_FEED_WEBSITE_URL` (the env-var name apl-feed reads). Operational config (location, MLAT name, MLAT on/off, gain, UAT toggling) lives in the webconfig UI at `http://<hostname>.local/`; the parse-time allowlist rejects those keys with a category-specific "where this setting actually lives" error (see `reject_unknown_boot_key`).

Expand Down Expand Up @@ -105,7 +105,7 @@ Both export via `export-image/`. Stable images are reproducible from the pinned

## Cross-repo coupling with `airplanes-live/feed`

The feed.env schema (which webconfig writes via `configspec.WriteKeys` — `LATITUDE`, `LONGITUDE`, `ALTITUDE`, `MLAT_USER`, `MLAT_ENABLED`, `GAIN`, `UAT_INPUT`, `DUMP978_SDR_SERIAL`, `DUMP978_GAIN`), the `airplanes-first-run.service` ordering, and the daemon runtime state-file pattern at `/run/<service>/state` are coordinated with `airplanes-live/feed`. The image-shipped 978 wrappers (`airplanes-978.sh`, `dump978-fa.sh`) read `UAT_INPUT` from feed.env and publish their decisions to two separate state files — `/run/dump978-fa/state` (producer, includes the `no_hardware` reason from the wrapper's `/sys/bus/usb/devices/*/serial` probe) and `/run/airplanes-978/state` (consumer, includes the `peer_no_hardware` reason refined from the producer file). 978 is **opt-in**: `UAT_INPUT` defaults empty everywhere (webconfig, `apl-feed 978 enable`, or hand-edited feed.env are the three opt-in surfaces); on hardware without a 978-serial RTL-SDR the producer self-disables cleanly via the probe instead of restart-looping. The boot config (`airplanes-config.txt`) is bootstrap-only and no longer touches operational keys — it only writes `MLATSERVER` + `TARGET` via the `FEED_HOST` synthesis. Concretely:
The feed.env schema (which webconfig writes via `configspec.WriteKeys` — `LATITUDE`, `LONGITUDE`, `ALTITUDE`, `MLAT_USER`, `MLAT_ENABLED`, `GAIN`, `UAT_INPUT`, `DUMP978_SDR_SERIAL`, `DUMP978_GAIN`), the `airplanes-first-run.service` ordering, and the daemon runtime state-file pattern at `/run/<service>/state` are coordinated with `airplanes-live/feed`. The image-shipped 978 wrappers (`airplanes-978.sh`, `dump978-fa.sh`) read `UAT_INPUT` from feed.env and publish their decisions to two separate state files — `/run/airplanes/dump978-fa/state` (producer, includes the `no_hardware` reason from the wrapper's `/sys/bus/usb/devices/*/serial` probe) and `/run/airplanes/978/state` (consumer, includes the `peer_no_hardware` reason refined from the producer file). 978 is **opt-in**: `UAT_INPUT` defaults empty everywhere (webconfig, `apl-feed 978 enable`, or hand-edited feed.env are the three opt-in surfaces); on hardware without a 978-serial RTL-SDR the producer self-disables cleanly via the probe instead of restart-looping. The boot config (`airplanes-config.txt`) is bootstrap-only and no longer touches operational keys — it only writes `MLATSERVER` + `TARGET` via the `FEED_HOST` synthesis. Concretely:

- The feed stack (feeder readsb, mlat-client venv, feed scripts, `apl-feed` CLI, and the `airplanes-feed` / `airplanes-mlat` units) is built into the runtime overlay from the `airplanes-live/feed` / readsb / mlat-client refs pinned in `runtime-overlay/config-{dev,stable}` (overlay build runs `stage-feed.sh`) and laid down by stage 02 as `managed_paths`; `stage-airplanes/01-install-feed/` only creates the `airplanes-feed` service account + group + state dir.
- `stage-airplanes/06-firstboot/00-run.sh` writes `/etc/airplanes/release-channel` (read by `feed/update.sh`'s allowlist for `AIRPLANES_FEED_BRANCH`).
Expand Down
26 changes: 13 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
runtime-overlay/scripts/release-workflow/*.sh \
runtime-overlay/src/etc/update-motd.d/10-airplanes-status \
runtime-overlay/src/share/airplanes/*.sh \
stage-airplanes/02-install-runtime-overlay/files/usr/local/lib/airplanes-runtime/recover-shim \
stage-airplanes/02-install-runtime-overlay/files/opt/airplanes/libexec/recover-shim \
stage-airplanes/02-install-runtime-overlay/files/etc/update-motd.d/09-airplanes-recovery-status \
.github/scripts/*.sh \
test/*.sh; do
Expand Down Expand Up @@ -562,7 +562,6 @@ jobs:
mkdir -p "$ROOT/etc/systemd/system" \
"$ROOT/usr/local/sbin" \
"$ROOT/usr/local/bin" \
"$ROOT/usr/local/share/airplanes" \
"$ROOT/usr/local/share/tar1090" \
"$ROOT/usr/share/graphs1090" \
"$ROOT/usr/bin" \
Expand All @@ -585,10 +584,10 @@ jobs:
install -m 0644 \
stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service \
"$ROOT/etc/systemd/system/"
mkdir -p "$ROOT/usr/local/lib/airplanes-runtime"
mkdir -p "$ROOT/opt/airplanes/libexec"
install -m 0755 \
stage-airplanes/02-install-runtime-overlay/files/usr/local/lib/airplanes-runtime/recover-shim \
"$ROOT/usr/local/lib/airplanes-runtime/recover-shim"
stage-airplanes/02-install-runtime-overlay/files/opt/airplanes/libexec/recover-shim \
"$ROOT/opt/airplanes/libexec/recover-shim"
install -m 0644 image-webconfig/files/etc/systemd/system/*.service \
"$ROOT/etc/systemd/system/"
install -m 0644 stage-airplanes/06c-grant-sudo/files/etc/systemd/system/*.service \
Expand All @@ -601,14 +600,15 @@ jobs:
install -m 0755 /dev/null "$ROOT/usr/local/sbin/airplanes-first-run"
install -m 0755 /dev/null "$ROOT/usr/local/sbin/airplanes-grant-sudo"
install -m 0755 /dev/null "$ROOT/usr/local/bin/apl-feed"
install -m 0755 /dev/null "$ROOT/usr/local/bin/airplanes-webconfig"
mkdir -p "$ROOT/usr/local/lib/airplanes-webconfig" "$ROOT/usr/local/lib/airplanes"
install -m 0755 /dev/null "$ROOT/usr/local/lib/airplanes-webconfig/reset"
install -m 0755 /dev/null "$ROOT/usr/local/lib/airplanes/render-status"
install -m 0755 /dev/null "$ROOT/usr/local/share/airplanes/readsb.sh"
install -m 0755 /dev/null "$ROOT/usr/local/share/airplanes/airplanes-978.sh"
install -m 0755 /dev/null "$ROOT/usr/local/share/airplanes/dump978-fa.sh"
install -m 0755 /dev/null "$ROOT/usr/local/share/airplanes/tar1090-uat-sync.sh"
# Decoder + webconfig payload now lives under the overlay at
# /opt/airplanes/current; stub the units' ExecStart targets there.
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/bin/airplanes-webconfig"
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/lib/airplanes-webconfig/reset"
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/lib/airplanes/render-status"
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/share/airplanes/readsb.sh"
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/share/airplanes/airplanes-978.sh"
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/share/airplanes/dump978-fa.sh"
install -D -m 0755 /dev/null "$ROOT/opt/airplanes/current/share/airplanes/tar1090-uat-sync.sh"
# tar1090 + graphs1090 ExecStart targets (placeholder shell scripts the
# upstream installers normally drop in)
install -m 0755 /dev/null "$ROOT/usr/local/share/tar1090/tar1090.sh"
Expand Down
8 changes: 4 additions & 4 deletions runtime-overlay/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# produced earlier in the product-release workflow. Local builds may set
# AIRPLANES_RUNTIME_OVERLAY_TAG to install from a published product release.
# Downloads/copies and verifies the release, extracts it under
# ${ROOTFS_DIR}/opt/airplanes-runtime/releases/v<version>/, flips
# ${ROOTFS_DIR}/opt/airplanes/releases/v<version>/, flips
# current, relinks decoder binaries, lays managed_paths. Skips
# systemd ops (handled by the chroot stage) and health gates (no
# running system to probe).
Expand Down Expand Up @@ -92,16 +92,16 @@ fi
# is canonical (and so a `dev-latest` floating tag still lands under its
# concrete v<X.Y.Z-dev-...> dir).
RELEASE_VERSION="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST")"
RELEASE_DIR_ABS="${TARGET_ROOT}/opt/airplanes-runtime/releases/v${RELEASE_VERSION}"
RELEASE_DIR_ABS="${TARGET_ROOT}/opt/airplanes/releases/v${RELEASE_VERSION}"

# Compute PREV_RELEASE_DIR from the pre-flip current symlink. Empty if
# there's no current yet (first install). The link target is always an
# on-device-canonical path (`/opt/airplanes-runtime/releases/v<X>/`); for
# on-device-canonical path (`/opt/airplanes/releases/v<X>/`); for
# downstream consumers under a build-mode rebase we surface it rebased so
# shell migrations can stat it under TARGET_ROOT, while we compare the
# link's literal target against the on-device equivalent of the new
# release path.
CURRENT_LINK="${TARGET_ROOT}/opt/airplanes-runtime/current"
CURRENT_LINK="${TARGET_ROOT}/opt/airplanes/current"
PREV_RELEASE_LINK_TARGET=""
PREV_RELEASE_DIR=""
if [[ -L "$CURRENT_LINK" ]]; then
Expand Down
Loading
Loading