diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a1d3e311..ea4ca7539e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,27 @@ -## 0.17.0 (Unreleased) +## 0.18.0 (Unreleased) + +## 0.17.0 (April 9, 2021) + +IMPROVEMENTS + +* `networking/v2/extensions/quotas.QuotaDetail.Reserved` can handle both `int` and `string` values [GH-2126](https://github.com/gophercloud/gophercloud/pull/2126) +* Added `blockstorage/v3/volumetypes.ListExtraSpecs` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.GetExtraSpec` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.CreateExtraSpecs` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.UpdateExtraSpec` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.DeleteExtraSpec` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `identity/v3/roles.ListAssignmentOpts.IncludeNames` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.AssignedRoles.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.Domain.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.Project.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.User.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.Group.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `blockstorage/extensions/availabilityzones.List` [GH-2135](https://github.com/gophercloud/gophercloud/pull/2135) +* Added `blockstorage/v3/volumetypes.ListAccesses` [GH-2138](https://github.com/gophercloud/gophercloud/pull/2138) +* Added `blockstorage/v3/volumetypes.AddAccess` [GH-2138](https://github.com/gophercloud/gophercloud/pull/2138) +* Added `blockstorage/v3/volumetypes.RemoveAccess` [GH-2138](https://github.com/gophercloud/gophercloud/pull/2138) +* Added `blockstorage/v3/qos.Create` [GH-2140](https://github.com/gophercloud/gophercloud/pull/2140) +* Added `blockstorage/v3/qos.Delete` [GH-2140](https://github.com/gophercloud/gophercloud/pull/2140) ## 0.16.0 (February 23, 2021) diff --git a/acceptance/openstack/blockstorage/v3/blockstorage.go b/acceptance/openstack/blockstorage/v3/blockstorage.go index 3394cee7f6..5e05024a1c 100644 --- a/acceptance/openstack/blockstorage/v3/blockstorage.go +++ b/acceptance/openstack/blockstorage/v3/blockstorage.go @@ -8,6 +8,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/qos" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/snapshots" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumetypes" @@ -176,6 +177,37 @@ func CreateVolumeTypeNoExtraSpecs(t *testing.T, client *gophercloud.ServiceClien return vt, nil } +// CreatePrivateVolumeType will create a private volume type with a random +// name and no extra specs. An error will be returned if the volume type was +// unable to be created. +func CreatePrivateVolumeType(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) { + name := tools.RandomString("ACPTTEST", 16) + description := "create_from_gophercloud" + isPublic := false + t.Logf("Attempting to create volume type: %s", name) + + createOpts := volumetypes.CreateOpts{ + Name: name, + ExtraSpecs: map[string]string{}, + Description: description, + IsPublic: &isPublic, + } + + vt, err := volumetypes.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + tools.PrintResource(t, vt) + th.AssertEquals(t, vt.IsPublic, false) + th.AssertEquals(t, vt.Name, name) + th.AssertEquals(t, vt.Description, description) + + t.Logf("Successfully created volume type: %s", vt.ID) + + return vt, nil +} + // DeleteSnapshot will delete a snapshot. A fatal error will occur if the // snapshot failed to be deleted. func DeleteSnapshot(t *testing.T, client *gophercloud.ServiceClient, snapshot *snapshots.Snapshot) { @@ -241,3 +273,49 @@ func DeleteVolumeType(t *testing.T, client *gophercloud.ServiceClient, vt *volum t.Logf("Successfully deleted volume type: %s", vt.ID) } + +// CreateQoS will create a QoS with one spec and a random name. An +// error will be returned if the volume was unable to be created. +func CreateQoS(t *testing.T, client *gophercloud.ServiceClient) (*qos.QoS, error) { + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create QoS: %s", name) + + createOpts := qos.CreateOpts{ + Name: name, + Consumer: qos.ConsumerFront, + Specs: map[string]string{ + "read_iops_sec": "20000", + }, + } + + qs, err := qos.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + tools.PrintResource(t, qs) + th.AssertEquals(t, qs.Consumer, "front-end") + th.AssertEquals(t, qs.Name, name) + th.AssertDeepEquals(t, qs.Specs, createOpts.Specs) + + t.Logf("Successfully created QoS: %s", qs.ID) + + return qs, nil +} + +// DeleteQoS will delete a QoS. A fatal error will occur if the QoS +// failed to be deleted. This works best when used as a deferred function. +func DeleteQoS(t *testing.T, client *gophercloud.ServiceClient, qs *qos.QoS) { + t.Logf("Attempting to delete QoS: %s", qs.ID) + + deleteOpts := qos.DeleteOpts{ + Force: true, + } + + err := qos.Delete(client, qs.ID, deleteOpts).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete QoS %s: %v", qs.ID, err) + } + + t.Logf("Successfully deleted QoS: %s", qs.ID) +} diff --git a/acceptance/openstack/blockstorage/v3/qos_test.go b/acceptance/openstack/blockstorage/v3/qos_test.go new file mode 100644 index 0000000000..95ce68be48 --- /dev/null +++ b/acceptance/openstack/blockstorage/v3/qos_test.go @@ -0,0 +1,20 @@ +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestQoS(t *testing.T) { + clients.SkipRelease(t, "stable/mitaka") + clients.RequireAdmin(t) + + client, err := clients.NewBlockStorageV3Client() + th.AssertNoErr(t, err) + + qs, err := CreateQoS(t, client) + th.AssertNoErr(t, err) + defer DeleteQoS(t, client, qs) +} diff --git a/acceptance/openstack/blockstorage/v3/volumetypes_test.go b/acceptance/openstack/blockstorage/v3/volumetypes_test.go index 2afe90c1d1..e6e898df40 100644 --- a/acceptance/openstack/blockstorage/v3/volumetypes_test.go +++ b/acceptance/openstack/blockstorage/v3/volumetypes_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/gophercloud/gophercloud/acceptance/clients" + identity "github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3" "github.com/gophercloud/gophercloud/acceptance/tools" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumetypes" th "github.com/gophercloud/gophercloud/testhelper" @@ -55,3 +56,111 @@ func TestVolumeTypes(t *testing.T) { th.AssertEquals(t, description, newVT.Description) th.AssertEquals(t, isPublic, newVT.IsPublic) } + +func TestVolumeTypesExtraSpecs(t *testing.T) { + clients.SkipRelease(t, "stable/mitaka") + clients.RequireAdmin(t) + + client, err := clients.NewBlockStorageV3Client() + th.AssertNoErr(t, err) + + vt, err := CreateVolumeTypeNoExtraSpecs(t, client) + th.AssertNoErr(t, err) + defer DeleteVolumeType(t, client, vt) + + createOpts := volumetypes.ExtraSpecsOpts{ + "capabilities": "gpu", + "volume_backend_name": "ssd", + } + + createdExtraSpecs, err := volumetypes.CreateExtraSpecs(client, vt.ID, createOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, createdExtraSpecs) + + th.AssertEquals(t, len(createdExtraSpecs), 2) + th.AssertEquals(t, createdExtraSpecs["capabilities"], "gpu") + th.AssertEquals(t, createdExtraSpecs["volume_backend_name"], "ssd") + + err = volumetypes.DeleteExtraSpec(client, vt.ID, "volume_backend_name").ExtractErr() + th.AssertNoErr(t, err) + + updateOpts := volumetypes.ExtraSpecsOpts{ + "capabilities": "gpu-2", + } + updatedExtraSpec, err := volumetypes.UpdateExtraSpec(client, vt.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, updatedExtraSpec) + + th.AssertEquals(t, updatedExtraSpec["capabilities"], "gpu-2") + + allExtraSpecs, err := volumetypes.ListExtraSpecs(client, vt.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, allExtraSpecs) + + th.AssertEquals(t, len(allExtraSpecs), 1) + th.AssertEquals(t, allExtraSpecs["capabilities"], "gpu-2") + + singleSpec, err := volumetypes.GetExtraSpec(client, vt.ID, "capabilities").Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, singleSpec) + + th.AssertEquals(t, singleSpec["capabilities"], "gpu-2") +} + +func TestVolumeTypesAccess(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewBlockStorageV3Client() + th.AssertNoErr(t, err) + + identityClient, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + vt, err := CreatePrivateVolumeType(t, client) + th.AssertNoErr(t, err) + defer DeleteVolumeType(t, client, vt) + + project, err := identity.CreateProject(t, identityClient, nil) + th.AssertNoErr(t, err) + defer identity.DeleteProject(t, identityClient, project.ID) + + addAccessOpts := volumetypes.AddAccessOpts{ + Project: project.ID, + } + + err = volumetypes.AddAccess(client, vt.ID, addAccessOpts).ExtractErr() + th.AssertNoErr(t, err) + + allPages, err := volumetypes.ListAccesses(client, vt.ID).AllPages() + th.AssertNoErr(t, err) + + accessList, err := volumetypes.ExtractAccesses(allPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, accessList) + + th.AssertEquals(t, len(accessList), 1) + th.AssertEquals(t, accessList[0].ProjectID, project.ID) + th.AssertEquals(t, accessList[0].VolumeTypeID, vt.ID) + + removeAccessOpts := volumetypes.RemoveAccessOpts{ + Project: project.ID, + } + + err = volumetypes.RemoveAccess(client, vt.ID, removeAccessOpts).ExtractErr() + th.AssertNoErr(t, err) + + allPages, err = volumetypes.ListAccesses(client, vt.ID).AllPages() + th.AssertNoErr(t, err) + + accessList, err = volumetypes.ExtractAccesses(allPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, accessList) + + th.AssertEquals(t, len(accessList), 0) +} diff --git a/openstack/baremetal/apiversions/doc.go b/openstack/baremetal/apiversions/doc.go index 93b5adc885..7fbbac0d80 100644 --- a/openstack/baremetal/apiversions/doc.go +++ b/openstack/baremetal/apiversions/doc.go @@ -3,11 +3,21 @@ Package apiversions provides information about the versions supported by a speci Example to list versions - allVersions, err := apiversions.List(client.ServiceClient()).AllPages() + allVersions, err := apiversions.List(baremetalClient).Extract() + if err != nil { + panic("unable to get API versions: " + err.Error()) + } + + for _, version := range allVersions.Versions { + fmt.Printf("%+v\n", version) + } Example to get a specific version - actual, err := apiversions.Get(client.ServiceClient(), "v1").Extract() + actual, err := apiversions.Get(baremetalClient).Extract() + if err != nil { + panic("unable to get API version: " + err.Error()) + } */ package apiversions diff --git a/openstack/blockstorage/apiversions/doc.go b/openstack/blockstorage/apiversions/doc.go index 05470516c4..8c38b506bf 100644 --- a/openstack/blockstorage/apiversions/doc.go +++ b/openstack/blockstorage/apiversions/doc.go @@ -6,15 +6,15 @@ Example of Retrieving all API Versions allPages, err := apiversions.List(client).AllPages() if err != nil { - panic("Unable to get API versions: %s", err) + panic("unable to get API versions: " + err.Error()) } allVersions, err := apiversions.ExtractAPIVersions(allPages) if err != nil { - panic("Unable to extract API versions: %s", err) + panic("unable to extract API versions: " + err.Error()) } - for _, version := range versions { + for _, version := range allVersions { fmt.Printf("%+v\n", version) } @@ -23,7 +23,7 @@ Example of Retrieving an API Version version, err := apiversions.Get(client, "v3").Extract() if err != nil { - panic("Unable to get API version: %s", err) + panic("unable to get API version: " + err.Error()) } fmt.Printf("%+v\n", version) diff --git a/openstack/blockstorage/extensions/availabilityzones/doc.go b/openstack/blockstorage/extensions/availabilityzones/doc.go new file mode 100644 index 0000000000..0b9a5a6b58 --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/doc.go @@ -0,0 +1,21 @@ +/* +Package availabilityzones provides the ability to get lists of +available volume availability zones. + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(volumeClient).AllPages() + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } +*/ +package availabilityzones diff --git a/openstack/blockstorage/extensions/availabilityzones/requests.go b/openstack/blockstorage/extensions/availabilityzones/requests.go new file mode 100644 index 0000000000..df10b856eb --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/requests.go @@ -0,0 +1,13 @@ +package availabilityzones + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List will return the existing availability zones. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/openstack/blockstorage/extensions/availabilityzones/results.go b/openstack/blockstorage/extensions/availabilityzones/results.go new file mode 100644 index 0000000000..0e115411c1 --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/results.go @@ -0,0 +1,33 @@ +package availabilityzones + +import ( + "github.com/gophercloud/gophercloud/pagination" +) + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an OpenStack +// AvailabilityZone. +type AvailabilityZone struct { + // The availability zone name + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` +} + +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/openstack/blockstorage/extensions/availabilityzones/testing/doc.go b/openstack/blockstorage/extensions/availabilityzones/testing/doc.go new file mode 100644 index 0000000000..a4408d7a0d --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/testing/doc.go @@ -0,0 +1,2 @@ +// availabilityzones unittests +package testing diff --git a/openstack/blockstorage/extensions/availabilityzones/testing/fixtures.go b/openstack/blockstorage/extensions/availabilityzones/testing/fixtures.go new file mode 100644 index 0000000000..4b500e4843 --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/testing/fixtures.go @@ -0,0 +1,52 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + az "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/availabilityzones" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const GetOutput = ` +{ + "availabilityZoneInfo": [ + { + "zoneName": "internal", + "zoneState": { + "available": true + } + }, + { + "zoneName": "nova", + "zoneState": { + "available": true + } + } + ] +}` + +var AZResult = []az.AvailabilityZone{ + { + ZoneName: "internal", + ZoneState: az.ZoneState{Available: true}, + }, + { + ZoneName: "nova", + ZoneState: az.ZoneState{Available: true}, + }, +} + +// HandleGetSuccessfully configures the test server to respond to a Get request +// for availability zone information. +func HandleGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, GetOutput) + }) +} diff --git a/openstack/blockstorage/extensions/availabilityzones/testing/requests_test.go b/openstack/blockstorage/extensions/availabilityzones/testing/requests_test.go new file mode 100644 index 0000000000..39f41bf09f --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/testing/requests_test.go @@ -0,0 +1,25 @@ +package testing + +import ( + "testing" + + az "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/availabilityzones" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// Verifies that availability zones can be listed correctly +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + HandleGetSuccessfully(t) + + allPages, err := az.List(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + + actual, err := az.ExtractAvailabilityZones(allPages) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, AZResult, actual) +} diff --git a/openstack/blockstorage/extensions/availabilityzones/urls.go b/openstack/blockstorage/extensions/availabilityzones/urls.go new file mode 100644 index 0000000000..fb4cdcf4e2 --- /dev/null +++ b/openstack/blockstorage/extensions/availabilityzones/urls.go @@ -0,0 +1,7 @@ +package availabilityzones + +import "github.com/gophercloud/gophercloud" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} diff --git a/openstack/blockstorage/v3/qos/doc.go b/openstack/blockstorage/v3/qos/doc.go new file mode 100644 index 0000000000..23894b651b --- /dev/null +++ b/openstack/blockstorage/v3/qos/doc.go @@ -0,0 +1,36 @@ +/* +Package qos provides information and interaction with the QoS specifications +for the Openstack Blockstorage service. + +Example to create a QoS specification + + createOpts := qos.CreateOpts{ + Name: "test", + Consumer: qos.ConsumerFront, + Specs: map[string]string{ + "read_iops_sec": "20000", + }, + } + + test, err := qos.Create(client, createOpts).Extract() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("QoS: %+v\n", test) + +Example to delete a QoS specification + + qosID := "d6ae28ce-fcb5-4180-aa62-d260a27e09ae" + + deleteOpts := qos.DeleteOpts{ + Force: false, + } + + err = qos.Delete(client, qosID, deleteOpts).ExtractErr() + if err != nil { + log.Fatal(err) + } + +*/ +package qos diff --git a/openstack/blockstorage/v3/qos/requests.go b/openstack/blockstorage/v3/qos/requests.go new file mode 100644 index 0000000000..af02972f67 --- /dev/null +++ b/openstack/blockstorage/v3/qos/requests.go @@ -0,0 +1,100 @@ +package qos + +import ( + "github.com/gophercloud/gophercloud" +) + +type CreateOptsBuilder interface { + ToQoSCreateMap() (map[string]interface{}, error) +} + +type QoSConsumer string + +const ( + ConsumerFront QoSConsumer = "front-end" + ConsumberBack QoSConsumer = "back-end" + ConsumerBoth QoSConsumer = "both" +) + +// CreateOpts contains options for creating a QoS specification. +// This object is passed to the qos.Create function. +type CreateOpts struct { + // The name of the QoS spec + Name string `json:"name"` + // The consumer of the QoS spec. Possible values are + // both, front-end, back-end. + Consumer QoSConsumer `json:"consumer,omitempty"` + // Specs is a collection of miscellaneous key/values used to set + // specifications for the QoS + Specs map[string]string `json:"-"` +} + +// ToQoSCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToQoSCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "qos_specs") + if err != nil { + return nil, err + } + + if opts.Specs != nil { + if v, ok := b["qos_specs"].(map[string]interface{}); ok { + for key, value := range opts.Specs { + v[key] = value + } + } + } + + return b, nil +} + +// Create will create a new QoS based on the values in CreateOpts. To extract +// the QoS object from the response, call the Extract method on the +// CreateResult. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToQoSCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToQoSDeleteQuery() (string, error) +} + +// DeleteOpts contains options for deleting a QoS. This object is passed to +// the qos.Delete function. +type DeleteOpts struct { + // Delete a QoS specification even if it is in-use + Force bool `q:"force"` +} + +// ToQoSDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToQoSDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete will delete the existing QoS with the provided ID. +func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) { + url := deleteURL(client, id) + if opts != nil { + query, err := opts.ToQoSDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := client.Delete(url, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/blockstorage/v3/qos/results.go b/openstack/blockstorage/v3/qos/results.go new file mode 100644 index 0000000000..700616427c --- /dev/null +++ b/openstack/blockstorage/v3/qos/results.go @@ -0,0 +1,41 @@ +package qos + +import "github.com/gophercloud/gophercloud" + +// QoS contains all the information associated with an OpenStack QoS specification. +type QoS struct { + // Name is the name of the QoS. + Name string `json:"name"` + // Unique identifier for the QoS. + ID string `json:"id"` + // Consumer of QoS + Consumer string `json:"consumer"` + // Arbitrary key-value pairs defined by the user. + Specs map[string]string `json:"specs"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the QoS object out of the commonResult object. +func (r commonResult) Extract() (*QoS, error) { + var s QoS + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractInto converts our response data into a QoS struct +func (r commonResult) ExtractInto(qos interface{}) error { + return r.Result.ExtractIntoStructPtr(qos, "qos_specs") +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/openstack/blockstorage/v3/qos/testing/doc.go b/openstack/blockstorage/v3/qos/testing/doc.go new file mode 100644 index 0000000000..0155a0963d --- /dev/null +++ b/openstack/blockstorage/v3/qos/testing/doc.go @@ -0,0 +1,2 @@ +// Package testing for qos_v3 +package testing diff --git a/openstack/blockstorage/v3/qos/testing/fixtures.go b/openstack/blockstorage/v3/qos/testing/fixtures.go new file mode 100644 index 0000000000..9cad5ff9a2 --- /dev/null +++ b/openstack/blockstorage/v3/qos/testing/fixtures.go @@ -0,0 +1,62 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/qos" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +var createQoSExpected = qos.QoS{ + ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + Name: "qos-001", + Consumer: "front-end", + Specs: map[string]string{ + "read_iops_sec": "20000", + }, +} + +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc("/qos-specs", 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, ` +{ + "qos_specs": { + "name": "qos-001", + "consumer": "front-end", + "read_iops_sec": "20000" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "qos_specs": { + "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + "name": "qos-001", + "consumer": "front-end", + "specs": { + "read_iops_sec": "20000" + } + } +} + `) + }) +} + +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc("/qos-specs/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/blockstorage/v3/qos/testing/requests_test.go b/openstack/blockstorage/v3/qos/testing/requests_test.go new file mode 100644 index 0000000000..72e8ac4c7d --- /dev/null +++ b/openstack/blockstorage/v3/qos/testing/requests_test.go @@ -0,0 +1,37 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/qos" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := qos.CreateOpts{ + Name: "qos-001", + Consumer: qos.ConsumerFront, + Specs: map[string]string{ + "read_iops_sec": "20000", + }, + } + actual, err := qos.Create(client.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &createQoSExpected, actual) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + res := qos.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", qos.DeleteOpts{}) + th.AssertNoErr(t, res.Err) +} diff --git a/openstack/blockstorage/v3/qos/urls.go b/openstack/blockstorage/v3/qos/urls.go new file mode 100644 index 0000000000..3d28c53822 --- /dev/null +++ b/openstack/blockstorage/v3/qos/urls.go @@ -0,0 +1,11 @@ +package qos + +import "github.com/gophercloud/gophercloud" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("qos-specs") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("qos-specs", id) +} diff --git a/openstack/blockstorage/v3/volumetypes/doc.go b/openstack/blockstorage/v3/volumetypes/doc.go index 8b2769cb7c..4e48e9c022 100644 --- a/openstack/blockstorage/v3/volumetypes/doc.go +++ b/openstack/blockstorage/v3/volumetypes/doc.go @@ -58,6 +58,108 @@ Example to update a Volume Type panic(err) } fmt.Println(volumetype) -*/ +Example to Create Extra Specs for a Volume Type + + typeID := "7ffaca22-f646-41d4-b79d-d7e4452ef8cc" + + createOpts := volumetypes.ExtraSpecsOpts{ + "capabilities": "gpu", + } + createdExtraSpecs, err := volumetypes.CreateExtraSpecs(client, typeID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Volume Type + + typeID := "7ffaca22-f646-41d4-b79d-d7e4452ef8cc" + + extraSpecs, err := volumetypes.ListExtraSpecs(client, typeID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Get specific Extra Spec for a Volume Type + + typeID := "7ffaca22-f646-41d4-b79d-d7e4452ef8cc" + + extraSpec, err := volumetypes.GetExtraSpec(client, typeID, "capabilities").Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpec) + +Example to Update Extra Specs for a Volume Type + + typeID := "7ffaca22-f646-41d4-b79d-d7e4452ef8cc" + + updateOpts := volumetypes.ExtraSpecsOpts{ + "capabilities": "capabilities-updated", + } + updatedExtraSpec, err := volumetypes.UpdateExtraSpec(client, typeID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Volume Type + + typeID := "7ffaca22-f646-41d4-b79d-d7e4452ef8cc" + err := volumetypes.DeleteExtraSpec(client, typeID, "capabilities").ExtractErr() + if err != nil { + panic(err) + } + +Example to List Volume Type Access + + typeID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := volumetypes.ListAccesses(client, typeID).AllPages() + if err != nil { + panic(err) + } + + allAccesses, err := volumetypes.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Volume Type + + typeID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := volumetypes.AddAccessOpts{ + Project: "15153a0979884b59b0592248ef947921", + } + + err := volumetypes.AddAccess(client, typeID, accessOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Volume Type + + typeID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := volumetypes.RemoveAccessOpts{ + Project: "15153a0979884b59b0592248ef947921", + } + + err := volumetypes.RemoveAccess(client, typeID, accessOpts).ExtractErr() + if err != nil { + panic(err) + } + +*/ package volumetypes diff --git a/openstack/blockstorage/v3/volumetypes/requests.go b/openstack/blockstorage/v3/volumetypes/requests.go index 869d426bf6..5b272bf05b 100644 --- a/openstack/blockstorage/v3/volumetypes/requests.go +++ b/openstack/blockstorage/v3/volumetypes/requests.go @@ -22,7 +22,7 @@ type CreateOpts struct { // the ID of the existing volume snapshot IsPublic *bool `json:"os-volume-type-access:is_public,omitempty"` // Extra spec key-value pairs defined by the user. - ExtraSpecs map[string]string `json:"extra_specs"` + ExtraSpecs map[string]string `json:"extra_specs,omitempty"` } // ToVolumeTypeCreateMap assembles a request body based on the contents of a @@ -120,7 +120,7 @@ type UpdateOpts struct { IsPublic *bool `json:"is_public,omitempty"` } -// ToVolumeUpdateMap assembles a request body based on the contents of an +// ToVolumeTypeUpdateMap assembles a request body based on the contents of an // UpdateOpts. func (opts UpdateOpts) ToVolumeTypeUpdateMap() (map[string]interface{}, error) { return gophercloud.BuildRequestBody(opts, "volume_type") @@ -140,3 +140,167 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return } + +// ListExtraSpecs requests all the extra-specs for the given volume type ID. +func ListExtraSpecs(client *gophercloud.ServiceClient, volumeTypeID string) (r ListExtraSpecsResult) { + resp, err := client.Get(extraSpecsListURL(client, volumeTypeID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetExtraSpec requests an extra-spec specified by key for the given volume type ID +func GetExtraSpec(client *gophercloud.ServiceClient, volumeTypeID string, key string) (r GetExtraSpecResult) { + resp, err := client.Get(extraSpecsGetURL(client, volumeTypeID, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// CreateExtraSpecs requests. +type CreateExtraSpecsOptsBuilder interface { + ToVolumeTypeExtraSpecsCreateMap() (map[string]interface{}, error) +} + +// ExtraSpecsOpts is a map that contains key-value pairs. +type ExtraSpecsOpts map[string]string + +// ToVolumeTypeExtraSpecsCreateMap assembles a body for a Create request based on +// the contents of ExtraSpecsOpts. +func (opts ExtraSpecsOpts) ToVolumeTypeExtraSpecsCreateMap() (map[string]interface{}, error) { + return map[string]interface{}{"extra_specs": opts}, nil +} + +// CreateExtraSpecs will create or update the extra-specs key-value pairs for +// the specified volume type. +func CreateExtraSpecs(client *gophercloud.ServiceClient, volumeTypeID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { + b, err := opts.ToVolumeTypeExtraSpecsCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(extraSpecsCreateURL(client, volumeTypeID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateExtraSpecOptsBuilder interface { + ToVolumeTypeExtraSpecUpdateMap() (map[string]string, string, error) +} + +// ToVolumeTypeExtraSpecUpdateMap assembles a body for an Update request based on +// the contents of a ExtraSpecOpts. +func (opts ExtraSpecsOpts) ToVolumeTypeExtraSpecUpdateMap() (map[string]string, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "volumetypes.ExtraSpecOpts" + err.Info = "Must have one and only one key-value pair" + return nil, "", err + } + + var key string + for k := range opts { + key = k + } + + return opts, key, nil +} + +// UpdateExtraSpec will updates the value of the specified volume type's extra spec +// for the key in opts. +func UpdateExtraSpec(client *gophercloud.ServiceClient, volumeTypeID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { + b, key, err := opts.ToVolumeTypeExtraSpecUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(extraSpecUpdateURL(client, volumeTypeID, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteExtraSpec will delete the key-value pair with the given key for the given +// volume type ID. +func DeleteExtraSpec(client *gophercloud.ServiceClient, volumeTypeID, key string) (r DeleteExtraSpecResult) { + resp, err := client.Delete(extraSpecDeleteURL(client, volumeTypeID, key), &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccesses retrieves the tenants which have access to a volume type. +func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager { + url := accessURL(client, id) + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessPage{pagination.SinglePageBase(r)} + }) +} + +// AddAccessOptsBuilder allows extensions to add additional parameters to the +// AddAccess requests. +type AddAccessOptsBuilder interface { + ToVolumeTypeAddAccessMap() (map[string]interface{}, error) +} + +// AddAccessOpts represents options for adding access to a volume type. +type AddAccessOpts struct { + // Project is the project/tenant ID to grant access. + Project string `json:"project"` +} + +// ToVolumeTypeAddAccessMap constructs a request body from AddAccessOpts. +func (opts AddAccessOpts) ToVolumeTypeAddAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "addProjectAccess") +} + +// AddAccess grants a tenant/project access to a volume type. +func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { + b, err := opts.ToVolumeTypeAddAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(accessActionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// RemoveAccess requests. +type RemoveAccessOptsBuilder interface { + ToVolumeTypeRemoveAccessMap() (map[string]interface{}, error) +} + +// RemoveAccessOpts represents options for removing access to a volume type. +type RemoveAccessOpts struct { + // Project is the project/tenant ID to remove access. + Project string `json:"project"` +} + +// ToVolumeTypeRemoveAccessMap constructs a request body from RemoveAccessOpts. +func (opts RemoveAccessOpts) ToVolumeTypeRemoveAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "removeProjectAccess") +} + +// RemoveAccess removes/revokes a tenant/project access to a volume type. +func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { + b, err := opts.ToVolumeTypeRemoveAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(accessActionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/blockstorage/v3/volumetypes/results.go b/openstack/blockstorage/v3/volumetypes/results.go index 2e3169070f..72c696ef13 100644 --- a/openstack/blockstorage/v3/volumetypes/results.go +++ b/openstack/blockstorage/v3/volumetypes/results.go @@ -5,7 +5,7 @@ import ( "github.com/gophercloud/gophercloud/pagination" ) -// Volume Type contains all the information associated with an OpenStack Volume Type. +// VolumeType contains all the information associated with an OpenStack Volume Type. type VolumeType struct { // Unique identifier for the volume type. ID string `json:"id"` @@ -68,7 +68,7 @@ func (r commonResult) ExtractInto(v interface{}) error { return r.Result.ExtractIntoStructPtr(v, "volume_type") } -// ExtractVolumesInto similar to ExtractInto but operates on a `list` of volume types +// ExtractVolumeTypesInto similar to ExtractInto but operates on a `list` of volume types func ExtractVolumeTypesInto(r pagination.Page, v interface{}) error { return r.(VolumeTypePage).Result.ExtractIntoSlicePtr(v, "volume_types") } @@ -92,3 +92,103 @@ type DeleteResult struct { type UpdateResult struct { commonResult } + +// extraSpecsResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type extraSpecsResult struct { + gophercloud.Result +} + +// ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type ListExtraSpecsResult struct { + extraSpecsResult +} + +// CreateExtraSpecsResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateExtraSpecsResult struct { + extraSpecsResult +} + +// Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +func (r extraSpecsResult) Extract() (map[string]string, error) { + var s struct { + ExtraSpecs map[string]string `json:"extra_specs"` + } + err := r.ExtractInto(&s) + return s.ExtraSpecs, err +} + +// extraSpecResult contains the result of a call for individual a single +// key-value pair. +type extraSpecResult struct { + gophercloud.Result +} + +// GetExtraSpecResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetExtraSpecResult struct { + extraSpecResult +} + +// UpdateExtraSpecResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateExtraSpecResult struct { + extraSpecResult +} + +// DeleteExtraSpecResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteExtraSpecResult struct { + gophercloud.ErrResult +} + +// Extract interprets any extraSpecResult as an ExtraSpec, if possible. +func (r extraSpecResult) Extract() (map[string]string, error) { + var s map[string]string + err := r.ExtractInto(&s) + return s, err +} + +// VolumeTypeAccess represents an ACL of project access to a specific Volume Type. +type VolumeTypeAccess struct { + // VolumeTypeID is the unique ID of the volume type. + VolumeTypeID string `json:"volume_type_id"` + + // ProjectID is the unique ID of the project. + ProjectID string `json:"project_id"` +} + +// AccessPage contains a single page of all VolumeTypeAccess entries for a volume type. +type AccessPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether an AccessPage is empty. +func (page AccessPage) IsEmpty() (bool, error) { + v, err := ExtractAccesses(page) + return len(v) == 0, err +} + +// ExtractAccesses interprets a page of results as a slice of VolumeTypeAccess. +func ExtractAccesses(r pagination.Page) ([]VolumeTypeAccess, error) { + var s struct { + VolumeTypeAccesses []VolumeTypeAccess `json:"volume_type_access"` + } + err := (r.(AccessPage)).ExtractInto(&s) + return s.VolumeTypeAccesses, err +} + +// AddAccessResult is the response from a AddAccess request. Call its +// ExtractErr method to determine if the request succeeded or failed. +type AddAccessResult struct { + gophercloud.ErrResult +} + +// RemoveAccessResult is the response from a RemoveAccess request. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RemoveAccessResult struct { + gophercloud.ErrResult +} diff --git a/openstack/blockstorage/v3/volumetypes/testing/fixtures.go b/openstack/blockstorage/v3/volumetypes/testing/fixtures.go index 979aee174c..eb617f19e5 100644 --- a/openstack/blockstorage/v3/volumetypes/testing/fixtures.go +++ b/openstack/blockstorage/v3/volumetypes/testing/fixtures.go @@ -152,3 +152,109 @@ func MockUpdateResponse(t *testing.T) { }`) }) } + +// ExtraSpecsGetBody provides a GET result of the extra_specs for a volume type +const ExtraSpecsGetBody = ` +{ + "extra_specs" : { + "capabilities": "gpu", + "volume_backend_name": "ssd" + } +} +` + +// GetExtraSpecBody provides a GET result of a particular extra_spec for a volume type +const GetExtraSpecBody = ` +{ + "capabilities": "gpu" +} +` + +// UpdatedExtraSpecBody provides an PUT result of a particular updated extra_spec for a volume type +const UpdatedExtraSpecBody = ` +{ + "capabilities": "gpu-2" +} +` + +// ExtraSpecs is the expected extra_specs returned from GET on a volume type's extra_specs +var ExtraSpecs = map[string]string{ + "capabilities": "gpu", + "volume_backend_name": "ssd", +} + +// ExtraSpec is the expected extra_spec returned from GET on a volume type's extra_specs +var ExtraSpec = map[string]string{ + "capabilities": "gpu", +} + +// UpdatedExtraSpec is the expected extra_spec returned from PUT on a volume type's extra_specs +var UpdatedExtraSpec = map[string]string{ + "capabilities": "gpu-2", +} + +func HandleExtraSpecsListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/types/1/extra_specs", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ExtraSpecsGetBody) + }) +} + +func HandleExtraSpecGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/types/1/extra_specs/capabilities", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetExtraSpecBody) + }) +} + +func HandleExtraSpecsCreateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/types/1/extra_specs", 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, "Accept", "application/json") + th.TestJSONRequest(t, r, `{ + "extra_specs": { + "capabilities": "gpu", + "volume_backend_name": "ssd" + } + }`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ExtraSpecsGetBody) + }) +} + +func HandleExtraSpecUpdateSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/types/1/extra_specs/capabilities", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, `{ + "capabilities": "gpu-2" + }`) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, UpdatedExtraSpecBody) + }) +} + +func HandleExtraSpecDeleteSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/types/1/extra_specs/capabilities", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.WriteHeader(http.StatusAccepted) + }) +} diff --git a/openstack/blockstorage/v3/volumetypes/testing/requests_test.go b/openstack/blockstorage/v3/volumetypes/testing/requests_test.go index b1435805d9..eb6f2e7c0e 100644 --- a/openstack/blockstorage/v3/volumetypes/testing/requests_test.go +++ b/openstack/blockstorage/v3/volumetypes/testing/requests_test.go @@ -1,6 +1,9 @@ package testing import ( + "fmt" + "net/http" + "reflect" "testing" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumetypes" @@ -117,3 +120,159 @@ func TestUpdate(t *testing.T) { th.CheckEquals(t, "vol-type-002", v.Name) th.CheckEquals(t, true, v.IsPublic) } + +func TestVolumeTypeExtraSpecsList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleExtraSpecsListSuccessfully(t) + + expected := ExtraSpecs + actual, err := volumetypes.ListExtraSpecs(client.ServiceClient(), "1").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestVolumeTypeExtraSpecGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleExtraSpecGetSuccessfully(t) + + expected := ExtraSpec + actual, err := volumetypes.GetExtraSpec(client.ServiceClient(), "1", "capabilities").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestVolumeTypeExtraSpecsCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleExtraSpecsCreateSuccessfully(t) + + createOpts := volumetypes.ExtraSpecsOpts{ + "capabilities": "gpu", + "volume_backend_name": "ssd", + } + expected := ExtraSpecs + actual, err := volumetypes.CreateExtraSpecs(client.ServiceClient(), "1", createOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestVolumeTypeExtraSpecUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleExtraSpecUpdateSuccessfully(t) + + updateOpts := volumetypes.ExtraSpecsOpts{ + "capabilities": "gpu-2", + } + expected := UpdatedExtraSpec + actual, err := volumetypes.UpdateExtraSpec(client.ServiceClient(), "1", updateOpts).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, expected, actual) +} + +func TestVolumeTypeExtraSpecDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleExtraSpecDeleteSuccessfully(t) + + res := volumetypes.DeleteExtraSpec(client.ServiceClient(), "1", "capabilities") + th.AssertNoErr(t, res.Err) +} + +func TestVolumeTypeListAccesses(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types/a5082c24-2a27-43a4-b48e-fcec1240e36b/os-volume-type-access", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "volume_type_access": [ + { + "project_id": "6f70656e737461636b20342065766572", + "volume_type_id": "a5082c24-2a27-43a4-b48e-fcec1240e36b" + } + ] + } + `) + }) + + expected := []volumetypes.VolumeTypeAccess{ + { + VolumeTypeID: "a5082c24-2a27-43a4-b48e-fcec1240e36b", + ProjectID: "6f70656e737461636b20342065766572", + }, + } + + allPages, err := volumetypes.ListAccesses(client.ServiceClient(), "a5082c24-2a27-43a4-b48e-fcec1240e36b").AllPages() + th.AssertNoErr(t, err) + + actual, err := volumetypes.ExtractAccesses(allPages) + th.AssertNoErr(t, err) + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} + +func TestVolumeTypeAddAccess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types/a5082c24-2a27-43a4-b48e-fcec1240e36b/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "addProjectAccess": { + "project": "6f70656e737461636b20342065766572" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) + + addAccessOpts := volumetypes.AddAccessOpts{ + Project: "6f70656e737461636b20342065766572", + } + + err := volumetypes.AddAccess(client.ServiceClient(), "a5082c24-2a27-43a4-b48e-fcec1240e36b", addAccessOpts).ExtractErr() + th.AssertNoErr(t, err) + +} + +func TestVolumeTypeRemoveAccess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/types/a5082c24-2a27-43a4-b48e-fcec1240e36b/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "accept", "application/json") + th.TestJSONRequest(t, r, ` + { + "removeProjectAccess": { + "project": "6f70656e737461636b20342065766572" + } + } + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) + + removeAccessOpts := volumetypes.RemoveAccessOpts{ + Project: "6f70656e737461636b20342065766572", + } + + err := volumetypes.RemoveAccess(client.ServiceClient(), "a5082c24-2a27-43a4-b48e-fcec1240e36b", removeAccessOpts).ExtractErr() + th.AssertNoErr(t, err) + +} diff --git a/openstack/blockstorage/v3/volumetypes/urls.go b/openstack/blockstorage/v3/volumetypes/urls.go index f794af86d1..c63ee47e62 100644 --- a/openstack/blockstorage/v3/volumetypes/urls.go +++ b/openstack/blockstorage/v3/volumetypes/urls.go @@ -21,3 +21,31 @@ func deleteURL(c *gophercloud.ServiceClient, id string) string { func updateURL(c *gophercloud.ServiceClient, id string) string { return c.ServiceURL("types", id) } + +func extraSpecsListURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("types", id, "extra_specs") +} + +func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("types", id, "extra_specs", key) +} + +func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("types", id, "extra_specs") +} + +func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("types", id, "extra_specs", key) +} + +func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("types", id, "extra_specs", key) +} + +func accessURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("types", id, "os-volume-type-access") +} + +func accessActionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("types", id, "action") +} diff --git a/openstack/identity/v3/roles/requests.go b/openstack/identity/v3/roles/requests.go index 007a15b9cc..851dae0a6e 100644 --- a/openstack/identity/v3/roles/requests.go +++ b/openstack/identity/v3/roles/requests.go @@ -204,6 +204,10 @@ type ListAssignmentsOpts struct { // Effective lists effective assignments at the user, project, and domain // level, allowing for the effects of group membership. Effective *bool `q:"effective"` + + // IncludeNames indicates whether to include names of any returned entities. + // Requires microversion 3.6 or later. + IncludeNames *bool `q:"include_names"` } // ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string. diff --git a/openstack/identity/v3/roles/results.go b/openstack/identity/v3/roles/results.go index 631c992a3c..da2c348e77 100644 --- a/openstack/identity/v3/roles/results.go +++ b/openstack/identity/v3/roles/results.go @@ -138,7 +138,8 @@ type RoleAssignment struct { // AssignedRole represents a Role in an assignment. type AssignedRole struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } // Scope represents a scope in a Role assignment. @@ -149,22 +150,26 @@ type Scope struct { // Domain represents a domain in a role assignment scope. type Domain struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } // Project represents a project in a role assignment scope. type Project struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } // User represents a user in a role assignment scope. type User struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } // Group represents a group in a role assignment scope. type Group struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } // RoleAssignmentPage is a single page of RoleAssignments results. diff --git a/openstack/identity/v3/roles/testing/fixtures.go b/openstack/identity/v3/roles/testing/fixtures.go index 9bcc2c7d07..bcbd8e2522 100644 --- a/openstack/identity/v3/roles/testing/fixtures.go +++ b/openstack/identity/v3/roles/testing/fixtures.go @@ -96,6 +96,7 @@ const UpdateOutput = ` } ` +// ListAssignmentOutput provides a result of ListAssignment request. const ListAssignmentOutput = ` { "role_assignments": [ @@ -141,6 +142,38 @@ const ListAssignmentOutput = ` } ` +// ListAssignmentWithNamesOutput provides a result of ListAssignment request with IncludeNames option. +const ListAssignmentWithNamesOutput = ` +{ + "role_assignments": [ + { + "links": { + "assignment": "http://identity:35357/v3/domains/161718/users/313233/roles/123456" + }, + "role": { + "id": "123456", + "name": "include_names_role" + }, + "scope": { + "domain": { + "id": "161718", + "name": "52833" + } + }, + "user": { + "id": "313233", + "name": "example-user-name" + } + } + ], + "links": { + "self": "http://identity:35357/v3/role_assignments?include_names=True", + "previous": null, + "next": null + } +} +` + // ListAssignmentsOnResourceOutput provides a result of ListAssignmentsOnResource request. const ListAssignmentsOnResourceOutput = ` { @@ -337,10 +370,22 @@ var SecondRoleAssignment = roles.RoleAssignment{ Group: roles.Group{}, } +// ThirdRoleAssignment is the third role assignment that has entity names in the List request. +var ThirdRoleAssignment = roles.RoleAssignment{ + Role: roles.AssignedRole{ID: "123456", Name: "include_names_role"}, + Scope: roles.Scope{Domain: roles.Domain{ID: "161718", Name: "52833"}}, + User: roles.User{ID: "313233", Name: "example-user-name"}, + Group: roles.Group{}, +} + // ExpectedRoleAssignmentsSlice is the slice of role assignments expected to be // returned from ListAssignmentOutput. var ExpectedRoleAssignmentsSlice = []roles.RoleAssignment{FirstRoleAssignment, SecondRoleAssignment} +// ExpectedRoleAssignmentsWithNamesSlice is the slice of role assignments expected to be +// returned from ListAssignmentWithNamesOutput. +var ExpectedRoleAssignmentsWithNamesSlice = []roles.RoleAssignment{ThirdRoleAssignment} + // HandleListRoleAssignmentsSuccessfully creates an HTTP handler at `/role_assignments` on the // test handler mux that responds with a list of two role assignments. func HandleListRoleAssignmentsSuccessfully(t *testing.T) { @@ -355,6 +400,21 @@ func HandleListRoleAssignmentsSuccessfully(t *testing.T) { }) } +// HandleListRoleAssignmentsSuccessfully creates an HTTP handler at `/role_assignments` on the +// test handler mux that responds with a list of two role assignments. +func HandleListRoleAssignmentsWithNamesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/role_assignments", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.AssertEquals(t, "include_names=true", r.URL.RawQuery) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListAssignmentWithNamesOutput) + }) +} + // RoleOnResource is the role in the ListAssignmentsOnResource request. var RoleOnResource = roles.Role{ ID: "9fe1d3", diff --git a/openstack/identity/v3/roles/testing/requests_test.go b/openstack/identity/v3/roles/testing/requests_test.go index c8ac5a9b03..d574d8ba93 100644 --- a/openstack/identity/v3/roles/testing/requests_test.go +++ b/openstack/identity/v3/roles/testing/requests_test.go @@ -147,6 +147,30 @@ func TestListAssignmentsSinglePage(t *testing.T) { th.CheckEquals(t, count, 1) } +func TestListAssignmentsWithNamesSinglePage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListRoleAssignmentsWithNamesSuccessfully(t) + + var includeNames = true + listOpts := roles.ListAssignmentsOpts{ + IncludeNames: &includeNames, + } + + count := 0 + err := roles.ListAssignments(client.ServiceClient(), listOpts).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := roles.ExtractRoleAssignments(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedRoleAssignmentsWithNamesSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + func TestListAssignmentsOnResource_ProjectsUsers(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/networking/v2/extensions/quotas/results.go b/openstack/networking/v2/extensions/quotas/results.go index 99f28771b0..4748ace52c 100644 --- a/openstack/networking/v2/extensions/quotas/results.go +++ b/openstack/networking/v2/extensions/quotas/results.go @@ -1,6 +1,12 @@ package quotas -import "github.com/gophercloud/gophercloud" +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/gophercloud/gophercloud" +) type commonResult struct { gophercloud.Result @@ -128,3 +134,40 @@ type QuotaDetail struct { // allocated/provisioned. This is what "quota" usually refers to. Limit int `json:"limit"` } + +// UnmarshalJSON overrides the default unmarshalling function to accept +// Reserved as a string. +// +// Due to a bug in Neutron, under some conditions Reserved is returned as a +// string. +// +// This method is left for compatibility with unpatched versions of Neutron. +// +// cf. https://bugs.launchpad.net/neutron/+bug/1918565 +func (q *QuotaDetail) UnmarshalJSON(b []byte) error { + type tmp QuotaDetail + var s struct { + tmp + Reserved interface{} `json:"reserved"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *q = QuotaDetail(s.tmp) + + switch t := s.Reserved.(type) { + case float64: + q.Reserved = int(t) + case string: + if q.Reserved, err = strconv.Atoi(t); err != nil { + return err + } + default: + return fmt.Errorf("reserved has unexpected type: %T", t) + } + + return nil +} diff --git a/openstack/networking/v2/extensions/quotas/testing/fixtures.go b/openstack/networking/v2/extensions/quotas/testing/fixtures.go index b81a1eed17..de4b579ce5 100644 --- a/openstack/networking/v2/extensions/quotas/testing/fixtures.go +++ b/openstack/networking/v2/extensions/quotas/testing/fixtures.go @@ -22,6 +22,11 @@ const GetResponseRaw = ` ` // GetDetailedResponseRaw is a sample response to a Get call with the detailed option. +// +// One "reserved" property is returned as a string to reflect a buggy behaviour +// of Neutron. +// +// cf. https://bugs.launchpad.net/neutron/+bug/1918565 const GetDetailedResponseRaw = ` { "quota" : { @@ -38,7 +43,7 @@ const GetDetailedResponseRaw = ` "port" : { "used": 0, "limit": 25, - "reserved": 0 + "reserved": "0" }, "rbac_policy" : { "used": 0, diff --git a/openstack/sharedfilesystems/apiversions/doc.go b/openstack/sharedfilesystems/apiversions/doc.go index 841a9c578c..dc0a55ebad 100644 --- a/openstack/sharedfilesystems/apiversions/doc.go +++ b/openstack/sharedfilesystems/apiversions/doc.go @@ -1,3 +1,30 @@ -// Package apiversions provides information and interaction with the different -// API versions for the Shared File System service, code-named Manila. +/* +Package apiversions provides information and interaction with the different +API versions for the Shared File System service, code-named Manila. + +Example to List API Versions + + allPages, err := apiversions.List(client).AllPages() + if err != nil { + panic(err) + } + + allVersions, err := apiversions.ExtractAPIVersions(allPages) + if err != nil { + panic(err) + } + + for _, version := range allVersions { + fmt.Printf("%+v\n", version) + } + +Example to Get an API Version + + version, err := apiVersions.Get(client, "v2.1").Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", version) +*/ package apiversions