package proxmox import ( "context" "errors" "fmt" "math" "net/url" "regexp" "sort" "strconv" "strings" ) type VMDisk struct { Key string `json:"key"` Storage string `json:"storage"` SizeGiB float64 `json:"sizeGiB"` } type VMHardware struct { Cores int `json:"cores"` Sockets int `json:"sockets"` MemoryMiB int `json:"memoryMiB"` Digest string `json:"digest"` Disks []VMDisk `json:"disks"` PowerState string `json:"powerState"` Blockers []string `json:"blockers"` Balloon int `json:"-"` } func ConfigInt(config map[string]any, key string, fallback int) int { value, ok := config[key] if !ok { return fallback } n, err := strconv.Atoi(fmt.Sprint(value)) if err != nil { return fallback } return n } func ReadHardware(config map[string]any) (VMHardware, error) { out := VMHardware{Cores: ConfigInt(config, "cores", 1), Sockets: ConfigInt(config, "sockets", 1), MemoryMiB: ConfigInt(config, "memory", 512), Balloon: ConfigInt(config, "balloon", 0), Disks: []VMDisk{}, Blockers: []string{}} out.Digest, _ = config["digest"].(string) if out.Digest == "" { return out, errors.New("配置摘要缺失") } if ConfigInt(config, "template", 0) == 1 { out.Blockers = append(out.Blockers, "模板不能在桌面规格页修改") } if lock := config["lock"]; lock != nil && fmt.Sprint(lock) != "" { out.Blockers = append(out.Blockers, "虚拟机处于 PVE 锁定状态") } for key, value := range config { if strings.HasPrefix(key, "numa") && key != "numa" { out.Blockers = append(out.Blockers, "自定义 NUMA 拓扑请在 PVE 维护") } if !regexp.MustCompile(`^(scsi|sata|virtio|ide)\d+$`).MatchString(key) { continue } text := fmt.Sprint(value) if strings.Contains(text, "media=cdrom") || strings.Contains(text, "cloudinit") { continue } volume := strings.Split(text, ",")[0] storage, _, ok := strings.Cut(volume, ":") if !ok || storage == "" { continue } for _, part := range strings.Split(text, ",") { if !strings.HasPrefix(part, "size=") { continue } size := strings.TrimPrefix(part, "size=") if len(size) == 0 { continue } factor := float64(1) if i := strings.IndexByte("KMGT", size[len(size)-1]); i >= 0 { factor = math.Pow(1024, float64(i+1)) size = size[:len(size)-1] } n, e := strconv.ParseFloat(size, 64) if e == nil && n > 0 && !math.IsInf(n, 0) && !math.IsNaN(n) { out.Disks = append(out.Disks, VMDisk{key, storage, n * factor / (1 << 30)}) } } } sort.Slice(out.Disks, func(i, j int) bool { return out.Disks[i].Key < out.Disks[j].Key }) return out, nil } func (c *Client) Hardware(ctx context.Context, node string, vmid int) (VMHardware, map[string]any, error) { var config map[string]any path := fmt.Sprintf("/nodes/%s/qemu/%d", url.PathEscape(node), vmid) err := c.request(ctx, "GET", path+"/config?current=1", nil, &config) if err != nil { return VMHardware{}, nil, err } hardware, err := ReadHardware(config) if err != nil { return hardware, config, err } hardware.PowerState, err = c.Power(ctx, node, vmid) if err != nil { return hardware, config, err } var pending []map[string]any err = c.request(ctx, "GET", path+"/pending", nil, &pending) if err != nil { return hardware, config, err } for _, item := range pending { if _, ok := item["pending"]; ok || ConfigInt(item, "delete", 0) > 0 { hardware.Blockers = append(hardware.Blockers, "存在待应用配置,请先在 PVE 处理") break } } return hardware, config, nil } func (c *Client) ConfigureHardware(ctx context.Context, node string, vmid, cores, sockets, memory int, digest string, current map[string]any) (string, error) { form := url.Values{"digest": {digest}} cpuChanged := cores != ConfigInt(current, "cores", 1) || sockets != ConfigInt(current, "sockets", 1) if cpuChanged { form.Set("cores", strconv.Itoa(cores)) form.Set("sockets", strconv.Itoa(sockets)) } if memory != ConfigInt(current, "memory", 512) { form.Set("memory", strconv.Itoa(memory)) } if _, ok := current["vcpus"]; ok && cpuChanged { form.Set("vcpus", strconv.Itoa(cores*sockets)) } var upid string err := c.request(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%d/config", url.PathEscape(node), vmid), form, &upid) if err == nil && upid == "" { err = errors.New("PVE did not return a configuration task ID") } return upid, err } func (c *Client) ResizeDisk(ctx context.Context, node string, vmid int, disk string, sizeGiB int, digest string) (string, error) { var upid string err := c.request(ctx, "PUT", fmt.Sprintf("/nodes/%s/qemu/%d/resize", url.PathEscape(node), vmid), url.Values{"disk": {disk}, "size": {strconv.Itoa(sizeGiB) + "G"}, "digest": {digest}}, &upid) if err == nil && upid == "" { err = errors.New("PVE did not return a resize task ID") } return upid, err }