Merge pull request #238 from jamiehannaford/server-rebuild-opts

Introducing RebuildOpts
diff --git a/.travis.yml b/.travis.yml
index 9c37aef..cf4f8ca 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,11 @@
 language: go
 install:
-  - go get -v ./...
+  - go get -v -tags 'fixtures acceptance' ./...
 go:
   - 1.1
   - 1.2
   - tip
+script: script/cibuild
 after_success:
   - go get code.google.com/p/go.tools/cmd/cover
   - go get github.com/axw/gocov/gocov
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
index ca810bc..9c8e322 100644
--- a/acceptance/openstack/compute/v2/flavors_test.go
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -17,7 +17,7 @@
 
 	t.Logf("ID\tRegion\tName\tStatus\tCreated")
 
-	pager := flavors.List(client, flavors.ListFilterOptions{})
+	pager := flavors.List(client, nil)
 	count, pages := 0, 0
 	pager.EachPage(func(page pagination.Page) (bool, error) {
 		t.Logf("---")
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
index 7fca3ec..6166fc8 100644
--- a/acceptance/openstack/compute/v2/images_test.go
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -17,7 +17,7 @@
 
 	t.Logf("ID\tRegion\tName\tStatus\tCreated")
 
-	pager := images.List(client)
+	pager := images.ListDetail(client, nil)
 	count, pages := 0, 0
 	pager.EachPage(func(page pagination.Page) (bool, error) {
 		pages++
diff --git a/acceptance/openstack/networking/v2/extensions/layer3_test.go b/acceptance/openstack/networking/v2/extensions/layer3_test.go
index 1289113..63e0be3 100644
--- a/acceptance/openstack/networking/v2/extensions/layer3_test.go
+++ b/acceptance/openstack/networking/v2/extensions/layer3_test.go
@@ -219,7 +219,7 @@
 }
 
 func updateRouter(t *testing.T, routerID string) {
-	r, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
+	_, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
 		Name: "another_name",
 	}).Extract()
 
diff --git a/acceptance/openstack/networking/v2/extensions/provider_test.go b/acceptance/openstack/networking/v2/extensions/provider_test.go
index dc21ae5..f10c9d9 100644
--- a/acceptance/openstack/networking/v2/extensions/provider_test.go
+++ b/acceptance/openstack/networking/v2/extensions/provider_test.go
@@ -6,24 +6,25 @@
 	"strconv"
 	"testing"
 
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
 )
 
 func TestNetworkCRUDOperations(t *testing.T) {
-	Setup(t)
-	defer Teardown()
+	base.Setup(t)
+	defer base.Teardown()
 
 	// Create a network
-	n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
+	n, err := networks.Create(base.Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
 	th.AssertNoErr(t, err)
 	th.AssertEquals(t, n.Name, "sample_network")
 	th.AssertEquals(t, n.AdminStateUp, true)
 	networkID := n.ID
 
 	// List networks
-	pager := networks.List(Client, networks.ListOpts{Limit: 2})
+	pager := networks.List(base.Client, networks.ListOpts{Limit: 2})
 	err = pager.EachPage(func(page pagination.Page) (bool, error) {
 		t.Logf("--- Page ---")
 
@@ -43,7 +44,7 @@
 	if networkID == "" {
 		t.Fatalf("In order to retrieve a network, the NetworkID must be set")
 	}
-	n, err = networks.Get(Client, networkID).Extract()
+	n, err = networks.Get(base.Client, networkID).Extract()
 	th.AssertNoErr(t, err)
 	th.AssertEquals(t, n.Status, "ACTIVE")
 	th.AssertDeepEquals(t, n.Subnets, []string{})
@@ -53,12 +54,12 @@
 	th.AssertEquals(t, n.ID, networkID)
 
 	// Update network
-	n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+	n, err = networks.Update(base.Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
 	th.AssertNoErr(t, err)
 	th.AssertEquals(t, n.Name, "new_network_name")
 
 	// Delete network
-	res := networks.Delete(Client, networkID)
+	res := networks.Delete(base.Client, networkID)
 	th.AssertNoErr(t, res.Err)
 }
 
diff --git a/acceptance/openstack/networking/v2/extensions/security_test.go b/acceptance/openstack/networking/v2/extensions/security_test.go
index 522219a..16ecca4 100644
--- a/acceptance/openstack/networking/v2/extensions/security_test.go
+++ b/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -120,18 +120,6 @@
 	t.Logf("Deleted security group %s", groupID)
 }
 
-func deletePort(t *testing.T, portID string) {
-	res := ports.Delete(base.Client, portID)
-	th.AssertNoErr(t, res.Err)
-	t.Logf("Deleted port %s", portID)
-}
-
-func deleteNetwork(t *testing.T, networkID string) {
-	res := networks.Delete(base.Client, networkID)
-	th.AssertNoErr(t, res.Err)
-	t.Logf("Deleted network %s", networkID)
-}
-
 func createSecRule(t *testing.T, groupID string) string {
 	r, err := rules.Create(base.Client, rules.CreateOpts{
 		Direction:    "ingress",
diff --git a/acceptance/openstack/networking/v2/network_test.go b/acceptance/openstack/networking/v2/network_test.go
index f00edba..be8a3a1 100644
--- a/acceptance/openstack/networking/v2/network_test.go
+++ b/acceptance/openstack/networking/v2/network_test.go
@@ -16,7 +16,7 @@
 	defer Teardown()
 
 	// Create a network
-	n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
+	n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
 	th.AssertNoErr(t, err)
 	defer networks.Delete(Client, n.ID)
 	th.AssertEquals(t, n.Name, "sample_network")
diff --git a/acceptance/openstack/networking/v2/port_test.go b/acceptance/openstack/networking/v2/port_test.go
index 363a7fe..7f22dbd 100644
--- a/acceptance/openstack/networking/v2/port_test.go
+++ b/acceptance/openstack/networking/v2/port_test.go
@@ -97,18 +97,17 @@
 }
 
 func createNetwork() (string, error) {
-	res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: true}).Extract()
+	res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
 	return res.ID, err
 }
 
 func createSubnet(networkID string) (string, error) {
-	enable := false
 	s, err := subnets.Create(Client, subnets.CreateOpts{
 		NetworkID:  networkID,
 		CIDR:       "192.168.199.0/24",
 		IPVersion:  subnets.IPv4,
 		Name:       "my_subnet",
-		EnableDHCP: &enable,
+		EnableDHCP: subnets.Down,
 	}).Extract()
 	return s.ID, err
 }
diff --git a/acceptance/openstack/networking/v2/subnet_test.go b/acceptance/openstack/networking/v2/subnet_test.go
index 2758e2a..097a303 100644
--- a/acceptance/openstack/networking/v2/subnet_test.go
+++ b/acceptance/openstack/networking/v2/subnet_test.go
@@ -38,7 +38,7 @@
 
 	// Setup network
 	t.Log("Setting up network")
-	n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: true}).Extract()
+	n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
 	th.AssertNoErr(t, err)
 	networkID := n.ID
 	defer networks.Delete(Client, networkID)
diff --git a/acceptance/rackspace/client_test.go b/acceptance/rackspace/client_test.go
new file mode 100644
index 0000000..e68aef8
--- /dev/null
+++ b/acceptance/rackspace/client_test.go
@@ -0,0 +1,29 @@
+// +build acceptance
+
+package rackspace
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/utils"
+	"github.com/rackspace/gophercloud/rackspace"
+)
+
+func TestAuthenticatedClient(t *testing.T) {
+	// Obtain credentials from the environment.
+	ao, err := utils.AuthOptions()
+	if err != nil {
+		t.Fatalf("Unable to acquire credentials: %v", err)
+	}
+
+	client, err := rackspace.AuthenticatedClient(ao)
+	if err != nil {
+		t.Fatalf("Unable to authenticate: %v", err)
+	}
+
+	if client.TokenID == "" {
+		t.Errorf("No token ID assigned to the client")
+	}
+
+	t.Logf("Client successfully acquired a token: %v", client.TokenID)
+}
diff --git a/acceptance/rackspace/identity/v2/extension_test.go b/acceptance/rackspace/identity/v2/extension_test.go
new file mode 100644
index 0000000..a50e015
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/extension_test.go
@@ -0,0 +1,54 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	extensions2 "github.com/rackspace/gophercloud/rackspace/identity/v2/extensions"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestExtensions(t *testing.T) {
+	service := authenticatedClient(t)
+
+	t.Logf("Extensions available on this identity endpoint:")
+	count := 0
+	var chosen string
+	err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		extensions, err := extensions2.ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+
+		for i, ext := range extensions {
+			if chosen == "" {
+				chosen = ext.Alias
+			}
+
+			t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace)
+			t.Logf("     alias=[%s] updated=[%s]", ext.Alias, ext.Updated)
+			t.Logf("     description=[%s]", ext.Description)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if chosen == "" {
+		t.Logf("No extensions found.")
+		return
+	}
+
+	ext, err := extensions2.Get(service, chosen).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Detail for extension [%s]:", chosen)
+	t.Logf("        name=[%s]", ext.Name)
+	t.Logf("   namespace=[%s]", ext.Namespace)
+	t.Logf("       alias=[%s]", ext.Alias)
+	t.Logf("     updated=[%s]", ext.Updated)
+	t.Logf(" description=[%s]", ext.Description)
+}
diff --git a/acceptance/rackspace/identity/v2/identity_test.go b/acceptance/rackspace/identity/v2/identity_test.go
new file mode 100644
index 0000000..019a9e6
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/identity_test.go
@@ -0,0 +1,51 @@
+// +build acceptance
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
+	// Obtain credentials from the environment.
+	options := gophercloud.AuthOptions{
+		Username: os.Getenv("RS_USERNAME"),
+		APIKey:   os.Getenv("RS_APIKEY"),
+	}
+
+	if options.Username == "" {
+		t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
+	}
+	if options.APIKey == "" {
+		t.Fatal("Please provide a Rackspace API key as RS_APIKEY.")
+	}
+
+	return options
+}
+
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+	ao := rackspaceAuthOptions(t)
+
+	provider, err := rackspace.NewClient(ao.IdentityEndpoint)
+	th.AssertNoErr(t, err)
+
+	if auth {
+		err = rackspace.Authenticate(provider, ao)
+		th.AssertNoErr(t, err)
+	}
+
+	return rackspace.NewIdentityV2(provider)
+}
+
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	return createClient(t, true)
+}
diff --git a/acceptance/rackspace/identity/v2/tenant_test.go b/acceptance/rackspace/identity/v2/tenant_test.go
new file mode 100644
index 0000000..6081a49
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/tenant_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	rstenants "github.com/rackspace/gophercloud/rackspace/identity/v2/tenants"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTenants(t *testing.T) {
+	service := authenticatedClient(t)
+
+	t.Logf("Tenants available to the currently issued token:")
+	count := 0
+	err := rstenants.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		tenants, err := rstenants.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		for i, tenant := range tenants {
+			t.Logf("[%02d]      id=[%s]", i, tenant.ID)
+			t.Logf("        name=[%s] enabled=[%v]", i, tenant.Name, tenant.Enabled)
+			t.Logf(" description=[%s]", tenant.Description)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No tenants listed for your current token.")
+	}
+}
diff --git a/acceptance/rackspace/pkg.go b/acceptance/rackspace/pkg.go
new file mode 100644
index 0000000..5d17b32
--- /dev/null
+++ b/acceptance/rackspace/pkg.go
@@ -0,0 +1 @@
+package rackspace
diff --git a/endpoint_search_test.go b/endpoint_search_test.go
new file mode 100644
index 0000000..3457453
--- /dev/null
+++ b/endpoint_search_test.go
@@ -0,0 +1,19 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestApplyDefaultsToEndpointOpts(t *testing.T) {
+	eo := EndpointOpts{Availability: AvailabilityPublic}
+	eo.ApplyDefaults("compute")
+	expected := EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+	th.CheckDeepEquals(t, expected, eo)
+
+	eo = EndpointOpts{Type: "compute"}
+	eo.ApplyDefaults("object-store")
+	expected = EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+	th.CheckDeepEquals(t, expected, eo)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
index c4ac157..ea2f7f5 100644
--- a/openstack/blockstorage/v1/apiversions/results.go
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -37,11 +37,8 @@
 	}
 
 	err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Versions, nil
+	return resp.Versions, err
 }
 
 // GetResult represents the result of a get operation.
@@ -56,9 +53,6 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Version, nil
+	return resp.Version, err
 }
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
index f3180d7..7fac925 100644
--- a/openstack/blockstorage/v1/snapshots/requests.go
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -1,50 +1,74 @@
 package snapshots
 
 import (
+	"fmt"
+
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 
 	"github.com/racker/perigee"
 )
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToSnapshotCreateMap() (map[string]interface{}, error)
+}
+
 // CreateOpts contains options for creating a Snapshot. This object is passed to
 // the snapshots.Create function. For more information about these parameters,
 // see the Snapshot object.
 type CreateOpts struct {
-	Description string                 // OPTIONAL
-	Force       bool                   // OPTIONAL
-	Metadata    map[string]interface{} // OPTIONAL
-	Name        string                 // OPTIONAL
-	VolumeID    string                 // REQUIRED
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Force bool
+	// OPTIONAL
+	Metadata map[string]interface{}
+	// OPTIONAL
+	Name string
+	// REQUIRED
+	VolumeID string
 }
 
-// Create will create a new Snapshot based on the values in CreateOpts. To extract
-// the Snapshot object from the response, call the Extract method on the
+// ToSnapshotCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.VolumeID == "" {
+		return nil, fmt.Errorf("Required CreateOpts field 'VolumeID' not set.")
+	}
+	s["volume_id"] = opts.VolumeID
+
+	if opts.Description != "" {
+		s["display_description"] = opts.Description
+	}
+	if opts.Force == true {
+		s["force"] = opts.Force
+	}
+	if opts.Metadata != nil {
+		s["metadata"] = opts.Metadata
+	}
+	if opts.Name != "" {
+		s["display_name"] = opts.Name
+	}
+
+	return map[string]interface{}{"snapshot": s}, nil
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To
+// extract the Snapshot object from the response, call the Extract method on the
 // CreateResult.
-func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
-	type snapshot struct {
-		Description *string                `json:"display_description,omitempty"`
-		Force       bool                   `json:"force,omitempty"`
-		Metadata    map[string]interface{} `json:"metadata,omitempty"`
-		Name        *string                `json:"display_name,omitempty"`
-		VolumeID    *string                `json:"volume_id,omitempty"`
-	}
-
-	type request struct {
-		Snapshot snapshot `json:"snapshot"`
-	}
-
-	reqBody := request{
-		Snapshot: snapshot{},
-	}
-
-	reqBody.Snapshot.Description = gophercloud.MaybeString(opts.Description)
-	reqBody.Snapshot.Name = gophercloud.MaybeString(opts.Name)
-	reqBody.Snapshot.VolumeID = gophercloud.MaybeString(opts.VolumeID)
-
-	reqBody.Snapshot.Force = opts.Force
-
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
+
+	reqBody, err := opts.ToSnapshotCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{200, 201},
@@ -63,8 +87,8 @@
 	return err
 }
 
-// Get retrieves the Snapshot with the provided ID. To extract the Snapshot object
-// from the response, call the Extract method on the GetResult.
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot
+// object from the response, call the Extract method on the GetResult.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var res GetResult
 	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
@@ -75,6 +99,12 @@
 	return res
 }
 
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToSnapshotListQuery() (string, error)
+}
+
 // ListOpts hold options for listing Snapshots. It is passed to the
 // snapshots.List function.
 type ListOpts struct {
@@ -83,15 +113,25 @@
 	VolumeID string `q:"volume_id"`
 }
 
-// List returns Snapshots optionally limited by the conditions provided in ListOpts.
-func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+// ToSnapshotListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSnapshotListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns Snapshots optionally limited by the conditions provided in
+// ListOpts.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
 	url := listURL(client)
 	if opts != nil {
-		query, err := gophercloud.BuildQueryString(opts)
+		query, err := opts.ToSnapshotListQuery()
 		if err != nil {
 			return pagination.Pager{Err: err}
 		}
-		url += query.String()
+		url += query
 	}
 
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
@@ -100,6 +140,12 @@
 	return pagination.NewPager(client, url, createPage)
 }
 
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to
+// the Update request.
+type UpdateMetadataOptsBuilder interface {
+	ToSnapshotUpdateMetadataMap() (map[string]interface{}, error)
+}
+
 // UpdateMetadataOpts contain options for updating an existing Snapshot. This
 // object is passed to the snapshots.Update function. For more information
 // about the parameters, see the Snapshot object.
@@ -107,20 +153,30 @@
 	Metadata map[string]interface{}
 }
 
+// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of
+// an UpdateMetadataOpts.
+func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Metadata != nil {
+		v["metadata"] = opts.Metadata
+	}
+
+	return v, nil
+}
+
 // UpdateMetadata will update the Snapshot with provided information. To
 // extract the updated Snapshot from the response, call the ExtractMetadata
 // method on the UpdateMetadataResult.
-func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts *UpdateMetadataOpts) UpdateMetadataResult {
-	type request struct {
-		Metadata map[string]interface{} `json:"metadata,omitempty"`
-	}
-
-	reqBody := request{}
-
-	reqBody.Metadata = opts.Metadata
-
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
 	var res UpdateMetadataResult
 
+	reqBody, err := opts.ToSnapshotUpdateMetadataMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{200},
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
index d29cc0d..ddfa81b 100644
--- a/openstack/blockstorage/v1/snapshots/requests_test.go
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -119,6 +119,7 @@
 		th.TestJSONRequest(t, r, `
 {
     "snapshot": {
+				"volume_id": "1234",
         "display_name": "snapshot-001"
     }
 }
@@ -130,6 +131,7 @@
 		fmt.Fprintf(w, `
 {
     "snapshot": {
+				"volume_id": "1234",
         "display_name": "snapshot-001",
         "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
     }
@@ -137,10 +139,11 @@
 		`)
 	})
 
-	options := &CreateOpts{Name: "snapshot-001"}
+	options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
 	n, err := Create(ServiceClient(), options).Extract()
 	th.AssertNoErr(t, err)
 
+	th.AssertEquals(t, n.VolumeID, "1234")
 	th.AssertEquals(t, n.Name, "snapshot-001")
 	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
 }
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
index 9509bca..dc94a32 100644
--- a/openstack/blockstorage/v1/snapshots/results.go
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -1,8 +1,6 @@
 package snapshots
 
 import (
-	"fmt"
-
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 
@@ -11,19 +9,32 @@
 
 // Snapshot contains all the information associated with an OpenStack Snapshot.
 type Snapshot struct {
-	Status           string            `mapstructure:"status"`              // currect status of the Snapshot
-	Name             string            `mapstructure:"display_name"`        // display name
-	Attachments      []string          `mapstructure:"attachments"`         // instances onto which the Snapshot is attached
-	AvailabilityZone string            `mapstructure:"availability_zone"`   // logical group
-	Bootable         string            `mapstructure:"bootable"`            // is the Snapshot bootable
-	CreatedAt        string            `mapstructure:"created_at"`          // date created
-	Description      string            `mapstructure:"display_discription"` // display description
-	VolumeType       string            `mapstructure:"volume_type"`         // see VolumeType object for more information
-	SnapshotID       string            `mapstructure:"snapshot_id"`         // ID of the Snapshot from which this Snapshot was created
-	SourceVolID      string            `mapstructure:"source_volid"`        // ID of the Volume from which this Snapshot was created
-	Metadata         map[string]string `mapstructure:"metadata"`            // user-defined key-value pairs
-	ID               string            `mapstructure:"id"`                  // unique identifier
-	Size             int               `mapstructure:"size"`                // size of the Snapshot, in GB
+	// Currect status of the Snapshot.
+	Status string `mapstructure:"status"`
+	// Display name.
+	Name string `mapstructure:"display_name"`
+	// Instances onto which the Snapshot is attached.
+	Attachments []string `mapstructure:"attachments"`
+	// Logical group.
+	AvailabilityZone string `mapstructure:"availability_zone"`
+	// Is the Snapshot bootable?
+	Bootable string `mapstructure:"bootable"`
+	// Date created.
+	CreatedAt string `mapstructure:"created_at"`
+	// Display description.
+	Description string `mapstructure:"display_discription"`
+	// See VolumeType object for more information.
+	VolumeType string `mapstructure:"volume_type"`
+	// ID of the Snapshot from which this Snapshot was created.
+	SnapshotID string `mapstructure:"snapshot_id"`
+	// ID of the Volume from which this Snapshot was created.
+	VolumeID string `mapstructure:"volume_id"`
+	// User-defined key-value pairs.
+	Metadata map[string]string `mapstructure:"metadata"`
+	// Unique identifier.
+	ID string `mapstructure:"id"`
+	// Size of the Snapshot, in GB.
+	Size int `mapstructure:"size"`
 }
 
 // CreateResult contains the response body and error from a Create request.
@@ -91,8 +102,6 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("snapshots: Error decoding snapshots.commonResult: %v", err)
-	}
-	return res.Snapshot, nil
+
+	return res.Snapshot, err
 }
diff --git a/openstack/blockstorage/v1/snapshots/util_test.go b/openstack/blockstorage/v1/snapshots/util_test.go
new file mode 100644
index 0000000..46b452e
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util_test.go
@@ -0,0 +1,37 @@
+package snapshots
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitForStatus(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(2 * time.Second)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+		{
+			"snapshot": {
+				"display_name": "snapshot-001",
+				"id": "1234",
+				"status":"available"
+			}
+		}`)
+	})
+
+	err := WaitForStatus(ServiceClient(), "1234", "available", 0)
+	if err == nil {
+		t.Errorf("Expected error: 'Time Out in WaitFor'")
+	}
+
+	err = WaitForStatus(ServiceClient(), "1234", "available", 3)
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
index bca27db..042a33e 100644
--- a/openstack/blockstorage/v1/volumes/requests.go
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -1,59 +1,90 @@
 package volumes
 
 import (
+	"fmt"
+
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 
 	"github.com/racker/perigee"
 )
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToVolumeCreateMap() (map[string]interface{}, error)
+}
+
 // CreateOpts contains options for creating a Volume. This object is passed to
 // the volumes.Create function. For more information about these parameters,
 // see the Volume object.
 type CreateOpts struct {
-	Availability                     string            // OPTIONAL
-	Description                      string            // OPTIONAL
-	Metadata                         map[string]string // OPTIONAL
-	Name                             string            // OPTIONAL
-	Size                             int               // REQUIRED
-	SnapshotID, SourceVolID, ImageID string            // REQUIRED (one of them)
-	VolumeType                       string            // OPTIONAL
+	// OPTIONAL
+	Availability string
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Metadata map[string]string
+	// OPTIONAL
+	Name string
+	// REQUIRED
+	Size int
+	// OPTIONAL
+	SnapshotID, SourceVolID, ImageID string
+	// OPTIONAL
+	VolumeType string
+}
+
+// ToVolumeCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Size == 0 {
+		return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.")
+	}
+	v["size"] = opts.Size
+
+	if opts.Availability != "" {
+		v["availability_zone"] = opts.Availability
+	}
+	if opts.Description != "" {
+		v["display_description"] = opts.Description
+	}
+	if opts.ImageID != "" {
+		v["imageRef"] = opts.ImageID
+	}
+	if opts.Metadata != nil {
+		v["metadata"] = opts.Metadata
+	}
+	if opts.Name != "" {
+		v["display_name"] = opts.Name
+	}
+	if opts.SourceVolID != "" {
+		v["source_volid"] = opts.SourceVolID
+	}
+	if opts.SnapshotID != "" {
+		v["snapshot_id"] = opts.SnapshotID
+	}
+	if opts.VolumeType != "" {
+		v["volume_type"] = opts.VolumeType
+	}
+
+	return map[string]interface{}{"volume": v}, nil
 }
 
 // Create will create a new Volume based on the values in CreateOpts. To extract
