diff --git a/CHANGELOG.md b/CHANGELOG.md index 218074d..552dd72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to **felhom-agent** are recorded here. Update on every code change that gets pushed. +## v0.36.7 — isolate the shared parent only on CREATE (no peer-group churn) (2026-06-15) + +Follow-up to v0.36.6: make-private+make-shared must run ONLY when the self-bind is first created, not on +every reconcile — re-doing it churns the peer-group id and ORPHANS the guest`s already-established slave +(propagation silently dies, guest sees empty). Guarded on the mountpoint check; on a fresh boot it runs +once before pve-guests so the guest slaves the right group. + ## v0.36.6 — shared parent gets its OWN peer group (make-private first) — ROOT CAUSE of double-bind (2026-06-15) The shared-parent self-bind INHERITED the root mount`s shared peer group (`/mnt/felhom-drives` was diff --git a/cmd/felhom-agent/main.go b/cmd/felhom-agent/main.go index 583b274..82064b2 100644 --- a/cmd/felhom-agent/main.go +++ b/cmd/felhom-agent/main.go @@ -44,7 +44,7 @@ import ( // version is the agent version. Overridable at build time with // -ldflags "-X main.version="; defaults to the in-repo CHANGELOG version. -var version = "0.36.6" +var version = "0.36.7" // runGuestHook is the PVE pre-start hook body (`felhom-agent guest-hook `). On the // pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots diff --git a/internal/localapi/intermediary.go b/internal/localapi/intermediary.go index 5c6d96a..ad33ccd 100644 --- a/internal/localapi/intermediary.go +++ b/internal/localapi/intermediary.go @@ -32,12 +32,15 @@ const sharedParentScript = `#!/bin/sh # the shared peer group (slave). Installed + enabled by felhom-agent. Idempotent. set -e mkdir -p ` + StableParentDir + ` -mountpoint -q ` + StableParentDir + ` || mount --bind ` + StableParentDir + ` ` + StableParentDir + ` -# make-PRIVATE first: the self-bind inherits the root mount's shared peer group, so without detaching it -# every drive bind under the parent would propagate back (and DOUBLE). Then make-shared = its OWN group, -# whose only slave is the guest's parent bind → binds propagate to the guest exactly once. -mount --make-private ` + StableParentDir + ` -mount --make-shared ` + StableParentDir + ` +# Isolate + share ONLY when first creating the self-bind (a fresh boot). The self-bind inherits the root +# mount's shared peer group, so make-private detaches it (else binds under it DOUBLE via the root peer), +# then make-shared gives it its own group whose only slave is the guest's parent bind. Re-running this on +# an existing parent would churn the peer-group id and orphan the guest's slave — so guard on mountpoint. +if ! mountpoint -q ` + StableParentDir + `; then + mount --bind ` + StableParentDir + ` ` + StableParentDir + ` + mount --make-private ` + StableParentDir + ` + mount --make-shared ` + StableParentDir + ` +fi ` const sharedParentUnitPath = "/etc/systemd/system/felhom-shared-parent.service" @@ -94,19 +97,22 @@ func (b *GuestBinder) EnsureSharedParent(ctx context.Context) error { if err := b.run(ctx, "mkdir", "-p", StableParentDir); err != nil { return fmt.Errorf("shared-parent: mkdir %s: %w", StableParentDir, err) } + // Isolate + share the parent ONLY when first creating the self-bind. The self-bind inherits the root + // mount's shared peer group, so make-PRIVATE detaches it (else binds under it double via the root + // peer); make-SHARED then gives it its OWN group whose only slave is the guest's parent bind. This + // must NOT run on every reconcile: re-doing make-private+make-shared churns the peer-group id and + // ORPHANS the guest's already-established slave (propagation silently dies). On a fresh host boot the + // parent isn't a mountpoint → this runs once, before pve-guests, so the guest slaves the right group. if !isHostMountpoint(StableParentDir) { if err := b.run(ctx, "mount", "--bind", StableParentDir, StableParentDir); err != nil { return fmt.Errorf("shared-parent: self-bind: %w", err) } - } - // make-PRIVATE first to detach from the root mount's shared peer group (the self-bind inherits it), - // THEN make-shared so the parent owns its OWN group — otherwise binds under it propagate back via the - // root peer and DOUBLE (observed live as 2 stacked binds per drive). - if err := b.run(ctx, "mount", "--make-private", StableParentDir); err != nil { - return fmt.Errorf("shared-parent: make-private: %w", err) - } - if err := b.run(ctx, "mount", "--make-shared", StableParentDir); err != nil { - return fmt.Errorf("shared-parent: make-shared: %w", err) + if err := b.run(ctx, "mount", "--make-private", StableParentDir); err != nil { + return fmt.Errorf("shared-parent: make-private: %w", err) + } + if err := b.run(ctx, "mount", "--make-shared", StableParentDir); err != nil { + return fmt.Errorf("shared-parent: make-shared: %w", err) + } } // Install the boot-persistence unit only when missing OR its content differs from what we ship (so a // unit-template fix deploys) — EnsureSharedParent runs on a periodic reconcile, and re-writing files +