Merge pull request #219 from smashwilson/update-identity-v2

Update Identity v2
diff --git a/.travis.yml b/.travis.yml
index b7f8d34..9c37aef 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,6 @@
 language: go
 install:
-  - go get -v .
-  - go get -v ./openstack/...
-  - go get -v ./rackspace/...
+  - go get -v ./...
 go:
   - 1.1
   - 1.2
@@ -13,4 +11,3 @@
   - go get github.com/mattn/goveralls
   - export PATH=$PATH:$HOME/gopath/bin/
   - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
-
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
new file mode 100644
index 0000000..2b4e062
--- /dev/null
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	extensions2 "github.com/rackspace/gophercloud/openstack/identity/v2/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnumerateExtensions(t *testing.T) {
+	service := authenticatedClient(t)
+
+	t.Logf("Extensions available on this identity endpoint:")
+	count := 0
+	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 {
+			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)
+}
+
+func TestGetExtension(t *testing.T) {
+	service := authenticatedClient(t)
+
+	ext, err := extensions2.Get(service, "OS-KSCRUD").Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckEquals(t, "OpenStack Keystone User CRUD", ext.Name)
+	th.CheckEquals(t, "http://docs.openstack.org/identity/api/ext/OS-KSCRUD/v1.0", ext.Namespace)
+	th.CheckEquals(t, "OS-KSCRUD", ext.Alias)
+	th.CheckEquals(t, "OpenStack extensions to Keystone v2.0 API enabling User Operations.", ext.Description)
+}
diff --git a/acceptance/openstack/identity/v2/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
index ff4c9cd..2ecd3ca 100644
--- a/acceptance/openstack/identity/v2/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -3,112 +3,46 @@
 package v2
 
 import (
-	"fmt"
-	"os"
 	"testing"
-	"text/tabwriter"
 
 	"github.com/rackspace/gophercloud"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
+	"github.com/rackspace/gophercloud/openstack"
 	"github.com/rackspace/gophercloud/openstack/utils"
+	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-type extractor func(*identity.Token) string
-
-func TestAuthentication(t *testing.T) {
-	// Create an initialized set of authentication options based on available OS_*
-	// environment variables.
+func v2AuthOptions(t *testing.T) gophercloud.AuthOptions {
+	// Obtain credentials from the environment.
 	ao, err := utils.AuthOptions()
-	if err != nil {
-		t.Error(err)
-		return
+	th.AssertNoErr(t, err)
+
+	// Trim out unused fields. Prefer authentication by API key to password.
+	ao.UserID, ao.DomainID, ao.DomainName = "", "", ""
+	if ao.APIKey != "" {
+		ao.Password = ""
 	}
 
-	// Attempt to authenticate with them.
-	client := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
-	r, err := identity.Authenticate(client, ao)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	// We're authenticated; now let's grab our authentication token.
-	tok, err := identity.GetToken(r)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	// Authentication tokens have a variety of fields which might be of some interest.
-	// Let's print a few of them out.
-	table := map[string]extractor{
-		"ID":      func(t *identity.Token) string { return tok.ID },
-		"Expires": func(t *identity.Token) string { return tok.Expires },
-	}
-
-	for attr, fn := range table {
-		fmt.Printf("Your token's %s is %s\n", attr, fn(tok))
-	}
-
-	// With each authentication, you receive a master directory of all the services
-	// your account can access.  This "service catalog", as OpenStack calls it,
-	// provides you the means to exploit other OpenStack services.
-	sc, err := identity.GetServiceCatalog(r)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	// Prepare our elastic tabstopped writer for our table.
-	w := new(tabwriter.Writer)
-	w.Init(os.Stdout, 2, 8, 2, ' ', 0)
-
-	// Different providers will provide different services.  Let's print them
-	// in summary.
-	ces, err := sc.CatalogEntries()
-	fmt.Println("Service Catalog Summary:")
-	fmt.Fprintln(w, "Name\tType\t")
-	for _, ce := range ces {
-		fmt.Fprintf(w, "%s\t%s\t\n", ce.Name, ce.Type)
-	}
-	w.Flush()
-
-	// Now let's print them in greater detail.
-	for _, ce := range ces {
-		fmt.Printf("Endpoints for %s/%s\n", ce.Name, ce.Type)
-		fmt.Fprintln(w, "Version\tRegion\tTenant\tPublic URL\tInternal URL\t")
-		for _, ep := range ce.Endpoints {
-			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t\n", ep.VersionID, ep.Region, ep.TenantID, ep.PublicURL, ep.InternalURL)
-		}
-		w.Flush()
-	}
+	return ao
 }
 
-func TestExtensions(t *testing.T) {
-	// Create an initialized set of authentication options based on available OS_*
-	// environment variables.
-	ao, err := utils.AuthOptions()
-	if err != nil {
-		t.Error(err)
-		return
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+	ao := v2AuthOptions(t)
+
+	provider, err := openstack.NewClient(ao.IdentityEndpoint)
+	th.AssertNoErr(t, err)
+
+	if auth {
+		err = openstack.AuthenticateV2(provider, ao)
+		th.AssertNoErr(t, err)
 	}
 
-	// Attempt to query extensions.
-	client := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
-	exts, err := identity.GetExtensions(client, ao)
-	if err != nil {
-		t.Error(err)
-		return
-	}
+	return openstack.NewIdentityV2(provider)
+}
 
-	// Print out a summary of supported extensions
-	aliases, err := exts.Aliases()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	fmt.Println("Extension Aliases:")
-	for _, alias := range aliases {
-		fmt.Printf("  %s\n", alias)
-	}
+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/openstack/identity/v2/pkg.go b/acceptance/openstack/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/openstack/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
new file mode 100644
index 0000000..2054598
--- /dev/null
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	tenants2 "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnumerateTenants(t *testing.T) {
+	service := authenticatedClient(t)
+
+	t.Logf("Tenants to which your current token grants access:")
+	count := 0
+	err := tenants2.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		tenants, err := tenants2.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+		for i, tenant := range tenants {
+			t.Logf("[%02d] name=[%s] id=[%s] description=[%s] enabled=[%v]",
+				i, tenant.Name, tenant.ID, tenant.Description, tenant.Enabled)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
new file mode 100644
index 0000000..47381a2
--- /dev/null
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -0,0 +1,38 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticate(t *testing.T) {
+	ao := v2AuthOptions(t)
+	service := unauthenticatedClient(t)
+
+	// Authenticated!
+	result := tokens2.Create(service, ao)
+
+	// Extract and print the token.
+	token, err := result.ExtractToken()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Acquired token: [%s]", token.ID)
+	t.Logf("The token will expire at: [%s]", token.ExpiresAt.String())
+	t.Logf("The token is valid for tenant: [%#v]", token.Tenant)
+
+	// Extract and print the service catalog.
+	catalog, err := result.ExtractServiceCatalog()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Acquired service catalog listing [%d] services", len(catalog.Entries))
+	for i, entry := range catalog.Entries {
+		t.Logf("[%02d]: name=[%s], type=[%s]", i, entry.Name, entry.Type)
+		for _, endpoint := range entry.Endpoints {
+			t.Logf("      - region=[%s] publicURL=[%s]", endpoint.Region, endpoint.PublicURL)
+		}
+	}
+}
diff --git a/acceptance/openstack/identity/v3/identity_test.go b/acceptance/openstack/identity/v3/identity_test.go
index ec184a0..e0503e2 100644
--- a/acceptance/openstack/identity/v3/identity_test.go
+++ b/acceptance/openstack/identity/v3/identity_test.go
@@ -1,3 +1,5 @@
+// +build acceptance
+
 package v3
 
 import (
diff --git a/openstack/client.go b/openstack/client.go
index 1b057d0..f3638be 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -6,7 +6,7 @@
 	"strings"
 
 	"github.com/rackspace/gophercloud"
-	identity2 "github.com/rackspace/gophercloud/openstack/identity/v2"
+	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"
@@ -99,38 +99,30 @@
 		v2Client.Endpoint = endpoint
 	}
 
-	result, err := identity2.Authenticate(v2Client, options)
+	result := tokens2.Create(v2Client, options)
+
+	token, err := result.ExtractToken()
 	if err != nil {
 		return err
 	}
 
-	token, err := identity2.GetToken(result)
+	catalog, err := result.ExtractServiceCatalog()
 	if err != nil {
 		return err
 	}
 
 	client.TokenID = token.ID
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
-		return v2endpointLocator(result, opts)
+		return v2endpointLocator(catalog, opts)
 	}
 
 	return nil
 }
 
-func v2endpointLocator(authResults identity2.AuthResults, opts gophercloud.EndpointOpts) (string, error) {
-	catalog, err := identity2.GetServiceCatalog(authResults)
-	if err != nil {
-		return "", err
-	}
-
-	entries, err := catalog.CatalogEntries()
-	if err != nil {
-		return "", err
-	}
-
+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([]identity2.Endpoint, 0, 1)
-	for _, entry := range entries {
+	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 {
@@ -155,6 +147,8 @@
 			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)
 		}
@@ -308,7 +302,11 @@
 	if err != nil {
 		return nil, err
 	}
-	return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+	return &gophercloud.ServiceClient{
+		Provider:     client,
+		Endpoint:     url,
+		ResourceBase: url + "v2.0/",
+	}, nil
 }
 
 // NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
diff --git a/openstack/client_test.go b/openstack/client_test.go
index dd39e77..257260c 100644
--- a/openstack/client_test.go
+++ b/openstack/client_test.go
@@ -6,16 +6,16 @@
 	"testing"
 
 	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/testhelper"
+	th "github.com/rackspace/gophercloud/testhelper"
 )
 
 func TestAuthenticatedClientV3(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
 	const ID = "0123456789"
 
-	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, `
 			{
 				"versions": {
@@ -37,10 +37,10 @@
 					]
 				}
 			}
-		`, testhelper.Endpoint()+"v3/", testhelper.Endpoint()+"v2.0/")
+		`, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
 	})
 
-	testhelper.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Add("X-Subject-Token", ID)
 
 		w.WriteHeader(http.StatusCreated)
@@ -50,24 +50,18 @@
 	options := gophercloud.AuthOptions{
 		UserID:           "me",
 		Password:         "secret",
-		IdentityEndpoint: testhelper.Endpoint(),
+		IdentityEndpoint: th.Endpoint(),
 	}
 	client, err := AuthenticatedClient(options)
-
-	if err != nil {
-		t.Fatalf("Unexpected error from AuthenticatedClient: %s", err)
-	}
-
-	if client.TokenID != ID {
-		t.Errorf("Expected token ID to be [%s], but was [%s]", ID, client.TokenID)
-	}
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, ID, client.TokenID)
 }
 
 func TestAuthenticatedClientV2(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, `
 			{
 				"versions": {
@@ -89,16 +83,16 @@
 					]
 				}
 			}
-		`, testhelper.Endpoint()+"v3/", testhelper.Endpoint()+"v2.0/")
+		`, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
 	})
 
