Added functionality for updating and resetting compute quotas. (#214)

* Added functionality for updating and resetting compute quotas.
Unit and acceptance tests added.

* Forgot to add my latest changes.
Modified acceptance test to better find the tenant-id

* Improved test coverage.
And fixed a bug while doing this.

* Moved FillFromQuotaSet to acceptance test package
Refractored ToComputeQuotaUpdateMap()
diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go
index 3392c2e..97d61f6 100644
--- a/acceptance/openstack/compute/v2/compute.go
+++ b/acceptance/openstack/compute/v2/compute.go
@@ -710,6 +710,24 @@
 	})
 }
 
+//Convenience method to fill an QuotaSet-UpdateOpts-struct from a QuotaSet-struct
+func  FillUpdateOptsFromQuotaSet(src quotasets.QuotaSet,dest *quotasets.UpdateOpts) {
+	dest.FixedIps = &src.FixedIps
+	dest.FloatingIps = &src.FloatingIps
+	dest.InjectedFileContentBytes = &src.InjectedFileContentBytes
+	dest.InjectedFilePathBytes = &src.InjectedFilePathBytes
+	dest.InjectedFiles = &src.InjectedFiles
+	dest.KeyPairs = &src.KeyPairs
+	dest.Ram = &src.Ram
+	dest.SecurityGroupRules = &src.SecurityGroupRules
+	dest.SecurityGroups = &src.SecurityGroups
+	dest.Cores = &src.Cores
+	dest.Instances = &src.Instances
+	dest.ServerGroups = &src.ServerGroups
+	dest.ServerGroupMembers = &src.ServerGroupMembers
+	dest.MetadataItems = &src.MetadataItems
+}
+
 // PrintServer will print an instance and all of its attributes.
 func PrintServer(t *testing.T, server *servers.Server) {
 	t.Logf("ID: %s", server.ID)
diff --git a/acceptance/openstack/compute/v2/quotaset_test.go b/acceptance/openstack/compute/v2/quotaset_test.go
index d9e73e9..2fb7c99 100644
--- a/acceptance/openstack/compute/v2/quotaset_test.go
+++ b/acceptance/openstack/compute/v2/quotaset_test.go
@@ -10,6 +10,8 @@
 	"github.com/gophercloud/gophercloud/acceptance/clients"
 	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
 	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"os"
 )
 
 func TestQuotasetGet(t *testing.T) {
@@ -53,3 +55,131 @@
 
 	return "", fmt.Errorf("Unable to get tenant ID")
 }
