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
8 changes: 4 additions & 4 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 All @@ -90,7 +90,7 @@ Go server lives in `airplanes-live/image-webconfig` (modules: `auth`, `feedenv`,

### Wi-Fi management

`/api/wifi` endpoints (list / add / update / delete / test / activate / status) proxy to a sudoers-pinned `apl-wifi` helper at `/usr/local/bin/apl-wifi`. The helper, its libs (`wifi-validators.sh`, `wifi-keyfile.sh`), the webconfig sudoers files, and the webconfig systemd units all ship from `airplanes-live/image-webconfig`'s `rootfs.tar.gz`. The helper owns atomic NetworkManager keyfile writes under `/etc/NetworkManager/system-connections/`, the connect-before-save test flow via `nmcli --wait`, and lock-out enforcement (`force_last` / `force_active_no_uplink` flags rechecked under flock). SSID/PSK/country/priority validators live at `/usr/local/lib/airplanes/wifi-validators.sh` and are sourced by both `airplanes-first-run` (boot-config flow, in image) and `apl-wifi` (UI flow, in image-webconfig) so identical inputs are accepted on either side. Managed keyfiles match `airplanes-config-wifi.nmconnection` or `airplanes-wifi-*.nmconnection`; foreign keyfiles surface read-only in the UI. `airplanes-webconfig.service` adds `/etc/NetworkManager/system-connections` to `ReadWritePaths=` because the sudo child inherits the unit's mount namespace.
`/api/wifi` endpoints (list / add / update / delete / test / activate / status) proxy to a sudoers-pinned `apl-wifi` helper at `/usr/local/bin/apl-wifi`. The helper, its libs (`wifi-validators.sh`, `wifi-keyfile.sh`), the webconfig sudoers files, and the webconfig systemd units all ship from `airplanes-live/image-webconfig`'s `rootfs.tar.gz`. The helper owns atomic NetworkManager keyfile writes under `/etc/NetworkManager/system-connections/`, the connect-before-save test flow via `nmcli --wait`, and lock-out enforcement (`force_last` / `force_active_no_uplink` flags rechecked under flock). SSID/PSK/country/priority validators live at `/opt/airplanes/current/lib/airplanes/wifi-validators.sh` and are sourced by both `airplanes-first-run` (boot-config flow, in image) and `apl-wifi` (UI flow, in image-webconfig) so identical inputs are accepted on either side. Managed keyfiles match `airplanes-config-wifi.nmconnection` or `airplanes-wifi-*.nmconnection`; foreign keyfiles surface read-only in the UI. `airplanes-webconfig.service` adds `/etc/NetworkManager/system-connections` to `ReadWritePaths=` because the sudo child inherits the unit's mount namespace.

## Channels

Expand All @@ -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
25 changes: 14 additions & 11 deletions runtime-overlay/config-stable
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ export AIRPLANES_GRAPHS1090_REPO="${AIRPLANES_GRAPHS1090_REPO:-https://github.co
export AIRPLANES_GRAPHS1090_BRANCH="${AIRPLANES_GRAPHS1090_BRANCH:-426c70c61889de2a8f63f2b40133ea72a08537ce}"

# Webconfig — downloaded as a prebuilt release, not compiled in the overlay.
# Pinned to a concrete stable release (post-self-update-removal; the v0.1.x
# tags up to v0.1.2 still carried the removed self-update surface). COMMIT_SHA
# must equal the release manifest's commit_sha — stage-webconfig.sh hard-fails
# the build on mismatch.
export AIRPLANES_WEBCONFIG_RELEASE_TAG="${AIRPLANES_WEBCONFIG_RELEASE_TAG:-v0.3.0}"
export AIRPLANES_WEBCONFIG_COMMIT_SHA="${AIRPLANES_WEBCONFIG_COMMIT_SHA:-30448fe56d121268bb91fada04d48611d3fc2156}"

# Feed scripts — overlay-built. The feed repo's only tag (v0.0.1) lags the dev
# head, so the stable pin is the current feed dev HEAD SHA as of this pin date
# (the pin must be a 40-hex SHA per test_channel_configs.bats).
# Pinned to a concrete stable release. v0.5.0 carries the /opt/airplanes FHS
# on-device layout plus the fixed Wi-Fi/SSH helper library path, matching the
# overlay this channel builds.
# COMMIT_SHA must equal the release manifest's commit_sha — stage-webconfig.sh
# hard-fails the build on mismatch.
export AIRPLANES_WEBCONFIG_RELEASE_TAG="${AIRPLANES_WEBCONFIG_RELEASE_TAG:-v0.5.0}"
export AIRPLANES_WEBCONFIG_COMMIT_SHA="${AIRPLANES_WEBCONFIG_COMMIT_SHA:-634e7edcfab1bfa16712e8d5777be96ede0cc506}"

# Feed scripts — overlay-built. The feed repo's semver tags are reserved for
# launch, so the stable pin is a concrete feed dev SHA (must be 40-hex per
# test_channel_configs.bats). Pinned to the feed dev head (feed #145), which
# carries the /opt/airplanes layout and the airplanes-stats units so the stable
# overlay's feed payload matches this channel and the dev channel.
export AIRPLANES_FEED_OVERLAY_REPO="${AIRPLANES_FEED_OVERLAY_REPO:-https://github.com/airplanes-live/feed.git}"
export AIRPLANES_FEED_OVERLAY_BRANCH="${AIRPLANES_FEED_OVERLAY_BRANCH:-18a07314a49f483e545d00da2532c72ccda03e5f}"
export AIRPLANES_FEED_OVERLAY_BRANCH="${AIRPLANES_FEED_OVERLAY_BRANCH:-430651d67d045639b84ad5e3438ba5302d33bcfb}"

# Feeder readsb fork (outbound airplanes-feed binary). The repo's version tags
# mirror upstream wiedehopf and lag the fork's dev head by hundreds of commits,
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