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
36 changes: 32 additions & 4 deletions libvirt/resource_libvirt_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ func resourceLibvirtVolume() *schema.Resource {
Optional: true,
ForceNew: true,
},
"base_volume_copy": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Default: false,
},
"xml": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -121,6 +127,7 @@ func resourceLibvirtVolumeCreate(ctx context.Context, d *schema.ResourceData, me
}

var img image
var baseVolume libvirt.StorageVol
// an source image was given, this mean we can't choose size
if source, ok := d.GetOk("source"); ok {
// source and size conflict
Expand Down Expand Up @@ -171,8 +178,6 @@ func resourceLibvirtVolumeCreate(ctx context.Context, d *schema.ResourceData, me

// first handle whether it has a backing image
// backing images can be specified by either (id), or by (name, pool)

var baseVolume libvirt.StorageVol
if baseVolumeID, ok := d.GetOk("base_volume_id"); ok {
if _, ok := d.GetOk("base_volume_name"); ok {
return diag.Errorf("'base_volume_name' can't be specified when also 'base_volume_id' is given")
Expand All @@ -195,11 +200,13 @@ func resourceLibvirtVolumeCreate(ctx context.Context, d *schema.ResourceData, me
if err != nil {
return diag.Errorf("can't retrieve base volume with name '%s': %s", baseVolumeName.(string), err)
}
} else if d.Get("base_volume_copy").(bool) {
diag.Errorf("'base_volume_id' or 'base_volume_name' must be specified when 'base_volume_copy' is set")
}

// FIXME - confirm test behaviour accurate
// if baseVolume != nil {
if baseVolume.Name != "" {
if !d.Get("base_volume_copy").(bool) && baseVolume.Name != "" {
backingStoreFragmentDef, err := newDefBackingStoreFromLibvirt(virConn, baseVolume)
if err != nil {
return diag.Errorf("could not retrieve backing store definition: %s", err.Error())
Expand Down Expand Up @@ -237,7 +244,12 @@ be smaller than the backing store specified with
return diag.Errorf("error applying XSLT stylesheet: %s", err)
}

volume, err := virConn.StorageVolCreateXML(pool, data, 0)
var volume libvirt.StorageVol
if d.Get("base_volume_copy").(bool) {
volume, err = virConn.StorageVolCreateXMLFrom(pool, data, baseVolume, 0)
} else {
volume, err = virConn.StorageVolCreateXML(pool, data, 0)
}
if err != nil {
if !isError(err, libvirt.ErrStorageVolExist) {
return diag.Errorf("error creating libvirt volume: %s", err)
Expand Down Expand Up @@ -266,6 +278,22 @@ be smaller than the backing store specified with
}
}

if requiresResize, err := volumeRequiresResize(virConn, d, volume, baseVolume, pool); err != nil {
errContext := ""
for _, d := range err {
errContext = errContext + ": " + d.Summary
}
log.Printf("[WARNING] Could not determine whether volume '%s' requires resize%s", volume.Name, errContext)
} else if requiresResize {
if size, ok := d.GetOk("size"); ok {
if err := virConn.StorageVolResize(volume, uint64(size.(int)), 0); err != nil {
return diag.Errorf("failed to resize volume '%s': %s", volume.Key, err)
} else {
log.Printf("[INFO] Volume '%s' successfully resized", volume.Key)
}
}
}

if err := waitForStateVolumeExists(ctx, virConn, volume.Key); err != nil {
return diag.FromErr(err)
}
Expand Down
179 changes: 179 additions & 0 deletions libvirt/resource_libvirt_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,88 @@ func testAccCheckLibvirtVolumeIsBackingStore(name string) resource.TestCheckFunc
}
}

func testAccCheckLibvirtVolumeDoesNotHaveBackingStore(name string) resource.TestCheckFunc {
return func(state *terraform.State) error {
virConn := testAccProvider.Meta().(*Client).libvirt

vol, err := getVolumeFromTerraformState(name, state, virConn)
if err != nil {
return err
}

volXMLDesc, err := virConn.StorageVolGetXMLDesc(*vol, 0)
if err != nil {
return fmt.Errorf("Error retrieving libvirt volume XML description: %w", err)
}

volumeDef := newDefVolume()
err = xml.Unmarshal([]byte(volXMLDesc), &volumeDef)
if err != nil {
return fmt.Errorf("Error reading libvirt volume XML description: %w", err)
}

if volumeDef.BackingStore != nil {
return fmt.Errorf("FAIL: volume has a backing store, but it shouldn't")
}

return nil
}
}

func testAccCheckLibvirtVolumeExpectedCapacity(name string, expectedCapacity uint64) resource.TestCheckFunc {
return func(state *terraform.State) error {
virConn := testAccProvider.Meta().(*Client).libvirt

vol, err := getVolumeFromTerraformState(name, state, virConn)
if err != nil {
return err
}

_, capacity, _, err := virConn.StorageVolGetInfo(*vol)
if err != nil {
return fmt.Errorf("Error retrieving libvirt volume info: %w", err)
}

if expectedCapacity != capacity {
return fmt.Errorf("FAIL: volume capacity is supposed to be %d bytes, but it's %d bytes", expectedCapacity, capacity)
}

return nil
}
}

func testAccCheckLibvirtVolumeExpectedFormat(name, expectedFormat string) resource.TestCheckFunc {
return func(state *terraform.State) error {
virConn := testAccProvider.Meta().(*Client).libvirt

vol, err := getVolumeFromTerraformState(name, state, virConn)
if err != nil {
return err
}

volXMLDesc, err := virConn.StorageVolGetXMLDesc(*vol, 0)
if err != nil {
return fmt.Errorf("Error retrieving libvirt volume XML description: %w", err)
}

volumeDef := newDefVolume()
err = xml.Unmarshal([]byte(volXMLDesc), &volumeDef)
if err != nil {
return fmt.Errorf("Error reading libvirt volume XML description: %w", err)
}

if volumeDef.Target == nil {
return fmt.Errorf("FAIL: volume XML description doesn't contain target element")
} else if volumeDef.Target.Format == nil {
return fmt.Errorf("FAIL: volume XML description doesn't contain target.format element")
} else if volumeDef.Target.Format.Type != expectedFormat {
return fmt.Errorf("FAIL: volume format is supposed to be '%s', but it is '%s'", expectedFormat, volumeDef.Target.Format.Type)
}

return nil
}
}

func TestAccLibvirtVolume_Basic(t *testing.T) {
var volume libvirt.StorageVol
randomVolumeResource := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
Expand Down Expand Up @@ -201,6 +283,103 @@ func TestAccLibvirtVolume_BackingStoreTestByName(t *testing.T) {
})
}

func TestAccLibvirtVolume_BackingStoreCopy(t *testing.T) {
randomStr := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
poolPath := "/tmp/terraform-provider-libvirt-pool-" + randomStr
var baseSize uint64 = 5 * 1024 * 1024
var copySize uint64 = 10 * 1024 * 1024
config := fmt.Sprintf(`
resource "libvirt_pool" "pool_%[1]s" {
name = "pool-%[1]s"
type = "dir"
path = "%[2]s"
}

resource "libvirt_volume" "base_raw_%[1]s" {
name = "base-raw-%[1]s"
format = "raw"
size = "%[3]d"
pool = "${libvirt_pool.pool_%[1]s.name}"
}

resource "libvirt_volume" "base_qcow2_%[1]s" {
name = "base-qcow2-%[1]s"
format = "qcow2"
size = "%[3]d"
pool = "${libvirt_pool.pool_%[1]s.name}"
}

resource "libvirt_volume" "copy_raw_from_raw_%[1]s" {
name = "copy-raw-from-raw-%[1]s"
format = "raw"
size = "%[4]d"
base_volume_copy = true
base_volume_id = "${libvirt_volume.base_raw_%[1]s.id}"
pool = "${libvirt_pool.pool_%[1]s.name}"
}

resource "libvirt_volume" "copy_raw_from_qcow2_%[1]s" {
name = "copy-raw-from-qcow2_%[1]s"
format = "raw"
size = "%[4]d"
base_volume_copy = true
base_volume_id = "${libvirt_volume.base_qcow2_%[1]s.id}"
pool = "${libvirt_pool.pool_%[1]s.name}"
}

resource "libvirt_volume" "copy_qcow2_from_raw_%[1]s" {
name = "copy-qcow2-from-raw-%[1]s"
format = "qcow2"
size = "%[4]d"
base_volume_copy = true
base_volume_id = "${libvirt_volume.base_raw_%[1]s.id}"
pool = "${libvirt_pool.pool_%[1]s.name}"
}

resource "libvirt_volume" "copy_qcow2_from_qcow2_%[1]s" {
name = "copy-qcow2-from-qcow2-%[1]s"
format = "qcow2"
size = "%[4]d"
base_volume_copy = true
base_volume_id = "${libvirt_volume.base_qcow2_%[1]s.id}"
pool = "${libvirt_pool.pool_%[1]s.name}"
}
`, randomStr, poolPath, baseSize, copySize)

volumes := map[string]string{
"copy_raw_from_raw_": "raw",
"copy_raw_from_qcow2_": "raw",
"copy_qcow2_from_raw_": "qcow2",
"copy_qcow2_from_qcow2_": "qcow2",
}
var volume libvirt.StorageVol
testCheckFuncs := []resource.TestCheckFunc{}
for baseName, format := range volumes {
fullName := "libvirt_volume." + baseName + randomStr
testCheckFuncs = append(testCheckFuncs,
testAccCheckLibvirtVolumeExists(fullName, &volume),
testAccCheckLibvirtVolumeDoesNotHaveBackingStore(fullName),
testAccCheckLibvirtVolumeExpectedCapacity(fullName, copySize),
testAccCheckLibvirtVolumeExpectedFormat(fullName, format),
)
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: resource.ComposeAggregateTestCheckFunc(
testAccCheckLibvirtVolumeDestroy,
testAccCheckLibvirtPoolDestroy,
),
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(testCheckFuncs...),
},
},
})
}

