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
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
// Package pvegate keeps the agent's own writes to /etc/pve out of the way of a Proxmox package step (R-812 option A,
|
|
// `09` §3 decision 163, `11` §5.10).
|
|
//
|
|
// WHY. A `pve` step upgrades pve-cluster / pve-manager / qemu-server / pve-container; their postinst scripts restart
|
|
// pmxcfs (the FUSE filesystem behind /etc/pve) and the API daemons. A write that lands while pmxcfs restarts fails or,
|
|
// worse, half-lands (design-R-812 §3 A, "can go wrong"). Backups and restore-tests are already kept out by the
|
|
// host-wide heavy-op gate; this gate covers everything else the agent writes: every non-GET Proxmox API call
|
|
// (proxmox.Client.doBody) and every root CLI that writes /etc/pve (proxmox.ExecRunner — `pct set|create|…`, `pvesm`,
|
|
// `pveum`, `felhom-pbs-apply create|reconcile`).
|
|
//
|
|
// THE RULE. Write waits while a step runs (bounded by its own context). Step marks the step and then waits until every
|
|
// write already in flight has finished; it never waits forever (its context bounds it, and the caller gives up and
|
|
// does not run the step). One step at a time. Pinned by pvegate_test.go and, at the two chokepoints, by
|
|
// proxmox TestPVEGate_*.
|
|
package pvegate
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
inFlight int
|
|
stepping bool
|
|
stepDone chan struct{}
|
|
)
|
|
|
|
// ErrStepRunning is returned by Step when another step already holds the gate.
|
|
var ErrStepRunning = errors.New("pvegate: a Proxmox package step is already running")
|
|
|
|
// Write marks one /etc/pve write in flight, first waiting while a Proxmox package step runs. The returned release must
|
|
// be called when the write has finished. waited reports how long the write was held back.
|
|
func Write(ctx context.Context) (release func(), waited time.Duration, err error) {
|
|
start := time.Now()
|
|
for {
|
|
mu.Lock()
|
|
if !stepping {
|
|
inFlight++
|
|
mu.Unlock()
|
|
var once sync.Once
|
|
return func() {
|
|
once.Do(func() {
|
|
mu.Lock()
|
|
inFlight--
|
|
mu.Unlock()
|
|
})
|
|
}, time.Since(start), nil
|
|
}
|
|
ch := stepDone
|
|
mu.Unlock()
|
|
select {
|
|
case <-ch:
|
|
case <-ctx.Done():
|
|
return nil, time.Since(start), ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step marks a Proxmox package step and waits until every /etc/pve write already in flight has finished. On error the
|
|
// gate is released again and the step must not run. end releases the gate and lets the held-back writes go.
|
|
func Step(ctx context.Context) (end func(), err error) {
|
|
mu.Lock()
|
|
if stepping {
|
|
mu.Unlock()
|
|
return nil, ErrStepRunning
|
|
}
|
|
stepping = true
|
|
done := make(chan struct{})
|
|
stepDone = done
|
|
mu.Unlock()
|
|
var once sync.Once
|
|
end = func() {
|
|
once.Do(func() {
|
|
mu.Lock()
|
|
stepping = false
|
|
close(done)
|
|
mu.Unlock()
|
|
})
|
|
}
|
|
for {
|
|
mu.Lock()
|
|
n := inFlight
|
|
mu.Unlock()
|
|
if n == 0 {
|
|
return end, nil
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
end()
|
|
return nil, ctx.Err()
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stepping reports whether a Proxmox package step holds the gate (for logs).
|
|
func Stepping() bool {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return stepping
|
|
}
|