// Package backupwindow holds the pure time arithmetic for the customer-configurable backup window // (v0.168.0). ONE setting — the window start W — drives every nightly leg at FIXED offsets so the // legs can never be misordered, and never stores a derived time: the DB dump runs at W, the tier-2 // mirror at W+60m, the off-box copy at W+105m; the whole-guest (PBS/vzdump) cycle is gated to // [W+2h, W+6h). Offsets are constants here, never persisted and never surfaced in the UI. package backupwindow import "fmt" // DefaultWindow is the last-resort window when neither settings nor controller.yaml supplies one. // It equals the historical hardcoded DB-dump time, so an un-configured box behaves exactly as before. const DefaultWindow = "02:30" // Fixed leg offsets from the window start W (minutes). NEVER stored, NEVER exposed in the UI — // changing spacing/ordering is a code change here, not customer data. const ( tier2OffsetMin = 60 // tier-2 mirror at W+60m offboxOffsetMin = 105 // off-box copy at W+105m gateStartMin = 120 // whole-guest gate opens at W+2h gateEndMin = 360 // whole-guest gate closes (exclusive) at W+6h ) // ParseHHMM parses "HH:MM" (24h) into minutes-since-midnight. It rejects anything but a valid // hour:minute — the same contract as the scheduler's parseDailyTime, kept here so this package is // dependency-free and reusable by the quiesce gate. func ParseHHMM(s string) (int, error) { var h, m int n, err := fmt.Sscanf(s, "%d:%d", &h, &m) if err != nil || n != 2 { return 0, fmt.Errorf("expected HH:MM format, got %q", s) } if h < 0 || h > 23 || m < 0 || m > 59 { return 0, fmt.Errorf("invalid time %q: hour must be 0-23, minute 0-59", s) } return h*60 + m, nil } // FmtHHMM renders minutes-since-midnight back to "HH:MM", wrapping across midnight (modulo 24h) so // derived legs past 23:59 read correctly (e.g. 23:30 + 60m → 00:30). func FmtHHMM(minutes int) string { minutes = ((minutes % 1440) + 1440) % 1440 return fmt.Sprintf("%02d:%02d", minutes/60, minutes%60) } // Valid reports whether s is a well-formed HH:MM window value (nil error = valid). func Valid(s string) error { _, err := ParseHHMM(s) return err } // LegTimes returns the three derived nightly-leg times (db=W, tier2=W+60m, offbox=W+105m), // wrap-safe across midnight. On an invalid start it returns three empty strings — callers pass a // value already resolved through EffectiveWindow, which never yields an invalid string. func LegTimes(start string) (db, tier2, offbox string) { m, err := ParseHHMM(start) if err != nil { return "", "", "" } return FmtHHMM(m), FmtHHMM(m + tier2OffsetMin), FmtHHMM(m + offboxOffsetMin) } // GateWindow returns the whole-guest backup gate bounds [W+2h, W+6h) as HH:MM strings (for the UI // "kb. között" line and the gate-denial log). Empty strings on an invalid start. func GateWindow(start string) (from, to string) { m, err := ParseHHMM(start) if err != nil { return "", "" } return FmtHHMM(m + gateStartMin), FmtHHMM(m + gateEndMin) } // EffectiveWindow resolves the active window by precedence: a valid settings value wins over a valid // controller.yaml value, which wins over DefaultWindow. An empty or corrupted value simply falls // through — so a bad settings string degrades to the yaml default rather than breaking scheduling. func EffectiveWindow(settingsVal, yamlVal string) string { if Valid(settingsVal) == nil { return settingsVal } if Valid(yamlVal) == nil { return yamlVal } return DefaultWindow }