package proxmox import ( "context" "encoding/json" "errors" "fmt" "net/url" "regexp" "strings" ) type PCIDevice struct { ID string `json:"id"` Class string `json:"class"` Vendor string `json:"vendor"` VendorName string `json:"vendor_name"` Device string `json:"device"` DeviceName string `json:"device_name"` IOMMUGroup *int `json:"iommugroup"` Driver string `json:"driver"` } type PCIMapping struct { ID string `json:"id"` Map []string `json:"map"` } type PendingOption struct { Key string `json:"key"` Value json.RawMessage `json:"value"` Pending json.RawMessage `json:"pending"` Delete json.RawMessage `json:"delete"` } func (c *Client) PCIDevices(ctx context.Context, node string) ([]PCIDevice, error) { var items []PCIDevice err := c.request(ctx, "GET", "/nodes/"+url.PathEscape(node)+"/hardware/pci?verbose=1", nil, &items) return items, err } func (c *Client) PCIMappings(ctx context.Context) ([]PCIMapping, error) { var items []PCIMapping err := c.request(ctx, "GET", "/cluster/mapping/pci", nil, &items) return items, err } func (c *Client) VMPending(ctx context.Context, node string, vmid int) ([]PendingOption, error) { var items []PendingOption err := c.request(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%d/pending", url.PathEscape(node), vmid), nil, &items) return items, err } var pciAddressPattern = regexp.MustCompile(`^(?:[0-9a-f]{4}:)?[0-9a-f]{2}:[0-1][0-9a-f](?:\.[0-7])?$`) var hostPCIKeyPattern = regexp.MustCompile(`^hostpci[0-9]+$`) // A functionless address selects every function in that PCI slot. func NormalizePCIAddress(value string) (string, bool) { value = strings.ToLower(strings.TrimSpace(value)) if !pciAddressPattern.MatchString(value) { return "", false } if strings.Count(value, ":") == 1 { value = "0000:" + value } return value, true } func PCIAddressMatches(selector, address string) bool { selector, ok := NormalizePCIAddress(selector) if !ok { return false } address, ok = NormalizePCIAddress(address) return ok && (selector == address || !strings.Contains(selector, ".") && strings.HasPrefix(address, selector+".")) } func (d PCIDevice) IsGPU() bool { class := strings.TrimPrefix(strings.ToLower(d.Class), "0x") return len(class) == 6 && strings.HasPrefix(class, "03") } type HostPCI struct { Addresses []string Mapping string Mediated bool } func IsHostPCIKey(key string) bool { return hostPCIKeyPattern.MatchString(key) } func PCIProperties(value string) (map[string]string, error) { properties := map[string]string{} for i, part := range strings.Split(value, ",") { key, val, found := strings.Cut(strings.TrimSpace(part), "=") if !found && i == 0 { key, val = "host", key } else if !found { return nil, errors.New("invalid PCI property") } if _, duplicate := properties[key]; key == "" || val == "" || duplicate { return nil, errors.New("invalid PCI property") } properties[key] = val } return properties, nil } func ParseHostPCI(value string) (HostPCI, error) { var out HostPCI properties, err := PCIProperties(value) if err != nil { return out, err } out.Mapping = properties["mapping"] out.Mediated = properties["mdev"] != "" if (properties["host"] == "") == (out.Mapping == "") { return out, errors.New("expected a host or mapping") } if properties["host"] != "" { for _, address := range strings.Split(properties["host"], ";") { normalized, ok := NormalizePCIAddress(address) if !ok { return out, errors.New("invalid PCI address") } out.Addresses = append(out.Addresses, normalized) } } return out, nil }