Files
felhom-controller/controller/internal/backupwindow/backupwindow.go
T

95 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
)
// UpdateLegStopOffsetMin is W+5h: the automatic update leg starts no step at or after it, and the
// full-system backup's gate stops waiting for the leg there, so that backup keeps at least one hour of
// its [W+2h, W+6h) window (`09` §3 decision 20, v0.271.0). Read by stacks.LegDeadline AND the quiesce
// gate — one constant, so the two can never disagree (TestLegDeadlineAndGateShareW5h).
const UpdateLegStopOffsetMin = 300
// UpdateLegLengthMin is how long the leg may start steps on a normal night: from the off-site leg (W+105m),
// after which it is chained, to W+5h. A MANUAL run of the night's chain (R-705) gives the leg this same
// length, counted from when it starts.
const UpdateLegLengthMin = UpdateLegStopOffsetMin - offboxOffsetMin
// 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. <from>–<to> 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
}