-	testhelper.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(http.StatusCreated)
+	th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprintf(w, `
 			{
 				"access": {
 					"token": {
-						"id": "01234567890"
+						"id": "01234567890",
+						"expires": "2014-10-01T10:00:00.000000Z"
 					},
 					"serviceCatalog": [
 						{
@@ -159,15 +153,9 @@
 	options := gophercloud.AuthOptions{
 		Username:         "me",
 		Password:         "secret",
-		IdentityEndpoint: testhelper.Endpoint(),
+		IdentityEndpoint: th.Endpoint(),
 	}
 	client, err := AuthenticatedClient(options)
-
-	if err != nil {
-		t.Fatalf("Unexpected error from AuthenticatedClient: %s", err)
-	}
-
-	if client.TokenID != "01234567890" {
-		t.Errorf("Expected token ID to be [01234567890], but was [%s]", client.TokenID)
-	}
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "01234567890", client.TokenID)
 }
diff --git a/openstack/common/README.md b/openstack/common/README.md
new file mode 100644
index 0000000..7b55795
--- /dev/null
+++ b/openstack/common/README.md
@@ -0,0 +1,3 @@
+# Common Resources
+
+This directory is for resources that are shared by multiple services.
diff --git a/openstack/common/extensions/doc.go b/openstack/common/extensions/doc.go
new file mode 100644
index 0000000..4a168f4
--- /dev/null
+++ b/openstack/common/extensions/doc.go
@@ -0,0 +1,15 @@
+// Package extensions provides information and interaction with the different extensions available
+// for an OpenStack service.
+//
+// The purpose of OpenStack API extensions is to:
+//
+// - Introduce new features in the API without requiring a version change.
+// - Introduce vendor-specific niche functionality.
+// - Act as a proving ground for experimental functionalities that might be included in a future
+//   version of the API.
+//
+// Extensions usually have tags that prevent conflicts with other extensions that define attributes
+// or resources with the same names, and with core resources and attributes.
+// Because an extension might not be supported by all plug-ins, its availability varies with deployments
+// and the specific plug-in.
+package extensions
diff --git a/openstack/networking/v2/extensions/errors.go b/openstack/common/extensions/errors.go
similarity index 100%
rename from openstack/networking/v2/extensions/errors.go
rename to openstack/common/extensions/errors.go
diff --git a/openstack/networking/v2/extensions/requests.go b/openstack/common/extensions/requests.go
similarity index 76%
rename from openstack/networking/v2/extensions/requests.go
rename to openstack/common/extensions/requests.go
index 2d3dc65..000151b 100755
--- a/openstack/networking/v2/extensions/requests.go
+++ b/openstack/common/extensions/requests.go
@@ -9,7 +9,7 @@
 // Get retrieves information for a specific extension using its alias.
 func Get(c *gophercloud.ServiceClient, alias string) GetResult {
 	var res GetResult
-	_, res.Err = perigee.Request("GET", extensionURL(c, alias), perigee.Options{
+	_, res.Err = perigee.Request("GET", ExtensionURL(c, alias), perigee.Options{
 		MoreHeaders: c.Provider.AuthenticatedHeaders(),
 		Results:     &res.Resp,
 		OkCodes:     []int{200},
@@ -17,10 +17,10 @@
 	return res
 }
 
-// List returns a Pager which allows you to iterate over the full collection of
-// extensions. It does not accept query parameters.
+// 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, listExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+	return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
 		return ExtensionPage{pagination.SinglePageBase(r)}
 	})
 }
diff --git a/openstack/common/extensions/requests_test.go b/openstack/common/extensions/requests_test.go
new file mode 100644
index 0000000..b0f655a
--- /dev/null
+++ b/openstack/common/extensions/requests_test.go
@@ -0,0 +1,113 @@
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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"
+		}
+	]
+}
+			`)
+	})
+
+	count := 0
+
+	List(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)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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.")
+	})
+}
diff --git a/openstack/networking/v2/extensions/results.go b/openstack/common/extensions/results.go
similarity index 74%
rename from openstack/networking/v2/extensions/results.go
rename to openstack/common/extensions/results.go
index 2b8408d..2b8d8b7 100755
--- a/openstack/networking/v2/extensions/results.go
+++ b/openstack/common/extensions/results.go
@@ -8,10 +8,13 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+// GetResult temporarility stores the result of a Get call.
+// Use its Extract() method to interpret it as an Extension.
 type GetResult struct {
 	gophercloud.CommonResult
 }
 
+// Extract interprets a GetResult as an Extension.
 func (r GetResult) Extract() (*Extension, error) {
 	if r.Err != nil {
 		return nil, r.Err
@@ -23,13 +26,13 @@
 
 	err := mapstructure.Decode(r.Resp, &res)
 	if err != nil {
-		return nil, fmt.Errorf("Error decoding Neutron extension: %v", err)
+		return nil, fmt.Errorf("Error decoding OpenStack extension: %v", err)
 	}
 
 	return res.Extension, nil
 }
 
-// Extension is a struct that represents a Neutron extension.
+// Extension is a struct that represents an OpenStack extension.
 type Extension struct {
 	Updated     string        `json:"updated"`
 	Name        string        `json:"name"`
@@ -39,8 +42,7 @@
 	Description string        `json:"description"`
 }
 
-// ExtensionPage is the page returned by a pager when traversing over a
-// collection of extensions.
+// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
 type ExtensionPage struct {
 	pagination.SinglePageBase
 }
@@ -54,9 +56,9 @@
 	return len(is) == 0, nil
 }
 
-// ExtractExtensions accepts a Page struct, specifically an ExtensionPage
-// struct, and extracts the elements into a slice of Extension structs. In other
-// words, a generic collection is mapped into a relevant slice.
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of Extension structs.
+// In other words, a generic collection is mapped into a relevant slice.
 func ExtractExtensions(page pagination.Page) ([]Extension, error) {
 	var resp struct {
 		Extensions []Extension `mapstructure:"extensions"`
diff --git a/openstack/common/extensions/urls.go b/openstack/common/extensions/urls.go
new file mode 100644
index 0000000..6460c66
--- /dev/null
+++ b/openstack/common/extensions/urls.go
@@ -0,0 +1,13 @@
+package extensions
+
+import "github.com/rackspace/gophercloud"
+
+// ExtensionURL generates the URL for an extension resource by name.
+func ExtensionURL(c *gophercloud.ServiceClient, name string) string {
+	return c.ServiceURL("extensions", name)
+}
+
+// ListExtensionURL generates the URL for the extensions resource collection.
+func ListExtensionURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("extensions")
+}
diff --git a/openstack/networking/v2/extensions/urls_test.go b/openstack/common/extensions/urls_test.go
similarity index 70%
rename from openstack/networking/v2/extensions/urls_test.go
rename to openstack/common/extensions/urls_test.go
index 2a1e6a1..3223b1c 100755
--- a/openstack/networking/v2/extensions/urls_test.go
+++ b/openstack/common/extensions/urls_test.go
@@ -14,13 +14,13 @@
 }
 
 func TestExtensionURL(t *testing.T) {
-	actual := extensionURL(endpointClient(), "agent")
-	expected := endpoint + "v2.0/extensions/agent"
+	actual := ExtensionURL(endpointClient(), "agent")
+	expected := endpoint + "extensions/agent"
 	th.AssertEquals(t, expected, actual)
 }
 
 func TestListExtensionURL(t *testing.T) {
-	actual := listExtensionURL(endpointClient())
-	expected := endpoint + "v2.0/extensions"
+	actual := ListExtensionURL(endpointClient())
+	expected := endpoint + "extensions"
 	th.AssertEquals(t, expected, actual)
 }
