Merge pull request #3 from jrperritt/rackspace-gophercloud-commits

Rackspace gophercloud commits
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index becaf44..2cf355b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,8 @@
 way than just downloading it. Here are the basic installation instructions:
 
 1. Configure your `$GOPATH` and run `go get` as described in the main
-[README](/README.md#how-to-install).
+[README](/README.md#how-to-install) but add `-tags "fixtures acceptance"` to
+get dependencies for unit and acceptance tests.
 
 2. Move into the directory that houses your local repository:
 
@@ -158,25 +159,25 @@
 To run all tests:
 
 ```bash
-go test ./...
+go test -tags fixtures ./...
 ```
 
 To run all tests with verbose output:
 
 ```bash
-go test -v ./...
+go test -v -tags fixtures ./...
 ```
 
 To run tests that match certain [build tags]():
 
 ```bash
-go test -tags "foo bar" ./...
+go test -tags "fixtures foo bar" ./...
 ```
 
 To run tests for a particular sub-package:
 
 ```bash
-cd ./path/to/package && go test .
+cd ./path/to/package && go test -tags fixtures .
 ```
 
 ## Style guide
diff --git a/acceptance/openstack/compute/v2/quotaset_test.go b/acceptance/openstack/compute/v2/quotaset_test.go
new file mode 100644
index 0000000..3851edf
--- /dev/null
+++ b/acceptance/openstack/compute/v2/quotaset_test.go
@@ -0,0 +1,60 @@
+// +build acceptance compute
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/quotasets"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestGetQuotaset(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	idclient := openstack.NewIdentityV2(client.ProviderClient)
+	quotaset, err := quotasets.Get(client, findTenant(t, idclient)).Extract()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("QuotaSet details:\n")
+	t.Logf("                   instances=[%d]\n", quotaset.Instances)
+	t.Logf("                       cores=[%d]\n", quotaset.Cores)
+	t.Logf("                         ram=[%d]\n", quotaset.Ram)
+	t.Logf("                   key_pairs=[%d]\n", quotaset.KeyPairs)
+	t.Logf("              metadata_items=[%d]\n", quotaset.MetadataItems)
+	t.Logf("             security_groups=[%d]\n", quotaset.SecurityGroups)
+	t.Logf("        security_group_rules=[%d]\n", quotaset.SecurityGroupRules)
+	t.Logf("                   fixed_ips=[%d]\n", quotaset.FixedIps)
+	t.Logf("                floating_ips=[%d]\n", quotaset.FloatingIps)
+	t.Logf(" injected_file_content_bytes=[%d]\n", quotaset.InjectedFileContentBytes)
+	t.Logf("    injected_file_path_bytes=[%d]\n", quotaset.InjectedFilePathBytes)
+	t.Logf("              injected_files=[%d]\n", quotaset.InjectedFiles)
+
+}
+
+func findTenant(t *testing.T, client *gophercloud.ServiceClient) string {
+	var tenantID string
+	err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		tenantList, err := tenants.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		for _, t := range tenantList {
+			tenantID = t.ID
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	return tenantID
+}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
index 3100b1f..da06c35 100644
--- a/acceptance/openstack/compute/v2/secgroup_test.go
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -107,6 +107,24 @@
 	th.AssertNoErr(t, err)
 
 	t.Logf("Deleted rule %s from group %s", rule.ID, id)
+
+	icmpOpts := secgroups.CreateRuleOpts{
+		ParentGroupID: id,
+		FromPort:      0,
+		ToPort:        0,
+		IPProtocol:    "ICMP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	icmpRule, err := secgroups.CreateRule(client, icmpOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Adding ICMP rule %s to group %s", icmpRule.ID, id)
+
+	err = secgroups.DeleteRule(client, icmpRule.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted ICMP rule %s from group %s", icmpRule.ID, id)
 }
 
 func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) {
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
index 045a8ae..ed948bd 100644
--- a/acceptance/openstack/networking/v2/extensions/lbaas/common.go
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
@@ -3,6 +3,7 @@
 import (
 	"testing"
 
+	"github.com/gophercloud/gophercloud"
 	base "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
@@ -22,7 +23,7 @@
 	s, err := subnets.Create(base.Client, subnets.CreateOpts{
 		NetworkID: n.ID,
 		CIDR:      "192.168.199.0/24",
-		IPVersion: subnets.IPv4,
+		IPVersion: gophercloud.IPv4,
 		Name:      "tmp_subnet",
 	}).Extract()
 	th.AssertNoErr(t, err)
diff --git a/acceptance/openstack/networking/v2/subnet_test.go b/acceptance/openstack/networking/v2/subnet_test.go
index f6ce17e..c9efd5c 100644
--- a/acceptance/openstack/networking/v2/subnet_test.go
+++ b/acceptance/openstack/networking/v2/subnet_test.go
@@ -11,7 +11,7 @@
 	th "github.com/gophercloud/gophercloud/testhelper"
 )
 
-func TestList(t *testing.T) {
+func TestSubnetList(t *testing.T) {
 	Setup(t)
 	defer Teardown()
 
@@ -32,7 +32,7 @@
 	th.CheckNoErr(t, err)
 }
 
-func TestCRUD(t *testing.T) {
+func TestSubnetCRUD(t *testing.T) {
 	Setup(t)
 	defer Teardown()
 
@@ -61,6 +61,7 @@
 	th.AssertEquals(t, s.IPVersion, 4)
 	th.AssertEquals(t, s.Name, "my_subnet")
 	th.AssertEquals(t, s.EnableDHCP, false)
+	th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
 	subnetID := s.ID
 
 	// Get subnet
@@ -79,6 +80,60 @@
 	t.Log("Delete subnet")
 	res := subnets.Delete(Client, subnetID)
 	th.AssertNoErr(t, res.Err)
+
+	// Create subnet with no gateway
+	t.Log("Create subnet with no gateway")
+	opts = subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  subnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: &enable,
+		NoGateway:  true,
+	}
+	s, err = subnets.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.NetworkID, networkID)
+	th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.Name, "my_subnet")
+	th.AssertEquals(t, s.EnableDHCP, false)
+	th.AssertEquals(t, s.GatewayIP, "")
+	subnetID = s.ID
+
+	// Get subnet
+	t.Log("Getting subnet with no gateway")
+	s, err = subnets.Get(Client, subnetID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, s.ID, subnetID)
+
+	// Update subnet
+	t.Log("Update subnet with no gateway")
+	s, err = subnets.Update(Client, subnetID, subnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, s.Name, "new_subnet_name")
+
+	// Delete subnet
+	t.Log("Delete subnet with no gateway")
+	res = subnets.Delete(Client, subnetID)
+	th.AssertNoErr(t, res.Err)
+
+	// Create subnet with invalid gateway configuration
+	t.Log("Create subnet with invalid gateway configuration")
+	opts = subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  subnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: &enable,
+		NoGateway:  true,
+		GatewayIP:  "192.168.199.1",
+	}
+	_, err = subnets.Create(Client, opts).Extract()
+	if err == nil {
+		t.Fatalf("Expected an error, got none")
+	}
 }
 
 func TestBatchCreate(t *testing.T) {
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
index 936f1c9..c9cf895 100644
--- a/openstack/blockstorage/v1/apiversions/urls.go
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -2,6 +2,7 @@
 
 import (
 	"strings"
+	"net/url"
 
 	"github.com/gophercloud/gophercloud"
 )
@@ -11,5 +12,7 @@
 }
 
 func listURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL("")
