mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-08-22 19:38:35 +00:00
* feat(vm): add support for numa architecture attribute (#1156) Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev> * fix: numa blocks reordering issue Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev> Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
31b6812ce2
commit
dbbd966736
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -22,6 +22,7 @@
|
||||
"cmode",
|
||||
"cpulimit",
|
||||
"CPUNUMA",
|
||||
"cpus",
|
||||
"cputype",
|
||||
"cpuunits",
|
||||
"customdiff",
|
||||
@ -33,8 +34,10 @@
|
||||
"gocritic",
|
||||
"gosimple",
|
||||
"hookscript",
|
||||
"hostnodes",
|
||||
"hostpci",
|
||||
"Hotplugged",
|
||||
"Hugepages",
|
||||
"iface",
|
||||
"importdisk",
|
||||
"iothread",
|
||||
|
@ -417,6 +417,17 @@ output "ubuntu_vm_public_key" {
|
||||
|
||||
Settings `hugepages` and `keep_hugepages` are only allowed for `root@pam` authenticated user.
|
||||
And required `cpu.numa` to be enabled.
|
||||
- `numa` - (Optional) The NUMA configuration.
|
||||
- `device` - (Required) The NUMA device name for Proxmox, in form
|
||||
of `numaX` where `X` is a sequential number from 0 to 7.
|
||||
- `cpus` - (Required) The CPU cores to assign to the NUMA node (format is `0-7;16-31`).
|
||||
- `memory` - (Required) The memory in megabytes to assign to the NUMA node.
|
||||
- `hostnodes` - (Optional) The NUMA host nodes.
|
||||
- `policy` - (Optional) The NUMA policy (defaults to `preferred`).
|
||||
- `interleave` - Interleave memory across nodes.
|
||||
- `preferred` - Prefer the specified node.
|
||||
- `bind` - Only use the specified node.
|
||||
|
||||
- `migrate` - (Optional) Migrate the VM on node change instead of re-creating
|
||||
it (defaults to `false`).
|
||||
- `name` - (Optional) The virtual machine name.
|
||||
|
@ -131,6 +131,12 @@ resource "proxmox_virtual_environment_vm" "example" {
|
||||
# hugepages = "2"
|
||||
}
|
||||
|
||||
# numa {
|
||||
# device = "numa0"
|
||||
# cpus = "0-1"
|
||||
# memory = 768
|
||||
# }
|
||||
|
||||
connection {
|
||||
type = "ssh"
|
||||
agent = false
|
||||
|
@ -109,7 +109,7 @@ type CustomNetworkDevices []CustomNetworkDevice
|
||||
type CustomNUMADevice struct {
|
||||
CPUIDs []string `json:"cpus" url:"cpus,semicolon"`
|
||||
HostNodeNames *[]string `json:"hostnodes,omitempty" url:"hostnodes,omitempty,semicolon"`
|
||||
Memory *float64 `json:"memory,omitempty" url:"memory,omitempty"`
|
||||
Memory *int `json:"memory,omitempty" url:"memory,omitempty"`
|
||||
Policy *string `json:"policy,omitempty" url:"policy,omitempty"`
|
||||
}
|
||||
|
||||
@ -455,8 +455,15 @@ type GetResponseData struct {
|
||||
NetworkDevice29 *CustomNetworkDevice `json:"net29,omitempty"`
|
||||
NetworkDevice30 *CustomNetworkDevice `json:"net30,omitempty"`
|
||||
NetworkDevice31 *CustomNetworkDevice `json:"net31,omitempty"`
|
||||
NUMADevices *CustomNUMADevices `json:"numa_devices,omitempty"`
|
||||
NUMAEnabled *types.CustomBool `json:"numa,omitempty"`
|
||||
NUMADevices0 *CustomNUMADevice `json:"numa0,omitempty"`
|
||||
NUMADevices1 *CustomNUMADevice `json:"numa1,omitempty"`
|
||||
NUMADevices2 *CustomNUMADevice `json:"numa2,omitempty"`
|
||||
NUMADevices3 *CustomNUMADevice `json:"numa3,omitempty"`
|
||||
NUMADevices4 *CustomNUMADevice `json:"numa4,omitempty"`
|
||||
NUMADevices5 *CustomNUMADevice `json:"numa5,omitempty"`
|
||||
NUMADevices6 *CustomNUMADevice `json:"numa6,omitempty"`
|
||||
NUMADevices7 *CustomNUMADevice `json:"numa7,omitempty"`
|
||||
OSType *string `json:"ostype,omitempty"`
|
||||
Overwrite *types.CustomBool `json:"force,omitempty"`
|
||||
PCIDevice0 *CustomPCIDevice `json:"hostpci0,omitempty"`
|
||||
@ -939,7 +946,7 @@ func (r CustomNUMADevice) EncodeValues(key string, v *url.Values) error {
|
||||
}
|
||||
|
||||
if r.Memory != nil {
|
||||
values = append(values, fmt.Sprintf("memory=%f", *r.Memory))
|
||||
values = append(values, fmt.Sprintf("memory=%d", *r.Memory))
|
||||
}
|
||||
|
||||
if r.Policy != nil {
|
||||
@ -1590,6 +1597,41 @@ func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a CustomNUMADevice string to an object.
|
||||
func (r *CustomNUMADevice) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal CustomNUMADevice: %w", err)
|
||||
}
|
||||
|
||||
pairs := strings.Split(s, ",")
|
||||
|
||||
for _, p := range pairs {
|
||||
v := strings.Split(strings.TrimSpace(p), "=")
|
||||
if len(v) == 2 {
|
||||
switch v[0] {
|
||||
case "cpus":
|
||||
r.CPUIDs = strings.Split(v[1], ";")
|
||||
case "hostnodes":
|
||||
hostnodes := strings.Split(v[1], ";")
|
||||
r.HostNodeNames = &hostnodes
|
||||
case "memory":
|
||||
memory, err := strconv.Atoi(v[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse memory size: %w", err)
|
||||
}
|
||||
|
||||
r.Memory = &memory
|
||||
case "policy":
|
||||
r.Policy = &v[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a CustomPCIDevice string to an object.
|
||||
func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
|
@ -274,6 +274,47 @@ func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomNUMADevice_UnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
want *CustomNUMADevice
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "numa device all options",
|
||||
line: `"cpus=1-2;3-4,hostnodes=1-2,memory=1024,policy=preferred"`,
|
||||
want: &CustomNUMADevice{
|
||||
CPUIDs: []string{"1-2", "3-4"},
|
||||
HostNodeNames: &[]string{"1-2"},
|
||||
Memory: types.IntPtr(1024),
|
||||
Policy: types.StrPtr("preferred"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "numa device cpus/memory only",
|
||||
line: `"cpus=1-2,memory=1024"`,
|
||||
want: &CustomNUMADevice{
|
||||
CPUIDs: []string{"1-2"},
|
||||
Memory: types.IntPtr(1024),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := &CustomNUMADevice{}
|
||||
if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -147,7 +147,7 @@ func CPUTypeValidator() schema.SchemaValidateDiagFunc {
|
||||
// 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 but also number ranges"),
|
||||
validation.StringMatch(regexp.MustCompile(`^\d+[\d-,]*$`), "must contain numbers or number ranges separated by ','"),
|
||||
)
|
||||
}
|
||||
|
||||
@ -338,3 +338,13 @@ func SerialDeviceValidator() schema.SchemaValidateDiagFunc {
|
||||
return nil, es
|
||||
})
|
||||
}
|
||||
|
||||
// RangeSemicolonValidator is a proxmox list validation function for ranges with semicolon.
|
||||
func RangeSemicolonValidator() schema.SchemaValidateDiagFunc {
|
||||
return validation.ToDiagFunc(
|
||||
validation.StringMatch(
|
||||
regexp.MustCompile(`^\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*$`),
|
||||
"must contain numbers or number ranges separated by ';'",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -19,6 +20,7 @@ import (
|
||||
"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"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
@ -133,6 +135,8 @@ const (
|
||||
maxResourceVirtualEnvironmentVMSerialDevices = 4
|
||||
maxResourceVirtualEnvironmentVMHostPCIDevices = 8
|
||||
maxResourceVirtualEnvironmentVMHostUSBDevices = 4
|
||||
// hardcoded /usr/share/perl5/PVE/QemuServer/Memory.pm: "our $MAX_NUMA = 8".
|
||||
maxResourceVirtualEnvironmentVMNUMADevices = 8
|
||||
|
||||
mkRebootAfterCreation = "reboot"
|
||||
mkOnBoot = "on_boot"
|
||||
@ -171,6 +175,13 @@ const (
|
||||
mkCPUAffinity = "affinity"
|
||||
mkDescription = "description"
|
||||
|
||||
mkNUMA = "numa"
|
||||
mkNUMADevice = "device"
|
||||
mkNUMACPUIDs = "cpus"
|
||||
mkNUMAHostNodeNames = "hostnodes"
|
||||
mkNUMAMemory = "memory"
|
||||
mkNUMAPolicy = "policy"
|
||||
|
||||
mkEFIDisk = "efi_disk"
|
||||
mkEFIDiskDatastoreID = "datastore_id"
|
||||
mkEFIDiskFileFormat = "file_format"
|
||||
@ -1049,7 +1060,7 @@ func VM() *schema.Resource {
|
||||
Description: "Hugepages will not be deleted after VM shutdown and can be used for subsequent starts",
|
||||
Optional: true,
|
||||
Default: dvMemoryKeepHugepages,
|
||||
RequiredWith: []string{"cpu.0.numa", "memory.0.hugepages"},
|
||||
RequiredWith: []string{"cpu.0.numa"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -1067,6 +1078,68 @@ func VM() *schema.Resource {
|
||||
Description: "The node name",
|
||||
Required: true,
|
||||
},
|
||||
mkNUMA: {
|
||||
Type: schema.TypeList,
|
||||
Description: "The NUMA topology",
|
||||
Optional: true,
|
||||
ForceNew: false,
|
||||
DefaultFunc: func() (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
},
|
||||
DiffSuppressFunc: structure.SuppressIfListsOfMapsAreEqualIgnoringOrderByKey(mkNUMADevice),
|
||||
DiffSuppressOnRefresh: true,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
mkNUMADevice: {
|
||||
Type: schema.TypeString,
|
||||
Description: "Numa node device ID",
|
||||
Optional: false,
|
||||
Required: true,
|
||||
RequiredWith: []string{"cpu.0.numa"},
|
||||
ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch(
|
||||
regexp.MustCompile(`^numa\d+$`),
|
||||
"numa node device ID must be in the format 'numaX' where X is a number",
|
||||
)),
|
||||
},
|
||||
mkNUMACPUIDs: {
|
||||
Type: schema.TypeString,
|
||||
Description: "CPUs accessing this NUMA node",
|
||||
Optional: false,
|
||||
Required: true,
|
||||
RequiredWith: []string{"cpu.0.numa"},
|
||||
ValidateDiagFunc: RangeSemicolonValidator(),
|
||||
},
|
||||
mkNUMAMemory: {
|
||||
Type: schema.TypeInt,
|
||||
Description: "Amount of memory this NUMA node provides",
|
||||
Optional: false,
|
||||
Required: true,
|
||||
RequiredWith: []string{"cpu.0.numa"},
|
||||
ValidateDiagFunc: validation.ToDiagFunc(
|
||||
validation.IntBetween(64, 268435456),
|
||||
),
|
||||
},
|
||||
mkNUMAHostNodeNames: {
|
||||
Type: schema.TypeString,
|
||||
Description: "Host NUMA nodes to use",
|
||||
Optional: true,
|
||||
RequiredWith: []string{"cpu.0.numa"},
|
||||
ValidateDiagFunc: RangeSemicolonValidator(),
|
||||
},
|
||||
mkNUMAPolicy: {
|
||||
Type: schema.TypeString,
|
||||
Description: "NUMA policy",
|
||||
Optional: true,
|
||||
Default: "preferred",
|
||||
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
|
||||
"bind",
|
||||
"interleave",
|
||||
"preferred",
|
||||
}, true)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mkMigrate: {
|
||||
Type: schema.TypeBool,
|
||||
Description: "Whether to migrate the VM on node change instead of re-creating it",
|
||||
@ -1795,6 +1868,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
||||
hostUSB := d.Get(mkHostUSB).([]interface{})
|
||||
keyboardLayout := d.Get(mkKeyboardLayout).(string)
|
||||
memory := d.Get(mkMemory).([]interface{})
|
||||
numa := d.Get(mkNUMA).([]interface{})
|
||||
operatingSystem := d.Get(mkOperatingSystem).([]interface{})
|
||||
serialDevice := d.Get(mkSerialDevice).([]interface{})
|
||||
onBoot := types.CustomBool(d.Get(mkOnBoot).(bool))
|
||||
@ -1969,6 +2043,10 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
||||
updateBody.PCIDevices = vmGetHostPCIDeviceObjects(d)
|
||||
}
|
||||
|
||||
if len(numa) > 0 {
|
||||
updateBody.NUMADevices = vmGetNumaDeviceObjects(d)
|
||||
}
|
||||
|
||||
if len(hostUSB) > 0 {
|
||||
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
||||
}
|
||||
@ -2389,6 +2467,8 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
|
||||
|
||||
pciDeviceObjects := vmGetHostPCIDeviceObjects(d)
|
||||
|
||||
numaDeviceObjects := vmGetNumaDeviceObjects(d)
|
||||
|
||||
usbDeviceObjects := vmGetHostUSBDeviceObjects(d)
|
||||
|
||||
keyboardLayout := d.Get(mkKeyboardLayout).(string)
|
||||
@ -2559,6 +2639,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
|
||||
KeyboardLayout: &keyboardLayout,
|
||||
NetworkDevices: networkDeviceObjects,
|
||||
NUMAEnabled: &cpuNUMA,
|
||||
NUMADevices: numaDeviceObjects,
|
||||
OSType: &operatingSystemType,
|
||||
PCIDevices: pciDeviceObjects,
|
||||
SCSIHardware: &scsiHardware,
|
||||
@ -3029,6 +3110,49 @@ func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices {
|
||||
return pciDeviceObjects
|
||||
}
|
||||
|
||||
func vmGetNumaDeviceObjects(d *schema.ResourceData) vms.CustomNUMADevices {
|
||||
numaNode := d.Get(mkNUMA).([]interface{})
|
||||
numaNodeObjects := make(vms.CustomNUMADevices, len(numaNode))
|
||||
|
||||
for i, numaNodeEntry := range numaNode {
|
||||
block := numaNodeEntry.(map[string]interface{})
|
||||
|
||||
deviceName := block[mkNUMADevice].(string)
|
||||
ids := block[mkNUMACPUIDs].(string)
|
||||
hostNodes, _ := block[mkNUMAHostNodeNames].(string)
|
||||
memory, _ := block[mkNUMAMemory].(int)
|
||||
policy, _ := block[mkNUMAPolicy].(string)
|
||||
|
||||
device := vms.CustomNUMADevice{
|
||||
Memory: &memory,
|
||||
Policy: &policy,
|
||||
}
|
||||
|
||||
if ids != "" {
|
||||
dIDs := strings.Split(ids, ";")
|
||||
device.CPUIDs = dIDs
|
||||
}
|
||||
|
||||
if hostNodes != "" {
|
||||
dHostNodes := strings.Split(hostNodes, ";")
|
||||
device.HostNodeNames = &dHostNodes
|
||||
}
|
||||
|
||||
if strings.HasPrefix(deviceName, "numa") {
|
||||
deviceID, err := strconv.Atoi(deviceName[4:])
|
||||
if err == nil {
|
||||
numaNodeObjects[deviceID] = device
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
numaNodeObjects[i] = device
|
||||
}
|
||||
|
||||
return numaNodeObjects
|
||||
}
|
||||
|
||||
func vmGetHostUSBDeviceObjects(d *schema.ResourceData) vms.CustomUSBDevices {
|
||||
usbDevice := d.Get(mkHostUSB).([]interface{})
|
||||
usbDeviceObjects := make(vms.CustomUSBDevices, len(usbDevice))
|
||||
@ -3492,6 +3616,44 @@ func vmReadCustom(
|
||||
cpu[mkCPUNUMA] = false
|
||||
}
|
||||
|
||||
currentNUMAList := d.Get(mkNUMA).([]interface{})
|
||||
numaMap := map[string]interface{}{}
|
||||
|
||||
numaDevices := getNUMAInfo(vmConfig, d)
|
||||
for ni, np := range numaDevices {
|
||||
if np == nil || np.CPUIDs == nil || np.HostNodeNames == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
numaNode := map[string]interface{}{}
|
||||
numaNode[mkNUMADevice] = ni
|
||||
|
||||
if len(np.CPUIDs) > 0 {
|
||||
numaNode[mkNUMACPUIDs] = strings.Join(np.CPUIDs, ";")
|
||||
}
|
||||
|
||||
numaNode[mkNUMAHostNodeNames] = strings.Join(*np.HostNodeNames, ";")
|
||||
numaNode[mkNUMAMemory] = np.Memory
|
||||
numaNode[mkNUMAPolicy] = np.Policy
|
||||
|
||||
numaMap[ni] = numaNode
|
||||
}
|
||||
|
||||
if len(clone) == 0 || len(currentNUMAList) > 0 {
|
||||
var numaList []interface{}
|
||||
|
||||
if len(currentNUMAList) > 0 {
|
||||
resMap := utils.MapResourceList(currentNUMAList, mkNUMADevice)
|
||||
devices := maps.Keys[map[string]interface{}](resMap)
|
||||
numaList = utils.OrderedListFromMapByKeyValues(numaMap, devices)
|
||||
} else {
|
||||
numaList = utils.OrderedListFromMap(numaMap)
|
||||
}
|
||||
|
||||
err := d.Set(mkNUMA, numaList)
|
||||
diags = append(diags, diag.FromErr(err)...)
|
||||
}
|
||||
|
||||
if vmConfig.CPUSockets != nil {
|
||||
cpu[mkCPUSockets] = *vmConfig.CPUSockets
|
||||
} else {
|
||||
@ -4971,6 +5133,17 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D
|
||||
rebootRequired = true
|
||||
}
|
||||
|
||||
// Prepare the new numa devices configuration.
|
||||
if d.HasChange(mkNUMA) {
|
||||
updateBody.NUMADevices = vmGetNumaDeviceObjects(d)
|
||||
|
||||
for i := len(updateBody.NUMADevices); i < maxResourceVirtualEnvironmentVMNUMADevices; i++ {
|
||||
del = append(del, fmt.Sprintf("numa%d", i))
|
||||
}
|
||||
|
||||
rebootRequired = true
|
||||
}
|
||||
|
||||
// Prepare the new usb devices configuration.
|
||||
if d.HasChange(mkHostUSB) {
|
||||
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
||||
@ -5477,6 +5650,21 @@ func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string
|
||||
return datastores
|
||||
}
|
||||
|
||||
func getNUMAInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomNUMADevice {
|
||||
numaDevices := map[string]*vms.CustomNUMADevice{}
|
||||
|
||||
numaDevices["numa0"] = resp.NUMADevices0
|
||||
numaDevices["numa1"] = resp.NUMADevices1
|
||||
numaDevices["numa2"] = resp.NUMADevices2
|
||||
numaDevices["numa3"] = resp.NUMADevices3
|
||||
numaDevices["numa4"] = resp.NUMADevices4
|
||||
numaDevices["numa5"] = resp.NUMADevices5
|
||||
numaDevices["numa6"] = resp.NUMADevices6
|
||||
numaDevices["numa7"] = resp.NUMADevices7
|
||||
|
||||
return numaDevices
|
||||
}
|
||||
|
||||
func getPCIInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomPCIDevice {
|
||||
pciDevices := map[string]*vms.CustomPCIDevice{}
|
||||
|
||||
|
@ -341,6 +341,21 @@ func TestVMSchema(t *testing.T) {
|
||||
mkMemoryShared: schema.TypeInt,
|
||||
})
|
||||
|
||||
numaSchema := test.AssertNestedSchemaExistence(t, s, mkNUMA)
|
||||
|
||||
test.AssertOptionalArguments(t, numaSchema, []string{
|
||||
mkNUMAHostNodeNames,
|
||||
mkNUMAPolicy,
|
||||
})
|
||||
|
||||
test.AssertValueTypes(t, numaSchema, map[string]schema.ValueType{
|
||||
mkNUMADevice: schema.TypeString,
|
||||
mkNUMACPUIDs: schema.TypeString,
|
||||
mkNUMAMemory: schema.TypeInt,
|
||||
mkNUMAHostNodeNames: schema.TypeString,
|
||||
mkNUMAPolicy: schema.TypeString,
|
||||
})
|
||||
|
||||
operatingSystemSchema := test.AssertNestedSchemaExistence(
|
||||
t,
|
||||
s,
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/utils"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
)
|
||||
|
||||
@ -86,7 +87,7 @@ func GetSchemaBlock(
|
||||
// recommended to use it only for small lists.
|
||||
// Ref: https://github.com/hashicorp/terraform-plugin-sdk/issues/477
|
||||
func SuppressIfListsAreEqualIgnoringOrder(key, _, _ string, d *schema.ResourceData) bool {
|
||||
// the key is a path to the list item, not the list itself, e.g. "tags.0"
|
||||
// the key is a path to the list item, not the list itself, e.g. "tags.#"
|
||||
lastDotIndex := strings.LastIndex(key, ".")
|
||||
if lastDotIndex != -1 {
|
||||
key = key[:lastDotIndex]
|
||||
@ -120,3 +121,52 @@ func SuppressIfListsAreEqualIgnoringOrder(key, _, _ string, d *schema.ResourceDa
|
||||
|
||||
return reflect.DeepEqual(oldEvents, newEvents)
|
||||
}
|
||||
|
||||
// SuppressIfListsOfMapsAreEqualIgnoringOrderByKey is a customdiff.SuppressionFunc that suppresses
|
||||
// changes to a list of resources if the old and new lists are equal, ignoring the order of the
|
||||
// elements.
|
||||
// It will be called for each resource attribute, so it is not super efficient. It is
|
||||
// recommended to use it only for small lists / small resources.
|
||||
// Note: The order of the attributes within the resource is still important.
|
||||
// Ref: https://github.com/hashicorp/terraform-plugin-sdk/issues/477
|
||||
func SuppressIfListsOfMapsAreEqualIgnoringOrderByKey(keyAttr string) schema.SchemaDiffSuppressFunc {
|
||||
// the attr is a path to the item's attribute, not the list itself, e.g. "numa.0.device"
|
||||
return func(attr, _, _ string, d *schema.ResourceData) bool {
|
||||
lastDotIndex := strings.LastIndex(attr, ".")
|
||||
if lastDotIndex != -1 {
|
||||
attr = attr[:lastDotIndex]
|
||||
}
|
||||
|
||||
lastDotIndex = strings.LastIndex(attr, ".")
|
||||
if lastDotIndex != -1 {
|
||||
attr = attr[:lastDotIndex]
|
||||
}
|
||||
|
||||
oldData, newData := d.GetChange(attr)
|
||||
if oldData == nil || newData == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
oldArray := oldData.([]interface{})
|
||||
newArray := newData.([]interface{})
|
||||
|
||||
if len(oldArray) != len(newArray) {
|
||||
return false
|
||||
}
|
||||
|
||||
oldKeys := utils.MapResourceList(oldArray, keyAttr)
|
||||
newKeys := utils.MapResourceList(newArray, keyAttr)
|
||||
|
||||
for k, v := range oldKeys {
|
||||
if _, ok := newKeys[k]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(v, newKeys[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
2
testacc
2
testacc
@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
TF_ACC=1 env $(cat testacc.env | xargs) go test -v -timeout 120s -run "$1" github.com/bpg/terraform-provider-proxmox/fwprovider/tests $2
|
||||
TF_ACC=1 env $(cat testacc.env | xargs) go test -v -timeout 360s -run "$1" github.com/bpg/terraform-provider-proxmox/fwprovider/tests $2
|
||||
|
@ -22,3 +22,35 @@ func OrderedListFromMap(inputMap map[string]interface{}) []interface{} {
|
||||
|
||||
return orderedList
|
||||
}
|
||||
|
||||
// MapResourceList generates a list of strings from a Terraform resource list (list of maps).
|
||||
// The list is generated from the value of the specified attribute.
|
||||
//
|
||||
// "Map" in this context is a functional programming term, not a Go map.
|
||||
// "Resource" in this context is a Terraform resource, i.e. a map of attributes.
|
||||
func MapResourceList(resourceList []interface{}, attrName string) map[string]interface{} {
|
||||
m := make(map[string]interface{}, len(resourceList))
|
||||
|
||||
for _, resource := range resourceList {
|
||||
r := resource.(map[string]interface{})
|
||||
key := r[attrName].(string)
|
||||
m[key] = r
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// OrderedListFromMapByKeyValues generates a list from a map's values.
|
||||
// The values are sorted based on the provided key list. If a key is not found in the map, it is skipped.
|
||||
func OrderedListFromMapByKeyValues(inputMap map[string]interface{}, keyList []string) []interface{} {
|
||||
orderedList := make([]interface{}, len(keyList))
|
||||
|
||||
for i, k := range keyList {
|
||||
val, ok := inputMap[k]
|
||||
if ok {
|
||||
orderedList[i] = val
|
||||
}
|
||||
}
|
||||
|
||||
return orderedList
|
||||
}
|
||||
|
49
utils/maps_test.go
Normal file
49
utils/maps_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapResourceList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
resourceList := []interface{}{
|
||||
map[string]interface{}{"name": "resource1", "attr": "value1"},
|
||||
map[string]interface{}{"name": "resource2", "attr": "value2"},
|
||||
map[string]interface{}{"name": "resource3", "attr": "value3"},
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"value1": map[string]interface{}{"name": "resource1", "attr": "value1"},
|
||||
"value2": map[string]interface{}{"name": "resource2", "attr": "value2"},
|
||||
"value3": map[string]interface{}{"name": "resource3", "attr": "value3"},
|
||||
}
|
||||
|
||||
result := MapResourceList(resourceList, "attr")
|
||||
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("MapResourceList() = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedListFromMapByKeyValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
inputMap := map[string]interface{}{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
"key4": "value4",
|
||||
}
|
||||
|
||||
keyList := []string{"key2", "key1", "key4"}
|
||||
|
||||
expected := []interface{}{"value2", "value1", "value4"}
|
||||
|
||||
result := OrderedListFromMapByKeyValues(inputMap, keyList)
|
||||
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("OrderedListFromMapByKeyValues() = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user