Skip to content

Commit 0c9d332

Browse files
authored
Add support for volume type access (#2138)
ListAccesses: List accesses of private volume type AddAccess: Add access of project to volume type RemoveAccess: Remove access of project from volume type
1 parent bad3698 commit 0c9d332

File tree

7 files changed

+351
-1
lines changed

7 files changed

+351
-1
lines changed

acceptance/openstack/blockstorage/v3/blockstorage.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,37 @@ func CreateVolumeTypeNoExtraSpecs(t *testing.T, client *gophercloud.ServiceClien
176176
return vt, nil
177177
}
178178

179+
// CreatePrivateVolumeType will create a private volume type with a random
180+
// name and no extra specs. An error will be returned if the volume type was
181+
// unable to be created.
182+
func CreatePrivateVolumeType(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) {
183+
name := tools.RandomString("ACPTTEST", 16)
184+
description := "create_from_gophercloud"
185+
isPublic := false
186+
t.Logf("Attempting to create volume type: %s", name)
187+
188+
createOpts := volumetypes.CreateOpts{
189+
Name: name,
190+
ExtraSpecs: map[string]string{},
191+
Description: description,
192+
IsPublic: &isPublic,
193+
}
194+
195+
vt, err := volumetypes.Create(client, createOpts).Extract()
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
tools.PrintResource(t, vt)
201+
th.AssertEquals(t, vt.IsPublic, false)
202+
th.AssertEquals(t, vt.Name, name)
203+
th.AssertEquals(t, vt.Description, description)
204+
205+
t.Logf("Successfully created volume type: %s", vt.ID)
206+
207+
return vt, nil
208+
}
209+
179210
// DeleteSnapshot will delete a snapshot. A fatal error will occur if the
180211
// snapshot failed to be deleted.
181212
func DeleteSnapshot(t *testing.T, client *gophercloud.ServiceClient, snapshot *snapshots.Snapshot) {

acceptance/openstack/blockstorage/v3/volumetypes_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/gophercloud/gophercloud/acceptance/clients"
9+
identity "github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3"
910
"github.com/gophercloud/gophercloud/acceptance/tools"
1011
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumetypes"
1112
th "github.com/gophercloud/gophercloud/testhelper"
@@ -109,3 +110,57 @@ func TestVolumeTypesExtraSpecs(t *testing.T) {
109110

110111
th.AssertEquals(t, singleSpec["capabilities"], "gpu-2")
111112
}
113+
114+
func TestVolumeTypesAccess(t *testing.T) {
115+
clients.RequireAdmin(t)
116+
117+
client, err := clients.NewBlockStorageV3Client()
118+
th.AssertNoErr(t, err)
119+
120+
identityClient, err := clients.NewIdentityV3Client()
121+
th.AssertNoErr(t, err)
122+
123+
vt, err := CreatePrivateVolumeType(t, client)
124+
th.AssertNoErr(t, err)
125+
defer DeleteVolumeType(t, client, vt)
126+
127+
project, err := identity.CreateProject(t, identityClient, nil)
128+
th.AssertNoErr(t, err)
129+
defer identity.DeleteProject(t, identityClient, project.ID)
130+
131+
addAccessOpts := volumetypes.AddAccessOpts{
132+
Project: project.ID,
133+
}
134+
135+
err = volumetypes.AddAccess(client, vt.ID, addAccessOpts).ExtractErr()
136+
th.AssertNoErr(t, err)
137+
138+
allPages, err := volumetypes.ListAccesses(client, vt.ID).AllPages()
139+
th.AssertNoErr(t, err)
140+
141+
accessList, err := volumetypes.ExtractAccesses(allPages)
142+
th.AssertNoErr(t, err)
143+
144+
tools.PrintResource(t, accessList)
145+
146+
th.AssertEquals(t, len(accessList), 1)
147+
th.AssertEquals(t, accessList[0].ProjectID, project.ID)
148+
th.AssertEquals(t, accessList[0].VolumeTypeID, vt.ID)
149+
150+
removeAccessOpts := volumetypes.RemoveAccessOpts{
151+
Project: project.ID,
152+
}
153+
154+
err = volumetypes.RemoveAccess(client, vt.ID, removeAccessOpts).ExtractErr()
155+
th.AssertNoErr(t, err)
156+
157+
allPages, err = volumetypes.ListAccesses(client, vt.ID).AllPages()
158+
th.AssertNoErr(t, err)
159+
160+
accessList, err = volumetypes.ExtractAccesses(allPages)
161+
th.AssertNoErr(t, err)
162+
163+
tools.PrintResource(t, accessList)
164+
165+
th.AssertEquals(t, len(accessList), 0)
166+
}

openstack/blockstorage/v3/volumetypes/doc.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,50 @@ Example to Delete an Extra Spec for a Volume Type
116116
if err != nil {
117117
panic(err)
118118
}
119+
120+
Example to List Volume Type Access
121+
122+
typeID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
123+
124+
allPages, err := volumetypes.ListAccesses(client, typeID).AllPages()
125+
if err != nil {
126+
panic(err)
127+
}
128+
129+
allAccesses, err := volumetypes.ExtractAccesses(allPages)
130+
if err != nil {
131+
panic(err)
132+
}
133+
134+
for _, access := range allAccesses {
135+
fmt.Printf("%+v", access)
136+
}
137+
138+
Example to Grant Access to a Volume Type
139+
140+
typeID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
141+
142+
accessOpts := volumetypes.AddAccessOpts{
143+
Project: "15153a0979884b59b0592248ef947921",
144+
}
145+
146+
err := volumetypes.AddAccess(client, typeID, accessOpts).ExtractErr()
147+
if err != nil {
148+
panic(err)
149+
}
150+
151+
Example to Remove/Revoke Access to a Volume Type
152+
153+
typeID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
154+
155+
accessOpts := volumetypes.RemoveAccessOpts{
156+
Project: "15153a0979884b59b0592248ef947921",
157+
}
158+
159+
err := volumetypes.RemoveAccess(client, typeID, accessOpts).ExtractErr()
160+
if err != nil {
161+
panic(err)
162+
}
163+
119164
*/
120165
package volumetypes

openstack/blockstorage/v3/volumetypes/requests.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ type UpdateOpts struct {
120120
IsPublic *bool `json:"is_public,omitempty"`
121121
}
122122

123-
// ToVolumeUpdateMap assembles a request body based on the contents of an
123+
// ToVolumeTypeUpdateMap assembles a request body based on the contents of an
124124
// UpdateOpts.
125125
func (opts UpdateOpts) ToVolumeTypeUpdateMap() (map[string]interface{}, error) {
126126
return gophercloud.BuildRequestBody(opts, "volume_type")
@@ -233,3 +233,74 @@ func DeleteExtraSpec(client *gophercloud.ServiceClient, volumeTypeID, key string
233233
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
234234
return
235235
}
236+
237+
// ListAccesses retrieves the tenants which have access to a volume type.
238+
func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager {
239+
url := accessURL(client, id)
240+
241+
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
242+
return AccessPage{pagination.SinglePageBase(r)}
243+
})
244+
}
245+
246+
// AddAccessOptsBuilder allows extensions to add additional parameters to the
247+
// AddAccess requests.
248+
type AddAccessOptsBuilder interface {
249+
ToVolumeTypeAddAccessMap() (map[string]interface{}, error)
250+
}
251+
252+
// AddAccessOpts represents options for adding access to a volume type.
253+
type AddAccessOpts struct {
254+
// Project is the project/tenant ID to grant access.
255+
Project string `json:"project"`
256+
}
257+
258+
// ToVolumeTypeAddAccessMap constructs a request body from AddAccessOpts.
259+
func (opts AddAccessOpts) ToVolumeTypeAddAccessMap() (map[string]interface{}, error) {
260+
return gophercloud.BuildRequestBody(opts, "addProjectAccess")
261+
}
262+
263+
// AddAccess grants a tenant/project access to a volume type.
264+
func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) {
265+
b, err := opts.ToVolumeTypeAddAccessMap()
266+
if err != nil {
267+
r.Err = err
268+
return
269+
}
270+
resp, err := client.Post(accessActionURL(client, id), b, nil, &gophercloud.RequestOpts{
271+
OkCodes: []int{202},
272+
})
273+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
274+
return
275+
}
276+
277+
// RemoveAccessOptsBuilder allows extensions to add additional parameters to the
278+
// RemoveAccess requests.
279+
type RemoveAccessOptsBuilder interface {
280+
ToVolumeTypeRemoveAccessMap() (map[string]interface{}, error)
281+
}
282+
283+
// RemoveAccessOpts represents options for removing access to a volume type.
284+
type RemoveAccessOpts struct {
285+
// Project is the project/tenant ID to remove access.
286+
Project string `json:"project"`
287+
}
288+
289+
// ToVolumeTypeRemoveAccessMap constructs a request body from RemoveAccessOpts.
290+
func (opts RemoveAccessOpts) ToVolumeTypeRemoveAccessMap() (map[string]interface{}, error) {
291+
return gophercloud.BuildRequestBody(opts, "removeProjectAccess")
292+
}
293+
294+
// RemoveAccess removes/revokes a tenant/project access to a volume type.
295+
func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) {
296+
b, err := opts.ToVolumeTypeRemoveAccessMap()
297+
if err != nil {
298+
r.Err = err
299+
return
300+
}
301+
resp, err := client.Post(accessActionURL(client, id), b, nil, &gophercloud.RequestOpts{
302+
OkCodes: []int{202},
303+
})
304+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
305+
return
306+
}

