diff --git a/example/variables.tf b/example/variables.tf index 77f0df09..eb47415c 100644 --- a/example/variables.tf +++ b/example/variables.tf @@ -28,5 +28,5 @@ variable "release_20240725_ubuntu_24_noble_lxc_img_url" { variable "release_20240725_ubuntu_24_noble_lxc_img_checksum" { type = string description = "The checksum for the Ubuntu 24.04 LXC image" - default = "10331782a01cd2348b421a261f0e15ba041358bd540f66f2432b162e70b90d28" + default = "d767d38cb25b2c25d84edc31a80dd1c29a8c922b74188b0e14768b2b2fb6df8e" } diff --git a/fwprovider/test/resource_vm_test.go b/fwprovider/test/resource_vm_test.go index b3a246c7..f2fddd7f 100644 --- a/fwprovider/test/resource_vm_test.go +++ b/fwprovider/test/resource_vm_test.go @@ -695,15 +695,59 @@ func TestAccResourceVMClone(t *testing.T) { initialization { datastore_id = "doesnotexist" ip_config { - ipv4 { - address = "172.16.2.57/32" - gateway = "172.16.2.10" - } + ipv4 { + address = "172.16.2.57/32" + gateway = "172.16.2.10" + } } } }`), ExpectError: regexp.MustCompile(`storage 'doesnotexist' does not exist`), }}}, + {"update disk speed and resize in a clone", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm" "template" { + node_name = "{{.NodeName}}" + started = false + disk { + datastore_id = "local-lvm" + interface = "virtio0" + file_format = "raw" + size = 20 + } + } + resource "proxmox_virtual_environment_vm" "clone" { + node_name = "{{.NodeName}}" + started = false + clone { + vm_id = proxmox_virtual_environment_vm.template.vm_id + } + disk { + datastore_id = "local-lvm" + interface = "virtio0" + iothread = true + discard = "on" + size = 30 + speed { + iops_read = 100 + iops_read_burstable = 1000 + iops_write = 400 + iops_write_burstable = 800 + } + } + }`), + Check: resource.ComposeTestCheckFunc( + ResourceAttributes("proxmox_virtual_environment_vm.clone", map[string]string{ + "disk.0.iothread": "true", + "disk.0.discard": "on", + "disk.0.size": "30", + "disk.0.speed.0.iops_read": "100", + "disk.0.speed.0.iops_read_burstable": "1000", + "disk.0.speed.0.iops_write": "400", + "disk.0.speed.0.iops_write_burstable": "800", + }), + ), + }}}, } for _, tt := range tests { diff --git a/fwprovider/vm/resource.go b/fwprovider/vm/resource.go index 9fd15f55..b51305b5 100644 --- a/fwprovider/vm/resource.go +++ b/fwprovider/vm/resource.go @@ -378,7 +378,7 @@ func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp resp.Diagnostics.AddError("Failed to get VM status", err.Error()) } - if resp.Diagnostics.HasError() { + if resp.Diagnostics.HasError() || status == nil { return } diff --git a/proxmox/helpers/ptr/ptr.go b/proxmox/helpers/ptr/ptr.go index a47264e0..62213c93 100644 --- a/proxmox/helpers/ptr/ptr.go +++ b/proxmox/helpers/ptr/ptr.go @@ -32,3 +32,14 @@ func Eq[T comparable](a, b *T) bool { return *a == *b } + +// UpdateIfChanged updates dst with src if src is not nil and different from dst. +// Returns true if an update was made. +func UpdateIfChanged[T comparable](dst **T, src *T) bool { + if src != nil && !Eq(*dst, src) { + *dst = src + return true + } + + return false +} diff --git a/proxmox/nodes/vms/custom_storage_device.go b/proxmox/nodes/vms/custom_storage_device.go index ff0e8471..1d67a635 100644 --- a/proxmox/nodes/vms/custom_storage_device.go +++ b/proxmox/nodes/vms/custom_storage_device.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) @@ -360,6 +361,40 @@ func (d *CustomStorageDevice) UnmarshalJSON(b []byte) error { return nil } +// MergeWith merges attributes of the given CustomStorageDevice with the current one. +// It will overwrite the current attributes with the given ones if they are not nil. +// The attributes that are not merged are: +// - DatastoreID +// - FileID +// - FileVolume +// - Format +// - Size +// +// It will return true if any attribute of the current CustomStorageDevice was changed. +func (d *CustomStorageDevice) MergeWith(m CustomStorageDevice) bool { + updated := false + + updated = ptr.UpdateIfChanged(&d.AIO, m.AIO) || updated + updated = ptr.UpdateIfChanged(&d.Backup, m.Backup) || updated + updated = ptr.UpdateIfChanged(&d.BurstableReadSpeedMbps, m.BurstableReadSpeedMbps) || updated + updated = ptr.UpdateIfChanged(&d.BurstableWriteSpeedMbps, m.BurstableWriteSpeedMbps) || updated + updated = ptr.UpdateIfChanged(&d.Cache, m.Cache) || updated + updated = ptr.UpdateIfChanged(&d.Discard, m.Discard) || updated + updated = ptr.UpdateIfChanged(&d.IOThread, m.IOThread) || updated + updated = ptr.UpdateIfChanged(&d.IopsRead, m.IopsRead) || updated + updated = ptr.UpdateIfChanged(&d.IopsWrite, m.IopsWrite) || updated + updated = ptr.UpdateIfChanged(&d.Media, m.Media) || updated + updated = ptr.UpdateIfChanged(&d.MaxIopsRead, m.MaxIopsRead) || updated + updated = ptr.UpdateIfChanged(&d.MaxIopsWrite, m.MaxIopsWrite) || updated + updated = ptr.UpdateIfChanged(&d.MaxReadSpeedMbps, m.MaxReadSpeedMbps) || updated + updated = ptr.UpdateIfChanged(&d.MaxWriteSpeedMbps, m.MaxWriteSpeedMbps) || updated + updated = ptr.UpdateIfChanged(&d.Replicate, m.Replicate) || updated + updated = ptr.UpdateIfChanged(&d.SSD, m.SSD) || updated + updated = ptr.UpdateIfChanged(&d.Serial, m.Serial) || updated + + return updated +} + // Filter returns a map of CustomStorageDevices filtered by the given function. func (d CustomStorageDevices) Filter(fn func(*CustomStorageDevice) bool) CustomStorageDevices { result := make(CustomStorageDevices) diff --git a/proxmoxtf/resource/vm/disk/disk.go b/proxmoxtf/resource/vm/disk/disk.go index fd64f963..9b0fe5e7 100644 --- a/proxmoxtf/resource/vm/disk/disk.go +++ b/proxmoxtf/resource/vm/disk/disk.go @@ -54,81 +54,77 @@ func GetInfo(resp *vms.GetResponseData, d *schema.ResourceData) vms.CustomStorag return storageDevices } -// CreateClone creates disks for a cloned VM. -func CreateClone( +// UpdateClone updates disks in a cloned VM. +func UpdateClone( ctx context.Context, - d *schema.ResourceData, planDisks vms.CustomStorageDevices, allDiskInfo vms.CustomStorageDevices, vmAPI *vms.Client, ) error { - disk := d.Get(MkDisk).([]interface{}) - for i := range disk { - diskBlock := disk[i].(map[string]interface{}) - diskInterface := diskBlock[mkDiskInterface].(string) - dataStoreID := diskBlock[mkDiskDatastoreID].(string) - diskSize := int64(diskBlock[mkDiskSize].(int)) + for diskInterface, planDisk := range planDisks { + currentDisk := allDiskInfo[diskInterface] - currentDiskInfo := allDiskInfo[diskInterface] - configuredDiskInfo := planDisks[diskInterface] - - if currentDiskInfo == nil { + if currentDisk == nil { diskUpdateBody := &vms.UpdateRequestBody{} + diskUpdateBody.AddCustomStorageDevice(diskInterface, *planDisk) - diskUpdateBody.AddCustomStorageDevice(diskInterface, *configuredDiskInfo) - - err := vmAPI.UpdateVM(ctx, diskUpdateBody) - if err != nil { + if err := vmAPI.UpdateVM(ctx, diskUpdateBody); err != nil { return fmt.Errorf("disk update fails: %w", err) } continue } - if diskSize < currentDiskInfo.Size.InGigabytes() { - return fmt.Errorf("disk resize fails requests size (%dG) is lower than current size (%d)", - diskSize, - *currentDiskInfo.Size, + if planDisk.Size.InMegabytes() < currentDisk.Size.InMegabytes() { + return fmt.Errorf("disk resize failure: requested size (%s) is lower than current size (%s)", + planDisk.Size.String(), + currentDisk.Size.String(), ) } - deleteOriginalDisk := types.CustomBool(true) - - diskMoveBody := &vms.MoveDiskRequestBody{ - DeleteOriginalDisk: &deleteOriginalDisk, - Disk: diskInterface, - TargetStorage: dataStoreID, - } - - diskResizeBody := &vms.ResizeDiskRequestBody{ - Disk: diskInterface, - Size: *types.DiskSizeFromGigabytes(diskSize), - } - moveDisk := false - if dataStoreID != "" { - moveDisk = true - - if allDiskInfo[diskInterface] != nil { - fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":") - moveDisk = dataStoreID != fileIDParts[0] - } + if *planDisk.DatastoreID != "" { + fileIDParts := strings.Split(currentDisk.FileVolume, ":") + moveDisk = *planDisk.DatastoreID != fileIDParts[0] } if moveDisk { + deleteOriginalDisk := types.CustomBool(true) + + diskMoveBody := &vms.MoveDiskRequestBody{ + DeleteOriginalDisk: &deleteOriginalDisk, + Disk: diskInterface, + TargetStorage: *planDisk.DatastoreID, + } + err := vmAPI.MoveVMDisk(ctx, diskMoveBody) if err != nil { return fmt.Errorf("disk move fails: %w", err) } } - if diskSize > currentDiskInfo.Size.InGigabytes() { + if planDisk.Size.InMegabytes() > currentDisk.Size.InMegabytes() { + diskResizeBody := &vms.ResizeDiskRequestBody{ + Disk: diskInterface, + Size: *planDisk.Size, + } + err := vmAPI.ResizeVMDisk(ctx, diskResizeBody) if err != nil { return fmt.Errorf("disk resize fails: %w", err) } } + + // update other disk parameters + if currentDisk.MergeWith(*planDisk) { + diskUpdateBody := &vms.UpdateRequestBody{} + diskUpdateBody.AddCustomStorageDevice(diskInterface, *currentDisk) + + if err := vmAPI.UpdateVM(ctx, diskUpdateBody); err != nil { + return fmt.Errorf("disk update fails: %w", err) + } + } } return nil diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 99d569ce..fe4eb9bd 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -2212,14 +2212,14 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d return diag.FromErr(e) } - allDiskInfo := disk.GetInfo(vmConfig, d) // from the cloned VM + clonedDiskInfo := 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) } - e = disk.CreateClone(ctx, d, planDisks, allDiskInfo, vmAPI) + e = disk.UpdateClone(ctx, planDisks, clonedDiskInfo, vmAPI) if e != nil { return diag.FromErr(e) } @@ -2268,8 +2268,8 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d if dataStoreID != "" { moveDisk = true - if allDiskInfo[diskInterface] != nil { - fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":") + if clonedDiskInfo[diskInterface] != nil { + fileIDParts := strings.Split(clonedDiskInfo[diskInterface].FileVolume, ":") moveDisk = dataStoreID != fileIDParts[0] } } @@ -2319,8 +2319,8 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d if dataStoreID != "" { moveDisk = true - if allDiskInfo[diskInterface] != nil { - fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":") + if clonedDiskInfo[diskInterface] != nil { + fileIDParts := strings.Split(clonedDiskInfo[diskInterface].FileVolume, ":") moveDisk = dataStoreID != fileIDParts[0] } }