182 lines
6.7 KiB
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)
|
|
}
|
|
}
|