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}}) }