vdi-cloud/platform/internal/server/gpu_test.go

182 lines
6.7 KiB
Go

package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"vdi-cloud/platform/pkg/proxmox"
)
func fakeGPUPVE(t *testing.T, f *fixture, mode string) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("GPU endpoint attempted write: %s %s", r.Method, r.URL.Path)
w.WriteHeader(500)
return
}
path := strings.TrimPrefix(r.URL.Path, "/api2/json")
var data any
switch path {
case "/cluster/resources":
if mode == "cluster-failed" {
w.WriteHeader(503)
return
}
data = []map[string]any{{"type": "node", "node": "node-a", "name": "node-a", "status": "online"}, {"type": "qemu", "node": "node-a", "vmid": 8000, "name": "managed-windows", "status": "stopped"}, {"type": "qemu", "node": "node-a", "vmid": 8001, "name": "unmanaged-ubuntu", "status": "running"}}
case "/nodes/node-a/hardware/pci":
if mode == "pci-denied" {
w.WriteHeader(403)
return
}
data = []map[string]any{{"id": "0000:00:02.0", "class": "0x030000", "device_name": "Intel GPU", "vendor_name": "Intel", "iommugroup": 0}, {"id": "0000:01:00.0", "class": "0x030200", "device_name": "NVIDIA GPU", "vendor": "0x10de", "iommugroup": 2}, {"id": "0000:01:00.1", "class": "0x040300", "device_name": "Audio"}, {"id": "0000:02:00.0", "class": "0x030000", "device_name": "AMD GPU", "iommugroup": -1}}
case "/access/permissions":
allowed := 1
if mode == "partial-visibility" {
allowed = 0
}
data = map[string]any{"/": map[string]int{"VM.Audit": allowed}}
case "/nodes/node-a/qemu/8000/pending":
value := "01:00,pcie=1"
if mode == "mapping" || mode == "mapping-missing" || mode == "mapping-candidates" {
value = "mapping=gpu-map"
}
if mode == "malformed" {
value = "host=invalid,SECRET=do-not-return"
}
rows := []map[string]any{{"key": "hostpci0", "value": value, "pending": "host=02:00.0", "delete": 1}}
if mode == "custom" {
rows = append(rows, map[string]any{"key": "args", "value": "-device vfio-pci,host=00:02.0 SECRET-do-not-return"})
}
data = rows
case "/nodes/node-a/qemu/8001/pending":
if mode == "config-failed" {
w.WriteHeader(403)
return
}
data = []map[string]any{}
if mode == "multiple" {
data = []map[string]any{{"key": "hostpci0", "value": "0000:01:00.0,mdev=test"}}
}
case "/cluster/mapping/pci":
if mode == "mapping-missing" {
w.WriteHeader(403)
return
}
entries := []string{"node=node-a,path=0000:01:00.0,id=10de:1234", "node=node-b,path=0000:03:00.0,id=10de:1234"}
if mode == "mapping-candidates" {
entries = append(entries, "node=node-a,path=0000:00:02.0,id=8086:1234")
}
data = []map[string]any{{"id": "gpu-map", "map": entries}}
default:
t.Errorf("unexpected GPU PVE read: %s", path)
http.NotFound(w, r)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": data})
}))
t.Cleanup(srv.Close)
f.server.PVE = &proxmox.Client{BaseURL: srv.URL, HTTP: srv.Client()}
}
func TestGPUInventoryReadOnlyCoverage(t *testing.T) {
for _, mode := range []string{"normal", "partial-visibility", "config-failed", "mapping", "mapping-missing", "mapping-candidates", "custom", "malformed", "multiple"} {
t.Run(mode, func(t *testing.T) {
f := newFixtureWorker(t, false)
fakeGPUPVE(t, f, mode)
if _, err := f.db.Exec("UPDATE desktops SET provider='proxmox',node='node-a',vmid=8000 WHERE id='desk_demo_windows'"); err != nil {
t.Fatal(err)
}
code, out := f.request("GET", "/resources/gpus?node=node-a", f.login("viewer"), nil, "")
expect(t, 200, code, out)
raw, _ := json.Marshal(out)
if strings.Contains(string(raw), "SECRET") {
t.Fatal("raw VM config leaked")
}
var view struct {
Complete bool
VisibleVMCount int
ScannedVMCount int
Items []gpuItem
Issues []gpuIssue
}
if err := json.Unmarshal(raw, &view); err != nil {
t.Fatal(err)
}
if len(view.Items) != 3 || view.VisibleVMCount != 2 || view.Items[0].IOMMUGroup == nil || *view.Items[0].IOMMUGroup != 0 || view.Items[2].IOMMUGroup != nil {
t.Fatalf("incorrect inventory %s", raw)
}
complete := mode == "normal" || mode == "mapping" || mode == "multiple"
if view.Complete != complete || (len(view.Issues) == 0) != complete {
t.Fatalf("coverage state %s", raw)
}
if complete && view.Items[0].State != "unreferenced" {
t.Fatalf("unreferenced classification %s", raw)
}
if mode == "config-failed" && view.ScannedVMCount != 1 {
t.Fatalf("failed config counted as scanned %s", raw)
}
if mode == "custom" || mode == "partial-visibility" || mode == "config-failed" || mode == "mapping-missing" || mode == "malformed" {
if view.Items[0].State != "unknown" {
t.Fatalf("unknown classified as free %s", raw)
}
}
if mode == "multiple" {
if view.Items[1].State != "multiple" || len(view.Items[1].References) != 2 {
t.Fatalf("multiple references lost %s", raw)
}
}
if mode == "normal" || mode == "mapping" {
owner := view.Items[1].References[0]
if owner.DesktopID != "desk_demo_windows" || !owner.PendingRemoval || owner.Phase != "current" || owner.VMStatus != "stopped" {
t.Fatalf("owner metadata %s", raw)
}
if view.Items[2].References[0].Phase != "pending" {
t.Fatalf("pending reservation lost %s", raw)
}
if mode == "mapping" && (owner.Source != "mapping" || owner.Mapping != "gpu-map") {
t.Fatalf("mapping owner %s", raw)
}
}
var count int
if err := f.db.Get(&count, "SELECT count(*) FROM tasks"); err != nil || count != 0 {
t.Fatalf("read created tasks: %d %v", count, err)
}
})
}
}
func TestGPUInventoryErrorsAndAuthentication(t *testing.T) {
f := newFixtureWorker(t, false)
admin := f.login("admin")
code, out := f.request("GET", "/resources/gpus?node=node-a", "", nil, "")
expect(t, 401, code, out)
code, out = f.request("GET", "/resources/gpus?node=node-a", admin, nil, "")
expect(t, 503, code, out)
for _, mode := range []string{"pci-denied", "cluster-failed", "normal"} {
fakeGPUPVE(t, f, mode)
code, out = f.request("GET", "/resources/gpus?node=node-a", admin, nil, "")
want := 502
if mode == "normal" {
want = 200
}
expect(t, want, code, out)
}
code, out = f.request("GET", "/resources/gpus", admin, nil, "")
expect(t, 400, code, out)
code, out = f.request("GET", "/resources/gpus?node=unknown", admin, nil, "")
expect(t, 404, code, out)
code, out = f.request("GET", "/resources/gpus?node=..%2Fsecret", admin, nil, "")
expect(t, 404, code, out)
r := httptest.NewRequest("POST", "/api/v1/resources/gpus?node=node-a", nil)
r.Header.Set("Authorization", "Bearer "+admin)
w := httptest.NewRecorder()
f.server.Router.ServeHTTP(w, r)
if w.Code != 404 {
t.Fatalf("GPU write route exists: %d", w.Code)
}
}