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

148 lines
4.8 KiB
Go

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
}