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