diff --git a/fwprovider/tests/resource_vm_clone_test.go b/fwprovider/tests/resource_vm_clone_test.go new file mode 100644 index 00000000..a8a37792 --- /dev/null +++ b/fwprovider/tests/resource_vm_clone_test.go @@ -0,0 +1,122 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package tests + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceVMCloneCPU(t *testing.T) { + te := initTestEnvironment(t) + + tests := []struct { + name string + step []resource.TestStep + }{ + {"copy cpu from template in full", []resource.TestStep{{ + Config: te.renderConfig(` + resource "proxmox_virtual_environment_vm" "test_cpu_clone_template" { + node_name = "{{.NodeName}}" + started = false + name = "test-cpu-clone-template" + template = "true" + + cpu { + cores = 2 + type = "host" + } + } + resource "proxmox_virtual_environment_vm" "test_cpu_clone" { + node_name = "{{.NodeName}}" + started = false + name = "test-cpu-clone" + + clone { + vm_id = proxmox_virtual_environment_vm.test_cpu_clone_template.id + } + }`), + Check: resource.ComposeTestCheckFunc( + testResourceAttributes("proxmox_virtual_environment_vm.test_cpu_clone", map[string]string{ + "cpu.0.cores": "2", + "cpu.0.type": "host", + }), + ), + }}}, + {"merge cpu attributes", []resource.TestStep{{ + Config: te.renderConfig(` + resource "proxmox_virtual_environment_vm" "test_cpu_clone_template" { + node_name = "{{.NodeName}}" + started = false + name = "test-cpu-clone-template" + template = "true" + + cpu { + cores = 2 + } + } + resource "proxmox_virtual_environment_vm" "test_cpu_clone" { + node_name = "{{.NodeName}}" + started = false + name = "test-cpu-clone" + + cpu { + type = "host" + } + + clone { + vm_id = proxmox_virtual_environment_vm.test_cpu_clone_template.id + } + }`), + Check: resource.ComposeTestCheckFunc( + testResourceAttributes("proxmox_virtual_environment_vm.test_cpu_clone", map[string]string{ + "cpu.0.cores": "2", + "cpu.0.type": "host", + }), + ), + }}}, + {"overwrite cpu attributes in full", []resource.TestStep{{ + Config: te.renderConfig(` + resource "proxmox_virtual_environment_vm" "test_cpu_clone_template" { + node_name = "{{.NodeName}}" + started = false + name = "test-cpu-clone-template" + template = "true" + } + resource "proxmox_virtual_environment_vm" "test_cpu_clone" { + node_name = "{{.NodeName}}" + started = false + name = "test-cpu-clone" + + cpu { + cores = 2 + type = "host" + } + + clone { + vm_id = proxmox_virtual_environment_vm.test_cpu_clone_template.id + } + }`), + Check: resource.ComposeTestCheckFunc( + testResourceAttributes("proxmox_virtual_environment_vm.test_cpu_clone", map[string]string{ + "cpu.0.cores": "2", + "cpu.0.type": "host", + }), + ), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.accProviders, + Steps: tt.step, + }) + }) + } +} diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go index fa014062..aab2f4b3 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -77,7 +77,7 @@ type CustomCPUEmulation struct { Flags *[]string `json:"flags,omitempty" url:"flags,omitempty,semicolon"` Hidden *types.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"` + Type *string `json:"cputype,omitempty" url:"cputype,omitempty"` } // CustomEFIDisk handles QEMU EFI disk parameters. @@ -820,7 +820,7 @@ func (r CustomCloudInitConfig) EncodeValues(_ string, v *url.Values) error { // 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), + fmt.Sprintf("cputype=%s", *r.Type), } if r.Flags != nil && len(*r.Flags) > 0 { @@ -1464,11 +1464,11 @@ func (r *CustomCPUEmulation) UnmarshalJSON(b []byte) error { v := strings.Split(strings.TrimSpace(p), "=") if len(v) == 1 { - r.Type = v[0] + r.Type = &v[0] } else if len(v) == 2 { switch v[0] { case "cputype": - r.Type = v[1] + r.Type = &v[1] case "flags": if v[1] != "" { f := strings.Split(v[1], ";") diff --git a/proxmoxtf/resource/validators/cpu.go b/proxmoxtf/resource/validators/cpu.go new file mode 100644 index 00000000..10e1b1f9 --- /dev/null +++ b/proxmoxtf/resource/validators/cpu.go @@ -0,0 +1,107 @@ +package validators + +import ( + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// CPUArchitectureValidator returns a schema validation function for a CPU architecture. +func CPUArchitectureValidator() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(validation.StringInSlice([]string{ + "aarch64", + "x86_64", + }, false)) +} + +// CPUTypeValidator returns a schema validation function for a CPU type. +func CPUTypeValidator() schema.SchemaValidateDiagFunc { + standardTypes := []string{ + "486", + "Broadwell", + "Broadwell-IBRS", + "Broadwell-noTSX", + "Broadwell-noTSX-IBRS", + "Cascadelake-Server", + "Cascadelake-Server-noTSX", + "Cascadelake-Server-v2", + "Cascadelake-Server-v4", + "Cascadelake-Server-v5", + "Conroe", + "Cooperlake", + "Cooperlake-v2", + "EPYC", + "EPYC-IBPB", + "EPYC-Milan", + "EPYC-Rome", + "EPYC-Rome-v2", + "EPYC-v3", + "Haswell", + "Haswell-IBRS", + "Haswell-noTSX", + "Haswell-noTSX-IBRS", + "Icelake-Client", + "Icelake-Client-noTSX", + "Icelake-Server", + "Icelake-Server-noTSX", + "Icelake-Server-v3", + "Icelake-Server-v4", + "Icelake-Server-v5", + "Icelake-Server-v6", + "IvyBridge", + "IvyBridge-IBRS", + "KnightsMill", + "Nehalem", + "Nehalem-IBRS", + "Opteron_G1", + "Opteron_G2", + "Opteron_G3", + "Opteron_G4", + "Opteron_G5", + "Penryn", + "SandyBridge", + "SandyBridge-IBRS", + "SapphireRapids", + "Skylake-Client", + "Skylake-Client-IBRS", + "Skylake-Client-noTSX-IBRS", + "Skylake-Client-v4", + "Skylake-Server", + "Skylake-Server-IBRS", + "Skylake-Server-noTSX-IBRS", + "Skylake-Server-v4", + "Skylake-Server-v5", + "Westmere", + "Westmere-IBRS", + "athlon", + "core2duo", + "coreduo", + "host", + "kvm32", + "kvm64", + "max", + "pentium", + "pentium2", + "pentium3", + "phenom", + "qemu32", + "qemu64", + "x86-64-v2", + "x86-64-v2-AES", + "x86-64-v3", + "x86-64-v4", + } + + return validation.ToDiagFunc(validation.Any( + validation.StringInSlice(standardTypes, false), + validation.StringMatch(regexp.MustCompile(`^custom-.+$`), "must be a valid custom CPU type"), + )) +} + +// CPUAffinityValidator returns a schema validation function for a CPU affinity. +func CPUAffinityValidator() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc( + validation.StringMatch(regexp.MustCompile(`^\d+[\d-,]*$`), "must contain numbers or number ranges separated by ','"), + ) +} diff --git a/proxmoxtf/resource/validators/cpu_test.go b/proxmoxtf/resource/validators/cpu_test.go new file mode 100644 index 00000000..414349c0 --- /dev/null +++ b/proxmoxtf/resource/validators/cpu_test.go @@ -0,0 +1,38 @@ +package validators + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCPUType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value string + valid bool + }{ + {"empty", "", false}, + {"invalid", "invalid", false}, + {"valid", "host", true}, + {"valid", "qemu64", true}, + {"valid", "custom-abc", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f := CPUTypeValidator() + res := f(tt.value, nil) + + if tt.valid { + require.Empty(t, res, "validate: '%s'", tt.value) + } else { + require.NotEmpty(t, res, "validate: '%s'", tt.value) + } + }) + } +} diff --git a/proxmoxtf/resource/vm/cpu/cpu.go b/proxmoxtf/resource/vm/cpu/cpu.go new file mode 100644 index 00000000..fe40f331 --- /dev/null +++ b/proxmoxtf/resource/vm/cpu/cpu.go @@ -0,0 +1,386 @@ +package cpu + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/bpg/terraform-provider-proxmox/proxmoxtf/structure" +) + +func UpdateWhenClone(d *schema.ResourceData, api proxmox.Client, updateBody *vms.UpdateRequestBody) { + cpu := d.Get(MkCPU).([]interface{}) + if len(cpu) > 0 && cpu[0] != nil { + cpuBlock := cpu[0].(map[string]interface{}) + + cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) + cpuCores := cpuBlock[mkCPUCores].(int) + cpuFlags := cpuBlock[mkCPUFlags].([]interface{}) + cpuHotplugged := cpuBlock[mkCPUHotplugged].(int) + cpuLimit := cpuBlock[mkCPULimit].(int) + cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool)) + cpuSockets := cpuBlock[mkCPUSockets].(int) + cpuType := cpuBlock[mkCPUType].(string) + cpuUnits := cpuBlock[mkCPUUnits].(int) + cpuAffinity := cpuBlock[mkCPUAffinity].(string) + + cpuFlagsConverted := make([]string, len(cpuFlags)) + + for fi, flag := range cpuFlags { + cpuFlagsConverted[fi] = flag.(string) + } + + // Only the root account is allowed to change the CPU architecture, which makes this check necessary. + if api.API().IsRootTicket() || + cpuArchitecture != dvCPUArchitecture { + updateBody.CPUArchitecture = &cpuArchitecture + } + + if cpuCores != dvCPUCores && cpuCores > 0 { + // update only if we have non-default & non-empty value + updateBody.CPUCores = &cpuCores + } + + updateBody.NUMAEnabled = &cpuNUMA + updateBody.CPUSockets = &cpuSockets + updateBody.CPUUnits = &cpuUnits + + if cpuType != dvCPUType && cpuType != "" { + // update only if we have non-default & non-empty value + if updateBody.CPUEmulation == nil { + updateBody.CPUEmulation = &vms.CustomCPUEmulation{} + } + + updateBody.CPUEmulation.Type = &cpuType + } + + if len(cpuFlagsConverted) > 0 { + if updateBody.CPUEmulation == nil { + updateBody.CPUEmulation = &vms.CustomCPUEmulation{} + } + + updateBody.CPUEmulation.Flags = &cpuFlagsConverted + } + + if cpuAffinity != "" { + updateBody.CPUAffinity = &cpuAffinity + } + + if cpuHotplugged > 0 { + updateBody.VirtualCPUCount = &cpuHotplugged + } + + if cpuLimit > 0 { + updateBody.CPULimit = &cpuLimit + } + } +} + +func Create(resource *schema.Resource, d *schema.ResourceData, api proxmox.Client, createBody *vms.CreateRequestBody) error { + cpuBlock, err := structure.GetSchemaBlock( + resource, + d, + []string{MkCPU}, + 0, + true, + ) + if err != nil { + return fmt.Errorf("error reading CPU block: %w", err) + } + + cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) + cpuCores := cpuBlock[mkCPUCores].(int) + cpuFlags := cpuBlock[mkCPUFlags].([]interface{}) + cpuHotplugged := cpuBlock[mkCPUHotplugged].(int) + cpuLimit := cpuBlock[mkCPULimit].(int) + cpuSockets := cpuBlock[mkCPUSockets].(int) + cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool)) + cpuType := cpuBlock[mkCPUType].(string) + cpuUnits := cpuBlock[mkCPUUnits].(int) + cpuAffinity := cpuBlock[mkCPUAffinity].(string) + + if cpuCores != dvCPUCores && cpuCores > 0 { + // set only if we have non-default & non-empty value + createBody.CPUCores = &cpuCores + } + + if cpuSockets != dvCPUSockets && cpuSockets > 0 { + // set only if we have non-default & non-empty value + createBody.CPUSockets = &cpuSockets + } + + if cpuUnits != dvCPUUnits && cpuUnits > 0 { + // set only if we have non-default & non-empty value + createBody.CPUUnits = &cpuUnits + } + + if cpuNUMA != dvCPUNUMA && cpuNUMA { + // set only if we have non-default & non-empty value + createBody.NUMAEnabled = &cpuNUMA + } + + if cpuLimit > 0 { + createBody.CPULimit = &cpuLimit + } + + if cpuAffinity != "" { + createBody.CPUAffinity = &cpuAffinity + } + + if cpuHotplugged > 0 { + createBody.VirtualCPUCount = &cpuHotplugged + } + + if cpuType != dvCPUType && cpuType != "" { + // set only if we have non-default & non-empty value + if createBody.CPUEmulation == nil { + createBody.CPUEmulation = &vms.CustomCPUEmulation{} + } + + createBody.CPUEmulation.Type = &cpuType + } + + cpuFlagsConverted := make([]string, len(cpuFlags)) + for fi, flag := range cpuFlags { + cpuFlagsConverted[fi] = flag.(string) + } + if len(cpuFlagsConverted) > 0 { + // set only if we have non-default & non-empty value + if createBody.CPUEmulation == nil { + createBody.CPUEmulation = &vms.CustomCPUEmulation{} + } + + createBody.CPUEmulation.Flags = &cpuFlagsConverted + } + + // Only the root account is allowed to change the CPU architecture, which makes this check necessary. + if api.API().IsRootTicket() || + cpuArchitecture != dvCPUArchitecture { + createBody.CPUArchitecture = &cpuArchitecture + } + + return nil +} + +func Read(vmConfig *vms.GetResponseData, api proxmox.Client, d *schema.ResourceData, clone bool) error { + // Compare the CPU configuration to the one stored in the state. + cpu := map[string]interface{}{} + + if vmConfig.CPUArchitecture != nil { + cpu[mkCPUArchitecture] = *vmConfig.CPUArchitecture + } else { + // Default value of "arch" is "" according to the API documentation. + // However, assume the provider's default value as a workaround when the root account is not being used. + if !api.API().IsRootTicket() { + cpu[mkCPUArchitecture] = dvCPUArchitecture + } else { + cpu[mkCPUArchitecture] = "" + } + } + + if vmConfig.CPUCores != nil { + cpu[mkCPUCores] = *vmConfig.CPUCores + //} else { + // // Default value of "cores" is "1" according to the API documentation. + // cpu[mkCPUCores] = 1 + } + + if vmConfig.VirtualCPUCount != nil { + cpu[mkCPUHotplugged] = *vmConfig.VirtualCPUCount + } else { + // Default value of "vcpus" is "1" according to the API documentation. + cpu[mkCPUHotplugged] = 0 + } + + if vmConfig.CPULimit != nil { + cpu[mkCPULimit] = *vmConfig.CPULimit + } else { + // Default value of "cpulimit" is "0" according to the API documentation. + cpu[mkCPULimit] = 0 + } + + if vmConfig.NUMAEnabled != nil { + cpu[mkCPUNUMA] = *vmConfig.NUMAEnabled + } else { + // Default value of "numa" is "false" according to the API documentation. + cpu[mkCPUNUMA] = false + } + + if vmConfig.CPUSockets != nil { + cpu[mkCPUSockets] = *vmConfig.CPUSockets + } else { + // Default value of "sockets" is "1" according to the API documentation. + cpu[mkCPUSockets] = 1 + } + + 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[mkCPUFlags] = convertedFlags + //} else { + // cpu[mkCPUFlags] = []interface{}{} + } + + cpu[mkCPUType] = vmConfig.CPUEmulation.Type + //} else { + // cpu[mkCPUFlags] = []interface{}{} + // // Default value of "cputype" is "qemu64" according to the QEMU documentation. + // cpu[mkCPUType] = dvCPUType + } + + if vmConfig.CPUUnits != nil { + cpu[mkCPUUnits] = *vmConfig.CPUUnits + } else { + // Default value of "cpuunits" is "1024" according to the API documentation. + cpu[mkCPUUnits] = 1024 + } + + if vmConfig.CPUAffinity != nil { + cpu[mkCPUAffinity] = *vmConfig.CPUAffinity + } else { + cpu[mkCPUAffinity] = "" + } + + currentCPU := d.Get(MkCPU).([]interface{}) + + //if len(clone) > 0 { + // if len(currentCPU) > 0 { + // err := d.Set(mkCPU, []interface{}{cpu}) + // diags = append(diags, diag.FromErr(err)...) + // } + //} else if len(currentCPU) > 0 || + // cpu[mkCPUArchitecture] != dvCPUArchitecture || + // cpu[mkCPUCores] != dvCPUCores || + // len(cpu[mkCPUFlags].([]interface{})) > 0 || + // cpu[mkCPUHotplugged] != dvCPUHotplugged || + // cpu[mkCPULimit] != dvCPULimit || + // cpu[mkCPUSockets] != dvCPUSockets || + // cpu[mkCPUType] != dvCPUType || + // cpu[mkCPUUnits] != dvCPUUnits { + // err := d.Set(mkCPU, []interface{}{cpu}) + // diags = append(diags, diag.FromErr(err)...) + //} + + if clone || len(currentCPU) > 0 || + cpu[mkCPUArchitecture] != dvCPUArchitecture || + //cpu[mkCPUCores] != dvCPUCores || + //len(cpu[mkCPUFlags].([]interface{})) > 0 || + cpu[mkCPUHotplugged] != dvCPUHotplugged || + cpu[mkCPULimit] != dvCPULimit || + cpu[mkCPUSockets] != dvCPUSockets || + //cpu[mkCPUType] != dvCPUType || + cpu[mkCPUUnits] != dvCPUUnits { + err := d.Set(MkCPU, []interface{}{cpu}) + if err != nil { + return fmt.Errorf("error setting CPU: %w", err) + } + } + + return nil +} + +func Update(d *schema.ResourceData, resource *schema.Resource, api proxmox.Client, updateBody *vms.UpdateRequestBody) ([]string, bool, error) { + // Prepare the new CPU configuration. + + if !d.HasChange(MkCPU) { + return nil, false, nil + } + + del := []string{} + cpuBlock, err := structure.GetSchemaBlock( + resource, + d, + []string{MkCPU}, + 0, + true, + ) + if err != nil { + return nil, false, err + } + + cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) + cpuCores := cpuBlock[mkCPUCores].(int) + cpuFlags := cpuBlock[mkCPUFlags].([]interface{}) + cpuHotplugged := cpuBlock[mkCPUHotplugged].(int) + cpuLimit := cpuBlock[mkCPULimit].(int) + cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool)) + cpuSockets := cpuBlock[mkCPUSockets].(int) + cpuType := cpuBlock[mkCPUType].(string) + cpuUnits := cpuBlock[mkCPUUnits].(int) + cpuAffinity := cpuBlock[mkCPUAffinity].(string) + + // Only the root account is allowed to change the CPU architecture, which makes this check necessary. + if api.API().IsRootTicket() || + cpuArchitecture != dvCPUArchitecture { + updateBody.CPUArchitecture = &cpuArchitecture + } + + if cpuCores != dvCPUCores && cpuCores > 0 { + // set only if we have non-default & non-empty value + updateBody.CPUCores = &cpuCores + } + + updateBody.CPUSockets = &cpuSockets + updateBody.CPUUnits = &cpuUnits + updateBody.NUMAEnabled = &cpuNUMA + + // CPU affinity is a special case, only root can change it. + // we can't even have it in the delete list, as PVE will return an error for non-root. + // Hence, checking explicitly if it has changed. + if d.HasChange(MkCPU + ".0." + mkCPUAffinity) { + if cpuAffinity != "" { + updateBody.CPUAffinity = &cpuAffinity + } else { + del = append(del, "affinity") + } + } + + if cpuHotplugged > 0 { + updateBody.VirtualCPUCount = &cpuHotplugged + } else { + del = append(del, "vcpus") + } + + if cpuLimit > 0 { + updateBody.CPULimit = &cpuLimit + } else { + del = append(del, "cpulimit") + } + + cpuFlagsConverted := make([]string, len(cpuFlags)) + + for fi, flag := range cpuFlags { + cpuFlagsConverted[fi] = flag.(string) + } + + if cpuType != dvCPUType && cpuType != "" { + // set only if we have non-default & non-empty value + if updateBody.CPUEmulation == nil { + updateBody.CPUEmulation = &vms.CustomCPUEmulation{} + } + + updateBody.CPUEmulation.Type = &cpuType + } + + if len(cpuFlagsConverted) > 0 { + // set only if we have non-default & non-empty value + if updateBody.CPUEmulation == nil { + updateBody.CPUEmulation = &vms.CustomCPUEmulation{} + } + + updateBody.CPUEmulation.Flags = &cpuFlagsConverted + } + + // TODO: this may not be true + rebootRequired := true + + return del, rebootRequired, nil +} diff --git a/proxmoxtf/resource/vm/cpu/schema.go b/proxmoxtf/resource/vm/cpu/schema.go new file mode 100644 index 00000000..ca40f9c8 --- /dev/null +++ b/proxmoxtf/resource/vm/cpu/schema.go @@ -0,0 +1,141 @@ +package cpu + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/validators" +) + +const ( + dvCPUArchitecture = "x86_64" + dvCPUCores = 1 + dvCPUHotplugged = 0 + dvCPULimit = 0 + dvCPUNUMA = false + dvCPUSockets = 1 + dvCPUType = "qemu64" + dvCPUUnits = 1024 + dvCPUAffinity = "" + + MkCPU = "cpu" + mkCPUArchitecture = "architecture" + mkCPUCores = "cores" + mkCPUFlags = "flags" + mkCPUHotplugged = "hotplugged" + mkCPULimit = "limit" + mkCPUNUMA = "numa" + mkCPUSockets = "sockets" + mkCPUType = "type" + mkCPUUnits = "units" + mkCPUAffinity = "affinity" +) + +func Schema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + MkCPU: { + Type: schema.TypeList, + Description: "The CPU allocation", + Optional: true, + Computed: true, + DefaultFunc: func() (interface{}, error) { + return []interface{}{ + map[string]interface{}{ + mkCPUArchitecture: dvCPUArchitecture, + mkCPUCores: dvCPUCores, + mkCPUFlags: []interface{}{}, + mkCPUHotplugged: dvCPUHotplugged, + mkCPULimit: dvCPULimit, + mkCPUNUMA: dvCPUNUMA, + mkCPUSockets: dvCPUSockets, + mkCPUType: dvCPUType, + mkCPUUnits: dvCPUUnits, + mkCPUAffinity: dvCPUAffinity, + }, + }, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkCPUArchitecture: { + Type: schema.TypeString, + Description: "The CPU architecture", + Optional: true, + Default: dvCPUArchitecture, + ValidateDiagFunc: validators.CPUArchitectureValidator(), + }, + mkCPUCores: { + Type: schema.TypeInt, + Description: "The number of CPU cores", + Optional: true, + Computed: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 2304)), + }, + mkCPUFlags: { + Type: schema.TypeList, + Description: "The CPU flags", + Optional: true, + Computed: true, + //DefaultFunc: func() (interface{}, error) { + // return []interface{}{}, nil + //}, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + mkCPUHotplugged: { + Type: schema.TypeInt, + Description: "The number of hotplugged vCPUs", + Optional: true, + Default: dvCPUHotplugged, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 2304)), + }, + mkCPULimit: { + Type: schema.TypeInt, + Description: "Limit of CPU usage", + Optional: true, + Default: dvCPULimit, + ValidateDiagFunc: validation.ToDiagFunc( + validation.IntBetween(0, 128), + ), + }, + mkCPUNUMA: { + Type: schema.TypeBool, + Description: "Enable/disable NUMA.", + Optional: true, + Default: dvCPUNUMA, + }, + mkCPUSockets: { + Type: schema.TypeInt, + Description: "The number of CPU sockets", + Optional: true, + Default: dvCPUSockets, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 16)), + }, + mkCPUType: { + Type: schema.TypeString, + Description: "The emulated CPU type", + Optional: true, + Computed: true, + ValidateDiagFunc: validators.CPUTypeValidator(), + }, + mkCPUUnits: { + Type: schema.TypeInt, + Description: "The CPU units", + Optional: true, + Default: dvCPUUnits, + ValidateDiagFunc: validation.ToDiagFunc( + validation.IntBetween(2, 262144), + ), + }, + mkCPUAffinity: { + Type: schema.TypeString, + Description: "The CPU affinity", + Optional: true, + Default: dvCPUAffinity, + ValidateDiagFunc: validators.CPUAffinityValidator(), + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, + } +} diff --git a/proxmoxtf/resource/vm/cpu/schema_test.go b/proxmoxtf/resource/vm/cpu/schema_test.go new file mode 100644 index 00000000..1f5e9575 --- /dev/null +++ b/proxmoxtf/resource/vm/cpu/schema_test.go @@ -0,0 +1,40 @@ +package cpu + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bpg/terraform-provider-proxmox/proxmoxtf/test" +) + +func TestCPUSchema(t *testing.T) { + t.Parallel() + + s := Schema() + + cpuSchema := test.AssertNestedSchemaExistence(t, s, MkCPU) + + test.AssertOptionalArguments(t, cpuSchema, []string{ + mkCPUArchitecture, + mkCPUCores, + mkCPUFlags, + mkCPUHotplugged, + mkCPUNUMA, + mkCPUSockets, + mkCPUType, + mkCPUUnits, + }) + + test.AssertValueTypes(t, cpuSchema, map[string]schema.ValueType{ + mkCPUArchitecture: schema.TypeString, + mkCPUCores: schema.TypeInt, + mkCPUFlags: schema.TypeList, + mkCPUHotplugged: schema.TypeInt, + mkCPUNUMA: schema.TypeBool, + mkCPUSockets: schema.TypeInt, + mkCPUType: schema.TypeString, + mkCPUUnits: schema.TypeInt, + }) + +} diff --git a/proxmoxtf/resource/vm/disk/schema_test.go b/proxmoxtf/resource/vm/disk/schema_test.go index 21dac165..8f69eba9 100644 --- a/proxmoxtf/resource/vm/disk/schema_test.go +++ b/proxmoxtf/resource/vm/disk/schema_test.go @@ -8,7 +8,7 @@ import ( "github.com/bpg/terraform-provider-proxmox/proxmoxtf/test" ) -func TestVMSchema(t *testing.T) { +func TestDiskSchema(t *testing.T) { t.Parallel() s := Schema() diff --git a/proxmoxtf/resource/vm/validators.go b/proxmoxtf/resource/vm/validators.go index f7a2192a..952d2725 100644 --- a/proxmoxtf/resource/vm/validators.go +++ b/proxmoxtf/resource/vm/validators.go @@ -52,105 +52,6 @@ func BIOSValidator() schema.SchemaValidateDiagFunc { }, false)) } -// CPUArchitectureValidator returns a schema validation function for a CPU architecture. -func CPUArchitectureValidator() schema.SchemaValidateDiagFunc { - return validation.ToDiagFunc(validation.StringInSlice([]string{ - "aarch64", - "x86_64", - }, false)) -} - -// CPUTypeValidator returns a schema validation function for a CPU type. -func CPUTypeValidator() schema.SchemaValidateDiagFunc { - standardTypes := []string{ - "486", - "Broadwell", - "Broadwell-IBRS", - "Broadwell-noTSX", - "Broadwell-noTSX-IBRS", - "Cascadelake-Server", - "Cascadelake-Server-noTSX", - "Cascadelake-Server-v2", - "Cascadelake-Server-v4", - "Cascadelake-Server-v5", - "Conroe", - "Cooperlake", - "Cooperlake-v2", - "EPYC", - "EPYC-IBPB", - "EPYC-Milan", - "EPYC-Rome", - "EPYC-Rome-v2", - "EPYC-v3", - "Haswell", - "Haswell-IBRS", - "Haswell-noTSX", - "Haswell-noTSX-IBRS", - "Icelake-Client", - "Icelake-Client-noTSX", - "Icelake-Server", - "Icelake-Server-noTSX", - "Icelake-Server-v3", - "Icelake-Server-v4", - "Icelake-Server-v5", - "Icelake-Server-v6", - "IvyBridge", - "IvyBridge-IBRS", - "KnightsMill", - "Nehalem", - "Nehalem-IBRS", - "Opteron_G1", - "Opteron_G2", - "Opteron_G3", - "Opteron_G4", - "Opteron_G5", - "Penryn", - "SandyBridge", - "SandyBridge-IBRS", - "SapphireRapids", - "Skylake-Client", - "Skylake-Client-IBRS", - "Skylake-Client-noTSX-IBRS", - "Skylake-Client-v4", - "Skylake-Server", - "Skylake-Server-IBRS", - "Skylake-Server-noTSX-IBRS", - "Skylake-Server-v4", - "Skylake-Server-v5", - "Westmere", - "Westmere-IBRS", - "athlon", - "core2duo", - "coreduo", - "host", - "kvm32", - "kvm64", - "max", - "pentium", - "pentium2", - "pentium3", - "phenom", - "qemu32", - "qemu64", - "x86-64-v2", - "x86-64-v2-AES", - "x86-64-v3", - "x86-64-v4", - } - - return validation.ToDiagFunc(validation.Any( - validation.StringInSlice(standardTypes, false), - validation.StringMatch(regexp.MustCompile(`^custom-.+$`), "must be a valid custom CPU type"), - )) -} - -// CPUAffinityValidator returns a schema validation function for a CPU affinity. -func CPUAffinityValidator() schema.SchemaValidateDiagFunc { - return validation.ToDiagFunc( - validation.StringMatch(regexp.MustCompile(`^\d+[\d-,]*$`), "must contain numbers or number ranges separated by ','"), - ) -} - // QEMUAgentTypeValidator is a schema validation function for QEMU agent types. func QEMUAgentTypeValidator() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringInSlice([]string{"isa", "virtio"}, false)) diff --git a/proxmoxtf/resource/vm/validators_test.go b/proxmoxtf/resource/vm/validators_test.go index 28ea96e4..9768300d 100644 --- a/proxmoxtf/resource/vm/validators_test.go +++ b/proxmoxtf/resource/vm/validators_test.go @@ -12,37 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCPUType(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - value string - valid bool - }{ - {"empty", "", false}, - {"invalid", "invalid", false}, - {"valid", "host", true}, - {"valid", "qemu64", true}, - {"valid", "custom-abc", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - f := CPUTypeValidator() - res := f(tt.value, nil) - - if tt.valid { - require.Empty(t, res, "validate: '%s'", tt.value) - } else { - require.NotEmpty(t, res, "validate: '%s'", tt.value) - } - }) - } -} - func TestMachineType(t *testing.T) { t.Parallel() diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 66f70dc1..91584b69 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -19,6 +19,7 @@ import ( "golang.org/x/exp/maps" + "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/cpu" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/disk" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/network" "github.com/bpg/terraform-provider-proxmox/utils" @@ -59,15 +60,6 @@ const ( dvCloneNodeName = "" dvCloneFull = true dvCloneRetries = 1 - dvCPUArchitecture = "x86_64" - dvCPUCores = 1 - dvCPUHotplugged = 0 - dvCPULimit = 0 - dvCPUNUMA = false - dvCPUSockets = 1 - dvCPUType = "qemu64" - dvCPUUnits = 1024 - dvCPUAffinity = "" dvDescription = "" dvEFIDiskDatastoreID = "local-lvm" @@ -164,17 +156,6 @@ const ( mkCloneNodeName = "node_name" mkCloneVMID = "vm_id" mkCloneFull = "full" - mkCPU = "cpu" - mkCPUArchitecture = "architecture" - mkCPUCores = "cores" - mkCPUFlags = "flags" - mkCPUHotplugged = "hotplugged" - mkCPULimit = "limit" - mkCPUNUMA = "numa" - mkCPUSockets = "sockets" - mkCPUType = "type" - mkCPUUnits = "units" - mkCPUAffinity = "affinity" mkDescription = "description" mkNUMA = "numa" @@ -495,108 +476,6 @@ func VM() *schema.Resource { MaxItems: 1, MinItems: 0, }, - mkCPU: { - Type: schema.TypeList, - Description: "The CPU allocation", - Optional: true, - DefaultFunc: func() (interface{}, error) { - return []interface{}{ - map[string]interface{}{ - mkCPUArchitecture: dvCPUArchitecture, - mkCPUCores: dvCPUCores, - mkCPUFlags: []interface{}{}, - mkCPUHotplugged: dvCPUHotplugged, - mkCPULimit: dvCPULimit, - mkCPUNUMA: dvCPUNUMA, - mkCPUSockets: dvCPUSockets, - mkCPUType: dvCPUType, - mkCPUUnits: dvCPUUnits, - mkCPUAffinity: dvCPUAffinity, - }, - }, nil - }, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - mkCPUArchitecture: { - Type: schema.TypeString, - Description: "The CPU architecture", - Optional: true, - Default: dvCPUArchitecture, - ValidateDiagFunc: CPUArchitectureValidator(), - }, - mkCPUCores: { - Type: schema.TypeInt, - Description: "The number of CPU cores", - Optional: true, - Default: dvCPUCores, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 2304)), - }, - mkCPUFlags: { - Type: schema.TypeList, - Description: "The CPU flags", - Optional: true, - DefaultFunc: func() (interface{}, error) { - return []interface{}{}, nil - }, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - mkCPUHotplugged: { - Type: schema.TypeInt, - Description: "The number of hotplugged vCPUs", - Optional: true, - Default: dvCPUHotplugged, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 2304)), - }, - mkCPULimit: { - Type: schema.TypeInt, - Description: "Limit of CPU usage", - Optional: true, - Default: dvCPULimit, - ValidateDiagFunc: validation.ToDiagFunc( - validation.IntBetween(0, 128), - ), - }, - mkCPUNUMA: { - Type: schema.TypeBool, - Description: "Enable/disable NUMA.", - Optional: true, - Default: dvCPUNUMA, - }, - mkCPUSockets: { - Type: schema.TypeInt, - Description: "The number of CPU sockets", - Optional: true, - Default: dvCPUSockets, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 16)), - }, - mkCPUType: { - Type: schema.TypeString, - Description: "The emulated CPU type", - Optional: true, - Default: dvCPUType, - ValidateDiagFunc: CPUTypeValidator(), - }, - mkCPUUnits: { - Type: schema.TypeInt, - Description: "The CPU units", - Optional: true, - Default: dvCPUUnits, - ValidateDiagFunc: validation.ToDiagFunc( - validation.IntBetween(2, 262144), - ), - }, - mkCPUAffinity: { - Type: schema.TypeString, - Description: "The CPU affinity", - Optional: true, - Default: dvCPUAffinity, - ValidateDiagFunc: CPUAffinityValidator(), - }, - }, - }, - MaxItems: 1, - MinItems: 0, - }, mkDescription: { Type: schema.TypeString, Description: "The description", @@ -1450,6 +1329,7 @@ func VM() *schema.Resource { }, } + structure.MergeSchema(s, cpu.Schema()) structure.MergeSchema(s, disk.Schema()) structure.MergeSchema(s, network.Schema()) @@ -1871,7 +1751,6 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d kvmArguments := d.Get(mkKVMArguments).(string) scsiHardware := d.Get(mkSCSIHardware).(string) cdrom := d.Get(mkCDROM).([]interface{}) - cpu := d.Get(mkCPU).([]interface{}) initialization := d.Get(mkInitialization).([]interface{}) hostPCI := d.Get(mkHostPCI).([]interface{}) hostUSB := d.Get(mkHostUSB).([]interface{}) @@ -1964,53 +1843,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } } - if len(cpu) > 0 && cpu[0] != nil { - cpuBlock := cpu[0].(map[string]interface{}) - - cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) - cpuCores := cpuBlock[mkCPUCores].(int) - cpuFlags := cpuBlock[mkCPUFlags].([]interface{}) - cpuHotplugged := cpuBlock[mkCPUHotplugged].(int) - cpuLimit := cpuBlock[mkCPULimit].(int) - cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool)) - cpuSockets := cpuBlock[mkCPUSockets].(int) - cpuType := cpuBlock[mkCPUType].(string) - cpuUnits := cpuBlock[mkCPUUnits].(int) - cpuAffinity := cpuBlock[mkCPUAffinity].(string) - - cpuFlagsConverted := make([]string, len(cpuFlags)) - - for fi, flag := range cpuFlags { - cpuFlagsConverted[fi] = flag.(string) - } - - // Only the root account is allowed to change the CPU architecture, which makes this check necessary. - if api.API().IsRootTicket() || - cpuArchitecture != dvCPUArchitecture { - updateBody.CPUArchitecture = &cpuArchitecture - } - - updateBody.CPUCores = &cpuCores - updateBody.CPUEmulation = &vms.CustomCPUEmulation{ - Flags: &cpuFlagsConverted, - Type: cpuType, - } - updateBody.NUMAEnabled = &cpuNUMA - updateBody.CPUSockets = &cpuSockets - updateBody.CPUUnits = &cpuUnits - - if cpuAffinity != "" { - updateBody.CPUAffinity = &cpuAffinity - } - - if cpuHotplugged > 0 { - updateBody.VirtualCPUCount = &cpuHotplugged - } - - if cpuLimit > 0 { - updateBody.CPULimit = &cpuLimit - } - } + cpu.UpdateWhenClone(d, api, updateBody) vmConfig, err := vmAPI.GetVM(ctx) if err != nil { @@ -2194,8 +2027,6 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d return diag.FromErr(e) } - ///////////////// - allDiskInfo := disk.GetInfo(vmConfig, d) // from the cloned VM planDisks, e := disk.GetDiskDeviceObjects(d, VM(), nil) // from the resource config @@ -2382,28 +2213,6 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) cdromFileID = "cdrom" } - cpuBlock, err := structure.GetSchemaBlock( - resource, - d, - []string{mkCPU}, - 0, - true, - ) - if err != nil { - return diag.FromErr(err) - } - - cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) - cpuCores := cpuBlock[mkCPUCores].(int) - cpuFlags := cpuBlock[mkCPUFlags].([]interface{}) - cpuHotplugged := cpuBlock[mkCPUHotplugged].(int) - cpuLimit := cpuBlock[mkCPULimit].(int) - cpuSockets := cpuBlock[mkCPUSockets].(int) - cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool)) - cpuType := cpuBlock[mkCPUType].(string) - cpuUnits := cpuBlock[mkCPUUnits].(int) - cpuAffinity := cpuBlock[mkCPUAffinity].(string) - description := d.Get(mkDescription).(string) var efiDisk *vms.CustomEFIDisk @@ -2590,11 +2399,6 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) } } - cpuFlagsConverted := make([]string, len(cpuFlags)) - for fi, flag := range cpuFlags { - cpuFlagsConverted[fi] = flag.(string) - } - ideDevice2Media := "cdrom" ideDevices := vms.CustomStorageDevices{ cdromCloudInitInterface: &vms.CustomStorageDevice{ @@ -2631,14 +2435,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) Boot: &vms.CustomBoot{ Order: &bootOrderConverted, }, - CloudInitConfig: initializationConfig, - CPUCores: &cpuCores, - CPUEmulation: &vms.CustomCPUEmulation{ - Flags: &cpuFlagsConverted, - Type: cpuType, - }, - CPUSockets: &cpuSockets, - CPUUnits: &cpuUnits, + CloudInitConfig: initializationConfig, DedicatedMemory: &memoryDedicated, DeletionProtection: &protection, EFIDisk: efiDisk, @@ -2647,7 +2444,6 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) IDEDevices: ideDevices, KeyboardLayout: &keyboardLayout, NetworkDevices: networkDeviceObjects, - NUMAEnabled: &cpuNUMA, NUMADevices: numaDeviceObjects, OSType: &operatingSystemType, PCIDevices: pciDeviceObjects, @@ -2664,6 +2460,11 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) VMID: &vmID, } + err = cpu.Create(resource, d, api, createBody) + if err != nil { + return diag.FromErr(err) + } + if sataDeviceObjects != nil { createBody.SATADevices = sataDeviceObjects } @@ -2676,24 +2477,6 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) createBody.VirtualIODevices = virtioDeviceObjects } - // Only the root account is allowed to change the CPU architecture, which makes this check necessary. - if api.API().IsRootTicket() || - cpuArchitecture != dvCPUArchitecture { - createBody.CPUArchitecture = &cpuArchitecture - } - - if cpuHotplugged > 0 { - createBody.VirtualCPUCount = &cpuHotplugged - } - - if cpuLimit > 0 { - createBody.CPULimit = &cpuLimit - } - - if cpuAffinity != "" { - createBody.CPUAffinity = &cpuAffinity - } - if description != "" { createBody.Description = &description } @@ -3582,48 +3365,8 @@ func vmReadCustom( diags = append(diags, diag.FromErr(err)...) } - // Compare the CPU configuration to the one stored in the state. - cpu := map[string]interface{}{} - - if vmConfig.CPUArchitecture != nil { - cpu[mkCPUArchitecture] = *vmConfig.CPUArchitecture - } else { - // Default value of "arch" is "" according to the API documentation. - // However, assume the provider's default value as a workaround when the root account is not being used. - if !api.API().IsRootTicket() { - cpu[mkCPUArchitecture] = dvCPUArchitecture - } else { - cpu[mkCPUArchitecture] = "" - } - } - - if vmConfig.CPUCores != nil { - cpu[mkCPUCores] = *vmConfig.CPUCores - } else { - // Default value of "cores" is "1" according to the API documentation. - cpu[mkCPUCores] = 1 - } - - if vmConfig.VirtualCPUCount != nil { - cpu[mkCPUHotplugged] = *vmConfig.VirtualCPUCount - } else { - // Default value of "vcpus" is "1" according to the API documentation. - cpu[mkCPUHotplugged] = 0 - } - - if vmConfig.CPULimit != nil { - cpu[mkCPULimit] = *vmConfig.CPULimit - } else { - // Default value of "cpulimit" is "0" according to the API documentation. - cpu[mkCPULimit] = 0 - } - - if vmConfig.NUMAEnabled != nil { - cpu[mkCPUNUMA] = *vmConfig.NUMAEnabled - } else { - // Default value of "numa" is "false" according to the API documentation. - cpu[mkCPUNUMA] = false - } + err := cpu.Read(vmConfig, api, d, len(clone) > 0) + diags = append(diags, diag.FromErr(err)...) currentNUMAList := d.Get(mkNUMA).([]interface{}) numaMap := map[string]interface{}{} @@ -3663,66 +3406,6 @@ func vmReadCustom( diags = append(diags, diag.FromErr(err)...) } - if vmConfig.CPUSockets != nil { - cpu[mkCPUSockets] = *vmConfig.CPUSockets - } else { - // Default value of "sockets" is "1" according to the API documentation. - cpu[mkCPUSockets] = 1 - } - - 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[mkCPUFlags] = convertedFlags - } else { - cpu[mkCPUFlags] = []interface{}{} - } - - cpu[mkCPUType] = vmConfig.CPUEmulation.Type - } else { - cpu[mkCPUFlags] = []interface{}{} - // Default value of "cputype" is "qemu64" according to the QEMU documentation. - cpu[mkCPUType] = "qemu64" - } - - if vmConfig.CPUUnits != nil { - cpu[mkCPUUnits] = *vmConfig.CPUUnits - } else { - // Default value of "cpuunits" is "1024" according to the API documentation. - cpu[mkCPUUnits] = 1024 - } - - if vmConfig.CPUAffinity != nil { - cpu[mkCPUAffinity] = *vmConfig.CPUAffinity - } else { - cpu[mkCPUAffinity] = "" - } - - currentCPU := d.Get(mkCPU).([]interface{}) - - if len(clone) > 0 { - if len(currentCPU) > 0 { - err := d.Set(mkCPU, []interface{}{cpu}) - diags = append(diags, diag.FromErr(err)...) - } - } else if len(currentCPU) > 0 || - cpu[mkCPUArchitecture] != dvCPUArchitecture || - cpu[mkCPUCores] != dvCPUCores || - len(cpu[mkCPUFlags].([]interface{})) > 0 || - cpu[mkCPUHotplugged] != dvCPUHotplugged || - cpu[mkCPULimit] != dvCPULimit || - cpu[mkCPUSockets] != dvCPUSockets || - cpu[mkCPUType] != dvCPUType || - cpu[mkCPUUnits] != dvCPUUnits { - err := d.Set(mkCPU, []interface{}{cpu}) - diags = append(diags, diag.FromErr(err)...) - } - allDiskInfo := disk.GetInfo(vmConfig, d) diags = append(diags, disk.Read(ctx, d, allDiskInfo, vmID, api, nodeName, len(clone) > 0)...) @@ -4961,79 +4644,14 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } } - // Prepare the new CPU configuration. - - if d.HasChange(mkCPU) { - cpuBlock, err := structure.GetSchemaBlock( - resource, - d, - []string{mkCPU}, - 0, - true, - ) - if err != nil { - return diag.FromErr(err) - } - - cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) - cpuCores := cpuBlock[mkCPUCores].(int) - cpuFlags := cpuBlock[mkCPUFlags].([]interface{}) - cpuHotplugged := cpuBlock[mkCPUHotplugged].(int) - cpuLimit := cpuBlock[mkCPULimit].(int) - cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool)) - cpuSockets := cpuBlock[mkCPUSockets].(int) - cpuType := cpuBlock[mkCPUType].(string) - cpuUnits := cpuBlock[mkCPUUnits].(int) - cpuAffinity := cpuBlock[mkCPUAffinity].(string) - - // Only the root account is allowed to change the CPU architecture, which makes this check necessary. - if api.API().IsRootTicket() || - cpuArchitecture != dvCPUArchitecture { - updateBody.CPUArchitecture = &cpuArchitecture - } - - updateBody.CPUCores = &cpuCores - updateBody.CPUSockets = &cpuSockets - updateBody.CPUUnits = &cpuUnits - updateBody.NUMAEnabled = &cpuNUMA - - // CPU affinity is a special case, only root can change it. - // we can't even have it in the delete list, as PVE will return an error for non-root. - // Hence, checking explicitly if it has changed. - if d.HasChange(mkCPU + ".0." + mkCPUAffinity) { - if cpuAffinity != "" { - updateBody.CPUAffinity = &cpuAffinity - } else { - del = append(del, "affinity") - } - } - - if cpuHotplugged > 0 { - updateBody.VirtualCPUCount = &cpuHotplugged - } else { - del = append(del, "vcpus") - } - - if cpuLimit > 0 { - updateBody.CPULimit = &cpuLimit - } else { - del = append(del, "cpulimit") - } - - cpuFlagsConverted := make([]string, len(cpuFlags)) - - for fi, flag := range cpuFlags { - cpuFlagsConverted[fi] = flag.(string) - } - - updateBody.CPUEmulation = &vms.CustomCPUEmulation{ - Flags: &cpuFlagsConverted, - Type: cpuType, - } - - rebootRequired = true + dd, rr, err := cpu.Update(d, resource, api, updateBody) + if err != nil { + return diag.FromErr(err) } + del = append(del, dd...) + rebootRequired = rebootRequired || rr + // Prepare the new disk device configuration. allDiskInfo := disk.GetInfo(vmConfig, d) @@ -5042,7 +4660,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D return diag.FromErr(err) } - rr, err := disk.Update(d, planDisks, allDiskInfo, updateBody) + rr, err = disk.Update(d, planDisks, allDiskInfo, updateBody) if err != nil { return diag.FromErr(err) } diff --git a/proxmoxtf/resource/vm/vm_test.go b/proxmoxtf/resource/vm/vm_test.go index 43348acb..79606929 100644 --- a/proxmoxtf/resource/vm/vm_test.go +++ b/proxmoxtf/resource/vm/vm_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/stretchr/testify/require" + "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/cpu" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/disk" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/network" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/test" @@ -45,7 +46,7 @@ func TestVMSchema(t *testing.T) { mkBootOrder, mkCDROM, mkClone, - mkCPU, + cpu.MkCPU, mkDescription, disk.MkDisk, mkEFIDisk, @@ -75,7 +76,7 @@ func TestVMSchema(t *testing.T) { mkBIOS: schema.TypeString, mkBootOrder: schema.TypeList, mkCDROM: schema.TypeList, - mkCPU: schema.TypeList, + cpu.MkCPU: schema.TypeList, mkDescription: schema.TypeString, disk.MkDisk: schema.TypeList, mkEFIDisk: schema.TypeList, @@ -153,30 +154,6 @@ func TestVMSchema(t *testing.T) { mkCloneVMID: schema.TypeInt, }) - cpuSchema := test.AssertNestedSchemaExistence(t, s, mkCPU) - - test.AssertOptionalArguments(t, cpuSchema, []string{ - mkCPUArchitecture, - mkCPUCores, - mkCPUFlags, - mkCPUHotplugged, - mkCPUNUMA, - mkCPUSockets, - mkCPUType, - mkCPUUnits, - }) - - test.AssertValueTypes(t, cpuSchema, map[string]schema.ValueType{ - mkCPUArchitecture: schema.TypeString, - mkCPUCores: schema.TypeInt, - mkCPUFlags: schema.TypeList, - mkCPUHotplugged: schema.TypeInt, - mkCPUNUMA: schema.TypeBool, - mkCPUSockets: schema.TypeInt, - mkCPUType: schema.TypeString, - mkCPUUnits: schema.TypeInt, - }) - efiDiskSchema := test.AssertNestedSchemaExistence(t, s, mkEFIDisk) test.AssertOptionalArguments(t, efiDiskSchema, []string{