// The destroy function should always handle the case where the resource might already be destroyed
// (manually, for example). If the resource is already destroyed, this should not return an error.
// This allows Terraform users to manually delete resources without breaking Terraform.
Expand Down
83 changes: 83 additions & 0 deletions libvirt/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,44 @@ import (
"log"

libvirt "github.com/digitalocean/go-libvirt"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

const (
volumeStateConfNotExists = resourceStateConfNotExists
volumeStateConfExists = resourceStateConfExists
)

// UnitsMap is used for converting storage size units from xml representation into bytes
// https://pkg.go.dev/github.com/libvirt/libvirt-go-xml#StorageVolumeSize
// https://libvirt.org/formatstorage.html#storage-volume-general-metadata
//nolint: mnd
var UnitsMap map[string]uint64 = map[string]uint64{
"": 1,
"B": 1,
"bytes": 1,
"KB": 1000,
"K": 1024,
"KiB": 1024,
"MB": 1_000_000,
"M": 1_048_576,
"MiB": 1_048_576,
"GB": 1_000_000_000,
"G": 1_073_741_824,
"GiB": 1_073_741_824,
"TB": 1_000_000_000_000,
"T": 1_099_511_627_776,
"TiB": 1_099_511_627_776,
"PB": 1_000_000_000_000_000,
"P": 1_125_899_906_842_624,
"PiB": 1_125_899_906_842_624,
"EB": 1_000_000_000_000_000_000,
"E": 1_152_921_504_606_846_976,
"EiB": 1_152_921_504_606_846_976,
}

func volumeExistsStateRefreshFunc(virConn *libvirt.Libvirt, key string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
_, err := virConn.StorageVolLookupByKey(key)
Expand Down Expand Up @@ -111,3 +141,56 @@ func volumeDelete(ctx context.Context, client *Client, key string) error {
}
return nil
}

// volumeRequiresResize checks whether a volume needs resizing after being created with StorageVolCreateXMLFrom.
// StorageVolCreateXMLFrom may ignore requested volume capacity in some cases. For example when qcow2 is involved,
// libvirt clones the volume using `qemu-img convert` which creates a new volume with the same capacity as the original.
func volumeRequiresResize(
virConn *libvirt.Libvirt,
d *schema.ResourceData,
volume,
baseVolume libvirt.StorageVol,
volumePool libvirt.StoragePool,
) (bool, diag.Diagnostics) {
if !d.Get("base_volume_copy").(bool) {
return false, nil
}

size := d.Get("size")
if size == nil {
return false, nil
}

volumeXML, err := newDefVolumeFromLibvirt(virConn, volume)
if err != nil {
return false, diag.Errorf("could not get volume '%s' xml definition: %s", volume.Name, err)
}

baseVolumeXML, err := newDefVolumeFromLibvirt(virConn, baseVolume)
if err != nil {
return false, diag.Errorf("could not get volume '%s' xml definition: %s", baseVolume.Name, err)
}

// do not resize in case allocation > requested size. Happens when there is substantial metadata overhead
if volumeXML.Allocation == nil || size.(int) <= int(volumeXML.Allocation.Value*UnitsMap[volumeXML.Allocation.Unit]) {
return false, nil
}

if baseVolumeXML.Capacity == nil || size.(int) <= int(baseVolumeXML.Capacity.Value*UnitsMap[baseVolumeXML.Capacity.Unit]) {
return false, nil
}

if volumePoolXML, err := newDefPoolFromLibvirt(virConn, volumePool); err != nil {
return false, err
} else if volumePoolXML.Type != "dir" {
return false, nil
}

if volumeXML.Target != nil && volumeXML.Target.Format != nil && baseVolumeXML.Target != nil && baseVolumeXML.Target.Format != nil {
if volumeXML.Target.Format.Type == "qcow2" || baseVolumeXML.Target.Format.Type == "qcow2" {
return true, nil
}
}

return false, nil
}
6 changes: 6 additions & 0 deletions website/docs/r/volume.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ The following arguments are supported:
volume is going to be searched inside of `pool`.
* `base_volume_pool` - (Optional) The name of the storage pool containing the
volume defined by `base_volume_name`.
* `base_volume_copy` - (Optional) If set to `true`, the volume is created as a copy of its backing volume
(by calling [virStorageVolCreateXMLFrom()](https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolCreateXMLFrom)
instead of [virStorageVolCreateXML()](https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolCreateXML), similar to `virsh vol-create-from`).
The created volume has no association with its backing volume, neither in its XML definition nor in the underlying storage backend.
For **qcow2**, this means that the volume is a brand-new, regular **qcow2** image rather than a CoW overlay of its backing file.
For **LVM**, this means that the volume is a regular volume rather than a snapshot volume. Data is simply copied from a backing volume.

### Altering libvirt's generated volume XML definition

Expand Down
Loading