openstack/blockstorage/v3/volumetypes/results.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,44 @@ func (r extraSpecResult) Extract() (map[string]string, error) {
151151
err := r.ExtractInto(&s)
152152
return s, err
153153
}
154+
155+
// VolumeTypeAccess represents an ACL of project access to a specific Volume Type.
156+
type VolumeTypeAccess struct {
157+
// VolumeTypeID is the unique ID of the volume type.
158+
VolumeTypeID string `json:"volume_type_id"`
159+
160+
// ProjectID is the unique ID of the project.
161+
ProjectID string `json:"project_id"`
162+
}
163+
164+
// AccessPage contains a single page of all VolumeTypeAccess entries for a volume type.
165+
type AccessPage struct {
166+
pagination.SinglePageBase
167+
}
168+
169+
// IsEmpty indicates whether an AccessPage is empty.
170+
func (page AccessPage) IsEmpty() (bool, error) {
171+
v, err := ExtractAccesses(page)
172+
return len(v) == 0, err
173+
}
174+
175+
// ExtractAccesses interprets a page of results as a slice of VolumeTypeAccess.
176+
func ExtractAccesses(r pagination.Page) ([]VolumeTypeAccess, error) {
177+
var s struct {
178+
VolumeTypeAccesses []VolumeTypeAccess `json:"volume_type_access"`
179+
}
180+
err := (r.(AccessPage)).ExtractInto(&s)
181+
return s.VolumeTypeAccesses, err
182+
}
183+
184+
// AddAccessResult is the response from a AddAccess request. Call its
185+
// ExtractErr method to determine if the request succeeded or failed.
186+
type AddAccessResult struct {
187+
gophercloud.ErrResult
188+
}
189+
190+
// RemoveAccessResult is the response from a RemoveAccess request. Call its
191+
// ExtractErr method to determine if the request succeeded or failed.
192+
type RemoveAccessResult struct {
193+
gophercloud.ErrResult
194+
}

