diff --git a/docs/resources/virtual_environment_vm.md b/docs/resources/virtual_environment_vm.md index 65a3c811..3b6fecfc 100644 --- a/docs/resources/virtual_environment_vm.md +++ b/docs/resources/virtual_environment_vm.md @@ -165,6 +165,7 @@ output "ubuntu_vm_public_key" { protection for AMD models. - `hotplugged` - (Optional) The number of hotplugged vCPUs (defaults to `0`). + - `numa` - (Boolean) Enable/disable NUMA. (default to `false`) - `sockets` - (Optional) The number of CPU sockets (defaults to `1`). - `type` - (Optional) The emulated CPU type (defaults to `qemu64`). - `486` - Intel 486. @@ -241,6 +242,19 @@ output "ubuntu_vm_public_key" { - `ssd` - (Optional) Whether to use an SSD emulation option for this disk ( defaults to `false`). Note that SSD emulation is not supported on VirtIO Block drives. +- `efi_disk` - (Optional) The efi disk device (required if `bios` is set + to `ovmf`) + - `datastore_id` (Optional) The identifier for the datastore to create + the disk in (defaults to `local-lvm`). + - `file_format` (Optional) The file format. + - `type` (Optional) Size and type of the OVMF EFI disk. `4m` is newer and + recommended, and required for Secure Boot. For backwards compatibility + use `2m`. Ignored for VMs with cpu.architecture=`aarch64` (defaults + to `2m`). + - `pre_enrolled_keys` (Optional) Use am EFI vars template with + distribution-specific and Microsoft Standard keys enrolled, if used with + EFI type=`4m`. Ignored for VMs with cpu.architecture=`aarch64` (defaults + to `false`). - `hostpci` - (Optional) A host PCI device mapping (multiple blocks supported). - `device` - (Required) The PCI device name for Proxmox, in form of `hostpciX` where `X` is a sequential number from 0 to 3. @@ -284,7 +298,7 @@ output "ubuntu_vm_public_key" { - `user_data_file_id` - (Optional) The identifier for a file containing custom user data (conflicts with `user_account`). - `vendor_data_file_id` - (Optional) The identifier for a file containing - all vendor data passed to the VM via cloud-init. + all vendor data passed to the VM via cloud-init. - `meta_data_file_id` - (Optional) The identifier for a file containing all meta data passed to the VM via cloud-init. - `keyboard_layout` - (Optional) The keyboard layout (defaults to `en-us`). diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf index e0fd4722..de25ad7d 100644 --- a/example/resource_virtual_environment_vm.tf +++ b/example/resource_virtual_environment_vm.tf @@ -9,6 +9,16 @@ resource "proxmox_virtual_environment_vm" "example_template" { description = "Managed by Terraform" + cpu { + numa = true + } + + efi_disk { + datastore_id = local.datastore_id + file_format = "raw" + type = "4m" + } + # disk { # datastore_id = local.datastore_id # file_id = proxmox_virtual_environment_file.ubuntu_cloud_image.id diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go index 65cb509c..16625621 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -82,9 +82,10 @@ type CustomCPUEmulation struct { // CustomEFIDisk handles QEMU EFI disk parameters. type CustomEFIDisk struct { - Size *types.DiskSize `json:"size,omitempty" url:"size,omitempty"` - FileVolume string `json:"file" url:"file"` - Format *string `json:"format,omitempty" url:"format,omitempty"` + FileVolume string `json:"file" url:"file"` + Format *string `json:"format,omitempty" url:"format,omitempty"` + Type *string `json:"efitype,omitempty" url:"efitype,omitempty"` + PreEnrolledKeys *types2.CustomBool `json:"pre-enrolled-keys,omitempty" url:"pre-enrolled-keys,omitempty,int"` } // CustomNetworkDevice handles QEMU network device parameters. @@ -784,8 +785,16 @@ func (r CustomEFIDisk) EncodeValues(key string, v *url.Values) error { values = append(values, fmt.Sprintf("format=%s", *r.Format)) } - if r.Size != nil { - values = append(values, fmt.Sprintf("size=%s", *r.Size)) + if r.Type != nil { + values = append(values, fmt.Sprintf("efitype=%s", *r.Type)) + } + + if r.PreEnrolledKeys != nil { + if *r.PreEnrolledKeys { + values = append(values, "pre-enrolled-keys=1") + } else { + values = append(values, "pre-enrolled-keys=0") + } } v.Add(key, strings.Join(values, ",")) @@ -1472,22 +1481,25 @@ func (r *CustomEFIDisk) UnmarshalJSON(b []byte) error { pairs := strings.Split(s, ",") - for _, p := range pairs { + for i, p := range pairs { v := strings.Split(strings.TrimSpace(p), "=") + if len(v) == 1 && i == 0 { + r.FileVolume = v[0] + } + if len(v) == 2 { switch v[0] { - case "format": - r.Format = &v[1] case "file": r.FileVolume = v[1] - case "size": - r.Size = new(types.DiskSize) - - err := r.Size.UnmarshalJSON([]byte(v[1])) - if err != nil { - return fmt.Errorf("failed to unmarshal disk size: %w", err) - } + case "format": + r.Format = &v[1] + case "efitype": + t := strings.ToLower(v[1]) + r.Type = &t + case "pre-enrolled-keys": + bv := types2.CustomBool(v[1] == "1") + r.PreEnrolledKeys = &bv } } } diff --git a/proxmox/types/disk_size.go b/proxmox/types/disk_size.go index 0bd54865..d65eb3e3 100644 --- a/proxmox/types/disk_size.go +++ b/proxmox/types/disk_size.go @@ -24,7 +24,12 @@ type DiskSize int64 // String returns the string representation of the disk size. func (r DiskSize) String() string { - return formatDiskSize(int64(r)) + return FormatDiskSize(r) +} + +// InMegabytes returns the disk size in megabytes. +func (r DiskSize) InMegabytes() int { + return int(int64(r) / 1024 / 1024) } // InGigabytes returns the disk size in gigabytes. @@ -39,7 +44,7 @@ func DiskSizeFromGigabytes(size int) DiskSize { // MarshalJSON marshals a disk size into a Proxmox API `` string. func (r DiskSize) MarshalJSON() ([]byte, error) { - bytes, err := json.Marshal(formatDiskSize(int64(r))) + bytes, err := json.Marshal(FormatDiskSize(r)) if err != nil { return nil, fmt.Errorf("cannot marshal disk size: %w", err) } @@ -51,27 +56,23 @@ func (r DiskSize) MarshalJSON() ([]byte, error) { func (r *DiskSize) UnmarshalJSON(b []byte) error { s := string(b) - size, err := parseDiskSize(&s) + size, err := ParseDiskSize(s) if err != nil { return err } - *r = DiskSize(size) + *r = size return nil } -// parseDiskSize parses a disk size string into a number of bytes. -func parseDiskSize(size *string) (int64, error) { - if size == nil { - return 0, nil - } - - matches := sizeRegex.FindStringSubmatch(*size) +// ParseDiskSize parses a disk size string into a number of bytes. +func ParseDiskSize(size string) (DiskSize, error) { + matches := sizeRegex.FindStringSubmatch(size) if len(matches) > 0 { fsize, err := strconv.ParseFloat(matches[1], 64) if err != nil { - return -1, fmt.Errorf("cannot parse disk size \"%s\": %w", *size, err) + return -1, fmt.Errorf("cannot parse disk size \"%s\": %w", size, err) } switch strings.ToLower(matches[3]) { @@ -85,13 +86,14 @@ func parseDiskSize(size *string) (int64, error) { fsize = fsize * 1024 * 1024 * 1024 * 1024 } - return int64(math.Ceil(fsize)), nil + return DiskSize(math.Ceil(fsize)), nil } - return -1, fmt.Errorf("cannot parse disk size \"%s\"", *size) + return -1, fmt.Errorf("cannot parse disk size \"%s\"", size) } -func formatDiskSize(size int64) string { +// FormatDiskSize turns a number of bytes into a disk size string. +func FormatDiskSize(size DiskSize) string { if size < 0 { return "" } diff --git a/proxmox/types/disk_size_test.go b/proxmox/types/disk_size_test.go index eb7b7dce..243417ff 100644 --- a/proxmox/types/disk_size_test.go +++ b/proxmox/types/disk_size_test.go @@ -17,37 +17,36 @@ func TestParseDiskSize(t *testing.T) { tests := []struct { name string - size *string + size string want int64 wantErr bool }{ - {"handle null size", nil, 0, false}, - {"parse TB", StrPtr("2TB"), 2199023255552, false}, - {"parse T", StrPtr("2T"), 2199023255552, false}, - {"parse fraction T", StrPtr("2.2T"), 2418925581108, false}, - {"parse GB", StrPtr("2GB"), 2147483648, false}, - {"parse G", StrPtr("2G"), 2147483648, false}, - {"parse M", StrPtr("2048M"), 2147483648, false}, - {"parse MB", StrPtr("2048MB"), 2147483648, false}, - {"parse MiB", StrPtr("2048MiB"), 2147483648, false}, - {"parse K", StrPtr("1K"), 1024, false}, - {"parse KB", StrPtr("2KB"), 2048, false}, - {"parse KiB", StrPtr("4KiB"), 4096, false}, - {"parse no units as bytes", StrPtr("12345"), 12345, false}, - {"error on bad format string", StrPtr("20l8G"), -1, true}, - {"error on unknown unit string", StrPtr("2048W"), -1, true}, - {"error on arbitrary string", StrPtr("something"), -1, true}, + {"parse TB", "2TB", 2199023255552, false}, + {"parse T", "2T", 2199023255552, false}, + {"parse fraction T", "2.2T", 2418925581108, false}, + {"parse GB", "2GB", 2147483648, false}, + {"parse G", "2G", 2147483648, false}, + {"parse M", "2048M", 2147483648, false}, + {"parse MB", "2048MB", 2147483648, false}, + {"parse MiB", "2048MiB", 2147483648, false}, + {"parse K", "1K", 1024, false}, + {"parse KB", "2KB", 2048, false}, + {"parse KiB", "4KiB", 4096, false}, + {"parse no units as bytes", "12345", 12345, false}, + {"error on bad format string", "20l8G", -1, true}, + {"error on unknown unit string", "2048W", -1, true}, + {"error on arbitrary string", "something", -1, true}, } for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := parseDiskSize(tt.size) + got, err := ParseDiskSize(tt.size) if (err != nil) != tt.wantErr { t.Errorf("parseDiskSize() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { + if int64(got) != tt.want { t.Errorf("parseDiskSize() got = %v, want %v", got, tt.want) } }) @@ -73,7 +72,7 @@ func TestFormatDiskSize(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { t.Parallel() - if got := formatDiskSize(tt.size); got != tt.want { + if got := FormatDiskSize(DiskSize(tt.size)); got != tt.want { t.Errorf("formatDiskSize() = %v, want %v", got, tt.want) } }) diff --git a/proxmoxtf/resource/utils.go b/proxmoxtf/resource/utils.go index b65221b2..5f3c4e6c 100644 --- a/proxmoxtf/resource/utils.go +++ b/proxmoxtf/resource/utils.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) func getBIOSValidator() schema.SchemaValidateDiagFunc { @@ -186,6 +187,29 @@ func getFileIDValidator() schema.SchemaValidateDiagFunc { }) } +//nolint:unused +func getFileSizeValidator() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(func(i interface{}, k string) ([]string, []error) { + v, ok := i.(string) + var es []error + + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return nil, es + } + + if v != "" { + _, err := types.ParseDiskSize(v) + if err != nil { + es = append(es, fmt.Errorf("expected %s to be a valid file size (100, 1M, 1G), got %s", k, v)) + return nil, es + } + } + + return []string{}, es + }) +} + func getKeyboardLayoutValidator() schema.SchemaValidateDiagFunc { return validation.ToDiagFunc(validation.StringInSlice([]string{ "da", @@ -506,6 +530,11 @@ func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string datastoresSet[fileIDParts[0]] = 1 } + if vm.EFIDisk != nil { + fileIDParts := strings.Split(vm.EFIDisk.FileVolume, ":") + datastoresSet[fileIDParts[0]] = 1 + } + datastores := []string{} for datastore := range datastoresSet { datastores = append(datastores, datastore) diff --git a/proxmoxtf/resource/vm.go b/proxmoxtf/resource/vm.go index 037664b6..84439039 100644 --- a/proxmoxtf/resource/vm.go +++ b/proxmoxtf/resource/vm.go @@ -47,6 +47,7 @@ const ( dvResourceVirtualEnvironmentVMCPUArchitecture = "x86_64" dvResourceVirtualEnvironmentVMCPUCores = 1 dvResourceVirtualEnvironmentVMCPUHotplugged = 0 + dvResourceVirtualEnvironmentVMCPUNUMA = false dvResourceVirtualEnvironmentVMCPUSockets = 1 dvResourceVirtualEnvironmentVMCPUType = "qemu64" dvResourceVirtualEnvironmentVMCPUUnits = 1024 @@ -63,6 +64,10 @@ const ( dvResourceVirtualEnvironmentVMDiskSpeedReadBurstable = 0 dvResourceVirtualEnvironmentVMDiskSpeedWrite = 0 dvResourceVirtualEnvironmentVMDiskSpeedWriteBurstable = 0 + dvResourceVirtualEnvironmentVMEFIDiskDatastoreID = "local-lvm" + dvResourceVirtualEnvironmentVMEFIDiskFileFormat = "qcow2" + dvResourceVirtualEnvironmentVMEFIDiskType = "2m" + dvResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys = false dvResourceVirtualEnvironmentVMInitializationDatastoreID = "local-lvm" dvResourceVirtualEnvironmentVMInitializationDNSDomain = "" dvResourceVirtualEnvironmentVMInitializationDNSServer = "" @@ -139,6 +144,7 @@ const ( mkResourceVirtualEnvironmentVMCPUCores = "cores" mkResourceVirtualEnvironmentVMCPUFlags = "flags" mkResourceVirtualEnvironmentVMCPUHotplugged = "hotplugged" + mkResourceVirtualEnvironmentVMCPUNUMA = "numa" mkResourceVirtualEnvironmentVMCPUSockets = "sockets" mkResourceVirtualEnvironmentVMCPUType = "type" mkResourceVirtualEnvironmentVMCPUUnits = "units" @@ -157,6 +163,11 @@ const ( mkResourceVirtualEnvironmentVMDiskSpeedReadBurstable = "read_burstable" mkResourceVirtualEnvironmentVMDiskSpeedWrite = "write" mkResourceVirtualEnvironmentVMDiskSpeedWriteBurstable = "write_burstable" + mkResourceVirtualEnvironmentVMEFIDisk = "efi_disk" + mkResourceVirtualEnvironmentVMEFIDiskDatastoreID = "datastore_id" + mkResourceVirtualEnvironmentVMEFIDiskFileFormat = "file_format" + mkResourceVirtualEnvironmentVMEFIDiskType = "type" + mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys = "pre_enrolled_keys" mkResourceVirtualEnvironmentVMHostPCI = "hostpci" mkResourceVirtualEnvironmentVMHostPCIDevice = "device" mkResourceVirtualEnvironmentVMHostPCIDeviceID = "id" @@ -449,6 +460,7 @@ func VM() *schema.Resource { mkResourceVirtualEnvironmentVMCPUArchitecture: dvResourceVirtualEnvironmentVMCPUArchitecture, mkResourceVirtualEnvironmentVMCPUCores: dvResourceVirtualEnvironmentVMCPUCores, mkResourceVirtualEnvironmentVMCPUFlags: []interface{}{}, + mkResourceVirtualEnvironmentVMCPUNUMA: dvResourceVirtualEnvironmentVMCPUNUMA, mkResourceVirtualEnvironmentVMCPUHotplugged: dvResourceVirtualEnvironmentVMCPUHotplugged, mkResourceVirtualEnvironmentVMCPUSockets: dvResourceVirtualEnvironmentVMCPUSockets, mkResourceVirtualEnvironmentVMCPUType: dvResourceVirtualEnvironmentVMCPUType, @@ -488,6 +500,12 @@ func VM() *schema.Resource { Default: dvResourceVirtualEnvironmentVMCPUHotplugged, ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 2304)), }, + mkResourceVirtualEnvironmentVMCPUNUMA: { + Type: schema.TypeBool, + Description: "Enable/disable NUMA.", + Optional: true, + Default: dvResourceVirtualEnvironmentVMCPUNUMA, + }, mkResourceVirtualEnvironmentVMCPUSockets: { Type: schema.TypeInt, Description: "The number of CPU sockets", @@ -644,6 +662,61 @@ func VM() *schema.Resource { MaxItems: 14, MinItems: 0, }, + mkResourceVirtualEnvironmentVMEFIDisk: { + Type: schema.TypeList, + Description: "The efidisk device", + Optional: true, + ForceNew: true, + DefaultFunc: func() (interface{}, error) { + return []interface{}{ + map[string]interface{}{ + mkResourceVirtualEnvironmentVMEFIDiskDatastoreID: dvResourceVirtualEnvironmentVMEFIDiskDatastoreID, + mkResourceVirtualEnvironmentVMEFIDiskType: dvResourceVirtualEnvironmentVMEFIDiskType, + mkResourceVirtualEnvironmentVMEFIDiskFileFormat: dvResourceVirtualEnvironmentVMEFIDiskFileFormat, + mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys: dvResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys, + }, + }, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMEFIDiskDatastoreID: { + Type: schema.TypeString, + Description: "The datastore id", + Optional: true, + Default: dvResourceVirtualEnvironmentVMEFIDiskDatastoreID, + }, + mkResourceVirtualEnvironmentVMEFIDiskFileFormat: { + Type: schema.TypeString, + Description: "The file format", + Optional: true, + ForceNew: true, + Computed: true, + ValidateDiagFunc: getFileFormatValidator(), + }, + mkResourceVirtualEnvironmentVMEFIDiskType: { + Type: schema.TypeString, + Description: "Size and type of the OVMF EFI disk", + Optional: true, + ForceNew: true, + Default: dvResourceVirtualEnvironmentVMEFIDiskType, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "2m", + "4m", + }, true)), + }, + mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys: { + Type: schema.TypeBool, + Description: "Use an EFI vars template with distribution-specific and Microsoft Standard " + + "keys enrolled, if used with efi type=`4m`.", + Optional: true, + ForceNew: true, + Default: dvResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys, + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, mkResourceVirtualEnvironmentVMInitialization: { Type: schema.TypeList, Description: "The cloud-init configuration", @@ -1544,6 +1617,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int) cpuFlags := cpuBlock[mkResourceVirtualEnvironmentVMCPUFlags].([]interface{}) cpuHotplugged := cpuBlock[mkResourceVirtualEnvironmentVMCPUHotplugged].(int) + cpuNUMA := types2.CustomBool(cpuBlock[mkResourceVirtualEnvironmentVMCPUNUMA].(bool)) cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) cpuType := cpuBlock[mkResourceVirtualEnvironmentVMCPUType].(string) cpuUnits := cpuBlock[mkResourceVirtualEnvironmentVMCPUUnits].(int) @@ -1565,6 +1639,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d Flags: &cpuFlagsConverted, Type: cpuType, } + updateBody.NUMAEnabled = &cpuNUMA updateBody.CPUSockets = &cpuSockets updateBody.CPUUnits = &cpuUnits @@ -1694,6 +1769,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } disk := d.Get(mkResourceVirtualEnvironmentVMDisk).([]interface{}) + efiDisk := d.Get(mkResourceVirtualEnvironmentVMEFIDisk).([]interface{}) vmConfig, e := vmAPI.GetVM(ctx) if e != nil { @@ -1805,6 +1881,65 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } } + efiDiskInfo := vmGetEfiDisk(d, nil) // from the resource config + + for i := range efiDisk { + diskBlock := efiDisk[i].(map[string]interface{}) + diskInterface := "efidisk0" + dataStoreID := diskBlock[mkResourceVirtualEnvironmentVMEFIDiskDatastoreID].(string) + efiType := diskBlock[mkResourceVirtualEnvironmentVMEFIDiskType].(string) + + currentDiskInfo := vmConfig.EFIDisk + configuredDiskInfo := efiDiskInfo + + if currentDiskInfo == nil { + diskUpdateBody := &vms.UpdateRequestBody{} + + diskUpdateBody.EFIDisk = configuredDiskInfo + + e = vmAPI.UpdateVM(ctx, diskUpdateBody) + if e != nil { + return diag.FromErr(e) + } + + continue + } + + if &efiType != currentDiskInfo.Type { + return diag.Errorf( + "resizing of efidisks is not supported.", + ) + } + + deleteOriginalDisk := types2.CustomBool(true) + + diskMoveBody := &vms.MoveDiskRequestBody{ + DeleteOriginalDisk: &deleteOriginalDisk, + Disk: diskInterface, + TargetStorage: dataStoreID, + } + + moveDisk := false + + if dataStoreID != "" { + moveDisk = true + + if allDiskInfo[diskInterface] != nil { + fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":") + moveDisk = dataStoreID != fileIDParts[0] + } + } + + if moveDisk { + moveDiskTimeout := d.Get(mkResourceVirtualEnvironmentVMTimeoutMoveDisk).(int) + + e = vmAPI.MoveVMDisk(ctx, diskMoveBody, moveDiskTimeout) + if e != nil { + return diag.FromErr(e) + } + } + } + return vmCreateStart(ctx, d, m) } @@ -1879,6 +2014,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) cpuFlags := cpuBlock[mkResourceVirtualEnvironmentVMCPUFlags].([]interface{}) cpuHotplugged := cpuBlock[mkResourceVirtualEnvironmentVMCPUHotplugged].(int) cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) + cpuNUMA := types2.CustomBool(cpuBlock[mkResourceVirtualEnvironmentVMCPUNUMA].(bool)) cpuType := cpuBlock[mkResourceVirtualEnvironmentVMCPUType].(string) cpuUnits := cpuBlock[mkResourceVirtualEnvironmentVMCPUUnits].(int) @@ -1888,6 +2024,29 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) return diag.FromErr(err) } + var efiDisk *vms.CustomEFIDisk + + efiDiskBlock := d.Get(mkResourceVirtualEnvironmentVMEFIDisk).([]interface{}) + if len(efiDiskBlock) > 0 { + block := efiDiskBlock[0].(map[string]interface{}) + + datastoreID, _ := block[mkResourceVirtualEnvironmentVMEFIDiskDatastoreID].(string) + fileFormat, _ := block[mkResourceVirtualEnvironmentVMEFIDiskFileFormat].(string) + efiType, _ := block[mkResourceVirtualEnvironmentVMEFIDiskType].(string) + preEnrolledKeys := types2.CustomBool(block[mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys].(bool)) + + if fileFormat == "" { + fileFormat = dvResourceVirtualEnvironmentVMEFIDiskFileFormat + } + + efiDisk = &vms.CustomEFIDisk{ + Type: &efiType, + FileVolume: fmt.Sprintf("%s:1", datastoreID), + Format: &fileFormat, + PreEnrolledKeys: &preEnrolledKeys, + } + } + virtioDeviceObjects := diskDeviceObjects["virtio"] scsiDeviceObjects := diskDeviceObjects["scsi"] // ideDeviceObjects := getOrderedDiskDeviceList(diskDeviceObjects, "ide") @@ -2055,10 +2214,12 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) CPUSockets: &cpuSockets, CPUUnits: &cpuUnits, DedicatedMemory: &memoryDedicated, + EFIDisk: efiDisk, FloatingMemory: &memoryFloating, IDEDevices: ideDevices, KeyboardLayout: &keyboardLayout, NetworkDevices: networkDeviceObjects, + NUMAEnabled: &cpuNUMA, OSType: &operatingSystemType, PCIDevices: pciDeviceObjects, SCSIHardware: &scsiHardware, @@ -2616,6 +2777,69 @@ func vmGetDiskDeviceObjects( return diskDeviceObjects, nil } +func vmGetEfiDisk(d *schema.ResourceData, disk []interface{}) *vms.CustomEFIDisk { + var efiDisk []interface{} + + if disk != nil { + efiDisk = disk + } else { + efiDisk = d.Get(mkResourceVirtualEnvironmentVMEFIDisk).([]interface{}) + } + + var efiDiskConfig *vms.CustomEFIDisk + + if len(efiDisk) > 0 { + efiDiskConfig = &vms.CustomEFIDisk{} + + block := efiDisk[0].(map[string]interface{}) + datastoreID, _ := block[mkResourceVirtualEnvironmentVMEFIDiskDatastoreID].(string) + fileFormat, _ := block[mkResourceVirtualEnvironmentVMEFIDiskFileFormat].(string) + efiType, _ := block[mkResourceVirtualEnvironmentVMEFIDiskType].(string) + preEnrolledKeys := types2.CustomBool(block[mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys].(bool)) + + // special case for efi disk, the size is ignored, see docs for more info + efiDiskConfig.FileVolume = fmt.Sprintf("%s:1", datastoreID) + efiDiskConfig.Format = &fileFormat + efiDiskConfig.Type = &efiType + efiDiskConfig.PreEnrolledKeys = &preEnrolledKeys + } + + return efiDiskConfig +} + +func vmGetEfiDiskAsStorageDevice(d *schema.ResourceData, disk []interface{}) (*vms.CustomStorageDevice, error) { + efiDisk := vmGetEfiDisk(d, disk) + + var storageDevice *vms.CustomStorageDevice + + if efiDisk != nil { + id := "0" + baseDiskInterface := "efidisk" + diskInterface := fmt.Sprint(baseDiskInterface, id) + + storageDevice = &vms.CustomStorageDevice{ + Enabled: true, + FileVolume: efiDisk.FileVolume, + Format: efiDisk.Format, + Interface: &diskInterface, + ID: &id, + } + + if efiDisk.Type != nil { + ds, err := types.ParseDiskSize(*efiDisk.Type) + if err != nil { + return nil, fmt.Errorf("invalid efi disk type: %s", err.Error()) + } + + sizeInt := ds.InMegabytes() + storageDevice.Size = &ds + storageDevice.SizeInt = &sizeInt + } + } + + return storageDevice, nil +} + func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices { pciDevice := d.Get(mkResourceVirtualEnvironmentVMHostPCI).([]interface{}) pciDeviceObjects := make(vms.CustomPCIDevices, len(pciDevice)) @@ -3038,6 +3262,13 @@ func vmReadCustom( cpu[mkResourceVirtualEnvironmentVMCPUHotplugged] = 0 } + if vmConfig.NUMAEnabled != nil { + cpu[mkResourceVirtualEnvironmentVMCPUNUMA] = *vmConfig.NUMAEnabled + } else { + // Default value of "numa" is "false" according to the API documentation. + cpu[mkResourceVirtualEnvironmentVMCPUNUMA] = false + } + if vmConfig.CPUSockets != nil { cpu[mkResourceVirtualEnvironmentVMCPUSockets] = *vmConfig.CPUSockets } else { @@ -3212,6 +3443,60 @@ func vmReadCustom( diags = append(diags, diag.FromErr(err)...) } + //nolint:nestif + if vmConfig.EFIDisk != nil { + efiDisk := map[string]interface{}{} + + fileIDParts := strings.Split(vmConfig.EFIDisk.FileVolume, ":") + + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskDatastoreID] = fileIDParts[0] + + if vmConfig.EFIDisk.Format != nil { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskFileFormat] = *vmConfig.EFIDisk.Format + } else { + // disk format may not be returned by config API if it is default for the storage, and that may be different + // from the default qcow2, so we need to read it from the storage API to make sure we have the correct value + files, err := api.Node(nodeName).ListDatastoreFiles(ctx, fileIDParts[0]) + if err != nil { + diags = append(diags, diag.FromErr(err)...) + } else { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskFileFormat] = "" + for _, v := range files { + if v.VolumeID == vmConfig.EFIDisk.FileVolume { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskFileFormat] = v.FileFormat + break + } + } + } + } + + if vmConfig.EFIDisk.Type != nil { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskType] = *vmConfig.EFIDisk.Type + } else { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskType] = dvResourceVirtualEnvironmentVMEFIDiskType + } + + if vmConfig.EFIDisk.PreEnrolledKeys != nil { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys] = *vmConfig.EFIDisk.PreEnrolledKeys + } else { + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys] = false + } + + currentEfiDisk := d.Get(mkResourceVirtualEnvironmentVMEFIDisk).([]interface{}) + + if len(clone) > 0 && len(currentEfiDisk) > 0 { + err := d.Set(mkResourceVirtualEnvironmentVMEFIDisk, []interface{}{efiDisk}) + diags = append(diags, diag.FromErr(err)...) + } else if len(currentEfiDisk) > 0 || + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskDatastoreID] != dvResourceVirtualEnvironmentVMEFIDiskDatastoreID || + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskType] != dvResourceVirtualEnvironmentVMEFIDiskType || + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys] != dvResourceVirtualEnvironmentVMEFIDiskPreEnrolledKeys || //nolint:lll + efiDisk[mkResourceVirtualEnvironmentVMEFIDiskFileFormat] != dvResourceVirtualEnvironmentVMEFIDiskFileFormat { + err := d.Set(mkResourceVirtualEnvironmentVMEFIDisk, []interface{}{efiDisk}) + diags = append(diags, diag.FromErr(err)...) + } + } + currentPCIList := d.Get(mkResourceVirtualEnvironmentVMHostPCI).([]interface{}) pciMap := map[string]interface{}{} var orderedPCIList []interface{} @@ -4216,6 +4501,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int) cpuFlags := cpuBlock[mkResourceVirtualEnvironmentVMCPUFlags].([]interface{}) cpuHotplugged := cpuBlock[mkResourceVirtualEnvironmentVMCPUHotplugged].(int) + cpuNUMA := types2.CustomBool(cpuBlock[mkResourceVirtualEnvironmentVMCPUNUMA].(bool)) cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) cpuType := cpuBlock[mkResourceVirtualEnvironmentVMCPUType].(string) cpuUnits := cpuBlock[mkResourceVirtualEnvironmentVMCPUUnits].(int) @@ -4229,6 +4515,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D updateBody.CPUCores = &cpuCores updateBody.CPUSockets = &cpuSockets updateBody.CPUUnits = &cpuUnits + updateBody.NUMAEnabled = &cpuNUMA if cpuHotplugged > 0 { updateBody.VirtualCPUCount = &cpuHotplugged @@ -4311,6 +4598,15 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } } + // Prepare the new efi disk configuration. + if d.HasChange(mkResourceVirtualEnvironmentVMEFIDisk) { + efiDisk := vmGetEfiDisk(d, nil) + + updateBody.EFIDisk = efiDisk + + rebootRequired = true + } + // Prepare the new cloud-init configuration. if d.HasChange(mkResourceVirtualEnvironmentVMInitialization) { initializationConfig := vmGetCloudInitConfig(d) @@ -4330,15 +4626,14 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D Media: &cdromMedia, } - if vmConfig.IDEDevice2 != nil { - if strings.Contains( + if vmConfig.IDEDevice2 != nil && + strings.Contains( vmConfig.IDEDevice2.FileVolume, fmt.Sprintf("vm-%d-cloudinit", vmID), ) { - tmp := updateBody.IDEDevices["ide2"] - tmp.Enabled = true - updateBody.IDEDevices["ide2"] = tmp - } + tmp := updateBody.IDEDevices["ide2"] + tmp.Enabled = true + updateBody.IDEDevices["ide2"] = tmp } } @@ -4535,6 +4830,37 @@ func vmUpdateDiskLocationAndSize( return diag.FromErr(err) } + // Add efidisk if it has changes + if d.HasChange(mkResourceVirtualEnvironmentVMEFIDisk) { + diskOld, diskNew := d.GetChange(mkResourceVirtualEnvironmentVMEFIDisk) + + oldEfiDisk, e := vmGetEfiDiskAsStorageDevice(d, diskOld.([]interface{})) + if e != nil { + return diag.FromErr(e) + } + + newEfiDisk, e := vmGetEfiDiskAsStorageDevice(d, diskNew.([]interface{})) + if e != nil { + return diag.FromErr(e) + } + + if oldEfiDisk != nil { + baseDiskInterface := diskDigitPrefix(*oldEfiDisk.Interface) + diskOldEntries[baseDiskInterface][*oldEfiDisk.Interface] = *oldEfiDisk + } + + if newEfiDisk != nil { + baseDiskInterface := diskDigitPrefix(*newEfiDisk.Interface) + diskNewEntries[baseDiskInterface][*newEfiDisk.Interface] = *newEfiDisk + } + + if oldEfiDisk != nil && newEfiDisk != nil && oldEfiDisk.Size != newEfiDisk.Size { + return diag.Errorf( + "resizing of efidisks is not supported.", + ) + } + } + var diskMoveBodies []*vms.MoveDiskRequestBody var diskResizeBodies []*vms.ResizeDiskRequestBody @@ -4566,7 +4892,7 @@ func vmUpdateDiskLocationAndSize( shutdownForDisksRequired = true } - if *oldDisk.SizeInt <= *diskNewEntries[prefix][oldKey].SizeInt { + if *oldDisk.SizeInt < *diskNewEntries[prefix][oldKey].SizeInt { diskResizeBodies = append( diskResizeBodies, &vms.ResizeDiskRequestBody{ diff --git a/proxmoxtf/resource/vm_test.go b/proxmoxtf/resource/vm_test.go index e8230598..03c989d9 100644 --- a/proxmoxtf/resource/vm_test.go +++ b/proxmoxtf/resource/vm_test.go @@ -45,6 +45,7 @@ func TestVMSchema(t *testing.T) { mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMDescription, mkResourceVirtualEnvironmentVMDisk, + mkResourceVirtualEnvironmentVMEFIDisk, mkResourceVirtualEnvironmentVMInitialization, mkResourceVirtualEnvironmentVMHostPCI, mkResourceVirtualEnvironmentVMKeyboardLayout, @@ -80,6 +81,7 @@ func TestVMSchema(t *testing.T) { mkResourceVirtualEnvironmentVMCPU: schema.TypeList, mkResourceVirtualEnvironmentVMDescription: schema.TypeString, mkResourceVirtualEnvironmentVMDisk: schema.TypeList, + mkResourceVirtualEnvironmentVMEFIDisk: schema.TypeList, mkResourceVirtualEnvironmentVMHostPCI: schema.TypeList, mkResourceVirtualEnvironmentVMInitialization: schema.TypeList, mkResourceVirtualEnvironmentVMIPv4Addresses: schema.TypeList, @@ -165,6 +167,7 @@ func TestVMSchema(t *testing.T) { mkResourceVirtualEnvironmentVMCPUCores, mkResourceVirtualEnvironmentVMCPUFlags, mkResourceVirtualEnvironmentVMCPUHotplugged, + mkResourceVirtualEnvironmentVMCPUNUMA, mkResourceVirtualEnvironmentVMCPUSockets, mkResourceVirtualEnvironmentVMCPUType, mkResourceVirtualEnvironmentVMCPUUnits, @@ -175,6 +178,7 @@ func TestVMSchema(t *testing.T) { mkResourceVirtualEnvironmentVMCPUCores: schema.TypeInt, mkResourceVirtualEnvironmentVMCPUFlags: schema.TypeList, mkResourceVirtualEnvironmentVMCPUHotplugged: schema.TypeInt, + mkResourceVirtualEnvironmentVMCPUNUMA: schema.TypeBool, mkResourceVirtualEnvironmentVMCPUSockets: schema.TypeInt, mkResourceVirtualEnvironmentVMCPUType: schema.TypeString, mkResourceVirtualEnvironmentVMCPUUnits: schema.TypeInt, @@ -216,6 +220,20 @@ func TestVMSchema(t *testing.T) { mkResourceVirtualEnvironmentVMDiskSpeedWriteBurstable: schema.TypeInt, }) + efiDiskSchema := test.AssertNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMEFIDisk) + + test.AssertOptionalArguments(t, efiDiskSchema, []string{ + mkResourceVirtualEnvironmentVMEFIDiskDatastoreID, + mkResourceVirtualEnvironmentVMEFIDiskFileFormat, + mkResourceVirtualEnvironmentVMEFIDiskType, + }) + + test.AssertValueTypes(t, efiDiskSchema, map[string]schema.ValueType{ + mkResourceVirtualEnvironmentVMEFIDiskDatastoreID: schema.TypeString, + mkResourceVirtualEnvironmentVMEFIDiskFileFormat: schema.TypeString, + mkResourceVirtualEnvironmentVMEFIDiskType: schema.TypeString, + }) + initializationSchema := test.AssertNestedSchemaExistence( t, s,