+	u, _ := url.Parse(c.ServiceURL(""))
+	u.Path = "/"
+	return u.String()
 }
diff --git a/openstack/blockstorage/v1/snapshots/fixtures.go b/openstack/blockstorage/v1/snapshots/fixtures.go
index b1bfef8..1dcdfca 100644
--- a/openstack/blockstorage/v1/snapshots/fixtures.go
+++ b/openstack/blockstorage/v1/snapshots/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package snapshots
 
 import (
diff --git a/openstack/blockstorage/v1/volumes/testing/fixtures.go b/openstack/blockstorage/v1/volumes/testing/fixtures.go
index 421cbf4..0d34d5e 100644
--- a/openstack/blockstorage/v1/volumes/testing/fixtures.go
+++ b/openstack/blockstorage/v1/volumes/testing/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package testing
 
 import (
diff --git a/openstack/blockstorage/v1/volumetypes/fixtures.go b/openstack/blockstorage/v1/volumetypes/fixtures.go
index 1969120..cb6fadf 100644
--- a/openstack/blockstorage/v1/volumetypes/fixtures.go
+++ b/openstack/blockstorage/v1/volumetypes/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package volumetypes
 
 import (
diff --git a/openstack/cdn/v1/base/fixtures.go b/openstack/cdn/v1/base/fixtures.go
index f95d893..226edae 100644
--- a/openstack/cdn/v1/base/fixtures.go
+++ b/openstack/cdn/v1/base/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package base
 
 import (
diff --git a/openstack/cdn/v1/flavors/fixtures.go b/openstack/cdn/v1/flavors/fixtures.go
index 285075e..5d07491 100644
--- a/openstack/cdn/v1/flavors/fixtures.go
+++ b/openstack/cdn/v1/flavors/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package flavors
 
 import (
diff --git a/openstack/cdn/v1/serviceassets/fixtures.go b/openstack/cdn/v1/serviceassets/fixtures.go
index 9c62514..a66c503 100644
--- a/openstack/cdn/v1/serviceassets/fixtures.go
+++ b/openstack/cdn/v1/serviceassets/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package serviceassets
 
 import (
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
index c882f8c..12d260e 100644
--- a/openstack/cdn/v1/services/fixtures.go
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package services
 
 import (
diff --git a/openstack/client.go b/openstack/client.go
index 2fa4750..3e11508 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -151,13 +151,17 @@
 		v3Client.Endpoint = endpoint
 	}
 
+	// copy the auth options to a local variable that we can change. `options`
+	// needs to stay as-is for reauth purposes
+	v3Options := options
+
 	var scope *tokens3.Scope
 	if options.TenantID != "" {
 		scope = &tokens3.Scope{
 			ProjectID: options.TenantID,
 		}
-		options.TenantID = ""
-		options.TenantName = ""
+		v3Options.TenantID = ""
+		v3Options.TenantName = ""
 	} else {
 		if options.TenantName != "" {
 			scope = &tokens3.Scope{
@@ -165,7 +169,7 @@
 				DomainID:    options.DomainID,
 				DomainName:  options.DomainName,
 			}
-			options.TenantName = ""
+			v3Options.TenantName = ""
 		}
 	}
 
diff --git a/openstack/compute/v2/extensions/defsecrules/fixtures.go b/openstack/compute/v2/extensions/defsecrules/fixtures.go
index d35af0b..4fee896 100644
--- a/openstack/compute/v2/extensions/defsecrules/fixtures.go
+++ b/openstack/compute/v2/extensions/defsecrules/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package defsecrules
 
 import (
@@ -72,6 +74,41 @@
 	})
 }
 
+func mockCreateRuleResponseICMPZero(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group_default_rule": {
+    "ip_protocol": "ICMP",
+    "from_port": 0,
+    "to_port": 0,
+    "cidr": "10.10.12.0/24"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_default_rule": {
+    "from_port": 0,
+    "id": "{ruleID}",
+    "ip_protocol": "ICMP",
+    "ip_range": {
+      "cidr": "10.10.12.0/24"
+    },
+    "to_port": 0
+  }
+}
+`)
+	})
+}
+
 func mockGetRuleResponse(t *testing.T, ruleID string) {
 	url := rootPath + "/" + ruleID
 	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
diff --git a/openstack/compute/v2/extensions/defsecrules/requests.go b/openstack/compute/v2/extensions/defsecrules/requests.go
index 6f24bb3..184fdc9 100644
--- a/openstack/compute/v2/extensions/defsecrules/requests.go
+++ b/openstack/compute/v2/extensions/defsecrules/requests.go
@@ -1,6 +1,8 @@
 package defsecrules
 
 import (
+	"strings"
+
 	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/pagination"
 )
@@ -14,10 +16,10 @@
 
 // CreateOpts represents the configuration for adding a new default rule.
 type CreateOpts struct {
-	// The lower bound of the port range that will be opened.
-	FromPort int `json:"from_port" required:"true"`
+	// The lower bound of the port range that will be opened.s
+	FromPort int `json:"from_port"`
 	// The upper bound of the port range that will be opened.
-	ToPort int `json:"to_port" required:"true"`
+	ToPort int `json:"to_port"`
 	// The protocol type that will be allowed, e.g. TCP.
 	IPProtocol string `json:"ip_protocol" required:"true"`
 	// ONLY required if FromGroupID is blank. This represents the IP range that
@@ -33,6 +35,12 @@
 
 // ToRuleCreateMap builds the create rule options into a serializable format.
 func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
+		return nil, gophercloud.ErrMissingInput{Argument: "FromPort"}
+	}
+	if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
+		return nil, gophercloud.ErrMissingInput{Argument: "ToPort"}
+	}
 	return gophercloud.BuildRequestBody(opts, "security_group_default_rule")
 }
 
diff --git a/openstack/compute/v2/extensions/defsecrules/requests_test.go b/openstack/compute/v2/extensions/defsecrules/requests_test.go
index 0e2a010..df568fe 100644
--- a/openstack/compute/v2/extensions/defsecrules/requests_test.go
+++ b/openstack/compute/v2/extensions/defsecrules/requests_test.go
@@ -69,6 +69,32 @@
 	th.AssertDeepEquals(t, expected, group)
 }
 
+func TestCreateICMPZero(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateRuleResponseICMPZero(t)
+
+	opts := CreateOpts{
+		IPProtocol: "ICMP",
+		FromPort:   0,
+		ToPort:     0,
+		CIDR:       "10.10.12.0/24",
+	}
+
+	group, err := Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &DefaultRule{
+		ID:         ruleID,
+		FromPort:   0,
+		ToPort:     0,
+		IPProtocol: "ICMP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/extensions/quotasets/doc.go b/openstack/compute/v2/extensions/quotasets/doc.go
new file mode 100644
index 0000000..721024e
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/doc.go
@@ -0,0 +1,3 @@
+// Package quotasets provides information and interaction with QuotaSet
+// extension for the OpenStack Compute service.
+package quotasets
diff --git a/openstack/compute/v2/extensions/quotasets/fixtures.go b/openstack/compute/v2/extensions/quotasets/fixtures.go
new file mode 100644
index 0000000..c1bb4ea
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/fixtures.go
@@ -0,0 +1,59 @@
+// +build fixtures
+
+package quotasets
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+   "quota_set" : {
+      "instances" : 25,
+      "security_groups" : 10,
+      "security_group_rules" : 20,
+      "cores" : 200,
+      "injected_file_content_bytes" : 10240,
+      "injected_files" : 5,
+      "metadata_items" : 128,
+      "ram" : 200000,
+      "keypairs" : 10,
+      "injected_file_path_bytes" : 255
+   }
+}
+`
+
+const FirstTenantID = "555544443333222211110000ffffeeee"
+
+// FirstQuotaSet is the first result in ListOutput.
+var FirstQuotaSet = QuotaSet{
+	FixedIps:                 0,
+	FloatingIps:              0,
+	InjectedFileContentBytes: 10240,
+	InjectedFilePathBytes:    255,
+	InjectedFiles:            5,
+	KeyPairs:                 10,
+	MetadataItems:            128,
+	Ram:                      200000,
+	SecurityGroupRules:       20,
+	SecurityGroups:           10,
+	Cores:                    200,
+	Instances:                25,
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request for sample tenant
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, 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/compute/v2/extensions/quotasets/requests.go b/openstack/compute/v2/extensions/quotasets/requests.go
new file mode 100644
index 0000000..52f0839
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/requests.go
@@ -0,0 +1,12 @@
+package quotasets
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+// Get returns public data about a previously created QuotaSet.
+func Get(client *gophercloud.ServiceClient, tenantID string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, tenantID), &res.Body, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/quotasets/requests_test.go b/openstack/compute/v2/extensions/quotasets/requests_test.go
new file mode 100644
index 0000000..5d766fa
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/requests_test.go
@@ -0,0 +1,16 @@
+package quotasets
+
+import (
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+	"testing"
+)
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+	actual, err := Get(client.ServiceClient(), FirstTenantID).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
diff --git a/openstack/compute/v2/extensions/quotasets/results.go b/openstack/compute/v2/extensions/quotasets/results.go
new file mode 100644
index 0000000..cbf4d6b
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/results.go
@@ -0,0 +1,86 @@
+package quotasets
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// QuotaSet is a set of operational limits that allow for control of compute usage.
+type QuotaSet struct {
+	//ID is tenant associated with this quota_set
+	ID string `mapstructure:"id"`
+	//FixedIps is number of fixed ips alloted this quota_set
+	FixedIps int `mapstructure:"fixed_ips"`
+	// FloatingIps is number of floating ips alloted this quota_set
+	FloatingIps int `mapstructure:"floating_ips"`
+	// InjectedFileContentBytes is content bytes allowed for each injected file
+	InjectedFileContentBytes int `mapstructure:"injected_file_content_bytes"`
+	// InjectedFilePathBytes is allowed bytes for each injected file path
+	InjectedFilePathBytes int `mapstructure:"injected_file_path_bytes"`
+	// InjectedFiles is injected files allowed for each project
+	InjectedFiles int `mapstructure:"injected_files"`
+	// KeyPairs is number of ssh keypairs
+	KeyPairs int `mapstructure:"keypairs"`
+	// MetadataItems is number of metadata items allowed for each instance
+	MetadataItems int `mapstructure:"metadata_items"`
+	// Ram is megabytes allowed for each instance
+	Ram int `mapstructure:"ram"`
+	// SecurityGroupRules is rules allowed for each security group
+	SecurityGroupRules int `mapstructure:"security_group_rules"`
+	// SecurityGroups security groups allowed for each project
+	SecurityGroups int `mapstructure:"security_groups"`
+	// Cores is number of instance cores allowed for each project
+	Cores int `mapstructure:"cores"`
+	// Instances is number of instances allowed for each project
+	Instances int `mapstructure:"instances"`
+}
+
+// QuotaSetPage stores a single, only page of QuotaSet results from a List call.
+type QuotaSetPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a QuotaSetsetPage is empty.
+func (page QuotaSetPage) IsEmpty() (bool, error) {
+	ks, err := ExtractQuotaSets(page)
+	return len(ks) == 0, err
+}
+
+// ExtractQuotaSets interprets a page of results as a slice of QuotaSets.
+func ExtractQuotaSets(page pagination.Page) ([]QuotaSet, error) {
+	var resp struct {
+		QuotaSets []QuotaSet `mapstructure:"quotas"`
+	}
+
+	err := mapstructure.Decode(page.(QuotaSetPage).Body, &resp)
+	results := make([]QuotaSet, len(resp.QuotaSets))
+	for i, q := range resp.QuotaSets {
+		results[i] = q
+	}
+	return results, 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) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		QuotaSet *QuotaSet `json:"quota_set" mapstructure:"quota_set"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+	return res.QuotaSet, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a QuotaSet.
+type GetResult struct {
+	quotaResult
+}
diff --git a/openstack/compute/v2/extensions/quotasets/urls.go b/openstack/compute/v2/extensions/quotasets/urls.go
new file mode 100644
index 0000000..c04d941
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/urls.go
@@ -0,0 +1,13 @@
+package quotasets
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-quota-sets"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func getURL(c *gophercloud.ServiceClient, tenantID string) string {
+	return c.ServiceURL(resourcePath, tenantID)
+}
diff --git a/openstack/compute/v2/extensions/quotasets/urls_test.go b/openstack/compute/v2/extensions/quotasets/urls_test.go
new file mode 100644
index 0000000..f19a6ad
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/urls_test.go
@@ -0,0 +1,16 @@
+package quotasets
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-quota-sets/wat", getURL(c, "wat"))
+}
diff --git a/openstack/compute/v2/extensions/secgroups/fixtures.go b/openstack/compute/v2/extensions/secgroups/fixtures.go
index 0f97ac8..e4ca587 100644
--- a/openstack/compute/v2/extensions/secgroups/fixtures.go
+++ b/openstack/compute/v2/extensions/secgroups/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package secgroups
 
 import (
@@ -216,6 +218,42 @@
 	})
 }
 
+func mockAddRuleResponseICMPZero(t *testing.T) {
+	th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group_rule": {
+    "from_port": 0,
+    "ip_protocol": "ICMP",
+    "to_port": 0,
+    "parent_group_id": "{groupID}",
+    "cidr": "0.0.0.0/0"
+  }
+}	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_rule": {
+    "from_port": 0,
+    "group": {},
+    "ip_protocol": "ICMP",
+    "to_port": 0,
+    "parent_group_id": "{groupID}",
+    "ip_range": {
+      "cidr": "0.0.0.0/0"
+    },
+    "id": "{ruleID}"
+  }
+}`)
+	})
+}
+
 func mockDeleteRuleResponse(t *testing.T, ruleID string) {
 	url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
 	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
index 81993bd..ec8019f 100644
--- a/openstack/compute/v2/extensions/secgroups/requests.go
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -101,23 +101,23 @@
 // CreateRuleOpts represents the configuration for adding a new rule to an
 // existing security group.
 type CreateRuleOpts struct {
-	// Required - the ID of the group that this rule will be added to.
+	// the ID of the group that this rule will be added to.
 	ParentGroupID string `json:"parent_group_id" required:"true"`
-	// Required - the lower bound of the port range that will be opened.
-	FromPort int `json:"from_port" required:"true"`
-	// Required - the upper bound of the port range that will be opened.
-	ToPort int `json:"to_port" required:"true"`
-	// Required - the protocol type that will be allowed, e.g. TCP.
+	// the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+	// the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+	// the protocol type that will be allowed, e.g. TCP.
 	IPProtocol string `json:"ip_protocol" required:"true"`
 	// ONLY required if FromGroupID is blank. This represents the IP range that
 	// will be the source of network traffic to your security group. Use
 	// 0.0.0.0/0 to allow all IP addresses.
-	CIDR string `json:"cidr,omitempty"`
+	CIDR string `json:"cidr,omitempty" or:"FromGroupID"`
 	// ONLY required if CIDR is blank. This value represents the ID of a group
 	// that forwards traffic to the parent group. So, instead of accepting
 	// network traffic from an entire IP range, you can instead refine the
 	// inbound source by an existing security group.
-	FromGroupID string `json:"group_id,omitempty"`
+	FromGroupID string `json:"group_id,omitempty" or:"CIDR"`
 }
 
 // CreateRuleOptsBuilder builds the create rule options into a serializable format.
diff --git a/openstack/compute/v2/extensions/secgroups/requests_test.go b/openstack/compute/v2/extensions/secgroups/requests_test.go
index 9496d4a..bdbedcd 100644
--- a/openstack/compute/v2/extensions/secgroups/requests_test.go
+++ b/openstack/compute/v2/extensions/secgroups/requests_test.go
@@ -217,6 +217,36 @@
 	th.AssertDeepEquals(t, expected, rule)
 }
 
