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 }