-// the Volume object from the response, call the Extract method on the CreateResult.
-func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
-
-	type volume struct {
-		Availability *string           `json:"availability_zone,omitempty"`
-		Description  *string           `json:"display_description,omitempty"`
-		ImageID      *string           `json:"imageRef,omitempty"`
-		Metadata     map[string]string `json:"metadata,omitempty"`
-		Name         *string           `json:"display_name,omitempty"`
-		Size         *int              `json:"size,omitempty"`
-		SnapshotID   *string           `json:"snapshot_id,omitempty"`
-		SourceVolID  *string           `json:"source_volid,omitempty"`
-		VolumeType   *string           `json:"volume_type,omitempty"`
-	}
-
-	type request struct {
-		Volume volume `json:"volume"`
-	}
-
-	reqBody := request{
-		Volume: volume{},
-	}
-
-	reqBody.Volume.Availability = gophercloud.MaybeString(opts.Availability)
-	reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
-	reqBody.Volume.ImageID = gophercloud.MaybeString(opts.ImageID)
-	reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
-	reqBody.Volume.Size = gophercloud.MaybeInt(opts.Size)
-	reqBody.Volume.SnapshotID = gophercloud.MaybeString(opts.SnapshotID)
-	reqBody.Volume.SourceVolID = gophercloud.MaybeString(opts.SourceVolID)
-	reqBody.Volume.VolumeType = gophercloud.MaybeString(opts.VolumeType)
-
+// the Volume object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
+
+	reqBody, err := opts.ToVolumeCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		ReqBody:     &reqBody,
@@ -72,8 +103,8 @@
 	return err
 }
 
-// Get retrieves the Volume with the provided ID. To extract the Volume object from
-// the response, call the Extract method on the GetResult.
+// Get retrieves the Volume with the provided ID. To extract the Volume object
+// from the response, call the Extract method on the GetResult.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var res GetResult
 	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
@@ -84,62 +115,97 @@
 	return res
 }
 
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToVolumeListQuery() (string, error)
+}
+
 // ListOpts holds options for listing Volumes. It is passed to the volumes.List
 // function.
 type ListOpts struct {
-	AllTenants bool              `q:"all_tenants"` // admin-only option. Set it to true to see all tenant volumes.
-	Metadata   map[string]string `q:"metadata"`    // List only volumes that contain Metadata.
-	Name       string            `q:"name"`        // List only volumes that have Name as the display name.
-	Status     string            `q:"status"`      // List only volumes that have a status of Status.
+	// admin-only option. Set it to true to see all tenant volumes.
+	AllTenants bool `q:"all_tenants"`
+	// List only volumes that contain Metadata.
+	Metadata map[string]string `q:"metadata"`
+	// List only volumes that have Name as the display name.
+	Name string `q:"name"`
+	// List only volumes that have a status of Status.
+	Status string `q:"status"`
+}
+
+// ToVolumeListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToVolumeListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
 }
 
 // List returns Volumes optionally limited by the conditions provided in ListOpts.
-func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
 	url := listURL(client)
 	if opts != nil {
-		query, err := gophercloud.BuildQueryString(opts)
+		query, err := opts.ToVolumeListQuery()
 		if err != nil {
 			return pagination.Pager{Err: err}
 		}
-		url += query.String()
+		url += query
 	}
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
 		return ListResult{pagination.SinglePageBase(r)}
 	}
-	return pagination.NewPager(client, listURL(client), createPage)
+	return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToVolumeUpdateMap() (map[string]interface{}, error)
 }
 
 // UpdateOpts contain options for updating an existing Volume. This object is passed
 // to the volumes.Update function. For more information about the parameters, see
 // the Volume object.
 type UpdateOpts struct {
-	Name        string            // OPTIONAL
-	Description string            // OPTIONAL
-	Metadata    map[string]string // OPTIONAL
+	// OPTIONAL
+	Name string
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Metadata map[string]string
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Description != "" {
+		v["display_description"] = opts.Description
+	}
+	if opts.Metadata != nil {
+		v["metadata"] = opts.Metadata
+	}
+	if opts.Name != "" {
+		v["display_name"] = opts.Name
+	}
+
+	return map[string]interface{}{"volume": v}, nil
 }
 
 // Update will update the Volume with provided information. To extract the updated
 // Volume from the response, call the Extract method on the UpdateResult.
-func Update(client *gophercloud.ServiceClient, id string, opts *UpdateOpts) UpdateResult {
-	type update struct {
-		Description *string           `json:"display_description,omitempty"`
-		Metadata    map[string]string `json:"metadata,omitempty"`
-		Name        *string           `json:"display_name,omitempty"`
-	}
-
-	type request struct {
-		Volume update `json:"volume"`
-	}
-
-	reqBody := request{
-		Volume: update{},
-	}
-
-	reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
-	reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
-
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
 	var res UpdateResult
 
+	reqBody, err := opts.ToVolumeUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{200},
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
index 54ff91d..7cd37d5 100644
--- a/openstack/blockstorage/v1/volumes/requests_test.go
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -119,7 +119,7 @@
 		th.TestJSONRequest(t, r, `
 {
     "volume": {
-        "display_name": "vol-001"
+        "size": 4
     }
 }
 			`)
@@ -130,18 +130,18 @@
 		fmt.Fprintf(w, `
 {
     "volume": {
-        "display_name": "vol-001",
+        "size": 4,
         "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
     }
 }
 		`)
 	})
 
-	options := &CreateOpts{Name: "vol-001"}
+	options := &CreateOpts{Size: 4}
 	n, err := Create(ServiceClient(), options).Extract()
 	th.AssertNoErr(t, err)
 
-	th.AssertEquals(t, n.Name, "vol-001")
+	th.AssertEquals(t, n.Size, 4)
 	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
 }
 
@@ -158,3 +158,27 @@
 	err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
 	th.AssertNoErr(t, err)
 }
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+		{
+			"volume": {
+				"display_name": "vol-002",
+				"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+		    }
+		}
+		`)
+	})
+
+	options := &UpdateOpts{Name: "vol-002"}
+	v, err := Update(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "vol-002", v.Name)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
index 78c863f..78eb6c1 100644
--- a/openstack/blockstorage/v1/volumes/results.go
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -1,8 +1,6 @@
 package volumes
 
 import (
-	"fmt"
-
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 
@@ -80,8 +78,6 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("volumes: Error decoding volumes.commonResult: %v", err)
-	}
-	return res.Volume, nil
+
+	return res.Volume, err
 }
diff --git a/openstack/blockstorage/v1/volumes/util_test.go b/openstack/blockstorage/v1/volumes/util_test.go
new file mode 100644
index 0000000..7de1326
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util_test.go
@@ -0,0 +1,37 @@
+package volumes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitForStatus(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(2 * time.Second)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+		{
+			"volume": {
+				"display_name": "vol-001",
+				"id": "1234",
+				"status":"available"
+			}
+		}`)
+	})
+
+	err := WaitForStatus(ServiceClient(), "1234", "available", 0)
+	if err == nil {
+		t.Errorf("Expected error: 'Time Out in WaitFor'")
+	}
+
+	err = WaitForStatus(ServiceClient(), "1234", "available", 3)
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
index afe650d..d4f880f 100644
--- a/openstack/blockstorage/v1/volumetypes/requests.go
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -6,6 +6,12 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToVolumeTypeCreateMap() (map[string]interface{}, error)
+}
+
 // CreateOpts are options for creating a volume type.
 type CreateOpts struct {
 	// OPTIONAL. See VolumeType.
@@ -14,26 +20,31 @@
 	Name string
 }
 
-// Create will create a new volume, optionally wih CreateOpts. To extract the
-// created volume type object, call the Extract method on the CreateResult.
-func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
-	type volumeType struct {
-		ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"`
-		Name       *string                `json:"name,omitempty"`
+// ToVolumeTypeCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) {
+	vt := make(map[string]interface{})
+
+	if opts.ExtraSpecs != nil {
+		vt["extra_specs"] = opts.ExtraSpecs
+	}
+	if opts.Name != "" {
+		vt["name"] = opts.Name
 	}
 
-	type request struct {
-		VolumeType volumeType `json:"volume_type"`
-	}
+	return map[string]interface{}{"volume_type": vt}, nil
+}
 
-	reqBody := request{
-		VolumeType: volumeType{},
-	}
-
-	reqBody.VolumeType.Name = gophercloud.MaybeString(opts.Name)
-	reqBody.VolumeType.ExtraSpecs = opts.ExtraSpecs
-
+// Create will create a new volume. To extract the created volume type object,
+// call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
+
+	reqBody, err := opts.ToVolumeTypeCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{200, 201},
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
index 8e5932a..77cc1f5 100644
--- a/openstack/blockstorage/v1/volumetypes/results.go
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -1,8 +1,6 @@
 package volumetypes
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -64,9 +62,6 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Volume Type: %v", err)
-	}
 
-	return res.VolumeType, nil
+	return res.VolumeType, err
 }
diff --git a/openstack/client.go b/openstack/client.go
index f3638be..97556d6 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -3,15 +3,11 @@
 import (
 	"fmt"
 	"net/url"
-	"strings"
 
 	"github.com/rackspace/gophercloud"
 	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
-	endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
-	services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
 	tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
 	"github.com/rackspace/gophercloud/openstack/utils"
-	"github.com/rackspace/gophercloud/pagination"
 )
 
 const (
@@ -32,8 +28,8 @@
 	u.Path, u.RawQuery, u.Fragment = "", "", ""
 	base := u.String()
 
-	endpoint = normalizeURL(endpoint)
-	base = normalizeURL(base)
+	endpoint = gophercloud.NormalizeURL(endpoint)
+	base = gophercloud.NormalizeURL(base)
 
 	if hadPath {
 		return &gophercloud.ProviderClient{
@@ -99,7 +95,7 @@
 		v2Client.Endpoint = endpoint
 	}
 
-	result := tokens2.Create(v2Client, options)
+	result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options})
 
 	token, err := result.ExtractToken()
 	if err != nil {
@@ -113,50 +109,12 @@
 
 	client.TokenID = token.ID
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
-		return v2endpointLocator(catalog, opts)
+		return V2EndpointURL(catalog, opts)
 	}
 
 	return nil
 }
 
-func v2endpointLocator(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
-	// Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
-	var endpoints = make([]tokens2.Endpoint, 0, 1)
-	for _, entry := range catalog.Entries {
-		if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
-			for _, endpoint := range entry.Endpoints {
-				if opts.Region == "" || endpoint.Region == opts.Region {
-					endpoints = append(endpoints, endpoint)
-				}
-			}
-		}
-	}
-
-	// Report an error if the options were ambiguous.
-	if len(endpoints) == 0 {
-		return "", gophercloud.ErrEndpointNotFound
-	}
-	if len(endpoints) > 1 {
-		return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
-	}
-
-	// Extract the appropriate URL from the matching Endpoint.
-	for _, endpoint := range endpoints {
-		switch opts.Availability {
-		case gophercloud.AvailabilityPublic:
-			return normalizeURL(endpoint.PublicURL), nil
-		case gophercloud.AvailabilityInternal:
-			return normalizeURL(endpoint.InternalURL), nil
-		case gophercloud.AvailabilityAdmin:
-			return normalizeURL(endpoint.AdminURL), nil
-		default:
-			return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
-		}
-	}
-
-	return "", gophercloud.ErrEndpointNotFound
-}
-
 // AuthenticateV3 explicitly authenticates against the identity v3 service.
 func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
 	return v3auth(client, "", options)
@@ -176,85 +134,12 @@
 	client.TokenID = token.ID
 
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
-		return v3endpointLocator(v3Client, opts)
+		return V3EndpointURL(v3Client, opts)
 	}
 
 	return nil
 }
 
-func v3endpointLocator(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
-	// Discover the service we're interested in.
-	var services = make([]services3.Service, 0, 1)
-	servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
-	err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
-		part, err := services3.ExtractServices(page)
-		if err != nil {
-			return false, err
-		}
-
-		for _, service := range part {
-			if service.Name == opts.Name {
-				services = append(services, service)
-			}
-		}
-
-		return true, nil
-	})
-	if err != nil {
-		return "", err
-	}
-
-	if len(services) == 0 {
-		return "", gophercloud.ErrServiceNotFound
-	}
-	if len(services) > 1 {
-		return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
-	}
-	service := services[0]
-
-	// Enumerate the endpoints available for this service.
-	var endpoints []endpoints3.Endpoint
-	endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
-		Availability: opts.Availability,
-		ServiceID:    service.ID,
-	})
-	err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
-		part, err := endpoints3.ExtractEndpoints(page)
-		if err != nil {
-			return false, err
-		}
-
-		for _, endpoint := range part {
-			if opts.Region == "" || endpoint.Region == opts.Region {
-				endpoints = append(endpoints, endpoint)
-			}
-		}
-
-		return true, nil
-	})
-	if err != nil {
-		return "", err
-	}
-
-	if len(endpoints) == 0 {
-		return "", gophercloud.ErrEndpointNotFound
-	}
-	if len(endpoints) > 1 {
-		return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
-	}
-	endpoint := endpoints[0]
-
-	return normalizeURL(endpoint.URL), nil
-}
-
-// normalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
-func normalizeURL(url string) string {
-	if !strings.HasSuffix(url, "/") {
-		return url + "/"
-	}
-	return url
-}
-
 // NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
 func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
 	v2Endpoint := client.IdentityBase + "v2.0/"
diff --git a/openstack/common/extensions/fixtures.go b/openstack/common/extensions/fixtures.go
new file mode 100644
index 0000000..0ed7de9
--- /dev/null
+++ b/openstack/common/extensions/fixtures.go
@@ -0,0 +1,91 @@
+// +build fixtures
+
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Extension results.
+const ListOutput = `
+{
+	"extensions": [
+		{
+			"updated": "2013-01-20T00:00:00-00:00",
+			"name": "Neutron Service Type Management",
+			"links": [],
+			"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+			"alias": "service-type",
+			"description": "API for retrieving service providers for Neutron advanced services"
+		}
+	]
+}`
+
+// GetOutput provides a single Extension result.
+const GetOutput = `
+{
+	"extension": {
+		"updated": "2013-02-03T10:00:00-00:00",
+		"name": "agent",
+		"links": [],
+		"namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
+		"alias": "agent",
+		"description": "The agent management extension."
+	}
+}
+`
+
+// ListedExtension is the Extension that should be parsed from ListOutput.
+var ListedExtension = Extension{
+	Updated:     "2013-01-20T00:00:00-00:00",
+	Name:        "Neutron Service Type Management",
+	Links:       []interface{}{},
+	Namespace:   "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+	Alias:       "service-type",
+	Description: "API for retrieving service providers for Neutron advanced services",
+}
+
+// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput.
+var ExpectedExtensions = []Extension{ListedExtension}
+
+// SingleExtension is the Extension that should be parsed from GetOutput.
+var SingleExtension = &Extension{
+	Updated:     "2013-02-03T10:00:00-00:00",
+	Name:        "agent",
+	Links:       []interface{}{},
+	Namespace:   "http://docs.openstack.org/ext/agent/api/v2.0",
+	Alias:       "agent",
+	Description: "The agent management extension.",
+}
+
+// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler
+// mux that response with a list containing a single tenant.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/extensions", 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, ListOutput)
+	})
+}
+
+// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with
+// a JSON payload corresponding to SingleExtension.
+func HandleGetExtensionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/extensions/agent", 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")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, GetOutput)
+	})
+}
diff --git a/openstack/common/extensions/requests_test.go b/openstack/common/extensions/requests_test.go
index b0f655a..6550283 100644
--- a/openstack/common/extensions/requests_test.go
+++ b/openstack/common/extensions/requests_test.go
@@ -1,113 +1,38 @@
 package extensions
 
 import (
-	"fmt"
-	"net/http"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
 )
 
-const TokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{
-		Provider: &gophercloud.ProviderClient{
-			TokenID: TokenID,
-		},
-		Endpoint: th.Endpoint(),
-	}
-}
-
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "GET")
-		th.TestHeader(t, r, "X-Auth-Token", TokenID)
-
-		w.Header().Add("Content-Type", "application/json")
-
-		fmt.Fprintf(w, `
-{
-	"extensions": [
-		{
-			"updated": "2013-01-20T00:00:00-00:00",
-			"name": "Neutron Service Type Management",
-			"links": [],
-			"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
-			"alias": "service-type",
-			"description": "API for retrieving service providers for Neutron advanced services"
-		}
-	]
-}
-			`)
-	})
+	HandleListExtensionsSuccessfully(t)
 
 	count := 0
 
-	List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+	List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
 		count++
 		actual, err := ExtractExtensions(page)
-		if err != nil {
-			t.Errorf("Failed to extract extensions: %v", err)
-		}
-
-		expected := []Extension{
-			Extension{
-				Updated:     "2013-01-20T00:00:00-00:00",
-				Name:        "Neutron Service Type Management",
-				Links:       []interface{}{},
-				Namespace:   "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
-				Alias:       "service-type",
-				Description: "API for retrieving service providers for Neutron advanced services",
-			},
-		}
-
-		th.AssertDeepEquals(t, expected, actual)
+		th.AssertNoErr(t, err)
+		th.AssertDeepEquals(t, ExpectedExtensions, actual)
 
 		return true, nil
 	})
 
-	if count != 1 {
-		t.Errorf("Expected 1 page, got %d", count)
-	}
+	th.CheckEquals(t, 1, count)
 }
 
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
+	HandleGetExtensionSuccessfully(t)
 
-	th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "GET")
-		th.TestHeader(t, r, "X-Auth-Token", TokenID)
-
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-
-		fmt.Fprintf(w, `
-{
-	"extension": {
-		"updated": "2013-02-03T10:00:00-00:00",
-		"name": "agent",
-		"links": [],
-		"namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
-		"alias": "agent",
-		"description": "The agent management extension."
-	}
-}
-		`)
-
-		ext, err := Get(ServiceClient(), "agent").Extract()
-		th.AssertNoErr(t, err)
-
-		th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
-		th.AssertEquals(t, ext.Name, "agent")
-		th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
-		th.AssertEquals(t, ext.Alias, "agent")
-		th.AssertEquals(t, ext.Description, "The agent management extension.")
-	})
+	actual, err := Get(client.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, SingleExtension, actual)
 }
diff --git a/openstack/common/extensions/results.go b/openstack/common/extensions/results.go
index 9319018..4827072 100755
--- a/openstack/common/extensions/results.go
+++ b/openstack/common/extensions/results.go
@@ -1,8 +1,6 @@
 package extensions
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -25,21 +23,18 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding OpenStack extension: %v", err)
-	}
 
-	return res.Extension, nil
+	return res.Extension, err
 }
 
 // Extension is a struct that represents an OpenStack extension.
 type Extension struct {
-	Updated     string        `json:"updated"`
-	Name        string        `json:"name"`
-	Links       []interface{} `json:"links"`
-	Namespace   string        `json:"namespace"`
-	Alias       string        `json:"alias"`
-	Description string        `json:"description"`
+	Updated     string        `json:"updated" mapstructure:"updated"`
+	Name        string        `json:"name" mapstructure:"name"`
+	Links       []interface{} `json:"links" mapstructure:"links"`
+	Namespace   string        `json:"namespace" mapstructure:"namespace"`
+	Alias       string        `json:"alias" mapstructure:"alias"`
+	Description string        `json:"description" mapstructure:"description"`
 }
 
 // ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
@@ -65,9 +60,6 @@
 	}
 
 	err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Extensions, nil
+	return resp.Extensions, err
 }
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index 47eb172..7af11fc 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -6,41 +6,65 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
-// ListFilterOptions helps control the results returned by the List() function.
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToFlavorListQuery() (string, error)
+}
+
+// ListOpts helps control the results returned by the List() function.
 // For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
 // Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
-type ListFilterOptions struct {
+type ListOpts struct {
 
 	// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
-	ChangesSince string
+	ChangesSince string `q:"changes-since"`
 
 	// MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
-	MinDisk, MinRAM int
+	MinDisk int `q:"minDisk"`
+	MinRAM  int `q:"minRam"`
 
 	// Marker and Limit control paging.
 	// Marker instructs List where to start listing from.
-	Marker string
+	Marker string `q:"marker"`
 
 	// Limit instructs List to refrain from sending excessively large lists of flavors.
-	Limit int
+	Limit int `q:"limit"`
+}
+
+// ToFlavorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
 }
 
 // List instructs OpenStack to provide a list of flavors.
 // You may provide criteria by which List curtails its results for easier processing.
-// See ListFilterOptions for more details.
-func List(client *gophercloud.ServiceClient, lfo ListFilterOptions) pagination.Pager {
+// See ListOpts for more details.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToFlavorListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
 		return FlavorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	return pagination.NewPager(client, listURL(client, lfo), createPage)
+	return pagination.NewPager(client, url, createPage)
 }
 
 // Get instructs OpenStack to provide details on a single flavor, identified by its ID.
 // Use ExtractFlavor to convert its result into a Flavor.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var gr GetResult
-	gr.Err = perigee.Get(flavorURL(client, id), perigee.Options{
+	gr.Err = perigee.Get(getURL(client, id), perigee.Options{
 		Results:     &gr.Resp,
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 	})
diff --git a/openstack/compute/v2/flavors/requests_test.go b/openstack/compute/v2/flavors/requests_test.go
index e1b6b4f..bc9b82e 100644
--- a/openstack/compute/v2/flavors/requests_test.go
+++ b/openstack/compute/v2/flavors/requests_test.go
@@ -6,27 +6,20 @@
 	"reflect"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
-	"github.com/rackspace/gophercloud/testhelper"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 const tokenID = "blerb"
 
-func serviceClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{
-		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
-		Endpoint: testhelper.Endpoint(),
-	}
-}
-
 func TestListFlavors(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+	th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
 		r.ParseForm()
@@ -58,7 +51,7 @@
 							}
 						]
 					}
-				`, testhelper.Server.URL)
+				`, th.Server.URL)
 		case "2":
 			fmt.Fprintf(w, `{ "flavors": [] }`)
 		default:
@@ -66,9 +59,8 @@
 		}
 	})
 
-	client := serviceClient()
 	pages := 0
-	err := List(client, ListFilterOptions{}).EachPage(func(page pagination.Page) (bool, error) {
+	err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
 		pages++
 
 		actual, err := ExtractFlavors(page)
@@ -96,12 +88,12 @@
 }
 
 func TestGetFlavor(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+	th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
 		fmt.Fprintf(w, `
@@ -118,8 +110,7 @@
 		`)
 	})
 