diff --git a/openstack/identity/v2/common_test.go b/openstack/identity/v2/common_test.go
deleted file mode 100644
index 0260424..0000000
--- a/openstack/identity/v2/common_test.go
+++ /dev/null
@@ -1,224 +0,0 @@
-package v2
-
-// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
-const authResultsOK = `{
-    "access":{
-        "token":{
-            "id": "ab48a9efdfedb23ty3494",
-            "expires": "2010-11-01T03:32:15-05:00",
-            "tenant":{
-                "id": "t1000",
-                "name": "My Project"
-            }
-        },
-        "user":{
-            "id": "u123",
-            "name": "jqsmith",
-            "roles":[{
-                    "id": "100",
-                    "name": "compute:admin"
-                },
-                {
-                    "id": "101",
-                    "name": "object-store:admin",
-                    "tenantId": "t1000"
-                }
-            ],
-            "roles_links":[]
-        },
-        "serviceCatalog":[{
-                "name": "Cloud Servers",
-                "type": "compute",
-                "endpoints":[{
-                        "tenantId": "t1000",
-                        "publicURL": "https://compute.north.host.com/v1/t1000",
-                        "internalURL": "https://compute.north.internal/v1/t1000",
-                        "region": "North",
-                        "versionId": "1",
-                        "versionInfo": "https://compute.north.host.com/v1/",
-                        "versionList": "https://compute.north.host.com/"
-                    },
-                    {
-                        "tenantId": "t1000",
-                        "publicURL": "https://compute.north.host.com/v1.1/t1000",
-                        "internalURL": "https://compute.north.internal/v1.1/t1000",
-                        "region": "North",
-                        "versionId": "1.1",
-                        "versionInfo": "https://compute.north.host.com/v1.1/",
-                        "versionList": "https://compute.north.host.com/"
-                    }
-                ],
-                "endpoints_links":[]
-            },
-            {
-                "name": "Cloud Files",
-                "type": "object-store",
-                "endpoints":[{
-                        "tenantId": "t1000",
-                        "publicURL": "https://storage.north.host.com/v1/t1000",
-                        "internalURL": "https://storage.north.internal/v1/t1000",
-                        "region": "North",
-                        "versionId": "1",
-                        "versionInfo": "https://storage.north.host.com/v1/",
-                        "versionList": "https://storage.north.host.com/"
-                    },
-                    {
-                        "tenantId": "t1000",
-                        "publicURL": "https://storage.south.host.com/v1/t1000",
-                        "internalURL": "https://storage.south.internal/v1/t1000",
-                        "region": "South",
-                        "versionId": "1",
-                        "versionInfo": "https://storage.south.host.com/v1/",
-                        "versionList": "https://storage.south.host.com/"
-                    }
-                ]
-            },
-            {
-                "name": "DNS-as-a-Service",
-                "type": "dnsextension:dns",
-                "endpoints":[{
-                        "tenantId": "t1000",
-                        "publicURL": "https://dns.host.com/v2.0/t1000",
-                        "versionId": "2.0",
-                        "versionInfo": "https://dns.host.com/v2.0/",
-                        "versionList": "https://dns.host.com/"
-                    }
-                ]
-            }
-        ]
-    }
-}`
-
-// Taken from: http://developer.openstack.org/api-ref-identity-v2.html
-const queryResults = `{
-	"extensions": {
-		"values": [
-			{
-				"updated": "2013-07-07T12:00:0-00:00",
-				"name": "OpenStack S3 API",
-				"links": [
-					{
-						"href": "https://github.com/openstack/identity-api",
-						"type": "text/html",
-						"rel": "describedby"
-					}
-				],
-				"namespace": "http://docs.openstack.org/identity/api/ext/s3tokens/v1.0",
-				"alias": "s3tokens",
-				"description": "OpenStack S3 API."
-			},
-			{
-				"updated": "2013-07-23T12:00:0-00:00",
-				"name": "OpenStack Keystone Endpoint Filter API",
-				"links": [
-					{
-						"href": "https://github.com/openstack/identity-api/blob/master/openstack-identity-api/v3/src/markdown/identity-api-v3-os-ep-filter-ext.md",
-						"type": "text/html",
-						"rel": "describedby"
-					}
-				],
-				"namespace": "http://docs.openstack.org/identity/api/ext/OS-EP-FILTER/v1.0",
-				"alias": "OS-EP-FILTER",
-				"description": "OpenStack Keystone Endpoint Filter API."
-			},
-			{
-				"updated": "2013-12-17T12:00:0-00:00",
-				"name": "OpenStack Federation APIs",
-				"links": [
-					{
-						"href": "https://github.com/openstack/identity-api",
-						"type": "text/html",
-						"rel": "describedby"
-					}
-				],
-				"namespace": "http://docs.openstack.org/identity/api/ext/OS-FEDERATION/v1.0",
-				"alias": "OS-FEDERATION",
-				"description": "OpenStack Identity Providers Mechanism."
-			},
-			{
-				"updated": "2013-07-11T17:14:00-00:00",
-				"name": "OpenStack Keystone Admin",
-				"links": [
-					{
-						"href": "https://github.com/openstack/identity-api",
-						"type": "text/html",
-						"rel": "describedby"
-					}
-				],
-				"namespace": "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0",
-				"alias": "OS-KSADM",
-				"description": "OpenStack extensions to Keystone v2.0 API enabling Administrative Operations."
-			},
-			{
-				"updated": "2014-01-20T12:00:0-00:00",
-				"name": "OpenStack Simple Certificate API",
-				"links": [
-					{
-						"href": "https://github.com/openstack/identity-api",
-						"type": "text/html",
-						"rel": "describedby"
-					}
-				],
-				"namespace": "http://docs.openstack.org/identity/api/ext/OS-SIMPLE-CERT/v1.0",
-				"alias": "OS-SIMPLE-CERT",
-				"description": "OpenStack simple certificate retrieval extension"
-			},
-			{
-				"updated": "2013-07-07T12:00:0-00:00",
-				"name": "OpenStack EC2 API",
-				"links": [
-					{
-						"href": "https://github.com/openstack/identity-api",
-						"type": "text/html",
-						"rel": "describedby"
-					}
-				],
-				"namespace": "http://docs.openstack.org/identity/api/ext/OS-EC2/v1.0",
-				"alias": "OS-EC2",
-				"description": "OpenStack EC2 Credentials backend."
-			}
-		]
-	}
-}`
-
-// Extensions query with a bogus JSON envelop.
-const bogusExtensionsResults = `{
-    "explosions":[{
-            "name": "Reset Password Extension",
-            "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
-            "alias": "RS-RPE",
-            "updated": "2011-01-22T13:25:27-06:00",
-            "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
-            "links":[{
-                    "rel": "describedby",
-                    "type": "application/pdf",
-                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
-                },
-                {
-                    "rel": "describedby",
-                    "type": "application/vnd.sun.wadl+xml",
-                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
-                }
-            ]
-        },
-        {
-            "name": "User Metadata Extension",
-            "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
-            "alias": "RS-META",
-            "updated": "2011-01-12T11:22:33-06:00",
-            "description": "Allows associating arbritrary metadata with a user.",
-            "links":[{
-                    "rel": "describedby",
-                    "type": "application/pdf",
-                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
-                },
-                {
-                    "rel": "describedby",
-                    "type": "application/vnd.sun.wadl+xml",
-                    "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
-                }
-            ]
-        }
-    ],
-    "extensions_links":[]
-}`
diff --git a/openstack/identity/v2/doc.go b/openstack/identity/v2/doc.go
deleted file mode 100644
index e8ab21b..0000000
--- a/openstack/identity/v2/doc.go
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
-Package v2 identity provides convenient OpenStack Identity V2 API client access.
-This package currently doesn't support the administrative access endpoints, but may appear in the future based on demand.
-
-Authentication
-
-Established convention in the OpenStack community suggests the use of environment variables to hold authentication parameters.
-For example, the following settings would be sufficient to authenticate against Rackspace:
-
-	# assumes Bash shell on a POSIX environment; use SET command for Windows.
-	export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0
-	export OS_USERNAME=xxxx
-	export OS_PASSWORD=yyyy
-
-while you'd need these additional settings to authenticate against, e.g., Nebula One:
-
-	export OS_TENANT_ID=zzzz
-	export OS_TENANT_NAME=wwww
-
-Be sure to consult with your provider to see which settings you'll need to authenticate with.
-
-A skeletal client gets started with Gophercloud by authenticating against his/her provider, like so:
-
-	package main
-
-	import (
-		"fmt"
-		"github.com/rackspace/gophercloud/openstack/identity"
-		"github.com/rackspace/gophercloud/openstack/utils"
-	)
-
-	func main() {
-		// Create an initialized set of authentication options based on available OS_*
-		// environment variables.
-		ao, err := utils.AuthOptions()
-		if err != nil {
-			panic(err)
-		}
-
-		// Attempt to authenticate with them.
-		r, err := identity.Authenticate(ao)
-		if err != nil {
-			panic(err)
-		}
-
-		// With each authentication, you receive a master directory of all the services
-		// your account can access.  This "service catalog", as OpenStack calls it,
-		// provides you the means to exploit other OpenStack services.
-		sc, err := identity.GetServiceCatalog(r)
-		if err != nil {
-			panic(err)
-		}
-
-		// Find the desired service(s) for our application.
-		computeService, err := findService(sc, "compute", ...)
-		if err != nil {
-			panic(err)
-		}
-
-		blockStorage, err := findService(sc, "block-storage", ...)
-		if err != nil {
-			panic(err)
-		}
-
-		// ... etc ...
-	}
-
-NOTE!
-Unlike versions 0.1.x of the Gophercloud API,
-0.2.0 and later will not provide a service look-up mechanism as a built-in feature of the Identity SDK binding.
-The 0.1.x behavior potentially opened its non-US users to legal liability by potentially selecting endpoints in undesirable regions
-in a non-obvious manner if a specific region was not explicitly specified.
-Starting with 0.2.0 and beyond, you'll need to use either your own service catalog query function or one in a separate package.
-This makes it plainly visible to a code auditor that if you indeed desired automatic selection of an arbitrary region,
-you made the conscious choice to use that feature.
-
-Extensions
-
-Some OpenStack deployments may support features that other deployments do not.
-Anything beyond the scope of standard OpenStack must be scoped by an "extension," a named, yet well-known, change to the API.
-Users may invoke IsExtensionAvailable() after grabbing a list of extensions from the server with GetExtensions().
-This of course assumes you know the name of the extension ahead of time.
-
-Here's a simple example of listing all the aliases for supported extensions.
-Once you have an alias to an extension, everything else about it may be queried through accessors.
-
-	package main
-
-	import (
-		"fmt"
-		"github.com/rackspace/gophercloud/openstack/identity"
-		"github.com/rackspace/gophercloud/openstack/utils"
-	)
-
-	func main() {
-		// Create an initialized set of authentication options based on available OS_*
-		// environment variables.
-		ao, err := utils.AuthOptions()
-		if err != nil {
-			panic(err)
-		}
-
-		// Attempt to query extensions.
-		exts, err := identity.GetExtensions(ao)
-		if err != nil {
-			panic(err)
-		}
-
-		// Print out a summary of supported extensions
-		aliases, err := exts.Aliases()
-		if err != nil {
-			panic(err)
-		}
-		fmt.Println("Extension Aliases:")
-		for _, alias := range aliases {
-			fmt.Printf("  %s\n", alias)
-		}
-	}
-*/
-package v2
diff --git a/openstack/identity/v2/errors.go b/openstack/identity/v2/errors.go
deleted file mode 100644
index 0dd1172..0000000
--- a/openstack/identity/v2/errors.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package v2
-
-import "fmt"
-
-// ErrNotImplemented errors may occur in two contexts:
-// (1) development versions of this package may return this error for endpoints which are defined but not yet completed, and,
-// (2) production versions of this package may return this error when a provider fails to offer the requested Identity extension.
-//
-// ErrEndpoint errors occur when the authentication URL provided to Authenticate() either isn't valid
-// or the endpoint provided doesn't respond like an Identity V2 API endpoint should.
-//
-// ErrCredentials errors occur when authentication fails due to the caller possessing insufficient access privileges.
-var (
-	ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
-	ErrEndpoint       = fmt.Errorf("Improper or missing Identity endpoint")
-	ErrCredentials    = fmt.Errorf("Improper or missing Identity credentials")
-)
diff --git a/openstack/identity/v2/extensions.go b/openstack/identity/v2/extensions.go
deleted file mode 100644
index df05a36..0000000
--- a/openstack/identity/v2/extensions.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package v2
-
-import (
-	"github.com/mitchellh/mapstructure"
-)
-
-// ExtensionDetails provides the details offered by the OpenStack Identity V2 extensions API
-// for a named extension.
-//
-// Name provides the name, presumably the same as that used to query the API with.
-//
-// Updated provides, in a sense, the version of the extension supported.  It gives the timestamp
-// of the most recent extension deployment.
-//
-// Description provides a more customer-oriented description of the extension.
-type ExtensionDetails struct {
-	Name        string
-	Namespace   string
-	Updated     string
-	Description string
-}
-
-// ExtensionsResult encapsulates the raw data returned by a call to
-// GetExtensions().  As OpenStack extensions may freely alter the response
-// bodies of structures returned to the client, you may only safely access the
-// data provided through separate, type-safe accessors or methods.
-type ExtensionsResult map[string]interface{}
-
-// IsExtensionAvailable returns true if and only if the provider supports the named extension.
-func (er ExtensionsResult) IsExtensionAvailable(alias string) bool {
-	e, err := extensions(er)
-	if err != nil {
-		return false
-	}
-	_, err = extensionIndexByAlias(e, alias)
-	return err == nil
-}
-
-// ExtensionDetailsByAlias returns more detail than the mere presence of an extension by the provider.
-// See the ExtensionDetails structure.
-func (er ExtensionsResult) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
-	e, err := extensions(er)
-	if err != nil {
-		return nil, err
-	}
-	i, err := extensionIndexByAlias(e, alias)
-	if err != nil {
-		return nil, err
-	}
-	ed := &ExtensionDetails{}
-	err = mapstructure.Decode(e[i], ed)
-	return ed, err
-}
-
-func extensionIndexByAlias(records []interface{}, alias string) (int, error) {
-	for i, er := range records {
-		extensionRecord := er.(map[string]interface{})
-		if extensionRecord["alias"] == alias {
-			return i, nil
-		}
-	}
-	return 0, ErrNotImplemented
-}
-
-func extensions(er ExtensionsResult) ([]interface{}, error) {
-	ei, ok := er["extensions"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	e := ei.(map[string]interface{})
-	vi, ok := e["values"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	v := vi.([]interface{})
-	return v, nil
-}
-
-// Aliases returns the set of extension handles, or "aliases" as OpenStack calls them.
-// These are not the names of the extensions, but rather opaque, symbolic monikers for their corresponding extension.
-// Use the ExtensionDetailsByAlias() method to query more information for an extension if desired.
-func (er ExtensionsResult) Aliases() ([]string, error) {
-	e, err := extensions(er)
-	if err != nil {
-		return nil, err
-	}
-	aliases := make([]string, len(e))
-	for i, ex := range e {
-		ext := ex.(map[string]interface{})
-		extn, ok := ext["alias"]
-		if ok {
-			aliases[i] = extn.(string)
-		}
-	}
-	return aliases, nil
-}
diff --git a/openstack/identity/v2/extensions/delegate.go b/openstack/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..1992a2c
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,85 @@
+package extensions
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"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
+}
+
+// IsEmpty returns true if the current page contains at least one Extension.
+func (page ExtensionPage) IsEmpty() (bool, error) {
+	is, err := ExtractExtensions(page)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// 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) {
+	// 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"`
+		} `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
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) GetResult {
+	return GetResult{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)}}
+	})
+}
diff --git a/openstack/identity/v2/extensions/delegate_test.go b/openstack/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..90eb21b
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,114 @@
+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"
+)
+
+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"
+			}
+		]
+	}
+}
+		`)
+	})
+
+	count := 0
+
+	err := List(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)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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.")
+	})
+}
diff --git a/openstack/identity/v2/extensions_test.go b/openstack/identity/v2/extensions_test.go
deleted file mode 100644
index 47f7e06..0000000
--- a/openstack/identity/v2/extensions_test.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package v2
-
-import (
-	"encoding/json"
-	"testing"
-)
-
-func TestIsExtensionAvailable(t *testing.T) {
-	// Make a response as we'd expect from the IdentityService.GetExtensions() call.
-	getExtensionsResults := make(map[string]interface{})
-	err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	e := ExtensionsResult(getExtensionsResults)
-	for _, alias := range []string{"OS-KSADM", "OS-FEDERATION"} {
-		if !e.IsExtensionAvailable(alias) {
-			t.Errorf("Expected extension %s present.", alias)
-			return
-		}
-	}
-	if e.IsExtensionAvailable("blort") {
-		t.Errorf("Input JSON doesn't list blort as an extension")
-		return
-	}
-}
-
-func TestGetExtensionDetails(t *testing.T) {
-	// Make a response as we'd expect from the IdentityService.GetExtensions() call.
-	getExtensionsResults := make(map[string]interface{})
-	err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	e := ExtensionsResult(getExtensionsResults)
-	ed, err := e.ExtensionDetailsByAlias("OS-KSADM")
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	actuals := map[string]string{
-		"name":        ed.Name,
-		"namespace":   ed.Namespace,
-		"updated":     ed.Updated,
-		"description": ed.Description,
-	}
-
-	expecteds := map[string]string{
-		"name":        "OpenStack Keystone Admin",
-		"namespace":   "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0",
-		"updated":     "2013-07-11T17:14:00-00:00",
-		"description": "OpenStack extensions to Keystone v2.0 API enabling Administrative Operations.",
-	}
-
-	for k, v := range expecteds {
-		if actuals[k] != v {
-			t.Errorf("Expected %s \"%s\", got \"%s\" instead", k, v, actuals[k])
-			return
-		}
-	}
-}
-
-func TestMalformedResponses(t *testing.T) {
-	getExtensionsResults := make(map[string]interface{})
-	err := json.Unmarshal([]byte(bogusExtensionsResults), &getExtensionsResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	e := ExtensionsResult(getExtensionsResults)
-
-	_, err = e.ExtensionDetailsByAlias("OS-KSADM")
-	if err == nil {
-		t.Error("Expected ErrNotImplemented at least")
-		return
-	}
-	if err != ErrNotImplemented {
-		t.Error("Expected ErrNotImplemented")
-		return
-	}
-
-	if e.IsExtensionAvailable("anything at all") {
-		t.Error("No extensions are available with a bogus result.")
-		return
-	}
-}
-
-func TestAliases(t *testing.T) {
-	getExtensionsResults := make(map[string]interface{})
-	err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	e := ExtensionsResult(getExtensionsResults)
-	aliases, err := e.Aliases()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	extensions := (((e["extensions"]).(map[string]interface{}))["values"]).([]interface{})
-	if len(aliases) != len(extensions) {
-		t.Error("Expected one alias name per extension")
-		return
-	}
-}
diff --git a/openstack/identity/v2/requests.go b/openstack/identity/v2/requests.go
deleted file mode 100644
index bb068b6..0000000
--- a/openstack/identity/v2/requests.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package v2
-
-import (
-	"github.com/racker/perigee"
-	"github.com/rackspace/gophercloud"
-)
-
-// AuthResults encapsulates the raw results from an authentication request.
-// As OpenStack allows extensions to influence the structure returned in
-// ways that Gophercloud cannot predict at compile-time, you should use
-// type-safe accessors to work with the data represented by this type,
-// such as ServiceCatalog() and Token().
-type AuthResults map[string]interface{}
-
-// Authenticate passes the supplied credentials to the OpenStack provider for authentication.
-// If successful, the caller may use Token() to retrieve the authentication token,
-// and ServiceCatalog() to retrieve the set of services available to the API user.
-func Authenticate(c *gophercloud.ServiceClient, options gophercloud.AuthOptions) (AuthResults, error) {
-	type AuthContainer struct {
-		Auth auth `json:"auth"`
-	}
-
-	var ar AuthResults
-
-	if c.Endpoint == "" {
-		return nil, ErrEndpoint
-	}
-
-	if (options.Username == "") || (options.Password == "" && options.APIKey == "") {
-		return nil, ErrCredentials
-	}
-
-	url := c.Endpoint + "tokens"
-	err := perigee.Post(url, perigee.Options{
-		ReqBody: &AuthContainer{
-			Auth: getAuthCredentials(options),
-		},
-		Results: &ar,
-	})
-	return ar, err
-}
-
-func getAuthCredentials(options gophercloud.AuthOptions) auth {
-	if options.APIKey == "" {
-		return auth{
-			PasswordCredentials: &struct {
-				Username string `json:"username"`
-				Password string `json:"password"`
-			}{
-				Username: options.Username,
-				Password: options.Password,
-			},
-			TenantID:   options.TenantID,
-			TenantName: options.TenantName,
-		}
-	}
-	return auth{
-		APIKeyCredentials: &struct {
-			Username string `json:"username"`
-			APIKey   string `json:"apiKey"`
-		}{
-			Username: options.Username,
-			APIKey:   options.APIKey,
-		},
-		TenantID:   options.TenantID,
-		TenantName: options.TenantName,
-	}
-}
-
-type auth struct {
-	PasswordCredentials interface{} `json:"passwordCredentials,omitempty"`
-	APIKeyCredentials   interface{} `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
-	TenantID            string      `json:"tenantId,omitempty"`
-	TenantName          string      `json:"tenantName,omitempty"`
-}
-
-// GetExtensions returns the OpenStack extensions available from this service.
-func GetExtensions(c *gophercloud.ServiceClient, options gophercloud.AuthOptions) (ExtensionsResult, error) {
-	var exts ExtensionsResult
-
-	url := c.Endpoint + "extensions"
-	err := perigee.Get(url, perigee.Options{
-		Results: &exts,
-	})
-	return exts, err
-}
diff --git a/openstack/identity/v2/service_catalog.go b/openstack/identity/v2/service_catalog.go
deleted file mode 100644
index 22d0d7b..0000000
--- a/openstack/identity/v2/service_catalog.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package v2
-
-import "github.com/mitchellh/mapstructure"
-
-// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
-// OpenStack extensions may alter the structure of the service catalog in ways unpredictable to Go at compile-time,
-// so this structure serves as a convenient anchor for type-safe accessors and methods.
-type ServiceCatalog struct {
-	serviceDescriptions []interface{}
-}
-
-// CatalogEntry provides a type-safe interface to an Identity API V2 service
-// catalog listing.  Each class of service, such as cloud DNS or block storage
-// services, will have a single CatalogEntry representing it.
-//
-// Name will contain the provider-specified name for the service.
-//
-// If OpenStack defines a type for the service, this field will contain that
-// type string.  Otherwise, for provider-specific services, the provider may
-// assign their own type strings.
-//
-// Endpoints will let the caller iterate over all the different endpoints that
-// may exist for the service.
-//
-// Note: when looking for the desired service, try, whenever possible, to key
-// off the type field.  Otherwise, you'll tie the representation of the service
-// to a specific provider.
-type CatalogEntry struct {
-	Name      string
-	Type      string
-	Endpoints []Endpoint
-}
-
-// Endpoint represents a single API endpoint offered by a service.
-// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
-// The significance of the Region field will depend upon your provider.
-//
-// In addition, the interface offered by the service will have version information associated with it
-// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
-//
-// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
-type Endpoint struct {
-	TenantID    string
-	PublicURL   string
-	InternalURL string
-	Region      string
-	VersionID   string
-	VersionInfo string
-	VersionList string
-}
-
-// GetServiceCatalog acquires the service catalog from a successful authentication's results.
-func GetServiceCatalog(ar AuthResults) (*ServiceCatalog, error) {
-	access := ar["access"].(map[string]interface{})
-	sds := access["serviceCatalog"].([]interface{})
-	sc := &ServiceCatalog{
-		serviceDescriptions: sds,
-	}
-	return sc, nil
-}
-
-// NumberOfServices yields the number of services the caller may use.  Note
-// that this does not necessarily equal the number of endpoints available for
-// use.
-func (sc *ServiceCatalog) NumberOfServices() int {
-	return len(sc.serviceDescriptions)
-}
-
-// CatalogEntries returns a slice of service catalog entries.
-// Each entry corresponds to a specific class of service offered by the API provider.
-// See the CatalogEntry structure for more details.
-func (sc *ServiceCatalog) CatalogEntries() ([]CatalogEntry, error) {
-	var err error
-	ces := make([]CatalogEntry, sc.NumberOfServices())
-	for i, sd := range sc.serviceDescriptions {
-		d := sd.(map[string]interface{})
-		eps, err := parseEndpoints(d["endpoints"].([]interface{}))
-		if err != nil {
-			return ces, err
-		}
-		ces[i] = CatalogEntry{
-			Name:      d["name"].(string),
-			Type:      d["type"].(string),
-			Endpoints: eps,
-		}
-	}
-	return ces, err
-}
-
-func parseEndpoints(eps []interface{}) ([]Endpoint, error) {
-	var err error
-	result := make([]Endpoint, len(eps))
-	for i, ep := range eps {
-		e := Endpoint{}
-		err = mapstructure.Decode(ep, &e)
-		if err != nil {
-			return result, err
-		}
-		result[i] = e
-	}
-	return result, err
-}
diff --git a/openstack/identity/v2/service_catalog_test.go b/openstack/identity/v2/service_catalog_test.go
deleted file mode 100644
index 143b48f..0000000
--- a/openstack/identity/v2/service_catalog_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package v2
-
-import (
-	"encoding/json"
-	"testing"
-)
-
-func TestServiceCatalog(t *testing.T) {
-	authResults := make(map[string]interface{})
-	err := json.Unmarshal([]byte(authResultsOK), &authResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	sc, err := GetServiceCatalog(authResults)
-	if err != nil {
-		panic(err)
-	}
-
-	if sc.NumberOfServices() != 3 {
-		t.Errorf("Expected 3 services; got %d", sc.NumberOfServices())
-	}
-
-	ces, err := sc.CatalogEntries()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	for _, ce := range ces {
-		if strNotInStrList(ce.Name, "Cloud Servers", "Cloud Files", "DNS-as-a-Service") {
-			t.Errorf("Expected \"%s\" to be one of Cloud Servers, Cloud Files, or DNS-as-a-Service", ce.Name)
-			return
-		}
-
-		if strNotInStrList(ce.Type, "dnsextension:dns", "object-store", "compute") {
-			t.Errorf("Expected \"%s\" to be one of dnsextension:dns, object-store, or compute")
-			return
-		}
-	}
-
-	eps := endpointsFor(ces, "compute")
-	if len(eps) != 2 {
-		t.Errorf("Expected 2 endpoints for compute service")
-		return
-	}
-	for _, ep := range eps {
-		if strNotInStrList(ep.VersionID, "1", "1.1", "1.1") {
-			t.Errorf("Expected versionID field of compute resource to be one of 1 or 1.1")
-			return
-		}
-	}
-
-	eps = endpointsFor(ces, "object-store")
-	if len(eps) != 2 {
-		t.Errorf("Expected 2 endpoints for object-store service")
-		return
-	}
-	for _, ep := range eps {
-		if ep.VersionID != "1" {
-			t.Errorf("Expected only version 1 object store API version")
-			return
-		}
-	}
-
-	eps = endpointsFor(ces, "dnsextension:dns")
-	if len(eps) != 1 {
-		t.Errorf("Expected 1 endpoint for DNS-as-a-Service service")
-		return
-	}
-	if eps[0].VersionID != "2.0" {
-		t.Errorf("Expected version 2.0 of DNS-as-a-Service service")
-		return
-	}
-}
-
-func endpointsFor(ces []CatalogEntry, t string) []Endpoint {
-	for _, ce := range ces {
-		if ce.Type == t {
-			return ce.Endpoints
-		}
-	}
-	panic("Precondition violated")
-}
-
-func strNotInStrList(needle, haystack1, haystack2, haystack3 string) bool {
-	if (needle != haystack1) && (needle != haystack2) && (needle != haystack3) {
-		return true
-	}
-	return false
-}
diff --git a/openstack/identity/v2/tenants/doc.go b/openstack/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..473a302
--- /dev/null
+++ b/openstack/identity/v2/tenants/doc.go
@@ -0,0 +1,7 @@
+/*
+Package tenants contains API calls that query for information about tenants on an OpenStack deployment.
+
+See: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+And: http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
+*/
+package tenants
diff --git a/openstack/identity/v2/tenants/requests.go b/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..5ffeaa7
--- /dev/null
+++ b/openstack/identity/v2/tenants/requests.go
@@ -0,0 +1,33 @@
+package tenants
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts filters the Tenants that are returned by the List call.
+type ListOpts struct {
+	// Marker is the ID of the last Tenant on the previous page.
+	Marker string `q:"marker"`
+
+	// Limit specifies the page size.
+	Limit int `q:"limit"`
+}
+
+// List enumerates the Tenants to which the current token has access.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return TenantPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+	}
+
+	url := listURL(client)
+	if opts != nil {
+		q, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += q.String()
+	}
+
+	return pagination.NewPager(client, url, createPage)
+}
diff --git a/openstack/identity/v2/tenants/requests_test.go b/openstack/identity/v2/tenants/requests_test.go
new file mode 100644
index 0000000..685bf96
--- /dev/null
+++ b/openstack/identity/v2/tenants/requests_test.go
@@ -0,0 +1,79 @@
+package tenants
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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(),
+	}
+
+	count := 0
+	err := List(client, 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)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
diff --git a/openstack/identity/v2/tenants/results.go b/openstack/identity/v2/tenants/results.go
new file mode 100644
index 0000000..e4e3f47
--- /dev/null
+++ b/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,75 @@
+package tenants
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Tenant is a grouping of users in the identity service.
+type Tenant struct {
+	// ID is a unique identifier for this tenant.
+	ID string `mapstructure:"id"`
+
+	// Name is a friendlier user-facing name for this tenant.
+	Name string `mapstructure:"name"`
+
+	// Description is a human-readable explanation of this Tenant's purpose.
+	Description string `mapstructure:"description"`
+
+	// Enabled indicates whether or not a tenant is active.
+	Enabled bool `mapstructure:"enabled"`
+}
+
+// TenantPage is a single page of Tenant results.
+type TenantPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page TenantPage) IsEmpty() (bool, error) {
+	tenants, err := ExtractTenants(page)
+	if err != nil {
+		return false, err
+	}
+	return len(tenants) == 0, nil
+}
+
+// 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"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	var url string
+	for _, l := range r.Links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
+
+// ExtractTenants returns a slice of Tenants contained in a single page of results.
+func ExtractTenants(page pagination.Page) ([]Tenant, error) {
+	casted := page.(TenantPage).Body
+	var response struct {
+		Tenants []Tenant `mapstructure:"tenants"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Tenants, err
+}
diff --git a/openstack/identity/v2/tenants/urls.go b/openstack/identity/v2/tenants/urls.go
new file mode 100644
index 0000000..1dd6ce0
--- /dev/null
+++ b/openstack/identity/v2/tenants/urls.go
@@ -0,0 +1,7 @@
+package tenants
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("tenants")
+}
diff --git a/openstack/identity/v2/token.go b/openstack/identity/v2/token.go
deleted file mode 100644
index df67d76..0000000
--- a/openstack/identity/v2/token.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package v2
-
-import (
-	"github.com/mitchellh/mapstructure"
-)
-
-// Token provides only the most basic information related to an authentication token.
-//
-// Id provides the primary means of identifying a user to the OpenStack API.
-// OpenStack defines this field as an opaque value, so do not depend on its content.
-// It is safe, however, to compare for equality.
-//
-// Expires provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
-// After this point in time, future API requests made using this authentication token will respond with errors.
-// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
-// See the AuthOptions structure for more details.
-//
-// TenantId provides the canonical means of identifying a tenant.
-// As with Id, this field is defined to be opaque, so do not depend on its content.
-// It is safe, however, to compare for equality.
-//
-// TenantName provides a human-readable tenant name corresponding to the TenantId.
-type Token struct {
-	ID, Expires          string
-	TenantID, TenantName string
-}
-
-// GetToken yields an unpacked collection of fields related to the user's access credentials, called a "token", if successful.
-// See the Token structure for more details.
-func GetToken(m AuthResults) (*Token, error) {
-	type (
-		Tenant struct {
-			ID   string
-			Name string
-		}
-
-		TokenDesc struct {
-			ID      string `mapstructure:"id"`
-			Expires string `mapstructure:"expires"`
-			Tenant
-		}
-	)
-
-	accessMap, err := getSubmap(m, "access")
-	if err != nil {
-		return nil, err
-	}
-	tokenMap, err := getSubmap(accessMap, "token")
-	if err != nil {
-		return nil, err
-	}
-	t := &TokenDesc{}
-	err = mapstructure.Decode(tokenMap, t)
-	if err != nil {
-		return nil, err
-	}
-	td := &Token{
-		ID:         t.ID,
-		Expires:    t.Expires,
-		TenantID:   t.Tenant.ID,
-		TenantName: t.Tenant.Name,
-	}
-	return td, nil
-}
-
-func getSubmap(m map[string]interface{}, name string) (map[string]interface{}, error) {
-	entry, ok := m[name]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	return entry.(map[string]interface{}), nil
-}
diff --git a/openstack/identity/v2/token_test.go b/openstack/identity/v2/token_test.go
deleted file mode 100644
index 9770ed5..0000000
--- a/openstack/identity/v2/token_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package v2
-
-import (
-	"encoding/json"
-	"testing"
-)
-
-func TestAccessToken(t *testing.T) {
-	authResults := make(map[string]interface{})
-	err := json.Unmarshal([]byte(authResultsOK), &authResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	tok, err := GetToken(authResults)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if tok.ID != "ab48a9efdfedb23ty3494" {
-		t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.ID)
-		return
-	}
-}
diff --git a/openstack/identity/v2/tokens/doc.go b/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..d26f642
--- /dev/null
+++ b/openstack/identity/v2/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens contains functions that issue and manipulate identity tokens.
+
+Reference: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+*/
+package tokens
diff --git a/openstack/identity/v2/tokens/errors.go b/openstack/identity/v2/tokens/errors.go
new file mode 100644
index 0000000..244db1b
--- /dev/null
+++ b/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,27 @@
+package tokens
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	// ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+	ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+	// ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
+	ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
+
+	// ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
+	ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
+
+	// 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.")
+)
+
+func unacceptedAttributeErr(attribute string) error {
+	return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..5bd5a70
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,80 @@
+package tokens
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// 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"`
+	}
+
+	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{
+		ReqBody: &request,
+		Results: &result.Resp,
+		OkCodes: []int{200, 203},
+	})
+	return result
+}
diff --git a/openstack/identity/v2/tokens/requests_test.go b/openstack/identity/v2/tokens/requests_test.go
new file mode 100644
index 0000000..0e6269f
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,268 @@
+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"
+)
+
+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()
+
+	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)
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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
+	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, `
+    {
+      "auth": {
+        "passwordCredentials": {
+          "username": "me",
+          "password": "swordfish"
+        }
+      }
+    }
+  `))
+}
+
+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",
+		Password: "opensesame",
+		TenantID: "fc394f2ab2df4114bde39905f800dc57",
+	}
+
+	isSuccessful(t, tokenPost(t, options, `
+    {
+      "auth": {
+        "tenantId": "fc394f2ab2df4114bde39905f800dc57",
+        "passwordCredentials": {
+          "username": "me",
+          "password": "opensesame"
+        }
+      }
+    }
+  `))
+}
+
+func TestCreateTokenWithTenantName(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username:   "me",
+		Password:   "opensesame",
+		TenantName: "demo",
+	}
+
+	isSuccessful(t, tokenPost(t, options, `
+    {
+      "auth": {
+        "tenantName": "demo",
+        "passwordCredentials": {
+          "username": "me",
+          "password": "opensesame"
+        }
+      }
+    }
+  `))
+}
+
+func TestProhibitUserID(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		UserID:   "1234",
+		Password: "thing",
+	}
+	tokenPostErr(t, options, ErrUserIDProvided)
+}
+
+func TestProhibitDomainID(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		Password: "thing",
+		DomainID: "1234",
+	}
+	tokenPostErr(t, options, ErrDomainIDProvided)
+}
+
+func TestProhibitDomainName(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username:   "me",
+		Password:   "thing",
+		DomainName: "wat",
+	}
+	tokenPostErr(t, options, ErrDomainNameProvided)
+}
+
+func TestRequireUsername(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Password: "thing",
+	}
+	tokenPostErr(t, options, ErrUsernameRequired)
+}
+
+func TestProhibitBothPasswordAndAPIKey(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)
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..e88b2c7
--- /dev/null
+++ b/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,133 @@
+package tokens
+
+import (
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// Token provides only the most basic information related to an authentication token.
+type Token struct {
+	// ID provides the primary means of identifying a user to the OpenStack API.
+	// OpenStack defines this field as an opaque value, so do not depend on its content.
+	// It is safe, however, to compare for equality.
+	ID string
+
+	// ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
+	// After this point in time, future API requests made using this authentication token will respond with errors.
+	// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
+	// See the AuthOptions structure for more details.
+	ExpiresAt time.Time
+
+	// Tenant provides information about the tenant to which this token grants access.
+	Tenant tenants.Tenant
+}
+
+// Endpoint represents a single API endpoint offered by a service.
+// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+//
+// In addition, the interface offered by the service will have version information associated with it
+// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
+//
+// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
+type Endpoint struct {
+	TenantID    string `mapstructure:"tenantId"`
+	PublicURL   string `mapstructure:"publicURL"`
+	InternalURL string `mapstructure:"internalURL"`
+	AdminURL    string `mapstructure:"adminURL"`
+	Region      string `mapstructure:"region"`
+	VersionID   string `mapstructure:"versionId"`
+	VersionInfo string `mapstructure:"versionInfo"`
+	VersionList string `mapstructure:"versionList"`
+}
+
+// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing.
+// Each class of service, such as cloud DNS or block storage services, will have a single
+// CatalogEntry representing it.
+//
+// Note: when looking for the desired service, try, whenever possible, to key off the type field.
+// Otherwise, you'll tie the representation of the service to a specific provider.
+type CatalogEntry struct {
+	// Name will contain the provider-specified name for the service.
+	Name string `mapstructure:"name"`
+
+	// Type will contain a type string if OpenStack defines a type for the service.
+	// Otherwise, for provider-specific services, the provider may assign their own type strings.
+	Type string `mapstructure:"type"`
+
+	// Endpoints will let the caller iterate over all the different endpoints that may exist for
+	// the service.
+	Endpoints []Endpoint `mapstructure:"endpoints"`
+}
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+type ServiceCatalog struct {
+	Entries []CatalogEntry
+}
+
+// CreateResult defers the interpretation of a created token.
+// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
+type CreateResult struct {
+	gophercloud.CommonResult
+}
+
+// ExtractToken returns the just-created Token from a CreateResult.
+func (result CreateResult) ExtractToken() (*Token, error) {
+	if result.Err != nil {
+		return nil, result.Err
+	}
+
+	var response struct {
+		Access struct {
+			Token struct {
+				Expires string         `mapstructure:"expires"`
+				ID      string         `mapstructure:"id"`
+				Tenant  tenants.Tenant `mapstructure:"tenant"`
+			} `mapstructure:"token"`
+		} `mapstructure:"access"`
+	}
+
+	err := mapstructure.Decode(result.Resp, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Token{
+		ID:        response.Access.Token.ID,
+		ExpiresAt: expiresTs,
+		Tenant:    response.Access.Token.Tenant,
+	}, nil
+}
+
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+	if result.Err != nil {
+		return nil, result.Err
+	}
+
+	var response struct {
+		Access struct {
+			Entries []CatalogEntry `mapstructure:"serviceCatalog"`
+		} `mapstructure:"access"`
+	}
+
+	err := mapstructure.Decode(result.Resp, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ServiceCatalog{Entries: response.Access.Entries}, nil
+}
+
+// createErr quickly packs an error in a CreateResult.
+func createErr(err error) CreateResult {
+	return CreateResult{gophercloud.CommonResult{Err: err}}
+}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..86d19f2
--- /dev/null
+++ b/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("tokens")
+}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index c970c67..e96da51 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -8,9 +8,6 @@
 	"github.com/rackspace/gophercloud"
 )
 
