diff --git a/libvirt/resource_libvirt_volume.go b/libvirt/resource_libvirt_volume.go index dbe897b95..a7c86a300 100644 --- a/libvirt/resource_libvirt_volume.go +++ b/libvirt/resource_libvirt_volume.go @@ -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, @@ -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 @@ -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") @@ -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()) @@ -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) @@ -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) } diff --git a/libvirt/resource_libvirt_volume_test.go b/libvirt/resource_libvirt_volume_test.go index cfe34fb30..ad2f13826 100644 --- a/libvirt/resource_libvirt_volume_test.go +++ b/libvirt/resource_libvirt_volume_test.go @@ -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) @@ -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. diff --git a/libvirt/volume.go b/libvirt/volume.go index 0f1af1807..62657909f 100644 --- a/libvirt/volume.go +++ b/libvirt/volume.go @@ -6,7 +6,9 @@ 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 ( @@ -14,6 +16,34 @@ const ( 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) @@ -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 +} diff --git a/website/docs/r/volume.html.markdown b/website/docs/r/volume.html.markdown index 8534f4d6b..9085f7650 100644 --- a/website/docs/r/volume.html.markdown +++ b/website/docs/r/volume.html.markdown @@ -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