Skip to content

Commit 7ace507

Browse files
authored
Merge pull request gophercloud#3332 from TOMOFUMI-KONDO/blockstorage-manage-exsiting
blockstorage: add manage-existing and unmanage api call
2 parents 281589e + 6fb7d11 commit 7ace507

14 files changed

Lines changed: 438 additions & 0 deletions

File tree

internal/acceptance/openstack/blockstorage/v3/blockstorage.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/gophercloud/gophercloud/v2"
1515
"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
1616
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/backups"
17+
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/manageablevolumes"
1718
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/qos"
1819
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots"
1920
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
@@ -779,3 +780,73 @@ func ReImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Vo
779780

780781
return nil
781782
}
783+
784+
func Unmanage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
785+
t.Logf("Attempting to unmanage volume %s", volume.ID)
786+
787+
err := volumes.Unmanage(context.TODO(), client, volume.ID).ExtractErr()
788+
if err != nil {
789+
return err
790+
}
791+
792+
gophercloud.WaitFor(context.TODO(), func(ctx context.Context) (bool, error) {
793+
if _, err := volumes.Get(ctx, client, volume.ID).Extract(); err != nil {
794+
if _, ok := err.(gophercloud.ErrResourceNotFound); ok {
795+
return true, nil
796+
}
797+
return false, err
798+
}
799+
return false, nil
800+
})
801+
802+
t.Logf("Successfully unmanaged volume %s", volume.ID)
803+
804+
return nil
805+
}
806+
807+
func ManageExisting(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (*volumes.Volume, error) {
808+
t.Logf("Attempting to manage existing volume %s", volume.Name)
809+
810+
manageOpts := manageablevolumes.ManageExistingOpts{
811+
Host: volume.Host,
812+
Ref: map[string]string{
813+
"source-name": fmt.Sprintf("volume-%s", volume.ID),
814+
},
815+
Name: volume.Name,
816+
AvailabilityZone: volume.AvailabilityZone,
817+
Description: volume.Description,
818+
VolumeType: volume.VolumeType,
819+
Bootable: volume.Bootable == "true",
820+
Metadata: volume.Metadata,
821+
}
822+
823+
managed, err := manageablevolumes.ManageExisting(context.TODO(), client, manageOpts).Extract()
824+
if err != nil {
825+
return managed, err
826+
}
827+
828+
ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
829+
defer cancel()
830+
831+
if err := volumes.WaitForStatus(ctx, client, managed.ID, "available"); err != nil {
832+
return managed, err
833+
}
834+
835+
managed, err = volumes.Get(context.TODO(), client, managed.ID).Extract()
836+
if err != nil {
837+
return managed, err
838+
}
839+
840+
tools.PrintResource(t, managed)
841+
th.AssertEquals(t, managed.Host, volume.Host)
842+
th.AssertEquals(t, managed.Name, volume.Name)
843+
th.AssertEquals(t, managed.AvailabilityZone, volume.AvailabilityZone)
844+
th.AssertEquals(t, managed.Description, volume.Description)
845+
th.AssertEquals(t, managed.VolumeType, volume.VolumeType)
846+
th.AssertEquals(t, managed.Bootable, volume.Bootable)
847+
th.AssertDeepEquals(t, managed.Metadata, volume.Metadata)
848+
849+
t.Logf("Successfully managed existing volume %s", managed.ID)
850+
851+
return managed, nil
852+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//go:build acceptance || blockstorage || volumes
2+
3+
package v3
4+
5+
import (
6+
"context"
7+
"testing"
8+
9+
"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
10+
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
11+
th "github.com/gophercloud/gophercloud/v2/testhelper"
12+
)
13+
14+
func TestManageableVolumes(t *testing.T) {
15+
clients.RequireLong(t)
16+
17+
client, err := clients.NewBlockStorageV3Client()
18+
th.AssertNoErr(t, err)
19+
20+
client.Microversion = "3.8"
21+
22+
volume1, err := CreateVolume(t, client)
23+
th.AssertNoErr(t, err)
24+
25+
err = Unmanage(t, client, volume1)
26+
if err != nil {
27+
DeleteVolume(t, client, volume1)
28+
}
29+
th.AssertNoErr(t, err)
30+
31+
managed1, err := ManageExisting(t, client, volume1)
32+
th.AssertNoErr(t, err)
33+
defer DeleteVolume(t, client, managed1)
34+
35+
th.CheckEquals(t, volume1.Host, managed1.Host)
36+
th.AssertEquals(t, volume1.Name, managed1.Name)
37+
th.AssertEquals(t, volume1.AvailabilityZone, managed1.AvailabilityZone)
38+
th.AssertEquals(t, volume1.Description, managed1.Description)
39+
th.AssertEquals(t, volume1.VolumeType, managed1.VolumeType)
40+
th.AssertEquals(t, volume1.Bootable, managed1.Bootable)
41+
th.AssertDeepEquals(t, volume1.Metadata, managed1.Metadata)
42+
th.AssertEquals(t, volume1.Size, managed1.Size)
43+
44+
allPages, err := volumes.List(client, volumes.ListOpts{}).AllPages(context.TODO())
45+
th.AssertNoErr(t, err)
46+
allVolumes, err := volumes.ExtractVolumes(allPages)
47+
th.AssertNoErr(t, err)
48+
49+
var found bool
50+
for _, v := range allVolumes {
51+
if v.ID == managed1.ID {
52+
found = true
53+
break
54+
}
55+
}
56+
th.AssertEquals(t, true, found)
57+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Package manageablevolumes information and interaction with manageable volumes
3+
for the OpenStack Block Storage service.
4+
5+
NOTE: Requires at least microversion 3.8
6+
7+
Example to manage an existing volume
8+
9+
manageOpts := manageablevolumes.ManageExistingOpts{
10+
Host: "host@lvm#LVM",
11+
Ref: map[string]string{
12+
"source-name": "volume-73796b96-169f-4675-a5bc-73fc0f8f9a17",
13+
},
14+
Name: "New Volume",
15+
AvailabilityZone: "nova",
16+
Description: "Volume imported from existingLV",
17+
VolumeType: "lvm",
18+
Bootable: true,
19+
Metadata: map[string]string{
20+
"key1": "value1",
21+
"key2": "value2"
22+
},
23+
}
24+
25+
managedVolume, err := manageablevolumes.ManageExisting(context.TODO(), client, manageOpts).Extract()
26+
if err != nil {
27+
log.Fatal(err)
28+
}
29+
30+
fmt.Printf("Managed volume: %+v\n", managedVolume)
31+
*/
32+
package manageablevolumes
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package manageablevolumes
2+
3+
import (
4+
"context"
5+
6+
"github.com/gophercloud/gophercloud/v2"
7+
)
8+
9+
// ManageExistingOptsBuilder allows extentions to add additional parameters to the ManageExisting request.
10+
type ManageExistingOptsBuilder interface {
11+
ToManageExistingMap() (map[string]any, error)
12+
}
13+
14+
// ManageExistingOpts contains options for managing a existing volume.
15+
// This object is passed to the volumes.ManageExisting function.
16+
// For more information about the parameters, see the Volume object and OpenStack BlockStorage API Guide.
17+
type ManageExistingOpts struct {
18+
// The OpenStack Block Storage host where the existing resource resides.
19+
// Optional only if cluster field is provided.
20+
Host string `json:"host,omitempty"`
21+
// The OpenStack Block Storage cluster where the resource resides.
22+
// Optional only if host field is provided.
23+
Cluster string `json:"cluster,omitempty"`
24+
// A reference to the existing volume.
25+
// The internal structure of this reference depends on the volume driver implementation.
26+
// For details about the required elements in the structure, see the documentation for the volume driver.
27+
Ref map[string]string `json:"ref,omitempty"`
28+
// Human-readable display name for the volume.
29+
Name string `json:"name,omitempty"`
30+
// The availability zone.
31+
AvailabilityZone string `json:"availability_zone,omitempty"`
32+
// Human-readable description for the volume.
33+
Description string `json:"description,omitempty"`
34+
// The associated volume type
35+
VolumeType string `json:"volume_type,omitempty"`
36+
// Indicates whether this is a bootable volume.
37+
Bootable bool `json:"bootable,omitempty"`
38+
// One or more metadata key and value pairs to associate with the volume.
39+
Metadata map[string]string `json:"metadata,omitempty"`
40+
}
41+
42+
// ToManageExistingMap assembles a request body based on the contents of a ManageExistingOpts.
43+
func (opts ManageExistingOpts) ToManageExistingMap() (map[string]any, error) {
44+
return gophercloud.BuildRequestBody(opts, "volume")
45+
}
46+
47+
// ManageExisting will manage an existing volume based on the values in ManageExistingOpts.
48+
// To extract the Volume object from response, call the Extract method on the ManageExistingResult.
49+
func ManageExisting(ctx context.Context, client *gophercloud.ServiceClient, opts ManageExistingOptsBuilder) (r ManageExistingResult) {
50+
b, err := opts.ToManageExistingMap()
51+
if err != nil {
52+
r.Err = err
53+
return
54+
}
55+
56+
resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{
57+
OkCodes: []int{202},
58+
})
59+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
60+
return
61+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package manageablevolumes
2+
3+
import (
4+
"github.com/gophercloud/gophercloud/v2"
5+
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
6+
)
7+
8+
type ManageExistingResult struct {
9+
gophercloud.Result
10+
}
11+
12+
// Extract will get the Volume object out of the ManageExistingResult object.
13+
func (r ManageExistingResult) Extract() (*volumes.Volume, error) {
14+
var s volumes.Volume
15+
err := r.ExtractInto(&s)
16+
return &s, err
17+
}
18+
19+
// ExtractInto converts our response data into a volume struct
20+
func (r ManageExistingResult) ExtractInto(v any) error {
21+
return r.Result.ExtractIntoStructPtr(v, "volume")
22+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// manageablevolumes unit tests
2+
package testing
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package testing
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"testing"
7+
8+
th "github.com/gophercloud/gophercloud/v2/testhelper"
9+
fake "github.com/gophercloud/gophercloud/v2/testhelper/client"
10+
)
11+
12+
func MockManageExistingResponse(t *testing.T) {
13+
th.Mux.HandleFunc("/manageable_volumes", func(w http.ResponseWriter, r *http.Request) {
14+
th.TestMethod(t, r, "POST")
15+
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
16+
th.TestHeader(t, r, "Content-Type", "application/json")
17+
th.TestHeader(t, r, "Accept", "application/json")
18+
th.TestJSONRequest(t, r, `
19+
{
20+
"volume": {
21+
"host": "host@lvm#LVM",
22+
"ref": {
23+
"source-name": "volume-73796b96-169f-4675-a5bc-73fc0f8f9a17"
24+
},
25+
"name": "New Volume",
26+
"availability_zone": "nova",
27+
"description": "Volume imported from existingLV",
28+
"volume_type": "lvm",
29+
"bootable": true,
30+
"metadata": {
31+
"key1": "value1",
32+
"key2": "value2"
33+
}
34+
}
35+
}
36+
`)
37+
38+
w.Header().Add("Content-Type", "application/json")
39+
w.WriteHeader(http.StatusAccepted)
40+
41+
fmt.Fprint(w, `
42+
{
43+
"volume": {
44+
"id": "23cf872b-c781-4cd4-847d-5f2ec8cbd91c",
45+
"status": "creating",
46+
"size": 0,
47+
"availability_zone": "nova",
48+
"created_at": "2025-03-20T11:58:05.000000",
49+
"updated_at": "2025-03-20T11:58:05.000000",
50+
"name": "New Volume",
51+
"description": "Volume imported from existingLV",
52+
"volume_type": "lvm",
53+
"snapshot_id": null,
54+
"source_volid": null,
55+
"metadata": {
56+
"key1": "value1",
57+
"key2": "value2"
58+
},
59+
"links": [
60+
{
61+
"href": "http://10.0.2.15:8776/v3/87c8522052ca4eed98bc672b4c1a3ddb/volumes/23cf872b-c781-4cd4-847d-5f2ec8cbd91c",
62+
"rel": "self"
63+
},
64+
{
65+
"href": "http://10.0.2.15:8776/87c8522052ca4eed98bc672b4c1a3ddb/volumes/23cf872b-c781-4cd4-847d-5f2ec8cbd91c",
66+
"rel": "bookmark"
67+
}
68+
],
69+
"user_id": "eae1472b5fc5496998a3d06550929e7e",
70+
"bootable": "true",
71+
"encrypted": false,
72+
"replication_status": null,
73+
"consistencygroup_id": null,
74+
"multiattach": false,
75+
"attachments": [],
76+
"created_at": "2014-07-18T00:12:54.000000",
77+
"migration_status": null,
78+
"group_id": null,
79+
"provider_id": null,
80+
"shared_targets": true,
81+
"service_uuid": null,
82+
"cluster_name": null,
83+
"volume_type_id": "a218796e-605b-4b6f-9dfc-8be95a0d7d03",
84+
"consumes_quota": true,
85+
"os-vol-mig-status-attr:migstat": null,
86+
"os-vol-mig-status-attr:name_id": null,
87+
"os-vol-tenant-attr:tenant_id": "87c8522052ca4eed98bc672b4c1a3ddb",
88+
"os-vol-host-attr:host": "host@lvm#LVM"
89+
}
90+
}
91+
`)
92+
})
93+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package testing
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/manageablevolumes"
8+
th "github.com/gophercloud/gophercloud/v2/testhelper"
9+
"github.com/gophercloud/gophercloud/v2/testhelper/client"
10+
)
11+
12+
func TestManageExisting(t *testing.T) {
13+
th.SetupHTTP()
14+
defer th.TeardownHTTP()
15+
16+
MockManageExistingResponse(t)
17+
18+
options := &manageablevolumes.ManageExistingOpts{
19+
Host: "host@lvm#LVM",
20+
Ref: map[string]string{"source-name": "volume-73796b96-169f-4675-a5bc-73fc0f8f9a17"},
21+
Name: "New Volume",
22+
AvailabilityZone: "nova",
23+
Description: "Volume imported from existingLV",
24+
VolumeType: "lvm",
25+
Bootable: true,
26+
Metadata: map[string]string{
27+
"key1": "value1",
28+
"key2": "value2",
29+
},
30+
}
31+
n, err := manageablevolumes.ManageExisting(context.TODO(), client.ServiceClient(), options).Extract()
32+
th.AssertNoErr(t, err)
33+
34+
th.AssertEquals(t, n.Host, "host@lvm#LVM")
35+
th.AssertEquals(t, n.Name, "New Volume")
36+
th.AssertEquals(t, n.AvailabilityZone, "nova")
37+
th.AssertEquals(t, n.Description, "Volume imported from existingLV")
38+
th.AssertEquals(t, n.Bootable, "true")
39+
th.AssertDeepEquals(t, n.Metadata, map[string]string{
40+
"key1": "value1",
41+
"key2": "value2",
42+
})
43+
th.AssertEquals(t, n.ID, "23cf872b-c781-4cd4-847d-5f2ec8cbd91c")
44+
}

0 commit comments

Comments
 (0)