diff --git a/proxmoxtf/resource/vm/disk/disk.go b/proxmoxtf/resource/vm/disk/disk.go index a6e67eddb..c7c7f7979 100644 --- a/proxmoxtf/resource/vm/disk/disk.go +++ b/proxmoxtf/resource/vm/disk/disk.go @@ -23,7 +23,7 @@ import ( ) // GetInfo returns the disk information for a VM. -func GetInfo(resp *vms.GetResponseData, d *schema.ResourceData) vms.CustomStorageDevices { +func GetInfo(resp *vms.GetResponseData, d *schema.ResourceData, vmID int) vms.CustomStorageDevices { currentDisk := d.Get(MkDisk) currentDiskList := currentDisk.([]interface{}) @@ -82,6 +82,8 @@ func GetInfo(resp *vms.GetResponseData, d *schema.ResourceData) vms.CustomStorag storageDevices["virtio14"] = resp.VirtualIODevice14 storageDevices["virtio15"] = resp.VirtualIODevice15 + cloudInitDiskName := fmt.Sprintf("vm-%d-cloudinit", vmID) + for k, v := range storageDevices { if v != nil { if currentDiskMap[k] != nil { @@ -98,6 +100,10 @@ func GetInfo(resp *vms.GetResponseData, d *schema.ResourceData) vms.CustomStorag // defensive copy of the loop variable iface := k v.Interface = &iface + + if strings.Contains(v.FileVolume, cloudInitDiskName) { + storageDevices["cloudinit"] = v + } } } @@ -591,6 +597,8 @@ func Read( currentDiskList := d.Get(MkDisk).([]interface{}) diskMap := map[string]interface{}{} + cloudInitDisk := diskObjects["cloudinit"] + var diags diag.Diagnostics for di, dd := range diskObjects { @@ -598,6 +606,10 @@ func Read( continue } + if cloudInitDisk != nil && cloudInitDisk.FileVolume == dd.FileVolume { + continue + } + if dd.IsCloudInitDrive(vmID) { continue } diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 9c749c833..4df563ade 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -74,7 +74,7 @@ const ( dvTPMStateDatastoreID = "local-lvm" dvTPMStateVersion = "v2.0" dvInitializationDatastoreID = "local-lvm" - dvInitializationInterface = "" + dvInitializationInterface = "ide2" dvInitializationDNSDomain = "" dvInitializationDNSServer = "" dvInitializationIPConfigIPv4Address = "" @@ -691,8 +691,9 @@ func VM() *schema.Resource { Default: dvInitializationDatastoreID, }, mkInitializationInterface: { - Type: schema.TypeString, - Description: "The IDE interface on which the CloudInit drive will be added", + Type: schema.TypeString, + Description: "The interface the cloud-init drive will be attached to " + + "(if you're using q35 or UEFI, ide doesn't work, so you may want to use scsi)", Optional: true, Default: dvInitializationInterface, ValidateDiagFunc: CloudInitInterfaceValidator(), @@ -1392,7 +1393,7 @@ func VM() *schema.Resource { // ConvertToStringSlice helps convert interface slice to string slice. func ConvertToStringSlice(interfaceSlice []interface{}) []string { - resultSlice := []string{} + resultSlice := make([]string, 0, len(interfaceSlice)) for _, val := range interfaceSlice { resultSlice = append(resultSlice, val.(string)) } @@ -1410,54 +1411,10 @@ func vmCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D return vmCreateCustom(ctx, d, m) } -// Check for an existing CloudInit IDE drive. If no such drive is found, return the specified `defaultValue`. -func findExistingCloudInitDrive(vmConfig *vms.GetResponseData, vmID int, defaultValue string) string { - ideDevices := []*vms.CustomStorageDevice{ - vmConfig.IDEDevice0, - vmConfig.IDEDevice1, - vmConfig.IDEDevice2, - vmConfig.IDEDevice3, - } - for i, device := range ideDevices { - if device != nil && device.Enabled && device.IsCloudInitDrive(vmID) { - return fmt.Sprintf("ide%d", i) - } - } - - sataDevices := []*vms.CustomStorageDevice{ - vmConfig.SATADevice0, - vmConfig.SATADevice1, - vmConfig.SATADevice2, - vmConfig.SATADevice3, - vmConfig.SATADevice4, - vmConfig.SATADevice5, - } - for i, device := range sataDevices { - if device != nil && device.Enabled && device.IsCloudInitDrive(vmID) { - return fmt.Sprintf("sata%d", i) - } - } - - scsiDevices := []*vms.CustomStorageDevice{ - vmConfig.SCSIDevice0, - vmConfig.SCSIDevice1, - vmConfig.SCSIDevice2, - vmConfig.SCSIDevice3, - vmConfig.SCSIDevice4, - vmConfig.SCSIDevice5, - vmConfig.SCSIDevice6, - vmConfig.SCSIDevice7, - vmConfig.SCSIDevice8, - vmConfig.SCSIDevice9, - vmConfig.SCSIDevice10, - vmConfig.SCSIDevice11, - vmConfig.SCSIDevice12, - vmConfig.SCSIDevice13, - } - for i, device := range scsiDevices { - if device != nil && device.Enabled && device.IsCloudInitDrive(vmID) { - return fmt.Sprintf("scsi%d", i) - } +// Check for an existing cloud-init drive. If no such drive is found, return the specified `defaultValue`. +func findExistingCloudInitDrive(diskInfo vms.CustomStorageDevices, defaultValue string) string { + if cloudInitDrive, found := diskInfo["cloudinit"]; found && cloudInitDrive.Interface != nil { + return *cloudInitDrive.Interface } return defaultValue @@ -1523,17 +1480,17 @@ func getStorageDevice(vmConfig *vms.GetResponseData, deviceName string) *vms.Cus } } -// Delete IDE interfaces that can then be used for CloudInit. The first interface will always +// Delete IDE interfaces that can then be used for cloud-init. The first interface will always // be deleted. The second will be deleted only if it isn't empty and isn't the same as the // first. -func deleteIdeDrives(ctx context.Context, vmAPI *vms.Client, itf1 string, itf2 string) diag.Diagnostics { +func deleteCloudInitDrives(ctx context.Context, vmAPI *vms.Client, itf1 string, itf2 string) diag.Diagnostics { ddUpdateBody := &vms.UpdateRequestBody{} ddUpdateBody.Delete = append(ddUpdateBody.Delete, itf1) - tflog.Debug(ctx, fmt.Sprintf("Deleting IDE interface '%s'", itf1)) + tflog.Debug(ctx, fmt.Sprintf("Deleting cloud-init interface '%s'", itf1)) if itf2 != "" && itf2 != itf1 { ddUpdateBody.Delete = append(ddUpdateBody.Delete, itf2) - tflog.Debug(ctx, fmt.Sprintf("Deleting IDE interface '%s'", itf2)) + tflog.Debug(ctx, fmt.Sprintf("Deleting cloud-init interface '%s'", itf2)) } e := vmAPI.UpdateVM(ctx, ddUpdateBody) @@ -1671,7 +1628,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d return diag.FromErr(err) } - datastores := getDiskDatastores(vmConfig, d) + datastores := getDiskDatastores(vmConfig, d, vmID) onlySharedDatastores := true @@ -1819,23 +1776,6 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d updateBody.SCSIHardware = &scsiHardware } - if len(cdrom) > 0 || len(initialization) > 0 { - ideDevices = vms.CustomStorageDevices{ - "ide0": &vms.CustomStorageDevice{ - Enabled: false, - }, - "ide1": &vms.CustomStorageDevice{ - Enabled: false, - }, - "ide2": &vms.CustomStorageDevice{ - Enabled: false, - }, - "ide3": &vms.CustomStorageDevice{ - Enabled: false, - }, - } - } - if len(cdrom) > 0 && cdrom[0] != nil { cdromBlock := cdrom[0].(map[string]interface{}) @@ -1854,6 +1794,8 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d FileVolume: cdromFileID, Media: &cdromMedia, } + + updateBody.IDEDevices = ideDevices } if len(cpu) > 0 && cpu[0] != nil { @@ -1909,31 +1851,44 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d return diag.FromErr(err) } + allDiskInfo := disk.GetInfo(vmConfig, d, vmID) // from the cloned VM + if len(initialization) > 0 && initialization[0] != nil { - tflog.Trace(ctx, "Preparing the CloudInit configuration") + tflog.Trace(ctx, "Preparing the cloud-init configuration") initializationBlock := initialization[0].(map[string]interface{}) initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string) initializationInterface := initializationBlock[mkInitializationInterface].(string) - existingInterface := findExistingCloudInitDrive(vmConfig, vmID, "ide2") + existingInterface := findExistingCloudInitDrive(allDiskInfo, dvInitializationInterface) if initializationInterface == "" { initializationInterface = existingInterface } - tflog.Trace(ctx, fmt.Sprintf("CloudInit IDE interface is '%s'", initializationInterface)) + tflog.Trace(ctx, fmt.Sprintf("cloud-init IDE interface is '%s'", initializationInterface)) const cdromCloudInitEnabled = true cdromCloudInitFileID := fmt.Sprintf("%s:cloudinit", initializationDatastoreID) cdromCloudInitMedia := "cdrom" - ideDevices[initializationInterface] = &vms.CustomStorageDevice{ + cloudInitDevice := &vms.CustomStorageDevice{ Enabled: cdromCloudInitEnabled, FileVolume: cdromCloudInitFileID, Media: &cdromCloudInitMedia, } - if err := deleteIdeDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil { + busDevs := busDevices{ + IDE: &updateBody.IDEDevices, + SCSI: &updateBody.SCSIDevices, + SATA: &updateBody.SATADevices, + } + + err = addCloudInitDisk(busDevs, initializationInterface, cloudInitDevice) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to add cloud-init device: %w", err)) + } + + if err := deleteCloudInitDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil { return err } @@ -1948,10 +1903,6 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d updateBody.USBDevices = vmGetHostUSBDeviceObjects(d) } - if len(cdrom) > 0 || len(initialization) > 0 { - updateBody.IDEDevices = ideDevices - } - if keyboardLayout != dvKeyboardLayout { updateBody.KeyboardLayout = &keyboardLayout } @@ -2074,8 +2025,6 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d ///////////////// - allDiskInfo := disk.GetInfo(vmConfig, d) // from the cloned VM - planDisks, e := disk.GetDiskDeviceObjects(d, VM(), nil) // from the resource config if e != nil { return diag.FromErr(e) @@ -2348,7 +2297,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) cdromCloudInitInterface = initializationBlock[mkInitializationInterface].(string) if cdromCloudInitInterface == "" { - cdromCloudInitInterface = "ide2" + cdromCloudInitInterface = dvInitializationInterface } } @@ -2469,20 +2418,32 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) cpuFlagsConverted[fi] = flag.(string) } - ideDevice2Media := "cdrom" + cdromMedia := "cdrom" ideDevices := vms.CustomStorageDevices{ - cdromCloudInitInterface: &vms.CustomStorageDevice{ - Enabled: cdromCloudInitEnabled, - FileVolume: cdromCloudInitFileID, - Media: &ideDevice2Media, - }, cdromInterface: &vms.CustomStorageDevice{ Enabled: cdromEnabled, FileVolume: cdromFileID, - Media: &ideDevice2Media, + Media: &cdromMedia, }, } + cloudInitDevice := &vms.CustomStorageDevice{ + Enabled: cdromCloudInitEnabled, + FileVolume: cdromCloudInitFileID, + Media: &cdromMedia, + } + + busDevs := busDevices{ + IDE: &ideDevices, + SCSI: &scsiDeviceObjects, + SATA: &sataDeviceObjects, + } + + err = addCloudInitDisk(busDevs, cdromCloudInitInterface, cloudInitDevice) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to add cloud-init device: %w", err)) + } + if memoryShared > 0 { memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID) memorySharedObject = &vms.CustomSharedMemory{ @@ -3508,7 +3469,7 @@ func vmReadCustom( } //////////////////// - allDiskInfo := disk.GetInfo(vmConfig, d) // from the cloned VM + allDiskInfo := disk.GetInfo(vmConfig, d, vmID) // from the cloned VM diags = append(diags, disk.Read(ctx, d, allDiskInfo, vmID, api, nodeName, len(clone) > 0)...) @@ -3691,12 +3652,11 @@ func vmReadCustom( // Compare the initialization configuration to the one stored in the state. initialization := map[string]interface{}{} - initializationInterface := findExistingCloudInitDrive(vmConfig, vmID, "") - if initializationInterface != "" { - initializationDevice := getStorageDevice(vmConfig, initializationInterface) + initializationDevice := allDiskInfo["cloudinit"] + if initializationDevice != nil && initializationDevice.Interface != nil { fileVolumeParts := strings.Split(initializationDevice.FileVolume, ":") - initialization[mkInitializationInterface] = initializationInterface + initialization[mkInitializationInterface] = *initializationDevice.Interface initialization[mkInitializationDatastoreID] = fileVolumeParts[0] } @@ -4798,7 +4758,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } // Prepare the new disk device configuration. - allDiskInfo := disk.GetInfo(vmConfig, d) + allDiskInfo := disk.GetInfo(vmConfig, d, vmID) planDisks, err := disk.GetDiskDeviceObjects(d, resource, nil) if err != nil { @@ -4847,16 +4807,16 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D initializationInterface := initializationBlock[mkInitializationInterface].(string) cdromMedia := "cdrom" - existingInterface := findExistingCloudInitDrive(vmConfig, vmID, "") + existingInterface := findExistingCloudInitDrive(allDiskInfo, "") if initializationInterface == "" && existingInterface == "" { - initializationInterface = "ide2" + initializationInterface = dvInitializationInterface } else if initializationInterface == "" { initializationInterface = existingInterface } mustMove := existingInterface != "" && initializationInterface != existingInterface if mustMove { - tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from %s to %s", existingInterface, initializationInterface)) + tflog.Debug(ctx, fmt.Sprintf("cloud-init must be moved from %s to %s", existingInterface, initializationInterface)) } oldInit, _ := d.GetChange(mkInitialization) @@ -4865,18 +4825,18 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D mustChangeDatastore := prevDatastoreID != initializationDatastoreID if mustChangeDatastore { - tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from datastore %s to datastore %s", + tflog.Debug(ctx, fmt.Sprintf("cloud-init must be moved from datastore %s to datastore %s", prevDatastoreID, initializationDatastoreID)) } if mustMove || mustChangeDatastore || existingInterface == "" { - // CloudInit must be moved, either from a device to another or from a datastore + // cloud-init must be moved, either from a device to another or from a datastore // to another (or both). This requires the VM to be stopped. if err := vmShutdown(ctx, vmAPI, d); err != nil { return err } - if err := deleteIdeDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil { + if err := deleteCloudInitDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil { return err } @@ -4887,11 +4847,22 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D fileVolume = ideDevice.FileVolume } - updateBody.IDEDevices[initializationInterface] = &vms.CustomStorageDevice{ + cloudInitDevice := &vms.CustomStorageDevice{ Enabled: true, FileVolume: fileVolume, Media: &cdromMedia, } + + busDevs := busDevices{ + IDE: &updateBody.IDEDevices, + SCSI: &updateBody.SCSIDevices, + SATA: &updateBody.SATADevices, + } + + err = addCloudInitDisk(busDevs, initializationInterface, cloudInitDevice) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to add cloud-init device: %w", err)) + } } rebootRequired = true @@ -5364,8 +5335,8 @@ func vmDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } // getDiskDatastores returns a list of the used datastores in a VM. -func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string { - storageDevices := disk.GetInfo(vm, d) +func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData, vmID int) []string { + storageDevices := disk.GetInfo(vm, d, vmID) datastoresSet := map[string]int{} for _, diskInfo := range storageDevices { @@ -5388,7 +5359,7 @@ func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string datastoresSet[fileIDParts[0]] = 1 } - datastores := []string{} + datastores := make([]string, 0, len(datastoresSet)) for datastore := range datastoresSet { datastores = append(datastores, datastore) } @@ -5451,3 +5422,48 @@ func getAgentTimeout(d *schema.ResourceData) (time.Duration, error) { return agentTimeout, nil } + +type busDevices struct { + IDE *vms.CustomStorageDevices + SCSI *vms.CustomStorageDevices + SATA *vms.CustomStorageDevices +} + +func addCloudInitDisk( + buses busDevices, + initializationInterface string, + cloudInitDevice *vms.CustomStorageDevice, +) error { + var devices *vms.CustomStorageDevices + + baseInitInterface := disk.DigitPrefix(initializationInterface) + switch baseInitInterface { + case "ide": + devices = buses.IDE + case "scsi": + devices = buses.SCSI + case "sata": + devices = buses.SATA + default: + return fmt.Errorf( + "invalid initialization interface type %#v (should be one of: ide[n],sata[n],scsi[n])", + initializationInterface, + ) + } + + if devices == nil { + return fmt.Errorf("bus %v is a null pointer", baseInitInterface) + } + + if *devices == nil { + *devices = make(vms.CustomStorageDevices) + } + + if _, found := (*devices)[initializationInterface]; found { + return fmt.Errorf("device %#v already exists", initializationInterface) + } + + (*devices)[initializationInterface] = cloudInitDevice + + return nil +}