9bdb4dae8f
gates / gates (push) Successful in 13s
Space preflight before anything is created (uncompressed size from the vzdump log / PBS snapshot, x1.2 + 5 GiB, thin metadata, off the tested guest's pool when another storage is eligible, unknown refuses, reported as a non-pass result). Failed scratch teardown and the stale-lock sweep retried every 10 min (the sweep under the heavy-op gate). A thin pool crossing 90% requests an immediate host report. Six red-proofs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
134 lines
5.5 KiB
Go
134 lines
5.5 KiB
Go
package restorespace
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"testing"
|
||
|
||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||
)
|
||
|
||
// R-672 (v0.133.0). The provider behind the restore-test's space preflight. No test reaches a real
|
||
// Proxmox or a real file: the API and ReadFile are fakes.
|
||
|
||
type fakeAPI struct {
|
||
cfg []proxmox.Storage
|
||
live []proxmox.Storage
|
||
content map[string][]proxmox.StorageContent
|
||
perms map[string]map[string]int
|
||
}
|
||
|
||
func (f fakeAPI) ListStorage(context.Context) ([]proxmox.Storage, error) { return f.cfg, nil }
|
||
func (f fakeAPI) NodeStorage(context.Context) ([]proxmox.Storage, error) { return f.live, nil }
|
||
func (f fakeAPI) StorageContent(_ context.Context, s string) ([]proxmox.StorageContent, error) {
|
||
return f.content[s], nil
|
||
}
|
||
func (f fakeAPI) Permissions(_ context.Context, p string) (map[string]int, error) {
|
||
if m, ok := f.perms[p]; ok {
|
||
return m, nil
|
||
}
|
||
return map[string]int{}, nil
|
||
}
|
||
|
||
const archive = "local:backup/vzdump-lxc-9201-2026_09_23-06_55_25.tar.zst"
|
||
|
||
// The real log's tail (demo-hp, 2026-09-23): 22.6 GB written, a 6.91 GB archive file.
|
||
const vzdumpLog = "2026-09-23 07:02:47 INFO: Total bytes written: 22607360000 (22GiB, 49MiB/s)\n2026-09-23 07:02:47 INFO: archive file size: 6.91GB\n"
|
||
|
||
// demoHP is demo-hp's storage layout: `local` (dir, backups), `local-lvm` (thin), `nvme-scratch` (dir,
|
||
// rootdir — but the agent holds NO grant there), a pbs.
|
||
func demoHP() fakeAPI {
|
||
return fakeAPI{
|
||
cfg: []proxmox.Storage{
|
||
{Storage: "local", Type: "dir", Path: "/var/lib/vz"},
|
||
{Storage: "local-lvm", Type: "lvmthin", VGName: "pve", ThinPool: "data"},
|
||
{Storage: "nvme-scratch", Type: "dir", Path: "/mnt/hdd_1"},
|
||
{Storage: "felhom-pbs", Type: "pbs"},
|
||
},
|
||
live: []proxmox.Storage{
|
||
{Storage: "local", Type: "dir", Content: "vztmpl,backup,iso,import", Active: 1, Avail: 4 << 30, Used: 34 << 30},
|
||
{Storage: "local-lvm", Type: "lvmthin", Content: "images,rootdir", Active: 1, Avail: 23210892 * 1024, Used: 33277043 * 1024},
|
||
{Storage: "nvme-scratch", Type: "dir", Content: "images,rootdir", Active: 1, Avail: 800 << 30},
|
||
{Storage: "felhom-pbs", Type: "pbs", Content: "backup", Active: 0},
|
||
},
|
||
content: map[string][]proxmox.StorageContent{
|
||
"local": {{VolID: archive, Size: 7417540996}},
|
||
"felhom-pbs": {{VolID: "felhom-pbs:backup/ct/9201/2026-09-23T02:00:00Z", Size: 21 << 30}},
|
||
},
|
||
perms: map[string]map[string]int{
|
||
"/storage/local": {"Datastore.Audit": 1, "Datastore.AllocateSpace": 1},
|
||
"/storage/local-lvm": {"Datastore.Audit": 1, "Datastore.AllocateSpace": 1},
|
||
// nvme-scratch: only the inherited box-wide Datastore.Audit — the trap proxmox.Permissions names.
|
||
"/storage/nvme-scratch": {"Datastore.Audit": 1},
|
||
},
|
||
}
|
||
}
|
||
|
||
// TestRestoredBytes_ReadsTheUncompressedSize — the vzdump log's "Total bytes written", never the archive
|
||
// FILE size (6.9 GB for a 22.6 GB restore).
|
||
//
|
||
// COMPANION RED-PROOF (REPORT): return the storage content's Size for a dir storage → 7417540996, and
|
||
// this test fails at "the compressed file size was used".
|
||
func TestRestoredBytes_ReadsTheUncompressedSize(t *testing.T) {
|
||
var asked string
|
||
p := &Provider{API: demoHP(), ReadFile: func(n string) ([]byte, error) { asked = n; return []byte(vzdumpLog), nil }}
|
||
n, src, err := p.RestoredBytes(context.Background(), archive)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if n == 7417540996 {
|
||
t.Fatal("the compressed file size was used — the restore writes 3× that")
|
||
}
|
||
if n != 22607360000 || asked != "/var/lib/vz/dump/vzdump-lxc-9201-2026_09_23-06_55_25.log" {
|
||
t.Fatalf("n=%d from %q (log %q)", n, src, asked)
|
||
}
|
||
}
|
||
|
||
func TestRestoredBytes_UnknownIsAnError(t *testing.T) {
|
||
cases := map[string]*Provider{
|
||
"no log": {API: demoHP(), ReadFile: func(string) ([]byte, error) { return nil, errors.New("ENOENT") }},
|
||
"log without size": {API: demoHP(), ReadFile: func(string) ([]byte, error) { return []byte("ERROR: failed\n"), nil }},
|
||
}
|
||
for name, p := range cases {
|
||
if n, _, err := p.RestoredBytes(context.Background(), archive); err == nil {
|
||
t.Fatalf("%s: got %d, want an error (the preflight then refuses)", name, n)
|
||
}
|
||
}
|
||
if _, _, err := (&Provider{API: demoHP()}).RestoredBytes(context.Background(), "local:backup/weird.vma"); err == nil {
|
||
t.Fatal("an unknown archive suffix must be an error")
|
||
}
|
||
}
|
||
|
||
func TestRestoredBytes_PBS(t *testing.T) {
|
||
p := &Provider{API: demoHP()}
|
||
n, _, err := p.RestoredBytes(context.Background(), "felhom-pbs:backup/ct/9201/2026-09-23T02:00:00Z")
|
||
if err != nil || n != 21<<30 {
|
||
t.Fatalf("n=%d err=%v", n, err)
|
||
}
|
||
}
|
||
|
||
// TestEligible_NeedsTheSpecificGrant — nvme-scratch takes rootdir but the agent holds only the inherited
|
||
// Datastore.Audit there, so it is NOT eligible; `local` holds no rootdir.
|
||
func TestEligible_NeedsTheSpecificGrant(t *testing.T) {
|
||
got, err := (&Provider{API: demoHP()}).Eligible(context.Background())
|
||
if err != nil || len(got) != 1 || got[0] != "local-lvm" {
|
||
t.Fatalf("eligible = %v (%v) — want only local-lvm on demo-hp", got, err)
|
||
}
|
||
}
|
||
|
||
func TestFree_ThinCarriesMetadata(t *testing.T) {
|
||
p := &Provider{API: demoHP(), ThinMeta: func(_ context.Context, vg, pool string) (float64, bool) {
|
||
if vg != "pve" || pool != "data" {
|
||
t.Fatalf("metadata read for %s/%s", vg, pool)
|
||
}
|
||
return 0.0265, true
|
||
}}
|
||
fr, err := p.Free(context.Background(), "local-lvm")
|
||
if err != nil || !fr.Thin || !fr.MetaKnown || fr.MetaUsedFraction != 0.0265 || fr.AvailBytes != 23210892*1024 {
|
||
t.Fatalf("free = %+v err=%v", fr, err)
|
||
}
|
||
if _, err := p.Free(context.Background(), "felhom-pbs"); err == nil {
|
||
t.Fatal("an inactive storage must be an error")
|
||
}
|
||
}
|