1// Package extensions contains common functions for creating block storage
2// resources that are extensions of the block storage API. See the `*_test.go`
3// files for example usages.
4package extensions
5
6import (
7	"fmt"
8	"strings"
9	"testing"
10
11	"github.com/gophercloud/gophercloud"
12	"github.com/gophercloud/gophercloud/acceptance/tools"
13	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/backups"
14	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
15	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
16	v3 "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
17	"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
18	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
19	th "github.com/gophercloud/gophercloud/testhelper"
20)
21
22// CreateUploadImage will upload volume it as volume-baked image. An name of new image or err will be
23// returned
24func CreateUploadImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (volumeactions.VolumeImage, error) {
25	if testing.Short() {
26		t.Skip("Skipping test that requires volume-backed image uploading in short mode.")
27	}
28
29	imageName := tools.RandomString("ACPTTEST", 16)
30	uploadImageOpts := volumeactions.UploadImageOpts{
31		ImageName: imageName,
32		Force:     true,
33	}
34
35	volumeImage, err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).Extract()
36	if err != nil {
37		return volumeImage, err
38	}
39
40	t.Logf("Uploading volume %s as volume-backed image %s", volume.ID, imageName)
41
42	if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil {
43		return volumeImage, err
44	}
45
46	t.Logf("Uploaded volume %s as volume-backed image %s", volume.ID, imageName)
47
48	return volumeImage, nil
49
50}
51
52// DeleteUploadedImage deletes uploaded image. An error will be returned
53// if the deletion request failed.
54func DeleteUploadedImage(t *testing.T, client *gophercloud.ServiceClient, imageID string) error {
55	if testing.Short() {
56		t.Skip("Skipping test that requires volume-backed image removing in short mode.")
57	}
58
59	t.Logf("Removing image %s", imageID)
60
61	err := images.Delete(client, imageID).ExtractErr()
62	if err != nil {
63		return err
64	}
65
66	return nil
67}
68
69// CreateVolumeAttach will attach a volume to an instance. An error will be
70// returned if the attachment failed.
71func CreateVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, server *servers.Server) error {
72	if testing.Short() {
73		t.Skip("Skipping test that requires volume attachment in short mode.")
74	}
75
76	attachOpts := volumeactions.AttachOpts{
77		MountPoint:   "/mnt",
78		Mode:         "rw",
79		InstanceUUID: server.ID,
80	}
81
82	t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID)
83
84	if err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr(); err != nil {
85		return err
86	}
87
88	if err := volumes.WaitForStatus(client, volume.ID, "in-use", 60); err != nil {
89		return err
90	}
91
92	t.Logf("Attached volume %s to server %s", volume.ID, server.ID)
93
94	return nil
95}
96
97// CreateVolumeReserve creates a volume reservation. An error will be returned
98// if the reservation failed.
99func CreateVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
100	if testing.Short() {
101		t.Skip("Skipping test that requires volume reservation in short mode.")
102	}
103
104	t.Logf("Attempting to reserve volume %s", volume.ID)
105
106	if err := volumeactions.Reserve(client, volume.ID).ExtractErr(); err != nil {
107		return err
108	}
109
110	t.Logf("Reserved volume %s", volume.ID)
111
112	return nil
113}
114
115// DeleteVolumeAttach will detach a volume from an instance. A fatal error will
116// occur if the snapshot failed to be deleted. This works best when used as a
117// deferred function.
118func DeleteVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
119	t.Logf("Attepting to detach volume volume: %s", volume.ID)
120
121	detachOpts := volumeactions.DetachOpts{
122		AttachmentID: volume.Attachments[0].AttachmentID,
123	}
124
125	if err := volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr(); err != nil {
126		t.Fatalf("Unable to detach volume %s: %v", volume.ID, err)
127	}
128
129	if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil {
130		t.Fatalf("Volume %s failed to become unavailable in 60 seconds: %v", volume.ID, err)
131	}
132
133	t.Logf("Detached volume: %s", volume.ID)
134}
135
136// DeleteVolumeReserve deletes a volume reservation. A fatal error will occur
137// if the deletion request failed. This works best when used as a deferred
138// function.
139func DeleteVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
140	if testing.Short() {
141		t.Skip("Skipping test that requires volume reservation in short mode.")
142	}
143
144	t.Logf("Attempting to unreserve volume %s", volume.ID)
145
146	if err := volumeactions.Unreserve(client, volume.ID).ExtractErr(); err != nil {
147		t.Fatalf("Unable to unreserve volume %s: %v", volume.ID, err)
148	}
149
150	t.Logf("Unreserved volume %s", volume.ID)
151}
152
153// ExtendVolumeSize will extend the size of a volume.
154func ExtendVolumeSize(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
155	t.Logf("Attempting to extend the size of volume %s", volume.ID)
156
157	extendOpts := volumeactions.ExtendSizeOpts{
158		NewSize: 2,
159	}
160
161	err := volumeactions.ExtendSize(client, volume.ID, extendOpts).ExtractErr()
162	if err != nil {
163		return err
164	}
165
166	if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil {
167		return err
168	}
169
170	return nil
171}
172
173// SetImageMetadata will apply the metadata to a volume.
174func SetImageMetadata(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
175	t.Logf("Attempting to apply image metadata to volume %s", volume.ID)
176
177	imageMetadataOpts := volumeactions.ImageMetadataOpts{
178		Metadata: map[string]string{
179			"image_name": "testimage",
180		},
181	}
182
183	err := volumeactions.SetImageMetadata(client, volume.ID, imageMetadataOpts).ExtractErr()
184	if err != nil {
185		return err
186	}
187
188	return nil
189}
190
191// CreateBackup will create a backup based on a volume. An error will be
192// will be returned if the backup could not be created.
193func CreateBackup(t *testing.T, client *gophercloud.ServiceClient, volumeID string) (*backups.Backup, error) {
194	t.Logf("Attempting to create a backup of volume %s", volumeID)
195
196	backupName := tools.RandomString("ACPTTEST", 16)
197	createOpts := backups.CreateOpts{
198		VolumeID: volumeID,
199		Name:     backupName,
200	}
201
202	backup, err := backups.Create(client, createOpts).Extract()
203	if err != nil {
204		return nil, err
205	}
206
207	err = WaitForBackupStatus(client, backup.ID, "available", 120)
208	if err != nil {
209		return nil, err
210	}
211
212	backup, err = backups.Get(client, backup.ID).Extract()
213	if err != nil {
214		return nil, err
215	}
216
217	t.Logf("Successfully created backup %s", backup.ID)
218	tools.PrintResource(t, backup)
219
220	th.AssertEquals(t, backup.Name, backupName)
221
222	return backup, nil
223}
224
225// DeleteBackup will delete a backup. A fatal error will occur if the backup
226// could not be deleted. This works best when used as a deferred function.
227func DeleteBackup(t *testing.T, client *gophercloud.ServiceClient, backupID string) {
228	if err := backups.Delete(client, backupID).ExtractErr(); err != nil {
229		t.Fatalf("Unable to delete backup %s: %s", backupID, err)
230	}
231
232	t.Logf("Deleted backup %s", backupID)
233}
234
235// WaitForBackupStatus will continually poll a backup, checking for a particular
236// status. It will do this for the amount of seconds defined.
237func WaitForBackupStatus(client *gophercloud.ServiceClient, id, status string, secs int) error {
238	return gophercloud.WaitFor(secs, func() (bool, error) {
239		current, err := backups.Get(client, id).Extract()
240		if err != nil {
241			return false, err
242		}
243
244		if current.Status == status {
245			return true, nil
246		}
247
248		return false, nil
249	})
250}
251
252// SetBootable will set a bootable status to a volume.
253func SetBootable(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
254	t.Logf("Attempting to apply bootable status to volume %s", volume.ID)
255
256	bootableOpts := volumeactions.BootableOpts{
257		Bootable: true,
258	}
259
260	err := volumeactions.SetBootable(client, volume.ID, bootableOpts).ExtractErr()
261	if err != nil {
262		return err
263	}
264
265	vol, err := v3.Get(client, volume.ID).Extract()
266	if err != nil {
267		return err
268	}
269
270	if strings.ToLower(vol.Bootable) != "true" {
271		return fmt.Errorf("Volume bootable status is %q, expected 'true'", vol.Bootable)
272	}
273
274	bootableOpts = volumeactions.BootableOpts{
275		Bootable: false,
276	}
277
278	err = volumeactions.SetBootable(client, volume.ID, bootableOpts).ExtractErr()
279	if err != nil {
280		return err
281	}
282
283	vol, err = v3.Get(client, volume.ID).Extract()
284	if err != nil {
285		return err
286	}
287
288	if strings.ToLower(vol.Bootable) == "true" {
289		return fmt.Errorf("Volume bootable status is %q, expected 'false'", vol.Bootable)
290	}
291
292	return nil
293}
294