+
+func getTenantIDByName(t *testing.T, client *gophercloud.ServiceClient, name string) (string, error) {
+	allPages, err := tenants.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to get list of tenants: %v", err)
+	}
+
+	allTenants, err := tenants.ExtractTenants(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract tenants: %v", err)
+	}
+
+	for _, tenant := range allTenants {
+		if tenant.Name == name{
+			return tenant.ID, nil
+		}
+	}
+
+	return "", fmt.Errorf("Unable to get tenant ID")
+}
+
+//What will be sent as desired Quotas to the Server
+var UpdatQuotaOpts = quotasets.UpdateOpts{
+	FixedIps:                 gophercloud.IntToPointer(10),
+	FloatingIps:              gophercloud.IntToPointer(10),
+	InjectedFileContentBytes: gophercloud.IntToPointer(10240),
+	InjectedFilePathBytes:    gophercloud.IntToPointer(255),
+	InjectedFiles:            gophercloud.IntToPointer(5),
+	KeyPairs:                 gophercloud.IntToPointer(10),
+	MetadataItems:            gophercloud.IntToPointer(128),
+	Ram:                      gophercloud.IntToPointer(20000),
+	SecurityGroupRules:       gophercloud.IntToPointer(20),
+	SecurityGroups:           gophercloud.IntToPointer(10),
+	Cores:                    gophercloud.IntToPointer(10),
+	Instances:                gophercloud.IntToPointer(4),
+	ServerGroups:             gophercloud.IntToPointer(2),
+	ServerGroupMembers:       gophercloud.IntToPointer(3),
+}
+
+//What the Server hopefully returns as the new Quotas
+var UpdatedQuotas = quotasets.QuotaSet{
+	FixedIps:                 10,
+	FloatingIps:              10,
+	InjectedFileContentBytes: 10240,
+	InjectedFilePathBytes:    255,
+	InjectedFiles:            5,
+	KeyPairs:                 10,
+	MetadataItems:            128,
+	Ram:                      20000,
+	SecurityGroupRules:       20,
+	SecurityGroups:           10,
+	Cores:                    10,
+	Instances:                4,
+	ServerGroups:             2,
+	ServerGroupMembers:       3,
+}
+
+func TestQuotasetUpdateDelete(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	idclient, err := clients.NewIdentityV2Client()
+	if err != nil{
+		t.Fatalf("Could not create IdentityClient to look up tenant id!")
+	}
+
+	tenantid, err := getTenantIDByName(t, idclient, os.Getenv("OS_TENANT_NAME"))
+	if err != nil{
+		t.Fatalf("Id for Tenant named '%' not found. Please set OS_TENANT_NAME appropriately", os.Getenv("OS_TENANT_NAME"))
+	}
+	
+
+	//save original quotas
+	orig, err := quotasets.Get(client,tenantid).Extract()
+	th.AssertNoErr(t,err)
+
+	//Test Update
+	res, err := quotasets.Update(client,tenantid,UpdatQuotaOpts).Extract()
+	th.AssertNoErr(t,err)
+	th.AssertEquals(t,UpdatedQuotas,*res)
+
+	//Test Delete
+	_,  err = quotasets.Delete(client,tenantid).Extract()
+	th.AssertNoErr(t,err)
+	//We dont know the default quotas, so just check if the quotas are not the same as before
+	newres, err := quotasets.Get(client,tenantid).Extract()
+	if newres == res{
+		t.Fatalf("Quotas after delete equal quotas before delete!")
+	}
+
+
+	restore := quotasets.UpdateOpts{}
+	FillUpdateOptsFromQuotaSet(*orig,&restore)
+
+	//restore original quotas
+	res, err = quotasets.Update(client,tenantid,restore).Extract()
+	th.AssertNoErr(t,err)
+
+	orig.ID = ""
+	th.AssertEquals(t,*orig,*res)
+
+}
+
+// Makes sure that the FillUpdateOptsFromQuotaSet() helper function works properly
+func TestFillFromQuotaSetHelperFunction(t *testing.T){
+	op := &quotasets.UpdateOpts{}
+	expected := `
+	{
+	"fixed_ips": 10,
+	"floating_ips": 10,
+	"injected_file_content_bytes": 10240,
+	"injected_file_path_bytes": 255,
+	"injected_files": 5,
+	"key_pairs": 10,
+	"metadata_items": 128,
+	"ram": 20000,
+	"security_group_rules": 20,
+	"security_groups": 10,
+	"cores": 10,
+	"instances": 4,
+	"server_groups": 2,
+	"server_group_members": 3
+	}`
+	FillUpdateOptsFromQuotaSet(UpdatedQuotas,op)
+	th.AssertJSONEquals(t,expected,op)
+}
\ No newline at end of file
diff --git a/openstack/compute/v2/extensions/quotasets/requests.go b/openstack/compute/v2/extensions/quotasets/requests.go
index 76beb17..64fc8ff 100644
--- a/openstack/compute/v2/extensions/quotasets/requests.go
+++ b/openstack/compute/v2/extensions/quotasets/requests.go
@@ -10,3 +10,70 @@
 	_, res.Err = client.Get(getURL(client, tenantID), &res.Body, nil)
 	return res
 }
