Skip to content

Add OpenPlotter to HaLOS migration scripts and runbook#30

Merged
mairas merged 16 commits into
mainfrom
feat/openplotter-migration-scripts
Jun 12, 2026
Merged

Add OpenPlotter to HaLOS migration scripts and runbook#30
mairas merged 16 commits into
mainfrom
feat/openplotter-migration-scripts

Conversation

@mairas

@mairas mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

Boats running OpenPlotter need a path to migrate their marine data onto HaLOS during an in-place reflash. This adds the tooling and a user-facing runbook for that one-way migration.

What's included

  • openplotter-backup.sh — runs on the live OpenPlotter, copies four data domains (Signal K, InfluxDB 2, Grafana, OpenCPN) to a USB stick as per-domain tar archives, verifies them, and only then prints SAFE TO REFLASH.
  • halos-restore.sh — runs on the freshly-flashed HaLOS device, reinstates all four domains, reconciles the InfluxDB bucket to HaLOS's pre-provisioned marine datasource, and preserves HaLOS defaults under *.halos-default for rollback.
  • docs/user-guide/openplotter-migration.md — runbook covering USB sizing, the backup safe-to-reflash gate, restore, and post-migration notes. Served alongside the scripts at /migrate/*.sh.

Verification against a live OpenPlotter

Checked every source-path and service-name assumption against a real OpenPlotter (openplotter-dashboards 4.1) and the actual upstream .deb packages it installs. Two corrections came out of it:

  • InfluxDB data path. OpenPlotter installs the official influxdb2 deb, which stores data at /var/lib/influxdb (bolt-path = /var/lib/influxdb/influxd.bolt), not /var/lib/influxdb2. The backup guarded on the wrong directory and would have silently skipped all historical telemetry. (The restore script's /var/lib/influxdb2 is the HaLOS influx Docker image's internal path — correct, and left as-is.)
  • Signal K socket activation. signalk.socket listens on port 3000, so stopping signalk.service alone lets the next client connection restart the server mid-copy. The backup now stops the socket first.

Confirmed correct without changes: Signal K (signalk.service, ~/.signalk), Grafana (grafana-server.service, /var/lib/grafana/grafana.db), and OpenCPN (~/.opencpn, ~/.local/share/opencpn).

Restore was previously verified end-to-end on live HaLOS hardware (halosdev). The InfluxDB backup path fix rests on the deb's authoritative config rather than a live backup run, since a fresh OpenPlotter has no Dashboards stack installed.

Closes #25, #26, #27, #28, #29.

🤖 Generated with Claude Code

Fly-by fix: docs/user-guide/troubleshooting.md told users to check container issues with docker logs; switched to journalctl -u <service-name>.service to match how container apps actually log (journald driver, compose in foreground) and the convention in installing-apps.md.

@mairas mairas force-pushed the feat/openplotter-migration-scripts branch 3 times, most recently from 2e44065 to 950f56f Compare June 11, 2026 12:39
mairas added 4 commits June 11, 2026 15:45
Collects Signal K, InfluxDB 2, Grafana and OpenCPN data from a live
OpenPlotter system to a USB stick as tar archives (ownership preserved
across FAT/exFAT), with a manifest and a completeness gate that prints
SAFE TO REFLASH only after verification. Part of #25.
Restore-side skeleton: USB/backup detection, manifest format-version and
completeness validation, marine-HaLOS guard, and Signal K + OpenCPN
restore with HaLOS defaults preserved aside for rollback. InfluxDB and
Grafana restore follow. Part of #26.
Raw InfluxDB data-dir swap with offline operator-token minting from the
boltdb, org/bucket rename to marine, v1 DBRP, and a smoke query. Grafana
boat datasources/dashboards re-imported via the API with the minted
token, degrading gracefully on newer Grafana schemas.

Validated end-to-end on a live HaLOS device: data queryable via Flux and
InfluxQL, token survives service restart, ownership pi:root 700, Grafana
admin:admin import works. Part of #27.
User-facing runbook for the in-place OpenPlotter to HaLOS migration:
USB sizing, fetch-then-run backup with the safe-to-reflash gate, link to
existing flashing docs, restore, and post-migration notes (Signal K
bucket reconciliation, best-effort dashboards, OpenCPN charts, rollback).
Adds the nav entry; scripts serve verbatim at /migrate/*.sh.
Closes #28, #29.
@mairas mairas force-pushed the feat/openplotter-migration-scripts branch from 950f56f to 1d09e99 Compare June 11, 2026 12:45
@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Code Review Results

Scope: merge-base 43465cf (origin/main) → working tree (4 files, +1138 lines)
Intent: One-way OpenPlotter→HaLOS in-place migration: backup script with a completeness gate (USB holds the only copy during the irreversible wipe), restore script with *.halos-default rollback and InfluxDB/Grafana reconciliation, plus a runbook served with the scripts at /migrate/*.sh.
Mode: interactive

Reviewers: correctness, testing, maintainability, project-standards, security, reliability, adversarial

  • security — scripts run with sudo via fetch-then-run, mint an all-access InfluxDB token, copy credential material to a USB stick
  • reliability — service stop/start ordering, verification gates, rollback paths during an irreversible migration
  • adversarial — 1138 added lines in a data-mutation domain where the USB stick is the only copy during the wipe

P1 — High

# File Issue Reviewer Confidence Route
1 openplotter-backup.sh:388 Verification asserts only influxd.bolt appears in the tar listing. Time-series data lives under engine/ (TSM/WAL); a backup with empty or truncated engine/ passes the gate and prints the COMPLETE banner. The restore-side smoke query downgrades zero rows to a warning, so the user can reflash on a green banner and lose all history. Suggested: also assert an engine/ entry exists in the listing and that the archive size is within tolerance of the measured INFLUX_BYTES. adversarial 0.72 manual -> downstream-resolver
2 halos-restore.sh:280 InfluxDB org/user/bucket names containing spaces are truncated by awk '{print $2}' column parsing (recovery_list, and select_telemetry_bucket:386). A boat org like "S/V Serenity" yields "S/V"; influxd recovery auth create-operator --org then fails and die fires after the data swap, stranding the user mid-restore. Parse the name column to end-of-line or match by ID throughout. (correctness rated P2, adversarial P1 — keeping P1 since the abort lands post-swap.) correctness, adversarial 0.80 manual -> downstream-resolver

P2 — Moderate

# File Issue Reviewer Confidence Route
3 halos-restore.sh:155,347 Re-run-after-partial-failure semantics are undefined. preserve_default skips when *.halos-default exists (correctly protecting the rollback copy), but nothing detects or documents the retry path, and align_to_marine renames org/bucket inside the restored copy in place. Reviewers disagreed: reliability rated this P1 (no pre-rename copy preserved); adversarial traced a re-run as largely convergent because every domain re-extracts from the USB archives. Synthesis: P2 — the USB stick still holds the pre-rename data, but the script needs either an explicit already-ran detection or documented retry guidance. reliability 0.70 manual -> downstream-resolver
4 halos-restore.sh (no trap; e.g. 175–182) No EXIT trap, unlike the backup script. A failure between a service stop and restart (e.g. Signal K tar extraction dies under set -e) leaves the service down and the live dir empty/partial, with recovery resting entirely on the runbook's *.halos-default note. Mirror the backup script's cleanup trap: restart stopped services and print the rollback path on non-zero exit. reliability, adversarial 0.84 manual -> downstream-resolver
5 openplotter-backup.sh:399 sync runs only after verification and the complete=yes marker. verify_backup's read-backs (tar -tf, [[ -s ]]) can be served from page cache, so power loss or stick removal between verification and sync can leave a stick whose on-media state never matched the banner. Add a sync before the read-back verification as well. reliability 0.66 gated_auto -> downstream-resolver
6 halos-restore.sh:469 Grafana import hardcodes Basic admin:admin. Live-hardware verification shows this works on current HaLOS, but a 401 is counted like any other failure and surfaces as the generic "import did not fully succeed" warning — indistinguishable from version skew. Detect 401 and emit a credential-specific message; document the coupling to HaLOS's default Grafana credentials. correctness, reliability, adversarial 0.70 manual -> downstream-resolver
7 halos-restore.sh:270 Operator-token extraction regex [A-Za-z0-9_-]{80,}== hard-requires double-equals padding. Current tokens end in ==, but any padding change yields an empty token and a die after the data swap. Relax to ={0,2} or parse the Token column positionally. correctness, adversarial 0.70 gated_auto -> downstream-resolver
8 halos-restore.sh:267 Recovery commands hardcode docker run ... influxdb:2.9.1 independent of the service image. Adversarial rated P1 (image absent on an offline boat), but the plan context shows the marine app ships exactly influxdb:2.9.1 and init_then_stop_influx starts the service first, so the image is present today — downgraded. The real issue is silent drift when HaLOS bumps the influx image: derive the tag from the installed container app, or check image presence and fail early with a clear message. adversarial 0.62 manual -> downstream-resolver
9 openplotter-backup.sh:309 The stick aggregates live credentials — grafana.db (datasource secrets, admin hash) is copied with explicit chmod a+r, and signalk.tar carries security.json password hashes — yet the banner/runbook only say "keep it somewhere safe". Drop the a+r (use 0600 owned by the real user) and state in the runbook that the backup contains credentials. security 0.70 manual -> downstream-resolver
10 halos-restore.sh:303 mint_operator_token returns user|org|token as one pipe-delimited string; the caller's parameter-expansion slicing mis-parses if the org name contains a pipe character. Org names are arbitrary boat data. Return the values on separate lines and read them. maintainability 0.66 manual -> downstream-resolver
11 halos-restore.sh:436 The token re-read (grep '^INFLUXDB_ADMIN_TOKEN=' | cut) is never asserted non-empty before the Grafana import. An empty token still produces Token headers and 2xx datasource creates, so the import reports success while every imported dashboard returns no data. Assert non-empty and hard-skip with a warning otherwise. reliability 0.63 gated_auto -> downstream-resolver
12 .github/workflows/deploy.yml No shellcheck/bash -n gate for 1000+ lines of safety-critical bash served verbatim to users; the only CI is mkdocs build --strict, which never parses shell. Testing rated P1; downgraded since the current scripts were hand-linted — the gate protects future edits. One CI step covers it. testing 0.82 manual -> downstream-resolver
13 .github/workflows/deploy.yml The plan's acceptance criterion "built site/migrate/*.sh is byte-identical to source" is unchecked. A future minifier plugin or exclude rule would silently break the documented curl URLs while --strict still passes. Add a cmp step after the build. testing 0.70 manual -> downstream-resolver
14 openplotter-backup.sh:381 verify_backup conditions per-domain checks on live source dirs rather than the manifest's present entries, so the manifest (the restore contract) and verification can disagree if a source dir disappears mid-run. Narrow window; key verification off the manifest. reliability 0.60 advisory -> human
15 halos-restore.sh:497 url was SELECTed from the boat data_source table but discarded in favor of the hardcoded HaLOS internal address — a misleading read inviting a future "fix" that would break the migration. maintainability 0.68 safe_auto -> review-fixer (applied)

P3 — Low

# File Issue Reviewer Confidence Route
16 halos-restore.sh:283 The all-access operator token is passed as --token argv through docker exec, visible via ps//proc during each call. Pass it via docker exec -e INFLUX_TOKEN instead, matching how the Grafana import already passes it through env. security 0.62 manual -> downstream-resolver
17 openplotter-backup.sh:254 Cleanup restarts services in stop order (socket before service), inverting the rationale that justified the stop ordering. systemd reconciles, so impact is nil today; a latent footgun if post-start verification is ever added. reliability 0.62 advisory -> human
18 openplotter-backup.sh:202 The 4 GiB FAT pre-check matches vfat/msdos only; FUSE-mounted exFAT/NTFS report fuseblk in df -T and skip the guard. The failure stays loud (tar dies, cleanup restarts services), but the user sees a raw tar error instead of "reformat the stick". reliability 0.60 advisory -> human
19 halos-restore.sh:411 (($(printf '%s' "${out:-0}") > 0)) reparses an already-numeric grep -c result through printf for no reason. maintainability 0.60 safe_auto -> review-fixer (applied)

Requirements Completeness

Plan: issue #24 (via closes #28, #29 — explicit).

  • R1 (back up SK/InfluxDB2/Grafana/OpenCPN to USB) — met
  • R2 (restore into fresh marine HaLOS) — met
  • R3 (verified completeness + explicit safe-to-reflash gate) — partially met: the gate exists, but finding chore: migrate org references from hatlabs to halos-org #1 shows it does not verify the telemetry data itself
  • R4 (base tooling only on OpenPlotter) — met (coreutils, tar, systemctl, df)
  • R5 (align to HaLOS marine defaults) — met (token mint, org/bucket rename, DBRP, documented SK plugin step)
  • R6 (fetch-then-run URLs + runbook) — met; mkdocs build --strict passes and site/migrate/*.sh are verbatim copies (verified during review)
  • R7 (CLI only) — met

Note: the PR implements units 1–3 (#25, #26, #27) but only closes #28/#29. Add Closes #25, #26, #27 to the PR body so the unit issues don't stay open after merge.

Applied Fixes

Residual Actionable Work

# File Issue Route Next Step
1 openplotter-backup.sh:388 Verification doesn't check engine/ telemetry data manual -> downstream-resolver Fix before merge — this is the safety gate the whole design rests on
2 halos-restore.sh:280 Whitespace in org/user/bucket names breaks awk parsing post-swap manual -> downstream-resolver Fix before merge
5, 7, 11 backup/restore sync-before-verify, token regex padding, empty-token assert gated_auto -> downstream-resolver Concrete fixes exist; need approval since they change verification/acceptance behavior
3, 4, 6, 8, 9, 10, 12, 13, 16 various See findings table manual -> downstream-resolver Fix in PR or file follow-up issues

Coverage

  • Suppressed: 0 at synthesis (reviewers self-suppressed below 0.60); no malformed findings dropped
  • Residual risks: fetch-then-run with sudo has no integrity check beyond HTTPS (documented delivery model); recovery_list silently picks the first org on a multi-org boat (the disambiguation prompt exists only for buckets); the smoke query's -520w window can mis-warn on >10-year data or pre-NTP clock skew; chown -R + non-recursive chmod ownership mirroring on the swapped InfluxDB dir relies on tar mode preservation
  • Testing gaps: interrupted-backup trap path, restore re-run, whitespace names, non-default Grafana credentials — all hardware-only today, no repeatable harness; proportionate floor is the CI lint gate (finding Document sudo password configuration #12)
  • Failed reviewers: none (7/7 returned; project-standards found no violations — heading style, hostname policy, strict build, and verbatim serving all check out)

Verdict: Ready with fixes

Reasoning: The scripts are carefully built and consistently fail safe — every traced failure path preserves data and exits loudly. But the two P1s undermine the design's core invariant: the completeness gate doesn't verify the telemetry data itself (#1), and arbitrary boat org/bucket names can abort the restore after the point of no return (#2). Both should land before this is announced to users. R3 is only partially met until #1 is fixed.

Fix order: #1 engine/ verification → #2 whitespace-safe name parsing → gated items (#5, #7, #11) → remaining P2s in-PR or as follow-up issues.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up to the review above: the three gated_auto findings were approved and applied in 344ce50 (the two safe_auto items landed earlier in 7ea4f87):

Verified with bash -n and shellcheck on both scripts (clean). The behavior-level effect of #5 and #11 (requires_verification) still needs a live-hardware pass; the next migration test run against openplotter.local covers it.

Remaining open findings: P1 #1 (engine/ verification) and #2 (whitespace-safe name parsing) plus the manual P2/P3 items — see the fix order in the review verdict.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Fix plan for the remaining review findings (all being addressed in this PR; none filed as follow-up issues):

Finding Approach
#1 engine/ not verified verify_backup keeps the tar listing once and additionally requires an ./engine/ entry (when the live source has one) and an archive size ≥ half the measured source size
#2 whitespace in names recovery_list parses the name as rest-of-line instead of awk column 2; align_to_marine's org-id lookup and select_telemetry_bucket switch to influx ... --json parsed with host python3 (tab-separated id/name), so arbitrary names survive
#3 re-run semantics Runbook troubleshooting note: re-running restore is safe (re-extracts from the stick; first run's *.halos-default remains the rollback copy)
#4 no EXIT trap in restore Trap restarts the service whose stop→start window was interrupted and prints the *.halos-default rollback pointer on any non-zero exit
#6 401 indistinguishable Grafana import counts 401/403 separately and prints a credential-specific note
#8 hardcoded influxdb:2.9.1 Recovery image is taken from the installed influxdb container when present (docker inspect .Config.Image), falling back to the literal; an explicit image-presence check with pull attempt fails early with a clear offline message
#9 credentials on the stick grafana.db lands owner-only (0600, real user) instead of a+r; completion banner and runbook "keep the stick safe" tip now state the backup contains credentials
#10 pipe-delimited triple mint_operator_token returns user/org/token on three lines, read line-wise
#12/#13 no CI gate New ci.yml on pull_request + push-to-main: bash -n + shellcheck on docs/migrate/*.sh, mkdocs build --strict, and cmp of built site/migrate/*.sh against source (the repo currently has no PR CI at all — deploy.yml only runs on main)
#14 verify vs manifest drift OpenCPN sizes measured per directory up front; backup and verification key off those measurements rather than re-probing live dirs
#17 restart order start_services replays STOPPED_SERVICES in reverse (service before socket)
#18 fuseblk bypasses FAT check When the filesystem reports fuseblk/unknown and the InfluxDB data exceeds 4 GiB, warn that FAT32 would fail — not a hard stop, since fuseblk is usually NTFS/exFAT which have no such limit

Rejected — #16 (operator token in argv): the suggested docker exec -e INFLUX_TOKEN=value form puts the token straight back into host argv, and the client-env form (-e INFLUX_TOKEN bare) is stripped by sudo's env_reset unless sudoers grants SETENV. Both variants trade a P3 exposure on a single-user system for platform-dependent breakage in hardware-verified code. Leaving as-is.

Housekeeping: PR body gains Closes references for #25/#26/#27, which this PR implements.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Fix-completion summary for the review / fix plan. All planned findings are resolved in commits 8967e7c, 93a7288, d9bba06, beebb23:

openplotter-backup.sh (8967e7c)

halos-restore.sh (93a7288)

  • Update logo with glow effects and larger header size #2recovery_list parses the name as rest-of-line; align_to_marine's org-id lookup and select_telemetry_bucket use influx … --json parsed by host python3 with tab-separated id/name, so org/bucket names with spaces survive end to end.
  • docs: add CM5 eMMC boot issue workaround #4 — new on_exit EXIT trap: restarts the service whose stop→swap→start window was interrupted (PENDING_RESTART set around the Signal K and InfluxDB swaps) and prints the *.halos-default rollback pointer on any failure.
  • Clarify container app two-phase install process #6 — the Grafana import counts 401/403 separately and prints a credential-specific note when the default admin login is rejected.
  • Update documentation for port-based routing #8 — the recovery image is taken from the installed influxdb container (docker inspect .Config.Image) with the influxdb:2.9.1 literal as fallback, plus an explicit presence check that pulls or fails early with a clear offline message.
  • Add GPS documentation page #10mint_operator_token returns user/org/token on three lines, read line-wise; the pipe-delimited packing is gone.

Runbook (d9bba06) — #3: troubleshooting entry stating a partial restore is safe to re-run (re-extracts from the stick; first run's *.halos-default remains the rollback copy); #9: "keep the stick safe" tip now mentions it contains credentials.

CI (beebb23) — #12/#13: new ci.yml on pull_request and push-to-main runs bash -n + shellcheck on docs/migrate/*.sh, mkdocs build --strict, and cmps the built site/migrate/*.sh against source.

Verification: bash -n and shellcheck clean on both scripts; embedded Python passes py_compile; the new awk/JSON parsing was exercised with spaced names, system-bucket filtering, and empty input; mkdocs build --strict passes locally and the built scripts are byte-identical to source. The runtime behavior of the trap, image detection, and name handling still needs the planned end-to-end run against live hardware.

Not fixed (rejected): #16 (operator token in argv) — docker exec -e VAR=value puts the token back into host argv, and the client-env form is stripped by sudo env_reset without SETENV. Recorded in the fix plan; no code change.

Housekeeping: PR body now closes #25#29.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

d38c8a5 removes the incorrect InfluxDB version-skew explanation from the unhealthy-on-restore messaging (runbook troubleshooting bullet and the restore script's health-check error). OpenPlotter on Bookworm installs influxdb2 2.9.1, never newer than what HaLOS runs, so that cause cannot occur; the messages now state only the observable failure and the db.halos-default recovery path.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

1d1952d switches the unhealthy-InfluxDB log instruction from sudo docker logs influxdb to sudo journalctl -u marine-influxdb-container.service -e (runbook + restore script). All container apps log via the journald driver (validator-enforced) and the unit runs docker compose up in the foreground, so the journal carries the container output — and it survives the docker rm -f the restore itself performs, where docker logs history would not. Also matches the log-viewing convention in installing-apps.md.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

0b0feb5: the data-locations table's Grafana row now gives the actual HaLOS path (/var/lib/container-apps/marine-grafana-container/data/data/grafana.db, from the compose volume ${CONTAINER_DATA_ROOT}/data:/var/lib/grafana) instead of just naming the container, keeping the re-imported-not-copied caveat.

@mairas

mairas commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

0a533c7: removed the "InfluxDB path differs by install method" bullet from the data-locations notes — the table already gives the correct host paths on both sides, and the /var/lib/influxdb2 container-internal path it referenced is never visible to anyone migrating by hand.

The data locations table is a reference for manual migration; how the
script transfers Grafana data is irrelevant there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mairas mairas merged commit eb1c590 into main Jun 12, 2026
1 check passed
@mairas mairas deleted the feat/openplotter-migration-scripts branch June 12, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unit 1: openplotter-backup.sh — OpenPlotter-side collector

1 participant