-// RFC3339Milli describes the time format used by identity API responses.
-const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
-
 // commonResult is the deferred result of a Create or a Get call.
 type commonResult struct {
 	gophercloud.CommonResult
@@ -42,7 +39,7 @@
 	}
 
 	// Attempt to parse the timestamp.
-	token.ExpiresAt, err = time.Parse(RFC3339Milli, response.Token.ExpiresAt)
+	token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
 	if err != nil {
 		return nil, err
 	}
diff --git a/openstack/networking/v2/extensions/delegate.go b/openstack/networking/v2/extensions/delegate.go
new file mode 100644
index 0000000..d08e1fd
--- /dev/null
+++ b/openstack/networking/v2/extensions/delegate.go
@@ -0,0 +1,41 @@
+package extensions
+
+import (
+	"github.com/rackspace/gophercloud"
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"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
+}
+
+// ExtractExtensions interprets a Page as a slice of Extensions.
+func ExtractExtensions(page pagination.Page) ([]Extension, error) {
+	inner, err := common.ExtractExtensions(page)
+	if err != nil {
+		return nil, err
+	}
+	outer := make([]Extension, len(inner))
+	for index, ext := range inner {
+		outer[index] = Extension{ext}
+	}
+	return outer, nil
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) GetResult {
+	return GetResult{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/openstack/networking/v2/extensions/requests_test.go b/openstack/networking/v2/extensions/delegate_test.go
similarity index 77%
rename from openstack/networking/v2/extensions/requests_test.go
rename to openstack/networking/v2/extensions/delegate_test.go
index db21772..8de8906 100755
--- a/openstack/networking/v2/extensions/requests_test.go
+++ b/openstack/networking/v2/extensions/delegate_test.go
@@ -5,6 +5,7 @@
 	"net/http"
 	"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"
@@ -14,7 +15,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -33,7 +34,7 @@
         }
     ]
 }
