A plan's cut takes the backups before asking its first tier, and a machine it cannot ask holds it (issue 1086)

The cut asked tier 0 itself, so the step before a first tier never ran and gitea was sent to novox with no restore point. A machine with no backup holder is now a failed backup that holds the plan.
This commit is contained in:
2026-10-18 01:54:10 +02:00
parent 47f9290b37
commit 7103e33294
3 changed files with 89 additions and 10 deletions
+63 -8
View File
@@ -111,8 +111,8 @@ func TestOnlyModulesWithAnItemProtectedByABackupAreAsked(t *testing.T) {
}
}
// A machine with no backup holder is asked nothing, the plan says so, and it does not hold the plan.
func TestANodeWithNoBackupHolderAsksNothingAndThePlanSaysSo(t *testing.T) {
// A machine with no backup holder cannot be asked: the plan holds and says so, and asks nothing (issue 1086).
func TestANodeWithNoBackupHolderHoldsThePlanAndSaysSo(t *testing.T) {
seat := &aFakeBackupSeat{}
seat.install(t)
entries := []inventory.Entry{aModuleWith(t, "copied", []string{"ace", "bare"}, anItem("a", ""))}
@@ -121,17 +121,17 @@ func TestANodeWithNoBackupHolderAsksNothingAndThePlanSaysSo(t *testing.T) {
if err != nil || proceed {
t.Fatalf("proceed=%v err=%v", proceed, err)
}
if strings.Join(seat.asked, " ") != "ace/copied" {
t.Fatalf("asked %v", seat.asked)
if len(seat.asked) != 0 {
t.Fatalf("a held plan asks nothing: %v", seat.asked)
}
if p.Backups == nil || len(p.Backups.NoHolder) != 1 || p.Backups.NoHolder[0] != "bare" {
t.Fatalf("the plan must say bare has no holder: %+v", p.Backups)
if p.Backups == nil || len(p.Backups.NoHolder) != 1 || p.Backups.NoHolder[0] != "bare" ||
p.Backups.State != inventory.BackupFailed || !strings.Contains(p.Note, "copied on bare") {
t.Fatalf("the plan must hold and say bare has no holder: %+v note %q", p.Backups, p.Note)
}
// Only the no-holder machine: nothing to wait for, the plan goes on, and still says it.
only := []inventory.Entry{aModuleWith(t, "copied", []string{"bare"}, anItem("a", ""))}
q := aPlanOf("copied")
proceed, err = stepBackups(t.Context(), q, backupFactsFrom(only, holders("ace")), backupT0)
if err != nil || !proceed || q.Backups == nil || q.Backups.NoHolder[0] != "bare" || q.Backups.State != inventory.BackupDone {
if err != nil || proceed || q.Backups.State != inventory.BackupFailed {
t.Fatalf("proceed=%v err=%v record=%+v", proceed, err, q.Backups)
}
}
@@ -237,3 +237,58 @@ func TestTheBackupHoldersAreEveryMachinesAndAFailedReadIsNotNoHolder(t *testing.
t.Fatalf("a failed read of the holders must fail the step: %v", err)
}
}
// Issue 1086: the cut of a plan asked its first tier itself, so gitea was sent to novox with no backup asked.
func giteaOnNovox(t *testing.T) inventory.Entry {
return aModuleWith(t, "gitea", []string{"novox"},
`{"id":"data","path":"/var/lib/gitea/data","class":"valuable","why":"repositories"}`)
}
func TestACutPlanAsksTheBackupOfGiteaOnNovoxBeforeItAsksItsFirstTier(t *testing.T) {
seat := &aFakeBackupSeat{}
seat.install(t)
facts := backupFactsFrom([]inventory.Entry{giteaOnNovox(t)}, holders("novox", "ace"))
p := aPlanOf("gitea", "mesh-delivery")
tierAsked := false
err := askFirstTierAtCut(p,
func(p *inventory.Plan) (bool, error) { return stepBackups(t.Context(), p, facts, backupT0) },
func() error { tierAsked = true; return nil })
if err != nil || tierAsked {
t.Fatalf("the first tier was asked before the backup: asked=%v err=%v", tierAsked, err)
}
if strings.Join(seat.asked, " ") != "novox/gitea" || p.Backups == nil || p.Backups.State != inventory.BackupPending {
t.Fatalf("asked %v, record %+v", seat.asked, p.Backups)
}
if !strings.HasPrefix(p.Note, backupsNote) {
t.Fatalf("note %q", p.Note)
}
}
func TestACutPlanHoldsWhenTheMachineItSendsDataToHasNoBackupHolder(t *testing.T) {
seat := &aFakeBackupSeat{}
seat.install(t)
facts := backupFactsFrom([]inventory.Entry{giteaOnNovox(t)}, holders("ace"))
p := aPlanOf("gitea")
tierAsked := false
err := askFirstTierAtCut(p,
func(p *inventory.Plan) (bool, error) { return stepBackups(t.Context(), p, facts, backupT0) },
func() error { tierAsked = true; return nil })
if err != nil || tierAsked {
t.Fatalf("asked=%v err=%v", tierAsked, err)
}
if p.Backups == nil || p.Backups.State != inventory.BackupFailed || !strings.Contains(p.Note, "novox") ||
!strings.Contains(p.Note, "held") {
t.Fatalf("the plan must hold and say so: %+v note %q", p.Backups, p.Note)
}
}
func TestACutPlanWhoseBackupsCouldNotBeWorkedOutAsksNothingAndSaysSo(t *testing.T) {
p := aPlanOf("gitea")
tierAsked := false
err := askFirstTierAtCut(p,
func(*inventory.Plan) (bool, error) { return false, fmt.Errorf("the catalogue could not be read") },
func() error { tierAsked = true; return nil })
if err != nil || tierAsked || !strings.Contains(p.Note, "could not be worked out") {
t.Fatalf("asked=%v err=%v note %q", tierAsked, err, p.Note)
}
}