Backport openstack/blockstorage/extensions/quotasets/
Change-Id: I4ab546714af9bfce738d3d0cfa6f86ed7f889d1d
Related-PROD: PROD-34272
diff --git a/openstack/blockstorage/extensions/quotasets/doc.go b/openstack/blockstorage/extensions/quotasets/doc.go
new file mode 100644
index 0000000..109f78f
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/doc.go
@@ -0,0 +1,42 @@
+/*
+Package quotasets enables retrieving and managing Block Storage quotas.
+
+Example to Get a Quota Set
+
+ quotaset, err := quotasets.Get(blockStorageClient, "project-id").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", quotaset)
+
+Example to Get Quota Set Usage
+
+ quotaset, err := quotasets.GetUsage(blockStorageClient, "project-id").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", quotaset)
+
+Example to Update a Quota Set
+
+ updateOpts := quotasets.UpdateOpts{
+ Volumes: gophercloud.IntToPointer(100),
+ }
+
+ quotaset, err := quotasets.Update(blockStorageClient, "project-id", updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", quotaset)
+
+Example to Delete a Quota Set
+
+ err := quotasets.Delete(blockStorageClient, "project-id").ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+*/
+package quotasets
diff --git a/openstack/blockstorage/extensions/quotasets/requests.go b/openstack/blockstorage/extensions/quotasets/requests.go
new file mode 100644
index 0000000..dd4a051
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/requests.go
@@ -0,0 +1,94 @@
+package quotasets
+
+import (
+ "fmt"
+
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git"
+)
+
+// Get returns public data about a previously created QuotaSet.
+func Get(client *gophercloud.ServiceClient, projectID string) (r GetResult) {
+ _, r.Err = client.Get(getURL(client, projectID), &r.Body, nil)
+ return
+}
+
+// GetDefaults returns public data about the project's default block storage quotas.
+func GetDefaults(client *gophercloud.ServiceClient, projectID string) (r GetResult) {
+ _, r.Err = client.Get(getDefaultsURL(client, projectID), &r.Body, nil)
+ return
+}
+
+// GetUsage returns detailed public data about a previously created QuotaSet.
+func GetUsage(client *gophercloud.ServiceClient, projectID string) (r GetUsageResult) {
+ u := fmt.Sprintf("%s?usage=true", getURL(client, projectID))
+ _, r.Err = client.Get(u, &r.Body, nil)
+ return
+}
+
+// Updates the quotas for the given projectID and returns the new QuotaSet.
+func Update(client *gophercloud.ServiceClient, projectID string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToBlockStorageQuotaUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+
+ _, r.Err = client.Put(updateURL(client, projectID), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return r
+}
+
+// UpdateOptsBuilder enables extensins to add parameters to the update request.
+type UpdateOptsBuilder interface {
+ // Extra specific name to prevent collisions with interfaces for other quotas
+ // (e.g. neutron)
+ ToBlockStorageQuotaUpdateMap() (map[string]interface{}, error)
+}
+
+// ToBlockStorageQuotaUpdateMap builds the update options into a serializable
+// format.
+func (opts UpdateOpts) ToBlockStorageQuotaUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "quota_set")
+}
+
+// Options for Updating the quotas of a Tenant.
+// All int-values are pointers so they can be nil if they are not needed.
+// You can use gopercloud.IntToPointer() for convenience
+type UpdateOpts struct {
+ // Volumes is the number of volumes that are allowed for each project.
+ Volumes *int `json:"volumes,omitempty"`
+
+ // Snapshots is the number of snapshots that are allowed for each project.
+ Snapshots *int `json:"snapshots,omitempty"`
+
+ // Gigabytes is the size (GB) of volumes and snapshots that are allowed for
+ // each project.
+ Gigabytes *int `json:"gigabytes,omitempty"`
+
+ // PerVolumeGigabytes is the size (GB) of volumes and snapshots that are
+ // allowed for each project and the specifed volume type.
+ PerVolumeGigabytes *int `json:"per_volume_gigabytes,omitempty"`
+
+ // Backups is the number of backups that are allowed for each project.
+ Backups *int `json:"backups,omitempty"`
+
+ // BackupGigabytes is the size (GB) of backups that are allowed for each
+ // project.
+ BackupGigabytes *int `json:"backup_gigabytes,omitempty"`
+
+ // Groups is the number of groups that are allowed for each project.
+ Groups *int `json:"groups,omitempty"`
+
+ // Force will update the quotaset even if the quota has already been used
+ // and the reserved quota exceeds the new quota.
+ Force bool `json:"force,omitempty"`
+}
+
+// Resets the quotas for the given tenant to their default values.
+func Delete(client *gophercloud.ServiceClient, projectID string) (r DeleteResult) {
+ _, r.Err = client.Delete(updateURL(client, projectID), &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/blockstorage/extensions/quotasets/results.go b/openstack/blockstorage/extensions/quotasets/results.go
new file mode 100644
index 0000000..23e71b0
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/results.go
@@ -0,0 +1,173 @@
+package quotasets
+
+import (
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git"
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
+)
+
+// QuotaSet is a set of operational limits that allow for control of block
+// storage usage.
+type QuotaSet struct {
+ // ID is project associated with this QuotaSet.
+ ID string `json:"id"`
+
+ // Volumes is the number of volumes that are allowed for each project.
+ Volumes int `json:"volumes"`
+
+ // Snapshots is the number of snapshots that are allowed for each project.
+ Snapshots int `json:"snapshots"`
+
+ // Gigabytes is the size (GB) of volumes and snapshots that are allowed for
+ // each project.
+ Gigabytes int `json:"gigabytes"`
+
+ // PerVolumeGigabytes is the size (GB) of volumes and snapshots that are
+ // allowed for each project and the specifed volume type.
+ PerVolumeGigabytes int `json:"per_volume_gigabytes"`
+
+ // Backups is the number of backups that are allowed for each project.
+ Backups int `json:"backups"`
+
+ // BackupGigabytes is the size (GB) of backups that are allowed for each
+ // project.
+ BackupGigabytes int `json:"backup_gigabytes"`
+
+ // Groups is the number of groups that are allowed for each project.
+ Groups int `json:"groups,omitempty"`
+}
+
+// QuotaUsageSet represents details of both operational limits of block
+// storage resources and the current usage of those resources.
+type QuotaUsageSet struct {
+ // ID is the project ID associated with this QuotaUsageSet.
+ ID string `json:"id"`
+
+ // Volumes is the volume usage information for this project, including
+ // in_use, limit, reserved and allocated attributes. Note: allocated
+ // attribute is available only when nested quota is enabled.
+ Volumes QuotaUsage `json:"volumes"`
+
+ // Snapshots is the snapshot usage information for this project, including
+ // in_use, limit, reserved and allocated attributes. Note: allocated
+ // attribute is available only when nested quota is enabled.
+ Snapshots QuotaUsage `json:"snapshots"`
+
+ // Gigabytes is the size (GB) usage information of volumes and snapshots
+ // for this project, including in_use, limit, reserved and allocated
+ // attributes. Note: allocated attribute is available only when nested
+ // quota is enabled.
+ Gigabytes QuotaUsage `json:"gigabytes"`
+
+ // PerVolumeGigabytes is the size (GB) usage information for each volume,
+ // including in_use, limit, reserved and allocated attributes. Note:
+ // allocated attribute is available only when nested quota is enabled and
+ // only limit is meaningful here.
+ PerVolumeGigabytes QuotaUsage `json:"per_volume_gigabytes"`
+
+ // Backups is the backup usage information for this project, including
+ // in_use, limit, reserved and allocated attributes. Note: allocated
+ // attribute is available only when nested quota is enabled.
+ Backups QuotaUsage `json:"backups"`
+
+ // BackupGigabytes is the size (GB) usage information of backup for this
+ // project, including in_use, limit, reserved and allocated attributes.
+ // Note: allocated attribute is available only when nested quota is
+ // enabled.
+ BackupGigabytes QuotaUsage `json:"backup_gigabytes"`
+
+ // Groups is the number of groups that are allowed for each project.
+ // Note: allocated attribute is available only when nested quota is
+ // enabled.
+ Groups QuotaUsage `json:"groups"`
+}
+
+// QuotaUsage is a set of details about a single operational limit that allows
+// for control of block storage usage.
+type QuotaUsage struct {
+ // InUse is the current number of provisioned resources of the given type.
+ InUse int `json:"in_use"`
+
+ // Allocated is the current number of resources of a given type allocated
+ // for use. It is only available when nested quota is enabled.
+ Allocated int `json:"allocated"`
+
+ // Reserved is a transitional state when a claim against quota has been made
+ // but the resource is not yet fully online.
+ Reserved int `json:"reserved"`
+
+ // Limit is the maximum number of a given resource that can be
+ // allocated/provisioned. This is what "quota" usually refers to.
+ Limit int `json:"limit"`
+}
+
+// QuotaSetPage stores a single page of all QuotaSet results from a List call.
+type QuotaSetPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a QuotaSetsetPage is empty.
+func (r QuotaSetPage) IsEmpty() (bool, error) {
+ ks, err := ExtractQuotaSets(r)
+ return len(ks) == 0, err
+}
+
+// ExtractQuotaSets interprets a page of results as a slice of QuotaSets.
+func ExtractQuotaSets(r pagination.Page) ([]QuotaSet, error) {
+ var s struct {
+ QuotaSets []QuotaSet `json:"quotas"`
+ }
+ err := (r.(QuotaSetPage)).ExtractInto(&s)
+ return s.QuotaSets, err
+}
+
+type quotaResult struct {
+ gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any QuotaSet resource response
+// as a QuotaSet struct.
+func (r quotaResult) Extract() (*QuotaSet, error) {
+ var s struct {
+ QuotaSet *QuotaSet `json:"quota_set"`
+ }
+ err := r.ExtractInto(&s)
+ return s.QuotaSet, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to
+// interpret it as a QuotaSet.
+type GetResult struct {
+ quotaResult
+}
+
+// UpdateResult is the response from a Update operation. Call its Extract method
+// to interpret it as a QuotaSet.
+type UpdateResult struct {
+ quotaResult
+}
+
+type quotaUsageResult struct {
+ gophercloud.Result
+}
+
+// GetUsageResult is the response from a Get operation. Call its Extract
+// method to interpret it as a QuotaSet.
+type GetUsageResult struct {
+ quotaUsageResult
+}
+
+// Extract is a method that attempts to interpret any QuotaUsageSet resource
+// response as a set of QuotaUsageSet structs.
+func (r quotaUsageResult) Extract() (QuotaUsageSet, error) {
+ var s struct {
+ QuotaUsageSet QuotaUsageSet `json:"quota_set"`
+ }
+ err := r.ExtractInto(&s)
+ return s.QuotaUsageSet, err
+}
+
+// DeleteResult is the response from a Delete operation. Call its ExtractErr
+// method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/blockstorage/extensions/quotasets/testing/doc.go b/openstack/blockstorage/extensions/quotasets/testing/doc.go
new file mode 100644
index 0000000..30d864e
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/testing/doc.go
@@ -0,0 +1,2 @@
+// quotasets unit tests
+package testing
diff --git a/openstack/blockstorage/extensions/quotasets/testing/fixtures.go b/openstack/blockstorage/extensions/quotasets/testing/fixtures.go
new file mode 100644
index 0000000..a410af9
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/testing/fixtures.go
@@ -0,0 +1,172 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git"
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/blockstorage/extensions/quotasets"
+ th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+const FirstTenantID = "555544443333222211110000ffffeeee"
+
+var getExpectedJSONBody = `
+{
+ "quota_set" : {
+ "volumes" : 8,
+ "snapshots" : 9,
+ "gigabytes" : 10,
+ "per_volume_gigabytes" : 11,
+ "backups" : 12,
+ "backup_gigabytes" : 13,
+ "groups": 14
+ }
+}`
+
+var getExpectedQuotaSet = quotasets.QuotaSet{
+ Volumes: 8,
+ Snapshots: 9,
+ Gigabytes: 10,
+ PerVolumeGigabytes: 11,
+ Backups: 12,
+ BackupGigabytes: 13,
+ Groups: 14,
+}
+
+var getUsageExpectedJSONBody = `
+{
+ "quota_set" : {
+ "id": "555544443333222211110000ffffeeee",
+ "volumes" : {
+ "in_use": 15,
+ "limit": 16,
+ "reserved": 17
+ },
+ "snapshots" : {
+ "in_use": 18,
+ "limit": 19,
+ "reserved": 20
+ },
+ "gigabytes" : {
+ "in_use": 21,
+ "limit": 22,
+ "reserved": 23
+ },
+ "per_volume_gigabytes" : {
+ "in_use": 24,
+ "limit": 25,
+ "reserved": 26
+ },
+ "backups" : {
+ "in_use": 27,
+ "limit": 28,
+ "reserved": 29
+ },
+ "backup_gigabytes" : {
+ "in_use": 30,
+ "limit": 31,
+ "reserved": 32
+ },
+ "groups" : {
+ "in_use": 40,
+ "limit": 41,
+ "reserved": 42
+ }
+ }
+}`
+
+var getUsageExpectedQuotaSet = quotasets.QuotaUsageSet{
+ ID: FirstTenantID,
+ Volumes: quotasets.QuotaUsage{InUse: 15, Limit: 16, Reserved: 17},
+ Snapshots: quotasets.QuotaUsage{InUse: 18, Limit: 19, Reserved: 20},
+ Gigabytes: quotasets.QuotaUsage{InUse: 21, Limit: 22, Reserved: 23},
+ PerVolumeGigabytes: quotasets.QuotaUsage{InUse: 24, Limit: 25, Reserved: 26},
+ Backups: quotasets.QuotaUsage{InUse: 27, Limit: 28, Reserved: 29},
+ BackupGigabytes: quotasets.QuotaUsage{InUse: 30, Limit: 31, Reserved: 32},
+ Groups: quotasets.QuotaUsage{InUse: 40, Limit: 41, Reserved: 42},
+}
+
+var fullUpdateExpectedJSONBody = `
+{
+ "quota_set": {
+ "volumes": 8,
+ "snapshots": 9,
+ "gigabytes": 10,
+ "per_volume_gigabytes": 11,
+ "backups": 12,
+ "backup_gigabytes": 13,
+ "groups": 14
+ }
+}`
+
+var fullUpdateOpts = quotasets.UpdateOpts{
+ Volumes: gophercloud.IntToPointer(8),
+ Snapshots: gophercloud.IntToPointer(9),
+ Gigabytes: gophercloud.IntToPointer(10),
+ PerVolumeGigabytes: gophercloud.IntToPointer(11),
+ Backups: gophercloud.IntToPointer(12),
+ BackupGigabytes: gophercloud.IntToPointer(13),
+ Groups: gophercloud.IntToPointer(14),
+}
+
+var fullUpdateExpectedQuotaSet = quotasets.QuotaSet{
+ Volumes: 8,
+ Snapshots: 9,
+ Gigabytes: 10,
+ PerVolumeGigabytes: 11,
+ Backups: 12,
+ BackupGigabytes: 13,
+ Groups: 14,
+}
+
+var partialUpdateExpectedJSONBody = `
+{
+ "quota_set": {
+ "volumes": 200,
+ "snapshots": 0,
+ "gigabytes": 0,
+ "per_volume_gigabytes": 0,
+ "backups": 0,
+ "backup_gigabytes": 0
+ }
+}`
+
+var partialUpdateOpts = quotasets.UpdateOpts{
+ Volumes: gophercloud.IntToPointer(200),
+ Snapshots: gophercloud.IntToPointer(0),
+ Gigabytes: gophercloud.IntToPointer(0),
+ PerVolumeGigabytes: gophercloud.IntToPointer(0),
+ Backups: gophercloud.IntToPointer(0),
+ BackupGigabytes: gophercloud.IntToPointer(0),
+}
+
+var partiualUpdateExpectedQuotaSet = quotasets.QuotaSet{Volumes: 200}
+
+// HandleSuccessfulRequest configures the test server to respond to an HTTP request.
+func HandleSuccessfulRequest(t *testing.T, httpMethod, uriPath, jsonOutput string, uriQueryParams map[string]string) {
+
+ th.Mux.HandleFunc(uriPath, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, httpMethod)
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ w.Header().Add("Content-Type", "application/json")
+
+ if uriQueryParams != nil {
+ th.TestFormValues(t, r, uriQueryParams)
+ }
+
+ fmt.Fprintf(w, jsonOutput)
+ })
+}
+
+// HandleDeleteSuccessfully tests quotaset deletion.
+func HandleDeleteSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusOK)
+ })
+}
diff --git a/openstack/blockstorage/extensions/quotasets/testing/requests_test.go b/openstack/blockstorage/extensions/quotasets/testing/requests_test.go
new file mode 100644
index 0000000..7be309a
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/testing/requests_test.go
@@ -0,0 +1,80 @@
+package testing
+
+import (
+ "errors"
+ "testing"
+
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/blockstorage/extensions/quotasets"
+ th "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper"
+ "gerrit.mcp.mirantis.net/debian/gophercloud.git/testhelper/client"
+)
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ uriQueryParms := map[string]string{}
+ HandleSuccessfulRequest(t, "GET", "/os-quota-sets/"+FirstTenantID, getExpectedJSONBody, uriQueryParms)
+ actual, err := quotasets.Get(client.ServiceClient(), FirstTenantID).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &getExpectedQuotaSet, actual)
+}
+
+func TestGetUsage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ uriQueryParms := map[string]string{"usage": "true"}
+ HandleSuccessfulRequest(t, "GET", "/os-quota-sets/"+FirstTenantID, getUsageExpectedJSONBody, uriQueryParms)
+ actual, err := quotasets.GetUsage(client.ServiceClient(), FirstTenantID).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, getUsageExpectedQuotaSet, actual)
+}
+
+func TestFullUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ uriQueryParms := map[string]string{}
+ HandleSuccessfulRequest(t, "PUT", "/os-quota-sets/"+FirstTenantID, fullUpdateExpectedJSONBody, uriQueryParms)
+ actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, fullUpdateOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &fullUpdateExpectedQuotaSet, actual)
+}
+
+func TestPartialUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ uriQueryParms := map[string]string{}
+ HandleSuccessfulRequest(t, "PUT", "/os-quota-sets/"+FirstTenantID, partialUpdateExpectedJSONBody, uriQueryParms)
+ actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, partialUpdateOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &partiualUpdateExpectedQuotaSet, actual)
+}
+
+type ErrorUpdateOpts quotasets.UpdateOpts
+
+func (opts ErrorUpdateOpts) ToBlockStorageQuotaUpdateMap() (map[string]interface{}, error) {
+ return nil, errors.New("This is an error")
+}
+
+func TestErrorInToBlockStorageQuotaUpdateMap(t *testing.T) {
+ opts := &ErrorUpdateOpts{}
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleSuccessfulRequest(t, "PUT", "/os-quota-sets/"+FirstTenantID, "", nil)
+ _, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract()
+ if err == nil {
+ t.Fatal("Error handling failed")
+ }
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDeleteSuccessfully(t)
+
+ err := quotasets.Delete(client.ServiceClient(), FirstTenantID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/extensions/quotasets/urls.go b/openstack/blockstorage/extensions/quotasets/urls.go
new file mode 100644
index 0000000..1fba536
--- /dev/null
+++ b/openstack/blockstorage/extensions/quotasets/urls.go
@@ -0,0 +1,21 @@
+package quotasets
+
+import "gerrit.mcp.mirantis.net/debian/gophercloud.git"
+
+const resourcePath = "os-quota-sets"
+
+func getURL(c *gophercloud.ServiceClient, projectID string) string {
+ return c.ServiceURL(resourcePath, projectID)
+}
+
+func getDefaultsURL(c *gophercloud.ServiceClient, projectID string) string {
+ return c.ServiceURL(resourcePath, projectID, "defaults")
+}
+
+func updateURL(c *gophercloud.ServiceClient, projectID string) string {
+ return getURL(c, projectID)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, projectID string) string {
+ return getURL(c, projectID)
+}