-			`)
+      `)
 	})
 
 	count := 0
@@ -47,12 +48,14 @@
 
 		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",
+				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",
+				},
 			},
 		}
 
@@ -70,7 +73,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -88,7 +91,7 @@
         "description": "The agent management extension."
     }
 }
-		`)
+    `)
 
 		ext, err := Get(fake.ServiceClient(), "agent").Extract()
 		th.AssertNoErr(t, err)
diff --git a/openstack/networking/v2/extensions/doc.go b/openstack/networking/v2/extensions/doc.go
deleted file mode 100755
index 7942c39..0000000
--- a/openstack/networking/v2/extensions/doc.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Package extensions provides information and interaction with the different
-// extensions available for the OpenStack Neutron service.
-//
-// The purpose of Networking API v2.0 extensions is to:
-//
-// - Introduce new features in the API without requiring a version change.
-// - Introduce vendor-specific niche functionality.
-// - Act as a proving ground for experimental functionalities that might be
-//   included in a future version of the API.
-//
-// Extensions usually have tags that prevent conflicts with other extensions
-// that define attributes or resources with the same names, and with core
-// resources and attributes. Because an extension might not be supported by all
-// plug-ins, its availability varies with deployments and the specific plug-in.
-package extensions
diff --git a/openstack/networking/v2/extensions/external/results_test.go b/openstack/networking/v2/extensions/external/results_test.go
index 41bc0c8..6bed126 100644
--- a/openstack/networking/v2/extensions/external/results_test.go
+++ b/openstack/networking/v2/extensions/external/results_test.go
@@ -15,7 +15,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
@@ -101,7 +101,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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")
@@ -186,7 +186,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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/provider/results_test.go b/openstack/networking/v2/extensions/provider/results_test.go
index ec0b528..74951d9 100644
--- a/openstack/networking/v2/extensions/provider/results_test.go
+++ b/openstack/networking/v2/extensions/provider/results_test.go
@@ -15,7 +15,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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/urls.go b/openstack/networking/v2/extensions/urls.go
deleted file mode 100755
index e31e76c..0000000
--- a/openstack/networking/v2/extensions/urls.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package extensions
-
-import "github.com/rackspace/gophercloud"
-
-const version = "v2.0"
-
-func extensionURL(c *gophercloud.ServiceClient, name string) string {
-	return c.ServiceURL(version, "extensions", name)
-}
-
-func listExtensionURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, "extensions")
-}
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
index 03c297f..6b22acd 100644
--- a/openstack/networking/v2/networks/requests_test.go
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -14,7 +14,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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/urls.go b/openstack/networking/v2/networks/urls.go
index 80c307c..33c2387 100644
--- a/openstack/networking/v2/networks/urls.go
+++ b/openstack/networking/v2/networks/urls.go
@@ -2,14 +2,12 @@
 
 import "github.com/rackspace/gophercloud"
 
