Skip to content

Commit 94e0dbb

Browse files
committed
feat: add base_volume_copy options of libvirt volume
If base_volume_copy option is set to false (default), a volume is created using StorageVolCreateXML(). Otherwise, StorageVolCreateXMLFrom() is used and the resulting volume doesn't have any association with the original.
1 parent 11dbcbf commit 94e0dbb

File tree

4 files changed

+299
-4
lines changed

4 files changed

+299
-4
lines changed

libvirt/resource_libvirt_volume.go

+32-4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ func resourceLibvirtVolume() *schema.Resource {
6060
Optional: true,
6161
ForceNew: true,
6262
},
63+
"base_volume_copy": {
64+
Type: schema.TypeBool,
65+
Optional: true,
66+
ForceNew: true,
67+
Default: false,
68+
},
6369
"xml": {
6470
Type: schema.TypeList,
6571
Optional: true,
@@ -121,6 +127,7 @@ func resourceLibvirtVolumeCreate(ctx context.Context, d *schema.ResourceData, me
121127
}
122128

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

172179
// first handle whether it has a backing image
173180
// backing images can be specified by either (id), or by (name, pool)
174-
175-
var baseVolume libvirt.StorageVol
176181
if baseVolumeID, ok := d.GetOk("base_volume_id"); ok {
177182
if _, ok := d.GetOk("base_volume_name"); ok {
178183
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
195200
if err != nil {
196201
return diag.Errorf("can't retrieve base volume with name '%s': %s", baseVolumeName.(string), err)
197202
}
203+
} else if d.Get("base_volume_copy").(bool) {
204+
diag.Errorf("'base_volume_id' or 'base_volume_name' must be specified when 'base_volume_copy' is set")
198205
}
199206

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

240-
volume, err := virConn.StorageVolCreateXML(pool, data, 0)
247+
var volume libvirt.StorageVol
248+
if d.Get("base_volume_copy").(bool) {
249+
volume, err = virConn.StorageVolCreateXMLFrom(pool, data, baseVolume, 0)
250+
} else {
251+
volume, err = virConn.StorageVolCreateXML(pool, data, 0)
252+
}
241253
if err != nil {
242254
if !isError(err, libvirt.ErrStorageVolExist) {
243255
return diag.Errorf("error creating libvirt volume: %s", err)
@@ -266,6 +278,22 @@ be smaller than the backing store specified with
266278
}
267279
}
268280

281+
if requiresResize, err := volumeRequiresResize(virConn, d, volume, baseVolume, pool); err != nil {
282+
errContext := ""
283+
for _, d := range err {
284+
errContext = errContext + ": " + d.Summary
285+
}
286+
log.Printf("[WARNING] Could not determine whether volume '%s' requires resize%s", volume.Name, errContext)
287+
} else if requiresResize {
288+
if size, ok := d.GetOk("size"); ok {
289+
if err := virConn.StorageVolResize(volume, uint64(size.(int)), 0); err != nil {
290+
return diag.Errorf("failed to resize volume '%s': %s", volume.Key, err)
291+
} else {
292+
log.Printf("[INFO] Volume '%s' successfully resized", volume.Key)
293+
}
294+
}
295+
}
296+
269297
if err := waitForStateVolumeExists(ctx, virConn, volume.Key); err != nil {
270298
return diag.FromErr(err)
271299
}

libvirt/resource_libvirt_volume_test.go

+179
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,88 @@ func testAccCheckLibvirtVolumeIsBackingStore(name string) resource.TestCheckFunc
8989
}
9090
}
9191

