Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul: Qemu Guest Agent #327

Merged
merged 4 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading