R-1124 + R-1012: forget a decommissioned drive that is gone; name an orphan thin device in a backup failure
gates / gates (push) Successful in 47s

R-1124: ForgetRetiredDrives (start-up + the 20 s tick) disables and removes the host mount unit of a
DECOMMISSIONED drive whose disk is absent and unmounted; the agent then no longer lists it. A present
decommissioned drive keeps its unit (one-click re-enroll). The file goes only through the new
felhom-priv-apply verb `unit-forget`, which re-checks marker, disabled, unmounted and absent as root.

R-1012: the read-only root helper felhom-thin-check <vg> <pool> (bundle) lists the pool's thin ids
(metadata snapshot reserved and released) next to LVM's before and after. BackupRunner.SetThinCheck runs
it before a snapshot-mode backup (log) and again after a failure, appending the finding in plain words to
the error the controller mails to the operator. Two reads, intersected; fails open; never removes.

Red-proofs: felhom.eu audits/agent-allowlist-rotation-2026-10-17/P1/. Code only; the CHANGELOG entry
follows the release.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-17 15:20:34 +02:00
parent 344c4f9030
commit e15d1d30ea
15 changed files with 1116 additions and 5 deletions
+9
View File
@@ -59,6 +59,7 @@ import (
"gitea.dooplex.hu/admin/felhom-agent/internal/selfupdate"
"gitea.dooplex.hu/admin/felhom-agent/internal/signedjobs"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
"gitea.dooplex.hu/admin/felhom-agent/internal/thincheck"
"gitea.dooplex.hu/admin/felhom-agent/internal/vzpartial"
"gitea.dooplex.hu/admin/felhom-agent/internal/wgtunnel"
)
@@ -1425,6 +1426,9 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
// re-assert below sees its unit.
localSrv.EnsureInternalPlace(ctx)
if mountReasserter != nil {
// R-1124: a decommissioned drive that is gone loses its host mount unit first (start-up and every tick), so
// no later boot waits for it and the re-assert below no longer lists it.
mountReasserter.ForgetRetiredDrives(ctx, intentReader)
mountReasserter.ReassertEnrolledMounts(ctx)
}
// F9: on startup (the host's bring-up/reconcile trigger), re-assert any enrolled guest data-drive
@@ -1463,6 +1467,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
// unit re-disabled at runtime), then re-assert the guest binds against the now-live mounts.
localSrv.EnsureInternalPlace(ctx) // `16` §5: a record written since the last tick (the signed job)
if mountReasserter != nil {
mountReasserter.ForgetRetiredDrives(ctx, intentReader) // R-1124: decommissioned + gone → unit removed
mountReasserter.ReassertEnrolledMounts(ctx)
}
localSrv.ReassertGuestBinds(ctx)
@@ -2061,8 +2066,12 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
},
Logger: logger,
}
// R-1012: an orphan thin device (a full host root during a snapshot) is NAMED in the backup failure the controller
// mails to the operator, through the read-only root helper. Never removed — the repair stays a human step.
thinChecker := &thincheck.Checker{API: px, Runner: &proxmox.ExecRunner{Mode: gaMode, SudoPath: cfg.Privileged.SudoPath}, Logger: logger}
for _, r := range tierRunners {
r.SetBeforeBackup(func(ctx context.Context, storage string, vmid int) { cleaner.Clean(ctx, storage, vmid) })
r.SetThinCheck(func(ctx context.Context, vmid int) string { return thincheck.Describe(thinChecker.Check(ctx, vmid)) })
}
srv.SetPartialSweep(func(ctx context.Context, vmid int) {
for _, t := range tierTargets {