openstack/blockstorage/v3/volumetypes/testing/requests_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package testing
22

33
import (
4+
"fmt"
5+
"net/http"
6+
"reflect"
47
"testing"
58

69
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumetypes"
@@ -177,3 +180,99 @@ func TestVolumeTypeExtraSpecDelete(t *testing.T) {
177180
res := volumetypes.DeleteExtraSpec(client.ServiceClient(), "1", "capabilities")
178181
th.AssertNoErr(t, res.Err)
179182
}
183+
184+
func TestVolumeTypeListAccesses(t *testing.T) {
185+
th.SetupHTTP()
186+
defer th.TeardownHTTP()
187+
188+
th.Mux.HandleFunc("/types/a5082c24-2a27-43a4-b48e-fcec1240e36b/os-volume-type-access", func(w http.ResponseWriter, r *http.Request) {
189+
th.TestMethod(t, r, "GET")
190+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
191+
w.Header().Add("Content-Type", "application/json")
192+
fmt.Fprintf(w, `
193+
{
194+
"volume_type_access": [
195+
{
196+
"project_id": "6f70656e737461636b20342065766572",
197+
"volume_type_id": "a5082c24-2a27-43a4-b48e-fcec1240e36b"
198+
}
199+
]
200+
}
201+
`)
202+
})
203+
204+
expected := []volumetypes.VolumeTypeAccess{
205+
{
206+
VolumeTypeID: "a5082c24-2a27-43a4-b48e-fcec1240e36b",
207+
ProjectID: "6f70656e737461636b20342065766572",
208+
},
209+
}
210+
211+
allPages, err := volumetypes.ListAccesses(client.ServiceClient(), "a5082c24-2a27-43a4-b48e-fcec1240e36b").AllPages()
212+
th.AssertNoErr(t, err)
213+
214+
actual, err := volumetypes.ExtractAccesses(allPages)
215+
th.AssertNoErr(t, err)
216+
217+
if !reflect.DeepEqual(expected, actual) {
218+
t.Errorf("Expected %#v, but was %#v", expected, actual)
219+
}
220+
}
221+
222+
func TestVolumeTypeAddAccess(t *testing.T) {
223+
th.SetupHTTP()
224+
defer th.TeardownHTTP()
225+
226+
th.Mux.HandleFunc("/types/a5082c24-2a27-43a4-b48e-fcec1240e36b/action", func(w http.ResponseWriter, r *http.Request) {
227+
th.TestMethod(t, r, "POST")
228+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
229+
th.TestHeader(t, r, "accept", "application/json")
230+
th.TestJSONRequest(t, r, `
231+
{
232+
"addProjectAccess": {
233+
"project": "6f70656e737461636b20342065766572"
234+
}
235+
}
236+
`)
237+
238+
w.Header().Add("Content-Type", "application/json")
239+
w.WriteHeader(http.StatusAccepted)
240+
})
241+
242+
addAccessOpts := volumetypes.AddAccessOpts{
243+
Project: "6f70656e737461636b20342065766572",
244+
}
245+
246+
err := volumetypes.AddAccess(client.ServiceClient(), "a5082c24-2a27-43a4-b48e-fcec1240e36b", addAccessOpts).ExtractErr()
247+
th.AssertNoErr(t, err)
248+
249+
}
250+
251+
func TestVolumeTypeRemoveAccess(t *testing.T) {
252+
th.SetupHTTP()
253+
defer th.TeardownHTTP()
254+
255+
th.Mux.HandleFunc("/types/a5082c24-2a27-43a4-b48e-fcec1240e36b/action", func(w http.ResponseWriter, r *http.Request) {
256+
th.TestMethod(t, r, "POST")
257+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
258+
th.TestHeader(t, r, "accept", "application/json")
259+
th.TestJSONRequest(t, r, `
260+
{
261+
"removeProjectAccess": {
262+
"project": "6f70656e737461636b20342065766572"
263+
}
264+
}
265+
`)
266+
267+
w.Header().Add("Content-Type", "application/json")
268+
w.WriteHeader(http.StatusAccepted)
269+
})
270+
271+
removeAccessOpts := volumetypes.RemoveAccessOpts{
272+
Project: "6f70656e737461636b20342065766572",
273+
}
274+
275+
err := volumetypes.RemoveAccess(client.ServiceClient(), "a5082c24-2a27-43a4-b48e-fcec1240e36b", removeAccessOpts).ExtractErr()
276+
th.AssertNoErr(t, err)
277+
278+
}

0 commit comments

Comments
 (0)