diff --git a/libvirt/pool.go b/libvirt/pool.go index 95f660bf9..d9b4a0fea 100644 --- a/libvirt/pool.go +++ b/libvirt/pool.go @@ -13,6 +13,15 @@ const ( poolStateConfNotExists = resourceStateConfNotExists ) +// storage pool state as returned from REMOTE_PROC_STORAGE_POOL_GET_INFO +const ( + poolStateInactive uint8 = iota + poolStateBuilding + poolStateRunning + poolStateDegraded + poolStateInaccessible +) + func poolExistsStateRefreshFunc(virConn *libvirt.Libvirt, uuid libvirt.UUID) retry.StateRefreshFunc { return func() (interface{}, string, error) { _, err := virConn.StoragePoolLookupByUUID(uuid) diff --git a/libvirt/resource_libvirt_volume.go b/libvirt/resource_libvirt_volume.go index 8229dd90e..4f9335b93 100644 --- a/libvirt/resource_libvirt_volume.go +++ b/libvirt/resource_libvirt_volume.go @@ -15,7 +15,9 @@ func resourceLibvirtVolume() *schema.Resource { return &schema.Resource{ CreateContext: resourceLibvirtVolumeCreate, ReadContext: resourceLibvirtVolumeRead, + UpdateContext: resourceLibvirtVolumeUpdate, DeleteContext: resourceLibvirtVolumeDelete, + CustomizeDiff: resourceLibvirtVolumeCustomDiff, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -37,7 +39,6 @@ func resourceLibvirtVolume() *schema.Resource { Type: schema.TypeInt, Optional: true, Computed: true, - ForceNew: true, }, "format": { Type: schema.TypeString, @@ -359,9 +360,48 @@ func resourceLibvirtVolumeRead(ctx context.Context, d *schema.ResourceData, meta return nil } -// resourceLibvirtVolumeDelete removed a volume resource. +// resourceLibvirtVolumeUpdate dinamically updates the size of a volume. +// When the new size is less than the previous one the volume will be destroyed and recreated. +func resourceLibvirtVolumeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Client) + + if d.HasChange("size") { + old, new := d.GetChange("size") + oldSize := old.(int) + newSize := new.(int) + + if newSize > oldSize { + log.Printf("[INFO] Resizing volume from %d to %d", oldSize, newSize) + + err := volumeResize(ctx, client, d.Id(), uint64(oldSize), uint64(newSize)) + if err != nil { + return diag.FromErr(err) + } + d.Set("size", newSize) + } + } + + return resourceLibvirtVolumeRead(ctx, d, meta) +} + +// resourceLibvirtVolumeDelete removes a volume resource. func resourceLibvirtVolumeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*Client) return diag.FromErr(volumeDelete(ctx, client, d.Id())) } + +// resourceLibvirtVolumeCustomDiff will notify the user that the volume needs to be recreated when the new size is less than the old one. +// The volume will then be destroyed and recreated. +func resourceLibvirtVolumeCustomDiff(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + if d.HasChange("size") { + oldSize, newSize := d.GetChange("size") + + if newSize.(int) < oldSize.(int) { + log.Printf("[DEBUG] new size < old size: the volume will be destroyed and recreated") + return d.ForceNew("size") + } + } + + return nil +} diff --git a/libvirt/resource_libvirt_volume_test.go b/libvirt/resource_libvirt_volume_test.go index cfe34fb30..dbc2b09a5 100644 --- a/libvirt/resource_libvirt_volume_test.go +++ b/libvirt/resource_libvirt_volume_test.go @@ -188,7 +188,7 @@ func TestAccLibvirtVolume_BackingStoreTestByName(t *testing.T) { name = "%s" base_volume_name = "${libvirt_volume.backing-%s.name}" pool = "${libvirt_pool.%s.name}" - } + } `, random, random, randomPoolPath, random, random, random, random, random, random, random), Check: resource.ComposeTestCheckFunc( testAccCheckLibvirtVolumeExists("libvirt_volume.backing-"+random, &volume), @@ -489,6 +489,134 @@ func TestAccLibvirtVolume_Import(t *testing.T) { }) } +func TestAccLibvirtVolume_ResizeShrink(t *testing.T) { + var volume libvirt.StorageVol + randomVolumeResource := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) + randomVolumeName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) + randomPoolName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) + randomPoolPath := "/tmp/terraform-provider-libvirt-pool-" + randomPoolName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + } + + resource "libvirt_volume" "%s" { + name = "%s" + format = "raw" + size = 1024 * 1024 + pool = "${libvirt_pool.%s.name}" + }`, randomPoolName, randomPoolName, randomPoolPath, randomVolumeResource, randomVolumeName, randomPoolName, + ), + ResourceName: "libvirt_volume." + randomVolumeResource, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibvirtVolumeExists("libvirt_volume."+randomVolumeResource, &volume), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "name", randomVolumeName), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "size", "1048576"), + ), + }, + { + Config: fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + } + + resource "libvirt_volume" "%s" { + name = "%s" + format = "raw" + size = 1024 + pool = "${libvirt_pool.%s.name}" + }`, randomPoolName, randomPoolName, randomPoolPath, randomVolumeResource, randomVolumeName, randomPoolName, + ), + ResourceName: "libvirt_volume." + randomVolumeResource, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibvirtVolumeExists("libvirt_volume."+randomVolumeResource, &volume), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "name", randomVolumeName), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "size", "1024"), + ), + }, + }, + }) +} + +func TestAccLibvirtVolume_ResizeExpand(t *testing.T) { + var volume libvirt.StorageVol + randomVolumeResource := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) + randomVolumeName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) + randomPoolName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) + randomPoolPath := "/tmp/terraform-provider-libvirt-pool-" + randomPoolName + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + } + + resource "libvirt_volume" "%s" { + name = "%s" + format = "raw" + size = 1024 * 1024 + pool = "${libvirt_pool.%s.name}" + }`, randomPoolName, randomPoolName, randomPoolPath, randomVolumeResource, randomVolumeName, randomPoolName, + ), + ResourceName: "libvirt_volume." + randomVolumeResource, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibvirtVolumeExists("libvirt_volume."+randomVolumeResource, &volume), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "name", randomVolumeName), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "size", "1048576"), + ), + }, + { + Config: fmt.Sprintf(` + resource "libvirt_pool" "%s" { + name = "%s" + type = "dir" + path = "%s" + } + + resource "libvirt_volume" "%s" { + name = "%s" + format = "raw" + size = 1024 * 1024 * 10 + pool = "${libvirt_pool.%s.name}" + }`, randomPoolName, randomPoolName, randomPoolPath, randomVolumeResource, randomVolumeName, randomPoolName, + ), + ResourceName: "libvirt_volume." + randomVolumeResource, + Check: resource.ComposeTestCheckFunc( + testAccCheckLibvirtVolumeExists("libvirt_volume."+randomVolumeResource, &volume), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "name", randomVolumeName), + resource.TestCheckResourceAttr( + "libvirt_volume."+randomVolumeResource, "size", "10485760"), + ), + }, + }, + }) +} + func testAccCheckLibvirtVolumeDestroy(state *terraform.State) error { virConn := testAccProvider.Meta().(*Client).libvirt for _, rs := range state.RootModule().Resources { diff --git a/libvirt/volume.go b/libvirt/volume.go index 0f1af1807..6298cd4d8 100644 --- a/libvirt/volume.go +++ b/libvirt/volume.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "time" libvirt "github.com/digitalocean/go-libvirt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" @@ -12,6 +13,9 @@ import ( const ( volumeStateConfNotExists = resourceStateConfNotExists volumeStateConfExists = resourceStateConfExists + volumeStateConfError = resourceStateConfError + volumeStateConfPending = resourceStateConfPending + volumeStateConfDone = resourceStateConfDone ) func volumeExistsStateRefreshFunc(virConn *libvirt.Libvirt, key string) retry.StateRefreshFunc { @@ -27,6 +31,20 @@ func volumeExistsStateRefreshFunc(virConn *libvirt.Libvirt, key string) retry.St } } +func volumeResizeDoneStateRefreshFunc(virConn *libvirt.Libvirt, volume libvirt.StorageVol, targetSize uint64) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + _, capacity, _, err := virConn.StorageVolGetInfo(volume) + if err != nil { + return virConn, resourceStateConfError, fmt.Errorf("failed to query volume '%s' info: %w", volume.Name, err) + } + + if capacity != targetSize { + return virConn, resourceStateConfPending, nil + } + return virConn, resourceStateConfDone, nil + } +} + func waitForStateVolumeExists(ctx context.Context, virConn *libvirt.Libvirt, key string) error { stateConf := &retry.StateChangeConf{ Pending: []string{volumeStateConfNotExists}, @@ -42,6 +60,21 @@ func waitForStateVolumeExists(ctx context.Context, virConn *libvirt.Libvirt, key return nil } +func waitForStateVolumeResizeDone(ctx context.Context, virConn *libvirt.Libvirt, volume libvirt.StorageVol, targetSize uint64) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{volumeStateConfPending}, + Target: []string{volumeStateConfDone}, + Refresh: volumeResizeDoneStateRefreshFunc(virConn, volume, targetSize), + Timeout: resourceStateTimeout, + MinTimeout: 1 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return err + } + return nil +} + // volumeDelete removes the volume identified by `key` from libvirt. func volumeDelete(ctx context.Context, client *Client, key string) error { virConn := client.libvirt @@ -111,3 +144,66 @@ func volumeDelete(ctx context.Context, client *Client, key string) error { } return nil } + +// volumeResizeCheck checks whether it is possible to increase the size of the provided volume by the given amount. +func volumeResizeCheck(client *Client, volume libvirt.StorageVol, pool libvirt.StoragePool, sizeIncrease uint64) error { + virConn := client.libvirt + + state, _, _, poolAvailable, err := virConn.StoragePoolGetInfo(pool) + if err != nil { + return fmt.Errorf("error retrieving info for storage pool '%s' : %w", pool.Name, err) + } + + if state != poolStateRunning { + return fmt.Errorf("the storage pool '%s' is in an invalid state (%d) for resizing", pool.Name, state) + } + + _, volumeCapacity, volumeAllocated, err := virConn.StorageVolGetInfo(volume) + if err != nil { + return fmt.Errorf("error retrieving info for volume '%s': %w", volume.Name, err) + } + log.Printf( + "[DEBUG] '%s' volume capacity=%d allocated=%d - %s pool available=%d - requested size increase=%d", + volume.Name, volumeCapacity, volumeAllocated, pool.Name, poolAvailable, sizeIncrease, + ) + + if sizeIncrease > poolAvailable { + return fmt.Errorf("not enough available space for storage pool '%s' to resize volume %s", pool.Name, volume.Name) + } + + return nil +} + +// volumeResize increases the size of the volume identified by `key' from the old to the new provided size +func volumeResize(ctx context.Context, client *Client, key string, oldSize, newSize uint64) error { + virConn := client.libvirt + + volume, err := virConn.StorageVolLookupByKey(key) + if err != nil { + return fmt.Errorf("volumeResize: Can't retrieve volume with key %s: %w", key, err) + } + + pool, err := virConn.StoragePoolLookupByName(volume.Pool) + if err != nil { + return fmt.Errorf("volumeResize: Failed to retrieve volume's storage pool %s: %w", volume.Pool, err) + } + + client.poolMutexKV.Lock(pool.Name) + defer client.poolMutexKV.Unlock(pool.Name) + + sizeDelta := newSize - oldSize + if err := volumeResizeCheck(client, volume, pool, sizeDelta); err != nil { + return fmt.Errorf("volumeResize: Failed while determining if the volume %s can be resized: %w", volume.Name, err) + } + + if err := virConn.StorageVolResize(volume, sizeDelta, libvirt.StorageVolResizeDelta); err != nil { + return fmt.Errorf("volumeResize: Failed to resize volume %s: %w", volume.Name, err) + } + + if err := waitForStateVolumeResizeDone(ctx, virConn, volume, newSize); err != nil { + return err + } + log.Printf("[INFO] The volume %s has been resized. Filesystem expansion might be necessary", volume.Name) + + return nil +}