agent v0.35.0: intermediary mount — guest-reboot re-propagation (load-bearing)

A guest's parent bind is non-recursive, so a guest reboot leaves enrolled drives
bound on the HOST but invisible in the fresh guest ns (propagation only delivers
new events). AttachDrive(vmid) now checks GuestSeesMount (/proc/<pid>/mountinfo)
and force re-binds (umount+mount) to re-propagate; a 20s periodic reconcile
self-heals guest reboots without an agent restart; BoundUnderParent reflects guest
visibility (the controller gate's signal). Caught + fixed in the live migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:26:32 +02:00
parent 3a9be73875
commit 26c6d1e4d1
5 changed files with 121 additions and 22 deletions
+17 -8
View File
@@ -69,11 +69,15 @@ type GuestLister interface {
type GuestAttacher interface {
// AttachDrive (intermediary model) binds the drive's felhom-data under the shared parent so it
// appears live in the guest at the returned stable path — no pct, no reboot. `where` = raw /mnt/<name>.
AttachDrive(ctx context.Context, where string) (guestPath string, err error)
// vmid is needed to verify GUEST visibility (a guest reboot needs a fresh re-bind to re-propagate).
AttachDrive(ctx context.Context, vmid int, where string) (guestPath string, err error)
// DetachDrive unmounts the drive's felhom-data from the shared parent (live, fail-closed).
DetachDrive(ctx context.Context, where string) error
// EnsureSharedParent makes the host stable parent shared + installs the boot-persistence unit.
EnsureSharedParent(ctx context.Context) error
// GuestSeesMount reports whether vmid's guest sees `path` as a mount in its own namespace (the
// guest-usable signal — distinct from the host having the bind). Backs BoundUnderParent.
GuestSeesMount(ctx context.Context, vmid int, path string) bool
// AttachBind is the LEGACY per-drive `pct set -mpN` bind (pre-intermediary). Retained for the
// transition; new attaches use AttachDrive.
AttachBind(ctx context.Context, vmid int, mountKey, where string) error
@@ -177,7 +181,7 @@ func (s *Server) handleDisks(w http.ResponseWriter, r *http.Request, vmid int) {
if di.Role == string(storage.RoleUserData) {
if gp := StablePathForRaw(t.MountPath); gp != "" {
di.GuestPath = gp
di.BoundUnderParent = s.boundUnderParent(gp)
di.BoundUnderParent = s.boundUnderParent(r.Context(), vmid, gp)
}
}
// Inspect the backing device for the UI's data-bearing hint (the authoritative check
@@ -389,7 +393,7 @@ func (s *Server) handleDiskGuestAttach(w http.ResponseWriter, r *http.Request, v
if err := s.guestAttach.EnsureSharedParent(r.Context()); err != nil {
s.logger.Warn("local-api: guest-attach — shared parent ensure failed (continuing)", "vmid", vmid, "err", err)
}
stable, err := s.guestAttach.AttachDrive(r.Context(), where)
stable, err := s.guestAttach.AttachDrive(r.Context(), vmid, where)
if err != nil {
s.logger.Error("local-api: guest-attach (intermediary)", "vmid", vmid, "where", where, "err", err)
writeErr(w, http.StatusBadGateway, "guest-attach failed: "+err.Error())
@@ -685,13 +689,18 @@ func (s *Server) handleDiskFormat(w http.ResponseWriter, r *http.Request, vmid i
"device is system/backup-protected — format requires an operator signature ("+dec.Reason+")")
}
// boundUnderParent reports whether a drive's felhom-data is currently bound at its stable guest path
// (intermediary model). Injectable via s.boundCheck for tests; defaults to the host mount-table read.
func (s *Server) boundUnderParent(stablePath string) bool {
// boundUnderParent reports whether a drive's felhom-data is bound at its stable guest path AND visible
// inside the guest (the usable-in-guest signal the controller's gate keys on — a guest reboot leaves the
// host bind in place but invisible to the guest until re-propagated). Injectable via s.boundCheck for
// tests; defaults to the guest-namespace mount check.
func (s *Server) boundUnderParent(ctx context.Context, vmid int, stablePath string) bool {
if s.boundCheck != nil {
return s.boundCheck(stablePath)
}
return isHostMountpoint(stablePath)
if s.guestAttach == nil {
return isHostMountpoint(stablePath)
}
return s.guestAttach.GuestSeesMount(ctx, vmid, stablePath)
}
// guestBoundPaths returns the set of guest mountpoint paths (the `mp=` of each entry in the guest's
@@ -798,7 +807,7 @@ func (s *Server) ReassertGuestBinds(ctx context.Context) {
s.logger.Warn("reconcile: enrolled drive not present (durable-id absent) — skipping", "vmid", vmid, "durable_id", id)
continue
}
stable, err := s.guestAttach.AttachDrive(ctx, where)
stable, err := s.guestAttach.AttachDrive(ctx, vmid, where)
if err != nil {
s.logger.Error("reconcile: AttachDrive failed", "vmid", vmid, "where", where, "err", err)
continue