mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
1176 lines
31 KiB
Go
1176 lines
31 KiB
Go
// Copyright 2019 Yunion
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package proxmox
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/errors"
|
|
"yunion.io/x/pkg/util/fileutils"
|
|
"yunion.io/x/pkg/util/osprofile"
|
|
"yunion.io/x/pkg/utils"
|
|
|
|
"yunion.io/x/cloudmux/pkg/apis"
|
|
api "yunion.io/x/cloudmux/pkg/apis/compute"
|
|
"yunion.io/x/cloudmux/pkg/cloudprovider"
|
|
"yunion.io/x/cloudmux/pkg/multicloud"
|
|
)
|
|
|
|
var (
|
|
rxIso = regexp.MustCompile(`(.*?),media`)
|
|
rxDeviceID = regexp.MustCompile(`\d+`)
|
|
rxDiskName = regexp.MustCompile(`(virtio|scsi|sata|ide)\d+`)
|
|
rxDiskType = regexp.MustCompile(`\D+`)
|
|
rxUnusedDiskName = regexp.MustCompile(`^(unused)\d+`)
|
|
rxNicName = regexp.MustCompile(`net\d+`)
|
|
rxSerialName = regexp.MustCompile(`serial\d+`)
|
|
rxUsbName = regexp.MustCompile(`usb\d+`)
|
|
)
|
|
|
|
type (
|
|
QemuDevices map[string]map[string]interface{}
|
|
QemuDevice map[string]interface{}
|
|
QemuDeviceParam []string
|
|
)
|
|
|
|
type Intermediate struct {
|
|
HardwareAddress string `json:"hardware-address"`
|
|
IPAddresses []struct {
|
|
IPAddress string `json:"ip-address"`
|
|
IPAddressType string `json:"ip-address-type"`
|
|
Prefix int `json:"prefix"`
|
|
} `json:"ip-addresses"`
|
|
Name string `json:"name"`
|
|
Statistics map[string]int64 `json:"statistics"`
|
|
}
|
|
|
|
type VmBase struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"Description"`
|
|
Tags string `json:"tags"`
|
|
Args string `json:"args"`
|
|
Bios string `json:"bios"`
|
|
OnBoot int `json:"onboot"`
|
|
Startup string `json:"startup"`
|
|
Tablet int `json:"tablet"`
|
|
Ostype string `json:"ostype"`
|
|
Memory int64 `json:"memory"`
|
|
Balloon int64 `json:"balloon"`
|
|
Cores int64 `json:"cores"`
|
|
Vcpus int64 `json:"vcpus"`
|
|
Sockets int64 `json:"sockets"`
|
|
Cpu string `json:"cpu"`
|
|
Numa int `json:"numa"`
|
|
Hotplug string `json:"hotplug"`
|
|
Boot string `json:"boot"`
|
|
Bootdisk string `json:"bootdisk"`
|
|
Kvm int `json:"kvm"`
|
|
Scsihw string `json:"scsihw"`
|
|
Hookscript string `json:"hookscript"`
|
|
Machine string `json:"machine"`
|
|
Ide2 string `json:"ide2,omitempty"`
|
|
Ciuser string `json:"ciuser"`
|
|
Cipassword string `json:"cipassword"`
|
|
Cicustom string `json:"cicustom"`
|
|
Searchdomain string `json:"searchdomain"`
|
|
Nameserver string `json:"nameserver"`
|
|
Sshkeys string `json:"sshkeys"`
|
|
Digest string `json:"digest,omitempty"`
|
|
}
|
|
|
|
type SInstanceDisk struct {
|
|
Storage string
|
|
VolId string
|
|
}
|
|
|
|
type SInstance struct {
|
|
multicloud.SInstanceBase
|
|
ProxmoxTags
|
|
|
|
host *SHost
|
|
|
|
QemuNetworks []SInstanceNic
|
|
PowerState string
|
|
Node string
|
|
|
|
VmId int `json:"vmid"`
|
|
Name string `json:"name"`
|
|
Description string `json:"desc"`
|
|
Digest string `json:"digest,omitempty"`
|
|
Pool string `json:"pool,omitempty"`
|
|
Bios string `json:"bios"`
|
|
EFIDisk QemuDevice `json:"efidisk,omitempty"`
|
|
Machine string `json:"machine,omitempty"`
|
|
Onboot bool `json:"onboot"`
|
|
Startup string `json:"startup,omitempty"`
|
|
Tablet bool `json:"tablet"`
|
|
Memory int `json:"memory"`
|
|
Balloon int `json:"balloon"`
|
|
QemuOs string `json:"ostype"`
|
|
QemuCores int `json:"cores"`
|
|
QemuSockets int `json:"sockets"`
|
|
QemuVcpus int `json:"vcpus"`
|
|
QemuCpu string `json:"cpu"`
|
|
QemuNuma bool `json:"numa"`
|
|
QemuKVM bool `json:"kvm"`
|
|
Hotplug string `json:"hotplug"`
|
|
QemuIso string `json:"iso"`
|
|
QemuPxe bool `json:"pxe"`
|
|
FullClone *int `json:"fullclone"`
|
|
Boot string `json:"boot"`
|
|
BootDisk string `json:"bootdisk,omitempty"`
|
|
Scsihw string `json:"scsihw,omitempty"`
|
|
QemuDisks map[string][]struct {
|
|
Driver string
|
|
DiskId string
|
|
Size int64
|
|
} `json:"disk"`
|
|
QemuUnusedDisks QemuDevices `json:"unused_disk"`
|
|
QemuVga QemuDevice `json:"vga,omitempty"`
|
|
QemuSerials QemuDevices `json:"serial,omitempty"`
|
|
QemuUsbs QemuDevices `json:"usb,omitempty"`
|
|
Hookscript string `json:"hookscript,omitempty"`
|
|
Tags string `json:"tags"`
|
|
Args string `json:"args"`
|
|
|
|
// Deprecated single disk.
|
|
DiskSize float64 `json:"diskGB"`
|
|
Storage string `json:"storage"`
|
|
StorageType string `json:"storageType"` // virtio|scsi (cloud-init defaults to scsi)
|
|
|
|
// Deprecated single nic.
|
|
QemuNicModel string `json:"nic"`
|
|
QemuBridge string `json:"bridge"`
|
|
QemuVlanTag int `json:"vlan"`
|
|
QemuMacAddr string `json:"mac"`
|
|
|
|
// cloud-init options
|
|
CIuser string `json:"ciuser"`
|
|
CIpassword string `json:"cipassword"`
|
|
CIcustom string `json:"cicustom"`
|
|
|
|
Searchdomain string `json:"searchdomain"`
|
|
Nameserver string `json:"nameserver"`
|
|
Sshkeys string `json:"sshkeys"`
|
|
}
|
|
|
|
func (self *SInstance) GetName() string {
|
|
if len(self.Name) > 0 {
|
|
return self.Name
|
|
}
|
|
return self.GetId()
|
|
}
|
|
|
|
func (self *SInstance) GetId() string {
|
|
return strconv.Itoa(self.VmId)
|
|
}
|
|
|
|
func (self *SInstance) GetGlobalId() string {
|
|
return self.GetId()
|
|
}
|
|
|
|
func (self *SInstance) Refresh() error {
|
|
id := strconv.Itoa(int(self.VmId))
|
|
ins, err := self.host.cli.GetInstance(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
self.QemuDisks = ins.QemuDisks
|
|
return jsonutils.Update(self, ins)
|
|
}
|
|
|
|
func (self *SInstance) AttachDisk(ctx context.Context, diskId string) error {
|
|
return cloudprovider.ErrNotSupported
|
|
}
|
|
|
|
func (self *SInstance) CreateDisk(ctx context.Context, opts *cloudprovider.GuestDiskCreateOptions) (string, error) {
|
|
body := map[string]string{}
|
|
params := url.Values{}
|
|
storage, err := self.host.cli.GetStorage(opts.StorageId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
driver := fmt.Sprintf("scsi%d", opts.Idx)
|
|
body[driver] = fmt.Sprintf("%s:%d", storage.Storage, opts.SizeMb/1024)
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", self.Node, self.VmId)
|
|
err = self.host.cli.put(res, params, jsonutils.Marshal(body))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = self.Refresh()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for storageName, disks := range self.QemuDisks {
|
|
if storageName != storage.Storage {
|
|
continue
|
|
}
|
|
for i := range disks {
|
|
if disks[i].Driver != driver {
|
|
continue
|
|
}
|
|
volumes, err := self.host.cli.GetDisks(self.Node, storage.Storage)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for i := range volumes {
|
|
volumes[i].storage = storage
|
|
if strings.HasSuffix(volumes[i].GetGlobalId(), "|"+volumes[i].VolId) {
|
|
return volumes[i].GetGlobalId(), nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "", errors.Wrapf(cloudprovider.ErrNotFound, "after created")
|
|
}
|
|
|
|
func (self *SInstance) ChangeConfig(ctx context.Context, opts *cloudprovider.SManagedVMChangeConfig) error {
|
|
return self.host.cli.ChangeConfig(self.VmId, opts.Cpu, opts.MemoryMB)
|
|
}
|
|
|
|
func (self *SInstance) DeleteVM(ctx context.Context) error {
|
|
return self.host.cli.DeleteVM(self.VmId)
|
|
}
|
|
|
|
func (self *SInstance) DeployVM(ctx context.Context, opts *cloudprovider.SInstanceDeployOptions) error {
|
|
return self.host.cli.ResetVmPassword(self.VmId, opts.Username, opts.Password)
|
|
}
|
|
|
|
func (self *SInstance) DetachDisk(ctx context.Context, diskId string) error {
|
|
diskInfo := strings.Split(diskId, "|")
|
|
storageName, volId := "", ""
|
|
if len(diskInfo) == 2 {
|
|
storageName, volId = diskInfo[0], diskInfo[1]
|
|
} else if len(diskInfo) == 3 {
|
|
storageName, volId = diskInfo[1], diskInfo[2]
|
|
} else {
|
|
return fmt.Errorf("invalid diskId %s", diskId)
|
|
}
|
|
for _storageName, disks := range self.QemuDisks {
|
|
if storageName != _storageName {
|
|
continue
|
|
}
|
|
for _, disk := range disks {
|
|
if disk.DiskId == volId {
|
|
return self.host.cli.DetachDisk(self.Node, self.VmId, disk.Driver)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *SInstance) GetBios() cloudprovider.TBiosType {
|
|
return cloudprovider.ToBiosType(self.Bios)
|
|
}
|
|
|
|
func (self *SInstance) GetBootOrder() string {
|
|
return "cdn"
|
|
}
|
|
|
|
func (self *SInstance) GetError() error {
|
|
return nil
|
|
}
|
|
|
|
func (self *SInstance) GetHostname() string {
|
|
return ""
|
|
}
|
|
|
|
func (self *SInstance) GetHypervisor() string {
|
|
return api.HYPERVISOR_PROXMOX
|
|
}
|
|
|
|
func (self *SInstance) VMIdExists(vmId int) (bool, error) {
|
|
resources, err := self.host.cli.GetClusterVmResources()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
_, res := resources[vmId]
|
|
return res, nil
|
|
}
|
|
|
|
func (self *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) {
|
|
ret := []cloudprovider.ICloudDisk{}
|
|
ins, err := self.host.cli.GetInstance(fmt.Sprintf("%d", self.VmId))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for storageName, disks := range ins.QemuDisks {
|
|
diskIds := []string{}
|
|
for i := range disks {
|
|
if strings.HasSuffix(disks[i].DiskId, ".iso") {
|
|
continue
|
|
}
|
|
// skip cloud-init drive
|
|
if strings.HasSuffix(disks[i].DiskId, ":cloudinit") || strings.Contains(disks[i].DiskId, "cloudinit") {
|
|
continue
|
|
}
|
|
diskIds = append(diskIds, disks[i].DiskId)
|
|
}
|
|
disks, err := self.host.cli.GetDisks(self.host.Node, storageName)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "GetDisks")
|
|
}
|
|
storages, err := self.host.cli.GetStoragesByHost(self.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var storage *SStorage
|
|
for i := range storages {
|
|
if storages[i].Storage == storageName {
|
|
storage = &storages[i]
|
|
}
|
|
}
|
|
if storage == nil {
|
|
return nil, errors.Wrapf(cloudprovider.ErrNotFound, "search storage %s", storageName)
|
|
}
|
|
for i := range disks {
|
|
if utils.IsInStringArray(disks[i].VolId, diskIds) {
|
|
disks[i].storage = storage
|
|
ret = append(ret, &disks[i])
|
|
}
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (self *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) {
|
|
return nil, cloudprovider.ErrNotSupported
|
|
}
|
|
|
|
func (self *SInstance) GetIHost() cloudprovider.ICloudHost {
|
|
return self.host
|
|
}
|
|
|
|
func (self *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) {
|
|
ret := []cloudprovider.ICloudNic{}
|
|
for i := range self.QemuNetworks {
|
|
self.QemuNetworks[i].ins = self
|
|
ret = append(ret, &self.QemuNetworks[i])
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (self *SInstance) GetInstanceType() string {
|
|
return fmt.Sprintf("ecs.g1.c%dm%d", self.GetVcpuCount(), self.GetVmemSizeMB()/1024)
|
|
}
|
|
|
|
func (self *SInstance) GetMachine() string {
|
|
return self.Machine
|
|
}
|
|
|
|
func (self *SInstance) GetStatus() string {
|
|
switch strings.ToLower(self.PowerState) {
|
|
case "running":
|
|
return api.VM_RUNNING
|
|
case "stopped":
|
|
return api.VM_READY
|
|
case "paused":
|
|
return api.VM_SUSPEND
|
|
}
|
|
return api.VM_UNKNOWN
|
|
}
|
|
|
|
func (self *SInstance) GetFullOsName() string {
|
|
return ""
|
|
}
|
|
|
|
func (self *SInstance) GetOsType() cloudprovider.TOsType {
|
|
isWin, _ := regexp.MatchString("(wxp|w2k|w2k3|w2k8|wvista|win7|win8|win10|win11)", self.QemuOs)
|
|
if isWin == true {
|
|
return cloudprovider.TOsType(osprofile.OS_TYPE_WINDOWS)
|
|
} else {
|
|
return cloudprovider.TOsType(osprofile.OS_TYPE_LINUX)
|
|
}
|
|
}
|
|
|
|
func (ins *SInstance) GetOsArch() string {
|
|
if utils.IsInStringArray(ins.QemuCpu, []string{"neoverse-n1"}) || strings.HasPrefix(ins.QemuCpu, "cortex-a") {
|
|
return apis.OS_ARCH_AARCH64
|
|
}
|
|
return ins.host.GetCpuArchitecture()
|
|
}
|
|
|
|
func (ins *SInstance) GetOsDist() string {
|
|
return ""
|
|
}
|
|
|
|
func (ins *SInstance) GetOsVersion() string {
|
|
return ""
|
|
}
|
|
|
|
func (ins *SInstance) GetOsLang() string {
|
|
return ""
|
|
}
|
|
|
|
func (self *SInstance) GetProjectId() string {
|
|
return ""
|
|
}
|
|
|
|
func (self *SInstance) GetVNCInfo(input *cloudprovider.ServerVncInput) (*cloudprovider.ServerVncOutput, error) {
|
|
vnc, err := self.host.cli.GetVNCInfo(self.Node, self.VmId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Official Proxmox console: connect via vncwebsocket on API port (8006),
|
|
// authenticated by PVEAuthCookie + vncticket query param.
|
|
vncURL := fmt.Sprintf(
|
|
"wss://%s:%d/api2/json/nodes/%s/qemu/%d/vncwebsocket?port=%d&vncticket=%s",
|
|
self.host.cli.host,
|
|
self.host.cli.port,
|
|
self.Node,
|
|
self.VmId,
|
|
vnc.Port,
|
|
url.QueryEscape(vnc.Ticket),
|
|
)
|
|
ret := &cloudprovider.ServerVncOutput{
|
|
Protocol: "vnc",
|
|
Url: vncURL,
|
|
Host: self.host.cli.host,
|
|
Port: int64(self.host.cli.port),
|
|
Password: vnc.Password,
|
|
Cookie: "PVEAuthCookie=" + self.host.cli.authTicket,
|
|
Hypervisor: api.HYPERVISOR_PROXMOX,
|
|
}
|
|
if len(ret.Password) == 0 {
|
|
// fallback for older PVE without generate-password
|
|
ret.Password = vnc.Ticket
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (self *SInstance) GetVcpuCount() int {
|
|
return int(self.QemuCores * self.QemuSockets)
|
|
}
|
|
|
|
func (self *SInstance) GetVmemSizeMB() int {
|
|
return int(self.Memory)
|
|
}
|
|
|
|
func (self *SInstance) GetVga() string {
|
|
return "std"
|
|
}
|
|
|
|
func (self *SInstance) GetVdi() string {
|
|
return "vnc"
|
|
}
|
|
|
|
func (ins *SInstance) RebuildRoot(ctx context.Context, desc *cloudprovider.SManagedVMRebuildRootConfig) (string, error) {
|
|
sysDriver, storageName, sysDiskId := "", "", ""
|
|
for _storageName, disks := range ins.QemuDisks {
|
|
for _, disk := range disks {
|
|
if disk.Driver == "scsi0" {
|
|
sysDriver = disk.Driver
|
|
storageName = _storageName
|
|
sysDiskId = disk.DiskId
|
|
break
|
|
}
|
|
}
|
|
if len(sysDriver) > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(sysDriver) == 0 {
|
|
return "", errors.Wrapf(cloudprovider.ErrNotFound, "sys driver not found")
|
|
}
|
|
storages, err := ins.host.cli.GetStoragesByHost(ins.Node)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "GetStoragesByHost")
|
|
}
|
|
var storage *SStorage
|
|
for i := range storages {
|
|
if storages[i].Storage == storageName {
|
|
storage = &storages[i]
|
|
}
|
|
}
|
|
if storage == nil {
|
|
return "", errors.Wrapf(cloudprovider.ErrNotFound, "storage %s not found", storageName)
|
|
}
|
|
|
|
defer func() {
|
|
err = ins.host.cli.ResetBootOrder(ins.Node, ins.VmId, sysDriver)
|
|
if err != nil {
|
|
log.Errorf("ResetBootOrder %s%d%s %s", ins.Node, ins.VmId, sysDriver, err)
|
|
}
|
|
}()
|
|
|
|
err = ins.host.cli.DetachDisk(ins.Node, ins.VmId, sysDriver)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "DetachDisk %s", sysDriver)
|
|
}
|
|
|
|
err = storage.CreateSystemDisk(ins.Node, ins.VmId, sysDriver, desc.ImageId, int(ins.DiskSize))
|
|
if err != nil {
|
|
ins.host.cli.AttachUnuseDisk(ins.Node, ins.VmId, sysDriver, sysDiskId)
|
|
return "", errors.Wrapf(err, "CreateSystemDisk %s", sysDriver)
|
|
}
|
|
|
|
err = ins.host.cli.RemoveUnuseDisk(ins.Node, ins.VmId)
|
|
if err != nil {
|
|
log.Errorf("RemoveUnuseDisk %s", err)
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func (cli *SProxmoxClient) AttachUnuseDisk(node string, vmId int, driver, diskId string) error {
|
|
vm, err := cli.GetInstance(strconv.Itoa(vmId))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", node, vmId)
|
|
err = cli.put(res, url.Values{}, jsonutils.Marshal(map[string]interface{}{
|
|
driver: diskId,
|
|
"digest": vm.Digest,
|
|
}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cli *SProxmoxClient) ResetBootOrder(node string, vmId int, driver string) error {
|
|
vm, err := cli.GetInstance(strconv.Itoa(vmId))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", node, vmId)
|
|
err = cli.put(res, url.Values{}, jsonutils.Marshal(map[string]interface{}{
|
|
"boot": fmt.Sprintf("order=%s", driver),
|
|
"digest": vm.Digest,
|
|
}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cli *SProxmoxClient) RemoveUnuseDisk(node string, vmId int) error {
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", node, vmId)
|
|
_, err := cli.post(res, jsonutils.Marshal(map[string]interface{}{
|
|
"delete": "unused0",
|
|
"background_delay": 5,
|
|
}))
|
|
return err
|
|
}
|
|
|
|
func (storage *SStorage) CreateSystemDisk(node string, vmId int, driver string, imageId string, sizeGb int) error {
|
|
vm, err := storage.cli.GetInstance(strconv.Itoa(vmId))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := map[string]interface{}{
|
|
driver: fmt.Sprintf("%s:%d,import-from=%s", storage.Storage, sizeGb, imageId),
|
|
"background_delay": 5,
|
|
"digest": vm.Digest,
|
|
}
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", node, vmId)
|
|
_, err = storage.cli.post(res, jsonutils.Marshal(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *SInstance) GetSecurityGroupIds() ([]string, error) {
|
|
return []string{}, nil
|
|
}
|
|
|
|
func (self *SInstance) SetSecurityGroups(secgroupIds []string) error {
|
|
return cloudprovider.ErrNotSupported
|
|
}
|
|
|
|
func (self *SInstance) StartVM(ctx context.Context) error {
|
|
return self.host.cli.StartVm(self.VmId)
|
|
}
|
|
|
|
func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error {
|
|
if self.GetStatus() == api.VM_READY {
|
|
return nil
|
|
}
|
|
return self.host.cli.StopVm(self.VmId)
|
|
}
|
|
|
|
func (self *SInstance) UpdateUserData(userData string) error {
|
|
return cloudprovider.ErrNotSupported
|
|
}
|
|
|
|
func (self *SInstance) UpdateVM(ctx context.Context, input cloudprovider.SInstanceUpdateOptions) error {
|
|
return cloudprovider.ErrNotSupported
|
|
}
|
|
|
|
// readDeviceConfig - get standard sub-conf strings where `key=value` and update conf map.
|
|
func (confMap QemuDevice) readDeviceConfig(confList []string) error {
|
|
// Add device config.
|
|
for _, conf := range confList {
|
|
key, value := ParseSubConf(conf, "=")
|
|
confMap[key] = value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *SProxmoxClient) GetVmAgentNetworkInterfaces(node string, VmId int) (map[string]string, error) {
|
|
intermediates := []Intermediate{}
|
|
ipMap := map[string]string{}
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/agent/network-get-interfaces", node, VmId)
|
|
err := self.getAgent(res, url.Values{}, &intermediates)
|
|
if err != nil {
|
|
return ipMap, errors.Wrap(err, "GetVmAgentNetworkInterfaces")
|
|
}
|
|
|
|
for _, intermediate := range intermediates {
|
|
for _, addr := range intermediate.IPAddresses {
|
|
if strings.ToLower(addr.IPAddressType) == "ipv4" {
|
|
ipMap[intermediate.HardwareAddress] = addr.IPAddress
|
|
}
|
|
}
|
|
}
|
|
|
|
return ipMap, nil
|
|
}
|
|
|
|
func (self *SProxmoxClient) GetVmPowerStatus(node string, VmId int) string {
|
|
current := map[string]string{}
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/status/current", node, VmId)
|
|
err := self.get(res, url.Values{}, ¤t)
|
|
if err != nil {
|
|
return "unkown"
|
|
}
|
|
|
|
power := "unkown"
|
|
if _, ok := current["qmpstatus"]; ok {
|
|
power = current["qmpstatus"]
|
|
}
|
|
|
|
return power
|
|
}
|
|
|
|
func (self *SProxmoxClient) GetQemuConfig(node string, VmId int) (*SInstance, error) {
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", node, VmId)
|
|
vmConfig := map[string]interface{}{}
|
|
vmBase := &VmBase{
|
|
Bios: "seabios",
|
|
OnBoot: 1,
|
|
Tablet: 1,
|
|
Ostype: "other",
|
|
Memory: 0,
|
|
Balloon: 0,
|
|
Cores: 0,
|
|
Vcpus: 0,
|
|
Sockets: 0,
|
|
Cpu: "host",
|
|
Numa: 0,
|
|
Hotplug: "network,disk,usb",
|
|
Boot: "cdn",
|
|
Kvm: 1,
|
|
Scsihw: "lsi",
|
|
Machine: "i440fx",
|
|
}
|
|
err := self.get(res, url.Values{}, &vmConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = jsonutils.Update(vmBase, vmConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config := SInstance{
|
|
VmId: int(VmId),
|
|
Digest: vmBase.Digest,
|
|
Name: vmBase.Name,
|
|
Description: strings.TrimSpace(vmBase.Description),
|
|
Tags: strings.TrimSpace(vmBase.Tags),
|
|
Args: strings.TrimSpace(vmBase.Args),
|
|
Bios: vmBase.Bios,
|
|
EFIDisk: QemuDevice{},
|
|
Machine: vmBase.Machine,
|
|
Onboot: Itob(vmBase.OnBoot),
|
|
Startup: vmBase.Startup,
|
|
Tablet: Itob(vmBase.Tablet),
|
|
QemuOs: vmBase.Ostype,
|
|
Memory: int(vmBase.Memory),
|
|
QemuCores: int(vmBase.Cores),
|
|
QemuSockets: int(vmBase.Sockets),
|
|
QemuCpu: vmBase.Cpu,
|
|
QemuNuma: Itob(vmBase.Numa),
|
|
QemuKVM: Itob(vmBase.Kvm),
|
|
Hotplug: vmBase.Hotplug,
|
|
QemuVlanTag: -1,
|
|
Boot: vmBase.Boot,
|
|
BootDisk: vmBase.Bootdisk,
|
|
Scsihw: vmBase.Scsihw,
|
|
Hookscript: vmBase.Hookscript,
|
|
QemuDisks: map[string][]struct {
|
|
Driver string
|
|
DiskId string
|
|
Size int64
|
|
}{},
|
|
QemuUnusedDisks: QemuDevices{},
|
|
QemuVga: QemuDevice{},
|
|
QemuNetworks: []SInstanceNic{},
|
|
QemuSerials: QemuDevices{},
|
|
QemuUsbs: QemuDevices{},
|
|
Node: node,
|
|
CIuser: vmBase.Ciuser,
|
|
CIpassword: vmBase.Cipassword,
|
|
Searchdomain: vmBase.Searchdomain,
|
|
Nameserver: vmBase.Nameserver,
|
|
}
|
|
|
|
// vmConfig Sample: map[ cpu:host
|
|
// net0:virtio=62:DF:XX:XX:XX:XX,bridge=vmbr0
|
|
// ide2:local:iso/xxx-xx.iso,media=cdrom memory:2048
|
|
// smbios1:uuid=8b3bf833-aad8-4545-xxx-xxxxxxx digest:aa6ce5xxxxx1b9ce33e4aaeff564d4 sockets:1
|
|
// name:terraform-ubuntu1404-template bootdisk:virtio0
|
|
// virtio0:ProxmoxxxxISCSI:vm-1014-disk-2,size=4G
|
|
// description:Base image
|
|
// cores:2 ostype:l26
|
|
if vmConfig["ide2"] != nil {
|
|
isoMatch := rxIso.FindStringSubmatch(vmConfig["ide2"].(string))
|
|
config.QemuIso = isoMatch[1]
|
|
}
|
|
|
|
if _, ok := vmConfig["sshkeys"]; ok {
|
|
config.Sshkeys, _ = url.PathUnescape(vmConfig["sshkeys"].(string))
|
|
}
|
|
|
|
config.PowerState = self.GetVmPowerStatus(node, VmId)
|
|
|
|
// Add disks.
|
|
diskNames := map[string]string{}
|
|
for k := range vmConfig {
|
|
if diskName := rxDiskName.FindStringSubmatch(k); len(diskName) > 0 {
|
|
diskNames[k] = diskName[0]
|
|
}
|
|
}
|
|
|
|
for driver, diskName := range diskNames {
|
|
diskConfStr := vmConfig[diskName].(string)
|
|
diskConfMap := ParsePMConf(diskConfStr, "volume")
|
|
|
|
if diskConfMap["volume"].(string) == "none" {
|
|
continue
|
|
}
|
|
// skip cloud-init drive, e.g. local-lvm:cloudinit
|
|
volume := diskConfMap["volume"].(string)
|
|
if strings.HasSuffix(volume, ":cloudinit") || strings.Contains(volume, "cloudinit") {
|
|
continue
|
|
}
|
|
|
|
size := int(0)
|
|
if sizeStr, ok := diskConfMap["size"].(string); ok {
|
|
size, _ = fileutils.GetSizeGb(sizeStr, 'G', 1024)
|
|
}
|
|
|
|
storageName, _ := ParseSubConf(diskConfMap["volume"].(string), ":")
|
|
_, ok := config.QemuDisks[storageName]
|
|
if !ok {
|
|
config.QemuDisks[storageName] = []struct {
|
|
Driver string
|
|
DiskId string
|
|
Size int64
|
|
}{}
|
|
}
|
|
config.QemuDisks[storageName] = append(config.QemuDisks[storageName], struct {
|
|
Driver string
|
|
DiskId string
|
|
Size int64
|
|
}{
|
|
Driver: driver,
|
|
DiskId: diskConfMap["volume"].(string),
|
|
Size: int64(size * 1024 * 1024 * 1024),
|
|
})
|
|
}
|
|
|
|
// Add unused disks
|
|
// unused0:local:100/vm-100-disk-1.qcow2
|
|
unusedDiskNames := []string{}
|
|
for k := range vmConfig {
|
|
// look for entries from the config in the format "unusedX:<storagepath>" where X is an integer
|
|
if unusedDiskName := rxUnusedDiskName.FindStringSubmatch(k); len(unusedDiskName) > 0 {
|
|
unusedDiskNames = append(unusedDiskNames, unusedDiskName[0])
|
|
}
|
|
}
|
|
if len(unusedDiskNames) > 0 {
|
|
log.Debugf("[DEBUG] unusedDiskNames: %v", unusedDiskNames)
|
|
}
|
|
|
|
for _, unusedDiskName := range unusedDiskNames {
|
|
unusedDiskConfStr := vmConfig[unusedDiskName].(string)
|
|
finalDiskConfMap := QemuDevice{}
|
|
|
|
// parse "unused0" to get the id '0' as an int
|
|
id := rxDeviceID.FindStringSubmatch(unusedDiskName)
|
|
slotID, err := strconv.Atoi(id[0])
|
|
if err != nil {
|
|
return nil, errors.Errorf("Unable to parse unused disk id from input string '%s' .", unusedDiskName)
|
|
}
|
|
finalDiskConfMap["slot"] = slotID
|
|
|
|
// parse the attributes from the unused disk
|
|
// extract the storage and file path from the unused disk entry
|
|
parsedUnusedDiskMap := ParsePMConf(unusedDiskConfStr, "storage+file")
|
|
storageName, fileName := ParseSubConf(parsedUnusedDiskMap["storage+file"].(string), ":")
|
|
finalDiskConfMap["storage"] = storageName
|
|
finalDiskConfMap["file"] = fileName
|
|
volId := parsedUnusedDiskMap["storage+file"].(string)
|
|
|
|
config.QemuUnusedDisks[volId] = finalDiskConfMap
|
|
}
|
|
|
|
//Display
|
|
if vga, ok := vmConfig["vga"]; ok {
|
|
vgaList := strings.Split(vga.(string), ",")
|
|
vgaMap := QemuDevice{}
|
|
|
|
// TODO: keep going if error?
|
|
err = vgaMap.readDeviceConfig(vgaList)
|
|
if err != nil {
|
|
log.Debugf("[ERROR] %q", err)
|
|
}
|
|
if len(vgaMap) > 0 {
|
|
config.QemuVga = vgaMap
|
|
}
|
|
}
|
|
|
|
// Add networks.
|
|
nicNames := []string{}
|
|
ipMap := make(map[string]string)
|
|
if config.PowerState == "running" {
|
|
ipMap, _ = self.GetVmAgentNetworkInterfaces(node, VmId)
|
|
}
|
|
|
|
for k := range vmConfig {
|
|
if nicName := rxNicName.FindStringSubmatch(k); len(nicName) > 0 {
|
|
nicNames = append(nicNames, nicName[0])
|
|
}
|
|
}
|
|
|
|
for _, nicName := range nicNames {
|
|
nicConfStr := vmConfig[nicName]
|
|
nicConfList := strings.Split(nicConfStr.(string), ",")
|
|
|
|
//id := rxDeviceID.FindStringSubmatch(nicName)
|
|
model, macaddr := ParseSubConf(nicConfList[0], "=")
|
|
_, network := ParseSubConf(nicConfList[1], "=")
|
|
//nicID := fmt.Sprintf("%d:%s", VmId, nicName)
|
|
|
|
// Add model and MAC address.
|
|
nicConf := SInstanceNic{
|
|
NicId: network.(string),
|
|
Model: model,
|
|
MacAddr: strings.ToLower(macaddr.(string)),
|
|
NetworkId: fmt.Sprintf("network/%s/%s", node, network),
|
|
}
|
|
|
|
if ip, ok := ipMap[nicConf.MacAddr]; ok {
|
|
nicConf.IpAddr = ip
|
|
}
|
|
|
|
// And device config to networks.
|
|
config.QemuNetworks = append(config.QemuNetworks, nicConf)
|
|
}
|
|
|
|
// Add serials
|
|
serialNames := []string{}
|
|
|
|
for k := range vmConfig {
|
|
if serialName := rxSerialName.FindStringSubmatch(k); len(serialName) > 0 {
|
|
serialNames = append(serialNames, serialName[0])
|
|
}
|
|
}
|
|
|
|
for _, serialName := range serialNames {
|
|
id := rxDeviceID.FindStringSubmatch(serialName)
|
|
serialID, _ := strconv.Atoi(id[0])
|
|
|
|
serialConfMap := QemuDevice{
|
|
"id": serialID,
|
|
"type": vmConfig[serialName],
|
|
}
|
|
|
|
// And device config to serials map.
|
|
if len(serialConfMap) > 0 {
|
|
config.QemuSerials[serialName] = serialConfMap
|
|
}
|
|
}
|
|
|
|
// Add usbs
|
|
usbNames := []string{}
|
|
|
|
for k := range vmConfig {
|
|
if usbName := rxUsbName.FindStringSubmatch(k); len(usbName) > 0 {
|
|
usbNames = append(usbNames, usbName[0])
|
|
}
|
|
}
|
|
|
|
for _, usbName := range usbNames {
|
|
usbConfStr := vmConfig[usbName]
|
|
usbConfList := strings.Split(usbConfStr.(string), ",")
|
|
id := rxDeviceID.FindStringSubmatch(usbName)
|
|
usbID, _ := strconv.Atoi(id[0])
|
|
_, host := ParseSubConf(usbConfList[0], "=")
|
|
|
|
usbConfMap := QemuDevice{
|
|
"id": usbID,
|
|
"host": host,
|
|
}
|
|
|
|
err = usbConfMap.readDeviceConfig(usbConfList[1:])
|
|
if err != nil {
|
|
log.Debugf("[ERROR] %q", err)
|
|
}
|
|
if usbConfMap["usb3"] == 1 {
|
|
usbConfMap["usb3"] = true
|
|
}
|
|
|
|
// And device config to usbs map.
|
|
if len(usbConfMap) > 0 {
|
|
config.QemuUsbs[usbName] = usbConfMap
|
|
}
|
|
}
|
|
|
|
return &config, nil
|
|
|
|
}
|
|
|
|
func (self *SProxmoxClient) GetInstances(hostId string) ([]SInstance, error) {
|
|
ret := []SInstance{}
|
|
resources, err := self.GetClusterVmResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, res := range resources {
|
|
if res.Template {
|
|
continue
|
|
}
|
|
if res.NodeId == hostId || len(hostId) == 0 {
|
|
instance, err := self.GetQemuConfig(res.Node, res.VmId)
|
|
if err != nil {
|
|
log.Warningf("get pve vm %s %d error: %v", res.Node, res.VmId, err)
|
|
continue
|
|
}
|
|
ret = append(ret, *instance)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (self *SProxmoxClient) GetInstance(id string) (*SInstance, error) {
|
|
resources, err := self.GetClusterVmResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodeName := ""
|
|
vmId, _ := strconv.Atoi(id)
|
|
if resource, ok := resources[vmId]; !ok {
|
|
return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", id)
|
|
} else {
|
|
nodeName = resource.Node
|
|
}
|
|
|
|
return self.GetQemuConfig(nodeName, vmId)
|
|
}
|
|
|
|
func (self *SProxmoxClient) StartVm(vmId int) error {
|
|
resources, err := self.GetClusterVmResources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nodeName := ""
|
|
if resource, ok := resources[vmId]; !ok {
|
|
return errors.Errorf("start VM id %d", vmId)
|
|
} else {
|
|
nodeName = resource.Node
|
|
}
|
|
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/status/start", nodeName, vmId)
|
|
params := url.Values{}
|
|
|
|
_, err = self.post(res, params)
|
|
return err
|
|
|
|
}
|
|
|
|
func (self *SProxmoxClient) StopVm(vmId int) error {
|
|
resources, err := self.GetClusterVmResources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nodeName := ""
|
|
if resource, ok := resources[vmId]; !ok {
|
|
return errors.Errorf("start VM id %d", vmId)
|
|
} else {
|
|
nodeName = resource.Node
|
|
}
|
|
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/status/stop", nodeName, vmId)
|
|
params := url.Values{}
|
|
|
|
_, err = self.post(res, params)
|
|
return err
|
|
}
|
|
|
|
func (self *SProxmoxClient) DetachDisk(node string, vmId int, driver string) error {
|
|
body := map[string]string{}
|
|
params := url.Values{}
|
|
body["delete"] = driver
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", node, vmId)
|
|
return self.put(res, params, jsonutils.Marshal(body))
|
|
}
|
|
|
|
func (self *SProxmoxClient) ChangeConfig(vmId int, cpu int, memMb int) error {
|
|
vm, err := self.GetInstance(strconv.Itoa(int(vmId)))
|
|
body := map[string]interface{}{}
|
|
if err != nil {
|
|
return errors.Wrapf(err, "ChangeConfig(%d)", vmId)
|
|
}
|
|
|
|
changed := false
|
|
if cpu > 0 {
|
|
vm.QemuCores = 1
|
|
vm.QemuSockets = cpu
|
|
vm.QemuVcpus = cpu
|
|
changed = true
|
|
body["cores"] = 1
|
|
body["sockets"] = cpu
|
|
body["vcpus"] = cpu
|
|
}
|
|
if memMb > 0 {
|
|
vm.Memory = memMb
|
|
body["memory"] = memMb
|
|
changed = true
|
|
}
|
|
if !changed {
|
|
return nil
|
|
}
|
|
|
|
params := url.Values{}
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/config", vm.Node, vmId)
|
|
return self.put(res, params, jsonutils.Marshal(body))
|
|
}
|
|
|
|
func (self *SProxmoxClient) ResetVmPassword(vmId int, username, password string) error {
|
|
resources, err := self.GetClusterVmResources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nodeName := ""
|
|
if resource, ok := resources[vmId]; !ok {
|
|
return errors.Errorf("failed to ResetVmPassword VM id %d", vmId)
|
|
} else {
|
|
nodeName = resource.Node
|
|
}
|
|
|
|
params := url.Values{}
|
|
body := map[string]interface{}{
|
|
"username": username,
|
|
"password": password,
|
|
}
|
|
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/agent/set-user-password", nodeName, vmId)
|
|
return self.put(res, params, jsonutils.Marshal(body))
|
|
|
|
}
|
|
|
|
func (self *SProxmoxClient) DeleteVM(vmId int) error {
|
|
id := strconv.Itoa(int(vmId))
|
|
vm1, err := self.GetInstance(id)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "GetInstance(%d)", vmId)
|
|
}
|
|
params := url.Values{}
|
|
params.Set("purge", "1")
|
|
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d", vm1.Node, vmId)
|
|
return self.del(res, params, nil)
|
|
}
|
|
|
|
func (self *SProxmoxClient) GenVM(name, node string, cores, memMB int) (*SInstance, error) {
|
|
|
|
vmId := self.GetClusterVmMaxId()
|
|
if vmId == -1 {
|
|
return nil, errors.Errorf("failed to get vm number by %d", vmId)
|
|
} else {
|
|
vmId++
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"vmid": vmId,
|
|
"name": name,
|
|
"ostype": "other",
|
|
"sockets": 1,
|
|
"cores": cores,
|
|
"cpu": "host",
|
|
"kvm": 1,
|
|
"hotplug": "network,disk,usb",
|
|
"memory": memMB,
|
|
"description": "",
|
|
"scsihw": "virtio-scsi-pci",
|
|
}
|
|
|
|
res := fmt.Sprintf("/nodes/%s/qemu", node)
|
|
_, err := self.post(res, jsonutils.Marshal(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vmIdRet := strconv.Itoa(vmId)
|
|
vm, err := self.GetInstance(vmIdRet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return vm, nil
|
|
}
|
|
|
|
type InstanceVnc struct {
|
|
Port int
|
|
Ticket string
|
|
Cert string
|
|
Password string
|
|
}
|
|
|
|
func (self *SProxmoxClient) GetVNCInfo(node string, vmId int) (*InstanceVnc, error) {
|
|
res := fmt.Sprintf("/nodes/%s/qemu/%d/vncproxy", node, vmId)
|
|
resp, err := self.post(res, map[string]interface{}{
|
|
"websocket": "1",
|
|
"generate-password": "1",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret := struct {
|
|
Data InstanceVnc
|
|
}{}
|
|
return &ret.Data, resp.Unmarshal(&ret)
|
|
}
|