-	client := serviceClient()
-	actual, err := Get(client, "12345").Extract()
+	actual, err := Get(fake.ServiceClient(), "12345").Extract()
 	if err != nil {
 		t.Fatalf("Unable to get flavor: %v", err)
 	}
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
index 68c8f58..1e274e3 100644
--- a/openstack/compute/v2/flavors/results.go
+++ b/openstack/compute/v2/flavors/results.go
@@ -78,12 +78,8 @@
 
 // NextPageURL uses the response's embedded link reference to navigate to the next page of results.
 func (p FlavorPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"flavors_links"`
+		Links []gophercloud.Link `mapstructure:"flavors_links"`
 	}
 
 	var r resp
@@ -92,17 +88,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
index 9e5b562..683c107 100644
--- a/openstack/compute/v2/flavors/urls.go
+++ b/openstack/compute/v2/flavors/urls.go
@@ -1,37 +1,13 @@
 package flavors
 
 import (
-	"fmt"
-	"net/url"
-	"strconv"
-
 	"github.com/rackspace/gophercloud"
 )
 
-func listURL(client *gophercloud.ServiceClient, lfo ListFilterOptions) string {
-	v := url.Values{}
-	if lfo.ChangesSince != "" {
-		v.Set("changes-since", lfo.ChangesSince)
-	}
-	if lfo.MinDisk != 0 {
-		v.Set("minDisk", strconv.Itoa(lfo.MinDisk))
-	}
-	if lfo.MinRAM != 0 {
-		v.Set("minRam", strconv.Itoa(lfo.MinRAM))
-	}
-	if lfo.Marker != "" {
-		v.Set("marker", lfo.Marker)
-	}
-	if lfo.Limit != 0 {
-		v.Set("limit", strconv.Itoa(lfo.Limit))
-	}
-	tail := ""
-	if len(v) > 0 {
-		tail = fmt.Sprintf("?%s", v.Encode())
-	}
-	return client.ServiceURL("flavors", "detail") + tail
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("flavors", id)
 }
 
-func flavorURL(client *gophercloud.ServiceClient, id string) string {
-	return client.ServiceURL("flavors", id)
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("flavors", "detail")
 }
diff --git a/openstack/compute/v2/flavors/urls_test.go b/openstack/compute/v2/flavors/urls_test.go
new file mode 100644
index 0000000..069da24
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls_test.go
@@ -0,0 +1,26 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "flavors/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "flavors/detail"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go
index a887cc6..603909c 100644
--- a/openstack/compute/v2/images/requests.go
+++ b/openstack/compute/v2/images/requests.go
@@ -1,25 +1,68 @@
 package images
 
 import (
-	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
 )
 
-// List enumerates the available images.
-func List(client *gophercloud.ServiceClient) pagination.Pager {
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToImageListQuery() (string, error)
+}
+
+// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
+type ListOpts struct {
+	// When the image last changed status (in date-time format).
+	ChangesSince string `q:"changes-since"`
+	// The number of Images to return.
+	Limit int `q:"limit"`
+	// UUID of the Image at which to set a marker.
+	Marker string `q:"marker"`
+	// The name of the Image.
+	Name string `q:"name:"`
+	// The name of the Server (in URL format).
+	Server string `q:"server"`
+	// The current status of the Image.
+	Status string `q:"status"`
+	// The value of the type of image (e.g. BASE, SERVER, ALL)
+	Type string `q:"type"`
+}
+
+// ToImageListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToImageListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListDetail enumerates the available images.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listDetailURL(client)
+	if opts != nil {
+		query, err := opts.ToImageListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
 		return ImagePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	return pagination.NewPager(client, listURL(client), createPage)
+	return pagination.NewPager(client, url, createPage)
 }
 
 // Get acquires additional detail about a specific image by ID.
 // Use ExtractImage() to intepret the result as an openstack Image.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var result GetResult
-	_, result.Err = perigee.Request("GET", imageURL(client, id), perigee.Options{
+	_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		Results:     &result.Resp,
 		OkCodes:     []int{200},
diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go
index 396c21f..9a05f97 100644
--- a/openstack/compute/v2/images/requests_test.go
+++ b/openstack/compute/v2/images/requests_test.go
@@ -1,32 +1,24 @@
 package images
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"reflect"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
-	"github.com/rackspace/gophercloud/testhelper"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
-const tokenID = "aaaaaa"
-
-func serviceClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{
-		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
-		Endpoint: testhelper.Endpoint(),
-	}
-}
-
 func TestListImages(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+	th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
 		r.ParseForm()
@@ -70,9 +62,9 @@
 		}
 	})
 
-	client := serviceClient()
 	pages := 0
-	err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+	options := &ListOpts{Limit: 2}
+	err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
 		pages++
 
 		actual, err := ExtractImages(page)
@@ -119,12 +111,12 @@
 }
 
 func TestGetImage(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+	th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
 		fmt.Fprintf(w, `
@@ -145,8 +137,7 @@
 		`)
 	})
 
-	client := serviceClient()
-	actual, err := Get(client, "12345678").Extract()
+	actual, err := Get(fake.ServiceClient(), "12345678").Extract()
 	if err != nil {
 		t.Fatalf("Unexpected error from Get: %v", err)
 	}
@@ -166,3 +157,19 @@
 		t.Errorf("Expected %#v, but got %#v", expected, actual)
 	}
 }
+
+func TestNextPageURL(t *testing.T) {
+	var page ImagePage
+	var body map[string]interface{}
+	bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`)
+	err := json.Unmarshal(bodyString, &body)
+	if err != nil {
+		t.Fatalf("Error unmarshaling data into page body: %v", err)
+	}
+	page.Body = body
+
+	expected := "http://192.154.23.87/12345/images/image4"
+	actual, err := page.NextPageURL()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
index f93c90c..3c22eeb 100644
--- a/openstack/compute/v2/images/results.go
+++ b/openstack/compute/v2/images/results.go
@@ -65,12 +65,8 @@
 
 // NextPageURL uses the response's embedded link reference to navigate to the next page of results.
 func (page ImagePage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"images_links"`
+		Links []gophercloud.Link `mapstructure:"images_links"`
 	}
 
 	var r resp
@@ -79,17 +75,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // ExtractImages converts a page of List results into a slice of usable Image structs.
diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go
index 4ae2269..9b3c86d 100644
--- a/openstack/compute/v2/images/urls.go
+++ b/openstack/compute/v2/images/urls.go
@@ -2,10 +2,10 @@
 
 import "github.com/rackspace/gophercloud"
 
-func listURL(client *gophercloud.ServiceClient) string {
+func listDetailURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("images", "detail")
 }
 
-func imageURL(client *gophercloud.ServiceClient, id string) string {
+func getURL(client *gophercloud.ServiceClient, id string) string {
 	return client.ServiceURL("images", id)
 }
diff --git a/openstack/compute/v2/images/urls_test.go b/openstack/compute/v2/images/urls_test.go
new file mode 100644
index 0000000..b1ab3d6
--- /dev/null
+++ b/openstack/compute/v2/images/urls_test.go
@@ -0,0 +1,26 @@
+package images
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "images/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+	actual := listDetailURL(endpointClient())
+	expected := endpoint + "images/detail"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index d22cb46..386e0f6 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -15,7 +15,7 @@
 		return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	return pagination.NewPager(client, detailURL(client), createPage)
+	return pagination.NewPager(client, listDetailURL(client), createPage)
 }
 
 // CreateOptsBuilder describes struct types that can be accepted by the Create call.
@@ -140,7 +140,7 @@
 
 // Delete requests that a server previously provisioned be removed from your account.
 func Delete(client *gophercloud.ServiceClient, id string) error {
-	_, err := perigee.Request("DELETE", serverURL(client, id), perigee.Options{
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
@@ -150,15 +150,15 @@
 // Get requests details on a single server, by ID.
 func Get(client *gophercloud.ServiceClient, id string) GetResult {
 	var result GetResult
-	_, result.Err = perigee.Request("GET", serverURL(client, id), perigee.Options{
+	_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
 		Results:     &result.Resp,
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 	})
 	return result
 }
 
-// UpdateOptsLike allows extentions to add additional attributes to the Update request.
-type UpdateOptsLike interface {
+// UpdateOptsBuilder allows extentions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
 	ToServerUpdateMap() map[string]interface{}
 }
 
@@ -192,9 +192,9 @@
 }
 
 // Update requests that various attributes of the indicated server be changed.
-func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsLike) UpdateResult {
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
 	var result UpdateResult
-	_, result.Err = perigee.Request("PUT", serverURL(client, id), perigee.Options{
+	_, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
 		Results:     &result.Resp,
 		ReqBody:     opts.ToServerUpdateMap(),
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index dd2e651..d284ed8 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -116,12 +116,8 @@
 
 // NextPageURL uses the response's embedded link reference to navigate to the next page of results.
 func (page ServerPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"servers_links"`
+		Links []gophercloud.Link `mapstructure:"servers_links"`
 	}
 
 	var r resp
@@ -130,17 +126,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 52be73e..57587ab 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -2,18 +2,30 @@
 
 import "github.com/rackspace/gophercloud"
 
-func listURL(client *gophercloud.ServiceClient) string {
+func createURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("servers")
 }
 
-func detailURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
+	return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("servers", "detail")
 }
 
-func serverURL(client *gophercloud.ServiceClient, id string) string {
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
 	return client.ServiceURL("servers", id)
 }
 
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return deleteURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+	return deleteURL(client, id)
+}
+
 func actionURL(client *gophercloud.ServiceClient, id string) string {
 	return client.ServiceURL("servers", id, "action")
 }
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
new file mode 100644
index 0000000..cc895c9
--- /dev/null
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -0,0 +1,56 @@
+package servers
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "servers"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "servers"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+	actual := listDetailURL(endpointClient())
+	expected := endpoint + "servers/detail"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestActionURL(t *testing.T) {
+	actual := actionURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo/action"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
new file mode 100644
index 0000000..5a311e4
--- /dev/null
+++ b/openstack/endpoint_location.go
@@ -0,0 +1,124 @@
+package openstack
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
+	services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
+// during the v2 identity service. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
+	// Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
+	var endpoints = make([]tokens2.Endpoint, 0, 1)
+	for _, entry := range catalog.Entries {
+		if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
+			for _, endpoint := range entry.Endpoints {
+				if opts.Region == "" || endpoint.Region == opts.Region {
+					endpoints = append(endpoints, endpoint)
+				}
+			}
+		}
+	}
+
+	// Report an error if the options were ambiguous.
+	if len(endpoints) > 1 {
+		return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+	}
+
+	// Extract the appropriate URL from the matching Endpoint.
+	for _, endpoint := range endpoints {
+		switch opts.Availability {
+		case gophercloud.AvailabilityPublic:
+			return gophercloud.NormalizeURL(endpoint.PublicURL), nil
+		case gophercloud.AvailabilityInternal:
+			return gophercloud.NormalizeURL(endpoint.InternalURL), nil
+		case gophercloud.AvailabilityAdmin:
+			return gophercloud.NormalizeURL(endpoint.AdminURL), nil
+		default:
+			return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
+		}
+	}
+
+	// Report an error if there were no matching endpoints.
+	return "", gophercloud.ErrEndpointNotFound
+}
+
+// V3EndpointURL discovers the endpoint URL for a specific service using multiple calls against
+// an identity v3 service endpoint. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V3EndpointURL(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
+	// Discover the service we're interested in.
+	var services = make([]services3.Service, 0, 1)
+	servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
+	err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
+		part, err := services3.ExtractServices(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, service := range part {
+			if service.Name == opts.Name {
+				services = append(services, service)
+			}
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		return "", err
+	}
+
+	if len(services) == 0 {
+		return "", gophercloud.ErrServiceNotFound
+	}
+	if len(services) > 1 {
+		return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
+	}
+	service := services[0]
+
+	// Enumerate the endpoints available for this service.
+	var endpoints []endpoints3.Endpoint
+	endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
+		Availability: opts.Availability,
+		ServiceID:    service.ID,
+	})
+	err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
+		part, err := endpoints3.ExtractEndpoints(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, endpoint := range part {
+			if opts.Region == "" || endpoint.Region == opts.Region {
+				endpoints = append(endpoints, endpoint)
+			}
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		return "", err
+	}
+
+	if len(endpoints) == 0 {
+		return "", gophercloud.ErrEndpointNotFound
+	}
+	if len(endpoints) > 1 {
+		return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+	}
+	endpoint := endpoints[0]
+
+	return gophercloud.NormalizeURL(endpoint.URL), nil
+}
diff --git a/openstack/endpoint_location_test.go b/openstack/endpoint_location_test.go
new file mode 100644
index 0000000..4e0569a
--- /dev/null
+++ b/openstack/endpoint_location_test.go
@@ -0,0 +1,225 @@
+package openstack
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// Service catalog fixtures take too much vertical space!
+var catalog2 = tokens2.ServiceCatalog{
+	Entries: []tokens2.CatalogEntry{
+		tokens2.CatalogEntry{
+			Type: "same",
+			Name: "same",
+			Endpoints: []tokens2.Endpoint{
+				tokens2.Endpoint{
+					Region:      "same",
+					PublicURL:   "https://public.correct.com/",
+					InternalURL: "https://internal.correct.com/",
+					AdminURL:    "https://admin.correct.com/",
+				},
+				tokens2.Endpoint{
+					Region:    "different",
+					PublicURL: "https://badregion.com/",
+				},
+			},
+		},
+		tokens2.CatalogEntry{
+			Type: "same",
+			Name: "different",
+			Endpoints: []tokens2.Endpoint{
+				tokens2.Endpoint{
+					Region:    "same",
+					PublicURL: "https://badname.com/",
+				},
+				tokens2.Endpoint{
+					Region:    "different",
+					PublicURL: "https://badname.com/+badregion",
+				},
+			},
+		},
+		tokens2.CatalogEntry{
+			Type: "different",
+			Name: "different",
+			Endpoints: []tokens2.Endpoint{
+				tokens2.Endpoint{
+					Region:    "same",
+					PublicURL: "https://badtype.com/+badname",
+				},
+				tokens2.Endpoint{
+					Region:    "different",
+					PublicURL: "https://badtype.com/+badregion+badname",
+				},
+			},
+		},
+	},
+}
+
+func TestV2EndpointExact(t *testing.T) {
+	expectedURLs := map[gophercloud.Availability]string{
+		gophercloud.AvailabilityPublic:   "https://public.correct.com/",
+		gophercloud.AvailabilityAdmin:    "https://admin.correct.com/",
+		gophercloud.AvailabilityInternal: "https://internal.correct.com/",
+	}
+
+	for availability, expected := range expectedURLs {
+		actual, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+			Type:         "same",
+			Name:         "same",
+			Region:       "same",
+			Availability: availability,
+		})
+		th.AssertNoErr(t, err)
+		th.CheckEquals(t, expected, actual)
+	}
+}
+
+func TestV2EndpointNone(t *testing.T) {
+	_, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "nope",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err)
+}
+
+func TestV2EndpointMultiple(t *testing.T) {
+	_, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "same",
+		Region:       "same",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") {
+		t.Errorf("Received unexpected error: %v", err)
+	}
+}
+
+func TestV2EndpointBadAvailability(t *testing.T) {
+	_, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "same",
+		Name:         "same",
+		Region:       "same",
+		Availability: "wat",
+	})
+	th.CheckEquals(t, err.Error(), "Unexpected availability in endpoint query: wat")
+}
+
+func setupV3Responses(t *testing.T) {
+	// Mock the service query.
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"links": {
+					"next": null,
+					"previous": null
+				},
+				"services": [
+					{
+						"description": "Correct",
+						"id": "1234",
+						"name": "same",
+						"type": "same"
+					},
+					{
+						"description": "Bad Name",
+						"id": "9876",
+						"name": "different",
+						"type": "same"
+					}
+				]
+			}
+		`)
+	})
+
+	// Mock the endpoint query.
+	th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestFormValues(t, r, map[string]string{
+			"service_id": "1234",
+			"interface":  "public",
+		})
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"endpoints": [
+					{
+						"id": "12",
+						"interface": "public",
+						"name": "the-right-one",
+						"region": "same",
+						"service_id": "1234",
+						"url": "https://correct:9000/"
+					},
+					{
+						"id": "14",
+						"interface": "public",
+						"name": "bad-region",
+						"region": "different",
+						"service_id": "1234",
+						"url": "https://bad-region:9001/"
+					}
+				],
+				"links": {
+					"next": null,
+					"previous": null
+				}
+			}
+    `)
+	})
+}
+
+func TestV3EndpointExact(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	setupV3Responses(t)
+
+	actual, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+		Type:         "same",
+		Name:         "same",
+		Region:       "same",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, actual, "https://correct:9000/")
+}
+
+func TestV3EndpointNoService(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+      {
+        "links": {
+          "next": null,
+          "previous": null
+        },
+        "services": []
+      }
+    `)
+	})
+
+	_, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+		Type:         "nope",
+		Name:         "same",
+		Region:       "same",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.CheckEquals(t, gophercloud.ErrServiceNotFound, err)
+}
diff --git a/openstack/identity/v2/extensions/delegate.go b/openstack/identity/v2/extensions/delegate.go
index 1992a2c..cee275f 100644
--- a/openstack/identity/v2/extensions/delegate.go
+++ b/openstack/identity/v2/extensions/delegate.go
@@ -7,16 +7,6 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
-// Extension is a single OpenStack extension.
-type Extension struct {
-	common.Extension
-}
-
-// GetResult wraps a GetResult from common.
-type GetResult struct {
-	common.GetResult
-}
-
 // ExtensionPage is a single page of Extension results.
 type ExtensionPage struct {
 	common.ExtensionPage
@@ -33,53 +23,30 @@
 
 // ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
 // elements into a slice of Extension structs.
-func ExtractExtensions(page pagination.Page) ([]Extension, error) {
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
 	// Identity v2 adds an intermediate "values" object.
 
-	type extension struct {
-		Updated     string        `mapstructure:"updated"`
-		Name        string        `mapstructure:"name"`
-		Namespace   string        `mapstructure:"namespace"`
-		Alias       string        `mapstructure:"alias"`
-		Description string        `mapstructure:"description"`
-		Links       []interface{} `mapstructure:"links"`
-	}
-
 	var resp struct {
 		Extensions struct {
-			Values []extension `mapstructure:"values"`
+			Values []common.Extension `mapstructure:"values"`
 		} `mapstructure:"extensions"`
 	}
 
 	err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
-
-	exts := make([]Extension, len(resp.Extensions.Values))
-	for i, original := range resp.Extensions.Values {
-		exts[i] = Extension{common.Extension{
-			Updated:     original.Updated,
-			Name:        original.Name,
-			Namespace:   original.Namespace,
-			Alias:       original.Alias,
-			Description: original.Description,
-			Links:       original.Links,
-		}}
-	}
-
-	return exts, err
+	return resp.Extensions.Values, err
 }
 
 // Get retrieves information for a specific extension using its alias.
-func Get(c *gophercloud.ServiceClient, alias string) GetResult {
-	return GetResult{common.Get(c, alias)}
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+	return common.Get(c, alias)
 }
 
 // List returns a Pager which allows you to iterate over the full collection of extensions.
 // It does not accept query parameters.
 func List(c *gophercloud.ServiceClient) pagination.Pager {
-	return pagination.NewPager(c, common.ListExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
-		return ExtensionPage{common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}}
+	return common.List(c).WithPageCreator(func(r pagination.LastHTTPResponse) pagination.Page {
+		return ExtensionPage{
+			ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
+		}
 	})
 }
diff --git a/openstack/identity/v2/extensions/delegate_test.go b/openstack/identity/v2/extensions/delegate_test.go
index 90eb21b..504118a 100644
--- a/openstack/identity/v2/extensions/delegate_test.go
+++ b/openstack/identity/v2/extensions/delegate_test.go
@@ -1,76 +1,25 @@
 package extensions
 
 import (
-	"fmt"
-	"net/http"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
 	common "github.com/rackspace/gophercloud/openstack/common/extensions"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
 )
 
-const TokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{
-		Provider: &gophercloud.ProviderClient{
-			TokenID: TokenID,
-		},
-		Endpoint: th.Endpoint(),
-	}
-}
-
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "GET")
-		th.TestHeader(t, r, "X-Auth-Token", TokenID)
-
-		w.Header().Add("Content-Type", "application/json")
-
-		fmt.Fprintf(w, `
-{
-	"extensions": {
-		"values": [
-			{
-				"updated": "2013-01-20T00:00:00-00:00",
-				"name": "Neutron Service Type Management",
-				"links": [],
-				"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
-				"alias": "service-type",
-				"description": "API for retrieving service providers for Neutron advanced services"
-			}
-		]
-	}
-}
-		`)
-	})
+	HandleListExtensionsSuccessfully(t)
 
 	count := 0
-
-	err := List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
 		count++
 		actual, err := ExtractExtensions(page)
 		th.AssertNoErr(t, err)
-
-		expected := []Extension{
-			Extension{
-				common.Extension{
-					Updated:     "2013-01-20T00:00:00-00:00",
-					Name:        "Neutron Service Type Management",
-					Links:       []interface{}{},
-					Namespace:   "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
-					Alias:       "service-type",
-					Description: "API for retrieving service providers for Neutron advanced services",
-				},
-			},
-		}
-
-		th.AssertDeepEquals(t, expected, actual)
+		th.CheckDeepEquals(t, common.ExpectedExtensions, actual)
 
 		return true, nil
 	})
@@ -81,34 +30,9 @@
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
+	common.HandleGetExtensionSuccessfully(t)
 
-	th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "GET")
-		th.TestHeader(t, r, "X-Auth-Token", TokenID)
-
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-
-		fmt.Fprintf(w, `
-{
-    "extension": {
-        "updated": "2013-02-03T10:00:00-00:00",
-        "name": "agent",
-        "links": [],
-        "namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
-        "alias": "agent",
-        "description": "The agent management extension."
-    }
-}
-    `)
-
-		ext, err := Get(ServiceClient(), "agent").Extract()
-		th.AssertNoErr(t, err)
-
-		th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
-		th.AssertEquals(t, ext.Name, "agent")
-		th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
-		th.AssertEquals(t, ext.Alias, "agent")
-		th.AssertEquals(t, ext.Description, "The agent management extension.")
-	})
+	actual, err := Get(client.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, common.SingleExtension, actual)
 }
diff --git a/openstack/identity/v2/extensions/fixtures.go b/openstack/identity/v2/extensions/fixtures.go
new file mode 100644
index 0000000..96cb7d2
--- /dev/null
+++ b/openstack/identity/v2/extensions/fixtures.go
@@ -0,0 +1,60 @@
+// +build fixtures
+
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single Extension result. It differs from the delegated implementation
+// by the introduction of an intermediate "values" member.
+const ListOutput = `
+{
+	"extensions": {
+		"values": [
+			{
+				"updated": "2013-01-20T00:00:00-00:00",
+				"name": "Neutron Service Type Management",
+				"links": [],
+				"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+				"alias": "service-type",
+				"description": "API for retrieving service providers for Neutron advanced services"
+			}
+		]
+	}
+}
+`
+
+// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
+// call.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/extensions", 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, `
+{
+  "extensions": {
+    "values": [
+      {
+        "updated": "2013-01-20T00:00:00-00:00",
+        "name": "Neutron Service Type Management",
+        "links": [],
+        "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+        "alias": "service-type",
+        "description": "API for retrieving service providers for Neutron advanced services"
+      }
+    ]
+  }
+}
+    `)
+	})
+
+}
diff --git a/openstack/identity/v2/tenants/fixtures.go b/openstack/identity/v2/tenants/fixtures.go
new file mode 100644
index 0000000..7f044ac
--- /dev/null
+++ b/openstack/identity/v2/tenants/fixtures.go
@@ -0,0 +1,65 @@
+// +build fixtures
+
+package tenants
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Tenant results.
+const ListOutput = `
+{
+	"tenants": [
+		{
+			"id": "1234",
+			"name": "Red Team",
+			"description": "The team that is red",
+			"enabled": true
+		},
+		{
+			"id": "9876",
+			"name": "Blue Team",
+			"description": "The team that is blue",
+			"enabled": false
+		}
+	]
+}
+`
+
+// RedTeam is a Tenant fixture.
+var RedTeam = Tenant{
+	ID:          "1234",
+	Name:        "Red Team",
+	Description: "The team that is red",
+	Enabled:     true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = Tenant{
+	ID:          "9876",
+	Name:        "Blue Team",
+	Description: "The team that is blue",
+	Enabled:     false,
+}
+
+// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
+var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
+
+// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
+// responds with a list of two tenants.
+func HandleListTenantsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, ListOutput)
+	})
+}
diff --git a/openstack/identity/v2/tenants/requests_test.go b/openstack/identity/v2/tenants/requests_test.go
index 685bf96..e8f172d 100644
--- a/openstack/identity/v2/tenants/requests_test.go
+++ b/openstack/identity/v2/tenants/requests_test.go
@@ -1,76 +1,26 @@
 package tenants
 
 import (
-	"fmt"
-	"net/http"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
 )
 
-const tokenID = "1234123412341234"
-
 func TestListTenants(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "GET")
-		th.TestHeader(t, r, "Accept", "application/json")
-		th.TestHeader(t, r, "X-Auth-Token", tokenID)
-
-		w.Header().Set("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-{
-  "tenants": [
-    {
-      "id": "1234",
-      "name": "Red Team",
-      "description": "The team that is red",
-      "enabled": true
-    },
-    {
-      "id": "9876",
-      "name": "Blue Team",
-      "description": "The team that is blue",
-      "enabled": false
-    }
-  ]
-}
-    `)
-	})
-
-	client := &gophercloud.ServiceClient{
-		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
-		Endpoint: th.Endpoint(),
-	}
+	HandleListTenantsSuccessfully(t)
 
 	count := 0
-	err := List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+	err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
 		count++
 
 		actual, err := ExtractTenants(page)
 		th.AssertNoErr(t, err)
 
-		expected := []Tenant{
-			Tenant{
-				ID:          "1234",
-				Name:        "Red Team",
-				Description: "The team that is red",
-				Enabled:     true,
-			},
-			Tenant{
-				ID:          "9876",
-				Name:        "Blue Team",
-				Description: "The team that is blue",
-				Enabled:     false,
-			},
-		}
-
-		th.CheckDeepEquals(t, expected, actual)
+		th.CheckDeepEquals(t, ExpectedTenantSlice, actual)
 
 		return true, nil
 	})
