267 lines
8.0 KiB
Go
267 lines
8.0 KiB
Go
package server
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"vdi-cloud/platform/pkg/proxmox"
|
||
)
|
||
|
||
type gpuReference struct {
|
||
VMID int `json:"vmid"`
|
||
VMName string `json:"vmName"`
|
||
VMStatus string `json:"vmStatus"`
|
||
Template bool `json:"template"`
|
||
DesktopID string `json:"desktopId"`
|
||
DesktopName string `json:"desktopName"`
|
||
Slot string `json:"slot"`
|
||
Phase string `json:"phase"`
|
||
Source string `json:"source"`
|
||
Mapping string `json:"mapping"`
|
||
Mediated bool `json:"mediated"`
|
||
PendingRemoval bool `json:"pendingRemoval"`
|
||
}
|
||
type gpuItem struct {
|
||
ID string `json:"id"`
|
||
Address string `json:"address"`
|
||
Name string `json:"name"`
|
||
Vendor string `json:"vendor"`
|
||
VendorID string `json:"vendorId"`
|
||
DeviceID string `json:"deviceId"`
|
||
Class string `json:"class"`
|
||
IOMMUGroup *int `json:"iommuGroup"`
|
||
State string `json:"state"`
|
||
References []gpuReference `json:"references"`
|
||
}
|
||
type gpuIssue struct {
|
||
Code string `json:"code"`
|
||
VMID int `json:"vmid"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
type gpuScan struct {
|
||
VM proxmox.Resource
|
||
Options []proxmox.PendingOption
|
||
Err error
|
||
}
|
||
|
||
// This reads the PVE configuration view, not the host driver or running QEMU state.
|
||
func (s *Server) gpuInventory(c *gin.Context) {
|
||
node := c.Query("node")
|
||
if node == "" || len(node) > 100 || strings.TrimSpace(node) != node {
|
||
s.fail(c, 400, "invalid_node", "请选择计算节点")
|
||
return
|
||
}
|
||
pve := s.currentPVE()
|
||
if pve == nil {
|
||
s.fail(c, 503, "pve_unconfigured", "尚未配置 Proxmox,无法读取真实显卡")
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(c.Request.Context(), 12*time.Second)
|
||
defer cancel()
|
||
resources, err := pve.Resources(ctx)
|
||
if err != nil {
|
||
s.fail(c, 502, "pve_unavailable", "无法读取节点和虚拟机清单,请检查 PVE 接入")
|
||
return
|
||
}
|
||
found := false
|
||
vms := []proxmox.Resource{}
|
||
for _, r := range resources {
|
||
if r.Type == "node" && r.Node == node {
|
||
found = true
|
||
}
|
||
if r.Type == "qemu" && r.Node == node {
|
||
vms = append(vms, r)
|
||
}
|
||
}
|
||
if !found {
|
||
s.fail(c, 404, "node_not_found", "节点不存在或当前 PVE 凭据不可见")
|
||
return
|
||
}
|
||
issues := []gpuIssue{}
|
||
issue := func(code string, vmid int, message string) { issues = append(issues, gpuIssue{code, vmid, message}) }
|
||
devices, err := pve.PCIDevices(ctx, node)
|
||
if err != nil {
|
||
s.fail(c, 502, "pci_unavailable", "无法读取节点 PCI 清单,请检查 PVE 连通性及根路径 Sys.Audit 权限")
|
||
return
|
||
}
|
||
items := []gpuItem{}
|
||
for _, d := range devices {
|
||
if !d.IsGPU() {
|
||
continue
|
||
}
|
||
address, ok := proxmox.NormalizePCIAddress(d.ID)
|
||
if !ok {
|
||
issue("pci_invalid", 0, "部分显卡 PCI 地址无法识别")
|
||
continue
|
||
}
|
||
group := d.IOMMUGroup
|
||
if group != nil && *group < 0 {
|
||
group = nil
|
||
}
|
||
items = append(items, gpuItem{ID: node + ":" + address, Address: address, Name: d.DeviceName, Vendor: d.VendorName, VendorID: d.Vendor, DeviceID: d.Device, Class: d.Class, IOMMUGroup: group, References: []gpuReference{}})
|
||
}
|
||
fullVisibility, err := pve.HasPermission(ctx, "/", "VM.Audit")
|
||
if err != nil || !fullVisibility {
|
||
issue("vm_visibility_partial", 0, "未确认根路径 VM.Audit 权限,虚拟机清单可能不完整")
|
||
}
|
||
var desktops []struct {
|
||
ID string `db:"id"`
|
||
Name string `db:"name"`
|
||
VMID int `db:"vmid"`
|
||
}
|
||
if err := s.DB.SelectContext(ctx, &desktops, "SELECT id,name,vmid FROM desktops WHERE provider='proxmox' AND node=$1 ORDER BY id", node); err != nil {
|
||
s.dbError(c, err)
|
||
return
|
||
}
|
||
sort.Slice(vms, func(i, j int) bool { return vms[i].VMID < vms[j].VMID })
|
||
visibleCount := len(vms)
|
||
if len(vms) > 200 {
|
||
vms = vms[:200]
|
||
issue("scan_limit", 0, "本次最多检查 200 台虚拟机,当前节点超出检查范围")
|
||
}
|
||
scans := make([]gpuScan, len(vms))
|
||
jobs := make(chan int, len(vms))
|
||
for i := range vms {
|
||
jobs <- i
|
||
}
|
||
close(jobs)
|
||
var wg sync.WaitGroup
|
||
for worker := 0; worker < 4; worker++ {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
for i := range jobs {
|
||
options, err := pve.VMPending(ctx, node, vms[i].VMID)
|
||
scans[i] = gpuScan{vms[i], options, err}
|
||
}
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
var mappings []proxmox.PCIMapping
|
||
mappingsLoaded := false
|
||
scannedCount := 0
|
||
for _, scan := range scans {
|
||
if scan.Err != nil {
|
||
issue("vm_config_unavailable", scan.VM.VMID, "无法读取虚拟机当前及待应用配置")
|
||
continue
|
||
}
|
||
scannedCount++
|
||
for _, option := range scan.Options {
|
||
if option.Key == "args" || option.Key == "hookscript" {
|
||
if rawOptionText(option.Value) != "" || rawOptionText(option.Pending) != "" {
|
||
issue("custom_vm_config", scan.VM.VMID, "虚拟机包含自定义参数或宿主脚本,额外设备引用未解析")
|
||
}
|
||
continue
|
||
}
|
||
if !proxmox.IsHostPCIKey(option.Key) {
|
||
continue
|
||
}
|
||
for _, phase := range []string{"current", "pending"} {
|
||
value := option.Value
|
||
if phase == "pending" {
|
||
value = option.Pending
|
||
}
|
||
if len(value) == 0 || string(value) == "null" {
|
||
continue
|
||
}
|
||
ref := gpuReference{VMID: scan.VM.VMID, VMName: scan.VM.Name, VMStatus: scan.VM.Status, Template: scan.VM.Template == 1, Slot: option.Key, Phase: phase, Source: "direct", PendingRemoval: phase == "current" && (string(option.Delete) == "1" || string(option.Delete) == "true")}
|
||
for _, d := range desktops {
|
||
if d.VMID == scan.VM.VMID {
|
||
ref.DesktopID, ref.DesktopName = d.ID, d.Name
|
||
break
|
||
}
|
||
}
|
||
parsed, err := proxmox.ParseHostPCI(rawOptionText(value))
|
||
if err != nil {
|
||
issue("hostpci_unparsed", ref.VMID, "虚拟机直通配置无法解析")
|
||
continue
|
||
}
|
||
ref.Mediated = parsed.Mediated
|
||
selectors := parsed.Addresses
|
||
if parsed.Mapping != "" {
|
||
ref.Source, ref.Mapping = "mapping", parsed.Mapping
|
||
if !mappingsLoaded {
|
||
mappings, _ = pve.PCIMappings(ctx)
|
||
mappingsLoaded = true
|
||
}
|
||
selectors, err = gpuMappingSelectors(mappings, parsed.Mapping, node)
|
||
if err != nil {
|
||
issue("mapping_unresolved", ref.VMID, "PCI 映射不可见、配置无效或当前节点未定义")
|
||
continue
|
||
}
|
||
if len(selectors) > 1 {
|
||
issue("mapping_candidates", ref.VMID, "PCI 映射包含多个候选设备,仅展示可能的配置引用")
|
||
}
|
||
}
|
||
for i := range items {
|
||
for _, selector := range selectors {
|
||
if proxmox.PCIAddressMatches(selector, items[i].Address) {
|
||
items[i].References = append(items[i].References, ref)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for i := range items {
|
||
items[i].State = "unreferenced"
|
||
if len(issues) != 0 {
|
||
items[i].State = "unknown"
|
||
}
|
||
owners := map[int]bool{}
|
||
for _, ref := range items[i].References {
|
||
owners[ref.VMID] = true
|
||
}
|
||
if len(owners) == 1 {
|
||
items[i].State = "referenced"
|
||
} else if len(owners) > 1 {
|
||
items[i].State = "multiple"
|
||
}
|
||
}
|
||
sort.Slice(items, func(i, j int) bool { return items[i].Address < items[j].Address })
|
||
c.JSON(200, gin.H{"node": node, "observedAt": time.Now().UTC(), "complete": len(issues) == 0, "visibleVmCount": visibleCount, "scannedVmCount": scannedCount, "items": items, "issues": issues})
|
||
}
|
||
|
||
func rawOptionText(value json.RawMessage) string {
|
||
var text string
|
||
_ = json.Unmarshal(value, &text)
|
||
return text
|
||
}
|
||
|
||
func gpuMappingSelectors(mappings []proxmox.PCIMapping, id, node string) ([]string, error) {
|
||
selectors := []string{}
|
||
for _, mapping := range mappings {
|
||
if mapping.ID != id {
|
||
continue
|
||
}
|
||
for _, entry := range mapping.Map {
|
||
properties, err := proxmox.PCIProperties(entry)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if properties["node"] != node {
|
||
continue
|
||
}
|
||
for _, path := range strings.Split(properties["path"], ";") {
|
||
address, ok := proxmox.NormalizePCIAddress(path)
|
||
if !ok {
|
||
return nil, fmt.Errorf("invalid mapping address")
|
||
}
|
||
selectors = append(selectors, address)
|
||
}
|
||
}
|
||
}
|
||
if len(selectors) == 0 {
|
||
return nil, fmt.Errorf("mapping not visible")
|
||
}
|
||
return selectors, nil
|
||
}
|