92+
func testAccCheckLibvirtVolumeDoesNotHaveBackingStore(name string) resource.TestCheckFunc {
93+
return func(state *terraform.State) error {
94+
virConn := testAccProvider.Meta().(*Client).libvirt
95+
96+
vol, err := getVolumeFromTerraformState(name, state, virConn)
97+
if err != nil {
98+
return err
99+
}
100+
101+
volXMLDesc, err := virConn.StorageVolGetXMLDesc(*vol, 0)
102+
if err != nil {
103+
return fmt.Errorf("Error retrieving libvirt volume XML description: %w", err)
104+
}
105+
106+
volumeDef := newDefVolume()
107+
err = xml.Unmarshal([]byte(volXMLDesc), &volumeDef)
108+
if err != nil {
109+
return fmt.Errorf("Error reading libvirt volume XML description: %w", err)
110+
}
111+
112+
if volumeDef.BackingStore != nil {
113+
return fmt.Errorf("FAIL: volume has a backing store, but it shouldn't")
114+
}
115+
116+
return nil
117+
}
118+
}
119+
120+
func testAccCheckLibvirtVolumeExpectedCapacity(name string, expectedCapacity uint64) resource.TestCheckFunc {
121+
return func(state *terraform.State) error {
122+
virConn := testAccProvider.Meta().(*Client).libvirt
123+
124+
vol, err := getVolumeFromTerraformState(name, state, virConn)
125+
if err != nil {
126+
return err
127+
}
128+
129+
_, capacity, _, err := virConn.StorageVolGetInfo(*vol)
130+
if err != nil {
131+
return fmt.Errorf("Error retrieving libvirt volume info: %w", err)
132+
}
133+
134+
if expectedCapacity != capacity {
135+
return fmt.Errorf("FAIL: volume capacity is supposed to be %d bytes, but it's %d bytes", expectedCapacity, capacity)
136+
}
137+
138+
return nil
139+
}
140+
}
141+
142+
func testAccCheckLibvirtVolumeExpectedFormat(name, expectedFormat string) resource.TestCheckFunc {
143+
return func(state *terraform.State) error {
144+
virConn := testAccProvider.Meta().(*Client).libvirt
145+
146+
vol, err := getVolumeFromTerraformState(name, state, virConn)
147+
if err != nil {
148+
return err
149+
}
150+
151+
volXMLDesc, err := virConn.StorageVolGetXMLDesc(*vol, 0)
152+
if err != nil {
153+
return fmt.Errorf("Error retrieving libvirt volume XML description: %w", err)
154+
}
155+
156+
volumeDef := newDefVolume()
157+
err = xml.Unmarshal([]byte(volXMLDesc), &volumeDef)
158+
if err != nil {
159+
return fmt.Errorf("Error reading libvirt volume XML description: %w", err)
160+
}
161+
162+
if volumeDef.Target == nil {
163+
return fmt.Errorf("FAIL: volume XML description doesn't contain target element")
164+
} else if volumeDef.Target.Format == nil {
165+
return fmt.Errorf("FAIL: volume XML description doesn't contain target.format element")
166+
} else if volumeDef.Target.Format.Type != expectedFormat {
167+
return fmt.Errorf("FAIL: volume format is supposed to be '%s', but it is '%s'", expectedFormat, volumeDef.Target.Format.Type)
168+
}
169+
170+
return nil
171+
}
172+
}
173+
92174
func TestAccLibvirtVolume_Basic(t *testing.T) {
93175
var volume libvirt.StorageVol
94176
randomVolumeResource := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
@@ -201,6 +283,103 @@ func TestAccLibvirtVolume_BackingStoreTestByName(t *testing.T) {
201283
})
202284
}
203285

