v0.53.0: restore guests INTO the felhom pool (pool-scoped-ACL enabler)

RestoreLXCOptions.Pool → pct restore --pool (omit-when-empty). New
reconcile.DefaultPool="felhom"; BringUpSpec.Pool threaded to the bring-up
restore; BOTH restore sites pool the guest (provision/DR via spec.Pool set to
DefaultPool by the CLI; restore-test scratch via DefaultPool = SPIKE residual
#2). No agent ACL change (ships in host-install v1.6.0); the pool param is inert
until the token has Pool.Allocate + the pool exists, so publishing is safe ahead
of the coordinated swap. Tests + red-proofs; go build/vet/test clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 12:32:41 +02:00
parent 33e3443545
commit 7639ab5c4b
8 changed files with 129 additions and 3 deletions
+8
View File
@@ -34,6 +34,11 @@ type RestoreLXCOptions struct {
Archive string // source archive volid, e.g. "local:backup/vzdump-lxc-9001-...tar.zst"
Storage string // target storage for the rootfs, e.g. "local-lvm"
Force bool // overwrite an existing VMID (destructive — caller must have authority)
// Pool allocates the restored guest INTO a PVE pool (pct restore --pool). "" = no pool. Under a
// pool-scoped token this is REQUIRED for the restore to authorize (VM.Allocate + Pool.Allocate are
// granted at /pool/<pool>, not /), and it makes the guest reachable by the scoped token afterwards
// (SPIKE-pool-scoped-acl-2026-07-01). Empty is valid — a broad-token restore needs no pool.
Pool string
// MountOverrides overrides specific mountpoints at restore time (mpN -> full value, e.g.
// "local-lvm:1,mp=/data,backup=0"). A restore param takes precedence over the archive's own
// mpN. The restore-test uses this to neutralize a SOURCE host bind-mount mountpoint (slice-10
@@ -59,6 +64,9 @@ func (c *Client) RestoreLXC(ctx context.Context, opts RestoreLXCOptions) (string
if opts.Force {
v.Set("force", "1")
}
if opts.Pool != "" {
v.Set("pool", opts.Pool) // allocate into the pool (pool-scoped token needs this — see RestoreLXCOptions.Pool)
}
for k, val := range opts.MountOverrides {
v.Set(k, val) // e.g. mp0 -> "local-lvm:1,mp=/data,backup=0" (overrides the archive's mp0)
}
+39
View File
@@ -0,0 +1,39 @@
package proxmox
import (
"context"
"io"
"net/http"
"strings"
"testing"
)
// RestoreLXC sends pool= only when Pool is set. Under a pool-scoped token the restore MUST carry
// pool=felhom (else VM.Allocate/Pool.Allocate 403); with Pool empty it MUST be omitted (a naive
// unconditional Set would send an empty pool= — omit-when-empty is the guarantee).
func TestRestoreLXC_PoolParam(t *testing.T) {
var body string
d := &mockDoer{fn: func(r *http.Request) (*http.Response, error) {
b, _ := io.ReadAll(r.Body)
body = string(b)
return jsonResp(200, `{"data":"`+testUPID+`"}`), nil
}}
c := newTestClient(d)
ctx := context.Background()
// Pool set → pool=felhom present in the POST body.
if _, err := c.RestoreLXC(ctx, RestoreLXCOptions{VMID: 9100, Archive: "local:backup/a.tar.zst", Storage: "local-lvm", Pool: "felhom"}); err != nil {
t.Fatalf("RestoreLXC(Pool): %v", err)
}
if !strings.Contains(body, "pool=felhom") {
t.Errorf("Pool set: expected pool=felhom in body, got %q", body)
}
// Pool empty → NO pool key at all (omit-when-empty).
if _, err := c.RestoreLXC(ctx, RestoreLXCOptions{VMID: 9100, Archive: "local:backup/a.tar.zst", Storage: "local-lvm"}); err != nil {
t.Fatalf("RestoreLXC(no Pool): %v", err)
}
if strings.Contains(body, "pool=") {
t.Errorf("Pool empty: pool key must be ABSENT, got %q", body)
}
}