v0.317.0: a second copy counts only on a proven different disk; the no-drive install speaks in words (felhom.eu architecture/16, build A)
gates / gates (push) Successful in 46s
gates / gates (push) Successful in 46s
Tier-2 and the one-copy notice ask the host agent (POST /disks/same-disk, agent 0.157.0): different + proven -> made and counted; same disk -> not made (D1); unknown (VM, hidden USB serial) -> made, not counted, the row says why (D2); a NAS never counts and no longer hides the notice (R-1075). An older agent keeps the st_dev rule, logged once. D4: a data-folder or media app on a box with no drive is refused in words with a link to the drives page (R-1068's raw-error half). MinAgent 0.157.0. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -67,6 +67,11 @@ type Tier2Target struct {
|
||||
IsSystemDrive bool // target is the internal SSD/system drive (DB/config only)
|
||||
StateOnly bool // §2.2: this target carries unit + MANDATORY only (optional legs skipped) — the SSD tier
|
||||
Reason string // why this target (Hungarian, for UI/logs)
|
||||
// Unproven (`16` D2): the agent cannot prove this target is another physical disk (a VM, a USB bridge
|
||||
// that hides the serial). The copy is still made; it does not count as a second copy, and the app's
|
||||
// row says why. UnprovenReason is the agent's reason code.
|
||||
Unproven bool
|
||||
UnprovenReason string
|
||||
}
|
||||
|
||||
// tier2FitsHeadroom reports whether a unit of unitGB fits on a system/rootfs drive while leaving a
|
||||
@@ -116,47 +121,75 @@ func (m *Manager) selectTier2TargetFrom(stackName, sourceDrive string, fullSize,
|
||||
sawNetworkCandidate = true
|
||||
break
|
||||
}
|
||||
if sp.Path == sourceDrive || m.sameDevice(sourceDrive, sp.Path) {
|
||||
break // pinned target is on the same physical disk — not off-drive; fall through
|
||||
if sp.Path == sourceDrive {
|
||||
break
|
||||
}
|
||||
rel, why := m.diskRelation(sourceDrive, sp.Path)
|
||||
if rel == DiskSame {
|
||||
break // pinned target is on the same physical disk — not off-drive; fall through (`16` D1)
|
||||
}
|
||||
label := sp.Label
|
||||
if label == "" {
|
||||
label = filepath.Base(sp.Path)
|
||||
}
|
||||
return &Tier2Target{
|
||||
NamespaceRoot: NamespaceRoot(sp.Path, true),
|
||||
Label: label,
|
||||
Reason: m.note("note.tier2.reason_manual"),
|
||||
NamespaceRoot: NamespaceRoot(sp.Path, true),
|
||||
Label: label,
|
||||
Reason: m.note("note.tier2.reason_manual"),
|
||||
Unproven: rel == DiskUnknown,
|
||||
UnprovenReason: unprovenWhy(rel, why),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Prefer another registered user-data drive on a DIFFERENT physical disk, NON-network.
|
||||
// 1. Prefer another registered user-data drive on a DIFFERENT physical disk, NON-network. A drive the
|
||||
// agent PROVES is another disk wins; failing that, the first drive it cannot judge (`16` D2: the copy
|
||||
// is made, not counted). A drive on the SAME disk is never a target (`16` D1).
|
||||
if m.settings != nil {
|
||||
var firstUnknown *Tier2Target
|
||||
for _, sp := range m.settings.GetSchedulableStoragePaths() {
|
||||
if sp.Path == sourceDrive || m.sameDevice(sourceDrive, sp.Path) {
|
||||
if sp.Path == sourceDrive {
|
||||
continue
|
||||
}
|
||||
if sp.IsNetwork() {
|
||||
sawNetworkCandidate = true // F-6C-1: never a tier-2 target
|
||||
continue
|
||||
}
|
||||
rel, why := m.diskRelation(sourceDrive, sp.Path)
|
||||
if rel == DiskSame {
|
||||
continue
|
||||
}
|
||||
label := sp.Label
|
||||
if label == "" {
|
||||
label = filepath.Base(sp.Path)
|
||||
}
|
||||
return &Tier2Target{
|
||||
NamespaceRoot: NamespaceRoot(sp.Path, true), // Model A: in-guest mount IS the namespace root
|
||||
Label: label,
|
||||
Reason: m.note("note.tier2.reason_other_drive"),
|
||||
}, nil
|
||||
t := &Tier2Target{
|
||||
NamespaceRoot: NamespaceRoot(sp.Path, true), // Model A: in-guest mount IS the namespace root
|
||||
Label: label,
|
||||
Reason: m.note("note.tier2.reason_other_drive"),
|
||||
Unproven: rel == DiskUnknown,
|
||||
UnprovenReason: unprovenWhy(rel, why),
|
||||
}
|
||||
if rel == DiskDifferent {
|
||||
return t, nil
|
||||
}
|
||||
if firstUnknown == nil {
|
||||
firstUnknown = t
|
||||
}
|
||||
}
|
||||
if firstUnknown != nil {
|
||||
return firstUnknown, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fall back to the internal SSD (system data path) — STATE-ONLY set only.
|
||||
sys := m.systemDataPath
|
||||
if sys == "" || m.sameDevice(sourceDrive, sys) {
|
||||
sysRel, sysWhy := DiskSame, ""
|
||||
if sys != "" {
|
||||
sysRel, sysWhy = m.diskRelation(sourceDrive, sys)
|
||||
}
|
||||
if sys == "" || sysRel == DiskSame {
|
||||
if sawNetworkCandidate {
|
||||
return nil, errTier2NetworkOnly // the only off-disk candidate was a NAS
|
||||
}
|
||||
@@ -175,9 +208,20 @@ func (m *Manager) selectTier2TargetFrom(stackName, sourceDrive string, fullSize,
|
||||
IsSystemDrive: true,
|
||||
StateOnly: true,
|
||||
Reason: m.note("note.tier2.reason_no_second"),
|
||||
// `16` D2: an app on a drive the agent cannot judge against the system disk — made, not counted.
|
||||
Unproven: sysRel == DiskUnknown,
|
||||
UnprovenReason: unprovenWhy(sysRel, sysWhy),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// unprovenWhy keeps the agent's reason only for an unknown verdict.
|
||||
func unprovenWhy(rel DiskRelation, why string) string {
|
||||
if rel == DiskUnknown {
|
||||
return why
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// tier2FitsSystemDrive checks the size-aware rootfs-headroom guard for the SSD target.
|
||||
func (m *Manager) tier2FitsSystemDrive(sys string, unitSizeBytes int64) bool {
|
||||
di := system.GetDiskUsage(sys)
|
||||
@@ -326,8 +370,9 @@ func (m *Manager) RunTier2(stackName string) error {
|
||||
m.logger.Printf("[INFO] [backup] Tier 2 for %s: no off-drive target — %s", stackName, reason)
|
||||
return nil
|
||||
}
|
||||
// Defense-in-depth off-drive guard (selection already enforced it).
|
||||
if m.sameDevice(sourceDrive, target.NamespaceRoot) {
|
||||
// Defense-in-depth off-drive guard (selection already enforced it) — asked again right before the run,
|
||||
// so a disk swapped in the same bay is re-judged (`16` §3 "before each Tier-2 run").
|
||||
if rel, _ := m.diskRelation(sourceDrive, target.NamespaceRoot); rel == DiskSame {
|
||||
m.recordTier2NoTarget(stackName, m.note("note.tier2.same_disk"))
|
||||
return nil
|
||||
}
|
||||
@@ -527,7 +572,10 @@ func (m *Manager) Tier2Info(stackName string) Tier2Info {
|
||||
}
|
||||
// Eligible alternative drives: registered, schedulable, on a DIFFERENT physical disk.
|
||||
for _, sp := range m.settings.GetSchedulableStoragePaths() {
|
||||
if sp.Path == source || m.sameDevice(source, sp.Path) {
|
||||
if sp.Path == source {
|
||||
continue
|
||||
}
|
||||
if rel, _ := m.diskRelation(source, sp.Path); rel == DiskSame {
|
||||
continue
|
||||
}
|
||||
label := sp.Label
|
||||
@@ -636,6 +684,9 @@ func (m *Manager) recordTier2SuccessWithUnit(stackName string, target *Tier2Targ
|
||||
c.LastDuration = dur.Round(time.Second).String()
|
||||
c.LastSizeHuman = humanizeBytes(sizeBytes)
|
||||
c.LastError = "" // a success invalidates the previous error
|
||||
// `16` D2: a copy on a disk the agent cannot prove is another one — kept, not counted.
|
||||
c.Unproven = target.Unproven
|
||||
c.UnprovenReason = target.UnprovenReason
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user