From f99847a5be230023e64fb050c23da8a4ab81a330 Mon Sep 17 00:00:00 2001 From: darken Date: Thu, 25 Jun 2026 00:32:16 +0200 Subject: [PATCH 1/5] refactor!: install the feeder stack under the /opt/airplanes FHS layout Move the runtime overlay, image-baked helpers, and per-service state under /opt/airplanes, /var/lib/airplanes, and /run/airplanes; stop creating symlinks in /usr/bin and /usr/local/{lib,share}. Decoders, wrappers, and render-status are reached via /opt/airplanes/current; the release gates reject re-squatting. BREAKING CHANGE: on-device install paths change; a feeder is reflashed. --- .claude/CLAUDE.md | 6 +- .github/workflows/ci.yml | 26 +-- runtime-overlay/install.sh | 8 +- .../manifest-inputs/managed_paths.json | 207 +++--------------- runtime-overlay/schema/example-manifest.json | 98 ++------- runtime-overlay/schema/manifest.schema.json | 2 +- runtime-overlay/scripts/build-release.sh | 4 +- .../scripts/gates/exec-bit-check.sh | 18 +- runtime-overlay/scripts/lib/install-common.sh | 87 ++++---- runtime-overlay/scripts/lib/stage-feed.sh | 16 +- .../scripts/lib/stage-graphs1090.sh | 4 +- runtime-overlay/scripts/lib/stage-tar1090.sh | 2 +- .../release-workflow/write-provenance.sh | 2 +- runtime-overlay/scripts/validate-manifest.sh | 25 +++ .../conf-available/89-airplanes-978.conf | 4 +- .../src/etc/update-motd.d/10-airplanes-status | 2 +- .../src/lib/airplanes-update-orchestrator | 4 +- .../src/lib/airplanes/render-status | 28 +-- .../src/lib/runtime-self-update.sh | 12 +- .../0001-create-service-accounts.sh | 6 +- .../src/share/airplanes/airplanes-978.sh | 17 +- .../src/share/airplanes/dump978-fa.sh | 8 +- runtime-overlay/src/share/airplanes/readsb.sh | 4 +- .../src/share/airplanes/tar1090-uat-sync.sh | 10 +- .../src/systemd/airplanes-978.service | 6 +- .../systemd/airplanes-tar1090-uat-sync.path | 4 +- .../airplanes-tar1090-uat-sync.service | 4 +- .../src/systemd/dump978-fa.service | 6 +- runtime-overlay/src/systemd/readsb.service | 2 +- runtime-overlay/update.sh | 2 +- stage-airplanes/00-prep/06-run.sh | 4 +- .../airplanes/libexec}/runtime-release.pub | 0 .../01-install-feed/01-run-chroot.sh | 4 +- .../02-install-runtime-overlay/00-run.sh | 18 +- .../01-run-chroot.sh | 22 +- .../airplanes-runtime-update-recover.service | 6 +- .../09-airplanes-recovery-status | 2 +- .../airplanes/libexec}/recover-shim | 14 +- .../05-install-webconfig/01-run-chroot.sh | 34 ++- .../files/usr/local/sbin/airplanes-first-run | 7 +- stage-airplanes/06a-run-tmpfs/00-run.sh | 6 +- .../system/airplanes-run-resize.service | 2 +- .../airplanes/libexec}/run-resize.sh | 0 .../06b-console-dashboard/00-run.sh | 38 +--- .../06b-console-dashboard/README.md | 14 +- .../system/airplanes-dashboard.service | 2 +- stage-airplanes/06d-cli-ergonomics/00-run.sh | 16 +- .../airplanes/libexec}/start-orchestrator.sh | 4 +- test/boot-smoke/extra-probe.sh | 89 ++++---- .../boot-smoke/lib/runtime-upgrade-helpers.sh | 4 +- test/lib/webconfig-fingerprint.sh | 14 +- test/overlay-smoke-inner.sh | 32 +-- .../build-release-input/managed_paths.json | 74 +------ .../lib/install_test_helpers.bash | 20 +- .../test_06b_render_status_gated.bats | 61 ++---- .../test_airplanes_978_wrapper.bats | 2 +- test/runtime-overlay/test_bridge_release.bats | 16 +- .../test_build_release_layout.bats | 6 +- .../runtime-overlay/test_feed_mutability.bats | 12 +- .../test_finalize_reconciles_aggregators.bats | 4 +- .../test_finalize_starts_activators.bats | 4 +- .../test_install_account_migration.bats | 2 +- .../test_install_buildmode.bats | 28 +-- .../test_install_copy_path_preimage.bats | 2 +- .../test_install_health_gate_feed.bats | 63 +++--- .../test_install_health_gate_readsb.bats | 8 +- .../test_install_managed_paths_symlink.bats | 16 +- .../test_install_migrations_shell.bats | 8 +- .../test_install_relink_decoder_binaries.bats | 41 ++-- .../test_install_runtime_smoke.bats | 21 +- .../test_install_same_version_safety.bats | 8 +- .../test_install_symlink_preimage.bats | 6 +- .../test_mlat_unit_condition.bats | 14 +- .../test_orchestrator_sequence.bats | 14 +- .../test_orchestrator_trampoline.bats | 8 +- test/runtime-overlay/test_pubkey_shipped.bats | 6 +- test/runtime-overlay/test_recover_shim.bats | 84 +++---- test/runtime-overlay/test_render_status.bats | 14 +- .../test_runtime_manifest_schema.bats | 2 +- ...test_self_update_failure_pre_mutation.bats | 8 +- .../test_self_update_flock_contention.bats | 4 +- ...elf_update_flock_held_across_protocol.bats | 4 +- ...test_self_update_health_passed_resume.bats | 26 +-- .../test_self_update_refuses_dirty_state.bats | 6 +- ..._self_update_rollback_from_each_state.bats | 32 +-- .../test_self_update_state_helpers.bats | 32 +-- .../test_self_update_state_machine.bats | 24 +- .../test_stage_feed_unit_symmetry.bats | 4 +- .../test_stage_install_runtime_overlay.bats | 43 ++-- .../runtime-overlay/test_symlink_cleanup.bats | 10 +- .../test_tar1090_uat_sync.bats | 2 +- test/update-regression-inner.sh | 18 +- 92 files changed, 742 insertions(+), 971 deletions(-) rename stage-airplanes/00-prep/files/{usr/share/airplanes => opt/airplanes/libexec}/runtime-release.pub (100%) rename stage-airplanes/02-install-runtime-overlay/files/{usr/local/lib/airplanes-runtime => opt/airplanes/libexec}/recover-shim (97%) rename stage-airplanes/06a-run-tmpfs/files/{usr/local/lib/airplanes => opt/airplanes/libexec}/run-resize.sh (100%) rename stage-airplanes/06d-cli-ergonomics/files/{usr/local/lib/airplanes-webconfig => opt/airplanes/libexec}/start-orchestrator.sh (87%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 25805fbe24..008370a3fd 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 @@ -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-` 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://.local/`; the parse-time allowlist rejects those keys with a category-specific "where this setting actually lives" error (see `reject_unknown_boot_key`). @@ -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//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//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`). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac9fb1e66..71e2da275c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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" \ @@ -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 \ @@ -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" diff --git a/runtime-overlay/install.sh b/runtime-overlay/install.sh index 90201c27f0..a1a5d14bf3 100755 --- a/runtime-overlay/install.sh +++ b/runtime-overlay/install.sh @@ -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/, flips +# ${ROOTFS_DIR}/opt/airplanes/releases/v/, flips # current, relinks decoder binaries, lays managed_paths. Skips # systemd ops (handled by the chroot stage) and health gates (no # running system to probe). @@ -92,16 +92,16 @@ fi # is canonical (and so a `dev-latest` floating tag still lands under its # concrete v 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/`); for +# on-device-canonical path (`/opt/airplanes/releases/v/`); 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 diff --git a/runtime-overlay/manifest-inputs/managed_paths.json b/runtime-overlay/manifest-inputs/managed_paths.json index b8534f945c..e13926a86a 100644 --- a/runtime-overlay/manifest-inputs/managed_paths.json +++ b/runtime-overlay/manifest-inputs/managed_paths.json @@ -2,217 +2,107 @@ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" + "target": "/opt/airplanes/current/systemd/readsb.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-978.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-978.service" + "target": "/opt/airplanes/current/systemd/airplanes-978.service" }, { "mode": "symlink", "link": "/etc/systemd/system/dump978-fa.service", - "target": "/opt/airplanes-runtime/current/systemd/dump978-fa.service" + "target": "/opt/airplanes/current/systemd/dump978-fa.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-tar1090-uat-sync.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-tar1090-uat-sync.service" + "target": "/opt/airplanes/current/systemd/airplanes-tar1090-uat-sync.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-tar1090-uat-sync.path", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-tar1090-uat-sync.path" + "target": "/opt/airplanes/current/systemd/airplanes-tar1090-uat-sync.path" }, { "mode": "symlink", "link": "/etc/systemd/system/tar1090.service", - "target": "/opt/airplanes-runtime/current/systemd/tar1090.service" + "target": "/opt/airplanes/current/systemd/tar1090.service" }, { "mode": "symlink", "link": "/etc/systemd/system/graphs1090.service", - "target": "/opt/airplanes-runtime/current/systemd/graphs1090.service" + "target": "/opt/airplanes/current/systemd/graphs1090.service" }, { "mode": "symlink", "link": "/etc/systemd/system/collectd.service.d/malarky.conf", - "target": "/opt/airplanes-runtime/current/etc/systemd/system/collectd.service.d/malarky.conf" - }, - { - "mode": "symlink", - "link": "/usr/bin/readsb", - "target": "/opt/airplanes-runtime/current/bin/readsb" - }, - { - "mode": "symlink", - "link": "/usr/bin/airplanes-978", - "target": "/opt/airplanes-runtime/current/bin/readsb" - }, - { - "mode": "symlink", - "link": "/usr/bin/dump978-fa", - "target": "/opt/airplanes-runtime/current/bin/dump978-fa" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes/render-status", - "target": "/opt/airplanes-runtime/current/lib/airplanes/render-status" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/readsb.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/readsb.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/airplanes-978.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-978.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/dump978-fa.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/dump978-fa.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/tar1090-uat-sync.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/tar1090-uat-sync.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/logo.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/logo.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/banner.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/banner.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/banner-narrow.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/banner-narrow.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/icon.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/icon.txt" + "target": "/opt/airplanes/current/etc/systemd/system/collectd.service.d/malarky.conf" }, { "mode": "symlink", "link": "/etc/update-motd.d/10-airplanes-status", - "target": "/opt/airplanes-runtime/current/etc/update-motd.d/10-airplanes-status" + "target": "/opt/airplanes/current/etc/update-motd.d/10-airplanes-status" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/88-tar1090.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/88-tar1090.conf" + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/88-tar1090.conf" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/88-graphs1090.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/88-graphs1090.conf" + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/88-graphs1090.conf" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/89-airplanes-978.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/89-airplanes-978.conf" + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/89-airplanes-978.conf" }, { "mode": "symlink", "link": "/usr/local/share/tar1090", - "target": "/opt/airplanes-runtime/current/share/tar1090" + "target": "/opt/airplanes/current/share/tar1090" }, { "mode": "symlink", "link": "/usr/share/graphs1090", - "target": "/opt/airplanes-runtime/current/share/graphs1090" - }, - { - "mode": "symlink", - "link": "/usr/local/bin/airplanes-webconfig", - "target": "/opt/airplanes-runtime/current/bin/airplanes-webconfig" + "target": "/opt/airplanes/current/share/graphs1090" }, { "mode": "symlink", "link": "/usr/local/bin/apl-wifi", - "target": "/opt/airplanes-runtime/current/bin/apl-wifi" + "target": "/opt/airplanes/current/bin/apl-wifi" }, { "mode": "symlink", "link": "/usr/local/bin/apl-ssh", - "target": "/opt/airplanes-runtime/current/bin/apl-ssh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes/wifi-validators.sh", - "target": "/opt/airplanes-runtime/current/lib/airplanes/wifi-validators.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes/ssh-validators.sh", - "target": "/opt/airplanes-runtime/current/lib/airplanes/ssh-validators.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes/wifi-keyfile.sh", - "target": "/opt/airplanes-runtime/current/lib/airplanes/wifi-keyfile.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes-webconfig/identity-export.sh", - "target": "/opt/airplanes-runtime/current/lib/airplanes-webconfig/identity-export.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes-webconfig/identity-import.sh", - "target": "/opt/airplanes-runtime/current/lib/airplanes-webconfig/identity-import.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes-webconfig/claim-rotate.sh", - "target": "/opt/airplanes-runtime/current/lib/airplanes-webconfig/claim-rotate.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes-webconfig/reset", - "target": "/opt/airplanes-runtime/current/lib/airplanes-webconfig/reset" + "target": "/opt/airplanes/current/bin/apl-ssh" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-webconfig.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-webconfig.service" + "target": "/opt/airplanes/current/systemd/airplanes-webconfig.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-webconfig-reset.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-webconfig-reset.service" + "target": "/opt/airplanes/current/systemd/airplanes-webconfig-reset.service" }, { "mode": "symlink", "link": "/usr/local/bin/apl-aggregator", - "target": "/opt/airplanes-runtime/current/bin/apl-aggregator" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes-webconfig/aggregator-run", - "target": "/opt/airplanes-runtime/current/lib/airplanes-webconfig/aggregator-run" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes-webconfig/aggregators", - "target": "/opt/airplanes-runtime/current/lib/airplanes-webconfig/aggregators" + "target": "/opt/airplanes/current/bin/apl-aggregator" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-aggregator@.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-aggregator@.service" + "target": "/opt/airplanes/current/systemd/airplanes-aggregator@.service" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/40-airplanes-webconfig.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/40-airplanes-webconfig.conf" + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/40-airplanes-webconfig.conf" }, { "mode": "copy", @@ -220,76 +110,45 @@ "from": "etc/sudoers.d/010_airplanes-webconfig", "owner": "root:root", "perm": "0440", - "post_install": ["/usr/sbin/visudo", "-cf", "/etc/sudoers.d/010_airplanes-webconfig"] - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/feed-airplanes", - "target": "/opt/airplanes-runtime/current/bin/feed-airplanes" + "post_install": [ + "/usr/sbin/visudo", + "-cf", + "/etc/sudoers.d/010_airplanes-webconfig" + ] }, { "mode": "symlink", "link": "/usr/local/bin/apl-feed", - "target": "/opt/airplanes-runtime/current/bin/apl-feed" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/airplanes-feed.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-feed.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/airplanes-mlat.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-mlat.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/airplanes-diagnostics.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-diagnostics.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/apl-feed", - "target": "/opt/airplanes-runtime/current/share/airplanes/apl-feed" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/lib", - "target": "/opt/airplanes-runtime/current/share/airplanes/lib" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/venv", - "target": "/opt/airplanes-runtime/current/share/airplanes/venv" + "target": "/opt/airplanes/current/bin/apl-feed" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-feed.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-feed.service" + "target": "/opt/airplanes/current/systemd/airplanes-feed.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-mlat.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-mlat.service" + "target": "/opt/airplanes/current/systemd/airplanes-mlat.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-diagnostics.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-diagnostics.service" + "target": "/opt/airplanes/current/systemd/airplanes-diagnostics.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-diagnostics.timer", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-diagnostics.timer" + "target": "/opt/airplanes/current/systemd/airplanes-diagnostics.timer" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-config-sync.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-config-sync.service" + "target": "/opt/airplanes/current/systemd/airplanes-config-sync.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-config-sync.timer", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-config-sync.timer" + "target": "/opt/airplanes/current/systemd/airplanes-config-sync.timer" } ] diff --git a/runtime-overlay/schema/example-manifest.json b/runtime-overlay/schema/example-manifest.json index 73e8405a0c..8777d5f5db 100644 --- a/runtime-overlay/schema/example-manifest.json +++ b/runtime-overlay/schema/example-manifest.json @@ -5,7 +5,9 @@ "channel": "stable", "commit_sha": "0123456789abcdef0123456789abcdef01234567", "build_date": "2026-05-19T12:34:56Z", - "arches": ["arm64"], + "arches": [ + "arm64" + ], "compat": { "base_os_codename": "trixie", "mlat_python_abi": "cp313", @@ -29,113 +31,56 @@ } }, "managed_paths": [ - { - "mode": "symlink", - "link": "/usr/bin/readsb", - "target": "/opt/airplanes-runtime/current/bin/readsb" - }, - { - "mode": "symlink", - "link": "/usr/bin/airplanes-978", - "target": "/opt/airplanes-runtime/current/bin/readsb" - }, { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" + "target": "/opt/airplanes/current/systemd/readsb.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-978.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-978.service" + "target": "/opt/airplanes/current/systemd/airplanes-978.service" }, { "mode": "symlink", "link": "/etc/systemd/system/dump978-fa.service", - "target": "/opt/airplanes-runtime/current/systemd/dump978-fa.service" + "target": "/opt/airplanes/current/systemd/dump978-fa.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-tar1090-uat-sync.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-tar1090-uat-sync.service" + "target": "/opt/airplanes/current/systemd/airplanes-tar1090-uat-sync.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-tar1090-uat-sync.path", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-tar1090-uat-sync.path" + "target": "/opt/airplanes/current/systemd/airplanes-tar1090-uat-sync.path" }, { "mode": "symlink", "link": "/usr/local/share/tar1090", - "target": "/opt/airplanes-runtime/current/share/tar1090" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes/render-status", - "target": "/opt/airplanes-runtime/current/lib/airplanes/render-status" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/readsb.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/readsb.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/venv", - "target": "/opt/airplanes-runtime/current/share/airplanes/venv" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/airplanes-978.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-978.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/dump978-fa.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/dump978-fa.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/tar1090-uat-sync.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/tar1090-uat-sync.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/logo.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/logo.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/banner.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/banner.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/banner-narrow.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/banner-narrow.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/icon.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/icon.txt" + "target": "/opt/airplanes/current/share/tar1090" }, { "mode": "symlink", "link": "/etc/update-motd.d/10-airplanes-status", - "target": "/opt/airplanes-runtime/current/etc/update-motd.d/10-airplanes-status" + "target": "/opt/airplanes/current/etc/update-motd.d/10-airplanes-status" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/89-airplanes-978.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/89-airplanes-978.conf" + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/89-airplanes-978.conf" }, { "mode": "copy", - "path": "/etc/sudoers.d/090_airplanes-runtime", - "from": "etc/sudoers.d/090_airplanes-runtime", + "path": "/etc/sudoers.d/010_airplanes-webconfig", + "from": "etc/sudoers.d/010_airplanes-webconfig", "owner": "root:root", "perm": "0440", - "post_install": ["/usr/sbin/visudo", "-c"] + "post_install": [ + "/usr/sbin/visudo", + "-c" + ] } ], "mutable_paths": [ @@ -150,7 +95,9 @@ "airplanes-tar1090-uat-sync.path" ], "disable": [], - "reload_or_restart": ["lighttpd.service"], + "reload_or_restart": [ + "lighttpd.service" + ], "daemon_reload": true }, "migrations": [ @@ -159,7 +106,10 @@ "type": "group_membership", "run_when": "every_install", "user": "readsb", - "groups": ["plugdev", "dialout"] + "groups": [ + "plugdev", + "dialout" + ] }, { "id": "tar1090-defaults-uat-key", diff --git a/runtime-overlay/schema/manifest.schema.json b/runtime-overlay/schema/manifest.schema.json index 146b251486..3bac54944e 100644 --- a/runtime-overlay/schema/manifest.schema.json +++ b/runtime-overlay/schema/manifest.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://airplanes.live/schema/runtime-overlay/manifest.schema.json", "title": "Runtime overlay release manifest", - "description": "Schema for /opt/airplanes-runtime/releases/v/manifest.json — declares what a single runtime overlay release is, what it installs, and how it installs.", + "description": "Schema for /opt/airplanes/releases/v/manifest.json — declares what a single runtime overlay release is, what it installs, and how it installs.", "type": "object", "additionalProperties": false, "allOf": [ diff --git a/runtime-overlay/scripts/build-release.sh b/runtime-overlay/scripts/build-release.sh index 17193b5e61..6187a1e02b 100755 --- a/runtime-overlay/scripts/build-release.sh +++ b/runtime-overlay/scripts/build-release.sh @@ -231,9 +231,9 @@ _check_release_local_path() { fi } -# managed_paths.target is an absolute /opt/airplanes-runtime/current/... path; +# managed_paths.target is an absolute /opt/airplanes/current/... path; # strip that prefix to get the release-local path. -CURRENT_PREFIX="/opt/airplanes-runtime/current/" +CURRENT_PREFIX="/opt/airplanes/current/" while IFS= read -r abs_target; do [[ -z "$abs_target" ]] && continue if [[ "$abs_target" != "$CURRENT_PREFIX"* ]]; then diff --git a/runtime-overlay/scripts/gates/exec-bit-check.sh b/runtime-overlay/scripts/gates/exec-bit-check.sh index d7329bb32c..35e1084ad4 100755 --- a/runtime-overlay/scripts/gates/exec-bit-check.sh +++ b/runtime-overlay/scripts/gates/exec-bit-check.sh @@ -19,7 +19,7 @@ # 2. Every ExecStart=/ExecStartPre=/ExecStartPost=/ExecStop=/ # ExecStopPost=/ExecReload= absolute path referenced by a unit in # the staged tree, whose path resolves into the release tree -# (either directly under /opt/airplanes-runtime/current/ or via a +# (either directly under /opt/airplanes/current/ or via a # managed_paths symlink), must satisfy the same shape constraints. # # Mode check is "regular file + any execute bit", not exact 0755. Git @@ -61,7 +61,7 @@ command -v jq >/dev/null 2>&1 || die "jq is required" manifest="$RELEASE_DIR/manifest.json" [[ -f "$manifest" ]] || die "manifest.json missing under $RELEASE_DIR" -CURRENT_PREFIX="/opt/airplanes-runtime/current/" +CURRENT_PREFIX="/opt/airplanes/current/" # Build a lookup from on-device absolute path -> release-local path for # every managed_paths symlink entry. This is how an ExecStart= that @@ -185,15 +185,25 @@ while IFS= read -r rel; do done < <(jq -r '.managed_paths[]? | select(.mode == "symlink") | .target' "$manifest" \ | awk -v p="$CURRENT_PREFIX" 'index($0, p) == 1 { print substr($0, length(p) + 1) }') +# Gate-validity guard. Every check above filters on CURRENT_PREFIX, so if the +# manifest declares symlink managed_paths but NONE resolved under it, the +# prefix has drifted out of sync with the manifest's targets and every shape +# check silently verified an empty set — emitting a false "ok". An /opt path +# rename that misses this constant springs exactly that trap, so fail loud. +managed_symlink_total="$(jq -r '[.managed_paths[]? | select(.mode == "symlink")] | length' "$manifest")" +if (( managed_symlink_total > 0 )) && (( ${#managed_link_to_release_local[@]} == 0 )); then + die "no managed_paths symlink target resolved under CURRENT_PREFIX=$CURRENT_PREFIX ($managed_symlink_total symlink entries declared) — prefix/manifest drift; the gate would otherwise silently pass" +fi + # --- ExecStart references gate ------------------------------------------- # Walk every unit in the staged systemd/ tree, parse Exec*= absolute # paths, and assert any path that resolves into the release tree is # executable. Resolution is: -# - Direct: path starts with /opt/airplanes-runtime/current/ +# - Direct: path starts with /opt/airplanes/current/ # -> release-local # - Indirect: path is a managed_paths[].link whose target starts with -# /opt/airplanes-runtime/current/ -> release-local +# /opt/airplanes/current/ -> release-local # - Otherwise: out of scope (host-installed binary like /usr/bin/apt-get). systemd_dir="$RELEASE_DIR/systemd" if [[ -d "$systemd_dir" ]]; then diff --git a/runtime-overlay/scripts/lib/install-common.sh b/runtime-overlay/scripts/lib/install-common.sh index ce3e835f14..2a90b9fe2f 100644 --- a/runtime-overlay/scripts/lib/install-common.sh +++ b/runtime-overlay/scripts/lib/install-common.sh @@ -9,7 +9,7 @@ # - runtime-overlay/src/lib/runtime-self-update.sh (state-machine wrapper) # # Boot-time recovery is NOT sourced from here. It is an image-owned POSIX-sh -# pointer shim (/usr/local/lib/airplanes-runtime/recover-shim) that uses only +# pointer shim (/opt/airplanes/libexec/recover-shim) that uses only # base-OS tools so it survives a fully broken overlay — see the image stage # at stage-airplanes/02-install-runtime-overlay/. # @@ -52,9 +52,9 @@ AIRPLANES_RUNTIME_RELEASE_ASSET_DIR="${AIRPLANES_RUNTIME_RELEASE_ASSET_DIR:-}" AIRPLANES_RUNTIME_PROBE_URL_BASE="${AIRPLANES_RUNTIME_PROBE_URL_BASE:-http://127.0.0.1}" # Minisign public key used to verify SHA256SUMS. Ships in the image at -# /usr/share/airplanes/runtime-release.pub (stage 00). Tests point this at a +# /opt/airplanes/libexec/runtime-release.pub (stage 00). Tests point this at a # tmpdir fixture. -AIRPLANES_RUNTIME_MINISIGN_PUBKEY="${AIRPLANES_RUNTIME_MINISIGN_PUBKEY:-/usr/share/airplanes/runtime-release.pub}" +AIRPLANES_RUNTIME_MINISIGN_PUBKEY="${AIRPLANES_RUNTIME_MINISIGN_PUBKEY:-/opt/airplanes/libexec/runtime-release.pub}" # Filesystem root for the runtime overlay tree. Build mode rebases this under # $ROOTFS_DIR; runtime mode uses /. Tests rebase it under a tmpdir so @@ -64,7 +64,7 @@ AIRPLANES_RUNTIME_ROOT="${AIRPLANES_RUNTIME_ROOT:-/}" # Per-check deadline used by the health gates (seconds). AIRPLANES_RUNTIME_HEALTH_DEADLINE="${AIRPLANES_RUNTIME_HEALTH_DEADLINE:-120}" -# Number of historical releases to keep under /opt/airplanes-runtime/releases/ +# Number of historical releases to keep under /opt/airplanes/releases/ # after a successful install (in addition to the new current). 2 = keep one # prior release for fast rollback. AIRPLANES_RUNTIME_RETAIN_RELEASES="${AIRPLANES_RUNTIME_RETAIN_RELEASES:-2}" @@ -520,7 +520,7 @@ airplanes_runtime_verify_manifest_sha() { # directory is `v/` (build-release.sh's contract). We extract # with `--strip-components=1` so the staging target receives the inner # tree directly; the caller passes the absolute on-device release dir -# (`/opt/airplanes-runtime/releases/v/`) so the resulting layout +# (`/opt/airplanes/releases/v/`) so the resulting layout # is the same as a hand-laid release. airplanes_runtime_extract_release_tarball() { @@ -545,13 +545,13 @@ airplanes_runtime_extract_release_tarball() { # --------------------------------------------------------------------------- # # Each manifest entry is one of: -# { mode: symlink, link: , target: } +# { mode: symlink, link: , target: } # { mode: copy, path: , from: , owner: u:g, perm: 0XXX, # post_install: [argv] } # # Symlink targets are required absolute by the schema (decision 2). We # rebase the *link* through TARGET_ROOT for build mode / tests; the symlink -# target is left literal because /opt/airplanes-runtime/current resolves +# target is left literal because /opt/airplanes/current resolves # inside the final image, not inside the staging tmpdir. airplanes_runtime_apply_managed_paths() { @@ -929,7 +929,7 @@ _airplanes_runtime_assert_safe_managed_path() { local norm="$p" [[ "$norm" != "/" ]] && norm="${norm%/}" case "$norm" in - ""|"/"|/usr|/etc|/var|/bin|/sbin|/lib|/lib64|/boot|/opt|/home|/root|/run|/proc|/sys|/dev|/opt/airplanes-runtime|/opt/airplanes-runtime/*) + ""|"/"|/usr|/etc|/var|/bin|/sbin|/lib|/lib64|/boot|/opt|/home|/root|/run|/proc|/sys|/dev|/opt/airplanes|/opt/airplanes/*) echo "ERROR: refusing to operate on critical system path: '$p'" >&2 return 1 ;; @@ -1486,7 +1486,7 @@ airplanes_runtime_flip_current() { echo "ERROR: flip_current: new release dir must be absolute (got: $new_release_dir)" >&2 return 1 fi - local current_link="${target_root}/opt/airplanes-runtime/current" + local current_link="${target_root}/opt/airplanes/current" install -d -m 755 "$(dirname "$current_link")" local tmp="${current_link}.tmp.$$" rm -f -- "$tmp" @@ -1494,21 +1494,24 @@ airplanes_runtime_flip_current() { mv -Tf -- "$tmp" "$current_link" } -# Re-create decoder binary symlinks. Both /usr/bin/readsb and -# /usr/bin/airplanes-978 point at /opt/airplanes-runtime/current/bin/readsb -# (the same binary handles both 1090 and 978 frame consumption when -# invoked under either name; decision 14 — both are symlinks, not -# hardlinks, so the post-flip relink is the canonical refresh). +# Re-create the decoder operator-convenience shims on PATH. The decode +# services exec the binaries by absolute /opt path via their wrapper *_BIN +# defaults; these /usr/local/bin shims exist only so an operator typing +# `readsb` or `dump978-fa` at a shell finds the active release's binary. We +# install under /usr/local/bin (FHS: locally-installed binaries) rather than +# /usr/bin (a tree the OS owns). The shims point through `current`, so they +# follow the atomic flip on their own; the post-flip relink keeps them +# canonical even if a shim was clobbered or predates this layout. airplanes_runtime_relink_decoder_binaries() { local target_root="$1" - local current_bin="/opt/airplanes-runtime/current/bin/readsb" - local link - for link in "/usr/bin/readsb" "/usr/bin/airplanes-978"; do - local abs="${target_root}${link}" + local current_bin_dir="/opt/airplanes/current/bin" + local name + for name in readsb dump978-fa; do + local abs="${target_root}/usr/local/bin/${name}" install -d -m 755 "$(dirname "$abs")" local tmp="${abs}.tmp.$$" rm -f -- "$tmp" - ln -s -- "$current_bin" "$tmp" + ln -s -- "${current_bin_dir}/${name}" "$tmp" mv -Tf -- "$tmp" "$abs" done } @@ -1731,7 +1734,7 @@ PY # counts. The unit-active check (caller adds airplanes-feed.service to the # aggregate probe) covers "started cleanly"; this function adds the # "the running binary is the release's binary" invariant by confirming the -# managed-path symlink for feed-airplanes points into the active release. +# feed-airplanes binary under current/ resolves into the active release. # # Args: # The short sha is currently informational only — the binary-identity proof is @@ -1739,17 +1742,17 @@ PY # keep the arg so a future build-stamped feed binary can be version-probed. _airplanes_runtime_probe_feed_binary_current() { local target_root="$1" - local link="${target_root}/usr/local/share/airplanes/feed-airplanes" - local current="${target_root}/opt/airplanes-runtime/current" - if [[ ! -L "$link" ]]; then - echo "ERROR: feed gate: $link is not a symlink (managed_paths not applied?)" >&2 + local current="${target_root}/opt/airplanes/current" + local bin="${current}/bin/feed-airplanes" + if [[ ! -e "$bin" ]]; then + echo "ERROR: feed gate: $bin missing (overlay not applied?)" >&2 return 1 fi local resolved current_resolved - resolved="$(readlink -f "$link" 2>/dev/null || true)" + resolved="$(readlink -f "$bin" 2>/dev/null || true)" current_resolved="$(readlink -f "$current" 2>/dev/null || true)" if [[ -z "$resolved" || -z "$current_resolved" ]]; then - echo "ERROR: feed gate: could not resolve feed-airplanes ($link) or current ($current)" >&2 + echo "ERROR: feed gate: could not resolve feed-airplanes ($bin) or current ($current)" >&2 return 1 fi if [[ "$resolved" != "$current_resolved"/* ]]; then @@ -1913,7 +1916,7 @@ airplanes_runtime_run_health_gates() { # (a decoder-only release does not ship the feed binary/unit). Read the # active release manifest — `current` already points at the new release at # health-gate time. - local active_manifest="${target_root}/opt/airplanes-runtime/current/manifest.json" + local active_manifest="${target_root}/opt/airplanes/current/manifest.json" local feed_short feed_short="$(_airplanes_runtime_manifest_feed_readsb_short_sha "$active_manifest")" @@ -1949,10 +1952,10 @@ airplanes_runtime_run_health_gates() { fi # UAT services (978): both produce key-value state files. - if ! _airplanes_runtime_probe_uat_state "${target_root}/run/dump978-fa/state" "$deadline"; then + if ! _airplanes_runtime_probe_uat_state "${target_root}/run/airplanes/dump978-fa/state" "$deadline"; then return 1 fi - if ! _airplanes_runtime_probe_uat_state "${target_root}/run/airplanes-978/state" "$deadline"; then + if ! _airplanes_runtime_probe_uat_state "${target_root}/run/airplanes/978/state" "$deadline"; then return 1 fi @@ -2064,7 +2067,7 @@ except Exception: _airplanes_runtime_read_installed_feed_contract() { local target_root="$1" local candidates=( - "${target_root}/usr/local/share/airplanes/lib/feed-contract-version" + "${target_root}/opt/airplanes/current/share/airplanes/lib/feed-contract-version" "${target_root}/etc/airplanes/feed-contract" ) local f @@ -2146,7 +2149,7 @@ _airplanes_runtime_read_python_abi() { # integer byte count, or empty if it can't be determined. _airplanes_runtime_free_bytes_for_releases() { local target_root="$1" - local dir="${target_root}/opt/airplanes-runtime/releases" + local dir="${target_root}/opt/airplanes/releases" # Walk up to the nearest existing ancestor — the releases dir may not # exist yet on a first install. while [[ ! -d "$dir" && -n "$dir" && "$dir" != "/" ]]; do @@ -2301,7 +2304,7 @@ airplanes_runtime_record_runtime_manifest() { # Build mode: write a regular file copy so host-side consumers (like # scripts/manifest-generator.sh running in stage 07-finalize, outside # the chroot) can read the manifest content. The symlink form points - # at /opt/airplanes-runtime/current/manifest.json which the host + # at /opt/airplanes/current/manifest.json which the host # cannot resolve when target_root != /. The first runtime self-update # on-device replaces this file with the symlink via `mv -Tf`, so the # auto-follow-current semantics take over once the device is live. @@ -2309,7 +2312,7 @@ airplanes_runtime_record_runtime_manifest() { # The `current` symlink target is an absolute path rooted at the # device's view of /, so resolve it manually against target_root # rather than letting `readlink -f` follow it on the host. - local current="${target_root}/opt/airplanes-runtime/current" + local current="${target_root}/opt/airplanes/current" local current_target current_target="$(readlink -- "$current")" || { echo "ERROR: ${current} is not a symlink (current pointer missing)" >&2 @@ -2323,7 +2326,7 @@ airplanes_runtime_record_runtime_manifest() { cp -- "$manifest_src" "$tmp" chmod 0644 "$tmp" else - ln -s -- "/opt/airplanes-runtime/current/manifest.json" "$tmp" + ln -s -- "/opt/airplanes/current/manifest.json" "$tmp" fi mv -Tf -- "$tmp" "$link" @@ -2333,14 +2336,14 @@ airplanes_runtime_record_runtime_manifest() { # Last-good-release pointer # --------------------------------------------------------------------------- # -# Image-owned file at /var/lib/airplanes-runtime/last-good-release recording +# Image-owned file at /var/lib/airplanes/runtime/last-good-release recording # the device-canonical path of the most recent release that passed its health # gates. The boot recovery shim uses it as the rollback target when the state # file's prev_release is missing or invalid. Written at (not after) the # HEALTH_PASSED transition so a reboot in the cleanup window cannot leave it # stale relative to a known-good release. -AIRPLANES_RUNTIME_LAST_GOOD_REL="${AIRPLANES_RUNTIME_LAST_GOOD_REL:-var/lib/airplanes-runtime/last-good-release}" +AIRPLANES_RUNTIME_LAST_GOOD_REL="${AIRPLANES_RUNTIME_LAST_GOOD_REL:-var/lib/airplanes/runtime/last-good-release}" airplanes_runtime_write_last_good_release() { local target_root="$1" on_device_release="$2" @@ -2447,16 +2450,16 @@ airplanes_runtime_finalize_after_health_passed() { # --------------------------------------------------------------------------- # # Keep at most AIRPLANES_RUNTIME_RETAIN_RELEASES under -# /opt/airplanes-runtime/releases/. The current-pointed release is always +# /opt/airplanes/releases/. The current-pointed release is always # retained even if it would otherwise be GC'd. airplanes_runtime_gc_old_releases() { local target_root="$1" - local releases_dir="${target_root}/opt/airplanes-runtime/releases" + local releases_dir="${target_root}/opt/airplanes/releases" [[ -d "$releases_dir" ]] || return 0 local current_target="" - if [[ -L "${target_root}/opt/airplanes-runtime/current" ]]; then - current_target="$(readlink -f "${target_root}/opt/airplanes-runtime/current")" + if [[ -L "${target_root}/opt/airplanes/current" ]]; then + current_target="$(readlink -f "${target_root}/opt/airplanes/current")" fi # List release dirs sorted by mtime newest-first, drop the top N. local -a victims=() @@ -2539,7 +2542,7 @@ airplanes_runtime_run_install_steps() { # # Helpers consumed by the self-update orchestrator and the boot-time recovery # oneshot. The state file lives at -# ${target_root}/var/lib/airplanes-runtime-upgrade/upgrade-state and persists +# ${target_root}/var/lib/airplanes/runtime-upgrade/upgrade-state and persists # the position of an in-flight upgrade across a power loss so the boot-time # recovery script can finish or undo whatever the orchestrator started. # @@ -2558,7 +2561,7 @@ airplanes_runtime_run_install_steps() { # Path constants; uppercase so a caller can override per test (the test # fixture rebases STATE_DIR under BATS_TEST_TMPDIR). -AIRPLANES_RUNTIME_STATE_DIR_REL="${AIRPLANES_RUNTIME_STATE_DIR_REL:-var/lib/airplanes-runtime-upgrade}" +AIRPLANES_RUNTIME_STATE_DIR_REL="${AIRPLANES_RUNTIME_STATE_DIR_REL:-var/lib/airplanes/runtime-upgrade}" AIRPLANES_RUNTIME_STATE_FILE_NAME="${AIRPLANES_RUNTIME_STATE_FILE_NAME:-upgrade-state}" AIRPLANES_RUNTIME_LOCK_FILE="${AIRPLANES_RUNTIME_LOCK_FILE:-/run/airplanes/runtime-update.lock}" diff --git a/runtime-overlay/scripts/lib/stage-feed.sh b/runtime-overlay/scripts/lib/stage-feed.sh index 42fd563e03..abb5b79da5 100644 --- a/runtime-overlay/scripts/lib/stage-feed.sh +++ b/runtime-overlay/scripts/lib/stage-feed.sh @@ -218,8 +218,8 @@ for src in "${feed_units[@]}"; do done # Gate airplanes-mlat.service on the prebuilt venv this overlay ships. The -# wrapper execs /usr/local/share/airplanes/venv/bin/mlat-client; without the -# venv the unit would restart-loop on a missing interpreter. ConditionPathExists +# wrapper execs /opt/airplanes/current/share/airplanes/venv/bin/mlat-client; without +# the venv the unit would restart-loop on a missing interpreter. ConditionPathExists # makes systemd skip the unit cleanly (inactive, condition-failed) on a # decoder-only release rather than start-fail it. We inject the condition here # (overlay side, where the venv is owned) rather than in the feed repo, whose @@ -233,7 +233,7 @@ if [[ -f "$mlat_unit" ]] && ! grep -q '^ConditionPathExists=' "$mlat_unit"; then /^\[Unit\]/ { print; in_unit = 1; next } in_unit && /^Description=/ { print - print "ConditionPathExists=/usr/local/share/airplanes/venv/bin/mlat-client" + print "ConditionPathExists=/opt/airplanes/current/share/airplanes/venv/bin/mlat-client" next } /^\[/ && !/^\[Unit\]/ { in_unit = 0 } @@ -288,16 +288,16 @@ printf '%s' "${FEED_REF}" > "$OUTPUT_DIR/components.feed_scripts.version" # # A Python venv embeds the absolute path it was created at into every # console-script shebang (and into pyvenv.cfg). The airplanes-mlat wrapper -# execs /usr/local/share/airplanes/venv/bin/mlat-client, so the venv MUST be -# built at that exact path inside the container — not at a relative or +# execs /opt/airplanes/current/share/airplanes/venv/bin/mlat-client, so the venv +# MUST be built at that exact path inside the container — not at a relative or # container-scoped path — or the on-device shebangs would point at a directory # that does not exist. We build there, verify the shebangs, then copy the tree # verbatim into the overlay staging dir (cp -a does not rewrite shebangs). # The venv MUST live at the path the airplanes-mlat wrapper execs. Overridable -# only for tests (which can't write under /usr/local without root); production -# always builds at the real on-device path so shebangs resolve. -VENV_TARGET="${AIRPLANES_VENV_TARGET:-/usr/local/share/airplanes/venv}" +# only for tests (which can't write under /opt without root); production always +# builds at the real on-device path so shebangs resolve. +VENV_TARGET="${AIRPLANES_VENV_TARGET:-/opt/airplanes/current/share/airplanes/venv}" # Test seam: a local source dir short-circuits the network clone. Production # always clones the pinned mlat-client ref. MLAT_SRC="${AIRPLANES_MLAT_SRC_DIR:-$SCRATCH_DIR/mlat-src}" diff --git a/runtime-overlay/scripts/lib/stage-graphs1090.sh b/runtime-overlay/scripts/lib/stage-graphs1090.sh index 141a39ff76..f1b04ca75a 100755 --- a/runtime-overlay/scripts/lib/stage-graphs1090.sh +++ b/runtime-overlay/scripts/lib/stage-graphs1090.sh @@ -15,7 +15,7 @@ # share/graphs1090/ $ipath tree install.sh produces # share/graphs1090/978-symlink/.gitkeep directory placeholder; the # inner data symlink to -# /run/airplanes-978 is +# /run/airplanes/978 is # runtime-only # systemd/graphs1090.service the unit (paths are # /usr/share/graphs1090, which @@ -252,7 +252,7 @@ if [[ ! -d "$OUTPUT_DIR/share/graphs1090/978-symlink" ]]; then # Defensive fallback: if install.sh skipped the mkdir for some reason, # create it so the runtime install.sh's symlink-target check has a # stable parent. The on-device install creates the inner `data` link - # pointing at /run/airplanes-978. + # pointing at /run/airplanes/978. install -d -m 0755 "$OUTPUT_DIR/share/graphs1090/978-symlink" # Non-hidden placeholder so actions/upload-artifact (which drops # hidden files by default) retains the otherwise-empty directory. diff --git a/runtime-overlay/scripts/lib/stage-tar1090.sh b/runtime-overlay/scripts/lib/stage-tar1090.sh index dbfb77635f..619d49f003 100755 --- a/runtime-overlay/scripts/lib/stage-tar1090.sh +++ b/runtime-overlay/scripts/lib/stage-tar1090.sh @@ -34,7 +34,7 @@ # snippet whose HTMLPATH token is replaced with $ipath. Passing $ipath as # /usr/local/share/tar1090 means the produced unit + lighttpd snippet # already carry the on-device absolute paths. The runtime-overlay -# install.sh symlinks /usr/local/share/tar1090 → /opt/airplanes-runtime/ +# install.sh symlinks /usr/local/share/tar1090 → /opt/airplanes/ # current/share/tar1090 on-device, so those absolute paths resolve # correctly without a per-release rewrite. # diff --git a/runtime-overlay/scripts/release-workflow/write-provenance.sh b/runtime-overlay/scripts/release-workflow/write-provenance.sh index 5842c9dfe0..097e9198df 100755 --- a/runtime-overlay/scripts/release-workflow/write-provenance.sh +++ b/runtime-overlay/scripts/release-workflow/write-provenance.sh @@ -95,7 +95,7 @@ trap 'rm -f -- "$tmp"' EXIT printf '## Verification\n\n' printf 'Verify the release before installing:\n\n' printf '```\n' - printf 'minisign -V -p /usr/share/airplanes/runtime-release.pub \\\n' + printf 'minisign -V -p /opt/airplanes/libexec/runtime-release.pub \\\n' printf ' -x runtime-SHA256SUMS.minisig -m runtime-SHA256SUMS\n' printf 'sha256sum -c runtime-SHA256SUMS\n' printf '```\n' diff --git a/runtime-overlay/scripts/validate-manifest.sh b/runtime-overlay/scripts/validate-manifest.sh index 6df0c6ba56..00b4bae10d 100755 --- a/runtime-overlay/scripts/validate-manifest.sh +++ b/runtime-overlay/scripts/validate-manifest.sh @@ -143,4 +143,29 @@ if [[ -n "$overlap_report" ]]; then exit 1 fi +# 5. Cross-field gate: managed destinations must not squat a filesystem tree we +# don't own. The device payload lives under /opt/airplanes; a managed symlink +# or copy landing in /usr/bin, /usr/local/lib/airplanes*, /usr/local/share/airplanes*, +# or /usr/share/airplanes* means the overlay is writing into OS- or +# distribution-owned trees. /usr/local/bin launchers and the kept third-party +# read-only data dirs (/usr/local/share/tar1090, /usr/share/graphs1090) are fine. +squat_report="$(jq -r ' + [ (.managed_paths // [])[] + | (if .mode == "symlink" then .link elif .mode == "copy" then .path else empty end) as $d + | select($d != null) + | select( + ($d | startswith("/usr/bin/")) or + ($d | startswith("/usr/local/lib/airplanes")) or + ($d | startswith("/usr/local/share/airplanes")) or + ($d | startswith("/usr/share/airplanes")) + ) + | $d ] + | join("; ") +' "$snapshot")" + +if [[ -n "$squat_report" ]]; then + echo "validate-manifest: managed destination squats a tree we do not own (FHS): ${squat_report}" >&2 + exit 1 +fi + exit 0 diff --git a/runtime-overlay/src/etc/lighttpd/conf-available/89-airplanes-978.conf b/runtime-overlay/src/etc/lighttpd/conf-available/89-airplanes-978.conf index 1c40066f64..abc77b293e 100644 --- a/runtime-overlay/src/etc/lighttpd/conf-available/89-airplanes-978.conf +++ b/runtime-overlay/src/etc/lighttpd/conf-available/89-airplanes-978.conf @@ -1,5 +1,5 @@ -# Map http://feeder/skyaware978/ → /run/airplanes-978/ so tar1090's UAT view +# Map http://feeder/skyaware978/ → /run/airplanes/978/ so tar1090's UAT view # (default URL_978="http://127.0.0.1/skyaware978") resolves to airplanes-978's # JSON output. Harmless when UAT_INPUT is empty / 978 is disabled (the dir # is missing/empty). -alias.url += ( "/skyaware978" => "/run/airplanes-978" ) +alias.url += ( "/skyaware978" => "/run/airplanes/978" ) diff --git a/runtime-overlay/src/etc/update-motd.d/10-airplanes-status b/runtime-overlay/src/etc/update-motd.d/10-airplanes-status index 1fba619b08..6e0759f6cb 100755 --- a/runtime-overlay/src/etc/update-motd.d/10-airplanes-status +++ b/runtime-overlay/src/etc/update-motd.d/10-airplanes-status @@ -11,4 +11,4 @@ unset "${!PATHS_@}" PATH=/usr/sbin:/usr/bin:/sbin:/bin export PATH -exec /usr/local/lib/airplanes/render-status --snapshot +exec /opt/airplanes/current/lib/airplanes/render-status --snapshot diff --git a/runtime-overlay/src/lib/airplanes-update-orchestrator b/runtime-overlay/src/lib/airplanes-update-orchestrator index c0cb8ee885..6b0862cbce 100755 --- a/runtime-overlay/src/lib/airplanes-update-orchestrator +++ b/runtime-overlay/src/lib/airplanes-update-orchestrator @@ -48,8 +48,8 @@ readonly _self_dir : "${AIRPLANES_ORCHESTRATOR_STATE_FILE:=/run/airplanes/orchestrator.state}" : "${AIRPLANES_ORCHESTRATOR_LOCK_FILE:=/run/airplanes/orchestrator.lock}" -: "${AIRPLANES_ORCHESTRATOR_RUNTIME_UPDATE:=/opt/airplanes-runtime/current/lib/runtime-self-update.sh}" -: "${AIRPLANES_ORCHESTRATOR_RUNTIME_UPGRADE_STATE:=/var/lib/airplanes-runtime-upgrade/upgrade-state}" +: "${AIRPLANES_ORCHESTRATOR_RUNTIME_UPDATE:=/opt/airplanes/current/lib/runtime-self-update.sh}" +: "${AIRPLANES_ORCHESTRATOR_RUNTIME_UPGRADE_STATE:=/var/lib/airplanes/runtime-upgrade/upgrade-state}" : "${AIRPLANES_ORCHESTRATOR_APT_GET:=/usr/bin/apt-get}" readonly STATE_FILE="$AIRPLANES_ORCHESTRATOR_STATE_FILE" diff --git a/runtime-overlay/src/lib/airplanes/render-status b/runtime-overlay/src/lib/airplanes/render-status index c81b16635e..b77758f410 100755 --- a/runtime-overlay/src/lib/airplanes/render-status +++ b/runtime-overlay/src/lib/airplanes/render-status @@ -41,16 +41,16 @@ LC_ALL=C : "${PATHS_AIRCRAFT_JSON:=/run/readsb/aircraft.json}" : "${PATHS_READSB_STATS:=/run/readsb/stats.json}" : "${PATHS_THERMAL:=/sys/class/thermal/thermal_zone0/temp}" -: "${PATHS_LOGO:=/usr/local/share/airplanes/logo.txt}" -: "${PATHS_BANNER:=/usr/local/share/airplanes/banner.txt}" -: "${PATHS_BANNER_NARROW:=/usr/local/share/airplanes/banner-narrow.txt}" -: "${PATHS_ICON:=/usr/local/share/airplanes/icon.txt}" -: "${PATHS_STATE_FILE_MLAT:=/run/airplanes-mlat/state}" -: "${PATHS_STATE_FILE_FEED:=/run/airplanes-feed/state}" -: "${PATHS_STATE_FILE_978:=/run/airplanes-978/state}" -: "${PATHS_STATE_FILE_DUMP978FA:=/run/dump978-fa/state}" +: "${PATHS_LOGO:=/opt/airplanes/current/share/airplanes/logo.txt}" +: "${PATHS_BANNER:=/opt/airplanes/current/share/airplanes/banner.txt}" +: "${PATHS_BANNER_NARROW:=/opt/airplanes/current/share/airplanes/banner-narrow.txt}" +: "${PATHS_ICON:=/opt/airplanes/current/share/airplanes/icon.txt}" +: "${PATHS_STATE_FILE_MLAT:=/run/airplanes/mlat/state}" +: "${PATHS_STATE_FILE_FEED:=/run/airplanes/feed/state}" +: "${PATHS_STATE_FILE_978:=/run/airplanes/978/state}" +: "${PATHS_STATE_FILE_DUMP978FA:=/run/airplanes/dump978-fa/state}" : "${PATHS_STATE_FILE_READSB:=/run/readsb/state}" -: "${PATHS_STATE_READER_LIB:=/usr/local/share/airplanes/lib/state-reader.sh}" +: "${PATHS_STATE_READER_LIB:=/opt/airplanes/current/share/airplanes/lib/state-reader.sh}" : "${PATHS_FEED_ENV:=/etc/airplanes/feed.env}" : "${PATHS_SYSFS_NET:=/sys/class/net}" # Absolute path: the MOTD wrapper deliberately strips /usr/local/bin from @@ -265,7 +265,7 @@ claim_state() { # state ∈ { enabled, disabled, misconfigured, failed, inactive, unknown } # reason is a stable token; "-" when no reason applies. # -# Daemon publishes its decision to /run/airplanes-mlat/state when active or +# Daemon publishes its decision to /run/airplanes/mlat/state when active or # transitioning. For the failed terminal state with ExecMainStatus=64 # (strict misconfig fail), the file persists across the failure via # RuntimeDirectoryPreserve=yes and we surface its reason. Otherwise we @@ -301,9 +301,9 @@ mlat_config_state() { } # Classify a 978-family daemon's runtime decision. The two units have -# separate state files (dump978-fa.sh owns /run/dump978-fa/state with the +# separate state files (dump978-fa.sh owns /run/airplanes/dump978-fa/state with the # producer's hardware-probe reasons; airplanes-978.sh owns the existing -# /run/airplanes-978/state with the consumer's classifier including +# /run/airplanes/978/state with the consumer's classifier including # peer_no_hardware refined from the producer file). Echoes # " " on a single line. The failed-branch consults the # unit-specific ExecMainStatus so a self-disable (exit 64) is treated as a @@ -368,7 +368,7 @@ readsb_config_state() { # Compute SD_MLAT_NOTE from the airplanes-mlat daemon's published state. # Caller passes the unit's ActiveState (typically from _unit_prop). Reads -# only the daemon-owned /run/airplanes-mlat/state — never feed.env. Empty +# only the daemon-owned /run/airplanes/mlat/state — never feed.env. Empty # note means no message to display (e.g. enabled-ok, or daemon never ran). # On a fresh image MLAT_ENABLED defaults to false AND geo is unset; the # daemon's classifier reports reason=mlat_enabled_false (MLAT_ENABLED wins @@ -1096,7 +1096,7 @@ collect_status_data() { esac # MLAT note. Set by _compute_mlat_note from the daemon's published - # decision in /run/airplanes-mlat/state — no feed.env reads. + # decision in /run/airplanes/mlat/state — no feed.env reads. _compute_mlat_note "$(_unit_prop airplanes-mlat.service ActiveState)" # SD_MLAT_WARN aliases SD_MLAT_NOTE so the panel renderers below # don't need wholesale edits — both names point at the same content. diff --git a/runtime-overlay/src/lib/runtime-self-update.sh b/runtime-overlay/src/lib/runtime-self-update.sh index 22f66c2f98..5ca2522c40 100755 --- a/runtime-overlay/src/lib/runtime-self-update.sh +++ b/runtime-overlay/src/lib/runtime-self-update.sh @@ -4,7 +4,7 @@ # recoverable. Two recovery actors share the persisted state: # - the rich in-process rollback (roll_back_and_exit) while this script runs; # - an image-owned POSIX-sh pointer shim at boot -# (/usr/local/lib/airplanes-runtime/recover-shim), the last-resort floor +# (/opt/airplanes/libexec/recover-shim), the last-resort floor # that only flips `current` back to the last-good release using base-OS # tools — it deliberately does NOT run migrations/cleanup. # A HEALTH_PASSED interruption (cleanup/GC not finished) is resumed by the @@ -34,8 +34,8 @@ _lib_candidates=( "${AIRPLANES_RUNTIME_INSTALL_COMMON:-}" "${_self_dir}/../../scripts/lib/install-common.sh" "${_self_dir}/../scripts/lib/install-common.sh" - "/opt/airplanes-runtime/current/scripts/lib/install-common.sh" - "/usr/local/lib/airplanes-runtime/install-common.sh" + "/opt/airplanes/current/scripts/lib/install-common.sh" + "/opt/airplanes/libexec/install-common.sh" ) _lib="" for _candidate in "${_lib_candidates[@]}"; do @@ -170,11 +170,11 @@ fi # Resolve release version → release 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}" # Snapshot the pre-update current symlink target so rollback knows where # to flip back to. Empty if there is no current yet (first install). -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 @@ -284,7 +284,7 @@ roll_back_and_exit() { airplanes_runtime_flip_current "$prev_on_device" "$TARGET_ROOT" || true airplanes_runtime_relink_decoder_binaries "$TARGET_ROOT" || true else - rm -f -- "${TARGET_ROOT}/opt/airplanes-runtime/current" + rm -f -- "${TARGET_ROOT}/opt/airplanes/current" fi ;; esac diff --git a/runtime-overlay/src/migrations/0001-create-service-accounts.sh b/runtime-overlay/src/migrations/0001-create-service-accounts.sh index 447bba5a15..f717f112ec 100755 --- a/runtime-overlay/src/migrations/0001-create-service-accounts.sh +++ b/runtime-overlay/src/migrations/0001-create-service-accounts.sh @@ -52,14 +52,14 @@ if ! getent group airplanes-feed >/dev/null; then fi if ! getent passwd airplanes-feed >/dev/null; then adduser --system --ingroup airplanes-feed \ - --home /usr/local/share/airplanes --no-create-home --quiet airplanes-feed + --home /opt/airplanes/current/share/airplanes --no-create-home --quiet airplanes-feed fi # airplanes-aggregator: the airplanes-aggregator@.service template runs # vendor third-party feeder code under this account (never root). Parity with # stage-airplanes/05-install-webconfig/01-run-chroot.sh. The unit's -# StateDirectory= provisions /var/lib/airplanes-aggregators at start, so only -# the account (with its private group) is created here. +# StateDirectory= provisions /var/lib/airplanes/aggregators/state at start, so +# only the account (with its private group) is created here. if ! getent passwd airplanes-aggregator >/dev/null; then adduser --system --no-create-home --group airplanes-aggregator fi diff --git a/runtime-overlay/src/share/airplanes/airplanes-978.sh b/runtime-overlay/src/share/airplanes/airplanes-978.sh index ea8a7acc22..5b599b374f 100755 --- a/runtime-overlay/src/share/airplanes/airplanes-978.sh +++ b/runtime-overlay/src/share/airplanes/airplanes-978.sh @@ -1,7 +1,7 @@ #!/bin/bash # Wrapper for airplanes-978 (UAT relay). Reads UAT_INPUT from the # EnvironmentFile-loaded environment to decide whether to run, and -# publishes that decision to /run/airplanes-978/state for consumers +# publishes that decision to /run/airplanes/978/state for consumers # (apl-feed status, render-status, webconfig dashboard). # # Decision matrix (state, reason): @@ -41,11 +41,11 @@ LONGITUDE="${LONGITUDE:-0}" UAT_INPUT="${UAT_INPUT-}" # Test hooks. Bats overrides these to skip real /run paths and stub the binary. -: "${AIRPLANES_978_RUNTIME_DIR:=/run/airplanes-978}" -: "${AIRPLANES_978_BIN:=/usr/bin/airplanes-978}" -: "${STATE_WRITER_LIB:=/usr/local/share/airplanes/lib/state-writer.sh}" -: "${STATE_READER_LIB:=/usr/local/share/airplanes/lib/state-reader.sh}" -: "${DUMP978_FA_STATE_FILE:=/run/dump978-fa/state}" +: "${AIRPLANES_978_RUNTIME_DIR:=/run/airplanes/978}" +: "${AIRPLANES_978_BIN:=/opt/airplanes/current/bin/readsb}" +: "${STATE_WRITER_LIB:=/opt/airplanes/current/share/airplanes/lib/state-writer.sh}" +: "${STATE_READER_LIB:=/opt/airplanes/current/share/airplanes/lib/state-reader.sh}" +: "${DUMP978_FA_STATE_FILE:=/run/airplanes/dump978-fa/state}" : "${AIRPLANES_978_FEED_ENV:=/etc/airplanes/feed.env}" # Watch poll interval for the disabled branch. Bats sets 0 so wrapper # invocations return promptly. Not for feed.env (see header comment). @@ -174,7 +174,10 @@ esac # silent_fail on the connector: dump978-fa restarts or is absent during 978-off # state shouldn't spam logs. With Wants=dump978-fa.service (non-blocking), # silent_fail is the right safety net. -exec "$AIRPLANES_978_BIN" \ +# exec -a preserves argv[0]="airplanes-978" now that the binary is the shared +# readsb under /opt (the old /usr/bin/airplanes-978 name-symlink is gone), so +# `ps` and any argv[0]-keyed readsb behaviour stay identical to the 1090 path. +exec -a airplanes-978 "$AIRPLANES_978_BIN" \ --net-only \ --max-range 460 \ --net \ diff --git a/runtime-overlay/src/share/airplanes/dump978-fa.sh b/runtime-overlay/src/share/airplanes/dump978-fa.sh index d656d7b4da..f14d6e0bae 100755 --- a/runtime-overlay/src/share/airplanes/dump978-fa.sh +++ b/runtime-overlay/src/share/airplanes/dump978-fa.sh @@ -2,7 +2,7 @@ # Wrapper for dump978-fa (978 MHz UAT receiver). Reads UAT_INPUT and # DUMP978_SDR_SERIAL from the EnvironmentFile-loaded environment, runs a # non-mutating USB-serial probe so the daemon does not thrash on hardware -# without a 978 dongle, and publishes the decision to /run/dump978-fa/state +# without a 978 dongle, and publishes the decision to /run/airplanes/dump978-fa/state # for consumers (apl-feed status, render-status, webconfig dashboard). # # Decision matrix (state, reason): @@ -43,9 +43,9 @@ DUMP978_RAW_BIND="${DUMP978_RAW_BIND:-127.0.0.1}" DUMP978_JSON_BIND="${DUMP978_JSON_BIND:-127.0.0.1}" # Test hooks. Bats overrides these to skip real /run paths and stub the binary. -: "${DUMP978_FA_RUNTIME_DIR:=/run/dump978-fa}" -: "${DUMP978_FA_BIN:=/usr/bin/dump978-fa}" -: "${STATE_WRITER_LIB:=/usr/local/share/airplanes/lib/state-writer.sh}" +: "${DUMP978_FA_RUNTIME_DIR:=/run/airplanes/dump978-fa}" +: "${DUMP978_FA_BIN:=/opt/airplanes/current/bin/dump978-fa}" +: "${STATE_WRITER_LIB:=/opt/airplanes/current/share/airplanes/lib/state-writer.sh}" # Probe override: glob expanded for USB serial files. Tests point this at a # temp dir; production reads /sys/bus/usb/devices/*/serial. : "${DUMP978_FA_USB_SERIAL_GLOB:=/sys/bus/usb/devices/*/serial}" diff --git a/runtime-overlay/src/share/airplanes/readsb.sh b/runtime-overlay/src/share/airplanes/readsb.sh index 82e8e8e62f..153c96760e 100755 --- a/runtime-overlay/src/share/airplanes/readsb.sh +++ b/runtime-overlay/src/share/airplanes/readsb.sh @@ -36,7 +36,7 @@ READSB_NET_OPTIONS="${READSB_NET_OPTIONS:-"--net-bi-port 30004,30104"}" # same limitation as dump978-fa; a pin that collides with the 978 serial is # caught earlier by the webconfig validator. : "${READSB_RUNTIME_DIR:=/run/readsb}" -: "${STATE_WRITER_LIB:=/usr/local/share/airplanes/lib/state-writer.sh}" +: "${STATE_WRITER_LIB:=/opt/airplanes/current/share/airplanes/lib/state-writer.sh}" # Probe override: glob expanded for USB serial files. Tests point this at a # temp dir; production reads /sys/bus/usb/devices/*/serial. : "${READSB_USB_SERIAL_GLOB:=/sys/bus/usb/devices/*/serial}" @@ -150,5 +150,5 @@ args+=( --write-json /run/readsb --quiet ) # Test seam — mirrors the ${AIRPLANES_PYTHON_BIN} pattern in feed's # scripts/lib/update-builds.sh so bats can intercept the exec without PATH # manipulation. Production behavior unchanged when READSB_BIN is unset. -READSB_BIN="${READSB_BIN:-/usr/bin/readsb}" +READSB_BIN="${READSB_BIN:-/opt/airplanes/current/bin/readsb}" exec "$READSB_BIN" "${args[@]}" diff --git a/runtime-overlay/src/share/airplanes/tar1090-uat-sync.sh b/runtime-overlay/src/share/airplanes/tar1090-uat-sync.sh index 8c4b8728c3..b1ba01775d 100755 --- a/runtime-overlay/src/share/airplanes/tar1090-uat-sync.sh +++ b/runtime-overlay/src/share/airplanes/tar1090-uat-sync.sh @@ -1,7 +1,7 @@ #!/bin/bash # Reconcile tar1090's ENABLE_978 with the airplanes-978 + dump978-fa runtime -# state. Driven by airplanes-tar1090-uat-sync.path (watches /run/airplanes-978/state -# and /run/dump978-fa/state) and run once at boot via the matching .service. +# state. Driven by airplanes-tar1090-uat-sync.path (watches /run/airplanes/978/state +# and /run/airplanes/dump978-fa/state) and run once at boot via the matching .service. # # tar1090's main loop tries to prune 978.json on every iteration when # ENABLE_978=yes and emits "978.json: No such file or directory" through bash's @@ -27,12 +27,12 @@ set -eu # Test hooks — bats overrides these to stub paths and the restart command. : "${AIRPLANES_TAR1090_DEFAULTS_PATH:=/etc/default/tar1090}" -: "${AIRPLANES_978_STATE_PATH:=/run/airplanes-978/state}" -: "${DUMP978_FA_STATE_PATH:=/run/dump978-fa/state}" +: "${AIRPLANES_978_STATE_PATH:=/run/airplanes/978/state}" +: "${DUMP978_FA_STATE_PATH:=/run/airplanes/dump978-fa/state}" : "${AIRPLANES_TAR1090_SYNC_RESTART_CMD:=systemctl try-restart tar1090.service}" # State file parser is inline rather than sourced from -# /usr/local/share/airplanes/lib/state-reader.sh: that library ships from +# /opt/airplanes/current/share/airplanes/lib/state-reader.sh: that library ships from # airplanes-live/feed, and (a) the reconcile only needs two fields, (b) # we need to read each file as a single snapshot to avoid mixing fields # from different atomic-rename generations, and (c) keeping zero runtime diff --git a/runtime-overlay/src/systemd/airplanes-978.service b/runtime-overlay/src/systemd/airplanes-978.service index 94d2a61d26..a691af47c7 100644 --- a/runtime-overlay/src/systemd/airplanes-978.service +++ b/runtime-overlay/src/systemd/airplanes-978.service @@ -7,15 +7,15 @@ Wants=network.target dump978-fa.service Type=simple EnvironmentFile=-/etc/airplanes/feed.env User=readsb -RuntimeDirectory=airplanes-978 +RuntimeDirectory=airplanes/978 RuntimeDirectoryMode=0755 -# Wrapper writes /run/airplanes-978/state, then either execs airplanes-978, +# Wrapper writes /run/airplanes/978/state, then either execs airplanes-978, # sleeps for uat_disabled (exit 0, unit stays active), or exits 64 when # UAT_INPUT is invalid. Preserve=yes keeps the state file across wrapper # re-runs so /api/status can keep reading decision=disabled. Symmetric # with airplanes-mlat.service. RuntimeDirectoryPreserve=yes -ExecStart=/usr/local/share/airplanes/airplanes-978.sh +ExecStart=/opt/airplanes/current/share/airplanes/airplanes-978.sh Restart=always RestartSec=15 # Misconfigured-input branch in the wrapper exits 64; this prevents systemd diff --git a/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.path b/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.path index 4e1838831d..0525670e72 100644 --- a/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.path +++ b/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.path @@ -2,8 +2,8 @@ Description=Watch airplanes-978 + dump978-fa runtime state for tar1090 sync [Path] -PathChanged=/run/airplanes-978/state -PathChanged=/run/dump978-fa/state +PathChanged=/run/airplanes/978/state +PathChanged=/run/airplanes/dump978-fa/state Unit=airplanes-tar1090-uat-sync.service [Install] diff --git a/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.service b/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.service index 0ad1871c5b..445c12699d 100644 --- a/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.service +++ b/runtime-overlay/src/systemd/airplanes-tar1090-uat-sync.service @@ -4,12 +4,12 @@ After=airplanes-first-run.service network.target [Service] Type=oneshot -# Reads /run/airplanes-978/state + /run/dump978-fa/state + /etc/default/tar1090. +# Reads /run/airplanes/978/state + /run/airplanes/dump978-fa/state + /etc/default/tar1090. # Rewrites /etc/default/tar1090 (preserving mode/owner) and invokes # `systemctl try-restart tar1090.service` only when ENABLE_978 actually # changes. Idempotent — safe to fire repeatedly from the matching .path # unit. -ExecStart=/usr/local/share/airplanes/tar1090-uat-sync.sh +ExecStart=/opt/airplanes/current/share/airplanes/tar1090-uat-sync.sh [Install] WantedBy=multi-user.target diff --git a/runtime-overlay/src/systemd/dump978-fa.service b/runtime-overlay/src/systemd/dump978-fa.service index 866cf6d8b3..532c6256ca 100644 --- a/runtime-overlay/src/systemd/dump978-fa.service +++ b/runtime-overlay/src/systemd/dump978-fa.service @@ -6,15 +6,15 @@ Wants=network.target [Service] Type=simple EnvironmentFile=-/etc/airplanes/feed.env -RuntimeDirectory=dump978-fa +RuntimeDirectory=airplanes/dump978-fa RuntimeDirectoryMode=0755 -# Wrapper writes /run/dump978-fa/state, then either execs dump978-fa, sleeps +# Wrapper writes /run/airplanes/dump978-fa/state, then either execs dump978-fa, sleeps # for the disabled cases (uat_disabled / no_hardware → exit 0, unit stays # active), or exits 64 for misconfigured input (UAT_INPUT invalid). # Preserve=yes keeps the state file across wrapper re-runs so /api/status # can keep reading decision=disabled. Symmetric with airplanes-mlat.service. RuntimeDirectoryPreserve=yes -ExecStart=/usr/local/share/airplanes/dump978-fa.sh +ExecStart=/opt/airplanes/current/share/airplanes/dump978-fa.sh SyslogIdentifier=dump978-fa Restart=always RestartSec=15 diff --git a/runtime-overlay/src/systemd/readsb.service b/runtime-overlay/src/systemd/readsb.service index 005246e9dc..cb34e4a8f9 100644 --- a/runtime-overlay/src/systemd/readsb.service +++ b/runtime-overlay/src/systemd/readsb.service @@ -16,7 +16,7 @@ RuntimeDirectoryMode=0755 # dir, so a stale aircraft.json/stats.prom doesn't linger until reboot. Mode # 0755 keeps the state file readable by the webconfig service user. RuntimeDirectoryPreserve=restart -ExecStart=/usr/local/share/airplanes/readsb.sh +ExecStart=/opt/airplanes/current/share/airplanes/readsb.sh Restart=always RestartSec=15 Nice=-5 diff --git a/runtime-overlay/update.sh b/runtime-overlay/update.sh index fe01aacf1c..670179e3f5 100755 --- a/runtime-overlay/update.sh +++ b/runtime-overlay/update.sh @@ -2,7 +2,7 @@ # update.sh — thin on-device update entrypoint for the runtime overlay. # # Called by the sudoers-pinned self-update helper at -# /opt/airplanes-runtime/current/lib/runtime-self-update.sh. The helper +# /opt/airplanes/current/lib/runtime-self-update.sh. The helper # is the canonical entry point and owns the upgrade flock at # /run/airplanes/runtime-update.lock for the entire upgrade protocol # (state read/write, backups, installer, restart, health gates, diff --git a/stage-airplanes/00-prep/06-run.sh b/stage-airplanes/00-prep/06-run.sh index ca6318d943..f7e2908a21 100755 --- a/stage-airplanes/00-prep/06-run.sh +++ b/stage-airplanes/00-prep/06-run.sh @@ -5,5 +5,5 @@ # this key before extracting any release artifact. Build-mode invocations # from stage 02-install-runtime-overlay point at the same file inside the # repo on the host so the verification path is identical in both modes. -install -D -m 0644 files/usr/share/airplanes/runtime-release.pub \ - "${ROOTFS_DIR}/usr/share/airplanes/runtime-release.pub" +install -D -m 0644 files/opt/airplanes/libexec/runtime-release.pub \ + "${ROOTFS_DIR}/opt/airplanes/libexec/runtime-release.pub" diff --git a/stage-airplanes/00-prep/files/usr/share/airplanes/runtime-release.pub b/stage-airplanes/00-prep/files/opt/airplanes/libexec/runtime-release.pub similarity index 100% rename from stage-airplanes/00-prep/files/usr/share/airplanes/runtime-release.pub rename to stage-airplanes/00-prep/files/opt/airplanes/libexec/runtime-release.pub diff --git a/stage-airplanes/01-install-feed/01-run-chroot.sh b/stage-airplanes/01-install-feed/01-run-chroot.sh index 7df2129a53..15bea6b658 100755 --- a/stage-airplanes/01-install-feed/01-run-chroot.sh +++ b/stage-airplanes/01-install-feed/01-run-chroot.sh @@ -29,9 +29,9 @@ if ! getent group airplanes-feed >/dev/null 2>&1; then fi if ! id -u airplanes-feed >/dev/null 2>&1; then adduser --system --ingroup airplanes-feed \ - --home /usr/local/share/airplanes --no-create-home --quiet airplanes-feed 2>/dev/null \ + --home /opt/airplanes/current/share/airplanes --no-create-home --quiet airplanes-feed 2>/dev/null \ || useradd --system --gid airplanes-feed \ - --home-dir /usr/local/share/airplanes --no-create-home airplanes-feed 2>/dev/null \ + --home-dir /opt/airplanes/current/share/airplanes --no-create-home airplanes-feed 2>/dev/null \ || { echo "ERROR: failed to create airplanes-feed user" >&2; exit 1; } fi diff --git a/stage-airplanes/02-install-runtime-overlay/00-run.sh b/stage-airplanes/02-install-runtime-overlay/00-run.sh index eabf1f9fb3..14a1bc43cb 100755 --- a/stage-airplanes/02-install-runtime-overlay/00-run.sh +++ b/stage-airplanes/02-install-runtime-overlay/00-run.sh @@ -9,10 +9,10 @@ set -e # the signed runtime assets produced earlier in CI # (AIRPLANES_RUNTIME_RELEASE_ASSET_DIR) or the current channel's published # product release. install.sh verifies (sha256 + minisign), extracts the tarball under -# ${ROOTFS_DIR}/opt/airplanes-runtime/releases/v/, flips the +# ${ROOTFS_DIR}/opt/airplanes/releases/v/, flips the # `current` symlink, relinks the /usr/bin/{readsb,airplanes-978,dump978-fa} # decoder binaries, and applies every managed_paths entry from the release -# manifest (the FHS stable-path symlink set into /opt/airplanes-runtime/current/). +# manifest (the FHS stable-path symlink set into /opt/airplanes/current/). # # The companion chroot stage handles the readsb service account, /var/globe_history, # systemctl enable, and the conf-enabled lighttpd hop — everything that has to @@ -27,10 +27,10 @@ set -e # the image (NOT through the overlay's managed_paths). Copy them in before # the overlay install so the unit's ConditionPathExists target exists. STAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -install -d -m 755 "${ROOTFS_DIR}/usr/local/lib/airplanes-runtime" +install -d -m 755 "${ROOTFS_DIR}/opt/airplanes/libexec" install -m 0755 \ - "${STAGE_DIR}/files/usr/local/lib/airplanes-runtime/recover-shim" \ - "${ROOTFS_DIR}/usr/local/lib/airplanes-runtime/recover-shim" + "${STAGE_DIR}/files/opt/airplanes/libexec/recover-shim" \ + "${ROOTFS_DIR}/opt/airplanes/libexec/recover-shim" install -d -m 755 "${ROOTFS_DIR}/etc/systemd/system" install -m 0644 \ "${STAGE_DIR}/files/etc/systemd/system/airplanes-runtime-update-recover.service" \ @@ -40,8 +40,8 @@ install -m 0755 \ "${STAGE_DIR}/files/etc/update-motd.d/09-airplanes-recovery-status" \ "${ROOTFS_DIR}/etc/update-motd.d/09-airplanes-recovery-status" # State + last-good dirs the shim and updater write into. -install -d -m 755 "${ROOTFS_DIR}/var/lib/airplanes-runtime-upgrade" -install -d -m 755 "${ROOTFS_DIR}/var/lib/airplanes-runtime" +install -d -m 755 "${ROOTFS_DIR}/var/lib/airplanes/runtime-upgrade" +install -d -m 755 "${ROOTFS_DIR}/var/lib/airplanes/runtime" # Copy the runtime-overlay source tree into a scratch dir outside the rootfs # so install.sh's $_self_dir resolution does not point at a path inside the @@ -68,7 +68,7 @@ cp -a "${BASE_DIR}/runtime-overlay" "$RUNTIME_OVERLAY_SRC" rm -rf -- "$RUNTIME_OVERLAY_SRC/.git" # Pubkey path. install-common.sh defaults to -# /usr/share/airplanes/runtime-release.pub which lives in the *target rootfs*, +# /opt/airplanes/libexec/runtime-release.pub which lives in the *target rootfs*, # not on the host where install.sh runs in build mode. Point it at the # in-repo copy that stage 00-prep installs into the rootfs from the same # file (the two paths are byte-identical — same source file on disk). @@ -76,7 +76,7 @@ rm -rf -- "$RUNTIME_OVERLAY_SRC/.git" # Test fixtures sign releases with a throwaway key and need an escape hatch. # Gate that escape hatch behind an explicit opt-in env var so a stray env # variable in CI cannot silently downgrade verification to a test key. -PUBKEY_HOST_PATH="${BASE_DIR}/stage-airplanes/00-prep/files/usr/share/airplanes/runtime-release.pub" +PUBKEY_HOST_PATH="${BASE_DIR}/stage-airplanes/00-prep/files/opt/airplanes/libexec/runtime-release.pub" if [[ "${AIRPLANES_RUNTIME_BUILD_TEST_PUBKEY:-0}" == "1" \ && -n "${AIRPLANES_RUNTIME_MINISIGN_PUBKEY:-}" ]]; then PUBKEY_HOST_PATH="$AIRPLANES_RUNTIME_MINISIGN_PUBKEY" diff --git a/stage-airplanes/02-install-runtime-overlay/01-run-chroot.sh b/stage-airplanes/02-install-runtime-overlay/01-run-chroot.sh index d8ff8d5dc8..75e6236b11 100755 --- a/stage-airplanes/02-install-runtime-overlay/01-run-chroot.sh +++ b/stage-airplanes/02-install-runtime-overlay/01-run-chroot.sh @@ -41,18 +41,18 @@ install -d -m 0755 -o readsb -g readsb /var/globe_history # packaged defaults across. if [[ ! -e /etc/default/tar1090 ]]; then install -d -m 0755 /etc/default - install -m 0644 /opt/airplanes-runtime/current/share/tar1090/example_config_dont_edit \ + install -m 0644 /opt/airplanes/current/share/tar1090/example_config_dont_edit \ /etc/default/tar1090 fi if [[ ! -e /etc/collectd/collectd.conf ]]; then install -d -m 0755 /etc/collectd - install -m 0644 /opt/airplanes-runtime/current/etc/collectd/collectd.conf \ + install -m 0644 /opt/airplanes/current/etc/collectd/collectd.conf \ /etc/collectd/collectd.conf fi if [[ ! -e /etc/cron.d/collectd_to_disk \ - && -e /opt/airplanes-runtime/current/etc/cron.d/collectd_to_disk ]]; then + && -e /opt/airplanes/current/etc/cron.d/collectd_to_disk ]]; then install -d -m 0755 /etc/cron.d - install -m 0644 /opt/airplanes-runtime/current/etc/cron.d/collectd_to_disk \ + install -m 0644 /opt/airplanes/current/etc/cron.d/collectd_to_disk \ /etc/cron.d/collectd_to_disk fi @@ -64,9 +64,9 @@ fi # the operator configures anything. airplanes-first-run later merges boot-config # FEED_HOST → MLATSERVER/TARGET into it. Never overwrite an existing feed.env. if [[ ! -e /etc/airplanes/feed.env \ - && -e /opt/airplanes-runtime/current/share/airplanes/feed.env.default ]]; then + && -e /opt/airplanes/current/share/airplanes/feed.env.default ]]; then install -d -m 0755 /etc/airplanes - install -m 0644 /opt/airplanes-runtime/current/share/airplanes/feed.env.default \ + install -m 0644 /opt/airplanes/current/share/airplanes/feed.env.default \ /etc/airplanes/feed.env fi @@ -75,11 +75,11 @@ fi # manifest is the single source of truth so a future release that adds a unit # does not require an image rebuild. collectd.service is apt-managed and the # unit ships with collectd-core; the others are overlay-owned via the -# /etc/systemd/system/ → /opt/airplanes-runtime/current/ symlinks the +# /etc/systemd/system/ → /opt/airplanes/current/ symlinks the # host-side stage laid down. UAT services self-disable cleanly when UAT_INPUT # is empty in /etc/airplanes/feed.env (wrappers publish a disabled decision # file and sleep so the unit stays active). -RUNTIME_MANIFEST=/opt/airplanes-runtime/current/manifest.json +RUNTIME_MANIFEST=/opt/airplanes/current/manifest.json if [[ ! -f "$RUNTIME_MANIFEST" ]]; then echo "ERROR: runtime overlay manifest not found at $RUNTIME_MANIFEST" >&2 exit 1 @@ -98,9 +98,9 @@ systemctl enable airplanes-runtime-update-recover.service # lighttpd conf-enabled stays image-owned; conf-available is overlay-owned # via the managed_paths symlinks. The two-hop chain (conf-enabled → image # absolute path → overlay current) is asserted in extra-probe.sh. -ln -sfn /opt/airplanes-runtime/current/etc/lighttpd/conf-available/89-airplanes-978.conf \ +ln -sfn /opt/airplanes/current/etc/lighttpd/conf-available/89-airplanes-978.conf \ /etc/lighttpd/conf-enabled/89-airplanes-978.conf -ln -sfn /opt/airplanes-runtime/current/etc/lighttpd/conf-available/88-tar1090.conf \ +ln -sfn /opt/airplanes/current/etc/lighttpd/conf-available/88-tar1090.conf \ /etc/lighttpd/conf-enabled/88-tar1090.conf -ln -sfn /opt/airplanes-runtime/current/etc/lighttpd/conf-available/88-graphs1090.conf \ +ln -sfn /opt/airplanes/current/etc/lighttpd/conf-available/88-graphs1090.conf \ /etc/lighttpd/conf-enabled/88-graphs1090.conf diff --git a/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service b/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service index ebb389336d..a538c469a4 100644 --- a/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service +++ b/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service @@ -13,13 +13,13 @@ Before=airplanes-tar1090-uat-sync.service airplanes-tar1090-uat-sync.path Before=tar1090.service graphs1090.service Before=collectd.service lighttpd.service Before=airplanes-webconfig.service airplanes-feed.service airplanes-mlat.service -RequiresMountsFor=/opt /var/lib/airplanes-runtime-upgrade -ConditionPathExists=/usr/local/lib/airplanes-runtime/recover-shim +RequiresMountsFor=/opt /var/lib/airplanes/runtime-upgrade +ConditionPathExists=/opt/airplanes/libexec/recover-shim [Service] Type=oneshot RemainAfterExit=yes -ExecStart=/usr/local/lib/airplanes-runtime/recover-shim +ExecStart=/opt/airplanes/libexec/recover-shim [Install] WantedBy=multi-user.target diff --git a/stage-airplanes/02-install-runtime-overlay/files/etc/update-motd.d/09-airplanes-recovery-status b/stage-airplanes/02-install-runtime-overlay/files/etc/update-motd.d/09-airplanes-recovery-status index 7a81a35940..616ff5b265 100755 --- a/stage-airplanes/02-install-runtime-overlay/files/etc/update-motd.d/09-airplanes-recovery-status +++ b/stage-airplanes/02-install-runtime-overlay/files/etc/update-motd.d/09-airplanes-recovery-status @@ -7,7 +7,7 @@ # POSIX sh, base-OS tools only — this must work even when the overlay is # broken (the whole point of the recovery shim). -STATUS_FILE="/var/lib/airplanes-runtime-upgrade/recovery-status" +STATUS_FILE="/var/lib/airplanes/runtime-upgrade/recovery-status" [ -r "$STATUS_FILE" ] || exit 0 diff --git a/stage-airplanes/02-install-runtime-overlay/files/usr/local/lib/airplanes-runtime/recover-shim b/stage-airplanes/02-install-runtime-overlay/files/opt/airplanes/libexec/recover-shim similarity index 97% rename from stage-airplanes/02-install-runtime-overlay/files/usr/local/lib/airplanes-runtime/recover-shim rename to stage-airplanes/02-install-runtime-overlay/files/opt/airplanes/libexec/recover-shim index 3bf26a2981..f394ef7d9e 100755 --- a/stage-airplanes/02-install-runtime-overlay/files/usr/local/lib/airplanes-runtime/recover-shim +++ b/stage-airplanes/02-install-runtime-overlay/files/opt/airplanes/libexec/recover-shim @@ -27,11 +27,11 @@ set -u -STATE_DIR="/var/lib/airplanes-runtime-upgrade" +STATE_DIR="/var/lib/airplanes/runtime-upgrade" STATE_FILE="${STATE_DIR}/upgrade-state" -RELEASES_DIR="/opt/airplanes-runtime/releases" -CURRENT_LINK="/opt/airplanes-runtime/current" -LAST_GOOD_FILE="/var/lib/airplanes-runtime/last-good-release" +RELEASES_DIR="/opt/airplanes/releases" +CURRENT_LINK="/opt/airplanes/current" +LAST_GOOD_FILE="/var/lib/airplanes/runtime/last-good-release" ATTEMPTS_FILE="${STATE_DIR}/recovery-attempts" STATUS_FILE="${STATE_DIR}/recovery-status" @@ -98,16 +98,16 @@ state_get() { } # valid_release: the candidate must be an absolute path under -# /opt/airplanes-runtime/releases/v*, must exist as a directory, and must +# /opt/airplanes/releases/v*, must exist as a directory, and must # contain a manifest.json (never boot a half-extracted tree). Rejects # path traversal by requiring the canonical releases-dir prefix and refusing # any `..` segment. valid_release() { _cand="$1" [ -n "$_cand" ] || return 1 - # Must be absolute (device-canonical path begins with /opt/airplanes-runtime). + # Must be absolute (device-canonical path begins with /opt/airplanes). case "$_cand" in - /opt/airplanes-runtime/releases/v*) ;; + /opt/airplanes/releases/v*) ;; *) return 1 ;; esac # Reject traversal. diff --git a/stage-airplanes/05-install-webconfig/01-run-chroot.sh b/stage-airplanes/05-install-webconfig/01-run-chroot.sh index f23d7cee3a..83e197a9bf 100755 --- a/stage-airplanes/05-install-webconfig/01-run-chroot.sh +++ b/stage-airplanes/05-install-webconfig/01-run-chroot.sh @@ -14,20 +14,34 @@ adduser --system --no-create-home --group airplanes-webconfig # overlay-shipped airplanes-aggregator@.service template (enabled # per-instance by apl-aggregator when a user opts in) declares # User=airplanes-aggregator and runs vendor feeder code (e.g. fr24feed) under -# this unprivileged account, never root. Its StateDirectory= provisions -# /var/lib/airplanes-aggregators at start, so only the account is created here. +# this unprivileged account, never root. Its StateDirectory= provisions the +# account-owned /var/lib/airplanes/aggregators/state at start; the root-owned +# install root and shared dirs are created below. # Mirrored on the overlay self-update path by runtime-overlay migration 0001. adduser --system --no-create-home --group airplanes-aggregator -# Per-user state dirs. The overlay rootfs ships only the files webconfig owns -# at install time, not these state directories, so create them here at mode -# 0700 with the right owner. -install -d -m 0700 -o airplanes-webconfig -g airplanes-webconfig /var/lib/airplanes-webconfig +# Shared first-party state root. Created root:root 0755 here so every +# per-service subdir below — and the runtime/aggregator dirs other stages and +# the overlay create — nests under a parent we own, never an account-owned one. +install -d -m 0755 /var/lib/airplanes + +# webconfig daemon state (account-owned, 0700). The overlay rootfs ships only +# the files webconfig owns at install time, not these state directories. +install -d -m 0700 -o airplanes-webconfig -g airplanes-webconfig /var/lib/airplanes/webconfig install -d -m 0700 -o airplanes-webconfig -g airplanes-webconfig /etc/airplanes/webconfig -# Upgrade-state marker dir written by the runtime-overlay update path; the -# overlay rootfs ships an empty placeholder dir but the on-image state dir -# must exist with the right owner before the service runs. -install -d -m 0700 -o airplanes-webconfig -g airplanes-webconfig /var/lib/airplanes-webconfig-upgrade +# Upgrade-state marker dir written by the runtime-overlay update path. Root-owned +# (0755) and deliberately a sibling of webconfig/, not a child: the unprivileged +# daemon reads the marker but must not be able to forge it, and it has to survive +# independently of the account-owned state dir. +install -d -m 0755 /var/lib/airplanes/webconfig-upgrade + +# Aggregator install root for the on-demand vendor binaries (e.g. fr24feed). +# Root-owned (0755) and OUTSIDE the airplanes-aggregator account's +# StateDirectory (/var/lib/airplanes/aggregators/state, provisioned per-instance +# by airplanes-aggregator@.service) so the unprivileged account execs binaries +# it cannot replace. +install -d -m 0755 /var/lib/airplanes/aggregators +install -d -m 0755 /var/lib/airplanes/aggregators/bin # /api/log/{unit} streams journalctl as the webconfig user. Adding it to # systemd-journal grants read access to the system journal without sudo. diff --git a/stage-airplanes/06-firstboot/files/usr/local/sbin/airplanes-first-run b/stage-airplanes/06-firstboot/files/usr/local/sbin/airplanes-first-run index 1f01f984a6..22291cd5d5 100755 --- a/stage-airplanes/06-firstboot/files/usr/local/sbin/airplanes-first-run +++ b/stage-airplanes/06-firstboot/files/usr/local/sbin/airplanes-first-run @@ -39,9 +39,10 @@ SSH_MIN_PASSWORD_LEN="${SSH_MIN_PASSWORD_LEN:-12}" # Shared Wi-Fi predicate + keyfile-writer libraries, also consumed by # /usr/local/bin/apl-wifi (webconfig flow). The libs ship from # airplanes-live/image-webconfig via the runtime overlay; production install -# location is /usr/local/lib/airplanes. Bats tests point APL_WIFI_LIB_DIR at the -# image-webconfig source checkout (AIRPLANES_IMAGE_WEBCONFIG_ROOT in CI). -APL_WIFI_LIB_DIR="${APL_WIFI_LIB_DIR:-/usr/local/lib/airplanes}" +# location is /opt/airplanes/current/lib/airplanes. Bats tests point +# APL_WIFI_LIB_DIR at the image-webconfig source checkout +# (AIRPLANES_IMAGE_WEBCONFIG_ROOT in CI). +APL_WIFI_LIB_DIR="${APL_WIFI_LIB_DIR:-/opt/airplanes/current/lib/airplanes}" # shellcheck source=/dev/null . "$APL_WIFI_LIB_DIR/wifi-validators.sh" # shellcheck source=/dev/null diff --git a/stage-airplanes/06a-run-tmpfs/00-run.sh b/stage-airplanes/06a-run-tmpfs/00-run.sh index 64727364a3..19d8cef145 100755 --- a/stage-airplanes/06a-run-tmpfs/00-run.sh +++ b/stage-airplanes/06a-run-tmpfs/00-run.sh @@ -3,9 +3,9 @@ # Lay down the resize script + its systemd unit + the /run/collectd # mount unit. Both units are enabled via the chroot step (next file) # so systemctl-stub captures the operation. -install -d -m 755 "${ROOTFS_DIR}/usr/local/lib/airplanes" -install -m 755 files/usr/local/lib/airplanes/run-resize.sh \ - "${ROOTFS_DIR}/usr/local/lib/airplanes/run-resize.sh" +install -d -m 755 "${ROOTFS_DIR}/opt/airplanes/libexec" +install -m 755 files/opt/airplanes/libexec/run-resize.sh \ + "${ROOTFS_DIR}/opt/airplanes/libexec/run-resize.sh" install -d -m 755 "${ROOTFS_DIR}/etc/systemd/system" install -m 644 files/etc/systemd/system/airplanes-run-resize.service \ diff --git a/stage-airplanes/06a-run-tmpfs/files/etc/systemd/system/airplanes-run-resize.service b/stage-airplanes/06a-run-tmpfs/files/etc/systemd/system/airplanes-run-resize.service index 580ba32803..0e16591faf 100644 --- a/stage-airplanes/06a-run-tmpfs/files/etc/systemd/system/airplanes-run-resize.service +++ b/stage-airplanes/06a-run-tmpfs/files/etc/systemd/system/airplanes-run-resize.service @@ -15,7 +15,7 @@ Conflicts=shutdown.target [Service] Type=oneshot RemainAfterExit=yes -ExecStart=/usr/local/lib/airplanes/run-resize.sh +ExecStart=/opt/airplanes/libexec/run-resize.sh StandardOutput=journal+console StandardError=journal+console diff --git a/stage-airplanes/06a-run-tmpfs/files/usr/local/lib/airplanes/run-resize.sh b/stage-airplanes/06a-run-tmpfs/files/opt/airplanes/libexec/run-resize.sh similarity index 100% rename from stage-airplanes/06a-run-tmpfs/files/usr/local/lib/airplanes/run-resize.sh rename to stage-airplanes/06a-run-tmpfs/files/opt/airplanes/libexec/run-resize.sh diff --git a/stage-airplanes/06b-console-dashboard/00-run.sh b/stage-airplanes/06b-console-dashboard/00-run.sh index 496897f100..6ce3b7238a 100755 --- a/stage-airplanes/06b-console-dashboard/00-run.sh +++ b/stage-airplanes/06b-console-dashboard/00-run.sh @@ -1,37 +1,11 @@ #!/bin/bash -e -# Install render-status + ASCII assets + motd hook image-owned UNLESS the -# runtime-overlay path has already placed them. Presence-of-symlink-target -# is the discriminator rather than an env flag, so any stage enumeration -# that runs 06b without first running 02-install-runtime-overlay (e.g. -# feed-overlay-smoke's docker container, which iterates stages without -# honouring SKIP files) still gets a working dashboard. Under the new -# path, 02-install-runtime-overlay already laid a symlink under -# current/lib/airplanes/render-status before 06b runs, so this block is a -# no-op. Dashboard service unit and getty override below stay unconditional -# — they're version-stable infrastructure that doesn't need runtime updates. -# -L checks for a symlink regardless of target validity (the overlay symlink -# uses an absolute /opt/airplanes-runtime/... target that only resolves in -# the live rootfs, not in chroot-time tests). -e covers a regular file -# placed by some other stage. Either present → 06b is a no-op. -if [[ ! -L "${ROOTFS_DIR}/usr/local/lib/airplanes/render-status" \ - && ! -e "${ROOTFS_DIR}/usr/local/lib/airplanes/render-status" ]]; then - install -D -m 755 "${BASE_DIR:-.}/runtime-overlay/src/lib/airplanes/render-status" \ - "${ROOTFS_DIR}/usr/local/lib/airplanes/render-status" - - install -D -m 644 "${BASE_DIR:-.}/runtime-overlay/src/share/airplanes/logo.txt" \ - "${ROOTFS_DIR}/usr/local/share/airplanes/logo.txt" - install -D -m 644 "${BASE_DIR:-.}/runtime-overlay/src/share/airplanes/banner.txt" \ - "${ROOTFS_DIR}/usr/local/share/airplanes/banner.txt" - install -D -m 644 "${BASE_DIR:-.}/runtime-overlay/src/share/airplanes/banner-narrow.txt" \ - "${ROOTFS_DIR}/usr/local/share/airplanes/banner-narrow.txt" - install -D -m 644 "${BASE_DIR:-.}/runtime-overlay/src/share/airplanes/icon.txt" \ - "${ROOTFS_DIR}/usr/local/share/airplanes/icon.txt" - - install -D -m 755 "${BASE_DIR:-.}/runtime-overlay/src/etc/update-motd.d/10-airplanes-status" \ - "${ROOTFS_DIR}/etc/update-motd.d/10-airplanes-status" -fi - +# render-status, the ASCII assets, and the motd hook are delivered by the +# runtime overlay (current/lib/airplanes/render-status + current/share/airplanes/*, +# laid by 02-install-runtime-overlay), so 06b no longer ships image-owned +# copies under /usr/local — that tree is no longer ours to write. 06b installs +# only the version-stable /etc infrastructure: the dashboard service unit and +# the getty override that hands /dev/tty1 to it. install -d -m 755 "${ROOTFS_DIR}/etc/systemd/system" install -m 644 files/etc/systemd/system/airplanes-dashboard.service \ "${ROOTFS_DIR}/etc/systemd/system/airplanes-dashboard.service" diff --git a/stage-airplanes/06b-console-dashboard/README.md b/stage-airplanes/06b-console-dashboard/README.md index 608fd207bc..23a5438e14 100644 --- a/stage-airplanes/06b-console-dashboard/README.md +++ b/stage-airplanes/06b-console-dashboard/README.md @@ -35,22 +35,26 @@ else (besides sshd's own `Last login:` line, which is out of scope). ## What lands in the rootfs -- `/usr/local/lib/airplanes/render-status` — bash renderer with three +The renderer and its artwork ship from the runtime overlay (laid by stage 02 +under `/opt/airplanes/current`); 06b installs only the dashboard service unit +and the getty override. The overlay paths: + +- `/opt/airplanes/current/lib/airplanes/render-status` — bash renderer with three modes: `--snapshot` (one-shot, no clear; used by the MOTD hook), `--live` (loop, double-buffered repaint every 5s; used by the systemd unit), `--once` (one-shot with screen clear). -- `/usr/local/share/airplanes/logo.txt` — 40×22 plane-badge artwork +- `/opt/airplanes/current/share/airplanes/logo.txt` — 40×22 plane-badge artwork used as the last-resort `--live` fallback when even the narrow banner won't fit. -- `/usr/local/share/airplanes/banner.txt` — 135×20 banner artwork +- `/opt/airplanes/current/share/airplanes/banner.txt` — 135×20 banner artwork (badge + "airplanes.live" wordmark) used at the top of the HDMI dashboard on wide displays. -- `/usr/local/share/airplanes/banner-narrow.txt` — 74×11 compact +- `/opt/airplanes/current/share/airplanes/banner-narrow.txt` — 74×11 compact banner (badge + "airplanes.live" wordmark) used at the top of the HDMI dashboard when the framebuffer console is too narrow for the wide banner. Fits any ≥720p HDMI output at the default 8×16 kernel console font. -- `/usr/local/share/airplanes/icon.txt` — 20×11 small ASCII airplane +- `/opt/airplanes/current/share/airplanes/icon.txt` — 20×11 small ASCII airplane badge used by the snapshot (SSH MOTD / `--once`) layout. Renders to the left of a Unicode-box header (title / random tagline / divider / feed version) — together they form the top banner, with the diff --git a/stage-airplanes/06b-console-dashboard/files/etc/systemd/system/airplanes-dashboard.service b/stage-airplanes/06b-console-dashboard/files/etc/systemd/system/airplanes-dashboard.service index 0674ccb909..f6f7746e6b 100644 --- a/stage-airplanes/06b-console-dashboard/files/etc/systemd/system/airplanes-dashboard.service +++ b/stage-airplanes/06b-console-dashboard/files/etc/systemd/system/airplanes-dashboard.service @@ -29,7 +29,7 @@ ExecStartPre=/usr/bin/sleep 6 # so the serial console on serial0,115200 is unaffected). A failure here # does not block the dashboard from starting. ExecStartPre=-/usr/bin/setterm --term linux --clear all --msg off -ExecStart=/usr/local/lib/airplanes/render-status --live +ExecStart=/opt/airplanes/current/lib/airplanes/render-status --live Restart=on-failure RestartSec=10s diff --git a/stage-airplanes/06d-cli-ergonomics/00-run.sh b/stage-airplanes/06d-cli-ergonomics/00-run.sh index 0b5e04d3aa..f359e22b3f 100755 --- a/stage-airplanes/06d-cli-ergonomics/00-run.sh +++ b/stage-airplanes/06d-cli-ergonomics/00-run.sh @@ -4,16 +4,18 @@ # human user on this image. Webconfig and systemd units use the absolute # /usr/local/bin/apl-feed path and are not affected. # -# Also installs /usr/local/lib/airplanes-webconfig/start-orchestrator.sh — -# the stable image-owned launch path that webconfig's sudoers entry pins to. -# The trampoline exec()s the runtime-overlay-shipped orchestrator after -# verifying it exists, or exits 75 (EX_TEMPFAIL) so the capability gate in -# webconfig returns HTTP 503 cleanly when the overlay is not in place. +# Also installs /opt/airplanes/libexec/start-orchestrator.sh — the stable +# image-owned launch path that webconfig's sudoers entry pins to. It lives in +# /opt/airplanes/libexec (image-baked, outside the versioned current/ tree) so +# it stays valid even while an overlay self-update flips current/. The +# trampoline exec()s the runtime-overlay-shipped orchestrator after verifying +# it exists, or exits 75 (EX_TEMPFAIL) so the capability gate in webconfig +# returns HTTP 503 cleanly when the overlay is not in place. install -d -m 755 "${ROOTFS_DIR}/usr/local/sbin" install -m 0755 files/usr/local/sbin/apl-feed \ "${ROOTFS_DIR}/usr/local/sbin/apl-feed" install -D -m 0755 \ - files/usr/local/lib/airplanes-webconfig/start-orchestrator.sh \ - "${ROOTFS_DIR}/usr/local/lib/airplanes-webconfig/start-orchestrator.sh" + files/opt/airplanes/libexec/start-orchestrator.sh \ + "${ROOTFS_DIR}/opt/airplanes/libexec/start-orchestrator.sh" diff --git a/stage-airplanes/06d-cli-ergonomics/files/usr/local/lib/airplanes-webconfig/start-orchestrator.sh b/stage-airplanes/06d-cli-ergonomics/files/opt/airplanes/libexec/start-orchestrator.sh similarity index 87% rename from stage-airplanes/06d-cli-ergonomics/files/usr/local/lib/airplanes-webconfig/start-orchestrator.sh rename to stage-airplanes/06d-cli-ergonomics/files/opt/airplanes/libexec/start-orchestrator.sh index e04f1244b4..4dbb36ab4d 100755 --- a/stage-airplanes/06d-cli-ergonomics/files/usr/local/lib/airplanes-webconfig/start-orchestrator.sh +++ b/stage-airplanes/06d-cli-ergonomics/files/opt/airplanes/libexec/start-orchestrator.sh @@ -2,7 +2,7 @@ # start-orchestrator.sh — stable image-owned launch path for # airplanes-update-orchestrator. Webconfig's sudoers entry pins this path, # so it MUST exist on every image regardless of which runtime-overlay tag -# is flipped in at /opt/airplanes-runtime/current. +# is flipped in at /opt/airplanes/current. # # The orchestrator binary itself lives inside the runtime overlay (which # moves with each runtime release). This trampoline exec()s the binary @@ -13,7 +13,7 @@ set -Eeuo pipefail -target=/opt/airplanes-runtime/current/lib/airplanes-update-orchestrator +target=/opt/airplanes/current/lib/airplanes-update-orchestrator if [[ ! -f "$target" ]]; then echo "start-orchestrator: target missing or not a regular file: $target" >&2 diff --git a/test/boot-smoke/extra-probe.sh b/test/boot-smoke/extra-probe.sh index e9d876e03f..b91da6ac0b 100755 --- a/test/boot-smoke/extra-probe.sh +++ b/test/boot-smoke/extra-probe.sh @@ -29,8 +29,8 @@ # intact via the kernel's mount table even if _orch_restore never runs: # # /usr/bin/apt-get -# /usr/local/share/airplanes/update.sh -# /opt/airplanes-runtime/current/lib/runtime-self-update.sh +# /opt/airplanes/current/share/airplanes/update.sh +# /opt/airplanes/current/lib/runtime-self-update.sh # # Each stub writes a marker file under # /run/airplanes/test-orchestrator-markers/.ok and sleeps briefly @@ -51,14 +51,14 @@ # and always present on both channels — its absence is independently # asserted earlier in this probe at line ~329 — so we only use it as # a defensive belt-and-braces check here. -_orch_trampoline=/usr/local/lib/airplanes-webconfig/start-orchestrator.sh -_orch_binary=/opt/airplanes-runtime/current/lib/airplanes-update-orchestrator +_orch_trampoline=/opt/airplanes/libexec/start-orchestrator.sh +_orch_binary=/opt/airplanes/current/lib/airplanes-update-orchestrator # Absolute paths the orchestrator invokes for each step. Kept in sync with # the script's defaults block. _orch_apt_get=/usr/bin/apt-get # _orch_feed_update removed — the orchestrator no longer has a feed step. -_orch_runtime_update=/opt/airplanes-runtime/current/lib/runtime-self-update.sh +_orch_runtime_update=/opt/airplanes/current/lib/runtime-self-update.sh _orch_state_file=/run/airplanes/orchestrator.state # Per-run tmpfs paths assigned by _orch_run_probe via mktemp -d so a @@ -361,8 +361,8 @@ _orch_dump_diagnostics() { echo "ns/mnt: $(readlink /proc/self/ns/mnt)" echo "id: uid=$(id -u) gid=$(id -g) euid=$EUID" for t in /usr/bin/apt-get \ - /usr/local/share/airplanes/update.sh \ - /opt/airplanes-runtime/current/lib/runtime-self-update.sh; do + /opt/airplanes/current/share/airplanes/update.sh \ + /opt/airplanes/current/lib/runtime-self-update.sh; do if [[ -x "$t" ]]; then xflag=x; else xflag=NOT-EXECUTABLE; fi if [[ -e "$t" ]]; then eflag=exists; else eflag=MISSING; fi echo "$t [$eflag $xflag]" @@ -1055,12 +1055,12 @@ _runtime_drive_update() { AIRPLANES_RUNTIME_OVERLAY_TAG="local-assets" \ AIRPLANES_RUNTIME_MIN_FREE_BYTES=0 \ AIRPLANES_RUNTIME_HEALTH_DEADLINE=90 \ - /opt/airplanes-runtime/current/lib/runtime-self-update.sh + /opt/airplanes/current/lib/runtime-self-update.sh } _runtime_current_version() { local cur - cur="$(readlink -f /opt/airplanes-runtime/current 2>/dev/null || true)" + cur="$(readlink -f /opt/airplanes/current 2>/dev/null || true)" printf '%s' "${cur##*/v}" } @@ -1114,7 +1114,7 @@ _runtime_upgrade_probe() { # --- GOOD vN+1 : expect convergence ------------------------------------- echo "image-probe: driving runtime-self-update to GOOD release" if ! _runtime_drive_update "$asset_base/good"; then - cat /var/lib/airplanes-runtime-upgrade/upgrade-state 2>/dev/null >&2 || true + cat /var/lib/airplanes/runtime-upgrade/upgrade-state 2>/dev/null >&2 || true journalctl -u readsb.service --no-pager -n 50 2>/dev/null >&2 || true fail "runtime-upgrade: GOOD update did not converge (helper exited non-zero)" fi @@ -1123,7 +1123,7 @@ _runtime_upgrade_probe() { || fail "runtime-upgrade: current did not flip after GOOD update (still v$baseline_ver)" local upg_state upg_state="$(awk -F= '/^state=/{sub(/^state=/,"");print;exit}' \ - /var/lib/airplanes-runtime-upgrade/upgrade-state 2>/dev/null || true)" + /var/lib/airplanes/runtime-upgrade/upgrade-state 2>/dev/null || true)" [[ "$upg_state" == "INSTALLED" ]] \ || fail "runtime-upgrade: GOOD update state=$upg_state, expected INSTALLED" # Consumer services restarted on the new release and healthy. @@ -1148,7 +1148,7 @@ _runtime_upgrade_probe() { [[ "$post_broken_ver" == "$pre_broken_ver" ]] \ || fail "runtime-upgrade: after BROKEN update current=v$post_broken_ver, expected rollback to v$pre_broken_ver" upg_state="$(awk -F= '/^state=/{sub(/^state=/,"");print;exit}' \ - /var/lib/airplanes-runtime-upgrade/upgrade-state 2>/dev/null || true)" + /var/lib/airplanes/runtime-upgrade/upgrade-state 2>/dev/null || true)" [[ "$upg_state" == ROLLED_BACK_* ]] \ || fail "runtime-upgrade: BROKEN update state=$upg_state, expected ROLLED_BACK_*" # Prior (good) release's services restored and healthy. @@ -1176,8 +1176,8 @@ _runtime_upgrade_probe() { # the persistence reboot — so the comparison is against the correct baseline. if [[ -f /var/lib/airplanes-boot-smoke/snapshot-mtimes ]]; then stat -c '%Y %n' \ - /usr/local/share/airplanes/feed-airplanes \ - /usr/local/share/airplanes/venv/bin/mlat-client \ + /opt/airplanes/current/bin/feed-airplanes \ + /opt/airplanes/current/share/airplanes/venv/bin/mlat-client \ > /var/lib/airplanes-boot-smoke/snapshot-mtimes echo "image-probe: re-baselined feed idempotency snapshot after rollback" fi @@ -1212,10 +1212,10 @@ echo "image-probe: starting image-side assertions" # Runtime-overlay symlink chain assertions. The decoder units, # render-status, decoder binaries, tar1090/graphs1090 surfaces and # lighttpd conf-available snippets all resolve through -# /opt/airplanes-runtime/current/ → versioned release dir. -if [[ -d /opt/airplanes-runtime ]]; then +# /opt/airplanes/current/ → versioned release dir. +if [[ -d /opt/airplanes ]]; then _runtime_link="$(readlink /etc/systemd/system/readsb.service 2>/dev/null || true)" - [[ "$_runtime_link" == "/opt/airplanes-runtime/current/systemd/readsb.service" ]] \ + [[ "$_runtime_link" == "/opt/airplanes/current/systemd/readsb.service" ]] \ || fail "readsb.service symlink unexpected: $_runtime_link" # Boot recovery is now an IMAGE-OWNED shim (it must survive a fully @@ -1225,7 +1225,7 @@ if [[ -d /opt/airplanes-runtime ]]; then || ! -f /etc/systemd/system/airplanes-runtime-update-recover.service ]]; then fail "airplanes-runtime-update-recover.service should be an image-owned regular file, not an overlay symlink" fi - [[ -x /usr/local/lib/airplanes-runtime/recover-shim ]] \ + [[ -x /opt/airplanes/libexec/recover-shim ]] \ || fail "recover-shim missing or not executable" systemctl is-enabled airplanes-runtime-update-recover.service >/dev/null \ || fail "airplanes-runtime-update-recover.service not enabled" @@ -1240,20 +1240,23 @@ if [[ -d /opt/airplanes-runtime ]]; then [[ -e /etc/lighttpd/conf-enabled/88-graphs1090.conf ]] \ || fail "lighttpd conf-enabled 88-graphs1090.conf does not resolve through overlay" - # decoder binary symlinks (both → readsb; airplanes-978 is a symlink, - # not a hardlink, per the v1 layout). - [[ "$(readlink /usr/bin/readsb 2>/dev/null)" == "/opt/airplanes-runtime/current/bin/readsb" ]] \ - || fail "/usr/bin/readsb symlink unexpected" - [[ "$(readlink /usr/bin/airplanes-978 2>/dev/null)" == "/opt/airplanes-runtime/current/bin/readsb" ]] \ - || fail "/usr/bin/airplanes-978 should symlink to current/bin/readsb" - [[ "$(readlink /usr/bin/dump978-fa 2>/dev/null)" == "/opt/airplanes-runtime/current/bin/dump978-fa" ]] \ - || fail "/usr/bin/dump978-fa symlink unexpected" + # decoder operator shims under /usr/local/bin (FHS — /usr/bin is no longer + # squatted). readsb covers both 1090 and 978 consumption; the 978 wrapper + # execs it via exec -a, so there is no separate airplanes-978 PATH alias. + [[ "$(readlink /usr/local/bin/readsb 2>/dev/null)" == "/opt/airplanes/current/bin/readsb" ]] \ + || fail "/usr/local/bin/readsb operator shim unexpected" + [[ "$(readlink /usr/local/bin/dump978-fa 2>/dev/null)" == "/opt/airplanes/current/bin/dump978-fa" ]] \ + || fail "/usr/local/bin/dump978-fa operator shim unexpected" + # The old /usr/bin decoder squats must be gone. + [[ ! -e /usr/bin/readsb ]] || fail "/usr/bin/readsb squat must not exist" + [[ ! -e /usr/bin/airplanes-978 ]] || fail "/usr/bin/airplanes-978 squat must not exist" + [[ ! -e /usr/bin/dump978-fa ]] || fail "/usr/bin/dump978-fa squat must not exist" # Runtime manifest pointer. On a fresh-flashed image (before the first # runtime self-update has fired) this is a regular-file copy of the # baked release's manifest, written by install.sh --build-mode. After # the first on-device runtime self-update it gets replaced (mv -Tf) by - # a symlink to /opt/airplanes-runtime/current/manifest.json so the + # a symlink to /opt/airplanes/current/manifest.json so the # pointer auto-follows current. Boot-smoke runs against a fresh image # so the regular-file case is what we see here; we just need the file # to be present and parseable. @@ -1263,9 +1266,9 @@ if [[ -d /opt/airplanes-runtime ]]; then || fail "/etc/airplanes/runtime-manifest.json is not parseable JSON with .version" # Public key shipped and well-formed. - [[ -r /usr/share/airplanes/runtime-release.pub ]] \ - || fail "/usr/share/airplanes/runtime-release.pub missing" - head -1 /usr/share/airplanes/runtime-release.pub | grep -q "minisign public key" \ + [[ -r /opt/airplanes/libexec/runtime-release.pub ]] \ + || fail "/opt/airplanes/libexec/runtime-release.pub missing" + head -1 /opt/airplanes/libexec/runtime-release.pub | grep -q "minisign public key" \ || fail "runtime-release.pub header malformed" # minisign actually parses the shipped key (no binary surprises). @@ -1279,7 +1282,7 @@ if [[ -d /opt/airplanes-runtime ]]; then # the failing library is visible in the harness log; bare # `ldd | grep` under `set -euo pipefail` can mask the real loader # error. - for _decoder_bin in /usr/bin/readsb /usr/bin/airplanes-978 /usr/bin/dump978-fa; do + for _decoder_bin in /opt/airplanes/current/bin/readsb /opt/airplanes/current/bin/dump978-fa; do [[ -e "$_decoder_bin" ]] || fail "decoder binary missing: $_decoder_bin" _ldd_out="$(ldd "$_decoder_bin" 2>&1)" _ldd_rc=$? @@ -1297,7 +1300,7 @@ if [[ -d /opt/airplanes-runtime ]]; then # ExecStart targets that resolve into the overlay must be # executable on the running system. Stage-airplanes ships these as - # symlinks into /opt/airplanes-runtime/current/, and the runtime + # symlinks into /opt/airplanes/current/, and the runtime # tarball is what owns the mode bits. A `0644` script behind an # ExecStart= line fails the unit at boot with `203/EXEC`, which is # how airplanes-runtime-update-recover.service broke on the first @@ -1306,13 +1309,13 @@ if [[ -d /opt/airplanes-runtime ]]; then # to the recovery + self-update + decoder-wrapper paths boot-smoke # can reach without SDR hardware. for _exec_target in \ - /opt/airplanes-runtime/current/lib/runtime-self-update.sh \ - /opt/airplanes-runtime/current/lib/airplanes-update-orchestrator \ - /opt/airplanes-runtime/current/share/airplanes/readsb.sh \ - /opt/airplanes-runtime/current/share/airplanes/airplanes-978.sh \ - /opt/airplanes-runtime/current/share/airplanes/dump978-fa.sh \ - /opt/airplanes-runtime/current/share/airplanes/tar1090-uat-sync.sh \ - /opt/airplanes-runtime/current/lib/airplanes/render-status \ + /opt/airplanes/current/lib/runtime-self-update.sh \ + /opt/airplanes/current/lib/airplanes-update-orchestrator \ + /opt/airplanes/current/share/airplanes/readsb.sh \ + /opt/airplanes/current/share/airplanes/airplanes-978.sh \ + /opt/airplanes/current/share/airplanes/dump978-fa.sh \ + /opt/airplanes/current/share/airplanes/tar1090-uat-sync.sh \ + /opt/airplanes/current/lib/airplanes/render-status \ ; do [[ -e "$_exec_target" ]] || fail "overlay file missing: $_exec_target" [[ -x "$_exec_target" ]] \ @@ -1357,7 +1360,7 @@ if [[ -d /opt/airplanes-runtime ]]; then # checks above already cover provenance. Skip units systemd doesn't load # (empty FragmentPath) and DynamicUser units (principal synthesised at # runtime). - for _unit_file in /opt/airplanes-runtime/current/systemd/*.service; do + for _unit_file in /opt/airplanes/current/systemd/*.service; do [[ -e "$_unit_file" ]] || continue _unit="$(basename "$_unit_file")" _frag="$(systemctl show "$_unit" --property=FragmentPath --value 2>/dev/null || true)" @@ -1428,9 +1431,9 @@ assert_service_healthy ssh.service # exist on every image regardless of which runtime-overlay tag is baked # in. The trampoline exec()s the overlay-shipped orchestrator binary # after a capability check; webconfig's sudoers entry pins this path. -assert_file /usr/local/lib/airplanes-webconfig/start-orchestrator.sh -[[ -x /usr/local/lib/airplanes-webconfig/start-orchestrator.sh ]] \ - || fail "/usr/local/lib/airplanes-webconfig/start-orchestrator.sh is not executable" +assert_file /opt/airplanes/libexec/start-orchestrator.sh +[[ -x /opt/airplanes/libexec/start-orchestrator.sh ]] \ + || fail "/opt/airplanes/libexec/start-orchestrator.sh is not executable" # RTL-SDR DVB blacklist (stage 00-prep/07-run.sh) ships as a real rootfs file # so the kernel DVB-T drivers stay off the SDR dongles and readsb / dump978-fa diff --git a/test/boot-smoke/lib/runtime-upgrade-helpers.sh b/test/boot-smoke/lib/runtime-upgrade-helpers.sh index ff105622d3..7177382b15 100644 --- a/test/boot-smoke/lib/runtime-upgrade-helpers.sh +++ b/test/boot-smoke/lib/runtime-upgrade-helpers.sh @@ -205,7 +205,7 @@ install_synthetic_runtime_releases() { [[ -f "$prebuilt/test.pub" ]] || { echo "ERROR: prebuilt dir $prebuilt missing test.pub" >&2; return 1; } - local staged_in_image=/opt/airplanes-runtime-test-releases + local staged_in_image=/opt/airplanes-test-releases local staged_host="$root$staged_in_image" rm -rf "$staged_host" install -d -m 0755 "$staged_host" @@ -216,7 +216,7 @@ install_synthetic_runtime_releases() { # by the throwaway key) verify. The production / PR-test pubkey is replaced # entirely; this is a test-only image. install -d -m 0755 "$root/usr/share/airplanes" - install -m 0644 "$prebuilt/test.pub" "$root/usr/share/airplanes/runtime-release.pub" + install -m 0644 "$prebuilt/test.pub" "$root/opt/airplanes/libexec/runtime-release.pub" # Run readsb net-only on this test image. The runtime-self-update health # gate requires readsb.service to reach AND hold active (NRestarts diff --git a/test/lib/webconfig-fingerprint.sh b/test/lib/webconfig-fingerprint.sh index 308fea075c..3c13d7e56b 100755 --- a/test/lib/webconfig-fingerprint.sh +++ b/test/lib/webconfig-fingerprint.sh @@ -28,10 +28,10 @@ WEBCONFIG_REGULAR_FILES=( /etc/lighttpd/conf-available/40-airplanes-webconfig.conf /usr/lib/tmpfiles.d/airplanes-webconfig.conf /usr/local/bin/airplanes-webconfig - /usr/local/lib/airplanes-webconfig/reset - /usr/local/share/airplanes/update.sh + /opt/airplanes/current/lib/airplanes-webconfig/reset + /opt/airplanes/current/share/airplanes/update.sh /etc/airplanes/feed.env - /var/lib/airplanes-webconfig/.update-regression-sentinel + /var/lib/airplanes/webconfig/.update-regression-sentinel /etc/airplanes/webconfig/.update-regression-sentinel ) @@ -46,17 +46,17 @@ WEBCONFIG_DIRS=( /etc/airplanes /etc/airplanes/webconfig /etc/sudoers.d - /var/lib/airplanes-webconfig - /usr/local/lib/airplanes-webconfig + /var/lib/airplanes/webconfig + /opt/airplanes/current/lib/airplanes-webconfig ) # Dirs whose contents are recursively fingerprinted to catch unexpected # files added by update.sh beyond the explicit lists above. WEBCONFIG_RECURSIVE_DIRS=( - /var/lib/airplanes-webconfig + /var/lib/airplanes/webconfig /etc/airplanes/webconfig /etc/sudoers.d - /usr/local/lib/airplanes-webconfig + /opt/airplanes/current/lib/airplanes-webconfig ) webconfig_fingerprint() { diff --git a/test/overlay-smoke-inner.sh b/test/overlay-smoke-inner.sh index ebe078339d..7245143084 100755 --- a/test/overlay-smoke-inner.sh +++ b/test/overlay-smoke-inner.sh @@ -68,7 +68,7 @@ id -u airplanes-feed >/dev/null 2>&1 || fail "airplanes-feed user missing (stage getent group airplanes-feed >/dev/null 2>&1 || fail "airplanes-feed group missing (stage 01 chroot)" [[ ! -f /etc/airplanes/feeder-id ]] || fail "feeder-id should NOT exist in build mode" [[ ! -f /etc/airplanes/feeder-claim-secret ]] || fail "feeder-claim-secret should NOT exist in build mode" -[[ ! -e /usr/local/share/airplanes/airplanes-uuid ]] || fail "airplanes-uuid symlink should NOT exist (new contract)" +[[ ! -e /opt/airplanes/current/share/airplanes/airplanes-uuid ]] || fail "airplanes-uuid symlink should NOT exist (new contract)" # enable links helper (reused by stage 06 assertions below). have_enable_link() { @@ -254,9 +254,9 @@ echo "==> stage-airplanes/06a-run-tmpfs/01-run-chroot.sh" ( cd /image/stage-airplanes/06a-run-tmpfs && bash 01-run-chroot.sh ) echo "==> 06a post-install assertions" -[[ -x /usr/local/lib/airplanes/run-resize.sh ]] \ +[[ -x /opt/airplanes/libexec/run-resize.sh ]] \ || fail "run-resize.sh missing or not executable" -[[ "$(stat -c %a /usr/local/lib/airplanes/run-resize.sh)" == "755" ]] \ +[[ "$(stat -c %a /opt/airplanes/libexec/run-resize.sh)" == "755" ]] \ || fail "run-resize.sh mode != 0755" [[ -f /etc/systemd/system/airplanes-run-resize.service ]] \ || fail "airplanes-run-resize.service missing" @@ -315,17 +315,17 @@ echo "==> stage-airplanes/06b-console-dashboard/01-run-chroot.sh" ( cd /image/stage-airplanes/06b-console-dashboard && bash 01-run-chroot.sh ) echo "==> 06b post-install assertions" -[[ -x /usr/local/lib/airplanes/render-status ]] || fail "render-status missing or not executable" -[[ "$(stat -c %a /usr/local/lib/airplanes/render-status)" == "755" ]] \ +[[ -x /opt/airplanes/current/lib/airplanes/render-status ]] || fail "render-status missing or not executable" +[[ "$(stat -c %a /opt/airplanes/current/lib/airplanes/render-status)" == "755" ]] \ || fail "render-status mode != 0755" -[[ -s /usr/local/share/airplanes/logo.txt ]] || fail "logo.txt missing or empty" -[[ "$(stat -c %a /usr/local/share/airplanes/logo.txt)" == "644" ]] \ +[[ -s /opt/airplanes/current/share/airplanes/logo.txt ]] || fail "logo.txt missing or empty" +[[ "$(stat -c %a /opt/airplanes/current/share/airplanes/logo.txt)" == "644" ]] \ || fail "logo.txt mode != 0644" -[[ -s /usr/local/share/airplanes/banner.txt ]] || fail "banner.txt missing or empty" -[[ "$(stat -c %a /usr/local/share/airplanes/banner.txt)" == "644" ]] \ +[[ -s /opt/airplanes/current/share/airplanes/banner.txt ]] || fail "banner.txt missing or empty" +[[ "$(stat -c %a /opt/airplanes/current/share/airplanes/banner.txt)" == "644" ]] \ || fail "banner.txt mode != 0644" -[[ -s /usr/local/share/airplanes/banner-narrow.txt ]] || fail "banner-narrow.txt missing or empty" -[[ "$(stat -c %a /usr/local/share/airplanes/banner-narrow.txt)" == "644" ]] \ +[[ -s /opt/airplanes/current/share/airplanes/banner-narrow.txt ]] || fail "banner-narrow.txt missing or empty" +[[ "$(stat -c %a /opt/airplanes/current/share/airplanes/banner-narrow.txt)" == "644" ]] \ || fail "banner-narrow.txt mode != 0644" [[ -f /etc/systemd/system/airplanes-dashboard.service ]] \ || fail "airplanes-dashboard.service missing" @@ -406,11 +406,11 @@ PATHS_CLAIM_PENDING=/nx \ PATHS_CLAIM_VERSION=/nx \ PATHS_AIRCRAFT_JSON=/nx \ PATHS_THERMAL=/nx \ -PATHS_LOGO=/usr/local/share/airplanes/logo.txt \ -PATHS_ICON=/usr/local/share/airplanes/icon.txt \ +PATHS_LOGO=/opt/airplanes/current/share/airplanes/logo.txt \ +PATHS_ICON=/opt/airplanes/current/share/airplanes/icon.txt \ TERM=dumb \ AIRPLANES_STATUS_TAGLINE_INDEX=0 \ - bash /usr/local/lib/airplanes/render-status --snapshot >"$SNAP_OUT" 2>&1 \ + bash /opt/airplanes/current/lib/airplanes/render-status --snapshot >"$SNAP_OUT" 2>&1 \ || fail "render-status --snapshot exited non-zero with all sources missing" # Banner header (icon + airplanes.live + tagline + version) replaces the # old standalone "Build channel=…" line. Section list mirrors the labels @@ -436,8 +436,8 @@ if grep -q 'dump978-fa' "$SNAP_OUT"; then fail "snapshot renders dump978-fa as a standalone row (should fold into uat978)" fi # Icon artwork must land in the rootfs at the documented path. -[[ -s /usr/local/share/airplanes/icon.txt ]] \ - || fail "/usr/local/share/airplanes/icon.txt missing in installed rootfs" +[[ -s /opt/airplanes/current/share/airplanes/icon.txt ]] \ + || fail "/opt/airplanes/current/share/airplanes/icon.txt missing in installed rootfs" grep -q '(not yet generated)' "$SNAP_OUT" \ || fail "snapshot did not show '(not yet generated)' for missing feeder-id" grep -q 'unclaimed' "$SNAP_OUT" \ diff --git a/test/runtime-overlay/fixtures/build-release-input/managed_paths.json b/test/runtime-overlay/fixtures/build-release-input/managed_paths.json index 4a6fddcb84..90fa07b4e2 100644 --- a/test/runtime-overlay/fixtures/build-release-input/managed_paths.json +++ b/test/runtime-overlay/fixtures/build-release-input/managed_paths.json @@ -1,97 +1,37 @@ [ - { - "mode": "symlink", - "link": "/usr/bin/readsb", - "target": "/opt/airplanes-runtime/current/bin/readsb" - }, - { - "mode": "symlink", - "link": "/usr/bin/airplanes-978", - "target": "/opt/airplanes-runtime/current/bin/readsb" - }, - { - "mode": "symlink", - "link": "/usr/bin/dump978-fa", - "target": "/opt/airplanes-runtime/current/bin/dump978-fa" - }, { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" + "target": "/opt/airplanes/current/systemd/readsb.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-978.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-978.service" + "target": "/opt/airplanes/current/systemd/airplanes-978.service" }, { "mode": "symlink", "link": "/etc/systemd/system/dump978-fa.service", - "target": "/opt/airplanes-runtime/current/systemd/dump978-fa.service" + "target": "/opt/airplanes/current/systemd/dump978-fa.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-tar1090-uat-sync.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-tar1090-uat-sync.service" + "target": "/opt/airplanes/current/systemd/airplanes-tar1090-uat-sync.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-tar1090-uat-sync.path", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-tar1090-uat-sync.path" - }, - { - "mode": "symlink", - "link": "/usr/local/lib/airplanes/render-status", - "target": "/opt/airplanes-runtime/current/lib/airplanes/render-status" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/readsb.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/readsb.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/airplanes-978.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/airplanes-978.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/dump978-fa.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/dump978-fa.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/tar1090-uat-sync.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/tar1090-uat-sync.sh" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/logo.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/logo.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/banner.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/banner.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/banner-narrow.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/banner-narrow.txt" - }, - { - "mode": "symlink", - "link": "/usr/local/share/airplanes/icon.txt", - "target": "/opt/airplanes-runtime/current/share/airplanes/icon.txt" + "target": "/opt/airplanes/current/systemd/airplanes-tar1090-uat-sync.path" }, { "mode": "symlink", "link": "/etc/update-motd.d/10-airplanes-status", - "target": "/opt/airplanes-runtime/current/etc/update-motd.d/10-airplanes-status" + "target": "/opt/airplanes/current/etc/update-motd.d/10-airplanes-status" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/89-airplanes-978.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/89-airplanes-978.conf" + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/89-airplanes-978.conf" } ] diff --git a/test/runtime-overlay/lib/install_test_helpers.bash b/test/runtime-overlay/lib/install_test_helpers.bash index 470ed96195..47fd583f12 100644 --- a/test/runtime-overlay/lib/install_test_helpers.bash +++ b/test/runtime-overlay/lib/install_test_helpers.bash @@ -54,16 +54,16 @@ mk_target_root() { local work="$1" local r="$work/root" install -d -m 755 \ - "$r/opt/airplanes-runtime/releases" \ - "$r/opt/airplanes-runtime" \ + "$r/opt/airplanes/releases" \ + "$r/opt/airplanes" \ "$r/etc/airplanes" \ "$r/etc/systemd/system" \ "$r/usr/bin" \ "$r/run/readsb" \ - "$r/run/airplanes-978" \ - "$r/run/dump978-fa" \ - "$r/var/lib/airplanes-runtime-upgrade" \ - "$r/var/lib/airplanes-runtime" \ + "$r/run/airplanes/978" \ + "$r/run/airplanes/dump978-fa" \ + "$r/var/lib/airplanes/runtime-upgrade" \ + "$r/var/lib/airplanes/runtime" \ "$r/run/airplanes" printf '%s' "$r" } @@ -74,7 +74,7 @@ mk_target_root() { # synthesise each row of the recovery matrix. mk_state_file() { local target_root="$1" state="$2"; shift 2 - local dir="$target_root/var/lib/airplanes-runtime-upgrade" + local dir="$target_root/var/lib/airplanes/runtime-upgrade" install -d -m 755 "$dir" { printf 'state=%s\n' "$state" @@ -89,17 +89,17 @@ mk_state_file() { # Read the `state=` value from 's upgrade-state file. read_state() { local target_root="$1" - local f="$target_root/var/lib/airplanes-runtime-upgrade/upgrade-state" + local f="$target_root/var/lib/airplanes/runtime-upgrade/upgrade-state" [[ -r "$f" ]] || { printf ''; return 0; } awk -F= '/^state=/ { sub(/^state=/, ""); print; exit }' "$f" } # Stage a synthetic release directory tree under at -# /opt/airplanes-runtime/releases/v/. Writes a minimal manifest +# /opt/airplanes/releases/v/. Writes a minimal manifest # the recovery + rollback paths can read. Echoes the absolute release dir. mk_target_release() { local target_root="$1" version="$2" - local d="$target_root/opt/airplanes-runtime/releases/v$version" + local d="$target_root/opt/airplanes/releases/v$version" install -d -m 755 \ "$d/bin" \ "$d/lib" \ diff --git a/test/runtime-overlay/test_06b_render_status_gated.bats b/test/runtime-overlay/test_06b_render_status_gated.bats index d98b6eef66..5aa0dd8e49 100644 --- a/test/runtime-overlay/test_06b_render_status_gated.bats +++ b/test/runtime-overlay/test_06b_render_status_gated.bats @@ -1,10 +1,10 @@ #!/usr/bin/env bats -# Tests for stage-airplanes/06b-console-dashboard/00-run.sh's idempotent -# install of render-status + ASCII assets + motd hook. The block installs -# image-side UNLESS the runtime-overlay path already placed the files (via -# stage-airplanes/02-install-runtime-overlay running ahead of 06b). The -# dashboard service unit and getty@tty1 override stay unconditional. +# Tests for stage-airplanes/06b-console-dashboard/00-run.sh. render-status, the +# ASCII assets, and the motd hook are delivered by the runtime overlay (laid by +# stage-airplanes/02-install-runtime-overlay under /opt/airplanes/current), so +# 06b installs ONLY the image-owned dashboard service unit and the getty@tty1 +# override. It must not write anything under /usr/local or /etc/update-motd.d. bats_require_minimum_version 1.5.0 @@ -24,57 +24,22 @@ run_06b() { bash -c "cd \"$REPO_ROOT/stage-airplanes/06b-console-dashboard\" && ./00-run.sh" } -@test "render-status absent: 06b installs the image-owned fallback" { +@test "06b installs the dashboard service unit and the getty override" { run run_06b [ "$status" -eq 0 ] - [ -x "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" ] - [ -f "$ROOTFS_DIR/usr/local/share/airplanes/logo.txt" ] - [ -f "$ROOTFS_DIR/usr/local/share/airplanes/banner.txt" ] - [ -f "$ROOTFS_DIR/usr/local/share/airplanes/banner-narrow.txt" ] - [ -f "$ROOTFS_DIR/usr/local/share/airplanes/icon.txt" ] - [ -x "$ROOTFS_DIR/etc/update-motd.d/10-airplanes-status" ] - - # Dashboard service + getty override are always image-owned. [ -f "$ROOTFS_DIR/etc/systemd/system/airplanes-dashboard.service" ] [ -f "$ROOTFS_DIR/etc/systemd/system/getty@tty1.service.d/override.conf" ] } -@test "render-status already symlinked into runtime overlay: 06b skips it" { - # Simulate 02-install-runtime-overlay having run ahead of 06b. - install -d -m 755 "$ROOTFS_DIR/usr/local/lib/airplanes" - install -d -m 755 "$ROOTFS_DIR/opt/airplanes-runtime/current/lib/airplanes" - : > "$ROOTFS_DIR/opt/airplanes-runtime/current/lib/airplanes/render-status" - chmod 0755 "$ROOTFS_DIR/opt/airplanes-runtime/current/lib/airplanes/render-status" - ln -sf /opt/airplanes-runtime/current/lib/airplanes/render-status \ - "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" - +@test "06b writes nothing under /usr/local or the motd hook (overlay-delivered)" { run run_06b [ "$status" -eq 0 ] - - # 06b should NOT have replaced the symlink with a regular file. - [ -L "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" ] - [ "$(readlink "$ROOTFS_DIR/usr/local/lib/airplanes/render-status")" \ - = "/opt/airplanes-runtime/current/lib/airplanes/render-status" ] - - # 06b should NOT have installed the image-owned ASCII assets either. + # render-status + artwork are overlay-only now; 06b must not re-create the + # old /usr/local fallback nor the image-owned motd hook. + [ ! -e "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" ] [ ! -e "$ROOTFS_DIR/usr/local/share/airplanes/logo.txt" ] + [ ! -e "$ROOTFS_DIR/usr/local/share/airplanes/banner.txt" ] + [ ! -e "$ROOTFS_DIR/usr/local/share/airplanes/banner-narrow.txt" ] + [ ! -e "$ROOTFS_DIR/usr/local/share/airplanes/icon.txt" ] [ ! -e "$ROOTFS_DIR/etc/update-motd.d/10-airplanes-status" ] - - # Dashboard service + getty override still land unconditionally. - [ -f "$ROOTFS_DIR/etc/systemd/system/airplanes-dashboard.service" ] - [ -f "$ROOTFS_DIR/etc/systemd/system/getty@tty1.service.d/override.conf" ] -} - -@test "render-status as plain file already present: 06b leaves it alone" { - # Defensive case: prior stage (not the overlay) put a real file there. - install -d -m 755 "$ROOTFS_DIR/usr/local/lib/airplanes" - printf '#!/bin/bash\necho prior\n' > "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" - chmod 0755 "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" - prior_sha=$(sha256sum "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" | cut -d' ' -f1) - - run run_06b - [ "$status" -eq 0 ] - - new_sha=$(sha256sum "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" | cut -d' ' -f1) - [ "$prior_sha" = "$new_sha" ] } diff --git a/test/runtime-overlay/test_airplanes_978_wrapper.bats b/test/runtime-overlay/test_airplanes_978_wrapper.bats index 60c22f4762..eda474a7d0 100644 --- a/test/runtime-overlay/test_airplanes_978_wrapper.bats +++ b/test/runtime-overlay/test_airplanes_978_wrapper.bats @@ -2,7 +2,7 @@ # Tests for airplanes-978.sh — the UAT relay wrapper. The wrapper reads # UAT_INPUT from the EnvironmentFile-loaded env, classifies into -# enabled / disabled / misconfigured, writes /run/airplanes-978/state via +# enabled / disabled / misconfigured, writes /run/airplanes/978/state via # state-writer.sh, and either execs the daemon, sleeps (uat_disabled → # exit 0, unit stays active), or exits 64 (misconfigured input). # diff --git a/test/runtime-overlay/test_bridge_release.bats b/test/runtime-overlay/test_bridge_release.bats index 50093fcc40..8ab2e60322 100644 --- a/test/runtime-overlay/test_bridge_release.bats +++ b/test/runtime-overlay/test_bridge_release.bats @@ -53,8 +53,8 @@ setup() { printf 'ok' > "$HTTPD_DOC/graphs1090/index.html" : > "$TARGET_ROOT/run/readsb/aircraft.json" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" } teardown() { @@ -83,7 +83,7 @@ write_manifest() { "managed_paths": [ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" } + "target": "/opt/airplanes/current/systemd/readsb.service" } ], "mutable_paths": [], "systemd": { "enable": ["readsb.service"], "daemon_reload": true }, @@ -142,12 +142,12 @@ run_install() { echo "$output" return 1 fi - [ -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] - [ -L "$TARGET_ROOT/opt/airplanes-runtime/current" ] + [ -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] + [ -L "$TARGET_ROOT/opt/airplanes/current" ] [ -L "$TARGET_ROOT/etc/systemd/system/readsb.service" ] # Component-object pin was accepted (no schema rejection). run jq -e '.components.readsb_wiedehopf.version == "9.9.9"' \ - "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER/manifest.json" + "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER/manifest.json" [ "$status" -eq 0 ] } @@ -157,7 +157,7 @@ run_install() { run run_install [ "$status" -ne 0 ] # No release dir laid down — refused before extraction. - [ ! -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] + [ ! -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] } @test "bridge: release with a newer schema version is refused pre-mutation" { @@ -165,5 +165,5 @@ run_install() { start_httpd run run_install [ "$status" -ne 0 ] - [ ! -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] + [ ! -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] } diff --git a/test/runtime-overlay/test_build_release_layout.bats b/test/runtime-overlay/test_build_release_layout.bats index 4fc52ba8e2..fbbe3f0680 100644 --- a/test/runtime-overlay/test_build_release_layout.bats +++ b/test/runtime-overlay/test_build_release_layout.bats @@ -212,17 +212,17 @@ setup() { # Delete a file the fixture's managed_paths.json references; the # cross-check inside the build must catch this before SHA256SUMS or # publish. - rm "$INPUT_DIR/share/airplanes/readsb.sh" + rm "$INPUT_DIR/systemd/readsb.service" run "$BUILD" "${GOOD_ARGS[@]}" [ "$status" -ne 0 ] [[ "$output" == *"managed_paths"* ]] - [[ "$output" == *"readsb.sh"* ]] + [[ "$output" == *"readsb.service"* ]] } @test "leaves no partial release dir when validation fails" { # Same trigger as above. The build must clean up its staging tree # rather than leave a half-published v1.4.0 under output-dir. - rm "$INPUT_DIR/share/airplanes/readsb.sh" + rm "$INPUT_DIR/systemd/readsb.service" run "$BUILD" "${GOOD_ARGS[@]}" [ "$status" -ne 0 ] [ ! -e "$OUTPUT_DIR/v1.4.0" ] diff --git a/test/runtime-overlay/test_feed_mutability.bats b/test/runtime-overlay/test_feed_mutability.bats index f0f9ad6399..fe9ed825d4 100644 --- a/test/runtime-overlay/test_feed_mutability.bats +++ b/test/runtime-overlay/test_feed_mutability.bats @@ -2,13 +2,13 @@ # Feed mutability audit: asserts the feed daemon wrappers, apl-feed CLI, and # runtime lib helpers never write under the immutable release directory -# (/opt/airplanes-runtime/current or /usr/local/share/airplanes when it is +# (/opt/airplanes/current or /opt/airplanes/current/share/airplanes when it is # a symlink into the release tree). The feed stack writes to: # - /run//state (tmpfs, fine) # - /etc/airplanes/feed.env (mutable config, fine) -# - /var/lib/airplanes-webconfig (webconfig state, fine) +# - /var/lib/airplanes/webconfig (webconfig state, fine) # but must NOT mutate its own install directory (which is the release tree -# once managed_paths-symlinked through /opt/airplanes-runtime/current/). +# once managed_paths-symlinked through /opt/airplanes/current/). # # Static-analysis approach: grep the staged share/airplanes scripts (daemon # wrappers, apl-feed subcommands, runtime libs) for shell redirect operators @@ -33,7 +33,7 @@ setup() { } # Grep all feed runtime scripts for writes targeting the install directory. -# We look for patterns that would write under /usr/local/share/airplanes/ +# We look for patterns that would write under /opt/airplanes/current/share/airplanes/ # (the IPATH on a standard install, symlinked into the overlay release tree) # or under $IPATH itself. The daemon wrappers must only write to /run/ (state # files) and /etc/airplanes/ (feed.env). Anything else is a mutability leak. @@ -69,7 +69,7 @@ _feed_runtime_scripts() { local hits="" while IFS= read -r script; do local matches - matches="$(grep -nE '(>|>>)\s*/usr/local/share/airplanes/' "$script" \ + matches="$(grep -nE '(>|>>)\s*/opt/airplanes/current/share/airplanes/' "$script" \ | grep -v '^\s*#' \ | grep -v 'state-writer' || true)" if [[ -n "$matches" ]]; then @@ -83,7 +83,7 @@ _feed_runtime_scripts() { local hits="" while IFS= read -r script; do local matches - matches="$(grep -nE '(install |cp |mkdir ).*/usr/local/share/airplanes/' "$script" \ + matches="$(grep -nE '(install |cp |mkdir ).*/opt/airplanes/current/share/airplanes/' "$script" \ | grep -v '^\s*#' || true)" if [[ -n "$matches" ]]; then hits+="$script: $matches"$'\n' diff --git a/test/runtime-overlay/test_finalize_reconciles_aggregators.bats b/test/runtime-overlay/test_finalize_reconciles_aggregators.bats index b940976ac8..6a86a7f2df 100644 --- a/test/runtime-overlay/test_finalize_reconciles_aggregators.bats +++ b/test/runtime-overlay/test_finalize_reconciles_aggregators.bats @@ -71,8 +71,8 @@ setup() { target_root="$(mk_target_root "$WORK")" new_dir="$(mk_target_release "$target_root" "0.0.2")" mk_state_file "$target_root" "HEALTH_PASSED" "new_release=$new_dir" - ln -s "/opt/airplanes-runtime/releases/v0.0.2" \ - "$target_root/opt/airplanes-runtime/current" + ln -s "/opt/airplanes/releases/v0.0.2" \ + "$target_root/opt/airplanes/current" AIRPLANES_BUILD_MODE=0 run airplanes_runtime_finalize_after_health_passed "$target_root" [ "$status" -eq 0 ] diff --git a/test/runtime-overlay/test_finalize_starts_activators.bats b/test/runtime-overlay/test_finalize_starts_activators.bats index ffdc925bde..3fd222a029 100644 --- a/test/runtime-overlay/test_finalize_starts_activators.bats +++ b/test/runtime-overlay/test_finalize_starts_activators.bats @@ -210,8 +210,8 @@ JSON # Stage a `current` symlink so the runtime-manifest symlink # record_runtime_manifest writes points at a realistic post-flip release. - ln -s "/opt/airplanes-runtime/releases/v0.0.2" \ - "$target_root/opt/airplanes-runtime/current" + ln -s "/opt/airplanes/releases/v0.0.2" \ + "$target_root/opt/airplanes/current" AIRPLANES_BUILD_MODE=0 run airplanes_runtime_finalize_after_health_passed "$target_root" [ "$status" -eq 0 ] diff --git a/test/runtime-overlay/test_install_account_migration.bats b/test/runtime-overlay/test_install_account_migration.bats index 96943e99cb..f0cac556f3 100644 --- a/test/runtime-overlay/test_install_account_migration.bats +++ b/test/runtime-overlay/test_install_account_migration.bats @@ -83,7 +83,7 @@ JSON [ "$status" -eq 0 ] run grep -E '^--system --group --home /usr/local/share/readsb --no-create-home --quiet readsb$' "$ADDUSER_LOG" [ "$status" -eq 0 ] - run grep -E '^--system --ingroup airplanes-feed --home /usr/local/share/airplanes --no-create-home --quiet airplanes-feed$' "$ADDUSER_LOG" + run grep -E '^--system --ingroup airplanes-feed --home /opt/airplanes/current/share/airplanes --no-create-home --quiet airplanes-feed$' "$ADDUSER_LOG" [ "$status" -eq 0 ] run grep -E '^--system --no-create-home --group airplanes-aggregator$' "$ADDUSER_LOG" [ "$status" -eq 0 ] diff --git a/test/runtime-overlay/test_install_buildmode.bats b/test/runtime-overlay/test_install_buildmode.bats index 7578a19b68..f21f325a6a 100644 --- a/test/runtime-overlay/test_install_buildmode.bats +++ b/test/runtime-overlay/test_install_buildmode.bats @@ -64,10 +64,10 @@ setup() { "managed_paths": [ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" }, + "target": "/opt/airplanes/current/systemd/readsb.service" }, { "mode": "symlink", - "link": "/usr/local/share/airplanes/readsb.sh", - "target": "/opt/airplanes-runtime/current/share/airplanes/readsb.sh" } + "link": "/usr/local/bin/apl-feed", + "target": "/opt/airplanes/current/bin/apl-feed" } ], "mutable_paths": [], "systemd": { "enable": [], "daemon_reload": true }, @@ -138,21 +138,23 @@ teardown() { echo "$output" return 1 fi - [ -d "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER" ] - [ -f "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER/manifest.json" ] - [ -f "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER/bin/readsb" ] - [ -L "$ROOTFS_DIR/opt/airplanes-runtime/current" ] + [ -d "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER" ] + [ -f "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER/manifest.json" ] + [ -f "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER/bin/readsb" ] + [ -L "$ROOTFS_DIR/opt/airplanes/current" ] # The current symlink target string is the on-device-canonical path # (NOT rebased through ROOTFS_DIR), because the link lives in the - # rootfs but resolves on the real device where /opt/airplanes-runtime + # rootfs but resolves on the real device where /opt/airplanes # is the actual on-disk root. - [ "$(readlink "$ROOTFS_DIR/opt/airplanes-runtime/current")" = "/opt/airplanes-runtime/releases/v$REL_VER" ] + [ "$(readlink "$ROOTFS_DIR/opt/airplanes/current")" = "/opt/airplanes/releases/v$REL_VER" ] # managed_paths laid down under ROOTFS_DIR. [ -L "$ROOTFS_DIR/etc/systemd/system/readsb.service" ] - [ "$(readlink "$ROOTFS_DIR/etc/systemd/system/readsb.service")" = "/opt/airplanes-runtime/current/systemd/readsb.service" ] - # Decoder binary symlinks created. - [ -L "$ROOTFS_DIR/usr/bin/readsb" ] - [ -L "$ROOTFS_DIR/usr/bin/airplanes-978" ] + [ "$(readlink "$ROOTFS_DIR/etc/systemd/system/readsb.service")" = "/opt/airplanes/current/systemd/readsb.service" ] + # Decoder operator shims created under /usr/local/bin (FHS; /usr/bin unsquatted). + [ -L "$ROOTFS_DIR/usr/local/bin/readsb" ] + [ -L "$ROOTFS_DIR/usr/local/bin/dump978-fa" ] + [ ! -e "$ROOTFS_DIR/usr/bin/readsb" ] + [ ! -e "$ROOTFS_DIR/usr/bin/airplanes-978" ] } @test "build mode rejects a tampered SHA256SUMS" { diff --git a/test/runtime-overlay/test_install_copy_path_preimage.bats b/test/runtime-overlay/test_install_copy_path_preimage.bats index c7feb92434..013aa3d406 100644 --- a/test/runtime-overlay/test_install_copy_path_preimage.bats +++ b/test/runtime-overlay/test_install_copy_path_preimage.bats @@ -105,7 +105,7 @@ JSON "managed_paths": [ { "mode": "symlink", "link": "/usr/bin/readsb", - "target": "/opt/airplanes-runtime/current/bin/readsb" }, + "target": "/opt/airplanes/current/bin/readsb" }, { "mode": "copy", "path": "/etc/sudoers.d/090_airplanes-runtime", "from": "etc/sudoers.d/090_airplanes-runtime", diff --git a/test/runtime-overlay/test_install_health_gate_feed.bats b/test/runtime-overlay/test_install_health_gate_feed.bats index c80d45412a..e88c40894a 100644 --- a/test/runtime-overlay/test_install_health_gate_feed.bats +++ b/test/runtime-overlay/test_install_health_gate_feed.bats @@ -51,49 +51,46 @@ JSON # --- feed binary-identity gate ---------------------------------------------- @test "feed binary gate: passes when feed-airplanes resolves into current" { - # Build a fake release tree + current symlink with a feed binary, then - # symlink the FHS path through current/. All symlinks use absolute paths + # Build a fake release tree + current symlink with a feed binary. The gate + # checks /opt/airplanes/current/bin/feed-airplanes directly. All paths are # under TARGET_ROOT so readlink -f resolves correctly on the host. - local rel="$TARGET_ROOT/opt/airplanes-runtime/releases/v1.0.0" + local rel="$TARGET_ROOT/opt/airplanes/releases/v1.0.0" install -d -m 755 "$rel/bin" printf '#!/bin/sh\n' > "$rel/bin/feed-airplanes" chmod 0755 "$rel/bin/feed-airplanes" - install -d -m 755 "$TARGET_ROOT/opt/airplanes-runtime" - ln -sfn "$rel" "$TARGET_ROOT/opt/airplanes-runtime/current" - install -d -m 755 "$TARGET_ROOT/usr/local/share/airplanes" - ln -sfn "$TARGET_ROOT/opt/airplanes-runtime/current/bin/feed-airplanes" \ - "$TARGET_ROOT/usr/local/share/airplanes/feed-airplanes" + install -d -m 755 "$TARGET_ROOT/opt/airplanes" + ln -sfn "$rel" "$TARGET_ROOT/opt/airplanes/current" run _airplanes_runtime_probe_feed_binary_current "$TARGET_ROOT" [ "$status" -eq 0 ] } @test "feed binary gate: fails when feed-airplanes resolves outside current" { - # current points at v1.0.0 but the FHS link points at a stale v0.9.0. - local rel_new="$TARGET_ROOT/opt/airplanes-runtime/releases/v1.0.0" - local rel_old="$TARGET_ROOT/opt/airplanes-runtime/releases/v0.9.0" + # current points at v1.0.0 but its feed-airplanes is itself a symlink to a + # stale v0.9.0 binary, so the running binary resolves out of the release. + local rel_new="$TARGET_ROOT/opt/airplanes/releases/v1.0.0" + local rel_old="$TARGET_ROOT/opt/airplanes/releases/v0.9.0" install -d -m 755 "$rel_new/bin" "$rel_old/bin" - printf '#!/bin/sh\n' > "$rel_new/bin/feed-airplanes" - chmod 0755 "$rel_new/bin/feed-airplanes" printf '#!/bin/sh\n' > "$rel_old/bin/feed-airplanes" chmod 0755 "$rel_old/bin/feed-airplanes" - install -d -m 755 "$TARGET_ROOT/opt/airplanes-runtime" - ln -sfn "$rel_new" "$TARGET_ROOT/opt/airplanes-runtime/current" - install -d -m 755 "$TARGET_ROOT/usr/local/share/airplanes" - ln -sfn "$rel_old/bin/feed-airplanes" \ - "$TARGET_ROOT/usr/local/share/airplanes/feed-airplanes" + ln -sfn "$rel_old/bin/feed-airplanes" "$rel_new/bin/feed-airplanes" + install -d -m 755 "$TARGET_ROOT/opt/airplanes" + ln -sfn "$rel_new" "$TARGET_ROOT/opt/airplanes/current" run _airplanes_runtime_probe_feed_binary_current "$TARGET_ROOT" [ "$status" -ne 0 ] [[ "$output" == *"outside active release"* ]] } -@test "feed binary gate: fails when feed-airplanes link is missing" { - install -d -m 755 "$TARGET_ROOT/opt/airplanes-runtime" - install -d -m 755 "$TARGET_ROOT/usr/local/share/airplanes" +@test "feed binary gate: fails when the feed binary is missing" { + # current exists but the active release ships no bin/feed-airplanes. + local rel="$TARGET_ROOT/opt/airplanes/releases/v1.0.0" + install -d -m 755 "$rel/bin" + install -d -m 755 "$TARGET_ROOT/opt/airplanes" + ln -sfn "$rel" "$TARGET_ROOT/opt/airplanes/current" run _airplanes_runtime_probe_feed_binary_current "$TARGET_ROOT" [ "$status" -ne 0 ] - [[ "$output" == *"not a symlink"* ]] + [[ "$output" == *"missing"* ]] } # --- restart order membership ----------------------------------------------- @@ -120,23 +117,33 @@ JSON # --- managed_paths + systemd.json declarations ------------------------------ -@test "managed_paths.json declares feed binary, apl-feed, and units" { +@test "managed_paths.json declares apl-feed launcher and feed/mlat units" { local mp mp="$(cat "$BATS_TEST_DIRNAME/../../runtime-overlay/manifest-inputs/managed_paths.json")" local link - # The mlat-client venv is intentionally NOT a managed_path yet — its - # prebuilt overlay delivery is deferred to a follow-up. + # Only the /usr/local/bin launcher shim and the /etc unit symlinks are + # managed. The feed binary, the *.sh wrappers, and the mlat-client venv + # ride in the overlay payload under /opt/airplanes/current and are reached + # by absolute path, not via a managed symlink. for link in \ - /usr/local/share/airplanes/feed-airplanes \ /usr/local/bin/apl-feed \ - /usr/local/share/airplanes/airplanes-feed.sh \ - /usr/local/share/airplanes/airplanes-mlat.sh \ /etc/systemd/system/airplanes-feed.service \ /etc/systemd/system/airplanes-mlat.service; do local mode mode="$(printf '%s' "$mp" | jq -r --arg l "$link" '[.[] | select(.link == $l)][0].mode')" [ "$mode" = "symlink" ] || { echo "missing symlink managed_path for $link (got mode=$mode)" >&2; return 1; } done + # De-squat: the feed payload must NOT be managed under /usr/local/share/airplanes. + local squat + for squat in \ + /usr/local/share/airplanes/feed-airplanes \ + /usr/local/share/airplanes/airplanes-feed.sh \ + /usr/local/share/airplanes/airplanes-mlat.sh; do + if printf '%s' "$mp" | jq -e --arg l "$squat" 'any(.[]; .link == $l)' >/dev/null; then + echo "unexpected managed squat for $squat" >&2 + return 1 + fi + done } @test "systemd.json enables airplanes-feed and airplanes-mlat" { diff --git a/test/runtime-overlay/test_install_health_gate_readsb.bats b/test/runtime-overlay/test_install_health_gate_readsb.bats index 2fe43f5594..2247287e7f 100644 --- a/test/runtime-overlay/test_install_health_gate_readsb.bats +++ b/test/runtime-overlay/test_install_health_gate_readsb.bats @@ -72,8 +72,8 @@ srv.serve_forever() # Pre-stage the UAT state files with valid (state, reason) so those # gates pass — we're testing readsb today; the UAT gate has its own # bats file. - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" run airplanes_runtime_run_health_gates "$TARGET_ROOT" kill "$HTTPD_PID" 2>/dev/null || true @@ -100,8 +100,8 @@ srv.serve_forever() # No HTTP fixture — point probe URL at a closed port. curl returns # connection-refused; the gate must fail before its deadline. export AIRPLANES_RUNTIME_PROBE_URL_BASE="http://127.0.0.1:1" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" run airplanes_runtime_run_health_gates "$TARGET_ROOT" [ "$status" -ne 0 ] diff --git a/test/runtime-overlay/test_install_managed_paths_symlink.bats b/test/runtime-overlay/test_install_managed_paths_symlink.bats index c048db2eb6..6919fffc95 100644 --- a/test/runtime-overlay/test_install_managed_paths_symlink.bats +++ b/test/runtime-overlay/test_install_managed_paths_symlink.bats @@ -1,7 +1,7 @@ #!/usr/bin/env bats # Tests symlink-mode managed_paths application. -# - link points at an absolute /opt/airplanes-runtime/current/... target +# - link points at an absolute /opt/airplanes/current/... target # - the on-disk link target string MUST be the absolute manifest value, not # a relative one # - the apply is atomic (the link is created via tmp + mv -Tf, never via @@ -27,9 +27,9 @@ write_manifest() { "channel": "stable", "managed_paths": [ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" }, + "target": "/opt/airplanes/current/systemd/readsb.service" }, { "mode": "symlink", "link": "/usr/bin/airplanes-978", - "target": "/opt/airplanes-runtime/current/bin/readsb" } + "target": "/opt/airplanes/current/bin/readsb" } ] } JSON @@ -45,11 +45,11 @@ JSON run readlink "$TARGET_ROOT/etc/systemd/system/readsb.service" [ "$status" -eq 0 ] - [ "$output" = "/opt/airplanes-runtime/current/systemd/readsb.service" ] + [ "$output" = "/opt/airplanes/current/systemd/readsb.service" ] run readlink "$TARGET_ROOT/usr/bin/airplanes-978" [ "$status" -eq 0 ] - [ "$output" = "/opt/airplanes-runtime/current/bin/readsb" ] + [ "$output" = "/opt/airplanes/current/bin/readsb" ] } @test "second apply atomically replaces an existing link" { @@ -64,7 +64,7 @@ JSON [ "$status" -eq 0 ] run readlink "$TARGET_ROOT/etc/systemd/system/readsb.service" [ "$status" -eq 0 ] - [ "$output" = "/opt/airplanes-runtime/current/systemd/readsb.service" ] + [ "$output" = "/opt/airplanes/current/systemd/readsb.service" ] } @test "relative target string in manifest is rejected" { @@ -102,7 +102,7 @@ JSON [ "$status" -eq 0 ] [ -L "$TARGET_ROOT/etc/systemd/system/readsb.service" ] [ "$(readlink "$TARGET_ROOT/etc/systemd/system/readsb.service")" = \ - "/opt/airplanes-runtime/current/systemd/readsb.service" ] + "/opt/airplanes/current/systemd/readsb.service" ] } @test "apply replaces a pre-existing regular file at the link path" { @@ -128,7 +128,7 @@ JSON [ "$status" -ne 0 ] # The overlay's own release tree is off-limits as a managed destination. - run _airplanes_runtime_assert_safe_managed_path "/opt/airplanes-runtime/releases/v1.0.0" + run _airplanes_runtime_assert_safe_managed_path "/opt/airplanes/releases/v1.0.0" [ "$status" -ne 0 ] # A relative path is refused. diff --git a/test/runtime-overlay/test_install_migrations_shell.bats b/test/runtime-overlay/test_install_migrations_shell.bats index 2568744f63..48ae5bed0e 100644 --- a/test/runtime-overlay/test_install_migrations_shell.bats +++ b/test/runtime-overlay/test_install_migrations_shell.bats @@ -75,10 +75,10 @@ JSON # Simulate a flip having occurred: point current at a different dir, but # still pass the original RELEASE_DIR to the rollback. The rollback # should resolve scripts from RELEASE_DIR (the new release), not from - # /opt/airplanes-runtime/current/. - install -d -m 755 "$TARGET_ROOT/opt/airplanes-runtime" - rm -f "$TARGET_ROOT/opt/airplanes-runtime/current" - ln -s "$BATS_TEST_TMPDIR/somewhere-else" "$TARGET_ROOT/opt/airplanes-runtime/current" + # /opt/airplanes/current/. + install -d -m 755 "$TARGET_ROOT/opt/airplanes" + rm -f "$TARGET_ROOT/opt/airplanes/current" + ln -s "$BATS_TEST_TMPDIR/somewhere-else" "$TARGET_ROOT/opt/airplanes/current" run airplanes_runtime_run_migrations_rollback "$BATS_TEST_TMPDIR/manifest.json" "$RELEASE_DIR" "$TARGET_ROOT" [ "$status" -eq 0 ] diff --git a/test/runtime-overlay/test_install_relink_decoder_binaries.bats b/test/runtime-overlay/test_install_relink_decoder_binaries.bats index a2be90d692..899a78fdc1 100644 --- a/test/runtime-overlay/test_install_relink_decoder_binaries.bats +++ b/test/runtime-overlay/test_install_relink_decoder_binaries.bats @@ -1,8 +1,11 @@ #!/usr/bin/env bats # Tests airplanes_runtime_relink_decoder_binaries: -# - Both /usr/bin/readsb and /usr/bin/airplanes-978 end up as symlinks to -# /opt/airplanes-runtime/current/bin/readsb (decision 14). +# - Two operator shims under /usr/local/bin — readsb and dump978-fa — each +# point at their OWN binary under /opt/airplanes/current/bin/. +# - The old /usr/bin/{readsb,airplanes-978} squat aliases are no longer +# created (the airplanes-978 PATH alias is dropped; the wrapper uses +# exec -a internally). # - The relink is atomic (no .tmp leftovers, no race window). # - Re-running replaces stale prior targets cleanly. @@ -16,34 +19,40 @@ setup() { TARGET_ROOT="$(mk_target_root "$BATS_TEST_TMPDIR")" } -@test "creates both symlinks pointing at current/bin/readsb" { +@test "creates both operator shims under /usr/local/bin pointing at their own current/bin binary" { run airplanes_runtime_relink_decoder_binaries "$TARGET_ROOT" [ "$status" -eq 0 ] - [ -L "$TARGET_ROOT/usr/bin/readsb" ] - [ -L "$TARGET_ROOT/usr/bin/airplanes-978" ] - [ "$(readlink "$TARGET_ROOT/usr/bin/readsb")" = "/opt/airplanes-runtime/current/bin/readsb" ] - [ "$(readlink "$TARGET_ROOT/usr/bin/airplanes-978")" = "/opt/airplanes-runtime/current/bin/readsb" ] + [ -L "$TARGET_ROOT/usr/local/bin/readsb" ] + [ -L "$TARGET_ROOT/usr/local/bin/dump978-fa" ] + [ "$(readlink "$TARGET_ROOT/usr/local/bin/readsb")" = "/opt/airplanes/current/bin/readsb" ] + [ "$(readlink "$TARGET_ROOT/usr/local/bin/dump978-fa")" = "/opt/airplanes/current/bin/dump978-fa" ] + # The dropped squat aliases must not be created. + [ ! -e "$TARGET_ROOT/usr/bin/readsb" ] + [ ! -e "$TARGET_ROOT/usr/bin/airplanes-978" ] } @test "atomic-replaces a stale prior target" { # Seed with a stale link pointing somewhere else. - rm -f "$TARGET_ROOT/usr/bin/readsb" - ln -s "/tmp/stale" "$TARGET_ROOT/usr/bin/readsb" + install -d -m 755 "$TARGET_ROOT/usr/local/bin" + rm -f "$TARGET_ROOT/usr/local/bin/readsb" + ln -s "/tmp/stale" "$TARGET_ROOT/usr/local/bin/readsb" run airplanes_runtime_relink_decoder_binaries "$TARGET_ROOT" [ "$status" -eq 0 ] - [ "$(readlink "$TARGET_ROOT/usr/bin/readsb")" = "/opt/airplanes-runtime/current/bin/readsb" ] + [ "$(readlink "$TARGET_ROOT/usr/local/bin/readsb")" = "/opt/airplanes/current/bin/readsb" ] } -@test "leaves no .tmp leftovers under /usr/bin/" { +@test "leaves no .tmp leftovers under /usr/local/bin/" { airplanes_runtime_relink_decoder_binaries "$TARGET_ROOT" - run find "$TARGET_ROOT/usr/bin" -name '*.tmp.*' + run find "$TARGET_ROOT/usr/local/bin" -name '*.tmp.*' [ "$status" -eq 0 ] [ -z "$output" ] } -@test "both links share the same physical target after flip" { +@test "each shim points at its own distinct binary after flip" { airplanes_runtime_relink_decoder_binaries "$TARGET_ROOT" - # Read the link strings — they're absolute and identical. - [ "$(readlink "$TARGET_ROOT/usr/bin/readsb")" = \ - "$(readlink "$TARGET_ROOT/usr/bin/airplanes-978")" ] + # Read the link strings — they're absolute and point at different binaries. + [ "$(readlink "$TARGET_ROOT/usr/local/bin/readsb")" = "/opt/airplanes/current/bin/readsb" ] + [ "$(readlink "$TARGET_ROOT/usr/local/bin/dump978-fa")" = "/opt/airplanes/current/bin/dump978-fa" ] + [ "$(readlink "$TARGET_ROOT/usr/local/bin/readsb")" != \ + "$(readlink "$TARGET_ROOT/usr/local/bin/dump978-fa")" ] } diff --git a/test/runtime-overlay/test_install_runtime_smoke.bats b/test/runtime-overlay/test_install_runtime_smoke.bats index f8d60f79fc..5b0e988d6c 100644 --- a/test/runtime-overlay/test_install_runtime_smoke.bats +++ b/test/runtime-overlay/test_install_runtime_smoke.bats @@ -59,7 +59,7 @@ setup() { "managed_paths": [ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" } + "target": "/opt/airplanes/current/systemd/readsb.service" } ], "mutable_paths": [], "systemd": { "enable": ["readsb.service"], "daemon_reload": true }, @@ -79,8 +79,8 @@ JSON # Pre-stage the on-device gate inputs: : > "$TARGET_ROOT/run/readsb/aircraft.json" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" # Spawn http.server. HTTPD_LOG="$BATS_TEST_TMPDIR/httpd.log" @@ -139,16 +139,17 @@ run_install_runtime() { echo "$output" return 1 fi - [ -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] - [ -L "$TARGET_ROOT/opt/airplanes-runtime/current" ] + [ -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] + [ -L "$TARGET_ROOT/opt/airplanes/current" ] # In runtime mode the link target is the absolute release-dir path # the install pipeline operated against. In production that's - # /opt/airplanes-runtime/releases/v/; in tests TARGET_ROOT is a + # /opt/airplanes/releases/v/; in tests TARGET_ROOT is a # tmpdir so the link string contains the rebase. - [ "$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" = "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] + [ "$(readlink "$TARGET_ROOT/opt/airplanes/current")" = "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] [ -L "$TARGET_ROOT/etc/systemd/system/readsb.service" ] - [ -L "$TARGET_ROOT/usr/bin/readsb" ] - [ -L "$TARGET_ROOT/usr/bin/airplanes-978" ] + [ -L "$TARGET_ROOT/usr/local/bin/readsb" ] + [ -L "$TARGET_ROOT/usr/local/bin/dump978-fa" ] + [ ! -e "$TARGET_ROOT/usr/bin/readsb" ] # systemd ops shimmed: daemon-reload + enable + restart all logged. run grep -F 'daemon-reload' "$SYSCTL_LOG" [ "$status" -eq 0 ] @@ -158,7 +159,7 @@ run_install_runtime() { [ "$status" -eq 0 ] # Runtime-manifest pointer was recorded. [ -L "$TARGET_ROOT/etc/airplanes/runtime-manifest.json" ] - [ "$(readlink "$TARGET_ROOT/etc/airplanes/runtime-manifest.json")" = "/opt/airplanes-runtime/current/manifest.json" ] + [ "$(readlink "$TARGET_ROOT/etc/airplanes/runtime-manifest.json")" = "/opt/airplanes/current/manifest.json" ] } @test "runtime install fails closed when health gate trips" { diff --git a/test/runtime-overlay/test_install_same_version_safety.bats b/test/runtime-overlay/test_install_same_version_safety.bats index 0ab9999f47..d1602955aa 100644 --- a/test/runtime-overlay/test_install_same_version_safety.bats +++ b/test/runtime-overlay/test_install_same_version_safety.bats @@ -75,10 +75,10 @@ srv.serve_forever() PORT="$(tr -d '[:space:]' < "$PORT_FILE")" ROOTFS_DIR="$BATS_TEST_TMPDIR/rootfs" - install -d -m 755 "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER" - : > "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER/sentinel" + install -d -m 755 "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER" + : > "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER/sentinel" # Wire up `current` to point AT the same dir we're about to extract. - ln -s "/opt/airplanes-runtime/releases/v$REL_VER" "$ROOTFS_DIR/opt/airplanes-runtime/current" + ln -s "/opt/airplanes/releases/v$REL_VER" "$ROOTFS_DIR/opt/airplanes/current" } teardown() { @@ -103,5 +103,5 @@ teardown() { [[ "$output" == *"active 'current' target"* ]] # The sentinel from the prior release MUST still exist — the install # did NOT rm -rf the live release. - [ -f "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER/sentinel" ] + [ -f "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER/sentinel" ] } diff --git a/test/runtime-overlay/test_install_symlink_preimage.bats b/test/runtime-overlay/test_install_symlink_preimage.bats index f141bbef43..9f9f9bc038 100644 --- a/test/runtime-overlay/test_install_symlink_preimage.bats +++ b/test/runtime-overlay/test_install_symlink_preimage.bats @@ -23,7 +23,7 @@ _mk_symlink_manifest() { "managed_paths": [ { "mode": "symlink", "link": "/usr/local/share/tar1090", - "target": "/opt/airplanes-runtime/current/share/tar1090" } + "target": "/opt/airplanes/current/share/tar1090" } ] } JSON @@ -41,7 +41,7 @@ JSON # Simulate apply: rm the dir and place a symlink. rm -rf "$TARGET_ROOT/usr/local/share/tar1090" - ln -s /opt/airplanes-runtime/current/share/tar1090 \ + ln -s /opt/airplanes/current/share/tar1090 \ "$TARGET_ROOT/usr/local/share/tar1090" run airplanes_runtime_restore_all_symlink_paths \ @@ -115,7 +115,7 @@ JSON "$BATS_TEST_TMPDIR/manifest.json" "$RELEASE_DIR" "$TARGET_ROOT" # Simulate first-time install: created by apply. install -d -m 755 "$TARGET_ROOT/usr/local/share" - ln -s /opt/airplanes-runtime/current/share/tar1090 \ + ln -s /opt/airplanes/current/share/tar1090 \ "$TARGET_ROOT/usr/local/share/tar1090" airplanes_runtime_restore_all_symlink_paths \ diff --git a/test/runtime-overlay/test_mlat_unit_condition.bats b/test/runtime-overlay/test_mlat_unit_condition.bats index a166889c65..8c9fce75ad 100644 --- a/test/runtime-overlay/test_mlat_unit_condition.bats +++ b/test/runtime-overlay/test_mlat_unit_condition.bats @@ -2,7 +2,7 @@ # Verify the overlay-staged airplanes-mlat.service includes the venv condition # gate. The overlay post-processes the unit after staging from the feed repo to -# add ConditionPathExists=/usr/local/share/airplanes/venv/bin/mlat-client so the +# add ConditionPathExists=/opt/airplanes/current/share/airplanes/venv/bin/mlat-client so the # unit never starts when the venv is absent (e.g. a decoder-only release). # # This test constructs a minimal feed unit, runs the awk injection from @@ -24,7 +24,7 @@ _inject_condition() { /^\[Unit\]/ { print; in_unit = 1; next } in_unit && /^Description=/ { print - print "ConditionPathExists=/usr/local/share/airplanes/venv/bin/mlat-client" + print "ConditionPathExists=/opt/airplanes/current/share/airplanes/venv/bin/mlat-client" next } /^\[/ && !/^\[Unit\]/ { in_unit = 0 } @@ -44,7 +44,7 @@ After=network.target airplanes-first-run.service [Service] User=airplanes-feed -ExecStart=/usr/local/share/airplanes/airplanes-mlat.sh +ExecStart=/opt/airplanes/current/share/airplanes/airplanes-mlat.sh Type=simple Restart=always @@ -56,7 +56,7 @@ EOF # The condition must appear exactly once. local count - count="$(grep -c '^ConditionPathExists=/usr/local/share/airplanes/venv/bin/mlat-client$' "$UNIT_DIR/airplanes-mlat.service")" + count="$(grep -c '^ConditionPathExists=/opt/airplanes/current/share/airplanes/venv/bin/mlat-client$' "$UNIT_DIR/airplanes-mlat.service")" [ "$count" -eq 1 ] # It must appear in the [Unit] section, after Description. @@ -77,7 +77,7 @@ Description=airplanes-mlat Wants=network.target [Service] -ExecStart=/usr/local/share/airplanes/airplanes-mlat.sh +ExecStart=/opt/airplanes/current/share/airplanes/airplanes-mlat.sh [Install] WantedBy=default.target @@ -95,11 +95,11 @@ EOF cat > "$UNIT_DIR/airplanes-mlat.service" <<'EOF' [Unit] Description=airplanes-mlat -ConditionPathExists=/usr/local/share/airplanes/venv/bin/mlat-client +ConditionPathExists=/opt/airplanes/current/share/airplanes/venv/bin/mlat-client Wants=network.target [Service] -ExecStart=/usr/local/share/airplanes/airplanes-mlat.sh +ExecStart=/opt/airplanes/current/share/airplanes/airplanes-mlat.sh [Install] WantedBy=default.target diff --git a/test/runtime-overlay/test_orchestrator_sequence.bats b/test/runtime-overlay/test_orchestrator_sequence.bats index c11e4ef2aa..b41e931976 100644 --- a/test/runtime-overlay/test_orchestrator_sequence.bats +++ b/test/runtime-overlay/test_orchestrator_sequence.bats @@ -68,7 +68,7 @@ run_orchestrator() { AIRPLANES_ORCHESTRATOR_STATE_FILE="$STATE_FILE" \ AIRPLANES_ORCHESTRATOR_LOCK_FILE="$LOCK_FILE" \ AIRPLANES_ORCHESTRATOR_RUNTIME_UPDATE="$TMP/sub/runtime-update.sh" \ - AIRPLANES_ORCHESTRATOR_RUNTIME_UPGRADE_STATE="$TMP/var/lib/airplanes-runtime-upgrade/upgrade-state" \ + AIRPLANES_ORCHESTRATOR_RUNTIME_UPGRADE_STATE="$TMP/var/lib/airplanes/runtime-upgrade/upgrade-state" \ AIRPLANES_ORCHESTRATOR_APT_GET="apt-get" \ bash "$ORCH" "$@" } @@ -207,10 +207,10 @@ EOF } @test "runtime same-version-replay is treated as a no-op success" { - install -d -m 0755 "$TMP/var/lib/airplanes-runtime-upgrade" + install -d -m 0755 "$TMP/var/lib/airplanes/runtime-upgrade" cat > "$TMP/sub/runtime-update.sh" < "$TMP/var/lib/airplanes-runtime-upgrade/upgrade-state" < "$TMP/var/lib/airplanes/runtime-upgrade/upgrade-state" < "$TMP/sub/runtime-update.sh" < "$TMP/var/lib/airplanes-runtime-upgrade/upgrade-state" < "$TMP/var/lib/airplanes/runtime-upgrade/upgrade-state" < "$TMP/var/lib/airplanes-runtime-upgrade/upgrade-state" < "$TMP/var/lib/airplanes/runtime-upgrade/upgrade-state" < "$TRAMP" chmod 0755 "$TRAMP" } diff --git a/test/runtime-overlay/test_pubkey_shipped.bats b/test/runtime-overlay/test_pubkey_shipped.bats index 85e8e31e47..cef41822b2 100644 --- a/test/runtime-overlay/test_pubkey_shipped.bats +++ b/test/runtime-overlay/test_pubkey_shipped.bats @@ -1,8 +1,8 @@ #!/usr/bin/env bats # The runtime-overlay release public key must ship with the image. Stage -# 00-prep installs it from files/usr/share/airplanes/runtime-release.pub -# to /usr/share/airplanes/runtime-release.pub in the rootfs; on-device +# 00-prep installs it from files/opt/airplanes/libexec/runtime-release.pub +# to /opt/airplanes/libexec/runtime-release.pub in the rootfs; on-device # install.sh verifies SHA256SUMS against the same key. # # Tests assert the file is committed, has the documented two-line minisign @@ -12,7 +12,7 @@ bats_require_minimum_version 1.5.0 setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - PUBKEY="$REPO_ROOT/stage-airplanes/00-prep/files/usr/share/airplanes/runtime-release.pub" + PUBKEY="$REPO_ROOT/stage-airplanes/00-prep/files/opt/airplanes/libexec/runtime-release.pub" } @test "pubkey file is committed at the expected path" { diff --git a/test/runtime-overlay/test_recover_shim.bats b/test/runtime-overlay/test_recover_shim.bats index 626cd056be..f95507f810 100644 --- a/test/runtime-overlay/test_recover_shim.bats +++ b/test/runtime-overlay/test_recover_shim.bats @@ -13,7 +13,7 @@ bats_require_minimum_version 1.5.0 load lib/install_test_helpers -SHIM_PATH="$REPO_ROOT/stage-airplanes/02-install-runtime-overlay/files/usr/local/lib/airplanes-runtime/recover-shim" +SHIM_PATH="$REPO_ROOT/stage-airplanes/02-install-runtime-overlay/files/opt/airplanes/libexec/recover-shim" setup() { TARGET_ROOT="$(mk_target_root "$BATS_TEST_TMPDIR")" @@ -23,7 +23,7 @@ setup() { # Helper: write a state file for the shim (uses the shim's own path layout). shim_state() { local state="$1"; shift - local dir="$TARGET_ROOT/var/lib/airplanes-runtime-upgrade" + local dir="$TARGET_ROOT/var/lib/airplanes/runtime-upgrade" install -d -m 755 "$dir" { printf 'state=%s\n' "$state" @@ -37,16 +37,16 @@ shim_state() { # Helper: create a valid release tree at a device-canonical path. shim_release() { local version="$1" - local d="$TARGET_ROOT/opt/airplanes-runtime/releases/v$version" + local d="$TARGET_ROOT/opt/airplanes/releases/v$version" install -d -m 755 "$d" printf '{"version":"%s"}\n' "$version" > "$d/manifest.json" - printf '%s' "/opt/airplanes-runtime/releases/v$version" + printf '%s' "/opt/airplanes/releases/v$version" } # Helper: point current at a device-canonical release path. shim_current() { local dev_path="$1" - local link="$TARGET_ROOT/opt/airplanes-runtime/current" + local link="$TARGET_ROOT/opt/airplanes/current" install -d -m 755 "$(dirname "$link")" rm -f "$link" ln -s "$dev_path" "$link" @@ -54,12 +54,12 @@ shim_current() { # Helper: read the state token from the state file. shim_read_state() { - sed -n 's/^state=//p' "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" 2>/dev/null | head -n1 + sed -n 's/^state=//p' "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" 2>/dev/null | head -n1 } # Helper: read the recovery-status token. shim_read_status() { - sed -n 's/^status=//p' "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/recovery-status" 2>/dev/null | head -n1 + sed -n 's/^status=//p' "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/recovery-status" 2>/dev/null | head -n1 } # --------------------------------------------------------------------------- @@ -69,17 +69,17 @@ shim_read_status() { local prev new prev="$(shim_release 1.0.0)" new="$(shim_release 1.1.0)" - shim_current "/opt/airplanes-runtime/releases/v1.1.0" + shim_current "/opt/airplanes/releases/v1.1.0" shim_state HEALTH_PASSED \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v1.0.0" \ + "new_release=/opt/airplanes/releases/v1.1.0" run sh "$SHIM_PATH" [ "$status" -eq 0 ] # current must NOT have changed — still points at 1.1.0. local cur - cur="$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" - [ "$cur" = "/opt/airplanes-runtime/releases/v1.1.0" ] + cur="$(readlink "$TARGET_ROOT/opt/airplanes/current")" + [ "$cur" = "/opt/airplanes/releases/v1.1.0" ] [ "$(shim_read_status)" = "ok" ] } @@ -90,16 +90,16 @@ shim_read_status() { local prev new prev="$(shim_release 1.0.0)" new="$(shim_release 1.1.0)" - shim_current "/opt/airplanes-runtime/releases/v1.1.0" + shim_current "/opt/airplanes/releases/v1.1.0" shim_state SYMLINK_FLIPPED \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v1.0.0" \ + "new_release=/opt/airplanes/releases/v1.1.0" run sh "$SHIM_PATH" [ "$status" -eq 0 ] local cur - cur="$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" - [ "$cur" = "/opt/airplanes-runtime/releases/v1.0.0" ] + cur="$(readlink "$TARGET_ROOT/opt/airplanes/current")" + [ "$cur" = "/opt/airplanes/releases/v1.0.0" ] [[ "$(shim_read_state)" == ROLLED_BACK_SHIM_ONLY_FROM_SYMLINK_FLIPPED ]] [ "$(shim_read_status)" = "rolled_back" ] } @@ -109,19 +109,19 @@ shim_read_status() { # --------------------------------------------------------------------------- @test "shim: missing prev_release falls back to last-good-release" { shim_release 0.9.0 - shim_current "/opt/airplanes-runtime/releases/v1.1.0" + shim_current "/opt/airplanes/releases/v1.1.0" shim_state SYMLINK_FLIPPED \ - "prev_release=/opt/airplanes-runtime/releases/v_GONE" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v_GONE" \ + "new_release=/opt/airplanes/releases/v1.1.0" # Write last-good pointing at 0.9.0 - printf '/opt/airplanes-runtime/releases/v0.9.0\n' \ - > "$TARGET_ROOT/var/lib/airplanes-runtime/last-good-release" + printf '/opt/airplanes/releases/v0.9.0\n' \ + > "$TARGET_ROOT/var/lib/airplanes/runtime/last-good-release" run sh "$SHIM_PATH" [ "$status" -eq 0 ] local cur - cur="$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" - [ "$cur" = "/opt/airplanes-runtime/releases/v0.9.0" ] + cur="$(readlink "$TARGET_ROOT/opt/airplanes/current")" + [ "$cur" = "/opt/airplanes/releases/v0.9.0" ] } # --------------------------------------------------------------------------- @@ -129,10 +129,10 @@ shim_read_status() { # --------------------------------------------------------------------------- @test "shim: 3 failed recovery attempts triggers needs_ssh" { # No valid prev and no valid last-good → every attempt fails. - shim_current "/opt/airplanes-runtime/releases/v_BROKEN" + shim_current "/opt/airplanes/releases/v_BROKEN" shim_state SYMLINK_FLIPPED \ - "prev_release=/opt/airplanes-runtime/releases/v_GONE" \ - "new_release=/opt/airplanes-runtime/releases/v_BROKEN" + "prev_release=/opt/airplanes/releases/v_GONE" \ + "new_release=/opt/airplanes/releases/v_BROKEN" # Run 3 times. sh "$SHIM_PATH" 2>/dev/null || true @@ -152,17 +152,17 @@ shim_read_status() { local prev prev="$(shim_release 1.0.0)" shim_release 1.1.0 - shim_current "/opt/airplanes-runtime/releases/v1.1.0" + shim_current "/opt/airplanes/releases/v1.1.0" shim_state HEALTH_RUNNING \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v1.0.0" \ + "new_release=/opt/airplanes/releases/v1.1.0" # Deliberately do NOT have install-common.sh present. run sh "$SHIM_PATH" [ "$status" -eq 0 ] local cur - cur="$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" - [ "$cur" = "/opt/airplanes-runtime/releases/v1.0.0" ] + cur="$(readlink "$TARGET_ROOT/opt/airplanes/current")" + [ "$cur" = "/opt/airplanes/releases/v1.0.0" ] } # --------------------------------------------------------------------------- @@ -190,14 +190,14 @@ shim_read_status() { # --------------------------------------------------------------------------- @test "shim: STARTED clears state without rollback" { shim_release 1.0.0 - shim_current "/opt/airplanes-runtime/releases/v1.0.0" + shim_current "/opt/airplanes/releases/v1.0.0" shim_state STARTED \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" + "prev_release=/opt/airplanes/releases/v1.0.0" run sh "$SHIM_PATH" [ "$status" -eq 0 ] # State file should be gone (cleared). - [ ! -e "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" ] + [ ! -e "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" ] } # --------------------------------------------------------------------------- @@ -206,16 +206,16 @@ shim_read_status() { @test "shim: SYSTEMD_OPS_DONE flips current back to prev_release" { shim_release 1.0.0 shim_release 1.1.0 - shim_current "/opt/airplanes-runtime/releases/v1.1.0" + shim_current "/opt/airplanes/releases/v1.1.0" shim_state SYSTEMD_OPS_DONE \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v1.0.0" \ + "new_release=/opt/airplanes/releases/v1.1.0" run sh "$SHIM_PATH" [ "$status" -eq 0 ] local cur - cur="$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" - [ "$cur" = "/opt/airplanes-runtime/releases/v1.0.0" ] + cur="$(readlink "$TARGET_ROOT/opt/airplanes/current")" + [ "$cur" = "/opt/airplanes/releases/v1.0.0" ] } # --------------------------------------------------------------------------- @@ -223,8 +223,8 @@ shim_read_status() { # --------------------------------------------------------------------------- @test "shim: valid_release rejects ../traversal" { shim_state SYMLINK_FLIPPED \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0/../../etc/shadow" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v1.0.0/../../etc/shadow" \ + "new_release=/opt/airplanes/releases/v1.1.0" run sh "$SHIM_PATH" # Should fail (no valid target → retrying/needs_ssh depending on attempts) diff --git a/test/runtime-overlay/test_render_status.bats b/test/runtime-overlay/test_render_status.bats index ae188ab9ff..0d18074ad7 100644 --- a/test/runtime-overlay/test_render_status.bats +++ b/test/runtime-overlay/test_render_status.bats @@ -266,7 +266,7 @@ STUB # OR calls install_state_reader_stub for a working stub. setup_mlat_state_test_env() { install_state_reader_stub - PATHS_STATE_FILE_MLAT="$TMP/run/airplanes-mlat/state" + PATHS_STATE_FILE_MLAT="$TMP/run/airplanes/mlat/state" } @test "mlat_config_state: active + state file present + state=enabled,reason=ok" { @@ -1389,7 +1389,7 @@ write_978_state() { } > "$PATHS_STATE_FILE_978" } -# Producer-side fixture: dump978-fa.sh writes /run/dump978-fa/state. +# Producer-side fixture: dump978-fa.sh writes /run/airplanes/dump978-fa/state. # _978_config_state picks this path when the unit is dump978-fa.service. write_dump978fa_state() { local decision="$1" reason="$2" @@ -1404,8 +1404,8 @@ write_dump978fa_state() { setup_978_state_test_env() { install_state_reader_stub - PATHS_STATE_FILE_978="$TMP/run/airplanes-978/state" - PATHS_STATE_FILE_DUMP978FA="$TMP/run/dump978-fa/state" + PATHS_STATE_FILE_978="$TMP/run/airplanes/978/state" + PATHS_STATE_FILE_DUMP978FA="$TMP/run/airplanes/dump978-fa/state" } # Stub systemctl returning chosen ActiveState/ExecMainStatus for the @@ -1468,7 +1468,7 @@ STUB [ "$output" = 'misconfigured unknown' ] } -@test "_978_config_state: dump978-fa reads /run/dump978-fa/state (not airplanes-978's)" { +@test "_978_config_state: dump978-fa reads /run/airplanes/dump978-fa/state (not airplanes-978's)" { setup_978_state_test_env # Different states in the two files; the unit-name dispatch must pick # the right one so a producer-side decision doesn't mask the consumer @@ -1480,7 +1480,7 @@ STUB [ "$output" = 'disabled no_hardware' ] } -@test "_978_config_state: airplanes-978 reads /run/airplanes-978/state (peer_no_hardware refinement)" { +@test "_978_config_state: airplanes-978 reads /run/airplanes/978/state (peer_no_hardware refinement)" { setup_978_state_test_env write_978_state enabled peer_no_hardware write_dump978fa_state disabled no_hardware @@ -2157,7 +2157,7 @@ EOF setup_feed_state_test_env() { install_state_reader_stub - PATHS_STATE_FILE_FEED="$TMP/run/airplanes-feed/state" + PATHS_STATE_FILE_FEED="$TMP/run/airplanes/feed/state" } # write_feed_state_endpoint diff --git a/test/runtime-overlay/test_runtime_manifest_schema.bats b/test/runtime-overlay/test_runtime_manifest_schema.bats index e6ab36057a..6b986c0009 100644 --- a/test/runtime-overlay/test_runtime_manifest_schema.bats +++ b/test/runtime-overlay/test_runtime_manifest_schema.bats @@ -150,7 +150,7 @@ mutate_golden() { mutated="$(mutate_golden ' (.managed_paths | map(if .mode == "copy" - then .path = "/usr/bin/readsb" + then .path = "/etc/systemd/system/readsb.service" else . end)) as $p | .managed_paths = $p diff --git a/test/runtime-overlay/test_self_update_failure_pre_mutation.bats b/test/runtime-overlay/test_self_update_failure_pre_mutation.bats index 5d1dde5605..d549d5ad05 100644 --- a/test/runtime-overlay/test_self_update_failure_pre_mutation.bats +++ b/test/runtime-overlay/test_self_update_failure_pre_mutation.bats @@ -49,12 +49,12 @@ setup() { [ "$(read_state "$TARGET_ROOT")" = "FAILED_PRE_MUTATION" ] # failure_reason captured in the state file. grep -E '^failure_reason=download_failed$' \ - "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" + "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" # Critical: NO release dir or current symlink were created (no # mutation happened). - [ ! -L "$TARGET_ROOT/opt/airplanes-runtime/current" ] - run find "$TARGET_ROOT/opt/airplanes-runtime/releases" -mindepth 1 -maxdepth 1 -type d + [ ! -L "$TARGET_ROOT/opt/airplanes/current" ] + run find "$TARGET_ROOT/opt/airplanes/releases" -mindepth 1 -maxdepth 1 -type d [ "$status" -eq 0 ] [ -z "$output" ] } @@ -137,5 +137,5 @@ srv.serve_forever() [ "$status" -ne 0 ] [ "$(read_state "$TARGET_ROOT")" = "FAILED_PRE_MUTATION" ] - [ ! -L "$TARGET_ROOT/opt/airplanes-runtime/current" ] + [ ! -L "$TARGET_ROOT/opt/airplanes/current" ] } diff --git a/test/runtime-overlay/test_self_update_flock_contention.bats b/test/runtime-overlay/test_self_update_flock_contention.bats index 47e46fe8fd..ab3ccd210c 100644 --- a/test/runtime-overlay/test_self_update_flock_contention.bats +++ b/test/runtime-overlay/test_self_update_flock_contention.bats @@ -48,7 +48,7 @@ setup() { # The state file must NOT exist beforehand — the contending invocation # writing one would corrupt this assertion. - [ ! -e "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" ] + [ ! -e "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" ] run env \ AIRPLANES_BUILD_MODE=0 \ @@ -61,7 +61,7 @@ setup() { [ "$status" -eq 75 ] [[ "$output" == *"another runtime-overlay update is in progress"* ]] # Critical: no state file written by the losing invocation. - [ ! -e "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" ] + [ ! -e "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" ] # Release the holder. rm -f "$BATS_TEST_TMPDIR/keep-holding" diff --git a/test/runtime-overlay/test_self_update_flock_held_across_protocol.bats b/test/runtime-overlay/test_self_update_flock_held_across_protocol.bats index f4a2bad6ce..10c28c23f7 100644 --- a/test/runtime-overlay/test_self_update_flock_held_across_protocol.bats +++ b/test/runtime-overlay/test_self_update_flock_held_across_protocol.bats @@ -63,8 +63,8 @@ JSON printf 'ok' > "$HTTPD_DOC/graphs1090/index.html" : > "$TARGET_ROOT/run/readsb/aircraft.json" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" HTTPD_LOG="$BATS_TEST_TMPDIR/httpd.log" PORT_FILE="$BATS_TEST_TMPDIR/httpd.port" diff --git a/test/runtime-overlay/test_self_update_health_passed_resume.bats b/test/runtime-overlay/test_self_update_health_passed_resume.bats index 6b8ae8125a..e4d7af4867 100644 --- a/test/runtime-overlay/test_self_update_health_passed_resume.bats +++ b/test/runtime-overlay/test_self_update_health_passed_resume.bats @@ -18,10 +18,10 @@ setup() { @test "write_last_good_release records the device-canonical path" { airplanes_runtime_write_last_good_release "$TARGET_ROOT" \ - "/opt/airplanes-runtime/releases/v1.2.3" - local f="$TARGET_ROOT/var/lib/airplanes-runtime/last-good-release" + "/opt/airplanes/releases/v1.2.3" + local f="$TARGET_ROOT/var/lib/airplanes/runtime/last-good-release" [ -f "$f" ] - [ "$(head -n1 "$f")" = "/opt/airplanes-runtime/releases/v1.2.3" ] + [ "$(head -n1 "$f")" = "/opt/airplanes/releases/v1.2.3" ] } @test "write_last_good_release rejects a relative path" { @@ -34,9 +34,9 @@ setup() { # resolve the manifest. local rel rel="$(mk_target_release "$TARGET_ROOT" 1.0.0)" - rm -f "$TARGET_ROOT/opt/airplanes-runtime/current" - ln -s "/opt/airplanes-runtime/releases/v1.0.0" \ - "$TARGET_ROOT/opt/airplanes-runtime/current" + rm -f "$TARGET_ROOT/opt/airplanes/current" + ln -s "/opt/airplanes/releases/v1.0.0" \ + "$TARGET_ROOT/opt/airplanes/current" # Runtime mode (not build mode) so a symlink pointer is written. AIRPLANES_BUILD_MODE=0 \ @@ -52,7 +52,7 @@ _mk_resume_manifest() { local entries="" first=1 link for link in "$@"; do [[ $first -eq 0 ]] && entries="$entries," - entries="$entries {\"mode\":\"symlink\",\"link\":\"$link\",\"target\":\"/opt/airplanes-runtime/current/bin/dummy\"}" + entries="$entries {\"mode\":\"symlink\",\"link\":\"$link\",\"target\":\"/opt/airplanes/current/bin/dummy\"}" first=0 done cat > "$dir/manifest.json" < "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" + > "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" run run_self_update [ "$status" -ne 0 ] [[ "$output" == *"malformed"* ]] diff --git a/test/runtime-overlay/test_self_update_rollback_from_each_state.bats b/test/runtime-overlay/test_self_update_rollback_from_each_state.bats index 0546eb5bb6..a3c78bca34 100644 --- a/test/runtime-overlay/test_self_update_rollback_from_each_state.bats +++ b/test/runtime-overlay/test_self_update_rollback_from_each_state.bats @@ -44,12 +44,12 @@ EOF # Pre-stage a prior release dir + a current symlink pointing at it # so the rollback path has somewhere to flip back to. PREV_VER="0.0.0" - PREV_DIR="$TARGET_ROOT/opt/airplanes-runtime/releases/v$PREV_VER" + PREV_DIR="$TARGET_ROOT/opt/airplanes/releases/v$PREV_VER" install -d -m 755 "$PREV_DIR/bin" "$PREV_DIR/lib" "$PREV_DIR/systemd" : > "$PREV_DIR/bin/readsb" chmod 755 "$PREV_DIR/bin/readsb" - ln -s "/opt/airplanes-runtime/releases/v$PREV_VER" \ - "$TARGET_ROOT/opt/airplanes-runtime/current" + ln -s "/opt/airplanes/releases/v$PREV_VER" \ + "$TARGET_ROOT/opt/airplanes/current" KEY_DIR="$BATS_TEST_TMPDIR/keys" install -d -m 700 "$KEY_DIR" @@ -91,8 +91,8 @@ JSON printf 'ok' > "$HTTPD_DOC/graphs1090/index.html" : > "$TARGET_ROOT/run/readsb/aircraft.json" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" HTTPD_LOG="$BATS_TEST_TMPDIR/httpd.log" PORT_FILE="$BATS_TEST_TMPDIR/httpd.port" @@ -161,11 +161,11 @@ run_self_update() { state="$(read_state "$TARGET_ROOT")" [[ "$state" == ROLLED_BACK_* ]] # current symlink reverted to prior release. - [ -L "$TARGET_ROOT/opt/airplanes-runtime/current" ] - [ "$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" \ - = "/opt/airplanes-runtime/releases/v$PREV_VER" ] + [ -L "$TARGET_ROOT/opt/airplanes/current" ] + [ "$(readlink "$TARGET_ROOT/opt/airplanes/current")" \ + = "/opt/airplanes/releases/v$PREV_VER" ] # new release dir cleaned up. - [ ! -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] + [ ! -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] } @test "health gate failure rolls back from HEALTH_RUNNING" { @@ -180,9 +180,9 @@ run_self_update() { state="$(read_state "$TARGET_ROOT")" [[ "$state" == ROLLED_BACK_* ]] # Confirm rolled-back symlink + dropped new release dir. - [ "$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" \ - = "/opt/airplanes-runtime/releases/v$PREV_VER" ] - [ ! -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] + [ "$(readlink "$TARGET_ROOT/opt/airplanes/current")" \ + = "/opt/airplanes/releases/v$PREV_VER" ] + [ ! -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] # Systemctl observed restart-of-readsb followed by the rollback's # stop-services pass — confirms we routed through the service-stop step # before reverting the symlink. The stop is a single multi-arg invocation @@ -195,7 +195,7 @@ run_self_update() { run run_self_update [ "$status" -ne 0 ] grep -E '^failure_reason=' \ - "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" + "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" } @test "rollback walks back to a clean state with no current pointing at new" { @@ -204,7 +204,7 @@ run_self_update() { [ "$status" -ne 0 ] # After rollback, current must NOT point at the new release. local current - current="$(readlink "$TARGET_ROOT/opt/airplanes-runtime/current")" - [[ "$current" != "/opt/airplanes-runtime/releases/v$REL_VER" ]] - [[ "$current" != "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ]] + current="$(readlink "$TARGET_ROOT/opt/airplanes/current")" + [[ "$current" != "/opt/airplanes/releases/v$REL_VER" ]] + [[ "$current" != "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ]] } diff --git a/test/runtime-overlay/test_self_update_state_helpers.bats b/test/runtime-overlay/test_self_update_state_helpers.bats index f1783c931a..b9e2a9901d 100644 --- a/test/runtime-overlay/test_self_update_state_helpers.bats +++ b/test/runtime-overlay/test_self_update_state_helpers.bats @@ -22,25 +22,25 @@ setup() { @test "state_write writes all keys and state_read returns the state" { airplanes_runtime_state_write "$TARGET_ROOT" STARTED \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" \ + "prev_release=/opt/airplanes/releases/v1.0.0" \ + "new_release=/opt/airplanes/releases/v1.1.0" \ "started_at=2026-05-20T00:00:00Z" run airplanes_runtime_state_read "$TARGET_ROOT" [ "$status" -eq 0 ] [ "$output" = "STARTED" ] run airplanes_runtime_state_get "$TARGET_ROOT" prev_release - [ "$output" = "/opt/airplanes-runtime/releases/v1.0.0" ] + [ "$output" = "/opt/airplanes/releases/v1.0.0" ] run airplanes_runtime_state_get "$TARGET_ROOT" new_release - [ "$output" = "/opt/airplanes-runtime/releases/v1.1.0" ] + [ "$output" = "/opt/airplanes/releases/v1.1.0" ] run airplanes_runtime_state_get "$TARGET_ROOT" started_at [ "$output" = "2026-05-20T00:00:00Z" ] } @test "state_write preserves prev_release / new_release on subsequent transitions" { airplanes_runtime_state_write "$TARGET_ROOT" STARTED \ - "prev_release=/opt/airplanes-runtime/releases/v1.0.0" \ - "new_release=/opt/airplanes-runtime/releases/v1.1.0" + "prev_release=/opt/airplanes/releases/v1.0.0" \ + "new_release=/opt/airplanes/releases/v1.1.0" airplanes_runtime_state_write "$TARGET_ROOT" PAYLOAD_EXTRACTED airplanes_runtime_state_write "$TARGET_ROOT" MIGRATIONS_FORWARD_DONE airplanes_runtime_state_write "$TARGET_ROOT" SYMLINK_FLIPPED @@ -48,9 +48,9 @@ setup() { run airplanes_runtime_state_read "$TARGET_ROOT" [ "$output" = "SYMLINK_FLIPPED" ] run airplanes_runtime_state_get "$TARGET_ROOT" prev_release - [ "$output" = "/opt/airplanes-runtime/releases/v1.0.0" ] + [ "$output" = "/opt/airplanes/releases/v1.0.0" ] run airplanes_runtime_state_get "$TARGET_ROOT" new_release - [ "$output" = "/opt/airplanes-runtime/releases/v1.1.0" ] + [ "$output" = "/opt/airplanes/releases/v1.1.0" ] } @test "state_write captures failure_reason on terminal failure" { @@ -77,9 +77,9 @@ setup() { } @test "state_read on malformed file returns UNKNOWN" { - install -d -m 755 "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade" + install -d -m 755 "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade" printf 'this is not a state file\n' \ - > "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" + > "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" run airplanes_runtime_state_read "$TARGET_ROOT" [ "$output" = "UNKNOWN" ] } @@ -96,13 +96,17 @@ setup() { # Remove the state dir BEFORE locking the parent so ensure_state_dir's # install -d fails. ensure_state_dir's `install -d -m 755` overrides # any chmod we apply to the state dir directly, so we must break - # the next-higher level instead. - rm -rf "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade" - chmod 0555 "$TARGET_ROOT/var/lib" + # the next-higher level instead. The state dir is now nested two deep + # (/var/lib/airplanes/runtime-upgrade), and /var/lib/airplanes already + # exists writable, so locking /var/lib no longer blocks the leaf — lock + # the immediate parent /var/lib/airplanes instead. + mkdir -p "$TARGET_ROOT/var/lib/airplanes" + rm -rf "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade" + chmod 0555 "$TARGET_ROOT/var/lib/airplanes" run airplanes_runtime_state_write "$TARGET_ROOT" STARTED rc=$status - chmod 0755 "$TARGET_ROOT/var/lib" + chmod 0755 "$TARGET_ROOT/var/lib/airplanes" [ "$rc" -ne 0 ] } diff --git a/test/runtime-overlay/test_self_update_state_machine.bats b/test/runtime-overlay/test_self_update_state_machine.bats index 5f0b6e0316..94780dd7f2 100644 --- a/test/runtime-overlay/test_self_update_state_machine.bats +++ b/test/runtime-overlay/test_self_update_state_machine.bats @@ -58,7 +58,7 @@ setup() { "managed_paths": [ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" } + "target": "/opt/airplanes/current/systemd/readsb.service" } ], "mutable_paths": [], "systemd": { "enable": ["readsb.service"], "daemon_reload": true }, @@ -76,8 +76,8 @@ JSON printf 'ok' > "$HTTPD_DOC/graphs1090/index.html" : > "$TARGET_ROOT/run/readsb/aircraft.json" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/dump978-fa/state" - printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes-978/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/dump978-fa/state" + printf 'state=enabled\nreason=ok\n' > "$TARGET_ROOT/run/airplanes/978/state" HTTPD_LOG="$BATS_TEST_TMPDIR/httpd.log" PORT_FILE="$BATS_TEST_TMPDIR/httpd.port" @@ -138,16 +138,16 @@ run_self_update() { fi [ "$(read_state "$TARGET_ROOT")" = "INSTALLED" ] - [ -d "$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER" ] - [ -L "$TARGET_ROOT/opt/airplanes-runtime/current" ] + [ -d "$TARGET_ROOT/opt/airplanes/releases/v$REL_VER" ] + [ -L "$TARGET_ROOT/opt/airplanes/current" ] [ -L "$TARGET_ROOT/etc/airplanes/runtime-manifest.json" ] - [ -L "$TARGET_ROOT/usr/bin/readsb" ] - [ -L "$TARGET_ROOT/usr/bin/airplanes-978" ] + [ -L "$TARGET_ROOT/usr/local/bin/readsb" ] + [ -L "$TARGET_ROOT/usr/local/bin/dump978-fa" ] # Snapshot of the recorded new_release survives in the state file # so a triage shell can inspect what was just installed. - grep -E "^new_release=$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER$" \ - "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" + grep -E "^new_release=$TARGET_ROOT/opt/airplanes/releases/v$REL_VER$" \ + "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" } @test "happy path triggers systemctl daemon-reload + enable + restart" { @@ -164,14 +164,14 @@ run_self_update() { @test "happy path clears stale terminal state before starting" { # Synthesise an INSTALLED leftover from a prior run. mk_state_file "$TARGET_ROOT" INSTALLED \ - "new_release=$TARGET_ROOT/opt/airplanes-runtime/releases/v0.0.0-old" + "new_release=$TARGET_ROOT/opt/airplanes/releases/v0.0.0-old" run run_self_update [ "$status" -eq 0 ] [ "$(read_state "$TARGET_ROOT")" = "INSTALLED" ] # new_release now reflects the fresh attempt, not the stale leftover. - grep -E "^new_release=$TARGET_ROOT/opt/airplanes-runtime/releases/v$REL_VER$" \ - "$TARGET_ROOT/var/lib/airplanes-runtime-upgrade/upgrade-state" + grep -E "^new_release=$TARGET_ROOT/opt/airplanes/releases/v$REL_VER$" \ + "$TARGET_ROOT/var/lib/airplanes/runtime-upgrade/upgrade-state" } @test "FAILED_PRE_MUTATION cleared on next attempt" { diff --git a/test/runtime-overlay/test_stage_feed_unit_symmetry.bats b/test/runtime-overlay/test_stage_feed_unit_symmetry.bats index 6a4b9d7976..d0f18ebe62 100644 --- a/test/runtime-overlay/test_stage_feed_unit_symmetry.bats +++ b/test/runtime-overlay/test_stage_feed_unit_symmetry.bats @@ -89,7 +89,7 @@ _unit_needs_enable() { local unit expected_link expected_target for unit in "${units[@]}"; do expected_link="/etc/systemd/system/$unit" - expected_target="/opt/airplanes-runtime/current/systemd/$unit" + expected_target="/opt/airplanes/current/systemd/$unit" if ! jq -e --arg link "$expected_link" --arg target "$expected_target" ' any(.[]; .mode == "symlink" and .link == $link and .target == $target) ' "$MANAGED_PATHS_JSON" >/dev/null; then @@ -100,7 +100,7 @@ _unit_needs_enable() { if (( ${#missing[@]} > 0 )); then printf 'managed_paths.json missing symlink for feed unit: %s\n' "${missing[@]}" >&2 echo "expected entry shape:" >&2 - echo ' { "mode": "symlink", "link": "/etc/systemd/system/", "target": "/opt/airplanes-runtime/current/systemd/" }' >&2 + echo ' { "mode": "symlink", "link": "/etc/systemd/system/", "target": "/opt/airplanes/current/systemd/" }' >&2 return 1 fi } diff --git a/test/runtime-overlay/test_stage_install_runtime_overlay.bats b/test/runtime-overlay/test_stage_install_runtime_overlay.bats index d9d4e453ab..cf4d9060d6 100644 --- a/test/runtime-overlay/test_stage_install_runtime_overlay.bats +++ b/test/runtime-overlay/test_stage_install_runtime_overlay.bats @@ -73,19 +73,13 @@ setup() { "managed_paths": [ { "mode": "symlink", "link": "/etc/systemd/system/readsb.service", - "target": "/opt/airplanes-runtime/current/systemd/readsb.service" }, + "target": "/opt/airplanes/current/systemd/readsb.service" }, { "mode": "symlink", "link": "/etc/systemd/system/airplanes-runtime-update-recover.service", - "target": "/opt/airplanes-runtime/current/systemd/airplanes-runtime-update-recover.service" }, - { "mode": "symlink", - "link": "/usr/local/lib/airplanes/render-status", - "target": "/opt/airplanes-runtime/current/lib/airplanes/render-status" }, + "target": "/opt/airplanes/current/systemd/airplanes-runtime-update-recover.service" }, { "mode": "symlink", "link": "/etc/lighttpd/conf-available/89-airplanes-978.conf", - "target": "/opt/airplanes-runtime/current/etc/lighttpd/conf-available/89-airplanes-978.conf" }, - { "mode": "symlink", - "link": "/usr/bin/dump978-fa", - "target": "/opt/airplanes-runtime/current/bin/dump978-fa" } + "target": "/opt/airplanes/current/etc/lighttpd/conf-available/89-airplanes-978.conf" } ], "mutable_paths": [], "systemd": { "enable": [], "daemon_reload": true }, @@ -155,28 +149,29 @@ teardown() { fi # Release tree exists. - [ -d "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER" ] - [ -f "$ROOTFS_DIR/opt/airplanes-runtime/releases/v$REL_VER/manifest.json" ] - [ -L "$ROOTFS_DIR/opt/airplanes-runtime/current" ] - [ "$(readlink "$ROOTFS_DIR/opt/airplanes-runtime/current")" = "/opt/airplanes-runtime/releases/v$REL_VER" ] + [ -d "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER" ] + [ -f "$ROOTFS_DIR/opt/airplanes/releases/v$REL_VER/manifest.json" ] + [ -L "$ROOTFS_DIR/opt/airplanes/current" ] + [ "$(readlink "$ROOTFS_DIR/opt/airplanes/current")" = "/opt/airplanes/releases/v$REL_VER" ] # managed_paths landed under ROOTFS_DIR with absolute on-device targets. [ -L "$ROOTFS_DIR/etc/systemd/system/readsb.service" ] - [ "$(readlink "$ROOTFS_DIR/etc/systemd/system/readsb.service")" = "/opt/airplanes-runtime/current/systemd/readsb.service" ] + [ "$(readlink "$ROOTFS_DIR/etc/systemd/system/readsb.service")" = "/opt/airplanes/current/systemd/readsb.service" ] [ -L "$ROOTFS_DIR/etc/systemd/system/airplanes-runtime-update-recover.service" ] - [ "$(readlink "$ROOTFS_DIR/etc/systemd/system/airplanes-runtime-update-recover.service")" = "/opt/airplanes-runtime/current/systemd/airplanes-runtime-update-recover.service" ] + [ "$(readlink "$ROOTFS_DIR/etc/systemd/system/airplanes-runtime-update-recover.service")" = "/opt/airplanes/current/systemd/airplanes-runtime-update-recover.service" ] - [ -L "$ROOTFS_DIR/usr/local/lib/airplanes/render-status" ] [ -L "$ROOTFS_DIR/etc/lighttpd/conf-available/89-airplanes-978.conf" ] - # Decoder binary symlinks created post-flip by install.sh. - [ -L "$ROOTFS_DIR/usr/bin/readsb" ] - [ "$(readlink "$ROOTFS_DIR/usr/bin/readsb")" = "/opt/airplanes-runtime/current/bin/readsb" ] - [ -L "$ROOTFS_DIR/usr/bin/airplanes-978" ] - [ "$(readlink "$ROOTFS_DIR/usr/bin/airplanes-978")" = "/opt/airplanes-runtime/current/bin/readsb" ] - [ -L "$ROOTFS_DIR/usr/bin/dump978-fa" ] - [ "$(readlink "$ROOTFS_DIR/usr/bin/dump978-fa")" = "/opt/airplanes-runtime/current/bin/dump978-fa" ] + # Decoder operator shims created post-flip by install.sh under /usr/local/bin + # (FHS-correct; /usr/bin is no longer squatted). + [ -L "$ROOTFS_DIR/usr/local/bin/readsb" ] + [ "$(readlink "$ROOTFS_DIR/usr/local/bin/readsb")" = "/opt/airplanes/current/bin/readsb" ] + [ -L "$ROOTFS_DIR/usr/local/bin/dump978-fa" ] + [ "$(readlink "$ROOTFS_DIR/usr/local/bin/dump978-fa")" = "/opt/airplanes/current/bin/dump978-fa" ] + # The old /usr/bin squats must not be created. + [ ! -e "$ROOTFS_DIR/usr/bin/readsb" ] + [ ! -e "$ROOTFS_DIR/usr/bin/airplanes-978" ] # Runtime-manifest pointer is a regular file copy in build mode so # host-side consumers (scripts/manifest-generator.sh in stage 07) can @@ -209,7 +204,7 @@ teardown() { @test "stage 02-install-runtime-overlay refuses when host pubkey is missing" { # Move the in-repo pubkey aside so the stage's host-path check fails. The # worktree is shared with the rest of the suite, so restore on exit. - local pub="$REPO_ROOT/stage-airplanes/00-prep/files/usr/share/airplanes/runtime-release.pub" + local pub="$REPO_ROOT/stage-airplanes/00-prep/files/opt/airplanes/libexec/runtime-release.pub" local bk="$BATS_TEST_TMPDIR/runtime-release.pub.bak" if [[ ! -f "$pub" ]]; then skip "pubkey not committed yet" diff --git a/test/runtime-overlay/test_symlink_cleanup.bats b/test/runtime-overlay/test_symlink_cleanup.bats index e53c265567..f64a647a86 100644 --- a/test/runtime-overlay/test_symlink_cleanup.bats +++ b/test/runtime-overlay/test_symlink_cleanup.bats @@ -22,7 +22,7 @@ mk_manifest_with_links() { local first=1 for link in "$@"; do [[ $first -eq 0 ]] && entries="$entries," - entries="$entries {\"mode\":\"symlink\",\"link\":\"$link\",\"target\":\"/opt/airplanes-runtime/current/bin/dummy\"}" + entries="$entries {\"mode\":\"symlink\",\"link\":\"$link\",\"target\":\"/opt/airplanes/current/bin/dummy\"}" first=0 done cat > "$dir/manifest.json" < stage-airplanes/01-install-feed/01-run-chroot.sh (service account + st # install it directly from the bind-mounted checkout below. ( cd /image/stage-airplanes/01-install-feed && bash 01-run-chroot.sh ) -echo "==> feed install.sh --build-mode (lays /usr/local/share/airplanes + update.sh)" +echo "==> feed install.sh --build-mode (lays /opt/airplanes/current/share/airplanes + update.sh)" # AIRPLANES_FEED_REPO=file:///feed (overlay-common) clones the bind-mounted # checkout; AIRPLANES_READSB_* come from config-dev. The lat/lon/altitude/MLAT # placeholders mirror what the deleted stage-01 chroot passed: build mode runs @@ -86,25 +86,25 @@ echo "==> stage-airplanes/06-firstboot/00-run.sh" # Catches `rm -rf $dir/*` regressions that leave dirs intact but wipe # contents — directory mode/owner alone wouldn't drift in that case. echo "==> seeding webconfig-owned sentinels" -printf 'regression-sentinel\n' > /var/lib/airplanes-webconfig/.update-regression-sentinel +printf 'regression-sentinel\n' > /var/lib/airplanes/webconfig/.update-regression-sentinel printf 'regression-sentinel\n' > /etc/airplanes/webconfig/.update-regression-sentinel chown airplanes-webconfig:airplanes-webconfig \ - /var/lib/airplanes-webconfig/.update-regression-sentinel \ + /var/lib/airplanes/webconfig/.update-regression-sentinel \ /etc/airplanes/webconfig/.update-regression-sentinel chmod 0600 \ - /var/lib/airplanes-webconfig/.update-regression-sentinel \ + /var/lib/airplanes/webconfig/.update-regression-sentinel \ /etc/airplanes/webconfig/.update-regression-sentinel # ---- Force the self-replace path of update.sh to fire -------------------- -# Stage 01 leaves $GIT/update.sh and /usr/local/share/airplanes/update.sh +# Stage 01 leaves $GIT/update.sh and /opt/airplanes/current/share/airplanes/update.sh # byte-identical, so update.sh's diff check at the top of main() would skip # the self-replace path. Overwriting the installed copy with a stub forces # the diff to differ; the runtime run will then exercise the install + mv -fT # self-replace and we fingerprint mode/sha post-update. echo "==> seeding stale installed updater" printf '#!/bin/bash\necho "stale stub" >&2\nexit 0\n' \ - > /usr/local/share/airplanes/update.sh -chmod 0755 /usr/local/share/airplanes/update.sh + > /opt/airplanes/current/share/airplanes/update.sh +chmod 0755 /opt/airplanes/current/share/airplanes/update.sh # ---- Capture pre-update fingerprint -------------------------------------- echo "==> capturing pre-update fingerprint" @@ -157,11 +157,11 @@ ln -sfn "$SYSTEMCTL_REGRESSION_SHIM" "$SYSTEMCTL_LINK" # (returns 0, ignores args). Avoids needing live cloud connectivity. # - AIRPLANES_FEED_REPO is already file:///feed via the common prelude. # - No --build-mode flag — this exercises the same path real feeders run -# when webconfig kicks `systemd-run /usr/local/share/airplanes/update.sh`. +# when webconfig kicks `systemd-run /opt/airplanes/current/share/airplanes/update.sh`. echo "==> running runtime-mode update.sh" set +e APL_FEED_BIN=/bin/true \ - /usr/local/share/airplanes/update.sh \ + /opt/airplanes/current/share/airplanes/update.sh \ > "$ARTIFACT_DIR/update.log" 2>&1 update_rc=$? set -e From 0b2a157929b9b48876294a12cc22f82ca41b606c Mon Sep 17 00:00:00 2001 From: darken Date: Thu, 25 Jun 2026 00:40:05 +0200 Subject: [PATCH 2/5] fix(gates): accept /opt/airplanes payload + libexec sudoers grants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the de-squat, web-config privileged helpers are granted by their absolute /opt/airplanes/current path (payload) and the orchestrator trampoline by its image-baked /opt/airplanes/libexec path, neither of which is a managed_paths symlink. Teach sudoers-managed-check to accept both — payload paths iff the file ships in the release tree, libexec paths as always-present image-baked helpers. --- .../scripts/gates/sudoers-managed-check.sh | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/runtime-overlay/scripts/gates/sudoers-managed-check.sh b/runtime-overlay/scripts/gates/sudoers-managed-check.sh index 5f255c0f95..e44aa67199 100755 --- a/runtime-overlay/scripts/gates/sudoers-managed-check.sh +++ b/runtime-overlay/scripts/gates/sudoers-managed-check.sh @@ -17,10 +17,14 @@ # etc/sudoers.d/* file, the command must be either # - a base-OS binary under /usr/bin, /bin, /usr/sbin, or /sbin # (always present on the Debian rootfs, never the overlay's job), or +# - under /opt/airplanes/libexec/ — an image-baked helper that ships at +# flash and is never touched by an overlay update (always present, like +# a base-OS binary), or +# - under /opt/airplanes/current/ — reached directly through the active +# release payload; provided iff the file ships in this release tree, or # - provided by a managed_paths entry (an exact .link/.path, or under a # managed directory entry). -# Anything under /usr/local (or anywhere else) that no managed_paths entry -# provides fails the build. +# Anything else that no managed_paths entry provides fails the build. # # Args: # --release-dir the v release tree (manifest.json + staged @@ -63,9 +67,20 @@ mapfile -t MANAGED < <(jq -r '.managed_paths[]? | (.link // .path) // empty' "$m # is_provided — true when the path is provided after an update: # a base-OS bin, an exact managed entry, or under a managed directory entry. is_provided() { - local p="$1" m + local p="$1" m rel case "$p" in /usr/bin/*|/bin/*|/usr/sbin/*|/sbin/*) return 0 ;; + # Image-baked helpers under /opt/airplanes/libexec ship at flash and + # survive every overlay update untouched (the recover-shim, the + # orchestrator trampoline). Treat them like base-OS binaries. + /opt/airplanes/libexec/*) return 0 ;; + # Anything under the active-release payload is provided iff the file + # actually ships in this release tree. `current` resolves to this tree + # on-device, so the grant follows the atomic flip. + /opt/airplanes/current/*) + rel="${p#/opt/airplanes/current/}" + [[ -e "$RELEASE_DIR/$rel" ]] && return 0 + ;; esac for m in "${MANAGED[@]}"; do [[ "$p" == "$m" || "$p" == "$m/"* ]] && return 0 From af45284522abdfff628de8f311b28ade981075d0 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 30 Jun 2026 15:12:44 +0200 Subject: [PATCH 3/5] fix: order the overlay recovery shim before first-run and dashboard first-run sources the overlay's wifi-validators and the console dashboard renders via the overlay's render-status, so both must wait for the recovery shim's last-good flip; otherwise a half-flipped current/ breaks them on a recovery boot. --- .../systemd/system/airplanes-runtime-update-recover.service | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service b/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service index a538c469a4..f10548374f 100644 --- a/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service +++ b/stage-airplanes/02-install-runtime-overlay/files/etc/systemd/system/airplanes-runtime-update-recover.service @@ -13,6 +13,10 @@ Before=airplanes-tar1090-uat-sync.service airplanes-tar1090-uat-sync.path Before=tar1090.service graphs1090.service Before=collectd.service lighttpd.service Before=airplanes-webconfig.service airplanes-feed.service airplanes-mlat.service +# first-run sources the overlay's wifi-validators.sh and the console dashboard +# renders via the overlay's render-status, so both must wait for the last-good +# flip too — otherwise a half-flipped current/ breaks them on a recovery boot. +Before=airplanes-first-run.service airplanes-dashboard.service RequiresMountsFor=/opt /var/lib/airplanes/runtime-upgrade ConditionPathExists=/opt/airplanes/libexec/recover-shim From af548239b8f85033ebf2e5bc2231057c10a69848 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 30 Jun 2026 19:23:42 +0200 Subject: [PATCH 4/5] fix: stage every feed daemon wrapper into the runtime overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay mirrors every airplanes-*.{service,timer} unit, but staged wrapper scripts from a hardcoded list missing airplanes-stats.sh — so airplanes-stats.service referenced a script the overlay never shipped. Glob airplanes-*.sh to match the unit glob. --- runtime-overlay/scripts/lib/stage-feed.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/runtime-overlay/scripts/lib/stage-feed.sh b/runtime-overlay/scripts/lib/stage-feed.sh index abb5b79da5..2473b1bc87 100644 --- a/runtime-overlay/scripts/lib/stage-feed.sh +++ b/runtime-overlay/scripts/lib/stage-feed.sh @@ -168,12 +168,15 @@ echo "stage-feed: cloning feed scripts from $FEED_REPO @ $FEED_REF" fetch_repo "$FEED_SRC" "$FEED_REPO" "$FEED_REF" FEED_SHA="$(git -C "$FEED_SRC" rev-parse HEAD)" -# Daemon wrappers → share/airplanes/ +# Daemon wrappers → share/airplanes/. Glob airplanes-*.sh so a wrapper added +# in feed is staged automatically, mirroring the airplanes-*.{service,timer} +# unit glob below. The prior hardcoded list silently dropped airplanes-stats.sh +# when feed added it — its airplanes-stats.service then referenced a script the +# overlay never shipped, which the exec-bit-check release gate rejects. install -d -m 0755 "$OUTPUT_DIR/share/airplanes" -for wrapper in airplanes-feed.sh airplanes-mlat.sh airplanes-diagnostics.sh; do - if [[ -f "$FEED_SRC/scripts/$wrapper" ]]; then - install -m 0755 "$FEED_SRC/scripts/$wrapper" "$OUTPUT_DIR/share/airplanes/$wrapper" - fi +for wrapper in "$FEED_SRC"/scripts/airplanes-*.sh; do + [[ -f "$wrapper" ]] || continue + install -m 0755 "$wrapper" "$OUTPUT_DIR/share/airplanes/$(basename "$wrapper")" done # apl-feed CLI entry point → bin/ From bd6d5ab70beec2aaa5ebd866dcdc277d715ba044 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 30 Jun 2026 19:23:42 +0200 Subject: [PATCH 5/5] test: stage render-status assets in the overlay smoke The overlay smoke skips the runtime-overlay stage but asserts render-status and the console-dashboard assets it now delivers. Stage those source files so the renderer self-test keeps running at PR time. --- test/overlay-smoke-inner.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/overlay-smoke-inner.sh b/test/overlay-smoke-inner.sh index 7245143084..c22b2bd3fa 100755 --- a/test/overlay-smoke-inner.sh +++ b/test/overlay-smoke-inner.sh @@ -7,6 +7,26 @@ set -euo pipefail # shellcheck source=lib/overlay-common.sh . /image/test/lib/overlay-common.sh +# Emulate the runtime overlay (stage 02) delivery of render-status, the +# console-dashboard ASCII assets, and the MOTD status hook. This fast smoke +# deliberately skips stage 02 — it needs signed release downloads — but the +# 06b block below runs render-status' renderer self-test and asserts these +# artifacts, so we stage them from runtime-overlay/src/, the very tree the +# overlay is built from. The MOTD hook goes straight to /etc/update-motd.d/ as +# a real 0755 file (the 06b assertion stat's its mode, which a managed_paths +# symlink would report as 777). Delivery fidelity — real overlay paths, modes, +# and managed_paths symlink wiring — is covered by the release gate +# (exec-bit-check) and the full boot-smoke; here we only need the renderer's +# inputs in place. +install -D -m 0755 /image/runtime-overlay/src/lib/airplanes/render-status \ + /opt/airplanes/current/lib/airplanes/render-status +for _asset in logo banner banner-narrow icon; do + install -D -m 0644 "/image/runtime-overlay/src/share/airplanes/${_asset}.txt" \ + "/opt/airplanes/current/share/airplanes/${_asset}.txt" +done +install -D -m 0755 /image/runtime-overlay/src/etc/update-motd.d/10-airplanes-status \ + /etc/update-motd.d/10-airplanes-status + echo "==> stage-airplanes/00-prep/00-run.sh" # Run from the stage dir so relative `files/` paths inside 00-run.sh resolve. ( cd /image/stage-airplanes/00-prep && bash 00-run.sh )