286+
func TestAccLibvirtVolume_BackingStoreCopy(t *testing.T) {
287+
randomStr := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
288+
poolPath := "/tmp/terraform-provider-libvirt-pool-" + randomStr
289+
var baseSize uint64 = 5 * 1024 * 1024
290+
var copySize uint64 = 10 * 1024 * 1024
291+
config := fmt.Sprintf(`
292+
resource "libvirt_pool" "pool_%[1]s" {
293+
name = "pool-%[1]s"
294+
type = "dir"
295+
path = "%[2]s"
296+
}
297+
298+
resource "libvirt_volume" "base_raw_%[1]s" {
299+
name = "base-raw-%[1]s"
300+
format = "raw"
301+
size = "%[3]d"
302+
pool = "${libvirt_pool.pool_%[1]s.name}"
303+
}
304+
305+
resource "libvirt_volume" "base_qcow2_%[1]s" {
306+
name = "base-qcow2-%[1]s"
307+
format = "qcow2"
308+
size = "%[3]d"
309+
pool = "${libvirt_pool.pool_%[1]s.name}"
310+
}
311+
312+
resource "libvirt_volume" "copy_raw_from_raw_%[1]s" {
313+
name = "copy-raw-from-raw-%[1]s"
314+
format = "raw"
315+
size = "%[4]d"
316+
base_volume_copy = true
317+
base_volume_id = "${libvirt_volume.base_raw_%[1]s.id}"
318+
pool = "${libvirt_pool.pool_%[1]s.name}"
319+
}
320+
321+
resource "libvirt_volume" "copy_raw_from_qcow2_%[1]s" {
322+
name = "copy-raw-from-qcow2_%[1]s"
323+
format = "raw"
324+
size = "%[4]d"
325+
base_volume_copy = true
326+
base_volume_id = "${libvirt_volume.base_qcow2_%[1]s.id}"
327+
pool = "${libvirt_pool.pool_%[1]s.name}"
328+
}
329+
330+
resource "libvirt_volume" "copy_qcow2_from_raw_%[1]s" {
331+
name = "copy-qcow2-from-raw-%[1]s"
332+
format = "qcow2"
333+
size = "%[4]d"
334+
base_volume_copy = true
335+
base_volume_id = "${libvirt_volume.base_raw_%[1]s.id}"
336+
pool = "${libvirt_pool.pool_%[1]s.name}"
337+
}
338+
339+
resource "libvirt_volume" "copy_qcow2_from_qcow2_%[1]s" {
340+
name = "copy-qcow2-from-qcow2-%[1]s"
341+
format = "qcow2"
342+
size = "%[4]d"
343+
base_volume_copy = true
344+
base_volume_id = "${libvirt_volume.base_qcow2_%[1]s.id}"
345+
pool = "${libvirt_pool.pool_%[1]s.name}"
346+
}
347+
`, randomStr, poolPath, baseSize, copySize)
348+
349+
volumes := map[string]string{
350+
"copy_raw_from_raw_": "raw",
351+
"copy_raw_from_qcow2_": "raw",
352+
"copy_qcow2_from_raw_": "qcow2",
353+
"copy_qcow2_from_qcow2_": "qcow2",
354+
}
355+
var volume libvirt.StorageVol
356+
testCheckFuncs := []resource.TestCheckFunc{}
357+
for baseName, format := range volumes {
358+
fullName := "libvirt_volume." + baseName + randomStr
359+
testCheckFuncs = append(testCheckFuncs,
360+
testAccCheckLibvirtVolumeExists(fullName, &volume),
361+
testAccCheckLibvirtVolumeDoesNotHaveBackingStore(fullName),
362+
testAccCheckLibvirtVolumeExpectedCapacity(fullName, copySize),
363+
testAccCheckLibvirtVolumeExpectedFormat(fullName, format),
364+
)
365+
}
366+
367+
resource.Test(t, resource.TestCase{
368+
PreCheck: func() { testAccPreCheck(t) },
369+
Providers: testAccProviders,
370+
CheckDestroy: resource.ComposeAggregateTestCheckFunc(
371+
testAccCheckLibvirtVolumeDestroy,
372+
testAccCheckLibvirtPoolDestroy,
373+
),
374+
Steps: []resource.TestStep{
375+
{
376+
Config: config,
377+
Check: resource.ComposeTestCheckFunc(testCheckFuncs...),
378+
},
379+
},
380+
})
381+
}
382+
204383
// The destroy function should always handle the case where the resource might already be destroyed
205384
// (manually, for example). If the resource is already destroyed, this should not return an error.
206385
// This allows Terraform users to manually delete resources without breaking Terraform.

libvirt/volume.go

+82
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,43 @@ import (
66
"log"
77

88
libvirt "github.com/digitalocean/go-libvirt"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
910
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1012
)
1113

1214
const (
1315
volumeStateConfNotExists = resourceStateConfNotExists
1416
volumeStateConfExists = resourceStateConfExists
1517
)
1618

