github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/internal/acceptance/openstack/blockstorage/v3/blockstorage.go (about)

     1  // Package v3 contains common functions for creating block storage based
     2  // resources for use in acceptance tests. See the `*_test.go` files for
     3  // example usages.
     4  package v3
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/http"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/vnpaycloud-console/gophercloud/v2"
    15  	"github.com/vnpaycloud-console/gophercloud/v2/internal/acceptance/tools"
    16  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/blockstorage/v3/backups"
    17  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/blockstorage/v3/qos"
    18  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/blockstorage/v3/snapshots"
    19  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/blockstorage/v3/volumes"
    20  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/blockstorage/v3/volumetypes"
    21  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/compute/v2/servers"
    22  	"github.com/vnpaycloud-console/gophercloud/v2/openstack/image/v2/images"
    23  	th "github.com/vnpaycloud-console/gophercloud/v2/testhelper"
    24  )
    25  
    26  // CreateSnapshot will create a snapshot of the specified volume.
    27  // Snapshot will be assigned a random name and description.
    28  func CreateSnapshot(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (*snapshots.Snapshot, error) {
    29  	snapshotName := tools.RandomString("ACPTTEST", 16)
    30  	snapshotDescription := tools.RandomString("ACPTTEST", 16)
    31  	t.Logf("Attempting to create snapshot: %s", snapshotName)
    32  
    33  	createOpts := snapshots.CreateOpts{
    34  		VolumeID:    volume.ID,
    35  		Name:        snapshotName,
    36  		Description: snapshotDescription,
    37  	}
    38  
    39  	snapshot, err := snapshots.Create(context.TODO(), client, createOpts).Extract()
    40  	if err != nil {
    41  		return snapshot, err
    42  	}
    43  
    44  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
    45  	defer cancel()
    46  
    47  	err = snapshots.WaitForStatus(ctx, client, snapshot.ID, "available")
    48  	if err != nil {
    49  		return snapshot, err
    50  	}
    51  
    52  	snapshot, err = snapshots.Get(context.TODO(), client, snapshot.ID).Extract()
    53  	if err != nil {
    54  		return snapshot, err
    55  	}
    56  
    57  	tools.PrintResource(t, snapshot)
    58  	th.AssertEquals(t, snapshot.Name, snapshotName)
    59  	th.AssertEquals(t, snapshot.VolumeID, volume.ID)
    60  
    61  	t.Logf("Successfully created snapshot: %s", snapshot.ID)
    62  
    63  	return snapshot, nil
    64  }
    65  
    66  // CreateVolume will create a volume with a random name and size of 1GB. An
    67  // error will be returned if the volume was unable to be created.
    68  func CreateVolume(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) {
    69  	volumeName := tools.RandomString("ACPTTEST", 16)
    70  	volumeDescription := tools.RandomString("ACPTTEST-DESC", 16)
    71  	t.Logf("Attempting to create volume: %s", volumeName)
    72  
    73  	createOpts := volumes.CreateOpts{
    74  		Size:        1,
    75  		Name:        volumeName,
    76  		Description: volumeDescription,
    77  	}
    78  
    79  	volume, err := volumes.Create(context.TODO(), client, createOpts, nil).Extract()
    80  	if err != nil {
    81  		return volume, err
    82  	}
    83  
    84  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
    85  	defer cancel()
    86  
    87  	err = volumes.WaitForStatus(ctx, client, volume.ID, "available")
    88  	if err != nil {
    89  		return volume, err
    90  	}
    91  
    92  	volume, err = volumes.Get(context.TODO(), client, volume.ID).Extract()
    93  	if err != nil {
    94  		return volume, err
    95  	}
    96  
    97  	tools.PrintResource(t, volume)
    98  	th.AssertEquals(t, volume.Name, volumeName)
    99  	th.AssertEquals(t, volume.Description, volumeDescription)
   100  	th.AssertEquals(t, volume.Size, 1)
   101  
   102  	t.Logf("Successfully created volume: %s", volume.ID)
   103  
   104  	return volume, nil
   105  }
   106  
   107  // CreateVolumeWithType will create a volume of the given volume type
   108  // with a random name and size of 1GB. An error will be returned if
   109  // the volume was unable to be created.
   110  func CreateVolumeWithType(t *testing.T, client *gophercloud.ServiceClient, vt *volumetypes.VolumeType) (*volumes.Volume, error) {
   111  	volumeName := tools.RandomString("ACPTTEST", 16)
   112  	volumeDescription := tools.RandomString("ACPTTEST-DESC", 16)
   113  	t.Logf("Attempting to create volume: %s", volumeName)
   114  
   115  	createOpts := volumes.CreateOpts{
   116  		Size:        1,
   117  		Name:        volumeName,
   118  		Description: volumeDescription,
   119  		VolumeType:  vt.Name,
   120  	}
   121  
   122  	volume, err := volumes.Create(context.TODO(), client, createOpts, nil).Extract()
   123  	if err != nil {
   124  		return volume, err
   125  	}
   126  
   127  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   128  	defer cancel()
   129  
   130  	err = volumes.WaitForStatus(ctx, client, volume.ID, "available")
   131  	if err != nil {
   132  		return volume, err
   133  	}
   134  
   135  	volume, err = volumes.Get(context.TODO(), client, volume.ID).Extract()
   136  	if err != nil {
   137  		return volume, err
   138  	}
   139  
   140  	tools.PrintResource(t, volume)
   141  	th.AssertEquals(t, volume.Name, volumeName)
   142  	th.AssertEquals(t, volume.Description, volumeDescription)
   143  	th.AssertEquals(t, volume.Size, 1)
   144  	th.AssertEquals(t, volume.VolumeType, vt.Name)
   145  
   146  	t.Logf("Successfully created volume: %s", volume.ID)
   147  
   148  	return volume, nil
   149  }
   150  
   151  // CreateVolumeType will create a volume type with a random name. An
   152  // error will be returned if the volume was unable to be created.
   153  func CreateVolumeType(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) {
   154  	name := tools.RandomString("ACPTTEST", 16)
   155  	description := "create_from_gophercloud"
   156  	t.Logf("Attempting to create volume type: %s", name)
   157  
   158  	createOpts := volumetypes.CreateOpts{
   159  		Name:        name,
   160  		ExtraSpecs:  map[string]string{"volume_backend_name": "fake_backend_name"},
   161  		Description: description,
   162  	}
   163  
   164  	vt, err := volumetypes.Create(context.TODO(), client, createOpts).Extract()
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	tools.PrintResource(t, vt)
   170  	th.AssertEquals(t, vt.IsPublic, true)
   171  	th.AssertEquals(t, vt.Name, name)
   172  	th.AssertEquals(t, vt.Description, description)
   173  	// TODO: For some reason returned extra_specs are empty even in API reference: https://developer.openstack.org/api-ref/block-storage/v3/?expanded=create-a-volume-type-detail#volume-types-types
   174  	// "extra_specs": {}
   175  	// th.AssertEquals(t, vt.ExtraSpecs, createOpts.ExtraSpecs)
   176  
   177  	t.Logf("Successfully created volume type: %s", vt.ID)
   178  
   179  	return vt, nil
   180  }
   181  
   182  // CreateVolumeTypeNoExtraSpecs will create a volume type with a random name and
   183  // no extra specs. This is required to bypass cinder-scheduler filters and be able
   184  // to create a volume with this volumeType. An error will be returned if the volume
   185  // type was unable to be created.
   186  func CreateVolumeTypeNoExtraSpecs(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) {
   187  	name := tools.RandomString("ACPTTEST", 16)
   188  	description := "create_from_gophercloud"
   189  	t.Logf("Attempting to create volume type: %s", name)
   190  
   191  	createOpts := volumetypes.CreateOpts{
   192  		Name:        name,
   193  		ExtraSpecs:  map[string]string{},
   194  		Description: description,
   195  	}
   196  
   197  	vt, err := volumetypes.Create(context.TODO(), client, createOpts).Extract()
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	tools.PrintResource(t, vt)
   203  	th.AssertEquals(t, vt.IsPublic, true)
   204  	th.AssertEquals(t, vt.Name, name)
   205  	th.AssertEquals(t, vt.Description, description)
   206  
   207  	t.Logf("Successfully created volume type: %s", vt.ID)
   208  
   209  	return vt, nil
   210  }
   211  
   212  // CreateVolumeTypeMultiAttach will create a volume type with a random name and
   213  // extra specs for multi-attach. An error will be returned if the volume type was
   214  // unable to be created.
   215  func CreateVolumeTypeMultiAttach(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) {
   216  	name := tools.RandomString("ACPTTEST", 16)
   217  	description := "create_from_gophercloud"
   218  	t.Logf("Attempting to create volume type: %s", name)
   219  
   220  	createOpts := volumetypes.CreateOpts{
   221  		Name:        name,
   222  		ExtraSpecs:  map[string]string{"multiattach": "<is> True"},
   223  		Description: description,
   224  	}
   225  
   226  	vt, err := volumetypes.Create(context.TODO(), client, createOpts).Extract()
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	tools.PrintResource(t, vt)
   232  	th.AssertEquals(t, vt.IsPublic, true)
   233  	th.AssertEquals(t, vt.Name, name)
   234  	th.AssertEquals(t, vt.Description, description)
   235  	th.AssertEquals(t, vt.ExtraSpecs["multiattach"], "<is> True")
   236  
   237  	t.Logf("Successfully created volume type: %s", vt.ID)
   238  
   239  	return vt, nil
   240  }
   241  
   242  // CreatePrivateVolumeType will create a private volume type with a random
   243  // name and no extra specs. An error will be returned if the volume type was
   244  // unable to be created.
   245  func CreatePrivateVolumeType(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) {
   246  	name := tools.RandomString("ACPTTEST", 16)
   247  	description := "create_from_gophercloud"
   248  	isPublic := false
   249  	t.Logf("Attempting to create volume type: %s", name)
   250  
   251  	createOpts := volumetypes.CreateOpts{
   252  		Name:        name,
   253  		ExtraSpecs:  map[string]string{},
   254  		Description: description,
   255  		IsPublic:    &isPublic,
   256  	}
   257  
   258  	vt, err := volumetypes.Create(context.TODO(), client, createOpts).Extract()
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	tools.PrintResource(t, vt)
   264  	th.AssertEquals(t, vt.IsPublic, false)
   265  	th.AssertEquals(t, vt.Name, name)
   266  	th.AssertEquals(t, vt.Description, description)
   267  
   268  	t.Logf("Successfully created volume type: %s", vt.ID)
   269  
   270  	return vt, nil
   271  }
   272  
   273  // DeleteSnapshot will delete a snapshot. A fatal error will occur if the
   274  // snapshot failed to be deleted.
   275  func DeleteSnapshot(t *testing.T, client *gophercloud.ServiceClient, snapshot *snapshots.Snapshot) {
   276  	err := snapshots.Delete(context.TODO(), client, snapshot.ID).ExtractErr()
   277  	if err != nil {
   278  		if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   279  			t.Logf("Snapshot %s is already deleted", snapshot.ID)
   280  			return
   281  		}
   282  		t.Fatalf("Unable to delete snapshot %s: %+v", snapshot.ID, err)
   283  	}
   284  
   285  	// Volumes can't be deleted until their snapshots have been,
   286  	// so block until the snapshoth as been deleted.
   287  	err = tools.WaitFor(func(ctx context.Context) (bool, error) {
   288  		_, err := snapshots.Get(ctx, client, snapshot.ID).Extract()
   289  		if err != nil {
   290  			return true, nil
   291  		}
   292  
   293  		return false, nil
   294  	})
   295  	if err != nil {
   296  		t.Fatalf("Error waiting for snapshot to delete: %v", err)
   297  	}
   298  
   299  	t.Logf("Deleted snapshot: %s", snapshot.ID)
   300  }
   301  
   302  // DeleteVolume will delete a volume. A fatal error will occur if the volume
   303  // failed to be deleted. This works best when used as a deferred function.
   304  func DeleteVolume(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
   305  	t.Logf("Attempting to delete volume: %s", volume.ID)
   306  
   307  	err := volumes.Delete(context.TODO(), client, volume.ID, volumes.DeleteOpts{}).ExtractErr()
   308  	if err != nil {
   309  		if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   310  			t.Logf("Volume %s is already deleted", volume.ID)
   311  			return
   312  		}
   313  		t.Fatalf("Unable to delete volume %s: %v", volume.ID, err)
   314  	}
   315  
   316  	// VolumeTypes can't be deleted until their volumes have been,
   317  	// so block until the volume is deleted.
   318  	err = tools.WaitFor(func(ctx context.Context) (bool, error) {
   319  		_, err := volumes.Get(ctx, client, volume.ID).Extract()
   320  		if err != nil {
   321  			return true, nil
   322  		}
   323  
   324  		return false, nil
   325  	})
   326  	if err != nil {
   327  		t.Fatalf("Error waiting for volume to delete: %v", err)
   328  	}
   329  
   330  	t.Logf("Successfully deleted volume: %s", volume.ID)
   331  }
   332  
   333  // DeleteVolumeType will delete a volume type. A fatal error will occur if the
   334  // volume type failed to be deleted. This works best when used as a deferred
   335  // function.
   336  func DeleteVolumeType(t *testing.T, client *gophercloud.ServiceClient, vt *volumetypes.VolumeType) {
   337  	t.Logf("Attempting to delete volume type: %s", vt.ID)
   338  
   339  	err := volumetypes.Delete(context.TODO(), client, vt.ID).ExtractErr()
   340  	if err != nil {
   341  		t.Fatalf("Unable to delete volume type %s: %v", vt.ID, err)
   342  	}
   343  
   344  	t.Logf("Successfully deleted volume type: %s", vt.ID)
   345  }
   346  
   347  // CreateQoS will create a QoS with one spec and a random name. An
   348  // error will be returned if the volume was unable to be created.
   349  func CreateQoS(t *testing.T, client *gophercloud.ServiceClient) (*qos.QoS, error) {
   350  	name := tools.RandomString("ACPTTEST", 16)
   351  	t.Logf("Attempting to create QoS: %s", name)
   352  
   353  	createOpts := qos.CreateOpts{
   354  		Name:     name,
   355  		Consumer: qos.ConsumerFront,
   356  		Specs: map[string]string{
   357  			"read_iops_sec": "20000",
   358  		},
   359  	}
   360  
   361  	qs, err := qos.Create(context.TODO(), client, createOpts).Extract()
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	tools.PrintResource(t, qs)
   367  	th.AssertEquals(t, qs.Consumer, "front-end")
   368  	th.AssertEquals(t, qs.Name, name)
   369  	th.AssertDeepEquals(t, qs.Specs, createOpts.Specs)
   370  
   371  	t.Logf("Successfully created QoS: %s", qs.ID)
   372  
   373  	return qs, nil
   374  }
   375  
   376  // DeleteQoS will delete a QoS. A fatal error will occur if the QoS
   377  // failed to be deleted. This works best when used as a deferred function.
   378  func DeleteQoS(t *testing.T, client *gophercloud.ServiceClient, qs *qos.QoS) {
   379  	t.Logf("Attempting to delete QoS: %s", qs.ID)
   380  
   381  	deleteOpts := qos.DeleteOpts{
   382  		Force: true,
   383  	}
   384  
   385  	err := qos.Delete(context.TODO(), client, qs.ID, deleteOpts).ExtractErr()
   386  	if err != nil {
   387  		t.Fatalf("Unable to delete QoS %s: %v", qs.ID, err)
   388  	}
   389  
   390  	t.Logf("Successfully deleted QoS: %s", qs.ID)
   391  }
   392  
   393  // CreateBackup will create a backup based on a volume. An error will be
   394  // will be returned if the backup could not be created.
   395  func CreateBackup(t *testing.T, client *gophercloud.ServiceClient, volumeID string) (*backups.Backup, error) {
   396  	t.Logf("Attempting to create a backup of volume %s", volumeID)
   397  
   398  	backupName := tools.RandomString("ACPTTEST", 16)
   399  	createOpts := backups.CreateOpts{
   400  		VolumeID: volumeID,
   401  		Name:     backupName,
   402  	}
   403  
   404  	backup, err := backups.Create(context.TODO(), client, createOpts).Extract()
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  
   409  	err = WaitForBackupStatus(client, backup.ID, "available")
   410  	if err != nil {
   411  		return nil, err
   412  	}
   413  
   414  	backup, err = backups.Get(context.TODO(), client, backup.ID).Extract()
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	t.Logf("Successfully created backup %s", backup.ID)
   420  	tools.PrintResource(t, backup)
   421  
   422  	th.AssertEquals(t, backup.Name, backupName)
   423  
   424  	return backup, nil
   425  }
   426  
   427  // DeleteBackup will delete a backup. A fatal error will occur if the backup
   428  // could not be deleted. This works best when used as a deferred function.
   429  func DeleteBackup(t *testing.T, client *gophercloud.ServiceClient, backupID string) {
   430  	if err := backups.Delete(context.TODO(), client, backupID).ExtractErr(); err != nil {
   431  		if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   432  			t.Logf("Backup %s is already deleted", backupID)
   433  			return
   434  		}
   435  		t.Fatalf("Unable to delete backup %s: %s", backupID, err)
   436  	}
   437  
   438  	t.Logf("Deleted backup %s", backupID)
   439  }
   440  
   441  // WaitForBackupStatus will continually poll a backup, checking for a particular
   442  // status. It will do this for the amount of seconds defined.
   443  func WaitForBackupStatus(client *gophercloud.ServiceClient, id, status string) error {
   444  	return tools.WaitFor(func(ctx context.Context) (bool, error) {
   445  		current, err := backups.Get(ctx, client, id).Extract()
   446  		if err != nil {
   447  			if gophercloud.ResponseCodeIs(err, http.StatusNotFound) && status == "deleted" {
   448  				return true, nil
   449  			}
   450  			return false, err
   451  		}
   452  
   453  		if current.Status == status {
   454  			return true, nil
   455  		}
   456  
   457  		return false, nil
   458  	})
   459  }
   460  
   461  // ResetBackupStatus will reset the status of a backup.
   462  func ResetBackupStatus(t *testing.T, client *gophercloud.ServiceClient, backup *backups.Backup, status string) error {
   463  	t.Logf("Attempting to reset the status of backup %s from %s to %s", backup.ID, backup.Status, status)
   464  
   465  	resetOpts := backups.ResetStatusOpts{
   466  		Status: status,
   467  	}
   468  	err := backups.ResetStatus(context.TODO(), client, backup.ID, resetOpts).ExtractErr()
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	return WaitForBackupStatus(client, backup.ID, status)
   474  }
   475  
   476  // CreateUploadImage will upload volume it as volume-baked image. An name of new image or err will be
   477  // returned
   478  func CreateUploadImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (volumes.VolumeImage, error) {
   479  	if testing.Short() {
   480  		t.Skip("Skipping test that requires volume-backed image uploading in short mode.")
   481  	}
   482  
   483  	imageName := tools.RandomString("ACPTTEST", 16)
   484  	uploadImageOpts := volumes.UploadImageOpts{
   485  		ImageName: imageName,
   486  		Force:     true,
   487  	}
   488  
   489  	volumeImage, err := volumes.UploadImage(context.TODO(), client, volume.ID, uploadImageOpts).Extract()
   490  	if err != nil {
   491  		return volumeImage, err
   492  	}
   493  
   494  	t.Logf("Uploading volume %s as volume-backed image %s", volume.ID, imageName)
   495  
   496  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   497  	defer cancel()
   498  
   499  	if err := volumes.WaitForStatus(ctx, client, volume.ID, "available"); err != nil {
   500  		return volumeImage, err
   501  	}
   502  
   503  	t.Logf("Uploaded volume %s as volume-backed image %s", volume.ID, imageName)
   504  
   505  	return volumeImage, nil
   506  }
   507  
   508  // DeleteUploadedImage deletes uploaded image. An error will be returned
   509  // if the deletion request failed.
   510  func DeleteUploadedImage(t *testing.T, client *gophercloud.ServiceClient, imageID string) error {
   511  	if testing.Short() {
   512  		t.Skip("Skipping test that requires volume-backed image removing in short mode.")
   513  	}
   514  
   515  	t.Logf("Removing image %s", imageID)
   516  
   517  	err := images.Delete(context.TODO(), client, imageID).ExtractErr()
   518  	if err != nil {
   519  		return err
   520  	}
   521  
   522  	return nil
   523  }
   524  
   525  // CreateVolumeAttach will attach a volume to an instance. An error will be
   526  // returned if the attachment failed.
   527  func CreateVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, server *servers.Server) error {
   528  	if testing.Short() {
   529  		t.Skip("Skipping test that requires volume attachment in short mode.")
   530  	}
   531  
   532  	attachOpts := volumes.AttachOpts{
   533  		MountPoint:   "/mnt",
   534  		Mode:         "rw",
   535  		InstanceUUID: server.ID,
   536  	}
   537  
   538  	t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID)
   539  
   540  	if err := volumes.Attach(context.TODO(), client, volume.ID, attachOpts).ExtractErr(); err != nil {
   541  		return err
   542  	}
   543  
   544  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   545  	defer cancel()
   546  
   547  	if err := volumes.WaitForStatus(ctx, client, volume.ID, "in-use"); err != nil {
   548  		return err
   549  	}
   550  
   551  	t.Logf("Attached volume %s to server %s", volume.ID, server.ID)
   552  
   553  	return nil
   554  }
   555  
   556  // CreateVolumeReserve creates a volume reservation. An error will be returned
   557  // if the reservation failed.
   558  func CreateVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
   559  	if testing.Short() {
   560  		t.Skip("Skipping test that requires volume reservation in short mode.")
   561  	}
   562  
   563  	t.Logf("Attempting to reserve volume %s", volume.ID)
   564  
   565  	if err := volumes.Reserve(context.TODO(), client, volume.ID).ExtractErr(); err != nil {
   566  		return err
   567  	}
   568  
   569  	t.Logf("Reserved volume %s", volume.ID)
   570  
   571  	return nil
   572  }
   573  
   574  // DeleteVolumeAttach will detach a volume from an instance. A fatal error will
   575  // occur if the snapshot failed to be deleted. This works best when used as a
   576  // deferred function.
   577  func DeleteVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
   578  	t.Logf("Attepting to detach volume volume: %s", volume.ID)
   579  
   580  	detachOpts := volumes.DetachOpts{
   581  		AttachmentID: volume.Attachments[0].AttachmentID,
   582  	}
   583  
   584  	if err := volumes.Detach(context.TODO(), client, volume.ID, detachOpts).ExtractErr(); err != nil {
   585  		t.Fatalf("Unable to detach volume %s: %v", volume.ID, err)
   586  	}
   587  
   588  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   589  	defer cancel()
   590  
   591  	if err := volumes.WaitForStatus(ctx, client, volume.ID, "available"); err != nil {
   592  		t.Fatalf("Volume %s failed to become unavailable in 60 seconds: %v", volume.ID, err)
   593  	}
   594  
   595  	t.Logf("Detached volume: %s", volume.ID)
   596  }
   597  
   598  // DeleteVolumeReserve deletes a volume reservation. A fatal error will occur
   599  // if the deletion request failed. This works best when used as a deferred
   600  // function.
   601  func DeleteVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
   602  	if testing.Short() {
   603  		t.Skip("Skipping test that requires volume reservation in short mode.")
   604  	}
   605  
   606  	t.Logf("Attempting to unreserve volume %s", volume.ID)
   607  
   608  	if err := volumes.Unreserve(context.TODO(), client, volume.ID).ExtractErr(); err != nil {
   609  		t.Fatalf("Unable to unreserve volume %s: %v", volume.ID, err)
   610  	}
   611  
   612  	t.Logf("Unreserved volume %s", volume.ID)
   613  }
   614  
   615  // ExtendVolumeSize will extend the size of a volume.
   616  func ExtendVolumeSize(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
   617  	t.Logf("Attempting to extend the size of volume %s", volume.ID)
   618  
   619  	extendOpts := volumes.ExtendSizeOpts{
   620  		NewSize: 2,
   621  	}
   622  
   623  	err := volumes.ExtendSize(context.TODO(), client, volume.ID, extendOpts).ExtractErr()
   624  	if err != nil {
   625  		return err
   626  	}
   627  
   628  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   629  	defer cancel()
   630  
   631  	if err := volumes.WaitForStatus(ctx, client, volume.ID, "available"); err != nil {
   632  		return err
   633  	}
   634  
   635  	return nil
   636  }
   637  
   638  // SetImageMetadata will apply the metadata to a volume.
   639  func SetImageMetadata(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
   640  	t.Logf("Attempting to apply image metadata to volume %s", volume.ID)
   641  
   642  	imageMetadataOpts := volumes.ImageMetadataOpts{
   643  		Metadata: map[string]string{
   644  			"image_name": "testimage",
   645  		},
   646  	}
   647  
   648  	err := volumes.SetImageMetadata(context.TODO(), client, volume.ID, imageMetadataOpts).ExtractErr()
   649  	if err != nil {
   650  		return err
   651  	}
   652  
   653  	return nil
   654  }
   655  
   656  // SetBootable will set a bootable status to a volume.
   657  func SetBootable(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
   658  	t.Logf("Attempting to apply bootable status to volume %s", volume.ID)
   659  
   660  	bootableOpts := volumes.BootableOpts{
   661  		Bootable: true,
   662  	}
   663  
   664  	err := volumes.SetBootable(context.TODO(), client, volume.ID, bootableOpts).ExtractErr()
   665  	if err != nil {
   666  		return err
   667  	}
   668  
   669  	vol, err := volumes.Get(context.TODO(), client, volume.ID).Extract()
   670  	if err != nil {
   671  		return err
   672  	}
   673  
   674  	if strings.ToLower(vol.Bootable) != "true" {
   675  		return fmt.Errorf("Volume bootable status is %q, expected 'true'", vol.Bootable)
   676  	}
   677  
   678  	bootableOpts = volumes.BootableOpts{
   679  		Bootable: false,
   680  	}
   681  
   682  	err = volumes.SetBootable(context.TODO(), client, volume.ID, bootableOpts).ExtractErr()
   683  	if err != nil {
   684  		return err
   685  	}
   686  
   687  	vol, err = volumes.Get(context.TODO(), client, volume.ID).Extract()
   688  	if err != nil {
   689  		return err
   690  	}
   691  
   692  	if strings.ToLower(vol.Bootable) == "true" {
   693  		return fmt.Errorf("Volume bootable status is %q, expected 'false'", vol.Bootable)
   694  	}
   695  
   696  	return nil
   697  }
   698  
   699  // ChangeVolumeType will extend the size of a volume.
   700  func ChangeVolumeType(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, vt *volumetypes.VolumeType) error {
   701  	t.Logf("Attempting to change the type of volume %s from %s to %s", volume.ID, volume.VolumeType, vt.Name)
   702  
   703  	changeOpts := volumes.ChangeTypeOpts{
   704  		NewType:         vt.Name,
   705  		MigrationPolicy: volumes.MigrationPolicyOnDemand,
   706  	}
   707  
   708  	err := volumes.ChangeType(context.TODO(), client, volume.ID, changeOpts).ExtractErr()
   709  	if err != nil {
   710  		return err
   711  	}
   712  
   713  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   714  	defer cancel()
   715  
   716  	if err := volumes.WaitForStatus(ctx, client, volume.ID, "available"); err != nil {
   717  		return err
   718  	}
   719  
   720  	return nil
   721  }
   722  
   723  // ResetVolumeStatus will reset the status of a volume.
   724  func ResetVolumeStatus(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, status string) error {
   725  	t.Logf("Attempting to reset the status of volume %s from %s to %s", volume.ID, volume.Status, status)
   726  
   727  	resetOpts := volumes.ResetStatusOpts{
   728  		Status: status,
   729  	}
   730  	err := volumes.ResetStatus(context.TODO(), client, volume.ID, resetOpts).ExtractErr()
   731  	if err != nil {
   732  		return err
   733  	}
   734  
   735  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   736  	defer cancel()
   737  
   738  	if err := volumes.WaitForStatus(ctx, client, volume.ID, status); err != nil {
   739  		return err
   740  	}
   741  
   742  	return nil
   743  }
   744  
   745  // ReImage will re-image a volume
   746  func ReImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, imageID string) error {
   747  	t.Logf("Attempting to re-image volume %s", volume.ID)
   748  
   749  	reimageOpts := volumes.ReImageOpts{
   750  		ImageID:         imageID,
   751  		ReImageReserved: false,
   752  	}
   753  
   754  	err := volumes.ReImage(context.TODO(), client, volume.ID, reimageOpts).ExtractErr()
   755  	if err != nil {
   756  		return err
   757  	}
   758  
   759  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   760  	defer cancel()
   761  
   762  	err = volumes.WaitForStatus(ctx, client, volume.ID, "available")
   763  	if err != nil {
   764  		return err
   765  	}
   766  
   767  	vol, err := volumes.Get(context.TODO(), client, volume.ID).Extract()
   768  	if err != nil {
   769  		return err
   770  	}
   771  
   772  	if vol.VolumeImageMetadata == nil {
   773  		return fmt.Errorf("volume does not have VolumeImageMetadata map")
   774  	}
   775  
   776  	if strings.ToLower(vol.VolumeImageMetadata["image_id"]) != imageID {
   777  		return fmt.Errorf("volume image id '%s', expected '%s'", vol.VolumeImageMetadata["image_id"], imageID)
   778  	}
   779  
   780  	return nil
   781  }