Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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-usb— bind form (host path), neverstorage:size. Propagationshared:49host↔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-datanamespace to100000:100000; the customer's existing data is never touched. Guest-root r+w confirmed. - 1C guest→controller-container:
-v /mnt:/mnt:rslave+/mntrshared in the guest → newly-mounted drives propagate into the running container (proven). The de-priv/mntgap, 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), andpct set <vmid> -mpN <drive>/felhom-data,mp=/mnt/<name>(RW bind). Idempotent; lowest freempN;wherevalidated. 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: controllerdocker rungains-v /mnt:/mnt:rslave; the bootstrap makes/mntrshared first. Scoped to/mnt(only felhom-data-namespace mounts). - P2C controller (v0.48.0):
agentapi.GuestAttach;runStorageInit/runStorageAttach/handleStorageRegistercallattachIntoGuestafter 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/...,dfdevice/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, detachedpct reboot, 202). ControllerpendingActivationDrives()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/ activate→agentapi.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 /mntduplicates the mp submount, sofindmnt /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,OnAbsentreplug 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
enrolledself-heals) + a present→absent hook clearsejected; 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
IntentStoreat<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 → intentejected→ NOT 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).