Skip to content

feat: OpenPlotter → Halos in-place migration scripts + runbook #24

Description

@mairas

Overview

Two CLI bash scripts plus a user-facing runbook that let an OpenPlotter (standard Raspberry Pi OS) marine user migrate their data to a freshly-flashed marine Halos install on the same device, in place, using an external USB stick as transport. openplotter-backup.sh runs on the live OpenPlotter system and copies Signal K, InfluxDB 2, Grafana, and OpenCPN data to the stick; the user reflashes to Halos (existing docs); halos-restore.sh runs on the fresh Halos system and reinstates the data, aligning it to Halos' marine defaults. Generalized from a real one-off migration.

Problem Frame

OpenPlotter users adopting Halos lose years of marine telemetry, Signal K configuration, Grafana dashboards, and OpenCPN setup unless they can carry it across. The original migration was done by hand across three hosts with 200+ ad-hoc commands — too error-prone and host-specific for a customer. This packages the validated procedure into two scripts safe enough for a non-expert, plus a runbook. Defining constraint: the reflash is in place and irreversible, so during the wipe window the USB stick holds the only copy of the data — backup must prove completeness before the user reflashes.

Requirements Trace

  • R1. Back up Signal K, InfluxDB 2, Grafana, and OpenCPN data from a live OpenPlotter system to a USB stick.
  • R2. Restore that data into a fresh marine Halos install so historical telemetry, dashboards, SK config, and OpenCPN setup are preserved and queryable.
  • R3. Backup must verify completeness and present an explicit "safe to reflash" gate before the user wipes.
  • R4. Backup must run on foreign territory with only base tooling (coreutils, tar, systemctl) — no docker, sqlite3, or influx CLI assumed on OpenPlotter.
  • R5. Restore must align boat data to Halos marine defaults so Halos' own provisioned Grafana datasource works against the historical data, and ongoing Signal K logging continues into the right bucket.
  • R6. Scripts delivered by fetch-then-run from stable docs.halos.fi/migrate/ URLs; runbook published in the docs site beside the existing migration guide.
  • R7. CLI only. No packaging, no new repo, no GUI.

Scope Boundaries

  • No HALPI2 / Halos flashing or reflashing instructions — link existing docs.
  • No Debian package, no new repo, no Cockpit/web/GUI.
  • No automated migration of ~/docker app data or /etc system config (boat-specific; out of scope).
  • No cross-device migration; single-device, in-place only.
  • Not swapping the Grafana data directory — dashboards/datasources are re-imported via the Grafana API into Halos' own Grafana (OIDC/provisioning intact).
  • Charts in user-relocated OpenCPN folders are best-effort: detected and warned, not guaranteed.

Context & Research

Target: marine Halos (halos-marine-containers)

  • InfluxDBapps/influxdb/: service marine-influxdb-container.service, container influxdb, image influxdb:2.9.1. Raw data dir /var/lib/container-apps/marine-influxdb-container/data/db → container /var/lib/influxdb2. Env /etc/container-apps/marine-influxdb-container/env, token var INFLUXDB_ADMIN_TOKEN. Defaults: org marine, bucket marine, admin pw halos-default, token placeholder halos-default-token-change-in-production. prestart regenerates the token only if placeholder/empty; also runs a password-sync step (verify it doesn't disturb an injected token).
  • Grafanaapps/grafana/: service marine-grafana-container.service, container grafana, image grafana/grafana:13.0.1, port 3000 on halos-proxy-network. Provisioned datasource: name InfluxDB, db marine, token via $__env{INFLUXDB_TOKEN}, isDefault: true, auto UID. Auth is OIDC/Authelia; no admin password set → built-in admin:admin is what the original run used against the internal IP (verify).
  • Signal Kapps/signalk-server/: service marine-signalk-server-container.service, container signalk-server, image signalk-server:v2.27.0. Data dir /var/lib/container-apps/marine-signalk-server-container/data/data/home/node/.signalk. prestart creates security.json only if missing, and auto-configures signalk-to-influxdb2 for org/bucket marine.

Source: OpenPlotter / standard Raspberry Pi OS

  • Signal K: ~/.signalk (exclude node_modules, skserver-raw_*.log); service likely signalk.service (confirm).
  • InfluxDB 2: /var/lib/influxdb2, service influxdb.service.
  • Grafana: /var/lib/grafana/grafana.db, service grafana-server.service.
  • OpenCPN: config ~/.opencpn, plugin data ~/.local/share/opencpn; charts in user-defined dirs (relocate risk). Desktop app, no service — must be closed during backup.

Docs site

MkDocs Material, site_url: https://docs.halos.fi, GitHub Pages via .github/workflows/deploy.yml (mkdocs build --strict). Existing guide docs/user-guide/app-data.md. Scripts serve at docs.halos.fi/migrate/... (not halos.fi/..., not served by this repo).

Key Technical Decisions

  • Raw InfluxDB data-dir swap, not portable backup/restore — portable path was tried and abandoned; raw swap is proven.
  • Mint the operator token at restore time from the boat's influxd.bolt via influxd recovery auth create-operator (through docker run --rm influxdb:2.9.1). No source credentials needed.
  • Rename the boat's data-bearing org + bucket to marine. Source names are arbitrary and boat-specific (OpenPlotter imposes no default; hurma/skdata were just this boat's). Discover by id, identify the telemetry bucket by data presence, and prompt the user when more than one candidate exists — never hardcode. The raw swap carries all orgs/buckets verbatim; the rename only retargets the primary one so Halos' default datasource and the SK influx writer line up. Additional buckets remain present under original names.
  • Add v1 DBRP marine/autogen (default) for InfluxQL/Grafana-v1; the boat's legacy skdata-db/skdata-rp DBRPs ride along in the raw data and back imported boat dashboards via a recreated boat datasource with preserved UID.
  • Grafana dashboards/datasources re-imported via API, Grafana data dir untouched. Restore reads the boat grafana.db with python3 stdlib sqlite3 (no host sqlite3 dependency).
  • Backup stops source services for a consistent raw copy, then restarts them.
  • USB is a genuine cross-filesystem copy both ways — no same-filesystem mv shortcut.
  • Manifest with format_version written by backup, validated by restore; refuses on mismatch; records per-domain presence/sizes for the completeness gate.
  • Halos defaults preserved aside before any swap: db.halos-default, env.halos-default, data.halos-default (recovery = rename back).