+
+//Updates the quotas for the given tenantID and returns the new quota-set
+func Update(client *gophercloud.ServiceClient, tenantID string, opts UpdateOptsBuilder) (res UpdateResult) {
+	reqBody, err := opts.ToComputeQuotaUpdateMap()
+	if err != nil {
+		res.Err = err
+		return
+	}
+
+	_, res.Err = client.Put(updateURL(client, tenantID), reqBody, &res.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
+	return res
+}
+
+//Resets the uotas for the given tenant to their default values
+func Delete(client *gophercloud.ServiceClient, tenantID string) (res DeleteResult) {
+	_, res.Err = client.Delete(deleteURL(client, tenantID), nil)
+	return
+}
+
+//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 {
+	//FixedIps is number of fixed ips alloted this quota_set
+	FixedIps *int `json:"fixed_ips,omitempty"`
+	// FloatingIps is number of floating ips alloted this quota_set
+	FloatingIps *int `json:"floating_ips,omitempty"`
+	// InjectedFileContentBytes is content bytes allowed for each injected file
+	InjectedFileContentBytes *int `json:"injected_file_content_bytes,omitempty"`
+	// InjectedFilePathBytes is allowed bytes for each injected file path
+	InjectedFilePathBytes *int `json:"injected_file_path_bytes,omitempty"`
+	// InjectedFiles is injected files allowed for each project
+	InjectedFiles *int `json:"injected_files,omitempty"`
+	// KeyPairs is number of ssh keypairs
+	KeyPairs *int `json:"key_pairs,omitempty"`
+	// MetadataItems is number of metadata items allowed for each instance
+	MetadataItems *int `json:"metadata_items,omitempty"`
+	// Ram is megabytes allowed for each instance
+	Ram *int `json:"ram,omitempty"`
+	// SecurityGroupRules is rules allowed for each security group
+	SecurityGroupRules *int `json:"security_group_rules,omitempty"`
+	// SecurityGroups security groups allowed for each project
+	SecurityGroups *int `json:"security_groups,omitempty"`
+	// Cores is number of instance cores allowed for each project
+	Cores *int `json:"cores,omitempty"`
+	// Instances is number of instances allowed for each project
+	Instances *int `json:"instances,omitempty"`
+	// Number of ServerGroups allowed for the project
+	ServerGroups *int `json:"server_groups,omitempty"`
+	// Max number of Members for each ServerGroup
+	ServerGroupMembers *int `json:"server_group_members,omitempty"`
+	//Users can force the update even if the quota has already been used and the reserved quota exceeds the new quota.
+	Force bool `json:"force,omitempty"`
+}
+
+type UpdateOptsBuilder interface {
+	//Extra specific name to prevent collisions with interfaces for other quotas (e.g. neutron)
+	ToComputeQuotaUpdateMap() (map[string]interface{}, error)
+}
+
+func (opts UpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) {
+
+	return gophercloud.BuildRequestBody(opts, "quota_set")
+}
+
+
+
diff --git a/openstack/compute/v2/extensions/quotasets/results.go b/openstack/compute/v2/extensions/quotasets/results.go
index f6c4e5a..44e6b06 100644
--- a/openstack/compute/v2/extensions/quotasets/results.go
+++ b/openstack/compute/v2/extensions/quotasets/results.go
@@ -20,7 +20,7 @@
 	// InjectedFiles is injected files allowed for each project
 	InjectedFiles int `json:"injected_files"`
 	// KeyPairs is number of ssh keypairs
-	KeyPairs int `json:"keypairs"`
+	KeyPairs int `json:"key_pairs"`
 	// MetadataItems is number of metadata items allowed for each instance
 	MetadataItems int `json:"metadata_items"`
 	// Ram is megabytes allowed for each instance
@@ -33,6 +33,10 @@
 	Cores int `json:"cores"`
 	// Instances is number of instances allowed for each project
 	Instances int `json:"instances"`
+	// Number of ServerGroups allowed for the project
+	ServerGroups int `json:"server_groups"`
+	// Max number of Members for each ServerGroup
+	ServerGroupMembers int `json:"server_group_members"`
 }
 
 // QuotaSetPage stores a single, only page of QuotaSet results from a List call.
@@ -73,3 +77,15 @@
 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
+}
+
+// DeleteResult is the response from a Delete operation. Call its Extract method to interpret it
+// as a QuotaSet.
+type DeleteResult struct {
+	quotaResult
+}
diff --git a/openstack/compute/v2/extensions/quotasets/testing/fixtures.go b/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
index 3fef872..0945f9b 100644
--- a/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
@@ -4,10 +4,10 @@
 	"fmt"
 	"net/http"
 	"testing"
-
 	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
 	th "github.com/gophercloud/gophercloud/testhelper"
 	"github.com/gophercloud/gophercloud/testhelper/client"
+	"github.com/gophercloud/gophercloud"
 )
 
 // GetOutput is a sample response to a Get call.
@@ -22,12 +22,13 @@
       "injected_files" : 5,
       "metadata_items" : 128,
       "ram" : 200000,
