package proxmox import ( "context" "encoding/json" "errors" "fmt" "math" "net/url" "regexp" "strconv" "strings" ) type Storage struct { Name string `json:"storage"` Available float64 `json:"avail"` Active int `json:"active"` Enabled int `json:"enabled"` Content string `json:"content"` } func (c *Client) VMConfig(ctx context.Context, node string, vmid int) (map[string]any, error) { var config map[string]any err := c.request(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%d/config", url.PathEscape(node), vmid), nil, &config) return config, err } func (c *Client) Storages(ctx context.Context, node string) ([]Storage, error) { var items []Storage err := c.request(ctx, "GET", "/nodes/"+url.PathEscape(node)+"/storage", nil, &items) return items, err } func (c *Client) HasPermission(ctx context.Context, path, permission string) (bool, error) { var permissions map[string]map[string]int err := c.request(ctx, "GET", "/access/permissions?path="+url.QueryEscape(path), nil, &permissions) return permissions[path][permission] == 1, err } func (c *Client) CanClone(ctx context.Context, node string, vmid int, full bool) (bool, error) { feature := "clone" if full { feature = "copy" } var data struct { HasFeature json.RawMessage `json:"hasFeature"` } err := c.request(ctx, "GET", fmt.Sprintf("/nodes/%s/qemu/%d/feature?feature=%s", url.PathEscape(node), vmid, feature), nil, &data) return string(data.HasFeature) == "1" || string(data.HasFeature) == "true", err } func (c *Client) Clone(ctx context.Context, node string, source, target int, full bool, storage string) (string, error) { form := url.Values{"newid": {strconv.Itoa(target)}, "name": {fmt.Sprintf("vdi-%d", target)}, "full": {"0"}} if full { form.Set("full", "1") if storage != "" { form.Set("storage", storage) } } var upid string err := c.request(ctx, "POST", fmt.Sprintf("/nodes/%s/qemu/%d/clone", url.PathEscape(node), source), form, &upid) if err == nil && upid == "" { err = errors.New("PVE did not return a clone task ID") } return upid, err } // TemplateDisks rejects passthrough and unmeasurable disks before a clone is queued. // It returns per-storage virtual disk sizes for a conservative capacity check. func TemplateDisks(config map[string]any) (map[string]float64, error) { if fmt.Sprint(config["template"]) != "1" { return nil, errors.New("所选 VM 不是 Proxmox 模板") } if lock, ok := config["lock"]; ok && fmt.Sprint(lock) != "" { return nil, errors.New("模板处于锁定状态,请等待 PVE 操作结束") } disks := map[string]float64{} pattern := regexp.MustCompile(`^((scsi|sata|ide|virtio)\d+|efidisk0|tpmstate0)$`) for key, value := range config { if strings.HasPrefix(key, "hostpci") || strings.HasPrefix(key, "usb") || key == "args" || key == "hookscript" { return nil, errors.New("模板包含直通设备或宿主自定义脚本,请先制作通用模板") } if !pattern.MatchString(key) { continue } disk := fmt.Sprint(value) if strings.Contains(disk, "media=cdrom") || strings.Contains(disk, "cloudinit") { continue } volume := strings.Split(disk, ",")[0] storage, _, ok := strings.Cut(volume, ":") if !ok || storage == "" { return nil, errors.New("模板包含非存储卷磁盘,不能自动克隆") } size := float64(0) for _, part := range strings.Split(disk, ",") { if !strings.HasPrefix(part, "size=") { continue } text := strings.TrimPrefix(part, "size=") if len(text) == 0 { continue } multiplier := float64(1) if index := strings.IndexByte("KMGT", text[len(text)-1]); index >= 0 { for n := 0; n <= index; n++ { multiplier *= 1024 } text = text[:len(text)-1] } amount, err := strconv.ParseFloat(text, 64) if err != nil || amount <= 0 || math.IsNaN(amount) || math.IsInf(amount, 0) { return nil, errors.New("无法识别模板磁盘容量") } size = amount * multiplier } if size <= 0 { return nil, errors.New("模板磁盘缺少容量信息") } disks[storage] += size } if len(disks) == 0 { return nil, errors.New("模板没有可克隆的系统磁盘") } return disks, nil }