Skip to content

Commit

Permalink
Merge pull request #327 from Tinyblargon/Overhaul#323
Browse files Browse the repository at this point in the history
Overhaul: Qemu Guest Agent
  • Loading branch information
Tinyblargon authored Apr 18, 2024
2 parents 2f0ce9e + ff84c23 commit 5a3afe8
Show file tree
Hide file tree
Showing 5 changed files with 342 additions and 64 deletions.
127 changes: 63 additions & 64 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,60 +38,60 @@ type AgentNetworkInterface struct {

// ConfigQemu - Proxmox API QEMU options
type ConfigQemu struct {
Agent int `json:"agent,omitempty"` // TODO should probably be a bool
Args string `json:"args,omitempty"`
Balloon int `json:"balloon,omitempty"` // TODO should probably be a bool
Bios string `json:"bios,omitempty"`
Boot string `json:"boot,omitempty"` // TODO should be an array of custom enums
BootDisk string `json:"bootdisk,omitempty"` // TODO discuss deprecation? Only returned as it's deprecated in the proxmox api
CIcustom string `json:"cicustom,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
CIpassword string `json:"cipassword,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
CIuser string `json:"ciuser,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Description string `json:"description,omitempty"`
Disks *QemuStorages `json:"disks,omitempty"`
EFIDisk QemuDevice `json:"efidisk,omitempty"` // TODO should be a struct
FullClone *int `json:"fullclone,omitempty"` // TODO should probably be a bool
HaGroup string `json:"hagroup,omitempty"`
HaState string `json:"hastate,omitempty"` // TODO should be custom type with enum
Hookscript string `json:"hookscript,omitempty"`
Hotplug string `json:"hotplug,omitempty"` // TODO should be a struct
Ipconfig IpconfigMap `json:"ipconfig,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Iso *IsoFile `json:"iso,omitempty"` // Same as Disks.Ide.Disk_2.CdRom.Iso
LinkedVmId uint `json:"linked_id,omitempty"` // Only returned setting it has no effect
Machine string `json:"machine,omitempty"` // TODO should be custom type with enum
Memory int `json:"memory,omitempty"` // TODO should be uint
Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations
Nameserver string `json:"nameserver,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead
Onboot *bool `json:"onboot,omitempty"`
Pool string `json:"pool,omitempty"` // TODO should be custom type as there are character and length limitations
Protection *bool `json:"protection,omitempty"`
QemuCores int `json:"cores,omitempty"` // TODO should be uint
QemuCpu string `json:"cpu,omitempty"` // TODO should be custom type with enum
QemuDisks QemuDevices `json:"disk,omitempty"` // DEPRECATED use Disks *QemuStorages instead
QemuIso string `json:"qemuiso,omitempty"` // DEPRECATED use Iso *IsoFile instead
QemuKVM *bool `json:"kvm,omitempty"`
QemuNetworks QemuDevices `json:"network,omitempty"` // TODO should be a struct
QemuNuma *bool `json:"numa,omitempty"`
QemuOs string `json:"ostype,omitempty"`
QemuPCIDevices QemuDevices `json:"hostpci,omitempty"` // TODO should be a struct
QemuPxe bool `json:"pxe,omitempty"`
QemuSerials QemuDevices `json:"serial,omitempty"` // TODO should be a struct
QemuSockets int `json:"sockets,omitempty"` // TODO should be uint
QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a struct
QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct
QemuVcpus int `json:"vcpus,omitempty"` // TODO should be uint
QemuVga QemuDevice `json:"vga,omitempty"` // TODO should be a struct
RNGDrive QemuDevice `json:"rng0,omitempty"` // TODO should be a struct
Scsihw string `json:"scsihw,omitempty"` // TODO should be custom type with enum
Searchdomain string `json:"searchdomain,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum?
Sshkeys string `json:"sshkeys,omitempty"` // TODO should be an array of strings
Startup string `json:"startup,omitempty"` // TODO should be a struct?
TPM *TpmState `json:"tpm,omitempty"`
Tablet *bool `json:"tablet,omitempty"`
Tags string `json:"tags,omitempty"` // TODO should be an array of a custom type as there are character and length limitations
VmID int `json:"vmid,omitempty"` // TODO should be a custom type as there are limitations
Agent *QemuGuestAgent `json:"agent,omitempty"`
Args string `json:"args,omitempty"`
Balloon int `json:"balloon,omitempty"` // TODO should probably be a bool
Bios string `json:"bios,omitempty"`
Boot string `json:"boot,omitempty"` // TODO should be an array of custom enums
BootDisk string `json:"bootdisk,omitempty"` // TODO discuss deprecation? Only returned as it's deprecated in the proxmox api
CIcustom string `json:"cicustom,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
CIpassword string `json:"cipassword,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
CIuser string `json:"ciuser,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Description string `json:"description,omitempty"`
Disks *QemuStorages `json:"disks,omitempty"`
EFIDisk QemuDevice `json:"efidisk,omitempty"` // TODO should be a struct
FullClone *int `json:"fullclone,omitempty"` // TODO should probably be a bool
HaGroup string `json:"hagroup,omitempty"`
HaState string `json:"hastate,omitempty"` // TODO should be custom type with enum
Hookscript string `json:"hookscript,omitempty"`
Hotplug string `json:"hotplug,omitempty"` // TODO should be a struct
Ipconfig IpconfigMap `json:"ipconfig,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Iso *IsoFile `json:"iso,omitempty"` // Same as Disks.Ide.Disk_2.CdRom.Iso
LinkedVmId uint `json:"linked_id,omitempty"` // Only returned setting it has no effect
Machine string `json:"machine,omitempty"` // TODO should be custom type with enum
Memory int `json:"memory,omitempty"` // TODO should be uint
Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations
Nameserver string `json:"nameserver,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead
Onboot *bool `json:"onboot,omitempty"`
Pool string `json:"pool,omitempty"` // TODO should be custom type as there are character and length limitations
Protection *bool `json:"protection,omitempty"`
QemuCores int `json:"cores,omitempty"` // TODO should be uint
QemuCpu string `json:"cpu,omitempty"` // TODO should be custom type with enum
QemuDisks QemuDevices `json:"disk,omitempty"` // DEPRECATED use Disks *QemuStorages instead
QemuIso string `json:"qemuiso,omitempty"` // DEPRECATED use Iso *IsoFile instead
QemuKVM *bool `json:"kvm,omitempty"`
QemuNetworks QemuDevices `json:"network,omitempty"` // TODO should be a struct
QemuNuma *bool `json:"numa,omitempty"`
QemuOs string `json:"ostype,omitempty"`
QemuPCIDevices QemuDevices `json:"hostpci,omitempty"` // TODO should be a struct
QemuPxe bool `json:"pxe,omitempty"`
QemuSerials QemuDevices `json:"serial,omitempty"` // TODO should be a struct
QemuSockets int `json:"sockets,omitempty"` // TODO should be uint
QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a struct
QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct
QemuVcpus int `json:"vcpus,omitempty"` // TODO should be uint
QemuVga QemuDevice `json:"vga,omitempty"` // TODO should be a struct
RNGDrive QemuDevice `json:"rng0,omitempty"` // TODO should be a struct
Scsihw string `json:"scsihw,omitempty"` // TODO should be custom type with enum
Searchdomain string `json:"searchdomain,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum?
Sshkeys string `json:"sshkeys,omitempty"` // TODO should be an array of strings
Startup string `json:"startup,omitempty"` // TODO should be a struct?
TPM *TpmState `json:"tpm,omitempty"`
Tablet *bool `json:"tablet,omitempty"`
Tags string `json:"tags,omitempty"` // TODO should be an array of a custom type as there are character and length limitations
VmID int `json:"vmid,omitempty"` // TODO should be a custom type as there are limitations
}

const (
Expand Down Expand Up @@ -188,8 +188,8 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire
if config.Args != "" {
params["args"] = config.Args
}
if config.Agent != 0 {
params["agent"] = config.Agent
if config.Agent != nil {
params["agent"] = config.Agent.mapToAPI(currentConfig.Agent)
}
if config.Balloon >= 1 {
params["balloon"] = config.Balloon
Expand Down Expand Up @@ -366,14 +366,8 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
config.VmID = vmr.vmId
}

if _, isSet := params["agent"]; isSet {
switch params["agent"].(type) {
case float64:
config.Agent = int(params["agent"].(float64))
case string:
AgentConfList := strings.Split(params["agent"].(string), ",")
config.Agent, _ = strconv.Atoi(AgentConfList[0])
}
if v, isSet := params["agent"]; isSet {
config.Agent = QemuGuestAgent{}.mapToSDK(v.(string))
}
if _, isSet := params["args"]; isSet {
config.Args = strings.TrimSpace(params["args"].(string))
Expand Down Expand Up @@ -894,6 +888,11 @@ func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeede
func (config ConfigQemu) Validate(current *ConfigQemu) (err error) {
// TODO test all other use cases
// TODO has no context about changes caused by updating the vm
if config.Agent != nil {
if err = config.Agent.Validate(); err != nil {
return
}
}
if config.Disks != nil {
err = config.Disks.Validate()
if err != nil {
Expand Down
94 changes: 94 additions & 0 deletions proxmox/config_qemu_guestagent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package proxmox

import (
"errors"
"strconv"
"strings"

"github.com/Telmate/proxmox-api-go/internal/util"
)

type QemuGuestAgent struct {
Enable *bool `json:"enable,omitempty"` // Optional
Type *QemuGuestAgentType `json:"type,omitempty"` // Optional
Freeze *bool `json:"freeze,omitempty"` // Optional
FsTrim *bool `json:"trim,omitempty"` // Optional
}

func (newSetting QemuGuestAgent) mapToAPI(currentSettings *QemuGuestAgent) string {
var params string
tmpEnable := "0"
if newSetting.Enable != nil {
if *newSetting.Enable {
tmpEnable = "1"
}
} else if currentSettings != nil && currentSettings.Enable != nil {
if *currentSettings.Enable {
tmpEnable = "1"
}
}
if newSetting.Freeze != nil {
params += ",freeze-fs-on-backup=" + boolToIntString(*newSetting.Freeze)
} else if currentSettings != nil && currentSettings.Freeze != nil {
params += ",freeze-fs-on-backup=" + boolToIntString(*currentSettings.Freeze)
}
if newSetting.FsTrim != nil {
params += ",fstrim_cloned_disks=" + boolToIntString(*newSetting.FsTrim)
} else if currentSettings != nil && currentSettings.FsTrim != nil {
params += ",fstrim_cloned_disks=" + boolToIntString(*currentSettings.FsTrim)
}
if newSetting.Type != nil {
if *newSetting.Type != QemuGuestAgentType_None {
params += ",type=" + strings.ToLower(string(*newSetting.Type))
}
} else if currentSettings != nil && currentSettings.Type != nil {
params += ",type=" + strings.ToLower(string(*currentSettings.Type))
}
return tmpEnable + params
}

func (QemuGuestAgent) mapToSDK(params string) *QemuGuestAgent {
config := QemuGuestAgent{}
tmpEnable, _ := strconv.ParseBool(params[0:1])
config.Enable = &tmpEnable
tmpParams := splitStringOfSettings(params)
if v, isSet := tmpParams["freeze-fs-on-backup"]; isSet {
tmpBool, _ := strconv.ParseBool(v.(string))
config.Freeze = &tmpBool
}
if v, isSet := tmpParams["fstrim_cloned_disks"]; isSet {
tmpBool, _ := strconv.ParseBool(v.(string))
config.FsTrim = &tmpBool
}
if v, isSet := tmpParams["type"]; isSet {
config.Type = util.Pointer(QemuGuestAgentType(v.(string)))
}
return &config
}

func (setting QemuGuestAgent) Validate() error {
if setting.Type != nil {
return setting.Type.Validate()
}
return nil
}

type QemuGuestAgentType string // enum

const (
QemuGuestAgentType_Isa QemuGuestAgentType = "isa"
QemuGuestAgentType_VirtIO QemuGuestAgentType = "virtio"
QemuGuestAgentType_None QemuGuestAgentType = "" // Used to unset the value. Proxmox enforces the default.
QemuGuestAgentType_Error_Invalid string = `invalid qemu guest agent type, should one of [` + string(QemuGuestAgentType_Isa) + `, ` + string(QemuGuestAgentType_VirtIO) + `, ""]`
)

func (q QemuGuestAgentType) Validate() error {
if q == QemuGuestAgentType_None {
return nil
}
switch QemuGuestAgentType(strings.ToLower(string(q))) {
case QemuGuestAgentType_Isa, QemuGuestAgentType_VirtIO:
return nil
}
return errors.New(QemuGuestAgentType_Error_Invalid)
}
51 changes: 51 additions & 0 deletions proxmox/config_qemu_guestagent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package proxmox

import (
"errors"
"testing"

"github.com/Telmate/proxmox-api-go/internal/util"
"github.com/stretchr/testify/require"
)

func Test_QemuGuestAgent_Validate(t *testing.T) {
tests := []struct {
name string
input QemuGuestAgent
output error
}{
{name: "Valid Type",
input: QemuGuestAgent{Type: util.Pointer(QemuGuestAgentType("isa"))}},
{name: "Invalid Type",
input: QemuGuestAgent{Type: util.Pointer(QemuGuestAgentType("invalid"))},
output: errors.New(QemuGuestAgentType_Error_Invalid)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.output, test.input.Validate())
})
}
}

func Test_QemuGuestAgentType_Validate(t *testing.T) {
tests := []struct {
name string
input QemuGuestAgentType
output error
}{
{name: `Valid ""`,
input: QemuGuestAgentType("")},
{name: "Valid lowercase",
input: QemuGuestAgentType("virtio")},
{name: "Valid UpperCase",
input: QemuGuestAgentType("VirtIO")},
{name: `Invalid`,
input: QemuGuestAgentType("invalid"),
output: errors.New(QemuGuestAgentType_Error_Invalid)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.output, test.input.Validate())
})
}
}
Loading

0 comments on commit 5a3afe8

Please sign in to comment.