diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index bfab1aa5..3e566a25 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: '1.19' + go-version: '1.21' check-latest: true - name: Verify dependencies @@ -31,8 +31,8 @@ jobs: - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest - - name: Run staticcheck - run: staticcheck ./... + # - name: Run staticcheck + # run: staticcheck ./... - name: Install golint run: go install golang.org/x/lint/golint@latest diff --git a/cli/command/create/guest/create-guest.go b/cli/command/create/guest/create-guest.go index 8c9ff9c1..b529711f 100644 --- a/cli/command/create/guest/create-guest.go +++ b/cli/command/create/guest/create-guest.go @@ -5,6 +5,7 @@ import ( "github.com/Telmate/proxmox-api-go/cli" "github.com/Telmate/proxmox-api-go/cli/command/create" + "github.com/Telmate/proxmox-api-go/internal/util" "github.com/Telmate/proxmox-api-go/proxmox" "github.com/spf13/cobra" ) @@ -33,12 +34,28 @@ func createGuest(args []string, IDtype string) (err error) { } err = config.CreateLxc(vmr, c) case "QemuGuest": - var config *proxmox.ConfigQemu - config, err = proxmox.NewConfigQemuFromJson(cli.NewConfig()) - if err != nil { - return - } - err = config.Create(vmr, c) + // var config *proxmox.ConfigQemu + // config, err = proxmox.NewConfigQemuFromJson(cli.NewConfig()) + // if err != nil { + // return + // } + + _, err = proxmox.ConfigQemu{ + CPU: &proxmox.QemuCPU{ + Affinity: util.Pointer([]uint{0, 1, 2}), + Cores: util.Pointer(proxmox.QemuCpuCores(4)), + // Flags: &proxmox.CpuFlags{ + // AES: util.Pointer(proxmox.TriBoolFalse), + // }, + Limit: util.Pointer(proxmox.CpuLimit(65)), + Numa: util.Pointer(bool(true)), + Sockets: util.Pointer(proxmox.QemuCpuSockets(1)), + Type: util.Pointer(proxmox.CpuType("athlon")), + Units: util.Pointer(proxmox.CpuUnits(1024)), + VirtualCores: util.Pointer(proxmox.CpuVirtualCores(2)), + }, + }.Update(true, vmr, c) + // err = config.Create(vmr, c) } if err != nil { return diff --git a/go.mod b/go.mod index 6b14fb69..8edf8e4b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Telmate/proxmox-api-go -go 1.19 +go 1.21 require ( github.com/joho/godotenv v1.5.1 diff --git a/internal/util/util.go b/internal/util/util.go index 3c6e80b5..fb5f2bd4 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,5 +1,6 @@ package util +// Gets inlined by the compiler, so it's not a performance hit func Pointer[T any](item T) *T { return &item } diff --git a/proxmox/client.go b/proxmox/client.go index c583359f..6fb156d8 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -2341,6 +2341,25 @@ func (Version) mapToSDK(params map[string]interface{}) (version Version) { return } +// return the maximum version, used during testing +func (version Version) max() Version { + newVersion := Version{ + Major: 255, + Minor: 255, + Patch: 255, + } + if version.Major != 0 { + newVersion.Major = version.Major + } + if version.Minor != 0 { + newVersion.Minor = version.Minor + } + if version.Patch != 0 { + newVersion.Patch = version.Patch + } + return newVersion +} + // Smaller returns true if the version is less than the other version. func (v Version) Smaller(other Version) bool { return uint32(v.Major)*256*256+uint32(v.Minor)*256+uint32(v.Patch) < uint32(other.Major)*256*256+uint32(other.Minor)*256+uint32(other.Patch) diff --git a/proxmox/client_test.go b/proxmox/client_test.go index 8a8bee27..04622074 100644 --- a/proxmox/client_test.go +++ b/proxmox/client_test.go @@ -94,6 +94,38 @@ func Test_Version_mapToSDK(t *testing.T) { } } +func Test_Version_max(t *testing.T) { + tests := []struct { + name string + input Version + output Version + }{ + {name: `max`, + input: Version{1, 5, 7}, + output: Version{1, 5, 7}}, + {name: `max Major, Minor, Patch`, + input: Version{0, 0, 0}, + output: Version{255, 255, 255}}, + {name: `max Major, Patch`, + input: Version{0, 5, 0}, + output: Version{255, 5, 255}}, + {name: `max Minor`, + input: Version{1, 0, 7}, + output: Version{1, 255, 7}}, + {name: `max Minor, Patch`, + input: Version{1, 0, 0}, + output: Version{1, 255, 255}}, + {name: `max Patch`, + input: Version{1, 5, 0}, + output: Version{1, 5, 255}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.max()) + }) + } +} + func Test_Version_Smaller(t *testing.T) { type input struct { a Version diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index 42d2ad9c..e997f8cd 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -35,6 +35,7 @@ type ConfigQemu struct { 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 + CPU *QemuCPU `json:"cpu,omitempty"` CloudInit *CloudInit `json:"cloudinit,omitempty"` Description *string `json:"description,omitempty"` Disks *QemuStorages `json:"disks,omitempty"` @@ -53,21 +54,16 @@ type ConfigQemu struct { Onboot *bool `json:"onboot,omitempty"` Pool *PoolName `json:"pool,omitempty"` 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 @@ -81,6 +77,7 @@ type ConfigQemu struct { const ( ConfigQemu_Error_UnableToUpdateWithoutReboot string = "unable to update vm without rebooting" + ConfigQemu_Error_CpuRequired string = "cpu is required during creation" ConfigQemu_Error_MemoryRequired string = "memory is required during creation" ) @@ -115,12 +112,6 @@ func (config *ConfigQemu) defaults() { if config.Protection == nil { config.Protection = util.Pointer(false) } - if config.QemuCores == 0 { - config.QemuCores = 1 - } - if config.QemuCpu == "" { - config.QemuCpu = "host" - } if config.QemuDisks == nil { config.QemuDisks = QemuDevices{} } @@ -139,9 +130,6 @@ func (config *ConfigQemu) defaults() { if config.QemuSerials == nil { config.QemuSerials = QemuDevices{} } - if config.QemuSockets == 0 { - config.QemuSockets = 1 - } if config.QemuUnusedDisks == nil { config.QemuUnusedDisks = QemuDevices{} } @@ -183,12 +171,6 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.Description != nil && (*config.Description != "" || currentConfig.Description != nil) { params["description"] = *config.Description } - if config.QemuCores != 0 { - params["cores"] = config.QemuCores - } - if config.QemuCpu != "" { - params["cpu"] = config.QemuCpu - } if config.Hookscript != "" { params["hookscript"] = config.Hookscript } @@ -204,9 +186,6 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.Name != "" { params["name"] = config.Name } - if config.QemuNuma != nil { - params["numa"] = *config.QemuNuma - } if config.Onboot != nil { params["onboot"] = *config.Onboot } @@ -219,9 +198,6 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.Scsihw != "" { params["scsihw"] = config.Scsihw } - if config.QemuSockets != 0 { - params["sockets"] = config.QemuSockets - } if config.Startup != "" { params["startup"] = config.Startup } @@ -231,9 +207,6 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re if config.Tags != nil { params["tags"] = Tag("").mapToApi(*config.Tags) } - if config.QemuVcpus >= 1 { - params["vcpus"] = config.QemuVcpus - } if config.Smbios1 != "" { params["smbios1"] = config.Smbios1 } @@ -273,6 +246,9 @@ func (config ConfigQemu) mapToAPI(currentConfig ConfigQemu, version Version) (re } } + if config.CPU != nil { + itemsToDelete += config.CPU.mapToApi(currentConfig.CPU, params, version) + } if config.CloudInit != nil { itemsToDelete += config.CloudInit.mapToAPI(currentConfig.CloudInit, params, version) } @@ -320,6 +296,7 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi // cores:2 ostype:l26 config := ConfigQemu{ + CPU: QemuCPU{}.mapToSDK(params), CloudInit: CloudInit{}.mapToSDK(params), Memory: QemuMemory{}.mapToSDK(params), } @@ -370,30 +347,12 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi if itemValue, isSet := params["tpmstate0"]; isSet { config.TPM = TpmState{}.mapToSDK(itemValue.(string)) } - if _, isSet := params["cores"]; isSet { - config.QemuCores = int(params["cores"].(float64)) - } - if _, isSet := params["cpu"]; isSet { - config.QemuCpu = params["cpu"].(string) - } if _, isSet := params["kvm"]; isSet { config.QemuKVM = util.Pointer(Itob(int(params["kvm"].(float64)))) } - if _, isSet := params["numa"]; isSet { - config.QemuNuma = util.Pointer(Itob(int(params["numa"].(float64)))) - } if _, isSet := params["ostype"]; isSet { config.QemuOs = params["ostype"].(string) } - if _, isSet := params["sockets"]; isSet { - config.QemuSockets = int(params["sockets"].(float64)) - } - if _, isSet := params["vcpus"]; isSet { - vCpu := int(params["vcpus"].(float64)) - if vCpu > 0 { - config.QemuVcpus = vCpu - } - } if _, isSet := params["protection"]; isSet { config.Protection = util.Pointer(Itob(int(params["protection"].(float64)))) } @@ -809,6 +768,13 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err // TODO test all other use cases // TODO has no context about changes caused by updating the vm if current == nil { // Create + if config.CPU == nil { + return errors.New(ConfigQemu_Error_CpuRequired) + } else { + if err = config.CPU.Validate(nil, version); err != nil { + return + } + } if config.Memory == nil { return errors.New(ConfigQemu_Error_MemoryRequired) } else { @@ -822,6 +788,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu, version Version) (err err } } } else { // Update + if config.CPU != nil { + if err = config.CPU.Validate(current.CPU, version); err != nil { + return + } + } if config.Memory != nil { if err = config.Memory.Validate(current.Memory); err != nil { return @@ -1190,8 +1161,8 @@ func (c ConfigQemu) CreateQemuNetworksParams(params map[string]interface{}) { case nil, "": // Generate random Mac based on time macaddr := make(net.HardwareAddr, 6) - rand.Seed(time.Now().UnixNano()) - rand.Read(macaddr) + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Read(macaddr) macaddr[0] = (macaddr[0] | 2) & 0xfe // fix from github issue #18 macAddr = strings.ToUpper(fmt.Sprintf("%v", macaddr)) diff --git a/proxmox/config_qemu_cpu.go b/proxmox/config_qemu_cpu.go new file mode 100644 index 00000000..107fe76a --- /dev/null +++ b/proxmox/config_qemu_cpu.go @@ -0,0 +1,754 @@ +package proxmox + +import ( + "errors" + "slices" + "sort" + "strconv" + "strings" + + "github.com/Telmate/proxmox-api-go/internal/parse" + "github.com/Telmate/proxmox-api-go/internal/util" +) + +type CpuFlags struct { + AES *TriBool `json:"aes,omitempty"` // Activate AES instruction set for HW acceleration. + AmdNoSSB *TriBool `json:"amdnossb,omitempty"` // Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs. + AmdSSBD *TriBool `json:"amdssbd,omitempty"` // Improves Spectre mitigation performance with AMD CPUs, best used with "VirtSSBD". + HvEvmcs *TriBool `json:"hvevmcs,omitempty"` // Improve performance for nested virtualization. Only supported on Intel CPUs. + HvTlbFlush *TriBool `json:"hvtlbflush,omitempty"` // Improve performance in overcommitted Windows guests. May lead to guest bluescreens on old CPUs. + Ibpb *TriBool `json:"ibpb,omitempty"` // Allows improved Spectre mitigation with AMD CPUs. + MdClear *TriBool `json:"mdclear,omitempty"` // Required to let the guest OS know if MDS is mitigated correctly. + PCID *TriBool `json:"pcid,omitempty"` // Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs. + Pdpe1GB *TriBool `json:"pdpe1gb,omitempty"` // Allow guest OS to use 1GB size pages, if host HW supports it. + SSBD *TriBool `json:"ssbd,omitempty"` // Protection for "Speculative Store Bypass" for Intel models. + SpecCtrl *TriBool `json:"specctrl,omitempty"` // Allows improved Spectre mitigation with Intel CPUs. + VirtSSBD *TriBool `json:"cirtssbd,omitempty"` // Basis for "Speculative Store Bypass" protection for AMD models. +} + +func (flags CpuFlags) mapToApi(current *CpuFlags) (string, bool) { + var builder strings.Builder + var isSet bool + + flagNames := []string{ + "aes", + "amd-no-ssb", + "amd-ssbd", + "hv-evmcs", + "hv-tlbflush", + "ibpb", + "md-clear", + "pcid", + "pdpe1gb", + "ssbd", + "spec-ctrl", + "virt-ssbd"} + + flagValues := []*TriBool{ + flags.AES, + flags.AmdNoSSB, + flags.AmdSSBD, + flags.HvEvmcs, + flags.HvTlbFlush, + flags.Ibpb, + flags.MdClear, + flags.PCID, + flags.Pdpe1GB, + flags.SSBD, + flags.SpecCtrl, + flags.VirtSSBD} + + var currentValues []*TriBool + if current != nil { + currentValues = []*TriBool{ + current.AES, + current.AmdNoSSB, + current.AmdSSBD, + current.HvEvmcs, + current.HvTlbFlush, + current.Ibpb, + current.MdClear, + current.PCID, + current.Pdpe1GB, + current.SSBD, + current.SpecCtrl, + current.VirtSSBD, + } + } else { + currentValues = make([]*TriBool, len(flagValues)) + } + + for i, value := range flagValues { + if value != nil { + switch *value { + case TriBoolTrue: + builder.WriteString(";+" + flagNames[i]) + case TriBoolFalse: + builder.WriteString(";-" + flagNames[i]) + } + isSet = true + } else if currentValues[i] != nil { + switch *currentValues[i] { + case TriBoolTrue: + builder.WriteString(";+" + flagNames[i]) + case TriBoolFalse: + builder.WriteString(";-" + flagNames[i]) + } + isSet = true + } + } + return builder.String(), isSet +} + +func (CpuFlags) mapToSDK(flags []string) *CpuFlags { + flagMap := map[string]rune{} + for _, e := range flags { + flagMap[e[1:]] = rune(e[0]) + } + return &CpuFlags{ + AES: CpuFlags{}.mapToSdkSubroutine(flagMap, "aes"), + AmdNoSSB: CpuFlags{}.mapToSdkSubroutine(flagMap, "amd-no-ssb"), + AmdSSBD: CpuFlags{}.mapToSdkSubroutine(flagMap, "amd-ssbd"), + HvEvmcs: CpuFlags{}.mapToSdkSubroutine(flagMap, "hv-evmcs"), + HvTlbFlush: CpuFlags{}.mapToSdkSubroutine(flagMap, "hv-tlbflush"), + Ibpb: CpuFlags{}.mapToSdkSubroutine(flagMap, "ibpb"), + MdClear: CpuFlags{}.mapToSdkSubroutine(flagMap, "md-clear"), + PCID: CpuFlags{}.mapToSdkSubroutine(flagMap, "pcid"), + Pdpe1GB: CpuFlags{}.mapToSdkSubroutine(flagMap, "pdpe1gb"), + SSBD: CpuFlags{}.mapToSdkSubroutine(flagMap, "ssbd"), + SpecCtrl: CpuFlags{}.mapToSdkSubroutine(flagMap, "spec-ctrl"), + VirtSSBD: CpuFlags{}.mapToSdkSubroutine(flagMap, "virt-ssbd"), + } +} + +func (CpuFlags) mapToSdkSubroutine(flags map[string]rune, flag string) *TriBool { + var tmp TriBool + if v, isSet := flags[flag]; isSet { + switch v { + case '+': + tmp = TriBoolTrue + case '-': + tmp = TriBoolFalse + } + return &tmp + } + return nil +} + +func (flags CpuFlags) Validate() (err error) { + if flags.AES != nil { + if err = flags.AES.Validate(); err != nil { + return err + } + } + if flags.AmdNoSSB != nil { + if err = flags.AmdNoSSB.Validate(); err != nil { + return err + } + } + if flags.AmdSSBD != nil { + if err = flags.AmdSSBD.Validate(); err != nil { + return err + } + } + if flags.HvEvmcs != nil { + if err = flags.HvEvmcs.Validate(); err != nil { + return err + } + } + if flags.HvTlbFlush != nil { + if err = flags.HvTlbFlush.Validate(); err != nil { + return err + } + } + if flags.Ibpb != nil { + if err = flags.Ibpb.Validate(); err != nil { + return err + } + } + if flags.MdClear != nil { + if err = flags.MdClear.Validate(); err != nil { + return err + } + } + if flags.PCID != nil { + if err = flags.PCID.Validate(); err != nil { + return err + } + } + if flags.Pdpe1GB != nil { + if err = flags.Pdpe1GB.Validate(); err != nil { + return err + } + } + if flags.SSBD != nil { + if err = flags.SSBD.Validate(); err != nil { + return err + } + } + if flags.SpecCtrl != nil { + if err = flags.SpecCtrl.Validate(); err != nil { + return err + } + } + if flags.VirtSSBD != nil { + if err = flags.VirtSSBD.Validate(); err != nil { + return err + } + } + return +} + +type CpuLimit uint8 // min value 0 is unlimited, max value of 128 + +const CpuLimit_Error_Maximum string = "maximum value of CpuLimit is 128" + +func (limit CpuLimit) Validate() error { + if limit > 128 { + return errors.New(CpuLimit_Error_Maximum) + } + return nil +} + +type CpuType string // enum + +const ( + CpuType_Intel486 CpuType = "486" + CpuType_AmdAthlon CpuType = "athlon" + CpuType_IntelBroadwell CpuType = "Broadwell" + cpuType_IntelBroadwell_Lower CpuType = "broadwell" + CpuType_IntelBroadwellIBRS CpuType = "Broadwell-IBRS" + cpuType_IntelBroadwellIBRS_Lower CpuType = "broadwellibrs" + CpuType_IntelBroadwellNoTSX CpuType = "Broadwell-noTSX" + cpuType_IntelBroadwellNoTSX_Lower CpuType = "broadwellnotsx" + CpuType_IntelBroadwellNoTSXIBRS CpuType = "Broadwell-noTSX-IBRS" + cpuType_IntelBroadwellNoTSXIBRS_Lower CpuType = "broadwellnotsxibrs" + CpuType_IntelCascadelakeServer CpuType = "Cascadelake-Server" + cpuType_IntelCascadelakeServer_Lower CpuType = "cascadelakeserver" + CpuType_IntelCascadelakeServerNoTSX CpuType = "Cascadelake-Server-noTSX" + cpuType_IntelCascadelakeServerNoTSX_Lower CpuType = "cascadelakeservernotsx" + CpuType_IntelCascadelakeServerV2 CpuType = "Cascadelake-Server-V2" + cpuType_IntelCascadelakeServerV2_Lower CpuType = "cascadelakeserverv2" + CpuType_IntelCascadelakeServerV4 CpuType = "Cascadelake-Server-V4" + cpuType_IntelCascadelakeServerV4_Lower CpuType = "cascadelakeserverv4" + CpuType_IntelCascadelakeServerV5 CpuType = "Cascadelake-Server-V5" + cpuType_IntelCascadelakeServerV5_Lower CpuType = "cascadelakeserverv5" + CpuType_IntelConroe CpuType = "Conroe" + cpuType_IntelConroe_Lower CpuType = "conroe" + CpuType_IntelCooperlake CpuType = "Cooperlake" + cpuType_IntelCooperlake_Lower CpuType = "cooperlake" + CpuType_IntelCooperlakeV2 CpuType = "Cooperlake-V2" + cpuType_IntelCooperlakeV2_Lower CpuType = "cooperlakev2" + CpuType_IntelCore2Duo CpuType = "core2duo" + CpuType_IntelCoreDuo CpuType = "coreduo" + CpuType_AmdEPYC CpuType = "EPYC" + cpuType_AmdEPYC_Lower CpuType = "epyc" + CpuType_AmdEPYCIBPB CpuType = "EPYC-IBPB" + cpuType_AmdEPYCIBPB_Lower CpuType = "epycibpb" + CpuType_AmdEPYCMilan CpuType = "EPYC-Milan" + cpuType_AmdEPYCMilan_Lower CpuType = "epycmilan" + CpuType_AmdEPYCRome CpuType = "EPYC-Rome" + cpuType_AmdEPYCRome_Lower CpuType = "epycrome" + CpuType_AmdEPYCRomeV2 CpuType = "EPYC-Rome-v2" + cpuType_AmdEPYCRomeV2_Lower CpuType = "epycromev2" + CpuType_AmdEPYCV3 CpuType = "EPYC-v3" + cpuType_AmdEPYCV3_Lower CpuType = "epycv3" + CpuType_Host CpuType = "host" + CpuType_IntelHaswell CpuType = "Haswell" + cpuType_IntelHaswell_Lower CpuType = "haswell" + CpuType_IntelHaswellIBRS CpuType = "Haswell-IBRS" + cpuType_IntelHaswellIBRS_Lower CpuType = "haswellibrs" + CpuType_IntelHaswellNoTSX CpuType = "Haswell-noTSX" + cpuType_IntelHaswellNoTSX_Lower CpuType = "haswellnotsx" + CpuType_IntelHaswellNoTSXIBRS CpuType = "Haswell-noTSX-IBRS" + cpuType_IntelHaswellNoTSXIBRS_Lower CpuType = "haswellnotsxibrs" + CpuType_IntelIcelakeClient CpuType = "Icelake-Client" + cpuType_IntelIcelakeClient_Lower CpuType = "icelakeclient" + CpuType_IntelIcelakeClientNoTSX CpuType = "Icelake-Client-noTSX" + cpuType_IntelIcelakeClientNoTSX_Lower CpuType = "icelakeclientnotsx" + CpuType_IntelIcelakeServer CpuType = "Icelake-Server" + cpuType_IntelIcelakeServer_Lower CpuType = "icelakeserver" + CpuType_IntelIcelakeServerNoTSX CpuType = "Icelake-Server-noTSX" + cpuType_IntelIcelakeServerNoTSX_Lower CpuType = "icelakeservernotsx" + CpuType_IntelIcelakeServerV3 CpuType = "Icelake-Server-v3" + cpuType_IntelIcelakeServerV3_Lower CpuType = "icelakeserverv3" + CpuType_IntelIcelakeServerV4 CpuType = "Icelake-Server-v4" + cpuType_IntelIcelakeServerV4_Lower CpuType = "icelakeserverv4" + CpuType_IntelIcelakeServerV5 CpuType = "Icelake-Server-v5" + cpuType_IntelIcelakeServerV5_Lower CpuType = "icelakeserverv5" + CpuType_IntelIcelakeServerV6 CpuType = "Icelake-Server-v6" + cpuType_IntelIcelakeServerV6_Lower CpuType = "icelakeserverv6" + CpuType_IntelIvybridge CpuType = "IvyBridge" + cpuType_IntelIvybridge_Lower CpuType = "ivybridge" + CpuType_IntelIvybridgeIBRS CpuType = "IvyBridge-IBRS" + cpuType_IntelIvybridgeIBRS_Lower CpuType = "ivyBridgeibrs" + CpuType_IntelKnightsmill CpuType = "KnightsMill" + cpuType_IntelKnightsmill_Lower CpuType = "knightsmill" + CpuType_QemuKvm32 CpuType = "kvm32" + CpuType_QemuKvm64 CpuType = "kvm64" + CpuType_QemuMax CpuType = "max" + CpuType_IntelNahalem CpuType = "Nahalem" + cpuType_IntelNahalem_Lower CpuType = "nahalem" + CpuType_IntelNahalemIBRS CpuType = "Nahalem-IRBS" + cpuType_IntelNahalemIBRS_Lower CpuType = "nahalemibrs" + CpuType_AmdOpteronG1 CpuType = "Opteron_G1" + cpuType_AmdOpteronG1_Lower CpuType = "opterong1" + CpuType_AmdOpteronG2 CpuType = "Opteron_G2" + cpuType_AmdOpteronG2_Lower CpuType = "opterong2" + CpuType_AmdOpteronG3 CpuType = "Opteron_G3" + cpuType_AmdOpteronG3_Lower CpuType = "opterong3" + CpuType_AmdOpteronG4 CpuType = "Opteron_G4" + cpuType_AmdOpteronG4_Lower CpuType = "opterong4" + CpuType_AmdOpteronG5 CpuType = "Opteron_G5" + cpuType_AmdOpteronG5_Lower CpuType = "opterong5" + CpuType_IntelPenrym CpuType = "Penrym" + cpuType_IntelPenrym_Lower CpuType = "penrym" + CpuType_IntelPentium CpuType = "pentium" + CpuType_IntelPentium2 CpuType = "pentium2" + CpuType_IntelPentium3 CpuType = "pentium3" + CpuType_AmdPhenom CpuType = "phenom" + CpuType_Qemu32 CpuType = "qemu32" + CpuType_Qemu64 CpuType = "qemu64" + CpuType_IntelSandyBridge CpuType = "SandyBridge" + cpuType_IntelSandyBridge_Lower CpuType = "sandybridge" + CpuType_IntelSandybridgeIBRS CpuType = "SandyBridge-IBRS" + cpuType_IntelSandybridgeIBRS_Lower CpuType = "sandybridgeibrs" + CpuType_IntelSapphireRapids CpuType = "SapphireRapids" + cpuType_IntelSapphireRapids_Lower CpuType = "sapphirerapids" + CpuType_IntelSkylakeClient CpuType = "Skylake-Client" + cpuType_IntelSkylakeClient_Lower CpuType = "skylakeclient" + CpuType_IntelSkylakeClientIBRS CpuType = "Skylake-Client-IBRS" + cpuType_IntelSkylakeClientIBRS_Lower CpuType = "skylakeclientibrs" + CpuType_IntelSkylakeClientNoTSXIBRS CpuType = "Skylake-Client-noTSX-IBRS" + cpuType_IntelSkylakeClientNoTSXIBRS_Lower CpuType = "skylakeclientnotsxibrs" + CpuType_IntelSkylakeClientV4 CpuType = "Skylake-Client-v4" + cpuType_IntelSkylakeClientV4_Lower CpuType = "skylakeclientv4" + CpuType_IntelSkylakeServer CpuType = "Skylake-Server" + cpuType_IntelSkylakeServer_Lower CpuType = "skylakeserver" + CpuType_IntelSkylakeServerIBRS CpuType = "Skylake-Server-IBRS" + cpuType_IntelSkylakeServerIBRS_Lower CpuType = "skylakeserveribrs" + CpuType_IntelSkylakeServerNoTSXIBRS CpuType = "Skylake-Server-noTSX-IBRS" + cpuType_IntelSkylakeServerNoTSXIBRS_Lower CpuType = "skylakeservernotsxibrs" + CpuType_IntelSkylakeServerV4 CpuType = "Skylake-Server-v4" + cpuType_IntelSkylakeServerV4_Lower CpuType = "skylakeserverv4" + CpuType_IntelSkylakeServerV5 CpuType = "Skylake-Server-v5" + cpuType_IntelSkylakeServerV5_Lower CpuType = "skylakeserverv5" + CpuType_IntelWestmere CpuType = "Westmere" + cpuType_IntelWestmere_Lower CpuType = "westmere" + CpuType_IntelWestmereIBRS CpuType = "Westmere-IBRS" + cpuType_IntelWestmereIBRS_Lower CpuType = "westmereibrs" + CpuType_X86_64_v2 CpuType = "x86-64-v2" + cpuType_X86_64_v2_Lower CpuType = "x8664v2" + CpuType_X86_64_v2_AES CpuType = "x86-64-v2-AES" + cpuType_X86_64_v2_AES_Lower CpuType = "x8664v2aes" + CpuType_X86_64_v3 CpuType = "x86-64-v3" + cpuType_X86_64_v3_Lower CpuType = "x8664v3" + CpuType_X86_64_v4 CpuType = "x86-64-v4" + cpuType_X86_64_v4_Lower CpuType = "x8664v4" +) + +func (CpuType) CpuBase() map[CpuType]CpuType { + return map[CpuType]CpuType{ + CpuType_AmdAthlon: CpuType_AmdAthlon, + CpuType_AmdPhenom: CpuType_AmdPhenom, + CpuType_Intel486: CpuType_Intel486, + CpuType_IntelCore2Duo: CpuType_IntelCore2Duo, + CpuType_IntelCoreDuo: CpuType_IntelCoreDuo, + CpuType_IntelPentium: CpuType_IntelPentium, + CpuType_IntelPentium2: CpuType_IntelPentium2, + CpuType_IntelPentium3: CpuType_IntelPentium3, + CpuType_QemuKvm32: CpuType_QemuKvm32, + CpuType_QemuKvm64: CpuType_QemuKvm64, + CpuType_QemuMax: CpuType_QemuMax, + CpuType_Qemu32: CpuType_Qemu32, + CpuType_Qemu64: CpuType_Qemu64, + CpuType_Host: CpuType_Host, + cpuType_AmdEPYC_Lower: CpuType_AmdEPYC, + cpuType_AmdEPYCIBPB_Lower: CpuType_AmdEPYCIBPB, + cpuType_AmdEPYCMilan_Lower: CpuType_AmdEPYCMilan, + cpuType_AmdEPYCRome_Lower: CpuType_AmdEPYCRome, + cpuType_AmdOpteronG1_Lower: CpuType_AmdOpteronG1, + cpuType_AmdOpteronG2_Lower: CpuType_AmdOpteronG2, + cpuType_AmdOpteronG3_Lower: CpuType_AmdOpteronG3, + cpuType_AmdOpteronG4_Lower: CpuType_AmdOpteronG4, + cpuType_AmdOpteronG5_Lower: CpuType_AmdOpteronG5, + cpuType_IntelBroadwell_Lower: CpuType_IntelBroadwell, + cpuType_IntelBroadwellIBRS_Lower: CpuType_IntelBroadwellIBRS, + cpuType_IntelBroadwellNoTSX_Lower: CpuType_IntelBroadwellNoTSX, + cpuType_IntelBroadwellNoTSXIBRS_Lower: CpuType_IntelBroadwellNoTSXIBRS, + cpuType_IntelCascadelakeServer_Lower: CpuType_IntelCascadelakeServer, + cpuType_IntelCascadelakeServerNoTSX_Lower: CpuType_IntelCascadelakeServerNoTSX, + cpuType_IntelConroe_Lower: CpuType_IntelConroe, + cpuType_IntelHaswell_Lower: CpuType_IntelHaswell, + cpuType_IntelHaswellIBRS_Lower: CpuType_IntelHaswellIBRS, + cpuType_IntelHaswellNoTSX_Lower: CpuType_IntelHaswellNoTSX, + cpuType_IntelHaswellNoTSXIBRS_Lower: CpuType_IntelHaswellNoTSXIBRS, + cpuType_IntelIcelakeClient_Lower: CpuType_IntelIcelakeClient, + cpuType_IntelIcelakeClientNoTSX_Lower: CpuType_IntelIcelakeClientNoTSX, + cpuType_IntelIcelakeServer_Lower: CpuType_IntelIcelakeServer, + cpuType_IntelIcelakeServerNoTSX_Lower: CpuType_IntelIcelakeServerNoTSX, + cpuType_IntelIvybridge_Lower: CpuType_IntelIvybridge, + cpuType_IntelIvybridgeIBRS_Lower: CpuType_IntelIvybridgeIBRS, + cpuType_IntelKnightsmill_Lower: CpuType_IntelKnightsmill, + cpuType_IntelNahalem_Lower: CpuType_IntelNahalem, + cpuType_IntelNahalemIBRS_Lower: CpuType_IntelNahalemIBRS, + cpuType_IntelPenrym_Lower: CpuType_IntelPenrym, + cpuType_IntelSandyBridge_Lower: CpuType_IntelSandyBridge, + cpuType_IntelSandybridgeIBRS_Lower: CpuType_IntelSandybridgeIBRS, + cpuType_IntelSkylakeClient_Lower: CpuType_IntelSkylakeClient, + cpuType_IntelSkylakeClientIBRS_Lower: CpuType_IntelSkylakeClientIBRS, + cpuType_IntelSkylakeClientNoTSXIBRS_Lower: CpuType_IntelSkylakeClientNoTSXIBRS, + cpuType_IntelSkylakeServer_Lower: CpuType_IntelSkylakeServer, + cpuType_IntelSkylakeServerIBRS_Lower: CpuType_IntelSkylakeServerIBRS, + cpuType_IntelSkylakeServerNoTSXIBRS_Lower: CpuType_IntelSkylakeServerNoTSXIBRS, + cpuType_IntelWestmere_Lower: CpuType_IntelWestmere, + cpuType_IntelWestmereIBRS_Lower: CpuType_IntelWestmereIBRS, + } +} + +func (CpuType) CpuV8(cpus map[CpuType]CpuType) { + cpus[cpuType_IntelCascadelakeServerV2_Lower] = CpuType_IntelCascadelakeServerV2 + cpus[cpuType_IntelCascadelakeServerV4_Lower] = CpuType_IntelCascadelakeServerV4 + cpus[cpuType_IntelCascadelakeServerV5_Lower] = CpuType_IntelCascadelakeServerV5 + cpus[cpuType_IntelCooperlake_Lower] = CpuType_IntelCooperlake + cpus[cpuType_IntelCooperlakeV2_Lower] = CpuType_IntelCooperlakeV2 + cpus[cpuType_AmdEPYCRomeV2_Lower] = CpuType_AmdEPYCRomeV2 + cpus[cpuType_AmdEPYCV3_Lower] = CpuType_AmdEPYCV3 + cpus[cpuType_IntelIcelakeServerV3_Lower] = CpuType_IntelIcelakeServerV3 + cpus[cpuType_IntelIcelakeServerV4_Lower] = CpuType_IntelIcelakeServerV4 + cpus[cpuType_IntelIcelakeServerV5_Lower] = CpuType_IntelIcelakeServerV5 + cpus[cpuType_IntelIcelakeServerV6_Lower] = CpuType_IntelIcelakeServerV6 + cpus[cpuType_IntelSapphireRapids_Lower] = CpuType_IntelSapphireRapids + cpus[cpuType_IntelSkylakeClientV4_Lower] = CpuType_IntelSkylakeClientV4 + cpus[cpuType_IntelSkylakeServerV4_Lower] = CpuType_IntelSkylakeServerV4 + cpus[cpuType_IntelSkylakeServerV5_Lower] = CpuType_IntelSkylakeServerV5 + cpus[cpuType_X86_64_v2_Lower] = CpuType_X86_64_v2 + cpus[cpuType_X86_64_v2_AES_Lower] = CpuType_X86_64_v2_AES + cpus[cpuType_X86_64_v3_Lower] = CpuType_X86_64_v3 + cpus[cpuType_X86_64_v4_Lower] = CpuType_X86_64_v4 +} + +func (CpuType) Error(version Version) error { + // v7 + cpus := CpuType("").CpuBase() + if !version.Smaller(Version{Major: 8}) { // v8 + CpuType("").CpuV8(cpus) + } + cpusConverted := make([]string, len(cpus)) + var index int + for _, e := range cpus { + cpusConverted[index] = string(e) + index++ + } + slices.Sort(cpusConverted) + return errors.New("cpuType can only be one of the following values: " + strings.Join(cpusConverted, ", ")) +} + +func (cpu CpuType) mapToApi(version Version) string { + cpus := CpuType("").CpuBase() + if !version.Smaller(Version{Major: 8}) { + cpu.CpuV8(cpus) + } + if v, ok := cpus[CpuType(strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(string(cpu), "_", ""), "-", "")))]; ok { + return string(v) + } + return "" +} + +func (cpu CpuType) Validate(version Version) error { + if cpu == "" || cpu.mapToApi(version) != "" { + return nil + } + return CpuType("").Error(version) +} + +type CpuUnits uint32 // min value 0 is unset, max value of 262144 + +const CpuUnits_Error_Maximum string = "maximum value of CpuUnits is 262144" + +func (units CpuUnits) Validate() error { + if units > 262144 { + return errors.New(CpuUnits_Error_Maximum) + } + return nil +} + +type CpuVirtualCores uint16 // min value 0 is unset, max value 512. is QemuCpuCores * CpuSockets + +func (cores CpuVirtualCores) Error() error { + return errors.New("CpuVirtualCores may have a maximum of " + strconv.FormatInt(int64(cores), 10)) +} + +func (vCores CpuVirtualCores) Validate(cores *QemuCpuCores, sockets *QemuCpuSockets, current *QemuCPU) error { + var usedCores, usedSockets CpuVirtualCores + if cores != nil { + usedCores = CpuVirtualCores(*cores) + } else if current != nil && current.Cores != nil { + usedCores = CpuVirtualCores(*current.Cores) + } + if sockets != nil { + usedSockets = CpuVirtualCores(*sockets) + } else if current != nil && current.Sockets != nil { + usedSockets = CpuVirtualCores(*current.Sockets) + } + if vCores > usedCores*usedSockets { + return (usedCores * usedSockets).Error() + } + return nil +} + +type QemuCPU struct { + Affinity *[]uint `json:"affinity,omitempty"` + Cores *QemuCpuCores `json:"cores,omitempty"` // Required during creation + Flags *CpuFlags `json:"flags,omitempty"` + Limit *CpuLimit `json:"limit,omitempty"` + Numa *bool `json:"numa,omitempty"` + Sockets *QemuCpuSockets `json:"sockets,omitempty"` + Type *CpuType `json:"type,omitempty"` + Units *CpuUnits `json:"units,omitempty"` + VirtualCores *CpuVirtualCores `json:"vcores,omitempty"` +} + +const ( + QemuCPU_Error_CoresRequired string = "cores is required" +) + +func (cpu QemuCPU) mapToApi(current *QemuCPU, params map[string]interface{}, version Version) (delete string) { + if cpu.Affinity != nil { + if len(*cpu.Affinity) != 0 { + params["affinity"] = cpu.mapToApiAffinity(*cpu.Affinity) + } else if current != nil && current.Affinity != nil { + params["affinity"] = "" + } + } + if cpu.Cores != nil { + params["cores"] = int(*cpu.Cores) + } + if cpu.Limit != nil { + if *cpu.Limit != 0 { + params["cpulimit"] = int(*cpu.Limit) + } else if current != nil && current.Limit != nil { + delete += ",cpulimit" + } + } + if cpu.Numa != nil { + params["numa"] = Btoi(*cpu.Numa) + } + if cpu.Sockets != nil { + params["sockets"] = int(*cpu.Sockets) + } + if cpu.Flags != nil || cpu.Type != nil { + var cpuType, flags string + var flagsSet bool + if current == nil { // Create + if cpu.Flags != nil { + flags, flagsSet = cpu.Flags.mapToApi(nil) + if flagsSet && flags == "" { + flagsSet = false + } + } + if cpu.Type != nil { + cpuType = cpu.Type.mapToApi(version) + } + } else { // Update + if cpu.Flags != nil { + flags, flagsSet = cpu.Flags.mapToApi(current.Flags) + } else { + flags, flagsSet = CpuFlags{}.mapToApi(current.Flags) + } + if cpu.Type != nil { + cpuType = cpu.Type.mapToApi(version) + } else if current.Type != nil { + cpuType = current.Type.mapToApi(version) + } + } + if flagsSet { + if flags != "" { + params["cpu"] = cpuType + ",flags=" + flags[1:] + } else { + params["cpu"] = cpuType + ",flags=" + } + } else if cpuType != "" { + params["cpu"] = cpuType + } + } + if cpu.Units != nil { + if *cpu.Units != 0 { + params["cpuunits"] = int(*cpu.Units) + } else if current != nil { + delete += ",cpuunits" + } + } + if cpu.VirtualCores != nil { + if *cpu.VirtualCores != 0 { + params["vcpus"] = int(*cpu.VirtualCores) + } else if current != nil && current.VirtualCores != nil { + delete += ",vcpus" + } + } + return +} + +func (QemuCPU) mapToApiAffinity(affinity []uint) string { + sort.Slice(affinity, func(i, j int) bool { + return affinity[i] < affinity[j] + }) + var builder strings.Builder + rangeStart, rangeEnd := affinity[0], affinity[0] + for i := 1; i < len(affinity); i++ { + if affinity[i] == affinity[i-1] { + continue + } + if affinity[i] == rangeEnd+1 { + // Continue the range + rangeEnd = affinity[i] + } else { + // Close the current range and start a new range + if rangeStart == rangeEnd { + builder.WriteString(strconv.Itoa(int(rangeStart)) + ",") + } else { + builder.WriteString(strconv.Itoa(int(rangeStart)) + "-" + strconv.Itoa(int(rangeEnd)) + ",") + } + rangeStart, rangeEnd = affinity[i], affinity[i] + } + } + // Append the last range + if rangeStart == rangeEnd { + builder.WriteString(strconv.Itoa(int(rangeStart))) + } else { + builder.WriteString(strconv.Itoa(int(rangeStart)) + "-" + strconv.Itoa(int(rangeEnd))) + } + return builder.String() +} + +func (QemuCPU) mapToSDK(params map[string]interface{}) *QemuCPU { + var cpu QemuCPU + if v, isSet := params["affinity"]; isSet { + if v.(string) != "" { + cpu.Affinity = util.Pointer(QemuCPU{}.mapToSdkAffinity(v.(string))) + } else { + cpu.Affinity = util.Pointer(make([]uint, 0)) + } + } + if v, isSet := params["cores"]; isSet { + cpu.Cores = util.Pointer(QemuCpuCores(v.(float64))) + } + if v, isSet := params["cpu"]; isSet { + cpuParams := strings.SplitN(v.(string), ",", 2) + cpu.Type = util.Pointer((CpuType)(cpuParams[0])) + if len(cpuParams) > 1 && len(cpuParams[1]) > 6 { + // `flags=` is the 6 characters bieng removed from the start of the string + cpu.Flags = CpuFlags{}.mapToSDK(strings.Split(cpuParams[1][6:], ";")) + } + } + if v, isSet := params["cpulimit"]; isSet { + tmp, _ := parse.Uint(v) + cpu.Limit = util.Pointer(CpuLimit(tmp)) + } + if v, isSet := params["cpuunits"]; isSet { + cpu.Units = util.Pointer(CpuUnits((v.(float64)))) + } + if v, isSet := params["numa"]; isSet { + cpu.Numa = util.Pointer(v.(float64) == 1) + } + if v, isSet := params["sockets"]; isSet { + cpu.Sockets = util.Pointer(QemuCpuSockets(v.(float64))) + } + if value, isSet := params["vcpus"]; isSet { + cpu.VirtualCores = util.Pointer(CpuVirtualCores((value.(float64)))) + } + return &cpu +} + +func (QemuCPU) mapToSdkAffinity(rawAffinity string) []uint { + result := make([]uint, 0) + for _, e := range strings.Split(rawAffinity, ",") { + if strings.Contains(e, "-") { + bounds := strings.Split(e, "-") + start, _ := strconv.Atoi(bounds[0]) + end, _ := strconv.Atoi(bounds[1]) + for i := start; i <= end; i++ { + result = append(result, uint(i)) + } + } else { + num, _ := strconv.Atoi(e) + result = append(result, uint(num)) + } + } + return result +} + +func (cpu QemuCPU) Validate(current *QemuCPU, version Version) (err error) { + if cpu.Cores != nil { + if err = cpu.Cores.Validate(); err != nil { + return + } + } else if current == nil { + return errors.New(QemuCPU_Error_CoresRequired) + } + if cpu.Flags != nil { + if err = cpu.Flags.Validate(); err != nil { + return + } + } + if cpu.Limit != nil { + if err = cpu.Limit.Validate(); err != nil { + return + } + } + if cpu.Sockets != nil { + if err = cpu.Sockets.Validate(); err != nil { + return + } + } + if cpu.Type != nil { + if err = cpu.Type.Validate(version); err != nil { + return + } + } + if cpu.Units != nil { + if err = cpu.Units.Validate(); err != nil { + return + } + } + if cpu.VirtualCores != nil { + if err = cpu.VirtualCores.Validate(cpu.Cores, cpu.Sockets, current); err != nil { + return + } + } + return +} + +type QemuCpuCores uint8 // min value 1, max value of 128 + +const ( + QemuCpuCores_Error_LowerBound string = "minimum value of QemuCpuCores is 1" + QemuCpuCores_Error_UpperBound string = "maximum value of QemuCpuCores is 128" +) + +func (cores QemuCpuCores) Validate() error { + if cores < 1 { + return errors.New(QemuCpuCores_Error_LowerBound) + } + if cores > 128 { + return errors.New(QemuCpuCores_Error_UpperBound) + } + return nil +} + +type QemuCpuSockets uint8 // min value 1, max value 4 + +const ( + QemuCpuSockets_Error_LowerBound string = "minimum value of QemuCpuSockets is 1" + QemuCpuSockets_Error_UpperBound string = "maximum value of QemuCpuSockets is 4" +) + +func (sockets QemuCpuSockets) Validate() error { + if sockets < 1 { + return errors.New(QemuCpuSockets_Error_LowerBound) + } + if sockets > 4 { + return errors.New(QemuCpuSockets_Error_UpperBound) + } + return nil +} diff --git a/proxmox/config_qemu_cpu_test.go b/proxmox/config_qemu_cpu_test.go new file mode 100644 index 00000000..cf4495af --- /dev/null +++ b/proxmox/config_qemu_cpu_test.go @@ -0,0 +1,455 @@ +package proxmox + +import ( + "errors" + "testing" + + "github.com/Telmate/proxmox-api-go/internal/util" + "github.com/stretchr/testify/require" +) + +func Test_CpuFlags_Validate(t *testing.T) { + tests := []struct { + name string + input CpuFlags + output error + }{ + {name: `Valid`, + input: CpuFlags{ + AES: util.Pointer(TriBoolTrue), + AmdNoSSB: util.Pointer(TriBoolFalse), + AmdSSBD: util.Pointer(TriBoolNone), + HvEvmcs: util.Pointer(TriBoolTrue), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolNone), + MdClear: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolFalse), + Pdpe1GB: util.Pointer(TriBoolNone), + SSBD: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolFalse), + VirtSSBD: util.Pointer(TriBoolNone)}}, + {name: `Invalid AES`, + input: CpuFlags{ + AES: util.Pointer(TriBool(2))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid AmdNoSSB`, + input: CpuFlags{ + AmdNoSSB: util.Pointer(TriBool(-2))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid AmdSSBD`, + input: CpuFlags{ + AmdSSBD: util.Pointer(TriBool(27))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid HvEvmcs`, + input: CpuFlags{ + HvEvmcs: util.Pointer(TriBool(-32))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid HvTlbFlush`, + input: CpuFlags{ + HvTlbFlush: util.Pointer(TriBool(2))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Ibpb`, + input: CpuFlags{ + Ibpb: util.Pointer(TriBool(-52))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid MdClear`, + input: CpuFlags{ + MdClear: util.Pointer(TriBool(52))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid PCID`, + input: CpuFlags{ + PCID: util.Pointer(TriBool(-82))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Pdpe1GB`, + input: CpuFlags{ + Pdpe1GB: util.Pointer(TriBool(2))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid SSBD`, + input: CpuFlags{ + SSBD: util.Pointer(TriBool(-3))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid SpecCtrl`, + input: CpuFlags{ + SpecCtrl: util.Pointer(TriBool(2))}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid VirtSSBD`, + input: CpuFlags{ + VirtSSBD: util.Pointer(TriBool(-2))}, + output: errors.New(TriBool_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CpuLimit_Validate(t *testing.T) { + tests := []struct { + name string + input CpuLimit + output error + }{ + {name: "Valid minimum", + input: 0}, + {name: "Valid maximum", + input: 128}, + {name: "Invalid maximum", + input: 129, + output: errors.New(CpuLimit_Error_Maximum)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} + +func Test_CpuType_Error(t *testing.T) { + testData := []struct { + name string + input Version + compare error + }{ + {name: `v8 > v7`, + input: Version{Major: 8}, + compare: CpuType("").Error(Version{Major: 7, Minor: 255, Patch: 255})}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Greater(t, len(CpuType("").Error(test.input).Error()), len(test.compare.Error()), test.name) + }) + } +} + +func Test_CpuType_Validate(t *testing.T) { + type testInput struct { + config CpuType + version Version + } + testData := []struct { + name string + input testInput + output error + }{ + // Invalid + {name: `Invalid`, + input: testInput{ + config: CpuType("gibbers"), + version: Version{}.max()}, + output: CpuType("").Error(Version{}.max())}, + {name: `Invalid V7`, + input: testInput{ + config: CpuType_AmdEPYCRomeV2, + version: Version{Major: 7}.max()}, + output: CpuType("").Error(Version{Major: 7}.max())}, + // Valid + {name: `Valid empty`, + input: testInput{ + config: CpuType(""), + version: Version{}.max()}}, + {name: `Valid normal`, + input: testInput{ + config: CpuType("Skylake-Server-noTSX-IBRS"), + version: Version{}.max()}}, + {name: `Valid lowercase`, + input: testInput{ + config: CpuType("skylakeclientnotsxibrs"), + version: Version{}.max()}}, + {name: `Valid weird`, + input: testInput{config: CpuType("S-k__-Yl_-A--k-e__-Se-R-v-__Er--n-OTs_X---I-_br-S"), + version: Version{}.max()}}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.config.Validate(test.input.version), test.output, test.name) + }) + } +} + +func Test_CpuUnits_Validate(t *testing.T) { + testData := []struct { + name string + input CpuUnits + output error + }{ + {name: `Invalid errors.New(CpuUnits_Error_Maximum)`, + input: 262145, + output: errors.New(CpuUnits_Error_Maximum)}, + {name: `Valid minimum`, + input: 0}, + {name: `Valid maximum`, + input: 262144}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.Validate(), test.output, test.name) + }) + } +} + +func Test_CpuVirtualCores_Validate(t *testing.T) { + type testInput struct { + virtualCores CpuVirtualCores + cores *QemuCpuCores + sockets *QemuCpuSockets + current *QemuCPU + } + testData := []struct { + name string + input testInput + output error + }{ + // Invalid + {name: `Invalid Create`, + input: testInput{ + virtualCores: 5, + cores: util.Pointer(QemuCpuCores(2)), + sockets: util.Pointer(QemuCpuSockets(2))}, + output: CpuVirtualCores(4).Error()}, + {name: `Invalid Update Cores`, + input: testInput{ + virtualCores: 8, + cores: util.Pointer(QemuCpuCores(1)), + current: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(3)), + Sockets: util.Pointer(QemuCpuSockets(2))}}, + output: CpuVirtualCores(2).Error()}, + {name: `Invalid Update Sockets`, + input: testInput{ + virtualCores: 10, + sockets: util.Pointer(QemuCpuSockets(2)), + current: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(4)), + Sockets: util.Pointer(QemuCpuSockets(3))}}, + output: CpuVirtualCores(8).Error()}, + {name: `Invalid Update`, + input: testInput{ + virtualCores: 16, + current: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(4)), + Sockets: util.Pointer(QemuCpuSockets(3))}}, + output: CpuVirtualCores(12).Error()}, + // Valid + {name: `Valid Create`, + input: testInput{ + virtualCores: 1, + cores: util.Pointer(QemuCpuCores(1)), + sockets: util.Pointer(QemuCpuSockets(1))}}, + {name: `Valid Update Cores`, + input: testInput{ + virtualCores: 2, + cores: util.Pointer(QemuCpuCores(2)), + current: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(1)), + Sockets: util.Pointer(QemuCpuSockets(1))}}}, + {name: `Valid Update Sockets`, + input: testInput{ + virtualCores: 3, + sockets: util.Pointer(QemuCpuSockets(3)), + current: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(1)), + Sockets: util.Pointer(QemuCpuSockets(4))}}}, + {name: `Valid Update`, + input: testInput{ + virtualCores: 4, + current: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(2)), + Sockets: util.Pointer(QemuCpuSockets(2))}}}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.virtualCores.Validate(test.input.cores, test.input.sockets, test.input.current), test.output, test.name) + }) + } +} + +func Test_QemuCPU_Validate(t *testing.T) { + baseConfig := func(config QemuCPU) QemuCPU { + if config.Cores == nil { + config.Cores = util.Pointer(QemuCpuCores(1)) + } + return config + } + type testInput struct { + config QemuCPU + current *QemuCPU + version Version + } + testData := []struct { + name string + input testInput + output error + }{ + // Invalid + {name: `Invalid errors.New(CpuLimit_Error_Maximum)`, + input: testInput{config: baseConfig(QemuCPU{Limit: util.Pointer(CpuLimit(129))})}, + output: errors.New(CpuLimit_Error_Maximum)}, + {name: `Invalid errors.New(QemuCpuCores_Error_LowerBound)`, + input: testInput{config: QemuCPU{Cores: util.Pointer(QemuCpuCores(0))}}, + output: errors.New(QemuCpuCores_Error_LowerBound)}, + {name: `Invalid errors.New(QemuCPU_Error_CoresRequired)`, + input: testInput{config: QemuCPU{}}, + output: errors.New(QemuCPU_Error_CoresRequired)}, + {name: `Invalid errors.New(QemuCpuSockets_Error_LowerBound)`, + input: testInput{config: baseConfig(QemuCPU{Sockets: util.Pointer(QemuCpuSockets(0))})}, + output: errors.New(QemuCpuSockets_Error_LowerBound)}, + {name: `Invalid errors.New(CpuUnits_Error_Maximum)`, + input: testInput{config: baseConfig(QemuCPU{Units: util.Pointer(CpuUnits(262145))})}, + output: errors.New(CpuUnits_Error_Maximum)}, + {name: `Invalid CpuVirtualCores(1).Error() 1 1`, + input: testInput{config: QemuCPU{ + Cores: util.Pointer(QemuCpuCores(1)), + Sockets: util.Pointer(QemuCpuSockets(1)), + VirtualCores: util.Pointer(CpuVirtualCores(2))}}, + output: CpuVirtualCores(1).Error()}, + {name: `Invalid Flags.AES errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + AES: util.Pointer(TriBool(2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.AmdNoSSB errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + AmdNoSSB: util.Pointer(TriBool(-2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.AmdSSBD errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + AmdSSBD: util.Pointer(TriBool(2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.HvEvmcs errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + HvEvmcs: util.Pointer(TriBool(-2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.HvTlbFlush errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + HvTlbFlush: util.Pointer(TriBool(2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.Ibpb errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + Ibpb: util.Pointer(TriBool(-2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.MdClear errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + MdClear: util.Pointer(TriBool(2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.PCID errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + PCID: util.Pointer(TriBool(-2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.Pdpe1GB errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + Pdpe1GB: util.Pointer(TriBool(2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.SSBD errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + SSBD: util.Pointer(TriBool(-2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.SpecCtrl errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + SpecCtrl: util.Pointer(TriBool(2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Flags.VirtSSBD errors.New(TriBool_Error_Invalid)`, + input: testInput{config: baseConfig(QemuCPU{Flags: util.Pointer(CpuFlags{ + VirtSSBD: util.Pointer(TriBool(-2))})})}, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Type`, + input: testInput{ + config: baseConfig(QemuCPU{Type: util.Pointer(CpuType("gibbers"))}), + version: Version{}.max()}, + output: CpuType("").Error(Version{}.max())}, + // Valid + {name: `Valid Maximum`, + input: testInput{ + config: QemuCPU{ + Cores: util.Pointer(QemuCpuCores(128)), + Flags: util.Pointer(CpuFlags{ + AES: util.Pointer(TriBoolTrue), + AmdNoSSB: util.Pointer(TriBoolFalse), + AmdSSBD: util.Pointer(TriBoolNone), + HvEvmcs: util.Pointer(TriBoolTrue), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolNone), + MdClear: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolFalse), + Pdpe1GB: util.Pointer(TriBoolNone), + SSBD: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolFalse), + VirtSSBD: util.Pointer(TriBoolNone)}), + Limit: util.Pointer(CpuLimit(128)), + Sockets: util.Pointer(QemuCpuSockets(4)), + Type: util.Pointer(CpuType(cpuType_AmdEPYCRomeV2_Lower)), + Units: util.Pointer(CpuUnits(262144)), + VirtualCores: util.Pointer(CpuVirtualCores(512))}, + version: Version{}.max()}}, + {name: `Valid Minimum`, + input: testInput{config: QemuCPU{ + Cores: util.Pointer(QemuCpuCores(128)), + Flags: util.Pointer(CpuFlags{}), + Limit: util.Pointer(CpuLimit(0)), + Sockets: util.Pointer(QemuCpuSockets(4)), + Type: util.Pointer(CpuType("")), + Units: util.Pointer(CpuUnits(0)), + VirtualCores: util.Pointer(CpuVirtualCores(0))}, + version: Version{}.max()}}, + {name: `Valid Update`, + input: testInput{ + config: QemuCPU{}, + current: &QemuCPU{}}}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.config.Validate(test.input.current, test.input.version), test.output, test.name) + }) + } +} + +func Test_QemuCpuCores_Validate(t *testing.T) { + testData := []struct { + name string + input QemuCpuCores + output error + }{ + // Invalid + {name: `Invalid errors.New(QemuCpuCores_Error_LowerBound)`, + input: 0, + output: errors.New(QemuCpuCores_Error_LowerBound)}, + {name: `Invalid errors.New(QemuCpuCores_Error_UpperBound)`, + input: 129, + output: errors.New(QemuCpuCores_Error_UpperBound)}, + // Valid + {name: `Valid LowerBound`, + input: 1}, + {name: `Valid UpperBound`, + input: 128}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.Validate(), test.output, test.name) + }) + } +} + +func Test_QemuCpuSockets_Validate(t *testing.T) { + testData := []struct { + name string + input QemuCpuSockets + output error + }{ + // Invalid + {name: "Invalid errors.New(CpuSockets_Error_LowerBound)", + input: 0, + output: errors.New(QemuCpuSockets_Error_LowerBound)}, + {name: "Invalid errors.New(CpuSockets_Error_UpperBound)", + input: 5, + output: errors.New(QemuCpuSockets_Error_UpperBound)}, + // Valid + {name: "Valid LowerBound", + input: 1}, + {name: "Valid UpperBound", + input: 4}, + } + for _, test := range testData { + t.Run(test.name, func(*testing.T) { + require.Equal(t, test.input.Validate(), test.output, test.name) + }) + } +} diff --git a/proxmox/config_qemu_test.go b/proxmox/config_qemu_test.go index 8fd12917..523bd7a7 100644 --- a/proxmox/config_qemu_test.go +++ b/proxmox/config_qemu_test.go @@ -180,6 +180,255 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { config: &ConfigQemu{Agent: &QemuGuestAgent{}}, currentConfig: ConfigQemu{Agent: &QemuGuestAgent{}}, output: map[string]interface{}{"agent": "0"}}}}, + {category: `CPU`, + create: []test{ + {name: `Affinity empty`, + config: &ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{})}}, + output: map[string]interface{}{}}, + {name: `Flags AES`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{AES: util.Pointer(TriBoolTrue)}}}, + output: map[string]interface{}{"cpu": ",flags=+aes"}}, + {name: `Flags AmdNoSSB`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{AmdNoSSB: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=-amd-no-ssb"}}, + {name: `Flags AmdSSBD`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{AmdSSBD: util.Pointer(TriBoolTrue)}}}, + output: map[string]interface{}{"cpu": ",flags=+amd-ssbd"}}, + {name: `Flags HvEvmcs`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{HvEvmcs: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=-hv-evmcs"}}, + {name: `Flags HvTlbFlush`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{HvTlbFlush: util.Pointer(TriBoolTrue)}}}, + output: map[string]interface{}{"cpu": ",flags=+hv-tlbflush"}}, + {name: `Flags Ibpb`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{Ibpb: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=-ibpb"}}, + {name: `Flags MdClear`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{MdClear: util.Pointer(TriBoolTrue)}}}, + output: map[string]interface{}{"cpu": ",flags=+md-clear"}}, + {name: `Flags PCID`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{PCID: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=-pcid"}}, + {name: `Flags Pdpe1GB`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{Pdpe1GB: util.Pointer(TriBoolTrue)}}}, + output: map[string]interface{}{"cpu": ",flags=+pdpe1gb"}}, + {name: `Flags SSBD`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{SSBD: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=-ssbd"}}, + {name: `Flags SpecCtrl`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{SpecCtrl: util.Pointer(TriBoolTrue)}}}, + output: map[string]interface{}{"cpu": ",flags=+spec-ctrl"}}, + {name: `Flags VirtSSBD`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{VirtSSBD: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=-virt-ssbd"}}, + {name: `Flags mixed`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBoolTrue), + AmdNoSSB: util.Pointer(TriBoolFalse), + AmdSSBD: util.Pointer(TriBoolTrue), + HvEvmcs: util.Pointer(TriBoolNone), + HvTlbFlush: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolFalse), + Pdpe1GB: util.Pointer(TriBoolNone)}}}, + output: map[string]interface{}{"cpu": ",flags=+aes;-amd-no-ssb;+amd-ssbd;+hv-tlbflush;+md-clear;-pcid"}}, + {name: `Flags all nil`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{}}}, + output: map[string]interface{}{}}, + {name: `Flags all none`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBoolNone), + AmdNoSSB: util.Pointer(TriBoolNone), + AmdSSBD: util.Pointer(TriBoolNone), + HvEvmcs: util.Pointer(TriBoolNone), + HvTlbFlush: util.Pointer(TriBoolNone), + MdClear: util.Pointer(TriBoolNone), + PCID: util.Pointer(TriBoolNone), + Pdpe1GB: util.Pointer(TriBoolNone), + SSBD: util.Pointer(TriBoolNone), + SpecCtrl: util.Pointer(TriBoolNone), + VirtSSBD: util.Pointer(TriBoolNone)}}}, + output: map[string]interface{}{}}, + {name: `Flags all none & Type ""`, + config: &ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{ + AES: util.Pointer(TriBoolNone), + AmdNoSSB: util.Pointer(TriBoolNone), + AmdSSBD: util.Pointer(TriBoolNone), + HvEvmcs: util.Pointer(TriBoolNone), + HvTlbFlush: util.Pointer(TriBoolNone), + MdClear: util.Pointer(TriBoolNone), + PCID: util.Pointer(TriBoolNone), + Pdpe1GB: util.Pointer(TriBoolNone), + SSBD: util.Pointer(TriBoolNone), + SpecCtrl: util.Pointer(TriBoolNone), + VirtSSBD: util.Pointer(TriBoolNone)}, + Type: util.Pointer(CpuType(""))}}, + output: map[string]interface{}{}}, + {name: `Limit`, + config: &ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(0))}}, + output: map[string]interface{}{}}, + {name: `Units 0`, + config: &ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(0))}}, + output: map[string]interface{}{}}, + {name: `VirtualCores 0`, + config: &ConfigQemu{CPU: &QemuCPU{VirtualCores: util.Pointer(CpuVirtualCores(0))}}, + output: map[string]interface{}{}}}, + createUpdate: []test{ + {name: `Affinity consecutive`, + config: &ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{0, 0, 1, 2, 2, 3})}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{0, 1, 2})}}, + output: map[string]interface{}{"affinity": "0-3"}}, + {name: `Affinity singular`, + config: &ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{2})}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{0, 1, 2})}}, + output: map[string]interface{}{"affinity": "2"}}, + {name: `Affinity mixed`, + config: &ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{5, 0, 4, 2, 9, 3, 2, 11, 7, 2, 12, 4, 13})}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{0, 1, 2})}}, + output: map[string]interface{}{"affinity": "0,2-5,7,9,11-13"}}, + {name: `Cores`, + config: &ConfigQemu{CPU: &QemuCPU{Cores: util.Pointer(QemuCpuCores(1))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Cores: util.Pointer(QemuCpuCores(2))}}, + output: map[string]interface{}{"cores": 1}}, + {name: `Limit`, + config: &ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(50))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(100))}}, + output: map[string]interface{}{"cpulimit": 50}}, + {name: `Numa`, + config: &ConfigQemu{CPU: &QemuCPU{Numa: util.Pointer(true)}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Numa: util.Pointer(false)}}, + output: map[string]interface{}{"numa": 1}}, + {name: `Sockets`, + config: &ConfigQemu{CPU: &QemuCPU{Sockets: util.Pointer(QemuCpuSockets(3))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Sockets: util.Pointer(QemuCpuSockets(2))}}, + output: map[string]interface{}{"sockets": 3}}, + {name: `Type lower`, + config: &ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(cpuType_X86_64_v2_AES_Lower)}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType_Host)}}, + version: Version{}.max(), + output: map[string]interface{}{"cpu": string(CpuType_X86_64_v2_AES)}}, + {name: `Type normal`, + config: &ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType_X86_64_v2_AES)}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType_Host)}}, + version: Version{}.max(), + output: map[string]interface{}{"cpu": string(CpuType_X86_64_v2_AES)}}, + {name: `Type weird`, + config: &ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType("X_-8-_6_-6-4---V_-2-aE--s__"))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType_Host)}}, + version: Version{}.max(), + output: map[string]interface{}{"cpu": string(CpuType_X86_64_v2_AES)}}, + {name: `Units 0`, + config: &ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(100))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(200))}}, + output: map[string]interface{}{"cpuunits": 100}}, + {name: `VirtualCores`, + config: &ConfigQemu{CPU: &QemuCPU{VirtualCores: util.Pointer(CpuVirtualCores(4))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{VirtualCores: util.Pointer(CpuVirtualCores(12))}}, + output: map[string]interface{}{"vcpus": 4}}, + }, + update: []test{ + {name: `Affinity empty`, + config: &ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{})}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{0, 1, 2})}}, + output: map[string]interface{}{"affinity": ""}}, + {name: `Affinity empty no current`, + config: &ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{})}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{}}, + output: map[string]interface{}{}}, + {name: `Flags nil`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{}}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=+aes;-pcid"}}, + {name: `Flags`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBoolTrue), + AmdNoSSB: util.Pointer(TriBoolNone), + HvTlbFlush: util.Pointer(TriBoolTrue), + Ibpb: util.Pointer(TriBoolNone), + MdClear: util.Pointer(TriBoolFalse), + PCID: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolFalse), + VirtSSBD: util.Pointer(TriBoolFalse)}}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AmdNoSSB: util.Pointer(TriBoolTrue), + HvEvmcs: util.Pointer(TriBoolFalse), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolFalse)}}}, + output: map[string]interface{}{"cpu": ",flags=+aes;-hv-evmcs;+hv-tlbflush;-md-clear;+pcid;-spec-ctrl;-virt-ssbd"}}, + {name: `Flags all none`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBoolNone), + AmdNoSSB: util.Pointer(TriBoolNone), + AmdSSBD: util.Pointer(TriBoolNone), + HvEvmcs: util.Pointer(TriBoolNone), + HvTlbFlush: util.Pointer(TriBoolNone), + MdClear: util.Pointer(TriBoolNone), + PCID: util.Pointer(TriBoolNone), + Pdpe1GB: util.Pointer(TriBoolNone), + SSBD: util.Pointer(TriBoolNone), + SpecCtrl: util.Pointer(TriBoolNone), + VirtSSBD: util.Pointer(TriBoolNone)}}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{ + AES: util.Pointer(TriBoolTrue), + AmdNoSSB: util.Pointer(TriBoolTrue), + AmdSSBD: util.Pointer(TriBoolTrue), + HvEvmcs: util.Pointer(TriBoolTrue), + HvTlbFlush: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolTrue), + Pdpe1GB: util.Pointer(TriBoolTrue), + SSBD: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolTrue), + VirtSSBD: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType_Host)}}, + output: map[string]interface{}{"cpu": "host,flags="}}, + {name: `Flags & Type, update Flags`, + config: &ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AmdNoSSB: util.Pointer(TriBoolTrue)}}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{ + HvEvmcs: util.Pointer(TriBoolFalse), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType_Host)}}, + output: map[string]interface{}{"cpu": "host,flags=+amd-no-ssb;-hv-evmcs;-hv-tlbflush;+ibpb;+md-clear;-spec-ctrl"}}, + {name: `Flags & Type, update Type`, + config: &ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType_X86_64_v2_AES)}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{ + HvEvmcs: util.Pointer(TriBoolFalse), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + SpecCtrl: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType_Host)}}, + version: Version{}.max(), + output: map[string]interface{}{"cpu": "x86-64-v2-AES,flags=-hv-evmcs;-hv-tlbflush;+ibpb;+md-clear;-spec-ctrl"}}, + {name: `Limit 0`, + config: &ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(0))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(100))}}, + output: map[string]interface{}{"delete": "cpulimit"}}, + {name: `Limit 0 no current`, + config: &ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(0))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{}}, + output: map[string]interface{}{}}, + {name: `Units 0`, + config: &ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(0))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(100))}}, + output: map[string]interface{}{"delete": "cpuunits"}}, + {name: `VirtualCores 0`, + config: &ConfigQemu{CPU: &QemuCPU{VirtualCores: util.Pointer(CpuVirtualCores(0))}}, + currentConfig: ConfigQemu{CPU: &QemuCPU{VirtualCores: util.Pointer(CpuVirtualCores(4))}}, + output: map[string]interface{}{"delete": "vcpus"}}, + }}, {category: `CloudInit`, // Create CloudInit no need for update as update and create behave the same. will be changed in the future createUpdate: []test{ {name: `CloudInit=nil`, @@ -3279,6 +3528,9 @@ func Test_ConfigQemu_mapToAPI(t *testing.T) { func Test_ConfigQemu_mapToStruct(t *testing.T) { baseConfig := func(config ConfigQemu) *ConfigQemu { + if config.CPU == nil { + config.CPU = &QemuCPU{} + } if config.Memory == nil { config.Memory = &QemuMemory{} } @@ -3326,6 +3578,159 @@ func Test_ConfigQemu_mapToStruct(t *testing.T) { {name: `Type`, input: map[string]interface{}{"agent": string("1,type=virtio")}, output: baseConfig(ConfigQemu{Agent: &QemuGuestAgent{Enable: util.Pointer(true), Type: util.Pointer(QemuGuestAgentType_VirtIO)}})}}}, + {category: `CPU`, + tests: []test{ + {name: `all`, + input: map[string]interface{}{ + "cores": float64(10), + "cpulimit": float64(35), + "cpuunits": float64(1234), + "numa": float64(0), + "sockets": float64(4), + "vcpus": float64(40), + "cpu": string("host,flags=-aes;+amd-no-ssb;-amd-ssbd;+hv-evmcs;-hv-tlbflush;+ibpb;+md-clear;-pcid;-pdpe1gb;-ssbd;+spec-ctrl;+virt-ssbd")}, + output: baseConfig(ConfigQemu{ + CPU: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(10)), + Flags: &CpuFlags{ + AES: util.Pointer(TriBoolFalse), + AmdNoSSB: util.Pointer(TriBoolTrue), + AmdSSBD: util.Pointer(TriBoolFalse), + HvEvmcs: util.Pointer(TriBoolTrue), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolFalse), + Pdpe1GB: util.Pointer(TriBoolFalse), + SSBD: util.Pointer(TriBoolFalse), + SpecCtrl: util.Pointer(TriBoolTrue), + VirtSSBD: util.Pointer(TriBoolTrue)}, + Limit: util.Pointer(CpuLimit(35)), + Numa: util.Pointer(false), + Sockets: util.Pointer(QemuCpuSockets(4)), + Type: util.Pointer(CpuType_Host), + Units: util.Pointer(CpuUnits(1234)), + VirtualCores: util.Pointer(CpuVirtualCores(40))}})}, + {name: `affinity consecutive`, + input: map[string]interface{}{"affinity": "2-4"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{2, 3, 4})}})}, + {name: `affinity empty`, + input: map[string]interface{}{"affinity": ""}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{})}})}, + {name: `affinity mixed`, + input: map[string]interface{}{"affinity": "2,4-6,8,10,12-15"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{2, 4, 5, 6, 8, 10, 12, 13, 14, 15})}})}, + {name: `affinity singular`, + input: map[string]interface{}{"affinity": "2"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Affinity: util.Pointer([]uint{2})}})}, + {name: `cores`, + input: map[string]interface{}{"cores": float64(1)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Cores: util.Pointer(QemuCpuCores(1))}})}, + {name: `cpu flag aes`, + input: map[string]interface{}{"cpu": ",flags=+aes"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{AES: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag amd-no-ssb`, + input: map[string]interface{}{"cpu": ",flags=-amd-no-ssb"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{AmdNoSSB: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag amd-ssbd`, + input: map[string]interface{}{"cpu": ",flags=+amd-ssbd"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{AmdSSBD: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag hv-evmcs`, + input: map[string]interface{}{"cpu": ",flags=-hv-evmcs"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{HvEvmcs: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag hv-tlbflush`, + input: map[string]interface{}{"cpu": ",flags=+hv-tlbflush"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{HvTlbFlush: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag ibpb`, + input: map[string]interface{}{"cpu": ",flags=-ibpb"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{Ibpb: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag md-clear`, + input: map[string]interface{}{"cpu": ",flags=+md-clear"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{MdClear: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag pcid`, + input: map[string]interface{}{"cpu": ",flags=-pcid"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{PCID: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag pdpe1gb`, + input: map[string]interface{}{"cpu": ",flags=+pdpe1gb"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{Pdpe1GB: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag ssbd`, + input: map[string]interface{}{"cpu": ",flags=-ssbd"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{SSBD: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag spec-ctrl`, + input: map[string]interface{}{"cpu": ",flags=+spec-ctrl"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{SpecCtrl: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flag virt-ssbd`, + input: map[string]interface{}{"cpu": ",flags=-virt-ssbd"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{VirtSSBD: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu flags multiple`, + input: map[string]interface{}{"cpu": ",flags=-aes;+amd-no-ssb;-amd-ssbd;-hv-evmcs;-hv-tlbflush;+ibpb;+md-clear;+pcid;-virt-ssbd"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{ + AES: util.Pointer(TriBoolFalse), + AmdNoSSB: util.Pointer(TriBoolTrue), + AmdSSBD: util.Pointer(TriBoolFalse), + HvEvmcs: util.Pointer(TriBoolFalse), + HvTlbFlush: util.Pointer(TriBoolFalse), + Ibpb: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolTrue), + PCID: util.Pointer(TriBoolTrue), + VirtSSBD: util.Pointer(TriBoolFalse)}, + Type: util.Pointer(CpuType(""))}})}, + {name: `cpu model only, no flags`, + input: map[string]interface{}{"cpu": string(CpuType_X86_64_v2_AES)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType("x86-64-v2-AES"))}})}, + {name: `cpu with flags`, + input: map[string]interface{}{"cpu": "x86-64-v2-AES,flags=+spec-ctrl;-md-clear"}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Flags: &CpuFlags{ + MdClear: util.Pointer(TriBoolFalse), + SpecCtrl: util.Pointer(TriBoolTrue)}, + Type: util.Pointer(CpuType_X86_64_v2_AES)}})}, + {name: `cpulimit float64`, + input: map[string]interface{}{"cpulimit": float64(10)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(10))}})}, + {name: `cpulimit string`, + input: map[string]interface{}{"cpulimit": string("25")}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(25))}})}, + {name: `cpuunits`, + input: map[string]interface{}{"cpuunits": float64(1000)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(1000))}})}, + {name: `numa true`, + input: map[string]interface{}{"numa": float64(1)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Numa: util.Pointer(true)}})}, + {name: `numa false`, + input: map[string]interface{}{"numa": float64(0)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Numa: util.Pointer(false)}})}, + {name: `sockets`, + input: map[string]interface{}{"sockets": float64(1)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{Sockets: util.Pointer(QemuCpuSockets(1))}})}, + {name: `vcpus`, + input: map[string]interface{}{"vcpus": float64(1)}, + output: baseConfig(ConfigQemu{CPU: &QemuCPU{VirtualCores: util.Pointer(CpuVirtualCores(1))}})}}}, {category: `CloudInit`, tests: []test{ {name: `ALL`, @@ -5614,6 +6019,11 @@ func Test_ConfigQemu_Validate(t *testing.T) { Concurrent: 10}}} } baseConfig := func(config ConfigQemu) ConfigQemu { + if config.CPU == nil { + config.CPU = &QemuCPU{Cores: util.Pointer(QemuCpuCores(1))} + } else if config.CPU.Cores == nil { + config.CPU.Cores = util.Pointer(QemuCpuCores(1)) + } if config.Memory == nil { config.Memory = &QemuMemory{CapacityMiB: util.Pointer(QemuMemoryCapacity(1024))} } @@ -5646,6 +6056,141 @@ func Test_ConfigQemu_Validate(t *testing.T) { invalid: []test{ {input: baseConfig(ConfigQemu{Agent: &QemuGuestAgent{Type: util.Pointer(QemuGuestAgentType("test"))}}), err: errors.New(QemuGuestAgentType_Error_Invalid)}}}, + {category: `CPU`, + valid: []test{ + {name: `Cores`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Cores: util.Pointer(QemuCpuCores(1))}})}, + {name: `Maximum`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(128)), + Sockets: util.Pointer(QemuCpuSockets(4)), + VirtualCores: util.Pointer(CpuVirtualCores(512))}})}, + {name: `Minimum`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(128)), + Sockets: util.Pointer(QemuCpuSockets(4)), + VirtualCores: util.Pointer(CpuVirtualCores(0))}})}, + {name: `Update`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{}}), + current: &ConfigQemu{CPU: &QemuCPU{}}}, + {name: `Flags all set`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBoolFalse), + AmdNoSSB: util.Pointer(TriBoolNone), + AmdSSBD: util.Pointer(TriBoolTrue), + HvEvmcs: util.Pointer(TriBoolFalse), + HvTlbFlush: util.Pointer(TriBoolNone), + Ibpb: util.Pointer(TriBoolTrue), + MdClear: util.Pointer(TriBoolFalse), + PCID: util.Pointer(TriBoolNone), + Pdpe1GB: util.Pointer(TriBoolTrue), + SSBD: util.Pointer(TriBoolFalse), + SpecCtrl: util.Pointer(TriBoolNone), + VirtSSBD: util.Pointer(TriBoolTrue)}}})}, + {name: `Flags all nil`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{}}})}, + {name: `Flags mixed`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AmdNoSSB: util.Pointer(TriBoolTrue), + AmdSSBD: util.Pointer(TriBoolFalse), + HvTlbFlush: util.Pointer(TriBoolTrue), + Ibpb: util.Pointer(TriBoolFalse), + MdClear: util.Pointer(TriBoolNone), + PCID: util.Pointer(TriBoolTrue), + Pdpe1GB: util.Pointer(TriBoolFalse), + SpecCtrl: util.Pointer(TriBoolTrue)}}})}, + {name: `Limit maximum`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(128))}})}, + {name: `Limit minimum`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(0))}})}, + {name: `Sockets`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Sockets: util.Pointer(QemuCpuSockets(1))}})}, + {name: `Type empty`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType(""))}})}, + {name: `Type host`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType_Host)}})}, + {name: `Units Minimum`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(0))}})}, + {name: `Units Maximum`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(262144))}})}}, + invalid: []test{ + {name: `Create erross.New(ConfigQemu_Error_CpuRequired)`, + err: errors.New(ConfigQemu_Error_CpuRequired)}, + {name: `errors.New(CpuLimit_Error_Maximum)`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Limit: util.Pointer(CpuLimit(129))}}), + err: errors.New(CpuLimit_Error_Maximum)}, + {name: `errors.New(CpuUnits_Error_Maximum)`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Units: util.Pointer(CpuUnits(262145))}}), + err: errors.New(CpuUnits_Error_Maximum)}, + {name: `errors.New(QemuCpuCores_Error_LowerBound)`, + input: ConfigQemu{CPU: &QemuCPU{Cores: util.Pointer(QemuCpuCores(0))}}, + err: errors.New(QemuCpuCores_Error_LowerBound)}, + {name: `errors.New(QemuCPU_Error_CoresRequired)`, + input: ConfigQemu{CPU: &QemuCPU{}}, + err: errors.New(QemuCPU_Error_CoresRequired)}, + {name: `errors.New(QemuCpuSockets_Error_LowerBound)`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(1)), + Sockets: util.Pointer(QemuCpuSockets(0))}}), + err: errors.New(QemuCpuSockets_Error_LowerBound)}, + {name: `CpuVirtualCores(1).Error() 1 1`, + input: ConfigQemu{CPU: &QemuCPU{ + Cores: util.Pointer(QemuCpuCores(1)), + Sockets: util.Pointer(QemuCpuSockets(1)), + VirtualCores: util.Pointer(CpuVirtualCores(2))}}, + err: CpuVirtualCores(1).Error()}, + {name: `Invalid AES`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AES: util.Pointer(TriBool(-2))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid AmdNoSSB`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AmdNoSSB: util.Pointer(TriBool(2))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid AmdSSBD`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + AmdSSBD: util.Pointer(TriBool(-27))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid HvEvmcs`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + HvEvmcs: util.Pointer(TriBool(32))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid HvTlbFlush`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + HvTlbFlush: util.Pointer(TriBool(-2))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Ibpb`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + Ibpb: util.Pointer(TriBool(52))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid MdClear`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + MdClear: util.Pointer(TriBool(-52))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid PCID`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + PCID: util.Pointer(TriBool(82))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid Pdpe1GB`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + Pdpe1GB: util.Pointer(TriBool(-2))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid SSBD`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + SSBD: util.Pointer(TriBool(3))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid SpecCtrl`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + SpecCtrl: util.Pointer(TriBool(-2))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid VirtSSBD`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Flags: &CpuFlags{ + VirtSSBD: util.Pointer(TriBool(2))}}}), + err: errors.New(TriBool_Error_Invalid)}, + {name: `Type`, + input: baseConfig(ConfigQemu{CPU: &QemuCPU{Type: util.Pointer(CpuType("invalid"))}}), + version: Version{}.max(), + err: CpuType("").Error(Version{}.max())}}}, {category: `CloudInit`, valid: []test{ {name: `All v7`, diff --git a/proxmox/type_tribool.go b/proxmox/type_tribool.go new file mode 100644 index 00000000..da6d9c4d --- /dev/null +++ b/proxmox/type_tribool.go @@ -0,0 +1,64 @@ +package proxmox + +import ( + "encoding/json" + "errors" + "strings" +) + +type TriBool int8 + +const ( + TriBoolFalse TriBool = -1 + TriBoolNone TriBool = 0 + TriBoolTrue TriBool = 1 + TriBool_Error_Invalid string = "invalid value for TriBool" +) + +func (b TriBool) MarshalJSON() ([]byte, error) { + var str string + switch b { + case TriBoolTrue: + str = "true" + case TriBoolFalse: + str = "false" + case TriBoolNone: + str = "none" + default: + return nil, errors.New(TriBool_Error_Invalid) + } + return json.Marshal(str) +} + +func (b *TriBool) UnmarshalJSON(data []byte) error { + // Trim the quotes from the JSON string value + str := strings.Trim(string(data), "\"") + for _, v := range []string{"true", "yes", "on"} { + if strings.EqualFold(str, v) { + *b = TriBoolTrue + return nil + } + } + for _, v := range []string{"false", "no", "off"} { + if strings.EqualFold(str, v) { + *b = TriBoolFalse + return nil + } + } + for _, v := range []string{"none", ""} { + if strings.EqualFold(str, v) { + *b = TriBoolNone + return nil + } + } + return errors.New(TriBool_Error_Invalid) +} + +func (b TriBool) Validate() error { + switch b { + case TriBoolTrue, TriBoolFalse, TriBoolNone: + return nil + default: + return errors.New(TriBool_Error_Invalid) + } +} diff --git a/proxmox/type_tribool_test.go b/proxmox/type_tribool_test.go new file mode 100644 index 00000000..a87b2798 --- /dev/null +++ b/proxmox/type_tribool_test.go @@ -0,0 +1,118 @@ +package proxmox + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_TriBool_MarshalJSON(t *testing.T) { + type testData struct { + TriBool TriBool `json:"triBool"` + } + tests := []struct { + name string + input testData + output []byte + err error + }{ + {name: `True`, + input: testData{TriBool: TriBoolTrue}, + output: []byte(`{"triBool":"true"}`)}, + {name: `False`, + input: testData{TriBool: TriBoolFalse}, + output: []byte(`{"triBool":"false"}`)}, + {name: `None`, + input: testData{TriBool: TriBoolNone}, + output: []byte(`{"triBool":"none"}`)}, + {name: `Invalid`, + input: testData{TriBool: TriBool(2)}, + err: errors.New(TriBool_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + output, err := json.Marshal(test.input) + require.Equal(t, test.output, output) + if test.err == nil { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.err.Error()) + } + }) + } +} + +func Test_TriBool_UnmarshalJSON(t *testing.T) { + type testData struct { + TriBool TriBool `json:"triBool"` + } + tests := []struct { + name string + input string + output testData + err error + }{ + {name: `True`, + input: `{"triBool":"true"}`, + output: testData{TriBool: TriBoolTrue}}, + {name: `Yes`, + input: `{"triBool":"yes"}`, + output: testData{TriBool: TriBoolTrue}}, + {name: `On`, + input: `{"triBool":"on"}`, + output: testData{TriBool: TriBoolTrue}}, + {name: `False`, + input: `{"triBool":"false"}`, + output: testData{TriBool: TriBoolFalse}}, + {name: `No`, + input: `{"triBool":"no"}`, + output: testData{TriBool: TriBoolFalse}}, + {name: `Off`, + input: `{"triBool":"off"}`, + output: testData{TriBool: TriBoolFalse}}, + {name: `None`, + input: `{"triBool":"none"}`, + output: testData{TriBool: TriBoolNone}}, + {name: `""`, + input: `{"triBool":""}`, + output: testData{TriBool: TriBoolNone}}, + {name: `Invalid`, + input: `{"triBool":"invalid"}`, + err: errors.New(TriBool_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var output testData + err := json.Unmarshal([]byte(test.input), &output) + require.Equal(t, test.output, output) + require.Equal(t, test.err, err) + }) + } +} + +func Test_TriBool_Validate(t *testing.T) { + tests := []struct { + name string + input TriBool + output error + }{ + {name: `Valid True`, + input: 1}, + {name: `Valid False`, + input: -1}, + {name: `Valid None`}, + {name: `Invalid upperBound`, + input: 2, + output: errors.New(TriBool_Error_Invalid)}, + {name: `Invalid lowerBound`, + input: -2, + output: errors.New(TriBool_Error_Invalid)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, test.input.Validate()) + }) + } +} diff --git a/test/api/CloudInit/shared_test.go b/test/api/CloudInit/shared_test.go index f81582b0..b08e6cde 100644 --- a/test/api/CloudInit/shared_test.go +++ b/test/api/CloudInit/shared_test.go @@ -27,15 +27,17 @@ func _create_vm_spec(network bool) pxapi.ConfigQemu { } config := pxapi.ConfigQemu{ - Name: "test-qemu01", - Bios: "seabios", - Tablet: util.Pointer(true), - Memory: &pxapi.QemuMemory{CapacityMiB: util.Pointer(pxapi.QemuMemoryCapacity(2048))}, - QemuOs: "l26", - QemuCores: 1, - QemuSockets: 1, - QemuCpu: "kvm64", - QemuNuma: util.Pointer(false), + Name: "test-qemu01", + Bios: "seabios", + Tablet: util.Pointer(true), + Memory: &pxapi.QemuMemory{CapacityMiB: util.Pointer(pxapi.QemuMemoryCapacity(2048))}, + QemuOs: "l26", + CPU: &pxapi.QemuCPU{ + Cores: util.Pointer(pxapi.QemuCpuCores(1)), + Numa: util.Pointer(false), + Sockets: util.Pointer(pxapi.QemuCpuSockets(1)), + Type: util.Pointer(pxapi.CpuType_QemuKvm64), + }, QemuKVM: util.Pointer(true), Hotplug: "network,disk,usb", QemuNetworks: networks, diff --git a/test/api/Qemu/shared_test.go b/test/api/Qemu/shared_test.go index 712dceaf..22b7d3a7 100644 --- a/test/api/Qemu/shared_test.go +++ b/test/api/Qemu/shared_test.go @@ -31,15 +31,17 @@ func _create_vm_spec(network bool) pxapi.ConfigQemu { } config := pxapi.ConfigQemu{ - Name: "test-qemu01", - Bios: "seabios", - Tablet: util.Pointer(true), - Memory: &pxapi.QemuMemory{CapacityMiB: util.Pointer(pxapi.QemuMemoryCapacity(128))}, - QemuOs: "l26", - QemuCores: 1, - QemuSockets: 1, - QemuCpu: "kvm64", - QemuNuma: util.Pointer(false), + Name: "test-qemu01", + Bios: "seabios", + Tablet: util.Pointer(true), + Memory: &pxapi.QemuMemory{CapacityMiB: util.Pointer(pxapi.QemuMemoryCapacity(128))}, + QemuOs: "l26", + CPU: &pxapi.QemuCPU{ + Cores: util.Pointer(pxapi.QemuCpuCores(1)), + Numa: util.Pointer(false), + Sockets: util.Pointer(pxapi.QemuCpuSockets(1)), + Type: util.Pointer(pxapi.CpuType_QemuKvm64), + }, QemuKVM: util.Pointer(true), Hotplug: "network,disk,usb", QemuNetworks: networks,