+func TestAddRuleICMPZero(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddRuleResponseICMPZero(t)
+
+	opts := CreateRuleOpts{
+		ParentGroupID: groupID,
+		FromPort:      0,
+		ToPort:        0,
+		IPProtocol:    "ICMP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := CreateRule(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Rule{
+		FromPort:      0,
+		ToPort:        0,
+		Group:         Group{},
+		IPProtocol:    "ICMP",
+		ParentGroupID: groupID,
+		IPRange:       IPRange{CIDR: "0.0.0.0/0"},
+		ID:            ruleID,
+	}
+
+	th.AssertDeepEquals(t, expected, rule)
+}
+
 func TestDeleteRule(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/extensions/startstop/fixtures.go b/openstack/compute/v2/extensions/startstop/fixtures.go
index 7169f7f..c5b4290 100644
--- a/openstack/compute/v2/extensions/startstop/fixtures.go
+++ b/openstack/compute/v2/extensions/startstop/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package startstop
 
 import (
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index 224b996..8b2c94d 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -236,6 +236,12 @@
 }
 `
 
+const ServerPasswordBody = `
+{
+    "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
+}
+`
+
 var (
 	// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
 	ServerHerp = Server{
@@ -430,8 +436,8 @@
 	})
 }
 
-// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server password
-// change request.
+// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion
+// request.
 func HandleServerForceDeletionSuccessfully(t *testing.T) {
 	th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "POST")
@@ -704,3 +710,14 @@
 		w.WriteHeader(http.StatusAccepted)
 	})
 }
+
+// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request.
+func HandlePasswordGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, ServerPasswordBody)
+	})
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 27ab764..6e23ada 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -731,3 +731,9 @@
 		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"}
 	}
 }
+
+// GetPassword makes a request against the nova API to get the encrypted administrative password.
+func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
+	_, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil)
+	return
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 931ab36..9a5d701 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -142,6 +142,15 @@
 	th.AssertNoErr(t, res.Err)
 }
 
+func TestGetPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePasswordGetSuccessfully(t)
+
+	res := GetPassword(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
+}
+
 func TestRebootServer(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index ff2e795..023d0dd 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -1,12 +1,15 @@
 package servers
 
 import (
+	"crypto/rsa"
+	"encoding/base64"
 	"fmt"
 	"net/url"
 	"path"
 
 	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/pagination"
+	"github.com/mitchellh/mapstructure"
 )
 
 type serverResult struct {
@@ -62,6 +65,47 @@
 	gophercloud.Result
 }
 
+// GetPasswordResult represent the result of a get os-server-password operation.
+type GetPasswordResult struct {
+	gophercloud.Result
+}
+
+// ExtractPassword gets the encrypted password.
+// If privateKey != nil the password is decrypted with the private key.
+// If privateKey == nil the encrypted password is returned and can be decrypted with:
+//   echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
+func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
+
+	if r.Err != nil {
+		return "", r.Err
+	}
+
+	var response struct {
+		Password string `mapstructure:"password"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	if err == nil && privateKey != nil && response.Password != "" {
+		return decryptPassword(response.Password, privateKey)
+	}
+	return response.Password, err
+}
+
+func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
+	b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
+
+	n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
+	if err != nil {
+		return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
+	}
+	password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
+	if err != nil {
+		return "", fmt.Errorf("Failed to decrypt password: %s", err)
+	}
+
+	return string(password), nil
+}
+
 // ExtractImageID gets the ID of the newly created server image from the header
 func (res CreateImageResult) ExtractImageID() (string, error) {
 	if res.Err != nil {
diff --git a/openstack/compute/v2/servers/results_test.go b/openstack/compute/v2/servers/results_test.go
new file mode 100644
index 0000000..5b56055
--- /dev/null
+++ b/openstack/compute/v2/servers/results_test.go
@@ -0,0 +1,99 @@
+// +build fixtures
+
+package servers
+
+import (
+	"crypto/rsa"
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"golang.org/x/crypto/ssh"
+)
+
+// Fail - No password in JSON.
+func TestExtractPassword_no_pwd_data(t *testing.T) {
+
+	var dejson interface{}
+	err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(nil)
+	th.AssertEquals(t, pwd, "")
+}
+
+// Ok - return encrypted password when no private key is given.
+func TestExtractPassword_encrypted_pwd(t *testing.T) {
+
+	var dejson interface{}
+	sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+	err := json.Unmarshal(sejson, &dejson)
+	fmt.Printf("%v\n", dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(nil)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd)
+}
+
+// Ok - return decrypted password when private key is given.
+// Decrytion can be verified by:
+//   echo "<enc_pwd>" | base64 -D | openssl rsautl -decrypt -inkey <privateKey.pem>
+func TestExtractPassword_decrypted_pwd(t *testing.T) {
+
+	privateKey, err := ssh.ParseRawPrivateKey([]byte(`
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+
+yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv
+X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+
+x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw
+JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1
+ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j
+RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1
+g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew
+OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4
+FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm//
+S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS
+/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db
+QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf
+fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm
+AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs
+n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ
+zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ
+xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb
+eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF
+G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R
+1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1
+ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX
+mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg
+09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c
+KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0=
+-----END RSA PRIVATE KEY-----
+`))
+	if err != nil {
+		t.Fatalf("Error parsing private key: %s\n", err)
+	}
+
+	var dejson interface{}
+	sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+	err = json.Unmarshal(sejson, &dejson)
+	fmt.Printf("%v\n", dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey))
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd)
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index a11504c..e892e8d 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -45,3 +45,7 @@
 func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
 	return client.ServiceURL("servers", id, "ips", network)
 }
