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:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -93,10 +94,13 @@ func (b *GuestBinder) EnsureSharedParent(ctx context.Context) error {
|
||||
if err := b.run(ctx, "mount", "--make-shared", StableParentDir); err != nil {
|
||||
return fmt.Errorf("shared-parent: make-shared: %w", err)
|
||||
}
|
||||
if err := b.installSharedParentUnit(ctx); err != nil {
|
||||
b.logger.Warn("shared-parent: boot-persistence unit install failed (live setup OK; survives until host reboot)", "err", err)
|
||||
// Install the boot-persistence unit only if it's not already there — EnsureSharedParent runs on a
|
||||
// periodic reconcile, and re-writing files + daemon-reload every tick would be wasteful.
|
||||
if _, err := os.Stat(sharedParentUnitPath); err != nil {
|
||||
if ierr := b.installSharedParentUnit(ctx); ierr != nil {
|
||||
b.logger.Warn("shared-parent: boot-persistence unit install failed (live setup OK; survives until host reboot)", "err", ierr)
|
||||
}
|
||||
}
|
||||
b.logger.Info("shared-parent: host stable parent is shared", "dir", StableParentDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -133,8 +137,15 @@ func (b *GuestBinder) installSharedParentUnit(ctx context.Context) error {
|
||||
// host PVE mount (/mnt/<name>); only `<where>/felhom-data` crosses into the guest (confinement). The
|
||||
// stable per-drive dir is created HOST-ROOT-owned (fail-closed when nothing is mounted under it); the
|
||||
// felhom-data namespace is created+chowned to the guest base so the in-guest controller owns it.
|
||||
// Idempotent: if the stable path is already a mountpoint, it's a no-op.
|
||||
func (b *GuestBinder) AttachDrive(ctx context.Context, where string) (string, error) {
|
||||
//
|
||||
// GUEST-REBOOT SAFETY (the load-bearing subtlety): a guest's parent bind is NON-RECURSIVE, so on a guest
|
||||
// reboot it does NOT carry the pre-existing drive submount, and mount propagation only delivers mount
|
||||
// events created AFTER the guest's bind exists. So "the host already has the bind" is NOT sufficient —
|
||||
// the GUEST may not see it. AttachDrive therefore checks whether vmid's guest actually sees the stable
|
||||
// path; if the host has the bind but the guest does not (the post-guest-reboot case), it FORCE re-binds
|
||||
// (umount + mount) to fire a fresh propagation event into the current guest namespace. Idempotent when
|
||||
// the guest already sees it.
|
||||
func (b *GuestBinder) AttachDrive(ctx context.Context, vmid int, where string) (string, error) {
|
||||
stable := StablePathForRaw(where)
|
||||
if stable == "" {
|
||||
return "", fmt.Errorf("guest-attach: %q is not a /mnt/<name> mount", where)
|
||||
@@ -151,17 +162,61 @@ func (b *GuestBinder) AttachDrive(ctx context.Context, where string) (string, er
|
||||
if err := b.run(ctx, "mkdir", "-p", stable); err != nil {
|
||||
return "", fmt.Errorf("guest-attach: stable dir %s: %w", stable, err)
|
||||
}
|
||||
if isHostMountpoint(stable) {
|
||||
b.logger.Info("guest-attach: already bound under parent (idempotent)", "where", where, "stable", stable)
|
||||
return stable, nil
|
||||
hostHas := isHostMountpoint(stable)
|
||||
guestSees := b.GuestSeesMount(ctx, vmid, stable)
|
||||
switch {
|
||||
case hostHas && guestSees:
|
||||
return stable, nil // fully live — no-op
|
||||
case hostHas && !guestSees:
|
||||
// Guest rebooted (or bound the parent before this drive's bind existed): re-fire propagation.
|
||||
if err := b.run(ctx, "umount", stable); err != nil {
|
||||
b.logger.Warn("guest-attach: re-bind umount failed (continuing to re-mount)", "stable", stable, "err", err)
|
||||
}
|
||||
if err := b.run(ctx, "mount", "--bind", src, stable); err != nil {
|
||||
return "", fmt.Errorf("guest-attach: re-bind %s -> %s: %w", src, stable, err)
|
||||
}
|
||||
b.logger.Info("guest-attach: re-bound drive to re-propagate into guest (post-reboot)", "vmid", vmid, "where", where, "stable", stable)
|
||||
default: // !hostHas
|
||||
if err := b.run(ctx, "mount", "--bind", src, stable); err != nil {
|
||||
return "", fmt.Errorf("guest-attach: bind %s -> %s: %w", src, stable, err)
|
||||
}
|
||||
b.logger.Info("guest-attach: drive bound under shared parent (live, no reboot)", "vmid", vmid, "where", where, "stable", stable)
|
||||
}
|
||||
if err := b.run(ctx, "mount", "--bind", src, stable); err != nil {
|
||||
return "", fmt.Errorf("guest-attach: bind %s -> %s: %w", src, stable, err)
|
||||
}
|
||||
b.logger.Info("guest-attach: drive bound under shared parent (live, no reboot)", "where", where, "stable", stable)
|
||||
return stable, nil
|
||||
}
|
||||
|
||||
// GuestSeesMount reports whether vmid's guest currently has `path` as a mount target in ITS mount
|
||||
// namespace (read from /proc/<guest-init-pid>/mountinfo). This is the GUEST-side truth the host-side
|
||||
// isHostMountpoint can't see — the signal that distinguishes "bound on the host" from "live in the
|
||||
// guest" after a guest reboot. A resolution/read error → false (treat as not-seen → AttachDrive re-binds,
|
||||
// which is safe). The controller's BoundUnderParent report keys on this.
|
||||
func (b *GuestBinder) GuestSeesMount(ctx context.Context, vmid int, path string) bool {
|
||||
pid := b.guestInitPID(ctx, vmid)
|
||||
if pid == "" {
|
||||
return false
|
||||
}
|
||||
data, err := os.ReadFile("/proc/" + pid + "/mountinfo")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) >= 5 && f[4] == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// guestInitPID returns the guest's PID-1 host PID (`lxc-info -n <vmid> -p -H`), or "" on error.
|
||||
func (b *GuestBinder) guestInitPID(ctx context.Context, vmid int) string {
|
||||
out, _, err := b.runner.Run(ctx, "lxc-info", "-n", strconv.Itoa(vmid), "-p", "-H")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// DetachDrive unmounts a drive's felhom-data from the stable parent (propagates OUT of the guest live),
|
||||
// leaving the bare HOST-ROOT-owned stable dir → fail-closed (the guest can't write to it even as root,
|
||||
// since host uid 0 is unmapped). No pct, no reboot. Idempotent: a non-mountpoint is a no-op.
|
||||
|
||||
Reference in New Issue
Block a user