diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index 8160e0ab..cb7a199e 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -32,7 +32,6 @@ type ( type ConfigQemu struct { 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 @@ -48,9 +47,9 @@ type ConfigQemu struct { 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 - Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead + Memory *QemuMemory `json:"memory,omitempty"` + Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations + Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead Onboot *bool `json:"onboot,omitempty"` Pool *PoolName `json:"pool,omitempty"` Protection *bool `json:"protection,omitempty"` @@ -82,6 +81,7 @@ type ConfigQemu struct { const ( ConfigQemu_Error_UnableToUpdateWithoutReboot string = "unable to update vm without rebooting" + ConfigQemu_Error_MemoryRequired string = "memory is required during creation" ) // Create - Tell Proxmox API to make the VM @@ -174,9 +174,6 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.Agent != nil { params["agent"] = config.Agent.mapToAPI(currentConfig.Agent) } - if config.Balloon >= 1 { - params["balloon"] = config.Balloon - } if config.Bios != "" { params["bios"] = config.Bios } @@ -208,9 +205,6 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.Machine != "" { params["machine"] = config.Machine } - if config.Memory != 0 { - params["memory"] = config.Memory - } if config.Name != "" { params["name"] = config.Name } @@ -286,6 +280,9 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.CloudInit != nil { itemsToDelete += config.CloudInit.mapToAPI(currentConfig.CloudInit, params, version) } + if config.Memory != nil { + itemsToDelete += config.Memory.mapToAPI(currentConfig.Memory, params) + } // Create EFI disk config.CreateQemuEfiParams(params) @@ -328,6 +325,7 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi config := ConfigQemu{ CloudInit: CloudInit{}.mapToSDK(params), + Memory: QemuMemory{}.mapToSDK(params), } if vmr != nil { @@ -343,12 +341,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if _, isSet := params["args"]; isSet { config.Args = strings.TrimSpace(params["args"].(string)) } - if _, isSet := params["balloon"]; isSet { - balloon := int(params["balloon"].(float64)) - if balloon > 0 { - config.Balloon = balloon - } - } //boot by default from hard disk (c), CD-ROM (d), network (n). if _, isSet := params["boot"]; isSet { config.Boot = params["boot"].(string) @@ -373,18 +365,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if _, isSet := params["machine"]; isSet { config.Machine = params["machine"].(string) } - if _, isSet := params["memory"]; isSet { - switch params["memory"].(type) { - case float64: - config.Memory = int(params["memory"].(float64)) - case string: - mem, err := strconv.ParseFloat(params["memory"].(string), 64) - if err != nil { - return nil, err - } - config.Memory = int(mem) - } - } if _, isSet := params["name"]; isSet { config.Name = params["name"].(string) } @@ -832,6 +812,32 @@ func (newConfig ConfigQemu) setAdvanced(currentConfig *ConfigQemu, rebootIfNeede func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err error) { // TODO test all other use cases // TODO has no context about changes caused by updating the vm + if current == nil { // Create + if config.Memory == nil { + return errors.New(ConfigQemu_Error_MemoryRequired) + } else { + if err = config.Memory.Validate(nil); err != nil { + return + } + } + if config.TPM != nil { + if err = config.TPM.Validate(nil); err != nil { + return + } + } + } else { // Update + if config.Memory != nil { + if err = config.Memory.Validate(current.Memory); err != nil { + return + } + } + if config.TPM != nil { + if err = config.TPM.Validate(current.TPM); err != nil { + return + } + } + } + // Shared if config.Agent != nil { if err = config.Agent.Validate(); err != nil { return @@ -853,17 +859,6 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err return } } - if config.TPM != nil { - if current == nil { - if err = config.TPM.Validate(nil); err != nil { - return - } - } else { - if err = config.TPM.Validate(current.TPM); err != nil { - return - } - } - } if config.Tags != nil { if err := Tag("").validate(*config.Tags); err != nil { return err diff --git a/proxmox/config_qemu_memory.go b/proxmox/config_qemu_memory.go new file mode 100644 index 00000000..3aba548b --- /dev/null +++ b/proxmox/config_qemu_memory.go @@ -0,0 +1,78 @@ +package proxmox + +import ( + "github.com/Telmate/proxmox-api-go/internal/parse" +) + +type QemuMemory struct { + CapacityMiB *QemuMemoryCapacity `json:"capacity,omitempty"` // min 1, max 4178944 + MinimumCapacityMiB *QemuMemoryBalloonCapacity `json:"balloon,omitempty"` // 0 to clear (balloon), max 4178944 + Shares *QemuMemoryShares `json:"shares,omitempty"` // 0 to clear, max 50000 +} + +func (config QemuMemory) mapToAPI(current *QemuMemory, params map[string]interface{}) string { + if current == nil { // create + if config.CapacityMiB != nil { + params["memory"] = *config.CapacityMiB + } + if config.MinimumCapacityMiB != nil { + params["balloon"] = *config.MinimumCapacityMiB + if config.CapacityMiB == nil { + params["memory"] = *config.MinimumCapacityMiB + } + } + if config.Shares != nil { + if *config.Shares > 0 { + params["shares"] = *config.Shares + } + } + return "" + } + // update + if config.CapacityMiB != nil { + params["memory"] = *config.CapacityMiB + if config.MinimumCapacityMiB == nil && current.MinimumCapacityMiB != nil && uint32(*current.MinimumCapacityMiB) > uint32(*config.CapacityMiB) { + params["balloon"] = *config.CapacityMiB + return ",shares" + } + } + if config.MinimumCapacityMiB != nil { + params["balloon"] = *config.MinimumCapacityMiB + if *config.MinimumCapacityMiB == 0 { + return ",shares" + } + } + if config.Shares != nil { + if *config.Shares == 0 { + return ",shares" + } + params["shares"] = *config.Shares + } + return "" +} + +func (QemuMemory) mapToSDK(params map[string]interface{}) *QemuMemory { + config := QemuMemory{} + if v, isSet := params["memory"]; isSet { + tmp, _ := parse.Uint(v) + tmpIntermediate := QemuMemoryCapacity(tmp) + config.CapacityMiB = &tmpIntermediate + } + if v, isSet := params["balloon"]; isSet { + tmp, _ := parse.Uint(v) + tmpIntermediate := QemuMemoryBalloonCapacity(tmp) + config.MinimumCapacityMiB = &tmpIntermediate + } + if v, isSet := params["shares"]; isSet { + tmp, _ := parse.Uint(v) + tmpIntermediate := QemuMemoryShares(tmp) + config.Shares = &tmpIntermediate + } + return &config +} + +type QemuMemoryBalloonCapacity uint32 // max 4178944 + +type QemuMemoryCapacity uint32 // max 4178944 + +type QemuMemoryShares uint16 // max 50000 diff --git a/proxmox/config_qemu_test.go b/proxmox/config_qemu_test.go index 138ff404..e41e0822 100644 --- a/proxmox/config_qemu_test.go +++ b/proxmox/config_qemu_test.go @@ -3187,6 +3187,49 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { currentConfig: ConfigQemu{Iso: &IsoFile{Storage: "test", File: "file.iso"}}, config: &ConfigQemu{Iso: &IsoFile{Storage: "NewStorage", File: "file.iso"}}, output: map[string]interface{}{"ide2": "NewStorage:iso/file.iso,media=cdrom"}}}}, + {category: `Memory`, + create: []test{ + {name: `MinimumCapacityMiB`, + config: &ConfigQemu{Memory: &QemuMemory{MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(1024))}}, + output: map[string]interface{}{ + "memory": QemuMemoryBalloonCapacity(1024), + "balloon": QemuMemoryBalloonCapacity(1024)}}, + {name: `Shares`, + config: &ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(40000))}}, + output: map[string]interface{}{"shares": QemuMemoryShares(40000)}}, + {name: `Shares 0`, + config: &ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(0))}}, + output: map[string]interface{}{}}}, + createUpdate: []test{ + {name: `CapacityMiB`, + config: &ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(2048))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(1024))}}, + output: map[string]interface{}{"memory": QemuMemoryCapacity(2048)}}}, + update: []test{ + {name: `CapacityMiB smaller then current MinimumCapacityMiB`, + config: &ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(1024))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(2048)), MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(2048))}}, + output: map[string]interface{}{"memory": QemuMemoryCapacity(1024), "balloon": QemuMemoryCapacity(1024), "delete": "shares"}}, + {name: `CapacityMiB smaller then current MinimumCapacityMiB and MinimumCapacityMiB lowered`, + config: &ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(1024)), MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(512))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(2048)), MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(2048))}}, + output: map[string]interface{}{"memory": QemuMemoryCapacity(1024), "balloon": QemuMemoryBalloonCapacity(512)}}, + {name: `MinimumCapacityMiB`, + config: &ConfigQemu{Memory: &QemuMemory{MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(1024))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(2048))}}, + output: map[string]interface{}{"balloon": QemuMemoryBalloonCapacity(1024)}}, + {name: `MinimumCapacityMiB 0`, + config: &ConfigQemu{Memory: &QemuMemory{MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(0))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(1024))}}, + output: map[string]interface{}{"balloon": QemuMemoryBalloonCapacity(0), "delete": "shares"}}, + {name: `Shares`, + config: &ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(40000))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(20000))}}, + output: map[string]interface{}{"shares": QemuMemoryShares(40000)}}, + {name: `Shares 0`, + config: &ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(0))}}, + currentConfig: ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(20000))}}, + output: map[string]interface{}{"delete": "shares"}}}}, {category: `Tags`, createUpdate: []test{ {name: `Tags Empty`, @@ -3236,6 +3279,9 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { func Test_ConfigQemu_mapToStruct(t *testing.T) { baseConfig := func(config ConfigQemu) *ConfigQemu { + if config.Memory == nil { + config.Memory = &QemuMemory{} + } return &config } parseIP := func(rawIP string) (ip netip.Addr) { @@ -5415,6 +5461,35 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { File: "debian-11.0.0-amd64-netinst.iso", Storage: "local", SizeInKibibytes: "377M"}}}}}})}}}, + {category: `Memory`, + tests: []test{ + {name: `All float64`, + input: map[string]interface{}{ + "memory": float64(1024), + "balloon": float64(512), + "shares": float64(50)}, + output: baseConfig(ConfigQemu{Memory: &QemuMemory{ + CapacityMiB: util.Pointer(QemuMemoryCapacity(1024)), + MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(512)), + Shares: util.Pointer(QemuMemoryShares(50))}})}, + {name: `All string`, + input: map[string]interface{}{ + "memory": "1024", + "balloon": "512", + "shares": "50"}, + output: baseConfig(ConfigQemu{Memory: &QemuMemory{ + CapacityMiB: util.Pointer(QemuMemoryCapacity(1024)), + MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(512)), + Shares: util.Pointer(QemuMemoryShares(50))}})}, + {name: `memory`, + input: map[string]interface{}{"memory": float64(2000)}, + output: baseConfig(ConfigQemu{Memory: &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(2000))}})}, + {name: `balloon`, + input: map[string]interface{}{"balloon": float64(1000)}, + output: baseConfig(ConfigQemu{Memory: &QemuMemory{MinimumCapacityMiB: util.Pointer(QemuMemoryBalloonCapacity(1000))}})}, + {name: `shares`, + input: map[string]interface{}{"shares": float64(100)}, + output: baseConfig(ConfigQemu{Memory: &QemuMemory{Shares: util.Pointer(QemuMemoryShares(100))}})}}}, {category: `Node`, tests: []test{ {name: `vmr nil`,