hub v0.130.0: OS updates, guest fast lane — rings, per-box switch, OS releases approved from ring 0, os-report, os_update desired block

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-04 10:56:39 +02:00
parent 6ed79cd2e9
commit c5f91174f6
25 changed files with 1338 additions and 2 deletions
+52
View File
@@ -25,6 +25,7 @@ import (
"gitea.dooplex.hu/admin/felhom-hub/internal/offsite"
"gitea.dooplex.hu/admin/felhom-hub/internal/offsiteheal"
"gitea.dooplex.hu/admin/felhom-hub/internal/offsitekeys"
"gitea.dooplex.hu/admin/felhom-hub/internal/osupdates"
"gitea.dooplex.hu/admin/felhom-hub/internal/pbsdrheal"
"gitea.dooplex.hu/admin/felhom-hub/internal/poke"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
@@ -384,6 +385,57 @@ func main() {
// unconfigured or the customer has no offsite tier.
apiHandler.SetOffsiteReissuer(webServer.ReissueOffsiteForCustomer)
// OS updates, guest fast lane (hub v0.130.0, `11` §8 step 2). A release is approved when every ring-0 box runs the
// set, healthy, for OS_APPROVE_AFTER (default 24h) and through OS_APPROVE_NIGHTS night runs (default 1) — the
// ruled "1–2 day wait". Either override is a TEST configuration and is logged loudly.
osSvc := &osupdates.Service{Store: dataStore, Emit: dispatcher.ProcessEvent, Logger: logger,
ApproveAfter: 24 * time.Hour, NightsRequired: 1,
Bump: func(hostID string) {
if _, err := dataStore.BumpHostDesired(hostID); err != nil {
logger.Printf("[WARN] osupdates: bump desired for %s: %v", hostID, err)
}
}}
if v := os.Getenv("OS_APPROVE_AFTER"); v != "" {
if d, derr := time.ParseDuration(v); derr == nil && d >= 0 {
osSvc.ApproveAfter = d
logger.Printf("[WARN] OS_APPROVE_AFTER=%s — OS releases approve after %s instead of 24h (TEST CONFIGURATION)", v, d)
} else {
logger.Printf("[ERROR] OS_APPROVE_AFTER=%q invalid — keeping 24h", v)
}
}
if v := os.Getenv("OS_APPROVE_NIGHTS"); v != "" {
if n, nerr := strconv.Atoi(v); nerr == nil && n >= 0 {
osSvc.NightsRequired = n
logger.Printf("[WARN] OS_APPROVE_NIGHTS=%s — OS releases need %d night run(s) instead of 1 (TEST CONFIGURATION)", v, n)
} else {
logger.Printf("[ERROR] OS_APPROVE_NIGHTS=%q invalid — keeping 1", v)
}
}
logger.Printf("[INFO] osupdates: approval rule = every ring-0 box healthy for %s and %d night run(s)", osSvc.ApproveAfter, osSvc.NightsRequired)
apiHandler.SetOSUpdateService(osSvc)
webServer.SetOSUpdateAdmin(osSvc)
go func() {
tk := time.NewTicker(60 * time.Second)
defer tk.Stop()
last := ""
for {
select {
case <-ctx.Done():
return
case <-tk.C:
st, err := osSvc.Evaluate()
if err != nil {
logger.Printf("[WARN] osupdates: evaluate: %v", err)
continue
}
if msg := st.Fingerprint + "|" + st.Waiting; msg != last {
last = msg
logger.Printf("[INFO] osupdates: candidate %s (%d packages, first seen %s): %s", st.Fingerprint, st.Packages, st.FirstSeen.UTC().Format(time.RFC3339), map[bool]string{true: "approved / nothing to wait for", false: "waiting — " + st.Waiting}[st.Waiting == ""])
}
}
}
}()
// Decision 69 (v0.127.0): the off-site KEY REGISTRAR. The box sends its public key; the hub writes it
// into the sub-account's authorized_keys pinned append-only; the daily check reads every file.
if offsiteKeyReady {