Files
felhom.eu/hub/internal/osupdates/pve_test.go
T
admin e7fb10200e R-812 option A (hub): the Proxmox package set — candidate, operator approval, System page
Layer pve: never auto-approved; the candidate is the Proxmox userspace set
every ring-0 box reports (kernel / boot / firmware names left out); the
operator's "Approve Proxmox set" button appears only after 2 healthy night
pve steps on every ring-0 box; an approval nudges no box (ring 1 by a signed
os_pve_step). 11 §5.10 written (BUILT, unreleased, not yet proven live); §8
step 6 split (userspace §5.10, kernel R-836).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-07 10:21:25 +02:00

98 lines
3.4 KiB
Go

package osupdates
import (
"strings"
"testing"
"time"
)
// R-812 option A (`09` §3 decision 163): the Proxmox package set — approved only by the operator's button, after every
// ring-0 box ran it in healthy night steps (the host health rule, judged on the box), never a kernel, never pushed to
// ring 1 by the desired state (a signed os_pve_step only).
var pveSet = []Package{{Name: "pve-manager", Version: "9.2.21", Origin: "Proxmox"},
{Name: "qemu-server", Version: "9.0.9", Origin: "Proxmox"}}
func (f *fix) pveNight(t *testing.T, host string, healthy bool) {
t.Helper()
out := "nothing"
if !healthy {
out = "health_failed"
}
f.ingest(t, host, Report{Layer: LayerPVE, Trigger: "night", Mode: "apply", Outcome: out, Healthy: healthy,
Installed: append([]Package{pk("libc6", "x"), {Name: "proxmox-kernel-helper", Version: "9.0.6", Origin: "Proxmox"}}, pveSet...)})
}
// COMPANION RED-PROOF (observed): add LayerPVE to Layers (the auto-approved list) → this fails.
func TestPVE_NeverAutoApproved(t *testing.T) {
f := newFix(t)
for i := 0; i < 3; i++ {
f.pveNight(t, "hp", true)
f.pveNight(t, "n100", true)
f.now = f.now.Add(25 * time.Hour)
f.s.Evaluate()
}
if rel, _ := f.s.Store.LatestOSRelease(LayerPVE); rel != nil {
t.Fatalf("a Proxmox set was auto-approved: %+v", rel)
}
}
// The button works only after two healthy nights on every ring-0 box; the release holds Proxmox userspace only (no
// kernel name, no Debian package) and nudges no box.
//
// COMPANION RED-PROOF (observed): drop the hostSlowRE check for the pve layer in candidate → "proxmox-kernel-helper".
func TestPVE_ApproveNeedsTwoHealthyNightsOnEveryRing0Box(t *testing.T) {
f := newFix(t)
f.pveNight(t, "hp", true)
f.pveNight(t, "n100", true)
if _, err := f.s.ApprovePVE(); err == nil || !strings.Contains(err.Error(), "1 of 2") {
t.Fatalf("approved after one night: %v", err)
}
f.now = f.now.Add(24 * time.Hour)
f.pveNight(t, "hp", true)
f.pveNight(t, "n100", true)
id, err := f.s.ApprovePVE()
if err != nil || !strings.HasPrefix(id, "os-pve-") {
t.Fatalf("%q %v", id, err)
}
rel, _ := f.s.Store.LatestOSRelease(LayerPVE)
if rel.ApprovedBy != "operator" || !strings.Contains(rel.PackagesJSON, "pve-manager") ||
strings.Contains(rel.PackagesJSON, "libc6") || strings.Contains(rel.PackagesJSON, "proxmox-kernel") {
t.Fatalf("release %+v", rel)
}
if len(f.bumps) != 0 {
t.Fatalf("a Proxmox approval must nudge no box (ring 1 takes it by a signed job): %v", f.bumps)
}
if b := f.s.DesiredBlock("cust1"); b.Release != nil || b.HostRelease != nil {
t.Fatalf("the Proxmox set leaked into the desired block: %+v", b)
}
}
func TestPVE_UnhealthyStepBlocksTheButton(t *testing.T) {
f := newFix(t)
f.pveNight(t, "hp", true)
f.pveNight(t, "n100", true)
f.now = f.now.Add(24 * time.Hour)
f.pveNight(t, "hp", false)
f.pveNight(t, "n100", true)
if _, err := f.s.ApprovePVE(); err == nil || !strings.Contains(err.Error(), "health_failed") {
t.Fatalf("an unhealthy Proxmox step did not block: %v", err)
}
}
// The System page lists the Proxmox candidate beside the Docker one.
func TestPVE_InTheCandidates(t *testing.T) {
f := newFix(t)
f.pveNight(t, "hp", true)
f.pveNight(t, "n100", true)
found := false
for _, c := range f.s.Candidates() {
if c.Layer == LayerPVE && c.Packages == 2 {
found = true
}
}
if !found {
t.Fatalf("no pve candidate with 2 packages: %+v", f.s.Candidates())
}
}