19+
// UnitsMap is used for converting storage size units from xml representation into bytes
20+
// https://pkg.go.dev/github.com/libvirt/libvirt-go-xml#StorageVolumeSize
21+
// https://libvirt.org/formatstorage.html#storage-volume-general-metadata
22+
var UnitsMap map[string]uint64 = map[string]uint64{
23+
"": 1,
24+
"B": 1,
25+
"bytes": 1,
26+
"KB": 1000,
27+
"K": 1024,
28+
"KiB": 1024,
29+
"MB": 1_000_000,
30+
"M": 1_048_576,
31+
"MiB": 1_048_576,
32+
"GB": 1_000_000_000,
33+
"G": 1_073_741_824,
34+
"GiB": 1_073_741_824,
35+
"TB": 1_000_000_000_000,
36+
"T": 1_099_511_627_776,
37+
"TiB": 1_099_511_627_776,
38+
"PB": 1_000_000_000_000_000,
39+
"P": 1_125_899_906_842_624,
40+
"PiB": 1_125_899_906_842_624,
41+
"EB": 1_000_000_000_000_000_000,
42+
"E": 1_152_921_504_606_846_976,
43+
"EiB": 1_152_921_504_606_846_976,
44+
}
45+
1746
func volumeExistsStateRefreshFunc(virConn *libvirt.Libvirt, key string) retry.StateRefreshFunc {
1847
return func() (interface{}, string, error) {
1948
_, err := virConn.StorageVolLookupByKey(key)
@@ -111,3 +140,56 @@ func volumeDelete(ctx context.Context, client *Client, key string) error {
111140
}
112141
return nil
113142
}
143+
144+
// volumeRequiresResize checks whether a volume needs resizing after being created with StorageVolCreateXMLFrom.
145+
// StorageVolCreateXMLFrom may ignore requested volume capacity in some cases. For example when qcow2 is involved,
146+
// libvirt clones the volume using `qemu-img convert` which creates a new volume with the same capacity as the original.
147+
func volumeRequiresResize(
148+
virConn *libvirt.Libvirt,
149+
d *schema.ResourceData,
150+
volume,
151+
baseVolume libvirt.StorageVol,
152+
volumePool libvirt.StoragePool,
153+
) (bool, diag.Diagnostics) {
154+
if !d.Get("base_volume_copy").(bool) {
155+
return false, nil
156+
}
157+
158+
size := d.Get("size")
159+
if size == nil {
160+
return false, nil
161+
}
162+
163+
volumeXML, err := newDefVolumeFromLibvirt(virConn, volume)
164+
if err != nil {
165+
return false, diag.Errorf("could not get volume '%s' xml definition: %s", volume.Name, err)
166+
}
167+
168+
baseVolumeXML, err := newDefVolumeFromLibvirt(virConn, baseVolume)
169+
if err != nil {
170+
return false, diag.Errorf("could not get volume '%s' xml definition: %s", baseVolume.Name, err)
171+
}
172+
173+
// do not resize in case allocation > requested size. Happens when there is substantial metadata overhead
174+
if volumeXML.Allocation == nil || size.(int) <= int(volumeXML.Allocation.Value*UnitsMap[volumeXML.Allocation.Unit]) {
175+
return false, nil
176+
}
177+
178+
if baseVolumeXML.Capacity == nil || size.(int) <= int(baseVolumeXML.Capacity.Value*UnitsMap[baseVolumeXML.Capacity.Unit]) {
179+
return false, nil
180+
}
181+
182+
if volumePoolXML, err := newDefPoolFromLibvirt(virConn, volumePool); err != nil {
183+
return false, err
184+
} else if volumePoolXML.Type != "dir" {
185+
return false, nil
186+
}
187+
188+
if volumeXML.Target != nil && volumeXML.Target.Format != nil && baseVolumeXML.Target != nil && baseVolumeXML.Target.Format != nil {
189+
if volumeXML.Target.Format.Type == "qcow2" || baseVolumeXML.Target.Format.Type == "qcow2" {
190+
return true, nil
191+
}
192+
}
193+
194+
return false, nil
195+
}

website/docs/r/volume.html.markdown

+6
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ The following arguments are supported:
6464
volume is going to be searched inside of `pool`.
6565
* `base_volume_pool` - (Optional) The name of the storage pool containing the
6666
volume defined by `base_volume_name`.
67+
* `base_volume_copy` - (Optional) If set to `true`, the volume is created as a copy of its backing volume
68+
(by calling [virStorageVolCreateXMLFrom()](https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolCreateXMLFrom)
69+
instead of [virStorageVolCreateXML()](https://libvirt.org/html/libvirt-libvirt-storage.html#virStorageVolCreateXML), similar to `virsh vol-create-from`).
70+
The created volume has no association with its backing volume, neither in its XML definition nor in the underlying storage backend.
71+
For **qcow2**, this means that the volume is a brand-new, regular **qcow2** image rather than a CoW overlay of its backing file.
72+
For **LVM**, this means that the volume is a regular volume rather than a snapshot volume. Data is simply copied from a backing volume.
6773

6874
### Altering libvirt's generated volume XML definition
6975

0 commit comments

Comments
 (0)