ce1a4b4758
The wrapper gains layer "pve" (slow lane): the host's Proxmox userspace packages only — origin "Proxmox Debian Repository", never a kernel / boot / firmware / microcode name (R14), no removal, no undo, a new package only from an allow-list; authority = a signed os_pve_step or the root-owned ring-0 mark. The night leg runs it in ring 0 after a healthy host step; ring 1 only by a signed job (PVEStepExecutor). While it runs, the agent's own /etc/pve writes (every non-GET API call, pct config verbs, pvesm, pveum, felhom-pbs-apply) wait on internal/pvegate. Health = the host rule + unchanged container ids + pveversion reads the installed pve-manager. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
package osupdate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/signedjobs"
|
|
)
|
|
|
|
// OpPVEStep is the signed op class of a Proxmox package step (R-812 option A, `11` §5.10): a ring-1 box takes an
|
|
// approved Proxmox set only through it. No undo in this release. CC may sign it until the first paying customer.
|
|
const OpPVEStep = "os_pve_step"
|
|
|
|
// PVEStepParams are the signed params. The wrapper compares Packages with the plan byte-for-byte.
|
|
type PVEStepParams struct {
|
|
ReleaseID string `json:"release_id"`
|
|
Packages []Package `json:"packages"`
|
|
VMID int `json:"vmid,omitempty"`
|
|
}
|
|
|
|
// PVEStepExecutor runs a verified os_pve_step (signedjobs.Executor) under the host-wide heavy-op gate (Gate) and the
|
|
// /etc/pve write gate (inside runPVE).
|
|
type PVEStepExecutor struct {
|
|
Leg *Leg
|
|
Guest func(ctx context.Context) (int, error)
|
|
Gate func(ctx context.Context) (release func(), err error)
|
|
}
|
|
|
|
// Execute implements signedjobs.Executor.
|
|
func (e PVEStepExecutor) Execute(ctx context.Context, op string, params json.RawMessage) error {
|
|
if op != OpPVEStep {
|
|
return signedjobs.ErrNoExecutor
|
|
}
|
|
so, ok := signedjobs.SignedOpFrom(ctx)
|
|
if !ok {
|
|
return fmt.Errorf("os_pve_step: no signed envelope in the context — the wrapper could not verify it")
|
|
}
|
|
var p PVEStepParams
|
|
if err := json.Unmarshal(params, &p); err != nil || len(p.Packages) == 0 {
|
|
return fmt.Errorf("os_pve_step: params must name the Proxmox set: %v", err)
|
|
}
|
|
vmid := p.VMID
|
|
if vmid == 0 {
|
|
if e.Guest == nil {
|
|
return fmt.Errorf("os_pve_step: no vmid and no guest finder")
|
|
}
|
|
v, err := e.Guest(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("os_pve_step: find the customer guest: %w", err)
|
|
}
|
|
vmid = v
|
|
}
|
|
if e.Gate != nil {
|
|
release, err := e.Gate(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("os_pve_step: heavy-op gate busy (a backup or restore-test runs): %w", err)
|
|
}
|
|
defer release()
|
|
}
|
|
rep := e.Leg.RunPVESigned(ctx, vmid, p, so.Blob, string(so.Sig))
|
|
switch rep.Outcome {
|
|
case "applied", "nothing":
|
|
if rep.Healthy {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("os_pve_step: %s (%s) %s", rep.Outcome, rep.HealthReason, string(rep.Refused))
|
|
}
|
|
|
|
// RunPVESigned is one signed Proxmox step (ring 1): the pve layer with the signed envelope, which the wrapper verifies
|
|
// itself, holding the /etc/pve write gate.
|
|
func (l *Leg) RunPVESigned(ctx context.Context, vmid int, p PVEStepParams, blob []byte, sig string) Report {
|
|
unlock := l.lockPass(true)
|
|
defer unlock()
|
|
l.sendUnsentLocked(ctx) // R-868
|
|
runID := l.now().UTC().Format("20060102T150405Z")
|
|
rid := p.ReleaseID
|
|
if rid == "" {
|
|
rid = "signed-" + runID
|
|
}
|
|
return l.runPVE(ctx, runID, vmid, "signed", l.Block(), dockerOpts{releaseID: rid, packages: p.Packages,
|
|
signed: map[string]string{"blob_b64": base64.StdEncoding.EncodeToString(blob), "sig": sig}})
|
|
}
|