Files
felhom-controller/controller/internal/backupwindow/backupwindow.go
T
admin 82c67e32e1 v0.168.0: customer-configurable backup window (Mentési időablak)
ONE setting (window start W) drives every nightly leg at fixed, never-stored
offsets: DB dump at W, tier-2 at W+60m, off-box at W+105m (wrap-safe). Precedence
settings > controller.yaml db_dump_schedule > 02:30.

- scheduler.UpdateDaily: retime a daily job at runtime (no restart) via a per-job
  buffered resched chan + a select case in runDailyJob.
- new pure package internal/backupwindow (LegTimes/GateWindow/EffectiveWindow).
- quiesce disk-tier window gate: scheduled cycles run only inside [W+2h,W+6h) with a
  safety valve (age>cadence+24h runs regardless); manual TriggerNow never gated.
  Backend.Due now also returns the backup age (from the agent's own /backup/due).
- backup page: Mentési időablak card (time input + derived leg/gate rows); POST
  /backups/window validates -> saves -> UpdateDaily x3 -> flash.

Tests: 5 groups, all red-proofed. Agent/cadence//backup/due untouched.
2026-07-24 20:55:44 +02:00

84 lines
3.5 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
)
// 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
}