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",
|
"cmode",
|
||||||
"cpulimit",
|
"cpulimit",
|
||||||
"CPUNUMA",
|
"CPUNUMA",
|
||||||
|
"cpus",
|
||||||
"cputype",
|
"cputype",
|
||||||
"cpuunits",
|
"cpuunits",
|
||||||
"customdiff",
|
"customdiff",
|
||||||
@ -33,8 +34,10 @@
|
|||||||
"gocritic",
|
"gocritic",
|
||||||
"gosimple",
|
"gosimple",
|
||||||
"hookscript",
|
"hookscript",
|
||||||
|
"hostnodes",
|
||||||
"hostpci",
|
"hostpci",
|
||||||
"Hotplugged",
|
"Hotplugged",
|
||||||
|
"Hugepages",
|
||||||
"iface",
|
"iface",
|
||||||
"importdisk",
|
"importdisk",
|
||||||
"iothread",
|
"iothread",
|
||||||
|
@ -417,6 +417,17 @@ output "ubuntu_vm_public_key" {
|
|||||||
|
|
||||||
Settings `hugepages` and `keep_hugepages` are only allowed for `root@pam` authenticated user.
|
Settings `hugepages` and `keep_hugepages` are only allowed for `root@pam` authenticated user.
|
||||||
And required `cpu.numa` to be enabled.
|
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
|
- `migrate` - (Optional) Migrate the VM on node change instead of re-creating
|
||||||
it (defaults to `false`).
|
it (defaults to `false`).
|
||||||
- `name` - (Optional) The virtual machine name.
|
- `name` - (Optional) The virtual machine name.
|
||||||
|
@ -131,6 +131,12 @@ resource "proxmox_virtual_environment_vm" "example" {
|
|||||||
# hugepages = "2"
|
# hugepages = "2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# numa {
|
||||||
|
# device = "numa0"
|
||||||
|
# cpus = "0-1"
|
||||||
|
# memory = 768
|
||||||
|
# }
|
||||||
|
|
||||||
connection {
|
connection {
|
||||||
type = "ssh"
|
type = "ssh"
|
||||||
agent = false
|
agent = false
|
||||||
|
@ -109,7 +109,7 @@ type CustomNetworkDevices []CustomNetworkDevice
|
|||||||
type CustomNUMADevice struct {
|
type CustomNUMADevice struct {
|
||||||
CPUIDs []string `json:"cpus" url:"cpus,semicolon"`
|
CPUIDs []string `json:"cpus" url:"cpus,semicolon"`
|
||||||
HostNodeNames *[]string `json:"hostnodes,omitempty" url:"hostnodes,omitempty,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"`
|
Policy *string `json:"policy,omitempty" url:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,8 +455,15 @@ type GetResponseData struct {
|
|||||||
NetworkDevice29 *CustomNetworkDevice `json:"net29,omitempty"`
|
NetworkDevice29 *CustomNetworkDevice `json:"net29,omitempty"`
|
||||||
NetworkDevice30 *CustomNetworkDevice `json:"net30,omitempty"`
|
NetworkDevice30 *CustomNetworkDevice `json:"net30,omitempty"`
|
||||||
NetworkDevice31 *CustomNetworkDevice `json:"net31,omitempty"`
|
NetworkDevice31 *CustomNetworkDevice `json:"net31,omitempty"`
|
||||||
NUMADevices *CustomNUMADevices `json:"numa_devices,omitempty"`
|
|
||||||
NUMAEnabled *types.CustomBool `json:"numa,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"`
|
OSType *string `json:"ostype,omitempty"`
|
||||||
Overwrite *types.CustomBool `json:"force,omitempty"`
|
Overwrite *types.CustomBool `json:"force,omitempty"`
|
||||||
PCIDevice0 *CustomPCIDevice `json:"hostpci0,omitempty"`
|
PCIDevice0 *CustomPCIDevice `json:"hostpci0,omitempty"`
|
||||||
@ -939,7 +946,7 @@ func (r CustomNUMADevice) EncodeValues(key string, v *url.Values) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Memory != nil {
|
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 {
|
if r.Policy != nil {
|
||||||
@ -1590,6 +1597,41 @@ func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
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.
|
// UnmarshalJSON converts a CustomPCIDevice string to an object.
|
||||||
func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
|
func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
|
||||||
var s string
|
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) {
|
func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ func CPUTypeValidator() schema.SchemaValidateDiagFunc {
|
|||||||
// CPUAffinityValidator returns a schema validation function for a CPU affinity.
|
// CPUAffinityValidator returns a schema validation function for a CPU affinity.
|
||||||
func CPUAffinityValidator() schema.SchemaValidateDiagFunc {
|
func CPUAffinityValidator() schema.SchemaValidateDiagFunc {
|
||||||
return validation.ToDiagFunc(
|
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
|
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"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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/disk"
|
||||||
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/network"
|
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/network"
|
||||||
"github.com/bpg/terraform-provider-proxmox/utils"
|
"github.com/bpg/terraform-provider-proxmox/utils"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -133,6 +135,8 @@ const (
|
|||||||
maxResourceVirtualEnvironmentVMSerialDevices = 4
|
maxResourceVirtualEnvironmentVMSerialDevices = 4
|
||||||
maxResourceVirtualEnvironmentVMHostPCIDevices = 8
|
maxResourceVirtualEnvironmentVMHostPCIDevices = 8
|
||||||
maxResourceVirtualEnvironmentVMHostUSBDevices = 4
|
maxResourceVirtualEnvironmentVMHostUSBDevices = 4
|
||||||
|
// hardcoded /usr/share/perl5/PVE/QemuServer/Memory.pm: "our $MAX_NUMA = 8".
|
||||||
|
maxResourceVirtualEnvironmentVMNUMADevices = 8
|
||||||
|
|
||||||
mkRebootAfterCreation = "reboot"
|
mkRebootAfterCreation = "reboot"
|
||||||
mkOnBoot = "on_boot"
|
mkOnBoot = "on_boot"
|
||||||
@ -171,6 +175,13 @@ const (
|
|||||||
mkCPUAffinity = "affinity"
|
mkCPUAffinity = "affinity"
|
||||||
mkDescription = "description"
|
mkDescription = "description"
|
||||||
|
|
||||||
|
mkNUMA = "numa"
|
||||||
|
mkNUMADevice = "device"
|
||||||
|
mkNUMACPUIDs = "cpus"
|
||||||
|
mkNUMAHostNodeNames = "hostnodes"
|
||||||
|
mkNUMAMemory = "memory"
|
||||||
|
mkNUMAPolicy = "policy"
|
||||||
|
|
||||||
mkEFIDisk = "efi_disk"
|
mkEFIDisk = "efi_disk"
|
||||||
mkEFIDiskDatastoreID = "datastore_id"
|
mkEFIDiskDatastoreID = "datastore_id"
|
||||||
mkEFIDiskFileFormat = "file_format"
|
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",
|
Description: "Hugepages will not be deleted after VM shutdown and can be used for subsequent starts",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Default: dvMemoryKeepHugepages,
|
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",
|
Description: "The node name",
|
||||||
Required: true,
|
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: {
|
mkMigrate: {
|
||||||
Type: schema.TypeBool,
|
Type: schema.TypeBool,
|
||||||
Description: "Whether to migrate the VM on node change instead of re-creating it",
|
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{})
|
hostUSB := d.Get(mkHostUSB).([]interface{})
|
||||||
keyboardLayout := d.Get(mkKeyboardLayout).(string)
|
keyboardLayout := d.Get(mkKeyboardLayout).(string)
|
||||||
memory := d.Get(mkMemory).([]interface{})
|
memory := d.Get(mkMemory).([]interface{})
|
||||||
|
numa := d.Get(mkNUMA).([]interface{})
|
||||||
operatingSystem := d.Get(mkOperatingSystem).([]interface{})
|
operatingSystem := d.Get(mkOperatingSystem).([]interface{})
|
||||||
serialDevice := d.Get(mkSerialDevice).([]interface{})
|
serialDevice := d.Get(mkSerialDevice).([]interface{})
|
||||||
onBoot := types.CustomBool(d.Get(mkOnBoot).(bool))
|
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)
|
updateBody.PCIDevices = vmGetHostPCIDeviceObjects(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(numa) > 0 {
|
||||||
|
updateBody.NUMADevices = vmGetNumaDeviceObjects(d)
|
||||||
|
}
|
||||||
|
|
||||||
if len(hostUSB) > 0 {
|
if len(hostUSB) > 0 {
|
||||||
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
||||||
}
|
}
|
||||||
@ -2389,6 +2467,8 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
|
|||||||
|
|
||||||
pciDeviceObjects := vmGetHostPCIDeviceObjects(d)
|
pciDeviceObjects := vmGetHostPCIDeviceObjects(d)
|
||||||
|
|
||||||
|
numaDeviceObjects := vmGetNumaDeviceObjects(d)
|
||||||
|
|
||||||
usbDeviceObjects := vmGetHostUSBDeviceObjects(d)
|
usbDeviceObjects := vmGetHostUSBDeviceObjects(d)
|
||||||
|
|
||||||
keyboardLayout := d.Get(mkKeyboardLayout).(string)
|
keyboardLayout := d.Get(mkKeyboardLayout).(string)
|
||||||
@ -2559,6 +2639,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
|
|||||||
KeyboardLayout: &keyboardLayout,
|
KeyboardLayout: &keyboardLayout,
|
||||||
NetworkDevices: networkDeviceObjects,
|
NetworkDevices: networkDeviceObjects,
|
||||||
NUMAEnabled: &cpuNUMA,
|
NUMAEnabled: &cpuNUMA,
|
||||||
|
NUMADevices: numaDeviceObjects,
|
||||||
OSType: &operatingSystemType,
|
OSType: &operatingSystemType,
|
||||||
PCIDevices: pciDeviceObjects,
|
PCIDevices: pciDeviceObjects,
|
||||||
SCSIHardware: &scsiHardware,
|
SCSIHardware: &scsiHardware,
|
||||||
@ -3029,6 +3110,49 @@ func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices {
|
|||||||
return pciDeviceObjects
|
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 {
|
func vmGetHostUSBDeviceObjects(d *schema.ResourceData) vms.CustomUSBDevices {
|
||||||
usbDevice := d.Get(mkHostUSB).([]interface{})
|
usbDevice := d.Get(mkHostUSB).([]interface{})
|
||||||
usbDeviceObjects := make(vms.CustomUSBDevices, len(usbDevice))
|
usbDeviceObjects := make(vms.CustomUSBDevices, len(usbDevice))
|
||||||
@ -3492,6 +3616,44 @@ func vmReadCustom(
|
|||||||
cpu[mkCPUNUMA] = false
|
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 {
|
if vmConfig.CPUSockets != nil {
|
||||||
cpu[mkCPUSockets] = *vmConfig.CPUSockets
|
cpu[mkCPUSockets] = *vmConfig.CPUSockets
|
||||||
} else {
|
} else {
|
||||||
@ -4971,6 +5133,17 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D
|
|||||||
rebootRequired = true
|
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.
|
// Prepare the new usb devices configuration.
|
||||||
if d.HasChange(mkHostUSB) {
|
if d.HasChange(mkHostUSB) {
|
||||||
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
||||||
@ -5477,6 +5650,21 @@ func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string
|
|||||||
return datastores
|
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 {
|
func getPCIInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomPCIDevice {
|
||||||
pciDevices := map[string]*vms.CustomPCIDevice{}
|
pciDevices := map[string]*vms.CustomPCIDevice{}
|
||||||
|
|
||||||
|
@ -341,6 +341,21 @@ func TestVMSchema(t *testing.T) {
|
|||||||
mkMemoryShared: schema.TypeInt,
|
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(
|
operatingSystemSchema := test.AssertNestedSchemaExistence(
|
||||||
t,
|
t,
|
||||||
s,
|
s,
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bpg/terraform-provider-proxmox/utils"
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,7 +87,7 @@ func GetSchemaBlock(
|
|||||||
// recommended to use it only for small lists.
|
// recommended to use it only for small lists.
|
||||||
// Ref: https://github.com/hashicorp/terraform-plugin-sdk/issues/477
|
// Ref: https://github.com/hashicorp/terraform-plugin-sdk/issues/477
|
||||||
func SuppressIfListsAreEqualIgnoringOrder(key, _, _ string, d *schema.ResourceData) bool {
|
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, ".")
|
lastDotIndex := strings.LastIndex(key, ".")
|
||||||
if lastDotIndex != -1 {
|
if lastDotIndex != -1 {
|
||||||
key = key[:lastDotIndex]
|
key = key[:lastDotIndex]
|
||||||
@ -120,3 +121,52 @@ func SuppressIfListsAreEqualIgnoringOrder(key, _, _ string, d *schema.ResourceDa
|
|||||||
|
|
||||||
return reflect.DeepEqual(oldEvents, newEvents)
|
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
|
#!/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
|
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