diff --git a/openstack/identity/v2/tenants/results.go b/openstack/identity/v2/tenants/results.go
index e4e3f47..c1220c3 100644
--- a/openstack/identity/v2/tenants/results.go
+++ b/openstack/identity/v2/tenants/results.go
@@ -2,6 +2,7 @@
 
 import (
 	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 )
 
@@ -36,12 +37,8 @@
 
 // NextPageURL extracts the "next" link from the tenants_links section of the result.
 func (page TenantPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"tenants_links"`
+		Links []gophercloud.Link `mapstructure:"tenants_links"`
 	}
 
 	var r resp
@@ -50,17 +47,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // ExtractTenants returns a slice of Tenants contained in a single page of results.
diff --git a/openstack/identity/v2/tokens/errors.go b/openstack/identity/v2/tokens/errors.go
index 244db1b..3a9172e 100644
--- a/openstack/identity/v2/tokens/errors.go
+++ b/openstack/identity/v2/tokens/errors.go
@@ -9,6 +9,9 @@
 	// ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
 	ErrUserIDProvided = unacceptedAttributeErr("UserID")
 
+	// ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
+	ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
 	// ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
 	ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
 
@@ -18,8 +21,8 @@
 	// ErrUsernameRequired is returned if you attempt ot authenticate without a Username.
 	ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
 
-	// ErrPasswordOrAPIKey is returned if you provide both a password and an API key.
-	ErrPasswordOrAPIKey = errors.New("Please supply exactly one of Password or APIKey in your AuthOptions.")
+	// ErrPasswordRequired is returned if you don't provide a password.
+	ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
 )
 
 func unacceptedAttributeErr(attribute string) error {
diff --git a/openstack/identity/v2/tokens/fixtures.go b/openstack/identity/v2/tokens/fixtures.go
new file mode 100644
index 0000000..1cb0d05
--- /dev/null
+++ b/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+
+package tokens
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &Token{
+	ID:        "aaaabbbbccccdddd",
+	ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
+	Tenant: tenants.Tenant{
+		ID:          "fc394f2ab2df4114bde39905f800dc57",
+		Name:        "test",
+		Description: "There are many tenants. This one is yours.",
+		Enabled:     true,
+	},
+}
+
+// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
+var ExpectedServiceCatalog = &ServiceCatalog{
+	Entries: []CatalogEntry{
+		CatalogEntry{
+			Name: "inscrutablewalrus",
+			Type: "something",
+			Endpoints: []Endpoint{
+				Endpoint{
+					PublicURL: "http://something0:1234/v2/",
+					Region:    "region0",
+				},
+				Endpoint{
+					PublicURL: "http://something1:1234/v2/",
+					Region:    "region1",
+				},
+			},
+		},
+		CatalogEntry{
+			Name: "arbitrarypenguin",
+			Type: "else",
+			Endpoints: []Endpoint{
+				Endpoint{
+					PublicURL: "http://else0:4321/v3/",
+					Region:    "region0",
+				},
+			},
+		},
+	},
+}
+
+// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
+const TokenCreationResponse = `
+{
+	"access": {
+		"token": {
+			"issued_at": "2014-01-30T15:30:58.000000Z",
+			"expires": "2014-01-31T15:30:58Z",
+			"id": "aaaabbbbccccdddd",
+			"tenant": {
+				"description": "There are many tenants. This one is yours.",
+				"enabled": true,
+				"id": "fc394f2ab2df4114bde39905f800dc57",
+				"name": "test"
+			}
+		},
+		"serviceCatalog": [
+			{
+				"endpoints": [
+					{
+						"publicURL": "http://something0:1234/v2/",
+						"region": "region0"
+					},
+					{
+						"publicURL": "http://something1:1234/v2/",
+						"region": "region1"
+					}
+				],
+				"type": "something",
+				"name": "inscrutablewalrus"
+			},
+			{
+				"endpoints": [
+					{
+						"publicURL": "http://else0:4321/v3/",
+						"region": "region0"
+					}
+				],
+				"type": "else",
+				"name": "arbitrarypenguin"
+			}
+		]
+	}
+}
+`
+
+// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenPost(t *testing.T, requestJSON string) {
+	th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		if requestJSON != "" {
+			th.TestJSONRequest(t, r, requestJSON)
+		}
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, TokenCreationResponse)
+	})
+}
+
+// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
+// service catalog.
+func IsSuccessful(t *testing.T, result CreateResult) {
+	token, err := result.ExtractToken()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedToken, token)
+
+	serviceCatalog, err := result.ExtractServiceCatalog()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
index 5bd5a70..c25a72b 100644
--- a/openstack/identity/v2/tokens/requests.go
+++ b/openstack/identity/v2/tokens/requests.go
@@ -5,73 +5,80 @@
 	"github.com/rackspace/gophercloud"
 )
 
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+
+	// ToTokenCreateMap assembles the Create request body, returning an error if parameters are
+	// missing or inconsistent.
+	ToTokenCreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptions struct {
+	gophercloud.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+	return AuthOptions{AuthOptions: original}
+}
+
+// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
+// request.
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+	// Error out if an unsupported auth option is present.
+	if auth.UserID != "" {
+		return nil, ErrUserIDProvided
+	}
+	if auth.APIKey != "" {
+		return nil, ErrAPIKeyProvided
+	}
+	if auth.DomainID != "" {
+		return nil, ErrDomainIDProvided
+	}
+	if auth.DomainName != "" {
+		return nil, ErrDomainNameProvided
+	}
+
+	// Username and Password are always required.
+	if auth.Username == "" {
+		return nil, ErrUsernameRequired
+	}
+	if auth.Password == "" {
+		return nil, ErrPasswordRequired
+	}
+
+	// Populate the request map.
+	authMap := make(map[string]interface{})
+
+	authMap["passwordCredentials"] = map[string]interface{}{
+		"username": auth.Username,
+		"password": auth.Password,
+	}
+
+	if auth.TenantID != "" {
+		authMap["tenantId"] = auth.TenantID
+	}
+	if auth.TenantName != "" {
+		authMap["tenantName"] = auth.TenantName
+	}
+
+	return map[string]interface{}{"auth": authMap}, nil
+}
+
 // Create authenticates to the identity service and attempts to acquire a Token.
 // If successful, the CreateResult
 // Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
 // which abstracts all of the gory details about navigating service catalogs and such.
-func Create(client *gophercloud.ServiceClient, auth gophercloud.AuthOptions) CreateResult {
-	type passwordCredentials struct {
-		Username string `json:"username"`
-		Password string `json:"password"`
+func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
+	request, err := auth.ToTokenCreateMap()
+	if err != nil {
+		return CreateResult{gophercloud.CommonResult{Err: err}}
 	}
 
-	type apiKeyCredentials struct {
-		Username string `json:"username"`
-		APIKey   string `json:"apiKey"`
-	}
-
-	var request struct {
-		Auth struct {
-			PasswordCredentials *passwordCredentials `json:"passwordCredentials,omitempty"`
-			APIKeyCredentials   *apiKeyCredentials   `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
-			TenantID            string               `json:"tenantId,omitempty"`
-			TenantName          string               `json:"tenantName,omitempty"`
-		} `json:"auth"`
-	}
-
-	// Error out if an unsupported auth option is present.
-	if auth.UserID != "" {
-		return createErr(ErrUserIDProvided)
-	}
-	if auth.DomainID != "" {
-		return createErr(ErrDomainIDProvided)
-	}
-	if auth.DomainName != "" {
-		return createErr(ErrDomainNameProvided)
-	}
-
-	// Username is always required.
-	if auth.Username == "" {
-		return createErr(ErrUsernameRequired)
-	}
-
-	// Populate either PasswordCredentials or APIKeyCredentials
-	if auth.Password != "" {
-		if auth.APIKey != "" {
-			return createErr(ErrPasswordOrAPIKey)
-		}
-
-		// Username + Password
-		request.Auth.PasswordCredentials = &passwordCredentials{
-			Username: auth.Username,
-			Password: auth.Password,
-		}
-	} else if auth.APIKey != "" {
-		// API key authentication.
-		request.Auth.APIKeyCredentials = &apiKeyCredentials{
-			Username: auth.Username,
-			APIKey:   auth.APIKey,
-		}
-	} else {
-		return createErr(ErrPasswordOrAPIKey)
-	}
-
-	// Populate the TenantName or TenantID, if provided.
-	request.Auth.TenantID = auth.TenantID
-	request.Auth.TenantName = auth.TenantName
-
 	var result CreateResult
-	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+	_, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{
 		ReqBody: &request,
 		Results: &result.Resp,
 		OkCodes: []int{200, 203},
diff --git a/openstack/identity/v2/tokens/requests_test.go b/openstack/identity/v2/tokens/requests_test.go
index 0e6269f..2f02825 100644
--- a/openstack/identity/v2/tokens/requests_test.go
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -1,153 +1,37 @@
 package tokens
 
 import (
-	"fmt"
-	"net/http"
 	"testing"
-	"time"
 
 	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
 	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
 )
 
-var expectedToken = &Token{
-	ID:        "aaaabbbbccccdddd",
-	ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
-	Tenant: tenants.Tenant{
-		ID:          "fc394f2ab2df4114bde39905f800dc57",
-		Name:        "test",
-		Description: "There are many tenants. This one is yours.",
-		Enabled:     true,
-	},
-}
-
-var expectedServiceCatalog = &ServiceCatalog{
-	Entries: []CatalogEntry{
-		CatalogEntry{
-			Name: "inscrutablewalrus",
-			Type: "something",
-			Endpoints: []Endpoint{
-				Endpoint{
-					PublicURL: "http://something0:1234/v2/",
-					Region:    "region0",
-				},
-				Endpoint{
-					PublicURL: "http://something1:1234/v2/",
-					Region:    "region1",
-				},
-			},
-		},
-		CatalogEntry{
-			Name: "arbitrarypenguin",
-			Type: "else",
-			Endpoints: []Endpoint{
-				Endpoint{
-					PublicURL: "http://else0:4321/v3/",
-					Region:    "region0",
-				},
-			},
-		},
-	},
-}
-
 func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
+	HandleTokenPost(t, requestJSON)
 
-	client := gophercloud.ServiceClient{Endpoint: th.Endpoint()}
-
-	th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "POST")
-		th.TestHeader(t, r, "Content-Type", "application/json")
-		th.TestHeader(t, r, "Accept", "application/json")
-		th.TestJSONRequest(t, r, requestJSON)
-
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-{
-  "access": {
-    "token": {
-      "issued_at": "2014-01-30T15:30:58.000000Z",
-      "expires": "2014-01-31T15:30:58Z",
-      "id": "aaaabbbbccccdddd",
-      "tenant": {
-        "description": "There are many tenants. This one is yours.",
-        "enabled": true,
-        "id": "fc394f2ab2df4114bde39905f800dc57",
-        "name": "test"
-      }
-    },
-    "serviceCatalog": [
-      {
-        "endpoints": [
-          {
-            "publicURL": "http://something0:1234/v2/",
-            "region": "region0"
-          },
-          {
-            "publicURL": "http://something1:1234/v2/",
-            "region": "region1"
-          }
-        ],
-        "type": "something",
-        "name": "inscrutablewalrus"
-      },
-      {
-        "endpoints": [
-          {
-            "publicURL": "http://else0:4321/v3/",
-            "region": "region0"
-          }
-        ],
-        "type": "else",
-        "name": "arbitrarypenguin"
-      }
-    ]
-  }
-}
-    `)
-	})
-
-	return Create(&client, options)
+	return Create(client.ServiceClient(), AuthOptions{options})
 }
 
 func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
+	HandleTokenPost(t, "")
 
-	client := gophercloud.ServiceClient{Endpoint: th.Endpoint()}
-
-	th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
-		th.TestMethod(t, r, "POST")
-		th.TestHeader(t, r, "Content-Type", "application/json")
-		th.TestHeader(t, r, "Accept", "application/json")
-
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `{}`)
-	})
-
-	actualErr := Create(&client, options).Err
+	actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
 	th.CheckEquals(t, expectedErr, actualErr)
 }
 
-func isSuccessful(t *testing.T, result CreateResult) {
-	token, err := result.ExtractToken()
-	th.AssertNoErr(t, err)
-	th.CheckDeepEquals(t, expectedToken, token)
-
-	serviceCatalog, err := result.ExtractServiceCatalog()
-	th.AssertNoErr(t, err)
-	th.CheckDeepEquals(t, expectedServiceCatalog, serviceCatalog)
-}
-
 func TestCreateWithPassword(t *testing.T) {
 	options := gophercloud.AuthOptions{
 		Username: "me",
 		Password: "swordfish",
 	}
 
-	isSuccessful(t, tokenPost(t, options, `
+	IsSuccessful(t, tokenPost(t, options, `
     {
       "auth": {
         "passwordCredentials": {
@@ -159,24 +43,6 @@
   `))
 }
 
-func TestCreateTokenWithAPIKey(t *testing.T) {
-	options := gophercloud.AuthOptions{
-		Username: "me",
-		APIKey:   "1234567890abcdef",
-	}
-
-	isSuccessful(t, tokenPost(t, options, `
-    {
-      "auth": {
-        "RAX-KSKEY:apiKeyCredentials": {
-          "username": "me",
-          "apiKey": "1234567890abcdef"
-        }
-      }
-    }
-  `))
-}
-
 func TestCreateTokenWithTenantID(t *testing.T) {
 	options := gophercloud.AuthOptions{
 		Username: "me",
@@ -184,7 +50,7 @@
 		TenantID: "fc394f2ab2df4114bde39905f800dc57",
 	}
 
-	isSuccessful(t, tokenPost(t, options, `
+	IsSuccessful(t, tokenPost(t, options, `
     {
       "auth": {
         "tenantId": "fc394f2ab2df4114bde39905f800dc57",
@@ -204,7 +70,7 @@
 		TenantName: "demo",
 	}
 
-	isSuccessful(t, tokenPost(t, options, `
+	IsSuccessful(t, tokenPost(t, options, `
     {
       "auth": {
         "tenantName": "demo",
@@ -223,15 +89,27 @@
 		UserID:   "1234",
 		Password: "thing",
 	}
+
 	tokenPostErr(t, options, ErrUserIDProvided)
 }
 
+func TestProhibitAPIKey(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		Password: "thing",
+		APIKey:   "123412341234",
+	}
+
+	tokenPostErr(t, options, ErrAPIKeyProvided)
+}
+
 func TestProhibitDomainID(t *testing.T) {
 	options := gophercloud.AuthOptions{
 		Username: "me",
 		Password: "thing",
 		DomainID: "1234",
 	}
+
 	tokenPostErr(t, options, ErrDomainIDProvided)
 }
 
@@ -241,6 +119,7 @@
 		Password:   "thing",
 		DomainName: "wat",
 	}
+
 	tokenPostErr(t, options, ErrDomainNameProvided)
 }
 
@@ -248,21 +127,14 @@
 	options := gophercloud.AuthOptions{
 		Password: "thing",
 	}
+
 	tokenPostErr(t, options, ErrUsernameRequired)
 }
 
-func TestProhibitBothPasswordAndAPIKey(t *testing.T) {
+func TestRequirePassword(t *testing.T) {
 	options := gophercloud.AuthOptions{
 		Username: "me",
-		Password: "thing",
-		APIKey:   "123412341234",
 	}
-	tokenPostErr(t, options, ErrPasswordOrAPIKey)
-}
 
-func TestRequirePasswordOrAPIKey(t *testing.T) {
-	options := gophercloud.AuthOptions{
-		Username: "me",
-	}
-	tokenPostErr(t, options, ErrPasswordOrAPIKey)
+	tokenPostErr(t, options, ErrPasswordRequired)
 }
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
index 86d19f2..cd4c696 100644
--- a/openstack/identity/v2/tokens/urls.go
+++ b/openstack/identity/v2/tokens/urls.go
@@ -2,6 +2,7 @@
 
 import "github.com/rackspace/gophercloud"
 
-func listURL(client *gophercloud.ServiceClient) string {
+// CreateURL generates the URL used to create new Tokens.
+func CreateURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("tokens")
 }
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
index 2dd2357..d1c2472 100644
--- a/openstack/identity/v3/endpoints/results.go
+++ b/openstack/identity/v3/endpoints/results.go
@@ -1,8 +1,6 @@
 package endpoints
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -24,11 +22,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Endpoint: %v", err)
-	}
 
-	return &res.Endpoint, nil
+	return &res.Endpoint, err
 }
 
 // CreateResult is the deferred result of a Create call.
@@ -77,8 +72,6 @@
 	}
 
 	err := mapstructure.Decode(page.(EndpointPage).Body, &response)
-	if err != nil {
-		return nil, err
-	}
-	return response.Endpoints, nil
+
+	return response.Endpoints, err
 }
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
index b4e7bd2..e4e068b 100644
--- a/openstack/identity/v3/services/results.go
+++ b/openstack/identity/v3/services/results.go
@@ -1,8 +1,6 @@
 package services
 
 import (
-	"fmt"
-
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 
@@ -25,11 +23,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Service: %v", err)
-	}
 
-	return &res.Service, nil
+	return &res.Service, err
 }
 
 // CreateResult is the deferred result of a Create call.
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index e96da51..1be98cb 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -40,11 +40,8 @@
 
 	// Attempt to parse the timestamp.
 	token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
-	if err != nil {
-		return nil, err
-	}
 
-	return &token, nil
+	return &token, err
 }
 
 // CreateResult is the deferred response from a Create call.
diff --git a/openstack/networking/v2/apiversions/requests_test.go b/openstack/networking/v2/apiversions/requests_test.go
index c49b509..d35af9f 100644
--- a/openstack/networking/v2/apiversions/requests_test.go
+++ b/openstack/networking/v2/apiversions/requests_test.go
@@ -65,6 +65,22 @@
 	}
 }
 
+func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+
+	ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := ExtractAPIVersions(page); err == nil {
+			t.Fatalf("Expected error, got nil")
+		}
+		return true, nil
+	})
+}
+
 func TestAPIInfo(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
@@ -148,3 +164,19 @@
 		t.Errorf("Expected 1 page, got %d", count)
 	}
 }
+
+func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+
+	ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := ExtractVersionResources(page); err == nil {
+			t.Fatalf("Expected error, got nil")
+		}
+		return true, nil
+	})
+}
diff --git a/openstack/networking/v2/apiversions/results.go b/openstack/networking/v2/apiversions/results.go
index e13998d..9715934 100644
--- a/openstack/networking/v2/apiversions/results.go
+++ b/openstack/networking/v2/apiversions/results.go
@@ -35,11 +35,8 @@
 	}
 
 	err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Versions, nil
+	return resp.Versions, err
 }
 
 // APIVersionResource represents a generic API resource. It contains the name
@@ -75,9 +72,6 @@
 	}
 
 	err := mapstructure.Decode(page.(APIVersionResourcePage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.APIVersionResources, nil
+	return resp.APIVersionResources, err
 }
diff --git a/openstack/networking/v2/apiversions/urls.go b/openstack/networking/v2/apiversions/urls.go
index c43f991..58aa2b6 100644
--- a/openstack/networking/v2/apiversions/urls.go
+++ b/openstack/networking/v2/apiversions/urls.go
@@ -7,9 +7,9 @@
 )
 
 func apiVersionsURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL("")
+	return c.Endpoint
 }
 
 func apiInfoURL(c *gophercloud.ServiceClient, version string) string {
-	return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+	return c.Endpoint + strings.TrimRight(version, "/") + "/"
 }
diff --git a/openstack/networking/v2/common/common_tests.go b/openstack/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..4160351
--- /dev/null
+++ b/openstack/networking/v2/common/common_tests.go
@@ -0,0 +1,14 @@
+package common
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+	sc := client.ServiceClient()
+	sc.ResourceBase = sc.Endpoint + "v2.0/"
+	return sc
+}
diff --git a/openstack/networking/v2/extensions/delegate_test.go b/openstack/networking/v2/extensions/delegate_test.go
index 8de8906..3d2ac78 100755
--- a/openstack/networking/v2/extensions/delegate_test.go
+++ b/openstack/networking/v2/extensions/delegate_test.go
@@ -6,16 +6,16 @@
 	"testing"
 
 	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -73,7 +73,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -92,14 +92,14 @@
     }
 }
     `)
-
-		ext, err := Get(fake.ServiceClient(), "agent").Extract()
-		th.AssertNoErr(t, err)
-
-		th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
-		th.AssertEquals(t, ext.Name, "agent")
-		th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
-		th.AssertEquals(t, ext.Alias, "agent")
-		th.AssertEquals(t, ext.Description, "The agent management extension.")
 	})
+
+	ext, err := Get(fake.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
+	th.AssertEquals(t, ext.Name, "agent")
+	th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
+	th.AssertEquals(t, ext.Alias, "agent")
+	th.AssertEquals(t, ext.Description, "The agent management extension.")
 }
diff --git a/openstack/networking/v2/extensions/external/requests.go b/openstack/networking/v2/extensions/external/requests.go
index afdd428..2f04593 100644
--- a/openstack/networking/v2/extensions/external/requests.go
+++ b/openstack/networking/v2/extensions/external/requests.go
@@ -24,12 +24,15 @@
 }
 
 // ToNetworkCreateMap casts a CreateOpts struct to a map.
-func (o CreateOpts) ToNetworkCreateMap() map[string]map[string]interface{} {
-	outer := o.Parent.ToNetworkCreateMap()
+func (o CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	outer, err := o.Parent.ToNetworkCreateMap()
+	if err != nil {
+		return nil, err
+	}
 
-	outer["network"]["router:external"] = o.External
+	outer["network"].(map[string]interface{})["router:external"] = o.External
 
-	return outer
+	return outer, nil
 }
 
 // UpdateOpts is the structure used when updating existing external network
@@ -41,10 +44,13 @@
 }
 
 // ToNetworkUpdateMap casts an UpdateOpts struct to a map.
-func (o UpdateOpts) ToNetworkUpdateMap() map[string]map[string]interface{} {
-	outer := o.Parent.ToNetworkUpdateMap()
+func (o UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+	outer, err := o.Parent.ToNetworkUpdateMap()
+	if err != nil {
+		return nil, err
+	}
 
-	outer["network"]["router:external"] = o.External
+	outer["network"].(map[string]interface{})["router:external"] = o.External
 
-	return outer
+	return outer, nil
 }
diff --git a/openstack/networking/v2/extensions/external/results.go b/openstack/networking/v2/extensions/external/results.go
index 4cd2133..47f7258 100644
--- a/openstack/networking/v2/extensions/external/results.go
+++ b/openstack/networking/v2/extensions/external/results.go
@@ -1,8 +1,6 @@
 package external
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
 	"github.com/rackspace/gophercloud/pagination"
@@ -37,52 +35,36 @@
 	External bool `mapstructure:"router:external" json:"router:external"`
 }
 
-// ExtractGet decorates a GetResult struct returned from a networks.Get()
-// function with extended attributes.
-func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
-	if r.Err != nil {
-		return nil, r.Err
+func commonExtract(e error, response map[string]interface{}) (*NetworkExternal, error) {
+	if e != nil {
+		return nil, e
 	}
+
 	var res struct {
 		Network *NetworkExternal `json:"network"`
 	}
-	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
-	return res.Network, nil
+
+	err := mapstructure.Decode(response, &res)
+
+	return res.Network, err
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
+	return commonExtract(r.Err, r.Resp)
 }
 
 // ExtractCreate decorates a CreateResult struct returned from a networks.Create()
 // function with extended attributes.
 func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) {
-	if r.Err != nil {
-		return nil, r.Err
-	}
-	var res struct {
-		Network *NetworkExternal `json:"network"`
-	}
-	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
-	return res.Network, nil
+	return commonExtract(r.Err, r.Resp)
 }
 
 // ExtractUpdate decorates a UpdateResult struct returned from a
 // networks.Update() function with extended attributes.
 func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) {
-	if r.Err != nil {
-		return nil, r.Err
-	}
-	var res struct {
-		Network *NetworkExternal `json:"network"`
-	}
-	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
-	return res.Network, nil
+	return commonExtract(r.Err, r.Resp)
 }
 
 // ExtractList accepts a Page struct, specifically a NetworkPage struct, and
@@ -94,9 +76,6 @@
 	}
 
 	err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Networks, nil
+	return resp.Networks, err
 }
diff --git a/openstack/networking/v2/extensions/external/results_test.go b/openstack/networking/v2/extensions/external/results_test.go
index 6bed126..916cd2c 100644
--- a/openstack/networking/v2/extensions/external/results_test.go
+++ b/openstack/networking/v2/extensions/external/results_test.go
@@ -1,6 +1,7 @@
 package external
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"testing"
@@ -228,3 +229,26 @@
 	th.AssertNoErr(t, err)
 	th.AssertEquals(t, true, n.External)
 }
+
+func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) {
+	gr := networks.GetResult{}
+	gr.Err = errors.New("")
+
+	if _, err := ExtractGet(gr); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+
+	ur := networks.UpdateResult{}
+	ur.Err = errors.New("")
+
+	if _, err := ExtractUpdate(ur); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+
+	cr := networks.CreateResult{}
+	cr.Err = errors.New("")
+
+	if _, err := ExtractCreate(cr); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
index b9153fc..19614be 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestList(t *testing.T) {
@@ -90,6 +90,34 @@
 	}
 }
 
+func TestInvalidNextPageURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`)
+	})
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		ExtractFloatingIPs(page)
+		return true, nil
+	})
+}
+
+func TestRequiredFieldsForCreate(t *testing.T) {
+	res1 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: ""})
+	if res1.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res2 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: "foo", PortID: ""})
+	if res2.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestCreate(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/results.go b/openstack/networking/v2/extensions/layer3/floatingips/results.go
index 4857c92..43892f0 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/results.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -89,12 +89,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p FloatingIPPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"floatingips_links"`
+		Links []gophercloud.Link `mapstructure:"floatingips_links"`
 	}
 
 	var r resp
@@ -103,17 +99,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a NetworkPage struct is empty.
@@ -134,9 +120,6 @@
 	}
 
 	err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.FloatingIPs, nil
+	return resp.FloatingIPs, err
 }
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
index dbe3f9f..355f20d 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/urls.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
@@ -2,15 +2,12 @@
 
 import "github.com/rackspace/gophercloud"
 
-const (
-	version      = "v2.0"
-	resourcePath = "floatingips"
-)
+const resourcePath = "floatingips"
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, resourcePath)
+	return c.ServiceURL(resourcePath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, resourcePath, id)
+	return c.ServiceURL(resourcePath, id)
 }
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
index 56a0d74..c34264d 100755
--- a/openstack/networking/v2/extensions/layer3/routers/requests_test.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
@@ -288,6 +288,17 @@
 	th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
 }
 
+func TestAddInterfaceRequiredOpts(t *testing.T) {
+	_, err := AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	_, err = AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestRemoveInterface(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go
index d14578c..eae647f 100755
--- a/openstack/networking/v2/extensions/layer3/routers/results.go
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -1,8 +1,6 @@
 package routers
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -53,12 +51,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p RouterPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"routers_links"`
+		Links []gophercloud.Link `mapstructure:"routers_links"`
 	}
 
 	var r resp