Open Questions

Resolved during planning

  • Scripts URL → docs.halos.fi/migrate/*.sh.
  • Renaming to marine integrates with Halos defaults (org+bucket default is marine).
  • Read grafana.db on Halos without extra deps → python3 stdlib sqlite3.
  • Same-filesystem move optimization → not applicable across USB.
  • Additional (non-telemetry) buckets → no special handling; raw swap carries all buckets verbatim; rename retargets only the primary telemetry bucket; prompt is solely about choosing the rename target.

Deferred to implementation

  • Exact OpenPlotter Signal K service name(s) — detect dynamically; tolerate absence.
  • Whether Grafana built-in admin:admin still authenticates given OIDC — verify; fall back to a temp admin.
  • Whether influx prestart password-sync disturbs the injected operator token — verify token survives restart.
  • Reconciling the restored SK influx-writer plugin config (arbitrary boat org/bucket) with the renamed marine bucket and Halos prestart auto-config — rewrite, defer to prestart, or document a manual step.
  • Whether mkdocs build --strict warns on raw .sh under docs/ (nav-exclude vs docs/assets/).
  • USB "clearly too small" threshold heuristic.

High-Level Technical Design

Directional guidance for review, not implementation specification.

sequenceDiagram
    participant U as User
    participant OP as OpenPlotter (live)
    participant USB as USB stick
    participant HA as Halos (fresh)
    U->>OP: fetch + run openplotter-backup.sh
    OP->>OP: stop signalk/influxdb/grafana, close OpenCPN
    OP->>USB: copy ~/.signalk, /var/lib/influxdb2,<br/>grafana.db, ~/.opencpn (+manifest)
    OP->>OP: verify completeness, restart services
    OP-->>U: "SAFE TO REFLASH"
    U->>HA: reflash to Halos (existing docs), then fetch + run halos-restore.sh
    HA->>USB: validate manifest (format_version)
    HA->>HA: SK: default aside, restore data, start, verify API
    HA->>HA: Influx: mint token (bolt), swap db, inject token,<br/>rename org/bucket→marine, DBRP, smoke query
    HA->>HA: Grafana: read boat grafana.db (python3),<br/>recreate boat DS (preserved UID), import dashboards
    HA->>HA: OpenCPN: restore ~/.opencpn + ~/.local/share/opencpn
    HA-->>U: health check + verification summary
Loading

Risks & Dependencies

Risk Mitigation
USB is the only data copy during the irreversible wipe Hard completeness gate; "safe to reflash" only on verified backup; runbook leads with this
Boat InfluxDB version newer than Halos influxd (2.9.1) → raw data won't open Document precondition; restore surfaces start failure, keeps db.halos-default for rollback
influx prestart password-sync clobbers injected operator token Inject token before first start; verify it survives restart
Source org/bucket names arbitrary; multiple buckets may exist Discover by id, identify telemetry bucket by data presence, prompt when ambiguous; never hardcode
Ongoing SK logging writes to old bucket name after migration Rename to marine + reconcile SK influx plugin config; documented manual step as fallback
Grafana OIDC blocks admin:admin API access Verify; fall back to provisioning a temporary admin
OpenCPN charts in relocated user folders not captured Detect from opencpn.conf, warn; document as best-effort
mkdocs --strict rejects raw .sh in docs tree Confirm/adjust placement or nav config

Implementation Units

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions