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)
+}