Files
felhom-agent/REPORT.md
T
2026-06-12 17:57:05 +02:00

6.0 KiB

REPORT — slice 10: external user-data drive passthrough (P1 spike + P2)

Agent v0.25.0 (+ controller v0.48.0, golden rebaked). P1 spike PASSED (gate); P2 BUILT + validated live on guest 9201. P3 (self-heal) + P4 (dual-role) are the next phases.

PHASE 1 — SPIKE (GATE PASSED), four proofs on 9201

  • 1A host→guest bind: pct set 9201 -mp0 /mnt/felhom-usb,mp=/mnt/felhom-usbbind form (host path), never storage:size. Propagation shared:49 host↔guest automatic.
  • 1B write — chown, not idmap: idmap not clean (mixed host ownership 1000+0, container-wide, restart, subuid). Decision (refined with Viktor): chown only a fresh <drive>/felhom-data namespace to 100000:100000; the customer's existing data is never touched. Guest-root r+w confirmed.
  • 1C guest→controller-container: -v /mnt:/mnt:rslave + /mnt rshared in the guest → newly-mounted drives propagate into the running container (proven). The de-priv /mnt gap, reopened scoped.
  • 1D app-container: busybox bind read+write; bytes land on /dev/sdb1.

PHASE 2 — passthrough (Model A: the felhom-data namespace is the in-guest mount)

  • P2A agent — POST /disks/guest-attach (internal/localapi, GuestBinder): self-scoped; creates <drive>/felhom-data, chowns it to the guest base (not -R), and pct set <vmid> -mpN <drive>/felhom-data,mp=/mnt/<name> (RW bind). Idempotent; lowest free mpN; where validated. Only Felhom's namespace crosses into the guest — the customer's other on-drive data never does. Tests: TestGuestAttach_* (slot select, idempotency, bad-path, not-configured).
  • P2B golden — configs/build-golden.sh: controller docker run gains -v /mnt:/mnt:rslave; the bootstrap makes /mnt rshared first. Scoped to /mnt (only felhom-data-namespace mounts).
  • P2C controller (v0.48.0): agentapi.GuestAttach; runStorageInit/runStorageAttach/ handleStorageRegister call attachIntoGuest after register (best-effort; P3 heals a miss).

Live validation (9201)

After guest-attach + a guest restart to activate mp0:

  • mp0 = /mnt/felhom-usb SOURCE /dev/sdb1[/felhom-data] (Model A; the [/felhom-data] suffix the controller's mount strip already handles).
  • Controller container mountinfo has /felhom-data /mnt/felhom-usb … /dev/sdb1.
  • An app (busybox bind) writes proof.txt → present on the host /mnt/felhom-usb/felhom-data/..., df device /dev/sdb1 (NOT the rootfs).
  • Banner cleared: [PASS] Storage paths: 1 connected, 0 disconnected.
  • go test ./... green (both repos).

LIVE-ACTIVATION — investigated, mechanism chosen (evidence-based)

A drive enrolled into a running unprivileged guest cannot be activated live, and the host-side bind-inject is blocked (proven on 9201): pct set doesn't hot-apply a mountpoint to a running guest; /proc/<pid>/root/... bind → mount: bad superblock (unprivileged mount-locking); nsenter -m into the guest ns loses the host source path. The bind activates at the next guest boot (validated: reboot → mp0 active /dev/sdb1[/felhom-data], controller sees it, banner clears). Fresh guests from the rebaked golden are unaffected (mp activates at first boot, before the controller starts).

Decision (Viktor): enroll persists via pct set with NO forced reboot; the UI shows a "pending activation" state + a user-triggered "Újraindítás most (~30s)" button that batches all pending drives; P3 self-heal flags "restart to reconnect" for recovered drives. The staging-mp live-propagation alternative is deferred to its own spike. (Build remaining — see below.)

Activation-UX — DONE (agent v0.26.0 + controller v0.49.0)

  • Agent POST /guest/reboot (self-scoped, detached pct reboot, 202). Controller pendingActivationDrives() flags registered drives the agent shows attached but not live-mounted in the container; settings banner + batched "Újraindítás most (~30 mp)" button → /api/storage/ activateagentapi.GuestReboot. Live-validated on 9201: activate → guest reboots → drive active, app writes to /dev/sdb1, banner absent. (Pending-banner display couldn't be forced live — LXC mp mounts resist manual unmount + no 2nd drive — but the logic is unit-confident.)
  • Minor known artifact: the bootstrap's mount --rbind /mnt /mnt duplicates the mp submount, so findmnt /mnt/<name> shows two (identical) rows — functional (IsMountPoint true, apps write fine); inherent to rbind (a non-recursive bind would hide the submounts from the container). Tidy later.

P3 self-heal reconcile — DONE (agent v0.27.0)

  • IntentStore (internal/storage/intent.go): durable, durable-id-keyed 4-state model (new/enrolled/ejected/decommissioned), atomic writes, OnAbsent replug rule. Intent recorded ONLY via the official enroll (/disks/guest-attach, incl. the idempotent path) + eject (/disks/eject, before unmount) paths.
  • Watchdog: re-mount candidate now intent-gated (only enrolled self-heals) + a present→absent hook clears ejected; exponential-backoff flapping guard (alert@4, hard stop@8, failure = "still not present a full backoff window after dispatch" so a slow async re-mount isn't miscounted).
  • Wired in main.go: one IntentStore at <state-dir>/drive-intents.json, shared by the watchdog
    • the local API; open failure degrades to ungated legacy remount.
  • Tests: intent states/persistence/replug-rule/gate + the watchdog scenarios (colleague-unmount→ reconciled, ejected/new/decommissioned→left alone, ejected→absent→replug→auto-mount, flapping→caps).
  • Live-validated on 9201: out-of-band umount → watchdog "self-heal — re-mounting enrolled drive (no eject intent)" → host HEALED within a tick; official eject → intent ejectedNOT reconciled (stays unmounted). (No golden change — P3 is host-side.)

Not done

  • P4 dual-role eligibility + backup-aware wipe warning (controller). Cross-drive backup ENGINE stays out of scope (needs a 2nd physical drive).