vdi-cloud/platform/pkg/proxmox/provision.go

124 lines
4.1 KiB
Go

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
}