Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions libvirt/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 42 additions & 2 deletions libvirt/resource_libvirt_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,7 +39,6 @@ func resourceLibvirtVolume() *schema.Resource {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"format": {
Type: schema.TypeString,
Expand Down Expand Up @@ -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
}
130 changes: 129 additions & 1 deletion libvirt/resource_libvirt_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down
96 changes: 96 additions & 0 deletions libvirt/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"time"

libvirt "github.com/digitalocean/go-libvirt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
Expand All @@ -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 {
Expand All @@ -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},
Expand All @@ -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
Expand Down Expand Up @@ -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
}