@@ -67,17 +61,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a RouterPage struct is empty.
@@ -98,11 +82,8 @@
 	}
 
 	err := mapstructure.Decode(page.(RouterPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Routers, nil
+	return resp.Routers, err
 }
 
 type commonResult struct {
@@ -120,11 +101,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron router: %v", err)
-	}
 
-	return res.Router, nil
+	return res.Router, err
 }
 
 // CreateResult represents the result of a create operation.
@@ -176,9 +154,6 @@
 
 	var res *InterfaceInfo
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron router interface: %v", err)
-	}
 
-	return res, nil
+	return res, err
 }
diff --git a/openstack/networking/v2/extensions/layer3/routers/urls.go b/openstack/networking/v2/extensions/layer3/routers/urls.go
index 512c8a4..bc22c2a 100644
--- a/openstack/networking/v2/extensions/layer3/routers/urls.go
+++ b/openstack/networking/v2/extensions/layer3/routers/urls.go
@@ -2,23 +2,20 @@
 
 import "github.com/rackspace/gophercloud"
 
-const (
-	version      = "v2.0"
-	resourcePath = "routers"
-)
+const resourcePath = "routers"
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, resourcePath)
+	return c.ServiceURL(resourcePath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, resourcePath, id)
+	return c.ServiceURL(resourcePath, id)
 }
 
 func addInterfaceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, resourcePath, id, "add_router_interface")
+	return c.ServiceURL(resourcePath, id, "add_router_interface")
 }
 
 func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, resourcePath, id, "remove_router_interface")
+	return c.ServiceURL(resourcePath, id, "remove_router_interface")
 }
diff --git a/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
index cae3524..dc1ece3 100644
--- a/openstack/networking/v2/extensions/lbaas/members/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
diff --git a/openstack/networking/v2/extensions/lbaas/members/results.go b/openstack/networking/v2/extensions/lbaas/members/results.go
index 9466fee..b006551 100644
--- a/openstack/networking/v2/extensions/lbaas/members/results.go
+++ b/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -1,8 +1,6 @@
 package members
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -46,12 +44,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p MemberPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"members_links"`
+		Links []gophercloud.Link `mapstructure:"members_links"`
 	}
 
 	var r resp
@@ -60,17 +54,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a MemberPage struct is empty.
@@ -113,11 +97,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron member: %v", err)
-	}
 
-	return res.Member, nil
+	return res.Member, err
 }
 
 // CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/members/urls.go b/openstack/networking/v2/extensions/lbaas/members/urls.go
index 9d5ecec..94b57e4 100644
--- a/openstack/networking/v2/extensions/lbaas/members/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/members/urls.go
@@ -3,15 +3,14 @@
 import "github.com/rackspace/gophercloud"
 
 const (
-	version      = "v2.0"
 	rootPath     = "lb"
 	resourcePath = "members"
 )
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, rootPath, resourcePath)
+	return c.ServiceURL(rootPath, resourcePath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, resourcePath, id)
+	return c.ServiceURL(rootPath, resourcePath, id)
 }
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
index 68a3288..5c5a1d2 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
@@ -158,6 +158,17 @@
 	th.AssertNoErr(t, err)
 }
 
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/results.go b/openstack/networking/v2/extensions/lbaas/monitors/results.go
index a41f20e..656ab1d 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/results.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -1,8 +1,6 @@
 package monitors
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -74,12 +72,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p MonitorPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"health_monitors_links"`
+		Links []gophercloud.Link `mapstructure:"health_monitors_links"`
 	}
 
 	var r resp
@@ -88,17 +82,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a PoolPage struct is empty.
@@ -119,11 +103,8 @@
 	}
 
 	err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Monitors, nil
+	return resp.Monitors, err
 }
 
 type commonResult struct {
@@ -141,11 +122,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron monitor: %v", err)
-	}
 
-	return res.Monitor, nil
+	return res.Monitor, err
 }
 
 // CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
index e4b2afc..46e84bb 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
@@ -3,15 +3,14 @@
 import "github.com/rackspace/gophercloud"
 
 const (
-	version      = "v2.0"
 	rootPath     = "lb"
 	resourcePath = "health_monitors"
 )
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, rootPath, resourcePath)
+	return c.ServiceURL(rootPath, resourcePath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, resourcePath, id)
+	return c.ServiceURL(rootPath, resourcePath, id)
 }
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
index 6af47a1..6da29a6 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
diff --git a/openstack/networking/v2/extensions/lbaas/pools/results.go b/openstack/networking/v2/extensions/lbaas/pools/results.go
index 5b2adde..4233176 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/results.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -1,8 +1,6 @@
 package pools
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -68,12 +66,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p PoolPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"pools_links"`
+		Links []gophercloud.Link `mapstructure:"pools_links"`
 	}
 
 	var r resp
@@ -82,17 +76,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a PoolPage struct is empty.
@@ -113,11 +97,8 @@
 	}
 
 	err := mapstructure.Decode(page.(PoolPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Pools, nil
+	return resp.Pools, err
 }
 
 type commonResult struct {
@@ -135,11 +116,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron pool: %v", err)
-	}
 
-	return res.Pool, nil
+	return res.Pool, err
 }
 
 // CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/pools/urls.go b/openstack/networking/v2/extensions/lbaas/pools/urls.go
index 124ef5d..6cd15b0 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/urls.go
@@ -3,24 +3,23 @@
 import "github.com/rackspace/gophercloud"
 
 const (
-	version      = "v2.0"
 	rootPath     = "lb"
 	resourcePath = "pools"
 	monitorPath  = "health_monitors"
 )
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, rootPath, resourcePath)
+	return c.ServiceURL(rootPath, resourcePath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, resourcePath, id)
+	return c.ServiceURL(rootPath, resourcePath, id)
 }
 
 func associateURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, resourcePath, id, monitorPath)
+	return c.ServiceURL(rootPath, resourcePath, id, monitorPath)
 }
 
 func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string {
-	return c.ServiceURL(version, rootPath, resourcePath, poolID, monitorPath, monitorID)
+	return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID)
 }
diff --git a/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
index f4e90c7..430f1a1 100644
--- a/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
@@ -139,7 +139,8 @@
         "admin_state_up": true,
         "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
         "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
-        "protocol_port": 80
+        "protocol_port": 80,
+				"session_persistence": {"type": "SOURCE_IP"}
     }
 }
 			`)
@@ -175,6 +176,7 @@
 		SubnetID:     "8032909d-47a1-4715-90af-5153ffe39861",
 		PoolID:       "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
 		ProtocolPort: 80,
+		Persistence:  &SessionPersistence{Type: "SOURCE_IP"},
 	}
 
 	r, err := Create(fake.ServiceClient(), opts).Extract()
@@ -195,6 +197,29 @@
 	th.AssertEquals(t, "NewVip", r.Name)
 }
 
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
@@ -254,7 +279,8 @@
 		th.TestJSONRequest(t, r, `
 {
     "vip": {
-        "connection_limit": 1000
+        "connection_limit": 1000,
+				"session_persistence": {"type": "SOURCE_IP"}
     }
 }
 			`)
@@ -284,7 +310,10 @@
 	})
 
 	i1000 := 1000
-	options := UpdateOpts{ConnLimit: &i1000}
+	options := UpdateOpts{
+		ConnLimit:   &i1000,
+		Persistence: &SessionPersistence{Type: "SOURCE_IP"},
+	}
 	vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract()
 	th.AssertNoErr(t, err)
 
diff --git a/openstack/networking/v2/extensions/lbaas/vips/results.go b/openstack/networking/v2/extensions/lbaas/vips/results.go
index 62efe09..5925adc 100644
--- a/openstack/networking/v2/extensions/lbaas/vips/results.go
+++ b/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -1,8 +1,6 @@
 package vips
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -28,7 +26,7 @@
 	Type string `mapstructure:"type" json:"type"`
 
 	// Name of cookie if persistence mode is set appropriately
-	CookieName string `mapstructure:"cookie_name" json:"cookie_name"`
+	CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"`
 }
 
 // VirtualIP is the primary load balancing configuration object that specifies
@@ -93,12 +91,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p VIPPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"vips_links"`
+		Links []gophercloud.Link `mapstructure:"vips_links"`
 	}
 
 	var r resp
@@ -107,17 +101,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a RouterPage struct is empty.
@@ -138,11 +122,8 @@
 	}
 
 	err := mapstructure.Decode(page.(VIPPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.VIPs, nil
+	return resp.VIPs, err
 }
 
 type commonResult struct {
@@ -160,11 +141,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron Virtual IP: %v", err)
-	}
 
-	return res.VirtualIP, nil
+	return res.VirtualIP, err
 }
 
 // CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/lbaas/vips/urls.go b/openstack/networking/v2/extensions/lbaas/vips/urls.go
index 570db6d..2b6f67e 100644
--- a/openstack/networking/v2/extensions/lbaas/vips/urls.go
+++ b/openstack/networking/v2/extensions/lbaas/vips/urls.go
@@ -3,15 +3,14 @@
 import "github.com/rackspace/gophercloud"
 
 const (
-	version      = "v2.0"
 	rootPath     = "lb"
 	resourcePath = "vips"
 )
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, rootPath, resourcePath)
+	return c.ServiceURL(rootPath, resourcePath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, resourcePath, id)
+	return c.ServiceURL(rootPath, resourcePath, id)
 }
diff --git a/openstack/networking/v2/extensions/provider/results.go b/openstack/networking/v2/extensions/provider/results.go
index a20b259..8fc21b4 100755
--- a/openstack/networking/v2/extensions/provider/results.go
+++ b/openstack/networking/v2/extensions/provider/results.go
@@ -1,8 +1,6 @@
 package provider
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
 	"github.com/rackspace/gophercloud/pagination"
@@ -70,14 +68,14 @@
 	if r.Err != nil {
 		return nil, r.Err
 	}
+
 	var res struct {
 		Network *NetworkExtAttrs `json:"network"`
 	}
+
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
-	return res.Network, nil
+
+	return res.Network, err
 }
 
 // ExtractCreate decorates a CreateResult struct returned from a networks.Create()
@@ -86,14 +84,14 @@
 	if r.Err != nil {
 		return nil, r.Err
 	}
+
 	var res struct {
 		Network *NetworkExtAttrs `json:"network"`
 	}
+
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
-	return res.Network, nil
+
+	return res.Network, err
 }
 
 // ExtractUpdate decorates a UpdateResult struct returned from a
@@ -102,14 +100,14 @@
 	if r.Err != nil {
 		return nil, r.Err
 	}
+
 	var res struct {
 		Network *NetworkExtAttrs `json:"network"`
 	}
+
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
-	return res.Network, nil
+
+	return res.Network, err
 }
 
 // ExtractList accepts a Page struct, specifically a NetworkPage struct, and
@@ -121,9 +119,6 @@
 	}
 
 	err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Networks, nil
+	return resp.Networks, err
 }
diff --git a/openstack/networking/v2/extensions/provider/results_test.go b/openstack/networking/v2/extensions/provider/results_test.go
index 74951d9..9801b2e 100644
--- a/openstack/networking/v2/extensions/provider/results_test.go
+++ b/openstack/networking/v2/extensions/provider/results_test.go
@@ -5,17 +5,17 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -109,7 +109,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -150,7 +150,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks", 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")
@@ -202,7 +202,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "PUT")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		th.TestHeader(t, r, "Content-Type", "application/json")
diff --git a/openstack/networking/v2/extensions/security/groups/requests_test.go b/openstack/networking/v2/extensions/security/groups/requests_test.go
index 857bdc6..5f074c7 100644
--- a/openstack/networking/v2/extensions/security/groups/requests_test.go
+++ b/openstack/networking/v2/extensions/security/groups/requests_test.go
@@ -5,10 +5,10 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
diff --git a/openstack/networking/v2/extensions/security/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go
index 6db613e..617d690 100644
--- a/openstack/networking/v2/extensions/security/groups/results.go
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -1,8 +1,6 @@
 package groups
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
@@ -40,12 +38,8 @@
 // reached the end of a page and the pager seeks to traverse over a new one. In
 // order to do this, it needs to construct the next page's URL.
 func (p SecGroupPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"security_groups_links"`
+		Links []gophercloud.Link `mapstructure:"security_groups_links"`
 	}
 
 	var r resp
@@ -54,17 +48,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a SecGroupPage struct is empty.
@@ -85,11 +69,8 @@
 	}
 
 	err := mapstructure.Decode(page.(SecGroupPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.SecGroups, nil
+	return resp.SecGroups, err
 }
 
 type commonResult struct {
@@ -107,11 +88,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron secgroup: %v", err)
-	}
 
-	return res.SecGroup, nil
+	return res.SecGroup, err
 }
 
 // CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/security/groups/urls.go b/openstack/networking/v2/extensions/security/groups/urls.go
index 2f2bbdd..84f7324 100644
--- a/openstack/networking/v2/extensions/security/groups/urls.go
+++ b/openstack/networking/v2/extensions/security/groups/urls.go
@@ -2,15 +2,12 @@
 
 import "github.com/rackspace/gophercloud"
 
-const (
-	version  = "v2.0"
-	rootPath = "security-groups"
-)
+const rootPath = "security-groups"
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, rootPath)
+	return c.ServiceURL(rootPath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, id)
+	return c.ServiceURL(rootPath, id)
 }
diff --git a/openstack/networking/v2/extensions/security/rules/requests_test.go b/openstack/networking/v2/extensions/security/rules/requests_test.go
index a2c7fbb..b5afef3 100644
--- a/openstack/networking/v2/extensions/security/rules/requests_test.go
+++ b/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -5,9 +5,9 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestURLs(t *testing.T) {
@@ -165,6 +165,25 @@
 	th.AssertNoErr(t, err)
 }
 
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{Direction: "something"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: "something"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4, SecGroupID: "something", Protocol: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
index 13d5c7d..ca8435e 100644
--- a/openstack/networking/v2/extensions/security/rules/results.go
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -1,8 +1,6 @@
 package rules
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -65,12 +63,8 @@
 // reached the end of a page and the pager seeks to traverse over a new one. In
 // order to do this, it needs to construct the next page's URL.
 func (p SecGroupRulePage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"security_group_rules_links"`
+		Links []gophercloud.Link `mapstructure:"security_group_rules_links"`
 	}
 
 	var r resp
@@ -79,17 +73,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a SecGroupRulePage struct is empty.
@@ -110,11 +94,8 @@
 	}
 
 	err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.SecGroupRules, nil
+	return resp.SecGroupRules, err
 }
 
 type commonResult struct {
@@ -132,11 +113,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron SecGroupRule: %v", err)
-	}
 
-	return res.SecGroupRule, nil
+	return res.SecGroupRule, err
 }
 
 // CreateResult represents the result of a create operation.
diff --git a/openstack/networking/v2/extensions/security/rules/urls.go b/openstack/networking/v2/extensions/security/rules/urls.go
index 2ffbf37..8e2b2bb 100644
--- a/openstack/networking/v2/extensions/security/rules/urls.go
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -2,15 +2,12 @@
 
 import "github.com/rackspace/gophercloud"
 
-const (
-	version  = "v2.0"
-	rootPath = "security-group-rules"
-)
+const rootPath = "security-group-rules"
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, rootPath)
+	return c.ServiceURL(rootPath)
 }
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, rootPath, id)
+	return c.ServiceURL(rootPath, id)
 }
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index 81b1545..8cfe4e0 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -1,9 +1,23 @@
 package networks
 
 import (
-	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
 )
 
 type networkOpts struct {
@@ -13,6 +27,12 @@
 	TenantID     string
 }
 
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToNetworkListQuery() (string, error)
+}
+
 // ListOpts allows the filtering and sorting of paginated collections through
 // the API. Filtering is achieved by passing in struct field values that map to
 // the network attributes you want to see returned. SortKey allows you to sort
@@ -31,17 +51,29 @@
 	SortDir      string `q:"sort_dir"`
 }
 
+// ToNetworkListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToNetworkListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // List returns a Pager which allows you to iterate over a collection of
 // networks. It accepts a ListOpts struct, which allows you to filter and sort
 // the returned collection for greater efficiency.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	// Build query parameters
-	q, err := gophercloud.BuildQueryString(&opts)
-	if err != nil {
-		return pagination.Pager{Err: err}
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToNetworkListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
 	}
-	u := listURL(c) + q.String()
-	return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+
+	return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
 		return NetworkPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	})
 }
@@ -62,7 +94,7 @@
 // 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 {
-	ToNetworkCreateMap() map[string]map[string]interface{}
+	ToNetworkCreateMap() (map[string]interface{}, error)
 }
 
 // CreateOpts is the common options struct used in this package's Create
@@ -70,26 +102,23 @@
 type CreateOpts networkOpts
 
 // ToNetworkCreateMap casts a CreateOpts struct to a map.
-func (o CreateOpts) ToNetworkCreateMap() map[string]map[string]interface{} {
-	inner := make(map[string]interface{})
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	n := make(map[string]interface{})
 
-	if o.AdminStateUp != nil {
-		inner["admin_state_up"] = &o.AdminStateUp
+	if opts.AdminStateUp != nil {
+		n["admin_state_up"] = &opts.AdminStateUp
 	}
-	if o.Name != "" {
-		inner["name"] = o.Name
+	if opts.Name != "" {
+		n["name"] = opts.Name
 	}
-	if o.Shared != nil {
-		inner["shared"] = &o.Shared
+	if opts.Shared != nil {
+		n["shared"] = &opts.Shared
 	}
-	if o.TenantID != "" {
-		inner["tenant_id"] = o.TenantID
+	if opts.TenantID != "" {
+		n["tenant_id"] = opts.TenantID
 	}
 
-	outer := make(map[string]map[string]interface{})
-	outer["network"] = inner
-
-	return outer
+	return map[string]interface{}{"network": n}, nil
 }
 
 // Create accepts a CreateOpts struct and creates a new network using the values
@@ -102,7 +131,11 @@
 func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
 
-	reqBody := opts.ToNetworkCreateMap()
+	reqBody, err := opts.ToNetworkCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
 
 	// Send request to API
 	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
@@ -119,7 +152,7 @@
 // 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 UpdateOptsBuilder interface {
-	ToNetworkUpdateMap() map[string]map[string]interface{}
+	ToNetworkUpdateMap() (map[string]interface{}, error)
 }
 
 // UpdateOpts is the common options struct used in this package's Update
@@ -127,23 +160,20 @@
 type UpdateOpts networkOpts
 
 // ToNetworkUpdateMap casts a UpdateOpts struct to a map.
-func (o UpdateOpts) ToNetworkUpdateMap() map[string]map[string]interface{} {
-	inner := make(map[string]interface{})
+func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+	n := make(map[string]interface{})
 
-	if o.AdminStateUp != nil {
-		inner["admin_state_up"] = &o.AdminStateUp
+	if opts.AdminStateUp != nil {
+		n["admin_state_up"] = &opts.AdminStateUp
 	}
-	if o.Name != "" {
-		inner["name"] = o.Name
+	if opts.Name != "" {
+		n["name"] = opts.Name
 	}
-	if o.Shared != nil {
-		inner["shared"] = &o.Shared
+	if opts.Shared != nil {
+		n["shared"] = &opts.Shared
 	}
 
-	outer := make(map[string]map[string]interface{})
-	outer["network"] = inner
-
-	return outer
+	return map[string]interface{}{"network": n}, nil
 }
 
 // Update accepts a UpdateOpts struct and updates an existing network using the
@@ -151,7 +181,11 @@
 func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) UpdateResult {
 	var res UpdateResult
 
-	reqBody := opts.ToNetworkUpdateMap()
+	reqBody, err := opts.ToNetworkUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
 
 	// Send request to API
 	_, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
index 6b22acd..a263b7b 100644
--- a/openstack/networking/v2/networks/requests_test.go
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -5,16 +5,16 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -97,7 +97,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -137,7 +137,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks", 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")
@@ -187,7 +187,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks", 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")
@@ -216,7 +216,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "PUT")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		th.TestHeader(t, r, "Content-Type", "application/json")
@@ -264,7 +264,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "DELETE")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/networks/results.go b/openstack/networking/v2/networks/results.go
index 2dbd55f..e605fcf 100644
--- a/openstack/networking/v2/networks/results.go
+++ b/openstack/networking/v2/networks/results.go
@@ -1,8 +1,6 @@
 package networks
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -23,11 +21,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
-	}
 
-	return res.Network, nil
+	return res.Network, err
 }
 
 // CreateResult represents the result of a create operation.
@@ -83,12 +78,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p NetworkPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"networks_links"`
+		Links []gophercloud.Link `mapstructure:"networks_links"`
 	}
 
 	var r resp
@@ -97,17 +88,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a NetworkPage struct is empty.
@@ -128,9 +109,6 @@
 	}
 
 	err := mapstructure.Decode(page.(NetworkPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Networks, nil
+	return resp.Networks, err
 }
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 6928d8f..c846de9 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -1,11 +1,31 @@
 package ports
 
 import (
-	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
 )
 
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToPortListQuery() (string, error)
+}
+
 // ListOpts allows the filtering and sorting of paginated collections through
 // the API. Filtering is achieved by passing in struct field values that map to
 // the port attributes you want to see returned. SortKey allows you to sort
@@ -27,6 +47,15 @@
 	SortDir      string `q:"sort_dir"`
 }
 
+// ToPortListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPortListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // List returns a Pager which allows you to iterate over a collection of
 // ports. It accepts a ListOpts struct, which allows you to filter and sort
 // the returned collection for greater efficiency.
