Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions internal/acceptance/openstack/blockstorage/v3/blockstorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/backups"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/manageablevolumes"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/qos"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
Expand Down Expand Up @@ -779,3 +780,78 @@ func ReImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Vo

return nil
}

func Unmanage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
t.Logf("Attempting to unmanage volume %s", volume.ID)

err := volumes.Unmanage(context.TODO(), client, volume.ID).ExtractErr()
if err != nil {
return err
}

err = gophercloud.WaitFor(context.TODO(), func(ctx context.Context) (bool, error) {
if _, err := volumes.Get(ctx, client, volume.ID).Extract(); err != nil {
if errCode, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok {
if errCode.Actual == 404 {
return true, nil
}
}
return false, err
}
return false, nil
})
if err != nil {
return fmt.Errorf("error waiting for volume %s to be unmanaged: %v", volume.ID, err)
}

t.Logf("Successfully unmanaged volume %s", volume.ID)

return nil
}

func ManageExisting(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (*volumes.Volume, error) {
t.Logf("Attempting to manage existing volume %s", volume.Name)

manageOpts := manageablevolumes.ManageExistingOpts{
Host: volume.Host,
Ref: map[string]string{
"source-name": fmt.Sprintf("volume-%s", volume.ID),
},
Name: volume.Name,
AvailabilityZone: volume.AvailabilityZone,
Description: volume.Description,
VolumeType: volume.VolumeType,
Bootable: volume.Bootable == "true",
Metadata: volume.Metadata,
}

managed, err := manageablevolumes.ManageExisting(context.TODO(), client, manageOpts).Extract()
if err != nil {
return managed, err
}

ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
defer cancel()

if err := volumes.WaitForStatus(ctx, client, managed.ID, "available"); err != nil {
return managed, err
}

managed, err = volumes.Get(context.TODO(), client, managed.ID).Extract()
if err != nil {
return managed, err
}

tools.PrintResource(t, managed)
th.AssertEquals(t, managed.Host, volume.Host)
th.AssertEquals(t, managed.Name, volume.Name)
th.AssertEquals(t, managed.AvailabilityZone, volume.AvailabilityZone)
th.AssertEquals(t, managed.Description, volume.Description)
th.AssertEquals(t, managed.VolumeType, volume.VolumeType)
th.AssertEquals(t, managed.Bootable, volume.Bootable)
th.AssertDeepEquals(t, managed.Metadata, volume.Metadata)

t.Logf("Successfully managed existing volume %s", managed.ID)

return managed, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build acceptance || blockstorage || volumes

package v3

import (
"context"
"testing"

"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
th "github.com/gophercloud/gophercloud/v2/testhelper"
)

func TestManageableVolumes(t *testing.T) {
clients.RequireLong(t)

client, err := clients.NewBlockStorageV3Client()
th.AssertNoErr(t, err)

client.Microversion = "3.8"

volume1, err := CreateVolume(t, client)
th.AssertNoErr(t, err)

err = Unmanage(t, client, volume1)
if err != nil {
DeleteVolume(t, client, volume1)
}
th.AssertNoErr(t, err)

managed1, err := ManageExisting(t, client, volume1)
th.AssertNoErr(t, err)
defer DeleteVolume(t, client, managed1)

th.CheckEquals(t, volume1.Host, managed1.Host)
th.AssertEquals(t, volume1.Name, managed1.Name)
th.AssertEquals(t, volume1.AvailabilityZone, managed1.AvailabilityZone)
th.AssertEquals(t, volume1.Description, managed1.Description)
th.AssertEquals(t, volume1.VolumeType, managed1.VolumeType)
th.AssertEquals(t, volume1.Bootable, managed1.Bootable)
th.AssertDeepEquals(t, volume1.Metadata, managed1.Metadata)
th.AssertEquals(t, volume1.Size, managed1.Size)

allPages, err := volumes.List(client, volumes.ListOpts{}).AllPages(context.TODO())
th.AssertNoErr(t, err)
allVolumes, err := volumes.ExtractVolumes(allPages)
th.AssertNoErr(t, err)

var found bool
for _, v := range allVolumes {
if v.ID == managed1.ID {
found = true
break
}
}
th.AssertEquals(t, true, found)
}
32 changes: 32 additions & 0 deletions openstack/blockstorage/v3/manageablevolumes/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Package manageablevolumes information and interaction with manageable volumes
for the OpenStack Block Storage service.

NOTE: Requires at least microversion 3.8

Example to manage an existing volume

manageOpts := manageablevolumes.ManageExistingOpts{
Host: "host@lvm#LVM",
Ref: map[string]string{
"source-name": "volume-73796b96-169f-4675-a5bc-73fc0f8f9a17",
},
Name: "New Volume",
AvailabilityZone: "nova",
Description: "Volume imported from existingLV",
VolumeType: "lvm",
Bootable: true,
Metadata: map[string]string{
"key1": "value1",
"key2": "value2"
},
}

managedVolume, err := manageablevolumes.ManageExisting(context.TODO(), client, manageOpts).Extract()
if err != nil {
log.Fatal(err)
}

fmt.Printf("Managed volume: %+v\n", managedVolume)
*/
package manageablevolumes
61 changes: 61 additions & 0 deletions openstack/blockstorage/v3/manageablevolumes/requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package manageablevolumes

import (
"context"

"github.com/gophercloud/gophercloud/v2"
)

// ManageExistingOptsBuilder allows extentions to add additional parameters to the ManageExisting request.
type ManageExistingOptsBuilder interface {
ToManageExistingMap() (map[string]any, error)
}

// ManageExistingOpts contains options for managing a existing volume.
// This object is passed to the volumes.ManageExisting function.
// For more information about the parameters, see the Volume object and OpenStack BlockStorage API Guide.
type ManageExistingOpts struct {
// The OpenStack Block Storage host where the existing resource resides.
// Optional only if cluster field is provided.
Host string `json:"host,omitempty"`
// The OpenStack Block Storage cluster where the resource resides.
// Optional only if host field is provided.
Cluster string `json:"cluster,omitempty"`
// A reference to the existing volume.
// The internal structure of this reference depends on the volume driver implementation.
// For details about the required elements in the structure, see the documentation for the volume driver.
Ref map[string]string `json:"ref,omitempty"`
// Human-readable display name for the volume.
Name string `json:"name,omitempty"`
// The availability zone.
AvailabilityZone string `json:"availability_zone,omitempty"`
// Human-readable description for the volume.
Description string `json:"description,omitempty"`
// The associated volume type
VolumeType string `json:"volume_type,omitempty"`
// Indicates whether this is a bootable volume.
Bootable bool `json:"bootable,omitempty"`
// One or more metadata key and value pairs to associate with the volume.
Metadata map[string]string `json:"metadata,omitempty"`
}

// ToManageExistingMap assembles a request body based on the contents of a ManageExistingOpts.
func (opts ManageExistingOpts) ToManageExistingMap() (map[string]any, error) {
return gophercloud.BuildRequestBody(opts, "volume")
}

// ManageExisting will manage an existing volume based on the values in ManageExistingOpts.
// To extract the Volume object from response, call the Extract method on the ManageExistingResult.
func ManageExisting(ctx context.Context, client *gophercloud.ServiceClient, opts ManageExistingOptsBuilder) (r ManageExistingResult) {
b, err := opts.ToManageExistingMap()
if err != nil {
r.Err = err
return
}

resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
22 changes: 22 additions & 0 deletions openstack/blockstorage/v3/manageablevolumes/results.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package manageablevolumes

import (
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
)

type ManageExistingResult struct {
gophercloud.Result
}

// Extract will get the Volume object out of the ManageExistingResult object.
func (r ManageExistingResult) Extract() (*volumes.Volume, error) {
var s volumes.Volume
err := r.ExtractInto(&s)
return &s, err
}

// ExtractInto converts our response data into a volume struct
func (r ManageExistingResult) ExtractInto(v any) error {
return r.Result.ExtractIntoStructPtr(v, "volume")
}
2 changes: 2 additions & 0 deletions openstack/blockstorage/v3/manageablevolumes/testing/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// manageablevolumes unit tests
package testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package testing

import (
"fmt"
"net/http"
"testing"

th "github.com/gophercloud/gophercloud/v2/testhelper"
fake "github.com/gophercloud/gophercloud/v2/testhelper/client"
)

func MockManageExistingResponse(t *testing.T) {
th.Mux.HandleFunc("/manageable_volumes", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
{
"volume": {
"host": "host@lvm#LVM",
"ref": {
"source-name": "volume-73796b96-169f-4675-a5bc-73fc0f8f9a17"
},
"name": "New Volume",
"availability_zone": "nova",
"description": "Volume imported from existingLV",
"volume_type": "lvm",
"bootable": true,
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
}
`)

w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)

fmt.Fprint(w, `
{
"volume": {
"id": "23cf872b-c781-4cd4-847d-5f2ec8cbd91c",
"status": "creating",
"size": 0,
"availability_zone": "nova",
"created_at": "2025-03-20T11:58:05.000000",
"updated_at": "2025-03-20T11:58:05.000000",
"name": "New Volume",
"description": "Volume imported from existingLV",
"volume_type": "lvm",
"snapshot_id": null,
"source_volid": null,
"metadata": {
"key1": "value1",
"key2": "value2"
},
"links": [
{
"href": "http://10.0.2.15:8776/v3/87c8522052ca4eed98bc672b4c1a3ddb/volumes/23cf872b-c781-4cd4-847d-5f2ec8cbd91c",
"rel": "self"
},
{
"href": "http://10.0.2.15:8776/87c8522052ca4eed98bc672b4c1a3ddb/volumes/23cf872b-c781-4cd4-847d-5f2ec8cbd91c",
"rel": "bookmark"
}
],
"user_id": "eae1472b5fc5496998a3d06550929e7e",
"bootable": "true",
"encrypted": false,
"replication_status": null,
"consistencygroup_id": null,
"multiattach": false,
"attachments": [],
"created_at": "2014-07-18T00:12:54.000000",
"migration_status": null,
"group_id": null,
"provider_id": null,
"shared_targets": true,
"service_uuid": null,
"cluster_name": null,
"volume_type_id": "a218796e-605b-4b6f-9dfc-8be95a0d7d03",
"consumes_quota": true,
"os-vol-mig-status-attr:migstat": null,
"os-vol-mig-status-attr:name_id": null,
"os-vol-tenant-attr:tenant_id": "87c8522052ca4eed98bc672b4c1a3ddb",
"os-vol-host-attr:host": "host@lvm#LVM"
}
}
`)
})
}
Loading
Loading