+
+func passwordURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "os-server-password")
+}
diff --git a/openstack/db/v1/configurations/fixtures.go b/openstack/db/v1/configurations/fixtures.go
index ae65416..9064c6c 100644
--- a/openstack/db/v1/configurations/fixtures.go
+++ b/openstack/db/v1/configurations/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package configurations
 
 import (
diff --git a/openstack/db/v1/databases/fixtures.go b/openstack/db/v1/databases/fixtures.go
index 4b35062..c99f990 100644
--- a/openstack/db/v1/databases/fixtures.go
+++ b/openstack/db/v1/databases/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package databases
 
 import (
diff --git a/openstack/db/v1/datastores/fixtures.go b/openstack/db/v1/datastores/fixtures.go
index 837b1f4..8caa586 100644
--- a/openstack/db/v1/datastores/fixtures.go
+++ b/openstack/db/v1/datastores/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package datastores
 
 import (
diff --git a/openstack/db/v1/flavors/fixtures.go b/openstack/db/v1/flavors/fixtures.go
index 6a013f9..257b214 100644
--- a/openstack/db/v1/flavors/fixtures.go
+++ b/openstack/db/v1/flavors/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package flavors
 
 import (
diff --git a/openstack/db/v1/instances/fixtures.go b/openstack/db/v1/instances/fixtures.go
index d0a3856..6be384c 100644
--- a/openstack/db/v1/instances/fixtures.go
+++ b/openstack/db/v1/instances/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package instances
 
 import (
diff --git a/openstack/db/v1/users/fixtures.go b/openstack/db/v1/users/fixtures.go
index 3b27005..3661154 100644
--- a/openstack/db/v1/users/fixtures.go
+++ b/openstack/db/v1/users/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package users
 
 import (
diff --git a/openstack/identity/v2/extensions/admin/roles/fixtures.go b/openstack/identity/v2/extensions/admin/roles/fixtures.go
index 519dfae..6b11f5c 100644
--- a/openstack/identity/v2/extensions/admin/roles/fixtures.go
+++ b/openstack/identity/v2/extensions/admin/roles/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package roles
 
 import (
diff --git a/openstack/identity/v2/users/fixtures.go b/openstack/identity/v2/users/fixtures.go
index ecd1768..7b0bc4c 100644
--- a/openstack/identity/v2/users/fixtures.go
+++ b/openstack/identity/v2/users/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package users
 
 import (
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
index 48c0a52..71b2f62 100644
--- a/openstack/networking/v2/extensions/layer3/routers/requests.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -40,6 +40,10 @@
 	})
 }
 
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
 type CreateOptsBuilder interface {
 	ToRouterCreateMap() (map[string]interface{}, error)
 }
diff --git a/openstack/networking/v2/subnets/errors.go b/openstack/networking/v2/subnets/errors.go
index 0db0a6e..d2f7b46 100644
--- a/openstack/networking/v2/subnets/errors.go
+++ b/openstack/networking/v2/subnets/errors.go
@@ -7,7 +7,8 @@
 }
 
 var (
-	errNetworkIDRequired = err("A network ID is required")
-	errCIDRRequired      = err("A valid CIDR is required")
-	errInvalidIPType     = err("An IP type must either be 4 or 6")
+	errNetworkIDRequired    = err("A network ID is required")
+	errCIDRRequired         = err("A valid CIDR is required")
+	errInvalidIPType        = err("An IP type must either be 4 or 6")
+	errInvalidGatewayConfig = err("Both disabling the gateway and specifying a gateway is not allowed")
 )
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 2706a5e..769474d 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -64,16 +64,6 @@
 	return
 }
 
-// IPVersion is the IP address version for the subnet. Valid instances are
-// 4 and 6
-type IPVersion int
-
-// Valid IP types
-const (
-	IPv4 IPVersion = 4
-	IPv6 IPVersion = 6
-)
-
 // CreateOptsBuilder is the interface options structs have to satisfy in order
 // to be used in the main Create operation in this package. Since many
 // extensions decorate or modify the common logic, it is useful for them to
@@ -84,16 +74,16 @@
 
 // CreateOpts represents the attributes used when creating a new subnet.
 type CreateOpts struct {
-	NetworkID       string           `json:"network_id" required:"true"`
-	CIDR            string           `json:"cidr" required:"true"`
-	Name            string           `json:"name,omitempty"`
-	TenantID        string           `json:"tenant_id,omitempty"`
-	AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
-	GatewayIP       string           `json:"gateway_ip,omitempty"`
-	IPVersion       IPVersion        `json:"ip_version,omitempty"`
-	EnableDHCP      *bool            `json:"enable_dhcp,omitempty"`
-	DNSNameservers  []string         `json:"dns_nameservers,omitempty"`
-	HostRoutes      []HostRoute      `json:"host_routes,omitempty"`
+	NetworkID       string                `json:"network_id" required:"true"`
+	CIDR            string                `json:"cidr" required:"true"`
+	Name            string                `json:"name,omitempty"`
+	TenantID        string                `json:"tenant_id,omitempty"`
+	AllocationPools []AllocationPool      `json:"allocation_pools,omitempty"`
+	GatewayIP       *string               `json:"gateway_ip"`
+	IPVersion       gophercloud.IPVersion `json:"ip_version,omitempty"`
+	EnableDHCP      *bool                 `json:"enable_dhcp,omitempty"`
+	DNSNameservers  []string              `json:"dns_nameservers,omitempty"`
+	HostRoutes      []HostRoute           `json:"host_routes,omitempty"`
 }
 
 // ToSubnetCreateMap casts a CreateOpts struct to a map.
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index 5178c90..4241c63 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -59,6 +59,24 @@
             "gateway_ip": "192.0.0.1",
             "cidr": "192.0.0.0/8",
             "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        },
+        {
+            "name": "my_gatewayless_subnet",
+            "enable_dhcp": true,
+            "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "dns_nameservers": [],
+            "allocation_pools": [
+                {
+                    "start": "192.168.1.2",
+                    "end": "192.168.1.254"
+                }
+            ],
+            "host_routes": [],
+            "ip_version": 4,
+            "gateway_ip": null,
+            "cidr": "192.168.1.0/24",
+            "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
         }
     ]
 }
@@ -112,6 +130,24 @@
 				CIDR:       "192.0.0.0/8",
 				ID:         "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
 			},