@@ -34,15 +63,17 @@
 // Default policy settings return only those ports that are owned by the tenant
 // who submits the request, unless the request is submitted by an user with
 // administrative rights.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	// Build query parameters
-	q, err := gophercloud.BuildQueryString(&opts)
-	if err != nil {
-		return pagination.Pager{Err: err}
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToPortListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
 	}
-	u := listURL(c) + q.String()
 
-	return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+	return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
 		return PortPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	})
 }
@@ -58,6 +89,14 @@
 	return res
 }
 
+// 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 {
+	ToPortCreateMap() (map[string]interface{}, error)
+}
+
 // CreateOpts represents the attributes used when creating a new port.
 type CreateOpts struct {
 	NetworkID      string
@@ -71,51 +110,54 @@
 	SecurityGroups []string
 }
 
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.NetworkID == "" {
+		return nil, errNetworkIDRequired
+	}
+	p["network_id"] = opts.NetworkID
+
+	if opts.DeviceID != "" {
+		p["device_id"] = opts.DeviceID
+	}
+	if opts.DeviceOwner != "" {
+		p["device_owner"] = opts.DeviceOwner
+	}
+	if opts.FixedIPs != nil {
+		p["fixed_ips"] = opts.FixedIPs
+	}
+	if opts.SecurityGroups != nil {
+		p["security_groups"] = opts.SecurityGroups
+	}
+	if opts.TenantID != "" {
+		p["tenant_id"] = opts.TenantID
+	}
+	if opts.AdminStateUp != nil {
+		p["admin_state_up"] = &opts.AdminStateUp
+	}
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+	if opts.MACAddress != "" {
+		p["mac_address"] = opts.MACAddress
+	}
+
+	return map[string]interface{}{"port": p}, nil
+}
+
 // Create accepts a CreateOpts struct and creates a new network using the values
 // provided. You must remember to provide a NetworkID value.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
 
-	type port struct {
-		NetworkID      string      `json:"network_id"`
-		Name           *string     `json:"name,omitempty"`
-		AdminStateUp   *bool       `json:"admin_state_up,omitempty"`
-		MACAddress     *string     `json:"mac_address,omitempty"`
-		FixedIPs       interface{} `json:"fixed_ips,omitempty"`
-		DeviceID       *string     `json:"device_id,omitempty"`
-		DeviceOwner    *string     `json:"device_owner,omitempty"`
-		TenantID       *string     `json:"tenant_id,omitempty"`
-		SecurityGroups []string    `json:"security_groups,omitempty"`
-	}
-	type request struct {
-		Port port `json:"port"`
-	}
-
-	// Validate
-	if opts.NetworkID == "" {
-		res.Err = errNetworkIDRequired
+	reqBody, err := opts.ToPortCreateMap()
+	if err != nil {
+		res.Err = err
 		return res
 	}
 
-	// Populate request body
-	reqBody := request{Port: port{
-		NetworkID:    opts.NetworkID,
-		Name:         gophercloud.MaybeString(opts.Name),
-		AdminStateUp: opts.AdminStateUp,
-		TenantID:     gophercloud.MaybeString(opts.TenantID),
-		MACAddress:   gophercloud.MaybeString(opts.MACAddress),
-		DeviceID:     gophercloud.MaybeString(opts.DeviceID),
-		DeviceOwner:  gophercloud.MaybeString(opts.DeviceOwner),
-	}}
-
-	if opts.FixedIPs != nil {
-		reqBody.Port.FixedIPs = opts.FixedIPs
-	}
-
-	if opts.SecurityGroups != nil {
-		reqBody.Port.SecurityGroups = opts.SecurityGroups
-	}
-
 	// Response
 	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
@@ -128,6 +170,14 @@
 	return res
 }
 
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update 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 UpdateOptsBuilder interface {
+	ToPortUpdateMap() (map[string]interface{}, error)
+}
+
 // UpdateOpts represents the attributes used when updating an existing port.
 type UpdateOpts struct {
 	Name           string
@@ -138,39 +188,43 @@
 	SecurityGroups []string
 }
 
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.DeviceID != "" {
+		p["device_id"] = opts.DeviceID
+	}
+	if opts.DeviceOwner != "" {
+		p["device_owner"] = opts.DeviceOwner
+	}
+	if opts.FixedIPs != nil {
+		p["fixed_ips"] = opts.FixedIPs
+	}
+	if opts.SecurityGroups != nil {
+		p["security_groups"] = opts.SecurityGroups
+	}
+	if opts.AdminStateUp != nil {
+		p["admin_state_up"] = &opts.AdminStateUp
+	}
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+
+	return map[string]interface{}{"port": p}, nil
+}
+
 // Update accepts a UpdateOpts struct and updates an existing port using the
 // values provided.
-func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
-	type port struct {
-		Name           *string     `json:"name,omitempty"`
-		AdminStateUp   *bool       `json:"admin_state_up,omitempty"`
-		FixedIPs       interface{} `json:"fixed_ips,omitempty"`
-		DeviceID       *string     `json:"device_id,omitempty"`
-		DeviceOwner    *string     `json:"device_owner,omitempty"`
-		SecurityGroups []string    `json:"security_groups,omitempty"`
-	}
-	type request struct {
-		Port port `json:"port"`
-	}
-
-	// Populate request body
-	reqBody := request{Port: port{
-		Name:         gophercloud.MaybeString(opts.Name),
-		AdminStateUp: opts.AdminStateUp,
-		DeviceID:     gophercloud.MaybeString(opts.DeviceID),
-		DeviceOwner:  gophercloud.MaybeString(opts.DeviceOwner),
-	}}
-
-	if opts.FixedIPs != nil {
-		reqBody.Port.FixedIPs = opts.FixedIPs
-	}
-
-	if opts.SecurityGroups != nil {
-		reqBody.Port.SecurityGroups = opts.SecurityGroups
-	}
-
-	// Response
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
 	var res UpdateResult
+
+	reqBody, err := opts.ToPortUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
 		ReqBody:     &reqBody,
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
index 7576341..9e323ef 100644
--- a/openstack/networking/v2/ports/requests_test.go
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -5,16 +5,16 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -93,7 +93,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -147,7 +147,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/ports", 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")
@@ -157,7 +157,14 @@
     "port": {
         "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
         "name": "private-port",
-        "admin_state_up": true
+        "admin_state_up": true,
+				"fixed_ips": [
+						{
+								"subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+								"ip_address": "10.0.0.2"
+						}
+				],
+				"security_groups": ["foo"]
     }
 }
 			`)
@@ -193,7 +200,15 @@
 	})
 
 	asu := true
-	options := CreateOpts{Name: "private-port", AdminStateUp: &asu, NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7"}
+	options := CreateOpts{
+		Name:         "private-port",
+		AdminStateUp: &asu,
+		NetworkID:    "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+		FixedIPs: []IP{
+			IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+		},
+		SecurityGroups: []string{"foo"},
+	}
 	n, err := Create(fake.ServiceClient(), options).Extract()
 	th.AssertNoErr(t, err)
 
@@ -211,11 +226,18 @@
 	th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
 }
 
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestUpdate(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "PUT")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		th.TestHeader(t, r, "Content-Type", "application/json")
@@ -288,7 +310,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "DELETE")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
index 91118a4..cedd658 100644
--- a/openstack/networking/v2/ports/results.go
+++ b/openstack/networking/v2/ports/results.go
@@ -1,8 +1,6 @@
 package ports
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -23,11 +21,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron port: %v", err)
-	}
 
-	return res.Port, nil
+	return res.Port, err
 }
 
 // CreateResult represents the result of a create operation.
@@ -94,10 +89,7 @@
 // to do this, it needs to construct the next page's URL.
 func (p PortPage) NextPageURL() (string, error) {
 	type resp struct {
-		Links []struct {
-			Href string `mapstructure:"href"`
-			Rel  string `mapstructure:"rel"`
-		} `mapstructure:"ports_links"`
+		Links []gophercloud.Link `mapstructure:"ports_links"`
 	}
 
 	var r resp
@@ -106,17 +98,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a PortPage struct is empty.
@@ -137,9 +119,6 @@
 	}
 
 	err := mapstructure.Decode(page.(PortPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Ports, nil
+	return resp.Ports, err
 }
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 092c914..8eed269 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -1,11 +1,31 @@
 package subnets
 
 import (
-	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
 )
 
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToSubnetListQuery() (string, error)
+}
+
 // ListOpts allows the filtering and sorting of paginated collections through
 // the API. Filtering is achieved by passing in struct field values that map to
 // the subnet attributes you want to see returned. SortKey allows you to sort
@@ -26,6 +46,15 @@
 	SortDir    string `q:"sort_dir"`
 }
 
+// ToSubnetListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSubnetListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // List returns a Pager which allows you to iterate over a collection of
 // subnets. It accepts a ListOpts struct, which allows you to filter and sort
 // the returned collection for greater efficiency.
@@ -33,13 +62,15 @@
 // Default policy settings return only those subnets that are owned by the tenant
 // who submits the request, unless the request is submitted by an user with
 // administrative rights.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	// Build query parameters
-	query, err := gophercloud.BuildQueryString(&opts)
-	if err != nil {
-		return pagination.Pager{Err: err}
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToSubnetListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
 	}
-	url := listURL(c) + query.String()
 
 	return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
 		return SubnetPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
@@ -63,6 +94,14 @@
 	IPv6 = 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
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToSubnetCreateMap() (map[string]interface{}, error)
+}
+
 // CreateOpts represents the attributes used when creating a new subnet.
 type CreateOpts struct {
 	// Required
@@ -76,65 +115,64 @@
 	IPVersion       int
 	EnableDHCP      *bool
 	DNSNameservers  []string
-	HostRoutes      []interface{}
+	HostRoutes      []HostRoute
+}
+
+// ToSubnetCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.NetworkID == "" {
+		return nil, errNetworkIDRequired
+	}
+	if opts.CIDR == "" {
+		return nil, errCIDRRequired
+	}
+	if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 {
+		return nil, errInvalidIPType
+	}
+
+	s["network_id"] = opts.NetworkID
+	s["cidr"] = opts.CIDR
+
+	if opts.EnableDHCP != nil {
+		s["enable_dhcp"] = &opts.EnableDHCP
+	}
+	if opts.Name != "" {
+		s["name"] = opts.Name
+	}
+	if opts.GatewayIP != "" {
+		s["gateway_ip"] = opts.GatewayIP
+	}
+	if opts.TenantID != "" {
+		s["tenant_id"] = opts.TenantID
+	}
+	if opts.IPVersion != 0 {
+		s["ip_version"] = opts.IPVersion
+	}
+	if len(opts.AllocationPools) != 0 {
+		s["allocation_pools"] = opts.AllocationPools
+	}
+	if len(opts.DNSNameservers) != 0 {
+		s["dns_nameservers"] = opts.DNSNameservers
+	}
+	if len(opts.HostRoutes) != 0 {
+		s["host_routes"] = opts.HostRoutes
+	}
+
+	return map[string]interface{}{"subnet": s}, nil
 }
 
 // Create accepts a CreateOpts struct and creates a new subnet using the values
 // provided. You must remember to provide a valid NetworkID, CIDR and IP version.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
 
-	// Validate required options
-	if opts.NetworkID == "" {
-		res.Err = errNetworkIDRequired
+	reqBody, err := opts.ToSubnetCreateMap()
+	if err != nil {
+		res.Err = err
 		return res
 	}
-	if opts.CIDR == "" {
-		res.Err = errCIDRRequired
-		return res
-	}
-	if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 {
-		res.Err = errInvalidIPType
-		return res
-	}
-
-	type subnet struct {
-		NetworkID       string           `json:"network_id"`
-		CIDR            string           `json:"cidr"`
-		Name            *string          `json:"name,omitempty"`
-		TenantID        *string          `json:"tenant_id,omitempty"`
-		AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
-		GatewayIP       *string          `json:"gateway_ip,omitempty"`
-		IPVersion       int              `json:"ip_version,omitempty"`
-		EnableDHCP      *bool            `json:"enable_dhcp,omitempty"`
-		DNSNameservers  []string         `json:"dns_nameservers,omitempty"`
-		HostRoutes      []interface{}    `json:"host_routes,omitempty"`
-	}
-	type request struct {
-		Subnet subnet `json:"subnet"`
-	}
-
-	reqBody := request{Subnet: subnet{
-		NetworkID:  opts.NetworkID,
-		CIDR:       opts.CIDR,
-		Name:       gophercloud.MaybeString(opts.Name),
-		TenantID:   gophercloud.MaybeString(opts.TenantID),
-		GatewayIP:  gophercloud.MaybeString(opts.GatewayIP),
-		EnableDHCP: opts.EnableDHCP,
-	}}
-
-	if opts.IPVersion != 0 {
-		reqBody.Subnet.IPVersion = opts.IPVersion
-	}
-	if len(opts.AllocationPools) != 0 {
-		reqBody.Subnet.AllocationPools = opts.AllocationPools
-	}
-	if len(opts.DNSNameservers) != 0 {
-		reqBody.Subnet.DNSNameservers = opts.DNSNameservers
-	}
-	if len(opts.HostRoutes) != 0 {
-		reqBody.Subnet.HostRoutes = opts.HostRoutes
-	}
 
 	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
@@ -146,44 +184,55 @@
 	return res
 }
 
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToSubnetUpdateMap() (map[string]interface{}, error)
+}
+
 // UpdateOpts represents the attributes used when updating an existing subnet.
 type UpdateOpts struct {
 	Name           string
 	GatewayIP      string
 	DNSNameservers []string
-	HostRoutes     []interface{}
+	HostRoutes     []HostRoute
 	EnableDHCP     *bool
 }
 
+// ToSubnetUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.EnableDHCP != nil {
+		s["enable_dhcp"] = &opts.EnableDHCP
+	}
+	if opts.Name != "" {
+		s["name"] = opts.Name
+	}
+	if opts.GatewayIP != "" {
+		s["gateway_ip"] = opts.GatewayIP
+	}
+	if len(opts.DNSNameservers) != 0 {
+		s["dns_nameservers"] = opts.DNSNameservers
+	}
+	if len(opts.HostRoutes) != 0 {
+		s["host_routes"] = opts.HostRoutes
+	}
+
+	return map[string]interface{}{"subnet": s}, nil
+}
+
 // Update accepts a UpdateOpts struct and updates an existing subnet using the
 // values provided.
-func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
-	type subnet struct {
-		Name           *string       `json:"name,omitempty"`
-		GatewayIP      *string       `json:"gateway_ip,omitempty"`
-		DNSNameservers []string      `json:"dns_nameservers,omitempty"`
-		HostRoutes     []interface{} `json:"host_routes,omitempty"`
-		EnableDHCP     *bool         `json:"enable_dhcp,omitempty"`
-	}
-	type request struct {
-		Subnet subnet `json:"subnet"`
-	}
-
-	reqBody := request{Subnet: subnet{
-		Name:       gophercloud.MaybeString(opts.Name),
-		GatewayIP:  gophercloud.MaybeString(opts.GatewayIP),
-		EnableDHCP: opts.EnableDHCP,
-	}}
-
-	if len(opts.DNSNameservers) != 0 {
-		reqBody.Subnet.DNSNameservers = opts.DNSNameservers
-	}
-
-	if len(opts.HostRoutes) != 0 {
-		reqBody.Subnet.HostRoutes = opts.HostRoutes
-	}
-
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
 	var res UpdateResult
+
+	reqBody, err := opts.ToSubnetUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
 	_, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
 		ReqBody:     &reqBody,
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index 9f3e8df..987064a 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -5,16 +5,16 @@
 	"net/http"
 	"testing"
 
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
 	"github.com/rackspace/gophercloud/pagination"
 	th "github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestList(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -128,7 +128,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -184,7 +184,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
+	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")
@@ -194,7 +194,15 @@
     "subnet": {
         "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
         "ip_version": 4,
-        "cidr": "192.168.199.0/24"
+        "cidr": "192.168.199.0/24",
+				"dns_nameservers": ["foo"],
+				"allocation_pools": [
+						{
+								"start": "192.168.199.2",
+								"end": "192.168.199.254"
+						}
+				],
+				"host_routes": [{"destination":"","nexthop": "bar"}]
     }
 }
 			`)
@@ -226,7 +234,21 @@
 		`)
 	})
 
-	opts := CreateOpts{NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", IPVersion: 4, CIDR: "192.168.199.0/24"}
+	opts := CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+		IPVersion: 4,
+		CIDR:      "192.168.199.0/24",
+		AllocationPools: []AllocationPool{
+			AllocationPool{
+				Start: "192.168.199.2",
+				End:   "192.168.199.254",
+			},
+		},
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []HostRoute{
+			HostRoute{NextHop: "bar"},
+		},
+	}
 	s, err := Create(fake.ServiceClient(), opts).Extract()
 	th.AssertNoErr(t, err)
 
@@ -248,11 +270,28 @@
 	th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
 }
 
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
 func TestUpdate(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "PUT")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		th.TestHeader(t, r, "Content-Type", "application/json")
@@ -260,7 +299,9 @@
 		th.TestJSONRequest(t, r, `
 {
     "subnet": {
-        "name": "my_new_subnet"
+        "name": "my_new_subnet",
+				"dns_nameservers": ["foo"],
+				"host_routes": [{"destination":"","nexthop": "bar"}]
     }
 }
 		`)
@@ -292,7 +333,13 @@
 	`)
 	})
 
-	opts := UpdateOpts{Name: "my_new_subnet"}
+	opts := UpdateOpts{
+		Name:           "my_new_subnet",
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []HostRoute{
+			HostRoute{NextHop: "bar"},
+		},
+	}
 	s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
 	th.AssertNoErr(t, err)
 
@@ -304,7 +351,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "DELETE")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go
index 5c5744c..3c9ef71 100644
--- a/openstack/networking/v2/subnets/results.go
+++ b/openstack/networking/v2/subnets/results.go
@@ -1,8 +1,6 @@
 package subnets
 
 import (
-	"fmt"
-
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -23,11 +21,8 @@
 	}
 
 	err := mapstructure.Decode(r.Resp, &res)
-	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron subnet: %v", err)
-	}
 
-	return res.Subnet, nil
+	return res.Subnet, err
 }
 
 // CreateResult represents the result of a create operation.
@@ -99,12 +94,8 @@
 // the end of a page and the pager seeks to traverse over a new one. In order
 // to do this, it needs to construct the next page's URL.
 func (p SubnetPage) NextPageURL() (string, error) {
-	type link struct {
-		Href string `mapstructure:"href"`
-		Rel  string `mapstructure:"rel"`
-	}
 	type resp struct {
-		Links []link `mapstructure:"subnets_links"`
+		Links []gophercloud.Link `mapstructure:"subnets_links"`
 	}
 
 	var r resp
@@ -113,17 +104,7 @@
 		return "", err
 	}
 
-	var url string
-	for _, l := range r.Links {
-		if l.Rel == "next" {
-			url = l.Href
-		}
-	}
-	if url == "" {
-		return "", nil
-	}
-
-	return url, nil
+	return gophercloud.ExtractNextURL(r.Links)
 }
 
 // IsEmpty checks whether a SubnetPage struct is empty.
@@ -144,9 +125,6 @@
 	}
 
 	err := mapstructure.Decode(page.(SubnetPage).Body, &resp)
-	if err != nil {
-		return nil, err
-	}
 
-	return resp.Subnets, nil
+	return resp.Subnets, err
 }
