diff --git a/CHANGELOG.md b/CHANGELOG.md index 412ab1d9..e0b627fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ENHANCEMENTS: -resource/virtual_environment_vm: Add `vga` argument +resource/virtual_environment_vm: Add `cpu.flags`, `cpu.type` and `vga` arguments ## 0.1.0 diff --git a/README.md b/README.md index aac8026f..d56ed38b 100644 --- a/README.md +++ b/README.md @@ -359,8 +359,52 @@ This resource doesn't expose any additional attributes. * `user_data_file_id` - (Optional) The ID of a file containing custom user data (conflicts with `user_account`) * `cpu` - (Optional) The CPU configuration * `cores` - (Optional) The number of CPU cores (defaults to `1`) + * `flags` - (Optional) The CPU flags + * `+aes`/`-aes` - Activate AES instruction set for HW acceleration + * `+amd-no-ssb`/`-amd-no-ssb` - Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs + * `+amd-ssbd`/`-amd-ssbd` - Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd" + * `+hv-evmcs`/`-hv-evmcs` - Improve performance for nested virtualization (only supported on Intel CPUs) + * `+hv-tlbflush`/`-hv-tlbflush` - Improve performance in overcommitted Windows guests (may lead to guest BSOD on old CPUs) + * `+ibpb`/`-ibpb` - Allows improved Spectre mitigation on AMD CPUs + * `+md-clear`/`-md-clear` - Required to let the guest OS know if MDS is mitigated correctly + * `+pcid`/`-pcid` - Meltdown fix cost reduction on Westmere, Sandy- and Ivy Bridge Intel CPUs + * `+pdpe1gb`/`-pdpe1gb` - Allows guest OS to use 1 GB size pages, if host HW supports it + * `+spec-ctrl`/`-spec-ctrl` - Allows improved Spectre mitigation with Intel CPUs + * `+ssbd`/`-ssbd` - Protection for "Speculative Store Bypass" for Intel models + * `+virt-ssbd`/`-virt-ssbd` - Basis for "Speculative Store Bypass" protection for AMD models * `hotplugged` - (Optional) The number of hotplugged vCPUs (defaults to `0`) * `sockets` - (Optional) The number of CPU sockets (defaults to `1`) + * `type` - (Optional) The emulated CPU type (defaults to `qemu64`) + * `486` - Intel 486 + * `Broadwell`/`Broadwell-IBRS`/`Broadwell-noTSX`/`Broadwell-noTSX-IBRS` - Intel Core Processor (Broadwell, 2014) + * `Cascadelake-Server` - Intel Xeon 32xx/42xx/52xx/62xx/82xx/92xx (2019) + * `Conroe` - Intel Celeron_4x0 (Conroe/Merom Class Core 2, 2006) + * `EPYC`/`EPYC-IBPB` - AMD EPYC Processor (2017) + * `Haswell`/`Haswell-IBRS`/`Haswell-noTSX`/`Haswell-noTSX-IBRS` - Intel Core Processor (Haswell, 2013) + * `IvyBridge`/`IvyBridge-IBRS` - Intel Xeon E3-12xx v2 (Ivy Bridge, 2012) + * `KnightsMill` - Intel Xeon Phi 72xx (2017) + * `Nehalem`/`Nehalem-IBRS` - Intel Core i7 9xx (Nehalem Class Core i7, 2008) + * `Opteron_G1` - AMD Opteron 240 (Gen 1 Class Opteron, 2004) + * `Opteron_G2` - AMD Opteron 22xx (Gen 2 Class Opteron, 2006) + * `Opteron_G3` - AMD Opteron 23xx (Gen 3 Class Opteron, 2009) + * `Opteron_G4` - AMD Opteron 62xx class CPU (2011) + * `Opteron_G5` - AMD Opteron 63xx class CPU (2012) + * `Penryn` - Intel Core 2 Duo P9xxx (Penryn Class Core 2, 2007) + * `SandyBridge`/`SandyBridge-IBRS` - Intel Xeon E312xx (Sandy Bridge, 2011) + * `Skylake-Client`/`Skylake-Client-IBRS` - Intel Core Processor (Skylake, 2015) + * `Skylake-Server`/`Skylake-Server-IBRS` - Intel Xeon Processor (Skylake, 2016) + * `Westmere`/`Westmere-IBRS` - Intel Westmere E56xx/L56xx/X56xx (Nehalem-C, 2010) + * `athlon` - AMD Athlon + * `core2duo` - Intel Core 2 Duo + * `coreduo` - Intel Core Duo + * `host` - Host passthrough + * `kvm32`/`kvm64` - Common KVM processor (32 & 64 bit variants) + * `max` - Maximum amount of features from host CPU + * `pentium` - Intel Pentium (1993) + * `pentium2` - Intel Pentium 2 (1997-1999) + * `pentium3` - Intel Pentium 3 (1999-2001) + * `phenom` - AMD Phenom (2010) + * `qemu32`/`qemu64` - QEMU Virtual CPU version 2.5+ (32 & 64 bit variants) * `description` - (Optional) The description * `disk` - (Optional) The disk configuration (multiple blocks supported) * `datastore_id` - (Optional) The ID of the datastore to create the disk in (defaults to `local-lvm`) diff --git a/proxmox/virtual_environment_vm_types.go b/proxmox/virtual_environment_vm_types.go index 6da1f8d0..f852fc6b 100644 --- a/proxmox/virtual_environment_vm_types.go +++ b/proxmox/virtual_environment_vm_types.go @@ -6,6 +6,7 @@ package proxmox import ( "encoding/json" + "errors" "fmt" "net/url" "strconv" @@ -55,6 +56,14 @@ type CustomCloudInitIPConfig struct { // CustomCloudInitSSHKeys handles QEMU cloud-init SSH keys parameters. type CustomCloudInitSSHKeys []string +// CustomCPUEmulation handles QEMU CPU emulation parameters. +type CustomCPUEmulation struct { + Flags *[]string `json:"flags,omitempty" url:"flags,omitempty,semicolon"` + Hidden *CustomBool `json:"hidden,omitempty" url:"hidden,omitempty,int"` + HVVendorID *string `json:"hv-vendor-id,omitempty" url:"hv-vendor-id,omitempty"` + Type string `json:"cputype,omitempty" url:"cputype,omitempty"` +} + // CustomEFIDisk handles QEMU EFI disk parameters. type CustomEFIDisk struct { DiskSize *int `json:"size,omitempty" url:"size,omitempty"` @@ -202,6 +211,7 @@ type VirtualEnvironmentVMCreateRequestBody struct { CloudInitConfig *CustomCloudInitConfig `json:"cloudinit,omitempty" url:"cloudinit,omitempty"` CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"` CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"` + CPUEmulation *CustomCPUEmulation `json:"cpu,omitempty" url:"cpu,omitempty"` CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"` CPUSockets *int `json:"sockets,omitempty" url:"sockets,omitempty"` CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"` @@ -322,6 +332,7 @@ type VirtualEnvironmentVMGetResponseData struct { CloudInitUsername *string `json:"ciuser,omitempty"` CPUArchitecture *string `json:"arch,omitempty"` CPUCores *int `json:"cores,omitempty"` + CPUEmulation *CustomCPUEmulation `json:"cpu,omitempty"` CPULimit *int `json:"cpulimit,omitempty"` CPUSockets *int `json:"sockets,omitempty"` CPUUnits *int `json:"cpuunits,omitempty"` @@ -574,6 +585,33 @@ func (r CustomCloudInitConfig) EncodeValues(key string, v *url.Values) error { return nil } +// EncodeValues converts a CustomCPUEmulation struct to a URL vlaue. +func (r CustomCPUEmulation) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("cputype=%s", r.Type), + } + + if r.Flags != nil && len(*r.Flags) > 0 { + values = append(values, fmt.Sprintf("flags=%s", strings.Join(*r.Flags, ";"))) + } + + if r.Hidden != nil { + if *r.Hidden { + values = append(values, "hidden=1") + } else { + values = append(values, "hidden=0") + } + } + + if r.HVVendorID != nil { + values = append(values, fmt.Sprintf("hv-vendor-id=%s", *r.HVVendorID)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + // EncodeValues converts a CustomEFIDisk struct to a URL vlaue. func (r CustomEFIDisk) EncodeValues(key string, v *url.Values) error { values := []string{ @@ -1136,6 +1174,51 @@ func (r *CustomCloudInitSSHKeys) UnmarshalJSON(b []byte) error { return nil } +// UnmarshalJSON converts a CustomCPUEmulation string to an object. +func (r *CustomCPUEmulation) UnmarshalJSON(b []byte) error { + var s string + + err := json.Unmarshal(b, &s) + + if err != nil { + return err + } + + if s == "" { + return errors.New("Unexpected empty string") + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + r.Type = v[0] + } else if len(v) == 2 { + switch v[0] { + case "cputype": + r.Type = v[1] + case "flags": + if v[1] != "" { + f := strings.Split(v[1], ";") + r.Flags = &f + } else { + f := []string{} + r.Flags = &f + } + case "hidden": + bv := CustomBool(v[1] == "1") + r.Hidden = &bv + case "hv-vendor-id": + r.HVVendorID = &v[1] + } + } + } + + return nil +} + // UnmarshalJSON converts a CustomNetworkDevice string to an object. func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error { var s string diff --git a/proxmoxtf/resource_virtual_environment_vm.go b/proxmoxtf/resource_virtual_environment_vm.go index b929a821..7d47f8cf 100644 --- a/proxmoxtf/resource_virtual_environment_vm.go +++ b/proxmoxtf/resource_virtual_environment_vm.go @@ -28,6 +28,7 @@ const ( dvResourceVirtualEnvironmentVMCPUCores = 1 dvResourceVirtualEnvironmentVMCPUHotplugged = 0 dvResourceVirtualEnvironmentVMCPUSockets = 1 + dvResourceVirtualEnvironmentVMCPUType = "qemu64" dvResourceVirtualEnvironmentVMDescription = "" dvResourceVirtualEnvironmentVMDiskDatastoreID = "local-lvm" dvResourceVirtualEnvironmentVMDiskFileFormat = "qcow2" @@ -81,8 +82,10 @@ const ( mkResourceVirtualEnvironmentVMCloudInitUserDataFileID = "user_data_file_id" mkResourceVirtualEnvironmentVMCPU = "cpu" mkResourceVirtualEnvironmentVMCPUCores = "cores" + mkResourceVirtualEnvironmentVMCPUFlags = "flags" mkResourceVirtualEnvironmentVMCPUHotplugged = "hotplugged" mkResourceVirtualEnvironmentVMCPUSockets = "sockets" + mkResourceVirtualEnvironmentVMCPUType = "type" mkResourceVirtualEnvironmentVMDescription = "description" mkResourceVirtualEnvironmentVMDisk = "disk" mkResourceVirtualEnvironmentVMDiskDatastoreID = "datastore_id" @@ -363,8 +366,10 @@ func resourceVirtualEnvironmentVM() *schema.Resource { defaultMap := map[string]interface{}{} defaultMap[mkResourceVirtualEnvironmentVMCPUCores] = dvResourceVirtualEnvironmentVMCPUCores + defaultMap[mkResourceVirtualEnvironmentVMCPUFlags] = []interface{}{} defaultMap[mkResourceVirtualEnvironmentVMCPUHotplugged] = dvResourceVirtualEnvironmentVMCPUHotplugged defaultMap[mkResourceVirtualEnvironmentVMCPUSockets] = dvResourceVirtualEnvironmentVMCPUSockets + defaultMap[mkResourceVirtualEnvironmentVMCPUType] = dvResourceVirtualEnvironmentVMCPUType defaultList[0] = defaultMap @@ -379,6 +384,15 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Default: dvResourceVirtualEnvironmentVMCPUCores, ValidateFunc: validation.IntBetween(1, 2304), }, + mkResourceVirtualEnvironmentVMCPUFlags: { + Type: schema.TypeList, + Description: "The CPU flags", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return []interface{}{}, nil + }, + Elem: &schema.Schema{Type: schema.TypeString}, + }, mkResourceVirtualEnvironmentVMCPUHotplugged: { Type: schema.TypeInt, Description: "The number of hotplugged vCPUs", @@ -393,6 +407,13 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Default: dvResourceVirtualEnvironmentVMCPUSockets, ValidateFunc: validation.IntBetween(1, 16), }, + mkResourceVirtualEnvironmentVMCPUType: { + Type: schema.TypeString, + Description: "The emulated CPU type", + Optional: true, + Default: dvResourceVirtualEnvironmentVMCPUType, + ValidateFunc: getCPUTypeValidator(), + }, }, }, MaxItems: 1, @@ -786,8 +807,10 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e } cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int) + cpuFlags := cpuBlock[mkResourceVirtualEnvironmentVMCPUFlags].([]interface{}) cpuHotplugged := cpuBlock[mkResourceVirtualEnvironmentVMCPUHotplugged].(int) cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) + cpuType := cpuBlock[mkResourceVirtualEnvironmentVMCPUType].(string) description := d.Get(mkResourceVirtualEnvironmentVMDescription).(string) diskDeviceObjects, err := resourceVirtualEnvironmentVMGetDiskDeviceObjects(d, m) @@ -847,6 +870,12 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e bootOrder = "cd" } + cpuFlagsConverted := make([]string, len(cpuFlags)) + + for fi, flag := range cpuFlags { + cpuFlagsConverted[fi] = flag.(string) + } + ideDevice2Media := "cdrom" ideDevices := proxmox.CustomStorageDevices{ proxmox.CustomStorageDevice{ @@ -879,10 +908,14 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e TrimClonedDisks: &agentTrim, Type: &agentType, }, - BootDisk: &bootDisk, - BootOrder: &bootOrder, - CloudInitConfig: cloudInitConfig, - CPUCores: &cpuCores, + BootDisk: &bootDisk, + BootOrder: &bootOrder, + CloudInitConfig: cloudInitConfig, + CPUCores: &cpuCores, + CPUEmulation: &proxmox.CustomCPUEmulation{ + Flags: &cpuFlagsConverted, + Type: cpuType, + }, CPUSockets: &cpuSockets, DedicatedMemory: &memoryDedicated, FloatingMemory: &memoryFloating, @@ -1557,12 +1590,33 @@ func resourceVirtualEnvironmentVMRead(d *schema.ResourceData, m interface{}) err cpu[mkResourceVirtualEnvironmentVMCPUSockets] = 0 } + if vmConfig.CPUEmulation != nil { + if vmConfig.CPUEmulation.Flags != nil { + convertedFlags := make([]interface{}, len(*vmConfig.CPUEmulation.Flags)) + + for fi, fv := range *vmConfig.CPUEmulation.Flags { + convertedFlags[fi] = fv + } + + cpu[mkResourceVirtualEnvironmentVMCPUFlags] = convertedFlags + } else { + cpu[mkResourceVirtualEnvironmentVMCPUFlags] = []interface{}{} + } + + cpu[mkResourceVirtualEnvironmentVMCPUType] = vmConfig.CPUEmulation.Type + } else { + cpu[mkResourceVirtualEnvironmentVMCPUFlags] = []interface{}{} + cpu[mkResourceVirtualEnvironmentVMCPUType] = "" + } + currentCPU := d.Get(mkResourceVirtualEnvironmentVMCPU).([]interface{}) if len(currentCPU) > 0 || cpu[mkResourceVirtualEnvironmentVMCPUCores] != dvResourceVirtualEnvironmentVMCPUCores || + len(cpu[mkResourceVirtualEnvironmentVMCPUFlags].([]interface{})) > 0 || cpu[mkResourceVirtualEnvironmentVMCPUHotplugged] != dvResourceVirtualEnvironmentVMCPUHotplugged || - cpu[mkResourceVirtualEnvironmentVMCPUSockets] != dvResourceVirtualEnvironmentVMCPUSockets { + cpu[mkResourceVirtualEnvironmentVMCPUSockets] != dvResourceVirtualEnvironmentVMCPUSockets || + cpu[mkResourceVirtualEnvironmentVMCPUType] != dvResourceVirtualEnvironmentVMCPUType { d.Set(mkResourceVirtualEnvironmentVMCPU, []interface{}{cpu}) } @@ -2040,8 +2094,10 @@ func resourceVirtualEnvironmentVMUpdate(d *schema.ResourceData, m interface{}) e } cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int) + cpuFlags := cpuBlock[mkResourceVirtualEnvironmentVMCPUFlags].([]interface{}) cpuHotplugged := cpuBlock[mkResourceVirtualEnvironmentVMCPUHotplugged].(int) cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) + cpuType := cpuBlock[mkResourceVirtualEnvironmentVMCPUType].(string) if cpuCores > 0 { body.CPUCores = &cpuCores @@ -2055,6 +2111,17 @@ func resourceVirtualEnvironmentVMUpdate(d *schema.ResourceData, m interface{}) e body.CPUSockets = &cpuSockets } + cpuFlagsConverted := make([]string, len(cpuFlags)) + + for fi, flag := range cpuFlags { + cpuFlagsConverted[fi] = flag.(string) + } + + body.CPUEmulation = &proxmox.CustomCPUEmulation{ + Flags: &cpuFlagsConverted, + Type: cpuType, + } + rebootRequired = true } diff --git a/proxmoxtf/resource_virtual_environment_vm_test.go b/proxmoxtf/resource_virtual_environment_vm_test.go index e0de5f32..a360d120 100644 --- a/proxmoxtf/resource_virtual_environment_vm_test.go +++ b/proxmoxtf/resource_virtual_environment_vm_test.go @@ -227,18 +227,24 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { testOptionalArguments(t, cpuSchema, []string{ mkResourceVirtualEnvironmentVMCPUCores, + mkResourceVirtualEnvironmentVMCPUFlags, mkResourceVirtualEnvironmentVMCPUHotplugged, mkResourceVirtualEnvironmentVMCPUSockets, + mkResourceVirtualEnvironmentVMCPUType, }) testSchemaValueTypes(t, cpuSchema, []string{ mkResourceVirtualEnvironmentVMCPUCores, + mkResourceVirtualEnvironmentVMCPUFlags, mkResourceVirtualEnvironmentVMCPUHotplugged, mkResourceVirtualEnvironmentVMCPUSockets, + mkResourceVirtualEnvironmentVMCPUType, }, []schema.ValueType{ schema.TypeInt, + schema.TypeList, schema.TypeInt, schema.TypeInt, + schema.TypeString, }) diskSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMDisk) diff --git a/proxmoxtf/utils.go b/proxmoxtf/utils.go index 8bdb21f3..8f04c35e 100644 --- a/proxmoxtf/utils.go +++ b/proxmoxtf/utils.go @@ -22,6 +22,114 @@ func getContentTypeValidator() schema.SchemaValidateFunc { }, false) } +func getCPUFlagsValidator() schema.SchemaValidateFunc { + return func(i interface{}, k string) (ws []string, es []error) { + list, ok := i.([]interface{}) + + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be []interface{}", k)) + return + } + + validator := validation.StringInSlice([]string{ + "+aes", + "-aes", + "+amd-no-ssb", + "-amd-no-ssb", + "+amd-ssbd", + "-amd-ssbd", + "+hv-evmcs", + "-hv-evmcs", + "+hv-tlbflush", + "-hv-tlbflush", + "+ibpb", + "-ibpb", + "+md-clear", + "-md-clear", + "+pcid", + "-pcid", + "+pdpe1gb", + "-pdpe1gb", + "+spec-ctrl", + "-spec-ctrl", + "+ssbd", + "-ssbd", + "+virt-ssbd", + "-virt-ssbd", + }, false) + + for li, lv := range list { + v, ok := lv.(string) + + if !ok { + es = append(es, fmt.Errorf("expected type of %s[%d] to be string", k, li)) + return + } + + warns, errs := validator(v, k) + + ws = append(ws, warns...) + es = append(es, errs...) + + if len(es) > 0 { + return + } + } + + return + } +} + +func getCPUTypeValidator() schema.SchemaValidateFunc { + return validation.StringInSlice([]string{ + "486", + "Broadwell", + "Broadwell-IBRS", + "Broadwell-noTSX", + "Broadwell-noTSX-IBRS", + "Cascadelake-Server", + "Conroe", + "EPYC", + "EPYC-IBPB", + "Haswell", + "Haswell-IBRS", + "Haswell-noTSX", + "Haswell-noTSX-IBRS", + "IvyBridge", + "IvyBridge-IBRS", + "KnightsMill", + "Nehalem", + "Nehalem-IBRS", + "Opteron_G1", + "Opteron_G2", + "Opteron_G3", + "Opteron_G4", + "Opteron_G5", + "Penryn", + "SandyBridge", + "SandyBridge-IBRS", + "Skylake-Client", + "Skylake-Client-IBRS", + "Skylake-Server", + "Skylake-Server-IBRS", + "Westmere", + "Westmere-IBRS", + "athlon", + "core2duo", + "coreduo", + "host", + "kvm32", + "kvm64", + "max", + "pentium", + "pentium2", + "pentium3", + "phenom", + "qemu32", + "qemu64", + }, false) +} + func getFileFormatValidator() schema.SchemaValidateFunc { return validation.StringInSlice([]string{ "qcow2",