+			Subnet{
+				Name:           "my_gatewayless_subnet",
+				EnableDHCP:     true,
+				NetworkID:      "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+				TenantID:       "4fd44f30292945e481c7b8a0c8908869",
+				DNSNameservers: []string{},
+				AllocationPools: []AllocationPool{
+					AllocationPool{
+						Start: "192.168.1.2",
+						End:   "192.168.1.254",
+					},
+				},
+				HostRoutes: []HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "",
+				CIDR:       "192.168.1.0/24",
+				ID:         "54d6f61d-db07-451c-9ab3-b9609b6b6f0c",
+			},
 		}
 
 		th.CheckDeepEquals(t, expected, actual)
@@ -194,6 +230,7 @@
     "subnet": {
         "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
         "ip_version": 4,
+				"gateway_ip": null,
         "cidr": "192.168.199.0/24",
 				"dns_nameservers": ["foo"],
 				"allocation_pools": [
@@ -270,6 +307,90 @@
 	th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
 }
 
+func TestCreateNoGateway(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets", 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, `
+{
+    "subnet": {
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+        "ip_version": 4,
+        "cidr": "192.168.1.0/24",
+				"gateway_ip": null,
+				"allocation_pools": [
+						{
+								"start": "192.168.1.2",
+								"end": "192.168.1.254"
+						}
+				]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "allocation_pools": [
+            {
+                "start": "192.168.1.2",
+                "end": "192.168.1.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": null,
+        "cidr": "192.168.1.0/24",
+        "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+    }
+}
+		`)
+	})
+
+	opts := CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+		IPVersion: 4,
+		CIDR:      "192.168.1.0/24",
+		AllocationPools: []AllocationPool{
+			AllocationPool{
+				Start: "192.168.1.2",
+				End:   "192.168.1.254",
+			},
+		},
+		DNSNameservers: []string{},
+	}
+	s, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{
+		AllocationPool{
+			Start: "192.168.1.2",
+			End:   "192.168.1.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "")
+	th.AssertEquals(t, s.CIDR, "192.168.1.0/24")
+	th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c")
+}
+
 func TestRequiredCreateOpts(t *testing.T) {
 	res := Create(fake.ServiceClient(), CreateOpts{})
 	if res.Err == nil {
diff --git a/openstack/orchestration/v1/buildinfo/fixtures.go b/openstack/orchestration/v1/buildinfo/fixtures.go
index 4e93126..87bc3ec 100644
--- a/openstack/orchestration/v1/buildinfo/fixtures.go
+++ b/openstack/orchestration/v1/buildinfo/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package buildinfo
 
 import (
diff --git a/openstack/orchestration/v1/stackevents/fixtures.go b/openstack/orchestration/v1/stackevents/fixtures.go
index 48524e5..e4af1bb 100644
--- a/openstack/orchestration/v1/stackevents/fixtures.go
+++ b/openstack/orchestration/v1/stackevents/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package stackevents
 
 import (
diff --git a/openstack/orchestration/v1/stackresources/fixtures.go b/openstack/orchestration/v1/stackresources/fixtures.go
index a622f7f..beec637 100644
--- a/openstack/orchestration/v1/stackresources/fixtures.go
+++ b/openstack/orchestration/v1/stackresources/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package stackresources
 
 import (
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
index f1d66f4..4126cf6 100644
--- a/openstack/orchestration/v1/stacks/fixtures.go
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package stacks
 
 import (
diff --git a/openstack/orchestration/v1/stacktemplates/fixtures.go b/openstack/orchestration/v1/stacktemplates/fixtures.go
index bfcaa90..4444a57 100644
--- a/openstack/orchestration/v1/stacktemplates/fixtures.go
+++ b/openstack/orchestration/v1/stacktemplates/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
 package stacktemplates
 
 import (