diff --git a/openstack/objectstorage/v1/accounts/accounts.go b/openstack/objectstorage/v1/accounts/accounts.go
deleted file mode 100644
index c460e45..0000000
--- a/openstack/objectstorage/v1/accounts/accounts.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package accounts
-
-import (
-	"strings"
-)
-
-// UpdateOpts is a structure that contains parameters for updating, creating, or deleting an
-// account's metadata.
-type UpdateOpts struct {
-	Metadata map[string]string
-	Headers  map[string]string
-}
-
-// GetOpts is a structure that contains parameters for getting an account's metadata.
-type GetOpts struct {
-	Headers map[string]string
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metatdata associated with the account.
-func ExtractMetadata(gr GetResult) map[string]string {
-	metadata := make(map[string]string)
-	for k, v := range gr.Header {
-		if strings.HasPrefix(k, "X-Account-Meta-") {
-			key := strings.TrimPrefix(k, "X-Account-Meta-")
-			metadata[key] = v[0]
-		}
-	}
-	return metadata
-}
diff --git a/openstack/objectstorage/v1/accounts/accounts_test.go b/openstack/objectstorage/v1/accounts/accounts_test.go
deleted file mode 100644
index 2c2a84a..0000000
--- a/openstack/objectstorage/v1/accounts/accounts_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package accounts
-
-import (
-	"net/http"
-	"reflect"
-	"testing"
-)
-
-func TestExtractAccountMetadata(t *testing.T) {
-	getResult := &http.Response{}
-
-	expected := map[string]string{}
-
-	actual := ExtractMetadata(getResult)
-
-	if !reflect.DeepEqual(expected, actual) {
-		t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
-	}
-}
diff --git a/openstack/objectstorage/v1/accounts/requests.go b/openstack/objectstorage/v1/accounts/requests.go
index c738573..55fcb05 100644
--- a/openstack/objectstorage/v1/accounts/requests.go
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -1,46 +1,107 @@
 package accounts
 
 import (
-	"net/http"
-
 	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
 )
 
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
-// Update is a function that creates, updates, or deletes an account's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
-	h := c.Provider.AuthenticatedHeaders()
-
-	for k, v := range opts.Headers {
-		h[k] = v
-	}
-
-	for k, v := range opts.Metadata {
-		h["X-Account-Meta-"+k] = v
-	}
-
-	_, err := perigee.Request("POST", accountURL(c), perigee.Options{
-		MoreHeaders: h,
-		OkCodes:     []int{204},
-	})
-	return err
+// GetOptsBuilder allows extensions to add additional headers to the Get
+// request.
+type GetOptsBuilder interface {
+	ToAccountGetMap() (map[string]string, error)
 }
 
-// Get is a function that retrieves an account's metadata. To extract just the custom
-// metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, opts GetOpts) (GetResult, error) {
+// GetOpts is a structure that contains parameters for getting an account's
+// metadata.
+type GetOpts struct {
+	Newest bool `h:"X-Newest"`
+}
+
+// ToAccountGetMap formats a GetOpts into a map[string]string of headers.
+func (opts GetOpts) ToAccountGetMap() (map[string]string, error) {
+	return gophercloud.BuildHeaders(opts)
+}
+
+// Get is a function that retrieves an account's metadata. To extract just the
+// custom metadata, call the ExtractMetadata method on the GetResult. To extract
+// all the headers that are returned (including the metadata), call the
+// ExtractHeaders method on the GetResult.
+func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult {
+	var res GetResult
 	h := c.Provider.AuthenticatedHeaders()
 
-	for k, v := range opts.Headers {
-		h[k] = v
+	if opts != nil {
+		headers, err := opts.ToAccountGetMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
 	}
 
-	resp, err := perigee.Request("HEAD", accountURL(c), perigee.Options{
+	resp, err := perigee.Request("HEAD", getURL(c), perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{204},
 	})
-	return &resp.HttpResponse, err
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional headers to the Update
+// request.
+type UpdateOptsBuilder interface {
+	ToAccountUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or
+// deleting an account's metadata.
+type UpdateOpts struct {
+	Metadata          map[string]string
+	ContentType       string `h:"Content-Type"`
+	DetectContentType bool   `h:"X-Detect-Content-Type"`
+	TempURLKey        string `h:"X-Account-Meta-Temp-URL-Key"`
+	TempURLKey2       string `h:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers.
+func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
+	headers, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		headers["X-Account-Meta-"+k] = v
+	}
+	return headers, err
+}
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+// To extract the headers returned, call the ExtractHeaders method on the
+// UpdateResult.
+func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+	h := c.Provider.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToAccountUpdateMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("POST", updateURL(c), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	res.Resp = &resp.HttpResponse
+	res.Err = err
+	return res
 }
diff --git a/openstack/objectstorage/v1/accounts/requests_test.go b/openstack/objectstorage/v1/accounts/requests_test.go
index 348f93e..0090eea 100644
--- a/openstack/objectstorage/v1/accounts/requests_test.go
+++ b/openstack/objectstorage/v1/accounts/requests_test.go
@@ -4,41 +4,50 @@
 	"net/http"
 	"testing"
 
-	"github.com/rackspace/gophercloud/testhelper"
+	th "github.com/rackspace/gophercloud/testhelper"
 	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 var metadata = map[string]string{"gophercloud-test": "accounts"}
 
 func TestUpdateAccount(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+	th.Mux.HandleFunc("/", 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, "X-Account-Meta-Gophercloud-Test", "accounts")
+
+		w.Header().Set("X-Account-Container-Count", "2")
+		w.Header().Set("X-Account-Bytes-Used", "14")
+		w.Header().Set("X-Account-Meta-Subject", "books")
+
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	err := Update(fake.ServiceClient(), UpdateOpts{Metadata: metadata})
+	options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+	_, err := Update(fake.ServiceClient(), options).ExtractHeaders()
 	if err != nil {
 		t.Fatalf("Unable to update account: %v", err)
 	}
 }
 
 func TestGetAccount(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "HEAD")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Set("X-Account-Meta-Foo", "bar")
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	_, err := Get(fake.ServiceClient(), GetOpts{})
+	expected := map[string]string{"Foo": "bar"}
+	actual, err := Get(fake.ServiceClient(), &GetOpts{}).ExtractMetadata()
 	if err != nil {
 		t.Fatalf("Unable to get account metadata: %v", err)
 	}
+	th.CheckDeepEquals(t, expected, actual)
 }
diff --git a/openstack/objectstorage/v1/accounts/results.go b/openstack/objectstorage/v1/accounts/results.go
new file mode 100644
index 0000000..8ff8183
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/results.go
@@ -0,0 +1,34 @@
+package accounts
+
+import (
+	"strings"
+
+	objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
+)
+
+// GetResult is returned from a call to the Get function. See v1.CommonResult.
+type GetResult struct {
+	objectstorage.CommonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metatdata associated with the account.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+
+	metadata := make(map[string]string)
+	for k, v := range gr.Resp.Header {
+		if strings.HasPrefix(k, "X-Account-Meta-") {
+			key := strings.TrimPrefix(k, "X-Account-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata, nil
+}
+
+// UpdateResult is returned from a call to the Update function. See v1.CommonResult.
+type UpdateResult struct {
+	objectstorage.CommonResult
+}
diff --git a/openstack/objectstorage/v1/accounts/urls.go b/openstack/objectstorage/v1/accounts/urls.go
index 53b1343..9952fe4 100644
--- a/openstack/objectstorage/v1/accounts/urls.go
+++ b/openstack/objectstorage/v1/accounts/urls.go
@@ -2,7 +2,10 @@
 
 import "github.com/rackspace/gophercloud"
 
-// accountURL returns the URI for making Account requests.
-func accountURL(c *gophercloud.ServiceClient) string {
+func getURL(c *gophercloud.ServiceClient) string {
 	return c.Endpoint
 }
+
+func updateURL(c *gophercloud.ServiceClient) string {
+	return getURL(c)
+}
diff --git a/openstack/objectstorage/v1/accounts/urls_test.go b/openstack/objectstorage/v1/accounts/urls_test.go
index f127a5e..074d52d 100644
--- a/openstack/objectstorage/v1/accounts/urls_test.go
+++ b/openstack/objectstorage/v1/accounts/urls_test.go
@@ -4,14 +4,23 @@
 	"testing"
 
 	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-func TestAccountURL(t *testing.T) {
-	client := gophercloud.ServiceClient{
-		Endpoint: "http://localhost:5000/v3/",
-	}
-	url := accountURL(&client)
-	if url != "http://localhost:5000/v3/" {
-		t.Errorf("Unexpected service URL generated: [%s]", url)
-	}
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient())
+	expected := endpoint
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient())
+	expected := endpoint
+	th.CheckEquals(t, expected, actual)
 }
diff --git a/openstack/objectstorage/v1/common.go b/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..1a6c44a
--- /dev/null
+++ b/openstack/objectstorage/v1/common.go
@@ -0,0 +1,25 @@
+package v1
+
+import (
+	"net/http"
+)
+
+// CommonResult is a structure that contains the response and error of a call to an
+// object storage endpoint.
+type CommonResult struct {
+	Resp *http.Response
+	Err  error
+}
+
+// ExtractHeaders will extract and return the headers from a *http.Response.
+func (cr CommonResult) ExtractHeaders() (http.Header, error) {
+	if cr.Err != nil {
+		return nil, cr.Err
+	}
+
+	var headers http.Header
+	if cr.Err != nil {
+		return headers, cr.Err
+	}
+	return cr.Resp.Header, nil
+}
diff --git a/openstack/objectstorage/v1/containers/requests.go b/openstack/objectstorage/v1/containers/requests.go
index d772a43..ce3f540 100644
--- a/openstack/objectstorage/v1/containers/requests.go
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -6,35 +6,50 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToContainerListParams() (bool, string, error)
+}
+
 // ListOpts is a structure that holds options for listing containers.
 type ListOpts struct {
 	Full      bool
-	Limit     int     `q:"limit"`
-	Marker    string  `q:"marker"`
-	EndMarker string  `q:"end_marker"`
-	Format    string  `q:"format"`
-	Prefix    string  `q:"prefix"`
-	Delimiter [1]byte `q:"delimiter"`
+	Limit     int    `q:"limit"`
+	Marker    string `q:"marker"`
+	EndMarker string `q:"end_marker"`
+	Format    string `q:"format"`
+	Prefix    string `q:"prefix"`
+	Delimiter string `q:"delimiter"`
 }
 
-// List is a function that retrieves containers associated with the account as well as account
-// metadata. It returns a pager which can be iterated with the EachPage function.
-func List(c *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
-	var headers map[string]string
+// ToContainerListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each container.
+func (opts ListOpts) ToContainerListParams() (bool, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return false, "", err
+	}
+	return opts.Full, q.String(), nil
+}
 
-	url := accountURL(c)
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+
+	url := listURL(c)
 	if opts != nil {
-		query, err := gophercloud.BuildQueryString(opts)
+		full, query, err := opts.ToContainerListParams()
 		if err != nil {
 			return pagination.Pager{Err: err}
 		}
-		url += query.String()
+		url += query
 
-		if !opts.Full {
-			headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+		if full {
+			headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
 		}
-	} else {
-		headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
 	}
 
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
@@ -48,6 +63,12 @@
 	return pager
 }
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToContainerCreateMap() (map[string]string, error)
+}
+
 // CreateOpts is a structure that holds parameters for creating a container.
 type CreateOpts struct {
 	Metadata          map[string]string
@@ -61,13 +82,25 @@
 	VersionsLocation  string `h:"X-Versions-Location"`
 }
 
+// ToContainerCreateMap formats a CreateOpts into a map of headers.
+func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
 // Create is a function that creates a new container.
-func Create(c *gophercloud.ServiceClient, containerName string, opts *CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
 	h := c.Provider.AuthenticatedHeaders()
 
 	if opts != nil {
-		headers, err := gophercloud.BuildHeaders(opts)
+		headers, err := opts.ToContainerCreateMap()
 		if err != nil {
 			res.Err = err
 			return res
@@ -76,13 +109,9 @@
 		for k, v := range headers {
 			h[k] = v
 		}
-
-		for k, v := range opts.Metadata {
-			h["X-Container-Meta-"+k] = v
-		}
 	}
 
-	resp, err := perigee.Request("PUT", containerURL(c, containerName), perigee.Options{
+	resp, err := perigee.Request("PUT", createURL(c, containerName), perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{201, 204},
 	})
@@ -94,7 +123,7 @@
 // Delete is a function that deletes a container.
 func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult {
 	var res DeleteResult
-	resp, err := perigee.Request("DELETE", containerURL(c, containerName), perigee.Options{
+	resp, err := perigee.Request("DELETE", deleteURL(c, containerName), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
@@ -103,8 +132,14 @@
 	return res
 }
 
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
-// container's metadata.
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToContainerUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or
+// deleting a container's metadata.
 type UpdateOpts struct {
 	Metadata               map[string]string
 	ContainerRead          string `h:"X-Container-Read"`
@@ -117,13 +152,26 @@
 	VersionsLocation       string `h:"X-Versions-Location"`
 }
 
-// Update is a function that creates, updates, or deletes a container's metadata.
-func Update(c *gophercloud.ServiceClient, containerName string, opts *UpdateOpts) UpdateResult {
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) UpdateResult {
 	var res UpdateResult
 	h := c.Provider.AuthenticatedHeaders()
 
 	if opts != nil {
-		headers, err := gophercloud.BuildHeaders(opts)
+		headers, err := opts.ToContainerUpdateMap()
 		if err != nil {
 			res.Err = err
 			return res
@@ -132,13 +180,9 @@
 		for k, v := range headers {
 			h[k] = v
 		}
-
-		for k, v := range opts.Metadata {
-			h["X-Container-Meta-"+k] = v
-		}
 	}
 
-	resp, err := perigee.Request("POST", containerURL(c, containerName), perigee.Options{
+	resp, err := perigee.Request("POST", updateURL(c, containerName), perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{204},
 	})
@@ -147,11 +191,12 @@
 	return res
 }
 
-// Get is a function that retrieves the metadata of a container. To extract just the custom
-// metadata, pass the GetResult response to the ExtractMetadata function.
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
 func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
 	var res GetResult
-	resp, err := perigee.Request("HEAD", containerURL(c, containerName), perigee.Options{
+	resp, err := perigee.Request("HEAD", getURL(c, containerName), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
diff --git a/openstack/objectstorage/v1/containers/requests_test.go b/openstack/objectstorage/v1/containers/requests_test.go
index 09930d0..9562676 100644
--- a/openstack/objectstorage/v1/containers/requests_test.go
+++ b/openstack/objectstorage/v1/containers/requests_test.go
@@ -6,20 +6,20 @@
 	"testing"
 
 	"github.com/rackspace/gophercloud/pagination"
-	"github.com/rackspace/gophercloud/testhelper"
+	th "github.com/rackspace/gophercloud/testhelper"
 	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
 var metadata = map[string]string{"gophercloud-test": "containers"}
 
 func TestListContainerInfo(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
 
 		w.Header().Set("Content-Type", "application/json")
 		r.ParseForm()
@@ -68,7 +68,7 @@
 			},
 		}
 
-		testhelper.CheckDeepEquals(t, expected, actual)
+		th.CheckDeepEquals(t, expected, actual)
 
 		return true, nil
 	})
@@ -79,13 +79,13 @@
 }
 
 func TestListContainerNames(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "text/plain")
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "text/plain")
 
 		w.Header().Set("Content-Type", "text/plain")
 		r.ParseForm()
@@ -112,7 +112,7 @@
 
 		expected := []string{"janeausten", "marktwain"}
 
-		testhelper.CheckDeepEquals(t, expected, actual)
+		th.CheckDeepEquals(t, expected, actual)
 
 		return true, nil
 	})
@@ -123,30 +123,34 @@
 }
 
 func TestCreateContainer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "PUT")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Add("X-Container-Meta-Foo", "bar")
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	_, err := Create(fake.ServiceClient(), "testContainer", nil).ExtractHeaders()
+	options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+	headers, err := Create(fake.ServiceClient(), "testContainer", options).ExtractHeaders()
 	if err != nil {
 		t.Fatalf("Unexpected error creating container: %v", err)
 	}
+	th.CheckEquals(t, "bar", headers["X-Container-Meta-Foo"][0])
 }
 
 func TestDeleteContainer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "DELETE")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
 		w.WriteHeader(http.StatusNoContent)
 	})
 
@@ -157,30 +161,31 @@
 }
 
 func TestUpateContainer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	_, err := Update(fake.ServiceClient(), "testContainer", nil).ExtractHeaders()
+	options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+	_, err := Update(fake.ServiceClient(), "testContainer", options).ExtractHeaders()
 	if err != nil {
 		t.Fatalf("Unexpected error updating container metadata: %v", err)
 	}
 }
 
 func TestGetContainer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "HEAD")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
 		w.WriteHeader(http.StatusNoContent)
 	})
 
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
index be96fca..227a9dc 100644
--- a/openstack/objectstorage/v1/containers/results.go
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -2,11 +2,12 @@
 
 import (
 	"fmt"
-	"net/http"
 	"strings"
 
-	"github.com/mitchellh/mapstructure"
+	objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
 	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
 )
 
 // Container represents a container resource.
@@ -97,8 +98,7 @@
 
 // GetResult represents the result of a get operation.
 type GetResult struct {
-	Resp *http.Response
-	Err  error
+	objectstorage.CommonResult
 }
 
 // ExtractMetadata is a function that takes a GetResult (of type *http.Response)
@@ -117,37 +117,23 @@
 	return metadata, nil
 }
 
-type commonResult struct {
-	Resp *http.Response
-	Err  error
-}
-
-func (cr commonResult) ExtractHeaders() (http.Header, error) {
-	var headers http.Header
-	if cr.Err != nil {
-		return headers, cr.Err
-	}
-
-	return cr.Resp.Header, nil
-}
-
 // CreateResult represents the result of a create operation. To extract the
 // the headers from the HTTP response, you can invoke the 'ExtractHeaders'
 // method on the result struct.
 type CreateResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // UpdateResult represents the result of an update operation. To extract the
 // the headers from the HTTP response, you can invoke the 'ExtractHeaders'
 // method on the result struct.
 type UpdateResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // DeleteResult represents the result of a delete operation. To extract the
 // the headers from the HTTP response, you can invoke the 'ExtractHeaders'
 // method on the result struct.
 type DeleteResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
diff --git a/openstack/objectstorage/v1/containers/urls.go b/openstack/objectstorage/v1/containers/urls.go
index 2a06f95..f864f84 100644
--- a/openstack/objectstorage/v1/containers/urls.go
+++ b/openstack/objectstorage/v1/containers/urls.go
@@ -2,12 +2,22 @@
 
 import "github.com/rackspace/gophercloud"
 
-// accountURL returns the URI used to list Containers.
-func accountURL(c *gophercloud.ServiceClient) string {
+func listURL(c *gophercloud.ServiceClient) string {
 	return c.Endpoint
 }
 
-// containerURL returns the URI for making Container requests.
-func containerURL(c *gophercloud.ServiceClient, container string) string {
+func createURL(c *gophercloud.ServiceClient, container string) string {
 	return c.ServiceURL(container)
 }
+
+func getURL(c *gophercloud.ServiceClient, container string) string {
+	return createURL(c, container)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container string) string {
+	return createURL(c, container)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container string) string {
+	return createURL(c, container)
+}
diff --git a/openstack/objectstorage/v1/containers/urls_test.go b/openstack/objectstorage/v1/containers/urls_test.go
index da37bf6..d043a2a 100644
--- a/openstack/objectstorage/v1/containers/urls_test.go
+++ b/openstack/objectstorage/v1/containers/urls_test.go
@@ -1,29 +1,43 @@
 package containers
 
 import (
-	"testing"
 	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"testing"
 )
 
-func TestAccountURL(t *testing.T) {
-	client := gophercloud.ServiceClient{
-		Endpoint: "http://localhost:5000/v1/",
-	}
-	expected := "http://localhost:5000/v1/"
-	actual := accountURL(&client)
-	if actual != expected {
-		t.Errorf("Unexpected service URL generated: [%s]", actual)
-	}
+const endpoint = "http://localhost:57909/"
 
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
 }
 
-func TestContainerURL(t *testing.T) {
-	client := gophercloud.ServiceClient{
-		Endpoint: "http://localhost:5000/v1/",
-	}
-	expected := "http://localhost:5000/v1/testContainer"
-	actual := containerURL(&client, "testContainer")
-	if actual != expected {
-		t.Errorf("Unexpected service URL generated: [%s]", actual)
-	}
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
 }
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index bc21496..3274e04 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -10,38 +10,52 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToObjectListParams() (bool, string, error)
+}
+
 // ListOpts is a structure that holds parameters for listing objects.
 type ListOpts struct {
 	Full      bool
-	Limit     int     `q:"limit"`
-	Marker    string  `q:"marker"`
-	EndMarker string  `q:"end_marker"`
-	Format    string  `q:"format"`
-	Prefix    string  `q:"prefix"`
-	Delimiter [1]byte `q:"delimiter"`
-	Path      string  `q:"path"`
+	Limit     int    `q:"limit"`
+	Marker    string `q:"marker"`
+	EndMarker string `q:"end_marker"`
+	Format    string `q:"format"`
+	Prefix    string `q:"prefix"`
+	Delimiter string `q:"delimiter"`
+	Path      string `q:"path"`
+}
+
+// ToObjectListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each object.
+func (opts ListOpts) ToObjectListParams() (bool, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return false, "", err
+	}
+	return opts.Full, q.String(), nil
 }
 
 // List is a function that retrieves all objects in a container. It also returns the details
 // for the container. To extract only the object information or names, pass the ListResult
 // response to the ExtractInfo or ExtractNames function, respectively.
-func List(c *gophercloud.ServiceClient, containerName string, opts *ListOpts) pagination.Pager {
-	var headers map[string]string
+func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager {
+	headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
 
-	url := containerURL(c, containerName)
+	url := listURL(c, containerName)
 	if opts != nil {
-		query, err := gophercloud.BuildQueryString(opts)
+		full, query, err := opts.ToObjectListParams()
 		if err != nil {
 			fmt.Printf("Error building query string: %v", err)
 			return pagination.Pager{Err: err}
 		}
-		url += query.String()
+		url += query
 
-		if !opts.Full {
-			headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+		if full {
+			headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
 		}
-	} else {
-		headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
 	}
 
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
@@ -55,6 +69,12 @@
 	return pager
 }
 
+// DownloadOptsBuilder allows extensions to add additional parameters to the
+// Download request.
+type DownloadOptsBuilder interface {
+	ToObjectDownloadParams() (map[string]string, string, error)
+}
+
 // DownloadOpts is a structure that holds parameters for downloading an object.
 type DownloadOpts struct {
 	IfMatch           string    `h:"If-Match"`
@@ -67,17 +87,31 @@
 	Signature         string    `q:"signature"`
 }
 
+// ToObjectDownloadParams formats a DownloadOpts into a query string and map of
+// headers.
+func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return nil, "", err
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, q.String(), err
+	}
+	return h, q.String(), nil
+}
+
 // Download is a function that retrieves the content and metadata for an object.
-// To extract just the content, pass the DownloadResult response to the ExtractContent
-// function.
-func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts *DownloadOpts) DownloadResult {
+// To extract just the content, pass the DownloadResult response to the
+// ExtractContent function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult {
 	var res DownloadResult
 
-	url := objectURL(c, containerName, objectName)
+	url := downloadURL(c, containerName, objectName)
 	h := c.Provider.AuthenticatedHeaders()
 
 	if opts != nil {
-		headers, err := gophercloud.BuildHeaders(opts)
+		headers, query, err := opts.ToObjectDownloadParams()
 		if err != nil {
 			res.Err = err
 			return res
@@ -87,12 +121,7 @@
 			h[k] = v
 		}
 
-		query, err := gophercloud.BuildQueryString(opts)
-		if err != nil {
-			res.Err = err
-			return res
-		}
-		url += query.String()
+		url += query
 	}
 
 	resp, err := perigee.Request("GET", url, perigee.Options{
@@ -104,6 +133,12 @@
 	return res
 }
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToObjectCreateParams() (map[string]string, string, error)
+}
+
 // CreateOpts is a structure that holds parameters for creating an object.
 type CreateOpts struct {
 	Metadata           map[string]string
@@ -124,16 +159,35 @@
 	Signature          string `q:"signature"`
 }
 
+// ToObjectCreateParams formats a CreateOpts into a query string and map of
+// headers.
+func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return nil, "", err
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, q.String(), err
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	return h, q.String(), nil
+}
+
 // Create is a function that creates a new object or replaces an existing object.
-func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts *CreateOpts) CreateResult {
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
 	var reqBody []byte
 
-	url := objectURL(c, containerName, objectName)
+	url := createURL(c, containerName, objectName)
 	h := c.Provider.AuthenticatedHeaders()
 
 	if opts != nil {
-		headers, err := gophercloud.BuildHeaders(opts)
+		headers, query, err := opts.ToObjectCreateParams()
 		if err != nil {
 			res.Err = err
 			return res
@@ -143,17 +197,7 @@
 			h[k] = v
 		}
 
-		for k, v := range opts.Metadata {
-			h["X-Object-Meta-"+k] = v
-		}
-
-		query, err := gophercloud.BuildQueryString(opts)
-		if err != nil {
-			res.Err = err
-			return res
-		}
-
-		url += query.String()
+		url += query
 	}
 
 	if content != nil {
@@ -175,7 +219,14 @@
 	return res
 }
 
-// CopyOpts is a structure that holds parameters for copying one object to another.
+// CopyOptsBuilder allows extensions to add additional parameters to the
+// Copy request.
+type CopyOptsBuilder interface {
+	ToObjectCopyMap() (map[string]string, error)
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to
+// another.
 type CopyOpts struct {
 	Metadata           map[string]string
 	ContentDisposition string `h:"Content-Disposition"`
@@ -184,29 +235,37 @@
 	Destination        string `h:"Destination,required"`
 }
 
+// ToObjectCopyMap formats a CopyOpts into a map of headers.
+func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
+	if opts.Destination == "" {
+		return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.")
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+	return h, nil
+}
+
 // Copy is a function that copies one object to another.
-func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts *CopyOpts) CopyResult {
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) CopyResult {
 	var res CopyResult
 	h := c.Provider.AuthenticatedHeaders()
 
-	if opts == nil {
-		res.Err = fmt.Errorf("Required CopyOpts field 'Destination' not set.")
-		return res
-	}
-	headers, err := gophercloud.BuildHeaders(opts)
+	headers, err := opts.ToObjectCopyMap()
 	if err != nil {
 		res.Err = err
 		return res
 	}
+
 	for k, v := range headers {
 		h[k] = v
 	}
 
-	for k, v := range opts.Metadata {
-		h["X-Object-Meta-"+k] = v
-	}
-
-	url := objectURL(c, containerName, objectName)
+	url := copyURL(c, containerName, objectName)
 	resp, err := perigee.Request("COPY", url, perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{201},
@@ -215,23 +274,38 @@
 	return res
 }
 
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+	ToObjectDeleteQuery() (string, error)
+}
+
 // DeleteOpts is a structure that holds parameters for deleting an object.
 type DeleteOpts struct {
 	MultipartManifest string `q:"multipart-manifest"`
 }
 
+// ToObjectDeleteQuery formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // Delete is a function that deletes an object.
-func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts *DeleteOpts) DeleteResult {
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult {
 	var res DeleteResult
-	url := objectURL(c, containerName, objectName)
+	url := deleteURL(c, containerName, objectName)
 
 	if opts != nil {
-		query, err := gophercloud.BuildQueryString(opts)
+		query, err := opts.ToObjectDeleteQuery()
 		if err != nil {
 			res.Err = err
 			return res
 		}
-		url += query.String()
+		url += query
 	}
 
 	resp, err := perigee.Request("DELETE", url, perigee.Options{
@@ -243,25 +317,40 @@
 	return res
 }
 
+// GetOptsBuilder allows extensions to add additional parameters to the
+// Get request.
+type GetOptsBuilder interface {
+	ToObjectGetQuery() (string, error)
+}
+
 // GetOpts is a structure that holds parameters for getting an object's metadata.
 type GetOpts struct {
 	Expires   string `q:"expires"`
 	Signature string `q:"signature"`
 }
 
+// ToObjectGetQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToObjectGetQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // Get is a function that retrieves the metadata of an object. To extract just the custom
 // metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts *GetOpts) GetResult {
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult {
 	var res GetResult
-	url := objectURL(c, containerName, objectName)
+	url := getURL(c, containerName, objectName)
 
 	if opts != nil {
-		query, err := gophercloud.BuildQueryString(opts)
+		query, err := opts.ToObjectGetQuery()
 		if err != nil {
 			res.Err = err
 			return res
 		}
-		url += query.String()
+		url += query
 	}
 
 	resp, err := perigee.Request("HEAD", url, perigee.Options{
@@ -273,6 +362,12 @@
 	return res
 }
 
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToObjectUpdateMap() (map[string]string, error)
+}
+
 // UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
 // object's metadata.
 type UpdateOpts struct {
@@ -285,13 +380,25 @@
 	DetectContentType  bool   `h:"X-Detect-Content-Type"`
 }
 
+// ToObjectUpdateMap formats a UpdateOpts into a map of headers.
+func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+	return h, nil
+}
+
 // Update is a function that creates, updates, or deletes an object's metadata.
-func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts *UpdateOpts) UpdateResult {
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult {
 	var res UpdateResult
 	h := c.Provider.AuthenticatedHeaders()
 
 	if opts != nil {
-		headers, err := gophercloud.BuildHeaders(opts)
+		headers, err := opts.ToObjectUpdateMap()
 		if err != nil {
 			res.Err = err
 			return res
@@ -300,13 +407,9 @@
 		for k, v := range headers {
 			h[k] = v
 		}
-
-		for k, v := range opts.Metadata {
-			h["X-Object-Meta-"+k] = v
-		}
 	}
 
-	url := objectURL(c, containerName, objectName)
+	url := updateURL(c, containerName, objectName)
 	resp, err := perigee.Request("POST", url, perigee.Options{
 		MoreHeaders: h,
 		OkCodes:     []int{202},
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
index 089081f..11d7c44 100644
--- a/openstack/objectstorage/v1/objects/requests_test.go
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -166,7 +166,8 @@
 	})
 
 	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
-	_, err := Create(fake.ServiceClient(), "testContainer", "testObject", content, nil).ExtractHeaders()
+	options := &CreateOpts{ContentType: "application/json"}
+	_, err := Create(fake.ServiceClient(), "testContainer", "testObject", content, options).ExtractHeaders()
 	if err != nil {
 		t.Fatalf("Unexpected error creating object: %v", err)
 	}
@@ -184,7 +185,8 @@
 		w.WriteHeader(http.StatusCreated)
 	})
 
-	_, err := Copy(fake.ServiceClient(), "testContainer", "testObject", &CopyOpts{Destination: "/newTestContainer/newTestObject"}).ExtractHeaders()
+	options := &CopyOpts{Destination: "/newTestContainer/newTestObject"}
+	_, err := Copy(fake.ServiceClient(), "testContainer", "testObject", options).ExtractHeaders()
 	if err != nil {
 		t.Fatalf("Unexpected error copying object: %v", err)
 	}
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
index f7db3ed..1dda7a3 100644
--- a/openstack/objectstorage/v1/objects/results.go
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -3,11 +3,12 @@
 import (
 	"fmt"
 	"io/ioutil"
-	"net/http"
 	"strings"
 
-	"github.com/mitchellh/mapstructure"
+	objectstorage "github.com/rackspace/gophercloud/openstack/objectstorage/v1"
 	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
 )
 
 // Object is a structure that holds information related to a storage object.
@@ -97,7 +98,7 @@
 
 // DownloadResult is a *http.Response that is returned from a call to the Download function.
 type DownloadResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // ExtractContent is a function that takes a DownloadResult (of type *http.Response)
@@ -117,7 +118,7 @@
 
 // GetResult is a *http.Response that is returned from a call to the Get function.
 type GetResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // ExtractMetadata is a function that takes a GetResult (of type *http.Response)
@@ -136,36 +137,22 @@
 	return metadata, nil
 }
 
-type commonResult struct {
-	Resp *http.Response
-	Err  error
-}
-
-func (cr commonResult) ExtractHeaders() (http.Header, error) {
-	var headers http.Header
-	if cr.Err != nil {
-		return headers, cr.Err
-	}
-
-	return cr.Resp.Header, nil
-}
-
 // CreateResult represents the result of a create operation.
 type CreateResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // UpdateResult represents the result of an update operation.
 type UpdateResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // DeleteResult represents the result of a delete operation.
 type DeleteResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
 
 // CopyResult represents the result of a copy operation.
 type CopyResult struct {
-	commonResult
+	objectstorage.CommonResult
 }
diff --git a/openstack/objectstorage/v1/objects/urls.go b/openstack/objectstorage/v1/objects/urls.go
index a377960..d2ec62c 100644
--- a/openstack/objectstorage/v1/objects/urls.go
+++ b/openstack/objectstorage/v1/objects/urls.go
@@ -1,13 +1,33 @@
 package objects
 
-import "github.com/rackspace/gophercloud"
+import (
+	"github.com/rackspace/gophercloud"
+)
 
-// objectURL returns the URI for making Object requests.
-func objectURL(c *gophercloud.ServiceClient, container, object string) string {
+func listURL(c *gophercloud.ServiceClient, container string) string {
+	return c.ServiceURL(container)
+}
+
+func copyURL(c *gophercloud.ServiceClient, container, object string) string {
 	return c.ServiceURL(container, object)
 }
 
-// containerURL returns the URI for making Container requests.
-func containerURL(c *gophercloud.ServiceClient, container string) string {
-	return c.ServiceURL(container)
+func createURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func getURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func downloadURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
 }
diff --git a/openstack/objectstorage/v1/objects/urls_test.go b/openstack/objectstorage/v1/objects/urls_test.go
index 89d1cb1..1dcfe35 100644
--- a/openstack/objectstorage/v1/objects/urls_test.go
+++ b/openstack/objectstorage/v1/objects/urls_test.go
@@ -2,27 +2,55 @@
 
 import (
 	"testing"
+
 	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-func TestContainerURL(t *testing.T) {
-	client := gophercloud.ServiceClient{
-		Endpoint: "http://localhost:5000/v1/",
-	}
-	expected := "http://localhost:5000/v1/testContainer"
-	actual := containerURL(&client, "testContainer")
-	if actual != expected {
-		t.Errorf("Unexpected service URL generated: %s", actual)
-	}
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
 }
 
-func TestObjectURL(t *testing.T) {
-	client := gophercloud.ServiceClient{
-		Endpoint: "http://localhost:5000/v1/",
-	}
-	expected := "http://localhost:5000/v1/testContainer/testObject"
-	actual := objectURL(&client, "testContainer", "testObject")
-	if actual != expected {
-		t.Errorf("Unexpected service URL generated: %s", actual)
-	}
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCopyURL(t *testing.T) {
+	actual := copyURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDownloadURL(t *testing.T) {
+	actual := downloadURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
 }
diff --git a/pagination/pager.go b/pagination/pager.go
index 22d6d84..75fe408 100644
--- a/pagination/pager.go
+++ b/pagination/pager.go
@@ -29,10 +29,10 @@
 
 // Pager knows how to advance through a specific resource collection, one page at a time.
 type Pager struct {
-	initialURL string
-
 	client *gophercloud.ServiceClient
 
+	initialURL string
+
 	createPage func(r LastHTTPResponse) Page
 
 	Err error
@@ -45,8 +45,18 @@
 // Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
 func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r LastHTTPResponse) Page) Pager {
 	return Pager{
-		initialURL: initialURL,
 		client:     client,
+		initialURL: initialURL,
+		createPage: createPage,
+	}
+}
+
+// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
+// useful for overriding List functions in delegation.
+func (p Pager) WithPageCreator(createPage func(r LastHTTPResponse) Page) Pager {
+	return Pager{
+		client:     p.client,
+		initialURL: p.initialURL,
 		createPage: createPage,
 	}
 }
diff --git a/params_test.go b/params_test.go
index 03eaefc..9f1d3bd 100644
--- a/params_test.go
+++ b/params_test.go
@@ -2,58 +2,141 @@
 
 import (
 	"net/url"
+	"reflect"
 	"testing"
+	"time"
 
 	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-func TestMaybeStringWithNonEmptyString(t *testing.T) {
-	testString := "carol"
-	expected := &testString
-	actual := MaybeString("carol")
-	th.CheckDeepEquals(t, actual, expected)
-}
-
-func TestMaybeStringWithEmptyString(t *testing.T) {
+func TestMaybeString(t *testing.T) {
+	testString := ""
 	var expected *string
-	actual := MaybeString("")
-	th.CheckDeepEquals(t, actual, expected)
+	actual := MaybeString(testString)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testString = "carol"
+	expected = &testString
+	actual = MaybeString(testString)
+	th.CheckDeepEquals(t, expected, actual)
 }
 
-func TestBuildQueryStringWithPointerToStruct(t *testing.T) {
-	expected := &url.URL{
-		RawQuery: "j=2&r=red",
-	}
+func TestMaybeInt(t *testing.T) {
+	testInt := 0
+	var expected *int
+	actual := MaybeInt(testInt)
+	th.CheckDeepEquals(t, expected, actual)
 
-	type Opts struct {
+	testInt = 4
+	expected = &testInt
+	actual = MaybeInt(testInt)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBuildQueryString(t *testing.T) {
+	opts := struct {
 		J int    `q:"j"`
-		R string `q:"r"`
-		C bool
+		R string `q:"r,required"`
+		C bool   `q:"c"`
+	}{
+		J: 2,
+		R: "red",
+		C: true,
 	}
-
-	opts := Opts{J: 2, R: "red"}
-
+	expected := &url.URL{RawQuery: "j=2&r=red&c=true"}
 	actual, err := BuildQueryString(&opts)
 	if err != nil {
 		t.Errorf("Error building query string: %v", err)
 	}
+	th.CheckDeepEquals(t, expected, actual)
 
-	th.CheckDeepEquals(t, actual, expected)
-}
-
-func TestBuildQueryStringWithoutRequiredFieldSet(t *testing.T) {
-	type Opts struct {
+	opts = struct {
 		J int    `q:"j"`
 		R string `q:"r,required"`
-		C bool
+		C bool   `q:"c"`
+	}{
+		J: 2,
+		C: true,
 	}
-
-	opts := Opts{J: 2, C: true}
-
-	_, err := BuildQueryString(&opts)
+	_, err = BuildQueryString(&opts)
 	if err == nil {
-		t.Error("Unexpected result: There should be an error thrown when a required field isn't set.")
+		t.Errorf("Expected error: 'Required field not set'")
+	}
+	th.CheckDeepEquals(t, expected, actual)
+
+	_, err = BuildQueryString(map[string]interface{}{"Number": 4})
+	if err == nil {
+		t.Errorf("Expected error: 'Options type is not a struct'")
+	}
+}
+
+func TestBuildHeaders(t *testing.T) {
+	testStruct := struct {
+		Accept string `h:"Accept"`
+		Num    int    `h:"Number,required"`
+		Style  bool   `h:"Style"`
+	}{
+		Accept: "application/json",
+		Num:    4,
+		Style:  true,
+	}
+	expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"}
+	actual, err := BuildHeaders(&testStruct)
+	th.CheckNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testStruct.Num = 0
+	_, err = BuildHeaders(&testStruct)
+	if err == nil {
+		t.Errorf("Expected error: 'Required header not set'")
 	}
 
-	t.Log(err)
+	_, err = BuildHeaders(map[string]interface{}{"Number": 4})
+	if err == nil {
+		t.Errorf("Expected error: 'Options type is not a struct'")
+	}
+}
+
+func TestIsZero(t *testing.T) {
+	var testMap map[string]interface{}
+	testMapValue := reflect.ValueOf(testMap)
+	expected := true
+	actual := isZero(testMapValue)
+	th.CheckEquals(t, expected, actual)
+	testMap = map[string]interface{}{"empty": false}
+	testMapValue = reflect.ValueOf(testMap)
+	expected = false
+	actual = isZero(testMapValue)
+	th.CheckEquals(t, expected, actual)
+
+	var testArray [2]string
+	testArrayValue := reflect.ValueOf(testArray)
+	expected = true
+	actual = isZero(testArrayValue)
+	th.CheckEquals(t, expected, actual)
+	testArray = [2]string{"one", "two"}
+	testArrayValue = reflect.ValueOf(testArray)
+	expected = false
+	actual = isZero(testArrayValue)
+	th.CheckEquals(t, expected, actual)
+
+	var testStruct struct {
+		A string
+		B time.Time
+	}
+	testStructValue := reflect.ValueOf(testStruct)
+	expected = true
+	actual = isZero(testStructValue)
+	th.CheckEquals(t, expected, actual)
+	testStruct = struct {
+		A string
+		B time.Time
+	}{
+		B: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
+	}
+	testStructValue = reflect.ValueOf(testStruct)
+	expected = false
+	actual = isZero(testStructValue)
+	th.CheckEquals(t, expected, actual)
+
 }
diff --git a/provider_client_test.go b/provider_client_test.go
new file mode 100644
index 0000000..b260246
--- /dev/null
+++ b/provider_client_test.go
@@ -0,0 +1,16 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedHeaders(t *testing.T) {
+	p := &ProviderClient{
+		TokenID: "1234",
+	}
+	expected := map[string]string{"X-Auth-Token": "1234"}
+	actual := p.AuthenticatedHeaders()
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/client.go b/rackspace/client.go
new file mode 100644
index 0000000..d8c4a19
--- /dev/null
+++ b/rackspace/client.go
@@ -0,0 +1,115 @@
+package rackspace
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/utils"
+	tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
+)
+
+const (
+	// RackspaceUSIdentity is an identity endpoint located in the United States.
+	RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/"
+
+	// RackspaceUKIdentity is an identity endpoint located in the UK.
+	RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/"
+)
+
+const (
+	v20 = "v2.0"
+)
+
+// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not
+// yet authenticated. Most users will probably prefer using the AuthenticatedClient function
+// instead.
+//
+// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint".
+// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity.
+func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+	if endpoint == "" {
+		return os.NewClient(RackspaceUSIdentity)
+	}
+	return os.NewClient(endpoint)
+}
+
+// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a
+// ProviderClient that's ready to operate.
+//
+// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to
+// the canonical, production Rackspace US identity endpoint.
+func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
+	client, err := NewClient(options.IdentityEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	err = Authenticate(client, options)
+	if err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+// Authenticate or re-authenticate against the most recent identity service supported at the
+// provided endpoint.
+func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	versions := []*utils.Version{
+		&utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+	}
+
+	chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
+	if err != nil {
+		return err
+	}
+
+	switch chosen.ID {
+	case v20:
+		return v2auth(client, endpoint, options)
+	default:
+		// The switch statement must be out of date from the versions list.
+		return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
+	}
+}
+
+// AuthenticateV2 explicitly authenticates with v2 of the identity service.
+func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	return v2auth(client, "", options)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+	v2Client := NewIdentityV2(client)
+	if endpoint != "" {
+		v2Client.Endpoint = endpoint
+	}
+
+	result := tokens2.Create(v2Client, tokens2.WrapOptions(options))
+
+	token, err := result.ExtractToken()
+	if err != nil {
+		return err
+	}
+
+	catalog, err := result.ExtractServiceCatalog()
+	if err != nil {
+		return err
+	}
+
+	client.TokenID = token.ID
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return os.V2EndpointURL(catalog, opts)
+	}
+
+	return nil
+}
+
+// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service.
+func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
+	v2Endpoint := client.IdentityBase + "v2.0/"
+
+	return &gophercloud.ServiceClient{
+		Provider: client,
+		Endpoint: v2Endpoint,
+	}
+}
diff --git a/rackspace/client_test.go b/rackspace/client_test.go
new file mode 100644
index 0000000..73b1c88
--- /dev/null
+++ b/rackspace/client_test.go
@@ -0,0 +1,38 @@
+package rackspace
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClientV2(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `
+      {
+        "access": {
+          "token": {
+            "id": "01234567890",
+            "expires": "2014-10-01T10:00:00.000000Z"
+          },
+          "serviceCatalog": []
+        }
+      }
+    `)
+	})
+
+	options := gophercloud.AuthOptions{
+		Username:         "me",
+		APIKey:           "09876543210",
+		IdentityEndpoint: th.Endpoint() + "v2.0/",
+	}
+	client, err := AuthenticatedClient(options)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "01234567890", client.TokenID)
+}
diff --git a/rackspace/identity/v2/extensions/delegate.go b/rackspace/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..fc547cd
--- /dev/null
+++ b/rackspace/identity/v2/extensions/delegate.go
@@ -0,0 +1,24 @@
+package extensions
+
+import (
+	"github.com/rackspace/gophercloud"
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of os.Extension structs.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+	return common.ExtractExtensions(page)
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+	return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return common.List(c)
+}
diff --git a/rackspace/identity/v2/extensions/delegate_test.go b/rackspace/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..e30f794
--- /dev/null
+++ b/rackspace/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,39 @@
+package extensions
+
+import (
+	"testing"
+
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	common.HandleListExtensionsSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+		th.AssertDeepEquals(t, common.ExpectedExtensions, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	common.HandleGetExtensionSuccessfully(t)
+
+	actual, err := Get(fake.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/rackspace/identity/v2/tenants/delegate.go b/rackspace/identity/v2/tenants/delegate.go
new file mode 100644
index 0000000..6cdd0cf
--- /dev/null
+++ b/rackspace/identity/v2/tenants/delegate.go
@@ -0,0 +1,17 @@
+package tenants
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs.
+func ExtractTenants(page pagination.Page) ([]os.Tenant, error) {
+	return os.ExtractTenants(page)
+}
+
+// List enumerates the tenants to which the current token grants access.
+func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager {
+	return os.List(client, opts)
+}
diff --git a/rackspace/identity/v2/tenants/delegate_test.go b/rackspace/identity/v2/tenants/delegate_test.go
new file mode 100644
index 0000000..eccbfe2
--- /dev/null
+++ b/rackspace/identity/v2/tenants/delegate_test.go
@@ -0,0 +1,28 @@
+package tenants
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListTenantsSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractTenants(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, os.ExpectedTenantSlice, actual)
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
diff --git a/rackspace/identity/v2/tokens/delegate.go b/rackspace/identity/v2/tokens/delegate.go
new file mode 100644
index 0000000..4f9885a
--- /dev/null
+++ b/rackspace/identity/v2/tokens/delegate.go
@@ -0,0 +1,60 @@
+package tokens
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+)
+
+var (
+	// ErrPasswordProvided is returned if both a password and an API key are provided to Create.
+	ErrPasswordProvided = errors.New("Please provide either a password or an API key.")
+)
+
+// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body
+// when API key authentication is used.
+type AuthOptions struct {
+	os.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+	return AuthOptions{AuthOptions: os.WrapOptions(original)}
+}
+
+// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it
+// will be used, otherwise
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+	if auth.APIKey == "" {
+		return auth.AuthOptions.ToTokenCreateMap()
+	}
+
+	// Verify that other required attributes are present.
+	if auth.Username == "" {
+		return nil, os.ErrUsernameRequired
+	}
+
+	authMap := make(map[string]interface{})
+
+	authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{
+		"username": auth.Username,
+		"apiKey":   auth.APIKey,
+	}
+
+	if auth.TenantID != "" {
+		authMap["tenantId"] = auth.TenantID
+	}
+	if auth.TenantName != "" {
+		authMap["tenantName"] = auth.TenantName
+	}
+
+	return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather
+// than interact with this service directly, users should generally call
+// rackspace.AuthenticatedClient().
+func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult {
+	return os.Create(client, auth)
+}
diff --git a/rackspace/identity/v2/tokens/delegate_test.go b/rackspace/identity/v2/tokens/delegate_test.go
new file mode 100644
index 0000000..6678ff4
--- /dev/null
+++ b/rackspace/identity/v2/tokens/delegate_test.go
@@ -0,0 +1,36 @@
+package tokens
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) os.CreateResult {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleTokenPost(t, requestJSON)
+
+	return Create(client.ServiceClient(), WrapOptions(options))
+}
+
+func TestCreateTokenWithAPIKey(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		APIKey:   "1234567890abcdef",
+	}
+
+	os.IsSuccessful(t, tokenPost(t, options, `
+    {
+      "auth": {
+        "RAX-KSKEY:apiKeyCredentials": {
+          "username": "me",
+          "apiKey": "1234567890abcdef"
+        }
+      }
+    }
+  `))
+}
diff --git a/results.go b/results.go
index f7d526c..647ba46 100644
--- a/results.go
+++ b/results.go
@@ -10,3 +10,28 @@
 
 // RFC3339Milli describes a time format used by API responses.
 const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
+
+// Link represents a structure that enables paginated collections how to
+// traverse backward or forward. The "Rel" field is usually either "next".
+type Link struct {
+	Href string `mapstructure:"href"`
+	Rel  string `mapstructure:"rel"`
+}
+
+// ExtractNextURL attempts to extract the next URL from a JSON structure. It
+// follows the common structure of nesting back and next links.
+func ExtractNextURL(links []Link) (string, error) {
+	var url string
+
+	for _, l := range links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
diff --git a/script/acceptancetest b/script/acceptancetest
new file mode 100755
index 0000000..d8039ae
--- /dev/null
+++ b/script/acceptancetest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the acceptance tests.
+
+exec go test -tags 'acceptance fixtures' ./acceptance/... $@
diff --git a/scripts/create-environment.sh b/script/bootstrap
old mode 100644
new mode 100755
similarity index 100%
rename from scripts/create-environment.sh
rename to script/bootstrap
diff --git a/script/cibuild b/script/cibuild
new file mode 100755
index 0000000..1cb389e
--- /dev/null
+++ b/script/cibuild
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Test script to be invoked by Travis.
+
+exec script/unittest -v
diff --git a/script/test b/script/test
new file mode 100755
index 0000000..1e03dff
--- /dev/null
+++ b/script/test
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run all the tests.
+
+exec go test -tags 'acceptance fixtures' ./... $@
diff --git a/script/unittest b/script/unittest
new file mode 100755
index 0000000..d3440a9
--- /dev/null
+++ b/script/unittest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the unit tests.
+
+exec go test -tags fixtures ./... $@
diff --git a/scripts/test-all.sh b/scripts/test-all.sh
deleted file mode 100755
index 096736f..0000000
--- a/scripts/test-all.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-#
-# This script is responsible for executing all the acceptance tests found in
-# the acceptance/ directory.
-
-# Find where _this_ script is running from.
-SCRIPTS=$(dirname $0)
-SCRIPTS=$(cd $SCRIPTS; pwd)
-
-# Locate the acceptance test / examples directory.
-ACCEPTANCE=$(cd $SCRIPTS/../acceptance; pwd)
-
-# Go workspace path
-WS=$(cd $SCRIPTS/..; pwd)
-
-# In order to run Go code interactively, we need the GOPATH environment
-# to be set.
-if [ "x$GOPATH" == "x" ]; then
-  export GOPATH=$WS
-  echo "WARNING: You didn't have your GOPATH environment variable set."
-  echo "         I'm assuming $GOPATH as its value."
-fi
-
-# Run all acceptance tests sequentially.
-# If any test fails, we fail fast.
-LIBS=$(ls $ACCEPTANCE/lib*.go)
-for T in $(ls -1 $ACCEPTANCE/[0-9][0-9]*.go); do
-  if ! [ -x $T ]; then
-    CMD="go run $T $LIBS -quiet"
-    echo "$CMD ..."
-    if ! $CMD ; then
-      echo "- FAILED.  Try re-running w/out the -quiet option to see output."
-      exit 1
-    fi
-  fi
-done
-
diff --git a/service_client_test.go b/service_client_test.go
new file mode 100644
index 0000000..84beb3f
--- /dev/null
+++ b/service_client_test.go
@@ -0,0 +1,14 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceURL(t *testing.T) {
+	c := &ServiceClient{Endpoint: "http://123.45.67.8/"}
+	expected := "http://123.45.67.8/more/parts/here"
+	actual := c.ServiceURL("more", "parts", "here")
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/util.go b/util.go
index c424759..1715458 100644
--- a/util.go
+++ b/util.go
@@ -2,6 +2,7 @@
 
 import (
 	"fmt"
+	"strings"
 	"time"
 )
 
@@ -20,3 +21,11 @@
 	}
 	return fmt.Errorf("Time out in WaitFor.")
 }
+
+// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+func NormalizeURL(url string) string {
+	if !strings.HasSuffix(url, "/") {
+		return url + "/"
+	}
+	return url
+}
diff --git a/util_test.go b/util_test.go
new file mode 100644
index 0000000..6fbd920
--- /dev/null
+++ b/util_test.go
@@ -0,0 +1,21 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitFor(t *testing.T) {
+	err := WaitFor(0, func() (bool, error) {
+		return true, nil
+	})
+	if err == nil {
+		t.Errorf("Expected error: 'Time out in WaitFor'")
+	}
+
+	err = WaitFor(5, func() (bool, error) {
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}