The orphan thin check runs only after a failed snapshot backup (R-1012 follow-up, unreleased)
gates / gates (push) Successful in 48s
gates / gates (push) Successful in 48s
v0.159.0/v0.159.1 ran the check before every snapshot-mode whole-guest backup, which takes a metadata snapshot of the live pool. Now explainFailure (vzdump start or task failure) is its only caller; the finding is still appended to the failed backup's error. Tests: failure = 1 call, success = 0 calls; red-proved against the v0.159.1 runner.go (2 calls / calls=1). Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -10,8 +10,9 @@ import (
|
||||
)
|
||||
|
||||
// R-1012: a snapshot backup that fails carries the orphan finding in its error — the text the controller mails to
|
||||
// the operator (`whole_guest_backup_failed`). The check runs once before the run and once, FRESH, after the failure.
|
||||
// RED-PROOF (P1/redproof-thincheck.txt): with explainFailure's append removed this test fails.
|
||||
// the operator (`whole_guest_backup_failed`). The check runs ONCE, after the failure — never before the run.
|
||||
// RED-PROOF (P1/redproof-thincheck.txt): with explainFailure's append removed this test fails. RED-PROOF (2026-10-17
|
||||
// night, felhom-agent REPORT.md): with the v0.159.0 pre-run check restored, calls = 2 and this test fails.
|
||||
func TestBackup_SnapshotFailure_NamesTheOrphan(t *testing.T) {
|
||||
const why = "probable cause: thin pool pve/data holds device 4 that LVM does not know"
|
||||
for name, api := range map[string]*fakeBackupAPI{
|
||||
@@ -29,15 +30,17 @@ func TestBackup_SnapshotFailure_NamesTheOrphan(t *testing.T) {
|
||||
if !strings.Contains(rec.Error, why) {
|
||||
t.Fatalf("the failure does not name the orphan: %q", rec.Error)
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("thin check ran %d times, want 2 (before the run and after the failure)", calls)
|
||||
if calls != 1 {
|
||||
t.Fatalf("thin check ran %d times, want 1 (after the failure only)", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A successful backup keeps a clean record (the pre-check only logs); a stop-mode backup takes no snapshot, so the
|
||||
// check never runs; an unwired runner behaves exactly as before.
|
||||
// A successful backup never runs the check (it takes a metadata snapshot of the live pool, and nobody needs the answer)
|
||||
// and keeps a clean record; a stop-mode backup takes no snapshot, so the check never runs; an unwired runner behaves
|
||||
// exactly as before. RED-PROOF (2026-10-17 night): with the v0.159.0 pre-run check restored, the success leg fails
|
||||
// with calls=1.
|
||||
func TestBackup_ThinCheck_SilentWhenNotApplicable(t *testing.T) {
|
||||
ok := &fakeBackupAPI{vzdumpUPID: "UPID:x",
|
||||
content: []proxmox.StorageContent{{VolID: "local:backup/vzdump-lxc-9201-x", Content: "backup", VMID: 9201, Size: 100, CTime: 1}}}
|
||||
@@ -45,8 +48,8 @@ func TestBackup_ThinCheck_SilentWhenNotApplicable(t *testing.T) {
|
||||
calls := 0
|
||||
r.SetThinCheck(func(context.Context, int) string { calls++; return "probable cause: x" })
|
||||
rec, err := r.Backup(context.Background(), 9201)
|
||||
if err != nil || !rec.Success || rec.Error != "" || calls != 1 {
|
||||
t.Fatalf("success: err=%v rec=%+v calls=%d (want one pre-check, clean record)", err, rec, calls)
|
||||
if err != nil || !rec.Success || rec.Error != "" || calls != 0 {
|
||||
t.Fatalf("success: err=%v rec=%+v calls=%d (want no check on a successful backup, clean record)", err, rec, calls)
|
||||
}
|
||||
|
||||
stop := NewBackupRunner(&fakeBackupAPI{vzdumpUPID: "UPID:x", waitErr: errors.New("boom")}, "local", proxmox.ModeStop, "", "", quiet())
|
||||
|
||||
@@ -62,8 +62,9 @@ type BackupRunner struct {
|
||||
// this target, so a leftover from a power cut cannot hold the space this backup needs. nil = nothing.
|
||||
beforeBackup func(ctx context.Context, storage string, vmid int)
|
||||
// thinCheck (R-1012) names an orphan thin device in the guest's pools in plain words ("" = none / not checked). It
|
||||
// runs before a snapshot-mode backup (a WARN in the log) and again when the vzdump fails, so the failure the
|
||||
// controller mails to the operator says why. It never removes anything. nil = not wired.
|
||||
// runs ONLY when a snapshot-mode vzdump has failed (start or task), so the failure the controller mails to the
|
||||
// operator says why; a successful backup never runs it (it takes a metadata snapshot of the live pool). It never
|
||||
// removes anything. nil = not wired.
|
||||
thinCheck func(ctx context.Context, vmid int) string
|
||||
logger *slog.Logger
|
||||
now func() time.Time
|
||||
@@ -117,8 +118,8 @@ func (r *BackupRunner) SetBeforeBackup(fn func(ctx context.Context, storage stri
|
||||
// SetThinCheck wires the R-1012 orphan check (see BackupRunner.thinCheck).
|
||||
func (r *BackupRunner) SetThinCheck(fn func(ctx context.Context, vmid int) string) { r.thinCheck = fn }
|
||||
|
||||
// explainFailure appends the orphan finding to a failed snapshot-mode backup's error. A FRESH check (not the one from
|
||||
// before the run): the failure itself may be what made the orphan. Bounded, so a stuck helper cannot hold the job.
|
||||
// explainFailure appends the orphan finding to a failed snapshot-mode backup's error — the only place the check runs, so
|
||||
// it sees the pool as the failure left it. Bounded, so a stuck helper cannot hold the job.
|
||||
func (r *BackupRunner) explainFailure(ctx context.Context, vmid int, rec *hub.Backup) {
|
||||
if r.thinCheck == nil || r.mode != proxmox.ModeSnapshot {
|
||||
return
|
||||
@@ -221,11 +222,8 @@ func (r *BackupRunner) backup(ctx context.Context, vmid int, onSnapshot func())
|
||||
return rec, fmt.Errorf("backup: %s", rec.Error)
|
||||
}
|
||||
|
||||
if r.thinCheck != nil && r.mode == proxmox.ModeSnapshot {
|
||||
if why := r.thinCheck(ctx, vmid); why != "" {
|
||||
r.logger.Warn("backup: an orphan thin device may make this snapshot backup fail (R-1012)", "vmid", vmid, "finding", why)
|
||||
}
|
||||
}
|
||||
// R-1012: no orphan check here. It takes a metadata snapshot of the live pool, and its answer is only needed when the
|
||||
// snapshot step has failed — explainFailure runs it then (pinned by TestBackup_ThinCheck_SilentWhenNotApplicable).
|
||||
upid, err := r.api.Vzdump(ctx, proxmox.VzdumpOptions{
|
||||
VMID: vmid, Storage: r.target, Mode: r.mode, Notes: r.notes,
|
||||
PruneBackups: r.localPruneSpec(ctx), // local target → keep-last=N; PBS/unknown → "" (no prune)
|
||||
|
||||
@@ -91,7 +91,7 @@ var manifest = []Capability{
|
||||
{"disk-mkfs-xfs", "guarded format (xfs)", "/usr/local/sbin/felhom-mkfs-guarded", []string{"/dev/sda", "xfs"}, false, ""},
|
||||
{"disk-smart", "disk SMART health read", "/usr/sbin/smartctl", []string{"-a", "-j", "/dev/sda"}, false, ""},
|
||||
{"disk-lvs", "thin-pool usage read", "/usr/sbin/lvs", []string{"--reportformat", "json", "--units", "b", "-o", "lv_name,data_percent,metadata_percent", "--", "pve/data"}, false, ""},
|
||||
{"thin-orphan-check", "orphan thin device check around a whole-guest backup (read-only, R-1012)", "/usr/local/sbin/felhom-thin-check", []string{"pve", "data"}, false, ""},
|
||||
{"thin-orphan-check", "orphan thin device check after a failed whole-guest backup (read-only, R-1012)", "/usr/local/sbin/felhom-thin-check", []string{"pve", "data"}, false, ""},
|
||||
|
||||
// ---- Storage mount units (watchdog re-mount) ----
|
||||
{"mount-unit-install", "fs-UUID mount unit install (root content check, R-861)", "/usr/local/sbin/felhom-priv-apply", []string{"unit", "mnt-felhom\\x2dx.mount"}, false, ""},
|
||||
|
||||
Reference in New Issue
Block a user