Post-repair hardware validation pipeline for Proxmox cluster hosts. Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// Package bootstate parses kernel cmdline parameters that the
|
||||
// orchestrator baked into the iPXE script. The agent consumes these
|
||||
// on startup to learn which run it belongs to and how to reach back.
|
||||
package bootstate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
OrchestratorURL string
|
||||
RunID int64
|
||||
MAC string
|
||||
Token string
|
||||
TLSCertFPR string // optional
|
||||
}
|
||||
|
||||
// ParseCmdline reads /proc/cmdline (or a user-supplied path for tests)
|
||||
// and pulls out the vetting.* parameters.
|
||||
func ParseCmdline(path string) (*Params, error) {
|
||||
if path == "" {
|
||||
path = "/proc/cmdline"
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
return ParseCmdlineString(string(b))
|
||||
}
|
||||
|
||||
func ParseCmdlineString(s string) (*Params, error) {
|
||||
fields := strings.Fields(strings.TrimSpace(s))
|
||||
var p Params
|
||||
for _, f := range fields {
|
||||
k, v, ok := strings.Cut(f, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "vetting.orchestrator":
|
||||
p.OrchestratorURL = v
|
||||
case "vetting.run_id":
|
||||
id, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vetting.run_id=%q: %w", v, err)
|
||||
}
|
||||
p.RunID = id
|
||||
case "vetting.mac":
|
||||
p.MAC = strings.ToLower(v)
|
||||
case "vetting.token":
|
||||
p.Token = v
|
||||
case "vetting.cert_fpr":
|
||||
p.TLSCertFPR = v
|
||||
}
|
||||
}
|
||||
if p.OrchestratorURL == "" || p.RunID == 0 || p.MAC == "" || p.Token == "" {
|
||||
return nil, errors.New("cmdline missing one of vetting.orchestrator, vetting.run_id, vetting.mac, vetting.token")
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package bootstate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCmdlineGoldenPath(t *testing.T) {
|
||||
s := `BOOT_IMAGE=vmlinuz initrd=initrd.img vetting.orchestrator=http://10.0.0.5:8080 vetting.run_id=42 vetting.mac=aa:bb:cc:dd:ee:ff vetting.token=deadbeefcafe vetting.cert_fpr=abc123 console=ttyS0,115200n8 quiet`
|
||||
p, err := ParseCmdlineString(s)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCmdlineString: %v", err)
|
||||
}
|
||||
if p.OrchestratorURL != "http://10.0.0.5:8080" || p.RunID != 42 || p.MAC != "aa:bb:cc:dd:ee:ff" ||
|
||||
p.Token != "deadbeefcafe" || p.TLSCertFPR != "abc123" {
|
||||
t.Fatalf("parsed wrong: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCmdlineMissingRequired(t *testing.T) {
|
||||
s := `vetting.orchestrator=http://x vetting.mac=aa:bb:cc:dd:ee:ff vetting.token=t`
|
||||
if _, err := ParseCmdlineString(s); err == nil {
|
||||
t.Fatalf("expected error when vetting.run_id missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCmdlineLowercasesMAC(t *testing.T) {
|
||||
s := `vetting.orchestrator=http://x vetting.run_id=1 vetting.mac=AA:BB:CC:DD:EE:FF vetting.token=t`
|
||||
p, err := ParseCmdlineString(s)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCmdlineString: %v", err)
|
||||
}
|
||||
if p.MAC != "aa:bb:cc:dd:ee:ff" {
|
||||
t.Fatalf("MAC not lowercased: %q", p.MAC)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user