-const Version = "v2.0"
-
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(Version, "networks", id)
+	return c.ServiceURL("networks", id)
 }
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(Version, "networks")
+	return c.ServiceURL("networks")
 }
 
 func getURL(c *gophercloud.ServiceClient, id string) string {
diff --git a/openstack/networking/v2/networks/urls_test.go b/openstack/networking/v2/networks/urls_test.go
index 713a547..caf77db 100644
--- a/openstack/networking/v2/networks/urls_test.go
+++ b/openstack/networking/v2/networks/urls_test.go
@@ -10,7 +10,7 @@
 const endpoint = "http://localhost:57909/"
 
 func endpointClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{Endpoint: endpoint}
+	return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
 }
 
 func TestGetURL(t *testing.T) {
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
index 662d364..7576341 100644
--- a/openstack/networking/v2/ports/requests_test.go
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -14,7 +14,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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")
@@ -215,7 +215,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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 +288,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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/urls.go b/openstack/networking/v2/ports/urls.go
index 558b399..6d0572f 100644
--- a/openstack/networking/v2/ports/urls.go
+++ b/openstack/networking/v2/ports/urls.go
@@ -2,14 +2,12 @@
 
 import "github.com/rackspace/gophercloud"
 
-const version = "v2.0"
-
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, "ports", id)
+	return c.ServiceURL("ports", id)
 }
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, "ports")
+	return c.ServiceURL("ports")
 }
 
 func listURL(c *gophercloud.ServiceClient) string {
diff --git a/openstack/networking/v2/ports/urls_tests.go b/openstack/networking/v2/ports/urls_tests.go
index 6fb20aa..7fadd4d 100644
--- a/openstack/networking/v2/ports/urls_tests.go
+++ b/openstack/networking/v2/ports/urls_tests.go
@@ -10,7 +10,7 @@
 const endpoint = "http://localhost:57909/"
 
 func endpointClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{Endpoint: endpoint}
+	return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
 }
 
 func TestListURL(t *testing.T) {
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index c7562f7..9f3e8df 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -14,7 +14,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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")
@@ -252,7 +252,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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")
@@ -304,7 +304,7 @@
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
-	th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+	th.Mux.HandleFunc("/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/urls.go b/openstack/networking/v2/subnets/urls.go
index ca70b66..0d02368 100644
--- a/openstack/networking/v2/subnets/urls.go
+++ b/openstack/networking/v2/subnets/urls.go
@@ -2,14 +2,12 @@
 
 import "github.com/rackspace/gophercloud"
 
-const version = "v2.0"
-
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
-	return c.ServiceURL(version, "subnets", id)
+	return c.ServiceURL("subnets", id)
 }
 
 func rootURL(c *gophercloud.ServiceClient) string {
-	return c.ServiceURL(version, "subnets")
+	return c.ServiceURL("subnets")
 }
 
 func listURL(c *gophercloud.ServiceClient) string {
diff --git a/openstack/networking/v2/subnets/urls_tests.go b/openstack/networking/v2/subnets/urls_tests.go
index b04b432..aeeddf3 100644
--- a/openstack/networking/v2/subnets/urls_tests.go
+++ b/openstack/networking/v2/subnets/urls_tests.go
@@ -10,7 +10,7 @@
 const endpoint = "http://localhost:57909/"
 
 func endpointClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{Endpoint: endpoint}
+	return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
 }
 
 func TestListURL(t *testing.T) {
diff --git a/openstack/utils/client.go b/openstack/utils/client.go
deleted file mode 100644
index f8a6c57..0000000
--- a/openstack/utils/client.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package utils
-
-import (
-	"fmt"
-
-	"github.com/rackspace/gophercloud"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-// Client contains information that defines a generic Openstack Client.
-type Client struct {
-	// Endpoint is the URL against which to authenticate.
-	Endpoint string
-	// Authority holds the results of authenticating against the Endpoint.
-	Authority identity.AuthResults
-	// Options holds the authentication options. Useful for auto-reauthentication.
-	Options gophercloud.AuthOptions
-}
-
-// EndpointOpts contains options for finding an endpoint for an Openstack Client.
-type EndpointOpts struct {
-	// Type is the service type for the client (e.g., "compute", "object-store").
-	// Type is a required field.
-	Type string
-	// Name is the service name for the client (e.g., "nova").
-	// Name is not a required field, but it is used if present. Services can have the
-	// same Type but different Name, which is one example of when both Type and Name are needed.
-	Name string
-	// Region is the region in which the service resides.
-	Region string
-	// URLType is they type of endpoint to be returned (e.g., "public", "private").
-	// URLType is not required, and defaults to "public".
-	URLType string
-}
-
-// NewClient returns a generic Openstack Client of type identity.Client. This is a helper function
-// to create a client for the various Openstack services.
-// Example (error checking omitted for brevity):
-//		ao, err := utils.AuthOptions()
-//		c, err := identity.NewClient(ao, identity.EndpointOpts{
-//			Type: "compute",
-//			Name: "nova",
-//		})
-//		serversClient := servers.NewClient(c.Endpoint, c.Authority, c.Options)
-func NewClient(ao gophercloud.AuthOptions, eo EndpointOpts) (Client, error) {
-	client := Client{
-		Options: ao,
-	}
-
-	c := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
-	ar, err := identity.Authenticate(c, ao)
-	if err != nil {
-		return client, err
-	}
-
-	client.Authority = ar
-
-	sc, err := identity.GetServiceCatalog(ar)
-	if err != nil {
-		return client, err
-	}
-
-	ces, err := sc.CatalogEntries()
-	if err != nil {
-		return client, err
-	}
-
-	var eps []identity.Endpoint
-
-	if eo.Name != "" {
-		for _, ce := range ces {
-			if ce.Type == eo.Type && ce.Name == eo.Name {
-				eps = ce.Endpoints
-			}
-		}
-	} else {
-		for _, ce := range ces {
-			if ce.Type == eo.Type {
-				eps = ce.Endpoints
-			}
-		}
-	}
-
-	var rep string
-	for _, ep := range eps {
-		if ep.Region == eo.Region {
-			switch eo.URLType {
-			case "public":
-				rep = ep.PublicURL
-			case "private":
-				rep = ep.InternalURL
-			default:
-				rep = ep.PublicURL
-			}
-		}
-	}
-
-	if rep != "" {
-		client.Endpoint = rep
-	} else {
-		return client, fmt.Errorf("No endpoint for given service type (%s) name (%s) and region (%s)", eo.Type, eo.Name, eo.Region)
-	}
-
-	return client, nil
-}
diff --git a/rackspace/monitoring/common.go b/rackspace/monitoring/common.go
deleted file mode 100644
index 900c86e..0000000
--- a/rackspace/monitoring/common.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package monitoring
-
-import (
-	"github.com/rackspace/gophercloud"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-type Options struct {
-	Endpoint       string
-	AuthOptions    gophercloud.AuthOptions
-	Authentication identity.AuthResults
-}
diff --git a/rackspace/monitoring/notificationPlans/requests.go b/rackspace/monitoring/notificationPlans/requests.go
deleted file mode 100644
index d4878c4..0000000
--- a/rackspace/monitoring/notificationPlans/requests.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package notificationPlans
-
-import (
-	"fmt"
-
-	"github.com/racker/perigee"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-	"github.com/rackspace/gophercloud/rackspace/monitoring"
-)
-
-var ErrNotImplemented = fmt.Errorf("notificationPlans feature not yet implemented")
-
-type Client struct {
-	options monitoring.Options
-}
-
-type DeleteResults map[string]interface{}
-
-func NewClient(mo monitoring.Options) *Client {
-	return &Client{
-		options: mo,
-	}
-}
-
-func (c *Client) Delete(id string) (DeleteResults, error) {
-	var dr DeleteResults
-
-	tok, err := identity.GetToken(c.options.Authentication)
-	if err != nil {
-		return nil, err
-	}
-	url := fmt.Sprintf("%s/notification_plans/%s", c.options.Endpoint, id)
-	err = perigee.Delete(url, perigee.Options{
-		Results: &dr,
-		OkCodes: []int{204},
-		MoreHeaders: map[string]string{
-			"X-Auth-Token": tok.ID,
-		},
-	})
-	return dr, err
-}
diff --git a/results.go b/results.go
index 3ca7c94..f7d526c 100644
--- a/results.go
+++ b/results.go
@@ -7,3 +7,6 @@
 	Resp map[string]interface{}
 	Err  error
 }
+
+// RFC3339Milli describes a time format used by API responses.
+const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
diff --git a/service_client.go b/service_client.go
index 83ad69b..55d3b98 100644
--- a/service_client.go
+++ b/service_client.go
@@ -11,9 +11,28 @@
 	// Endpoint is the base URL of the service's API, acquired from a service catalog.
 	// It MUST end with a /.
 	Endpoint string
+
+	// ResourceBase is the base URL shared by the resources within a service's API. It should include
+	// the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used
+	// as-is, instead.
+	ResourceBase string
+}
+
+// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
+func (client *ServiceClient) ResourceBaseURL() string {
+	if client.ResourceBase != "" {
+		return client.ResourceBase
+	}
+	return client.Endpoint
 }
 
 // ServiceURL constructs a URL for a resource belonging to this provider.
 func (client *ServiceClient) ServiceURL(parts ...string) string {
-	return client.Endpoint + strings.Join(parts, "/")
+	return client.ResourceBaseURL() + strings.Join(parts, "/")
+}
+
+// AuthenticatedHeaders returns a collection of HTTP request headers that mark a request as
+// belonging to the currently authenticated user.
+func (client *ServiceClient) AuthenticatedHeaders() map[string]string {
+	return client.Provider.AuthenticatedHeaders()
 }