-      "keypairs" : 10,
-      "injected_file_path_bytes" : 255
+      "key_pairs" : 10,
+      "injected_file_path_bytes" : 255,
+	  "server_groups" : 2,
+	  "server_group_members" : 3
    }
 }
 `
-
 const FirstTenantID = "555544443333222211110000ffffeeee"
 
 // FirstQuotaSet is the first result in ListOutput.
@@ -44,6 +45,34 @@
 	SecurityGroups:           10,
 	Cores:                    200,
 	Instances:                25,
+	ServerGroups: 			  2,
+	ServerGroupMembers:       3,
+}
+
+
+//The expected update Body. Is also returned by PUT request
+const UpdateOutput = `{"quota_set":{"cores":200,"fixed_ips":0,"floating_ips":0,"injected_file_content_bytes":10240,"injected_file_path_bytes":255,"injected_files":5,"instances":25,"key_pairs":10,"metadata_items":128,"ram":200000,"security_group_rules":20,"security_groups":10,"server_groups":2,"server_group_members":3}}`
+
+//The expected partialupdate Body. Is also returned by PUT request
+const PartialUpdateBody = `{"quota_set":{"cores":200, "force":true}}`
+
+
+//Result of Quota-update
+var UpdatedQuotaSet = quotasets.UpdateOpts{
+	FixedIps:                 gophercloud.IntToPointer(0),
+	FloatingIps:              gophercloud.IntToPointer(0),
+	InjectedFileContentBytes: gophercloud.IntToPointer(10240),
+	InjectedFilePathBytes:    gophercloud.IntToPointer(255),
+	InjectedFiles:            gophercloud.IntToPointer(5),
+	KeyPairs:                 gophercloud.IntToPointer(10),
+	MetadataItems:            gophercloud.IntToPointer(128),
+	Ram:                      gophercloud.IntToPointer(200000),
+	SecurityGroupRules:       gophercloud.IntToPointer(20),
+	SecurityGroups:           gophercloud.IntToPointer(10),
+	Cores:                    gophercloud.IntToPointer(200),
+	Instances:                gophercloud.IntToPointer(25),
+	ServerGroups: 			  gophercloud.IntToPointer(2),
+	ServerGroupMembers:       gophercloud.IntToPointer(3),
 }
 
 // HandleGetSuccessfully configures the test server to respond to a Get request for sample tenant
@@ -56,3 +85,36 @@
 		fmt.Fprintf(w, GetOutput)
 	})
 }
+
+// HandlePutSuccessfully configures the test server to respond to a Put request for sample tenant
+func HandlePutSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t,r,UpdateOutput)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, UpdateOutput)
+	})
+}
+
+// HandlePartialPutSuccessfully configures the test server to respond to a Put request for sample tenant that only containes specific values
+func HandlePartialPutSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t,r,PartialUpdateBody)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, UpdateOutput)
+	})
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request for sample tenant
+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)
+		th.TestBody(t,r,"")	
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(202)
+	})
+}
\ No newline at end of file
diff --git a/openstack/compute/v2/extensions/quotasets/testing/requests_test.go b/openstack/compute/v2/extensions/quotasets/testing/requests_test.go
index 8fc1fd4..e63f6aa 100644
--- a/openstack/compute/v2/extensions/quotasets/testing/requests_test.go
+++ b/openstack/compute/v2/extensions/quotasets/testing/requests_test.go
@@ -6,6 +6,8 @@
 	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
 	th "github.com/gophercloud/gophercloud/testhelper"
 	"github.com/gophercloud/gophercloud/testhelper/client"
+	"github.com/gophercloud/gophercloud"
+	"errors"
 )
 
 func TestGet(t *testing.T) {
@@ -16,3 +18,48 @@
 	th.AssertNoErr(t, err)
 	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
 }
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePutSuccessfully(t)
+	actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, UpdatedQuotaSet).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
+
+func TestPartialUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePartialPutSuccessfully(t)
+	opts := quotasets.UpdateOpts{Cores:gophercloud.IntToPointer(200), Force:true}
+	actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteSuccessfully(t)
+	_, err := quotasets.Delete(client.ServiceClient(), FirstTenantID).Extract()
+	th.AssertNoErr(t, err)
+}
+
+type ErrorUpdateOpts quotasets.UpdateOpts;
+
+func (opts ErrorUpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) {
+	return nil, errors.New("This is an error")
+}
+
+func TestErrorInToComputeQuotaUpdateMap(t *testing.T){
+	opts := &ErrorUpdateOpts{}
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePutSuccessfully(t)
+	_, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract()
+	if err == nil{
+		t.Fatal("Error handling failed")
+	}
+}
+
diff --git a/openstack/compute/v2/extensions/quotasets/urls.go b/openstack/compute/v2/extensions/quotasets/urls.go
index e910376..4e15613 100644
--- a/openstack/compute/v2/extensions/quotasets/urls.go
+++ b/openstack/compute/v2/extensions/quotasets/urls.go
@@ -11,3 +11,11 @@
 func getURL(c *gophercloud.ServiceClient, tenantID string) string {
 	return c.ServiceURL(resourcePath, tenantID)
 }
+
+func updateURL(c *gophercloud.ServiceClient, tenantID string) string {
+	return getURL(c, tenantID)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, tenantID string) string {
+	return getURL(c, tenantID)
+}
\ No newline at end of file