Merge branch 'v0.2.0'
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 2655ebc..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,16 +0,0 @@
-# EditorConfig is awesome: http://EditorConfig.org
-
-# top-most EditorConfig file
-root = true
-
-# All files
-[*]
-end_of_line = lf
-insert_final_newline = true
-charset = utf-8
-trim_trailing_whitespace = true
-
-# Golang
-[*.go]
-indent_style = tab
-indent_size = 2
diff --git a/.travis.yml b/.travis.yml
index 6e1dbd0..cf4f8ca 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,14 @@
 language: go
 install:
-  - go get -v .
+  - go get -v -tags 'fixtures acceptance' ./...
 go:
   - 1.1
   - 1.2
   - tip
+script: script/cibuild
 after_success:
   - go get code.google.com/p/go.tools/cmd/cover
   - go get github.com/axw/gocov/gocov
   - go get github.com/mattn/goveralls
   - export PATH=$PATH:$HOME/gopath/bin/
   - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..a293b50
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,275 @@
+# Contributing to gophercloud
+
+- [Getting started](#getting-started)
+- [Tests](#tests)
+- [Style guide](#basic-style-guide)
+- [5 ways to get involved](#5-ways-to-get-involved)
+
+## Setting up your git workspace
+
+As a contributor you will need to setup your workspace in a slightly different
+way than just downloading it. Here are the basic installation instructions:
+
+1. Configure your `$GOPATH` and run `go get` as described in the main
+[README](/#how-to-install).
+
+2. Move into the directory that houses your local repository:
+
+   ```bash
+   cd ${GOPATH}/src/github.com/rackspace/gophercloud
+   ```
+
+3. Fork the `rackspace/gophercloud` repository and update your remote refs. You
+will need to rename the `origin` remote branch to `upstream`, and add your
+fork as `origin` instead:
+
+   ```bash
+   git remote rename origin upstream
+   git remote add origin git@github.com/<my_username>/gophercloud
+   ```
+
+4. Checkout the latest development branch ([click here](/branches) to see all
+the branches):
+
+   ```bash
+   git checkout v0.2.0
+   ```
+
+5. If you're working on something (discussed more in detail below), you will
+need to checkout a new feature branch:
+
+   ```bash
+   git checkout -b my-new-feature
+   ```
+
+Another thing to bear in mind is that you will need to add a few extra
+environment variables for acceptance tests - this is documented in our
+[acceptance tests readme](/acceptance).
+
+## Tests
+
+When working on a new or existing feature, testing will be the backbone of your
+work since it helps uncover and prevent regressions in the codebase. There are
+two types of test we use in gophercloud: unit tests and acceptance tests, which
+are both described below.
+
+### Unit tests
+
+Unit tests are the fine-grained tests that establish and ensure the behaviour
+of individual units of functionality. We usually test on an
+operation-by-operation basis (an operation typically being an API action) with
+the use of mocking to set up explicit expectations. Each operation will set up
+its HTTP response expectation, and then test how the system responds when fed
+this controlled, pre-determined input.
+
+To make life easier, we've introduced a bunch of test helpers to simplify the
+process of testing expectations with assertions:
+
+```go
+import (
+  "testing"
+
+  "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSomething(t *testing.T) {
+  result, err := Operation()
+
+  testhelper.AssertEquals(t, "foo", result.Bar)
+  testhelper.AssertNoErr(t, err)
+}
+
+func TestSomethingElse(t *testing.T) {
+  testhelper.CheckEquals(t, "expected", "actual")
+}
+```
+
+`AssertEquals` and `AssertNoErr` will throw a fatal error if a value does not
+match an expected value or if an error has been declared, respectively. You can
+also use `CheckEquals` and `CheckNoErr` for the same purpose; the only difference
+being that `t.Errorf` is raised rather than `t.Fatalf`.
+
+Here is a truncated example of mocked HTTP responses:
+
+```go
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGet(t *testing.T) {
+	// Setup the HTTP request multiplexer and server
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		// Test we're using the correct HTTP method
+		th.TestMethod(t, r, "GET")
+
+		// Test we're setting the auth token
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		// Set the appropriate headers for our mocked response
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		// Set the HTTP body
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "name": "private-network",
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "shared": true,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+			`)
+	})
+
+	// Call our API operation
+	network, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+
+	// Assert no errors and equality
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Status, "ACTIVE")
+}
+```
+
+### Acceptance tests
+
+As we've already mentioned, unit tests have a very narrow and confined focus -
+they test small units of behaviour. Acceptance tests on the other hand have a
+far larger scope: they are fully functional tests that test the entire API of a
+service in one fell swoop. They don't care about unit isolation or mocking
+expectations, they instead do a full run-through and consequently test how the
+entire system _integrates_ together. When an API satisfies expectations, it
+proves by default that the requirements for a contract have been met.
+
+Please be aware that acceptance tests will hit a live API - and may incur
+service charges from your provider. Although most tests handle their own
+teardown procedures, it is always worth manually checking that resources are
+deleted after the test suite finishes.
+
+### Running tests
+
+To run all tests:
+
+```bash
+go test ./...
+```
+
+To run all tests with verbose output:
+
+```bash
+go test -v ./...
+```
+
+To run tests that match certain [build tags]():
+
+```bash
+go test -tags "foo bar" ./...
+```
+
+To run tests for a particular sub-package:
+
+```bash
+cd ./path/to/package && go test .
+```
+
+## Basic style guide
+
+We follow the standard formatting recommendations and language idioms set out
+in the [Effective Go](https://golang.org/doc/effective_go.html) guide. It's
+definitely worth reading - but the relevant sections are
+[formatting](https://golang.org/doc/effective_go.html#formatting)
+and [names](https://golang.org/doc/effective_go.html#names).
+
+## 5 ways to get involved
+
+There are five main ways you can get involved in our open-source project, and
+each is described briefly below. Once you've made up your mind and decided on
+your fix, you will need to follow the same basic steps that all submissions are
+required to adhere to:
+
+1. [fork](https://help.github.com/articles/fork-a-repo/) the `rackspace/gophercloud` repository
+2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches)
+3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/)
+
+### 1. Providing feedback
+
+On of the easiest ways to get readily involved in our project is to let us know
+about your experiences using our SDK. Feedback like this is incredibly useful
+to us, because it allows us to refine and change features based on what our
+users want and expect of us. There are a bunch of ways to get in contact! You
+can [ping us](mailto:sdk-support@rackspace.com) via e-mail, talk to us on irc
+(#rackspace-dev on freenode), [tweet us](https://twitter.com/rackspace), or
+submit an issue on our [bug tracker](/issues). Things you might like to tell us
+are:
+
+* how easy was it to start using our SDK?
+* did it meet your expectations? If not, why not?
+* did our documentation help or hinder you?
+* what could we improve in general?
+
+### 2. Fixing bugs
+
+If you want to start fixing open bugs, we'd really appreciate that! Bug fixing
+is central to any project. The best way to get started is by heading to our
+[bug tracker](https://github.com/rackspace/gophercloud/issues) and finding open
+bugs that you think nobody is working on. It might be useful to comment on the
+thread to see the current state of the issue and if anybody has made any
+breakthroughs on it so far.
+
+### 3. Improving documentation
+
+We have three forms of documentation:
+
+* short README documents that briefly introduce a topic
+* reference documentation on [godoc.org](http://godoc.org) that is automatically
+generated from source code comments
+* user documentation on our [homepage](http://gophercloud.io) that includes
+getting started guides, installation guides and code samples
+
+If you feel that a certain section could be improved - whether it's to clarify
+ambiguity, correct a technical mistake, or to fix a grammatical error - please
+feel entitled to do so! We welcome doc pull requests with the same childlike
+enthusiasm as any other contribution!
+
+### 4. Optimizing existing features
+
+If you would like to improve or optimize an existing feature, please be aware
+that we adhere to [semantic versioning](http://semver.org) - which means that
+we cannot introduce breaking changes to the API without a major version change
+(v1.x -> v2.x). Making that leap is a big step, so we encourage contributors to
+refactor rather than rewrite. Running tests will prevent regression and avoid
+the possibility of breaking somebody's current implementation.
+
+Another tip is to keep the focus of your work as small as possible - try not to
+introduce a change that affects lots and lots of files because it introduces
+added risk and increases the cognitive load on the reviewers checking your
+work. Change-sets which are easily understood and will not negatively impact
+users are more likely to be integrated quickly.
+
+Lastly, if you're seeking to optimize a particular operation, you should try to
+demonstrate a negative performance impact - perhaps using go's inbuilt
+[benchmark capabilities](http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go).
+
+### 5. Working on a new feature
+
+If you've found something we've left out, definitely feel free to start work on
+introducing that feature. It's always useful to open an issue or submit a pull
+request early on to indicate your intent to a core contributor - this enables
+quick/early feedback and can help steer you in the right direction by avoiding
+known issues. It might also help you avoid losing time implementing something
+that might not ever work. One tip is to prefix your Pull Request issue title
+with [wip] - then people know it's a work in progress.
+
+You must ensure that all of your work is well tested - both in terms of unit
+and acceptance tests. Untested code will not be merged because it introduces
+too much of a risk to end-users.
+
+Happy hacking!
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 9076695..eb97094 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -1,6 +1,12 @@
 Contributors
 ============
 
-Samuel A. Falvo II <sam.falvo@rackspace.com>
-Glen Campbell <glen.campbell@rackspace.com>
-Jesse Noller <jesse.noller@rackspace.com>
+| Name | Email |
+| ---- | ----- |
+| Samuel A. Falvo II | <sam.falvo@rackspace.com>
+| Glen Campbell | <glen.campbell@rackspace.com>
+| Jesse Noller | <jesse.noller@rackspace.com>
+| Jon Perritt | <jon.perritt@rackspace.com>
+| Ash Wilson | <ash.wilson@rackspace.com>
+| Jamie Hannaford | <jamie.hannaford@rackspace.com>
+| Don Schenck | don.schenck@rackspace.com>
diff --git a/README.asciidoc b/README.asciidoc
deleted file mode 100644
index b7a7c01..0000000
--- a/README.asciidoc
+++ /dev/null
@@ -1,44 +0,0 @@
-== Gophercloud -- V0.1.0 image:https://secure.travis-ci.org/rackspace/gophercloud.png?branch=master["build status",link="https://travis-ci.org/rackspace/gophercloud"]
-
-Gophercloud currently lets you authenticate with OpenStack providers to create and manage servers.
-We are working on extending the API to further include cloud files, block storage, DNS, databases, security groups, and other features.
-
-WARNING: This library is still in the very early stages of development. Unless you want to contribute, it probably isn't what you want.  Yet.
-
-=== Outstanding Features
-
-1.  Apache 2.0 License, making Gophercloud friendly to commercial and open-source enterprises alike.
-2.  Gophercloud is one of the most actively maintained Go SDKs for OpenStack.
-3.  Gophercloud supports Identity V2 and Nova V2 APIs.  More coming soon!
-4.  The up-coming Gophercloud 0.2.0 release supports API extensions, and makes writing support for new extensions easy.
-5.  Gophercloud supports automatic reauthentication upon auth token timeout, if enabled by your software.
-6.  Gophercloud is the only SDK implementation with actual acceptance-level integration tests.
-
-=== What Does it Look Like?
-
-The Gophercloud 0.1.0 and earlier APIs are now deprecated and obsolete.
-No new feature development will occur for 0.1.0 or 0.0.0.
-However, we will accept and provide bug fixes for these APIs.
-Please refer to the acceptance tests in the master brach for code examples using the v0.1.0 API.
-The most up to date documentation for version 0.1.x can be found at link:http://godoc.org/github.com/rackspace/gophercloud[our Godoc.org documentation].
-
-We are working on a new API that provides much better support for extensions, pagination, and other features that proved difficult to implement before.
-This new API will be substantially more Go-idiomatic as well; one of the complaints received about 0.1.x and earlier is that it didn't "feel" right.
-To see what this new API is going to look like, you can look at the code examples up on the link:http://gophercloud.io/docs.html[Gophercloud website].
-If you're interested in tracking progress, note that features for version 0.2.0 will appear in the `v0.2.0` branch until merged to master.
-
-=== How can I Contribute?
-
-After using Gophercloud for a while, you might find that it lacks some useful feature, or that existing behavior seems buggy.  We welcome contributions
-from our users for both missing functionality as well as for bug fixes.  We encourage contributors to collaborate with the
-link:http://gophercloud.io/community.html[Gophercloud community.]
-
-Finally, Gophercloud maintains its own link:http://gophercloud.io[announcements and updates blog.]
-Feel free to check back now and again to see what's new.
-
-== License
-
-Copyright (C) 2013, 2014 Rackspace, Inc.
-
-Licensed under the Apache License, Version 2.0
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..96dcd31
--- /dev/null
+++ b/README.md
@@ -0,0 +1,154 @@
+# gophercloud: the OpenStack SDK for Go
+[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=v0.2.0)](https://travis-ci.org/rackspace/gophercloud)
+
+gophercloud is a flexible SDK that allows you to consume and work with OpenStack
+clouds in a simple and idiomatic way using golang. Many services are supported,
+including Compute, Block Storage, Object Storage, Networking, and Identity.
+Each service API is backed with getting started guides, code samples, reference
+documentation, unit tests and acceptance tests.
+
+## Useful links
+
+* [gophercloud homepage](http://gophercloud.io)
+* [Reference documentation](http://godoc.org/github.com/rackspace/gophercloud)
+* [Getting started guides](http://gophercloud.io/docs)
+* [Effective Go](https://golang.org/doc/effective_go.html)
+
+## How to install
+
+Before installing, you need to ensure that your [GOPATH environment variable](https://golang.org/doc/code.html#GOPATH)
+is pointing to an appropriate directory where you want to install gophercloud:
+
+```bash
+mkdir $HOME/go
+export GOPATH=$HOME/go
+```
+
+Once this is set up, you can install the gophercloud package like so:
+
+```bash
+go get github.com/rackspace/gophercloud
+```
+
+This will install all the source files you need into a `pkg` directory, which is
+referenceable from your own source files.
+
+## Getting started
+
+### Credentials
+
+Because you'll be hitting an API, you will need to retrieve your OpenStack
+credentials and either store them as environment variables or in your local Go
+files. The first method is recommended because it decouples credential
+information from source code, allowing you to push the latter to your version
+control system without any security risk.
+
+You will need to retrieve the following:
+
+* username
+* password
+* tenant name or tenant ID
+* a valid Keystone identity URL
+
+For users that have the OpenStack dashboard installed, there's a shortcut. If
+you visit the `project/access_and_security` path in Horizon and click on the
+"Download OpenStack RC File" button at the top right hand corner, you will
+download a bash file that exports all of your access details to environment
+variables. To execute the file, run `source admin-openrc.sh` and you will be
+prompted for your password.
+
+### Authentication
+
+Once you have access to your credentials, you can begin plugging them into
+gophercloud. The next step is authentication, and this is handled by a base
+"Provider" struct. To get one, you can either pass in your credentials
+explicitly, or tell gophercloud to use environment variables:
+
+```go
+import (
+  "github.com/rackspace/gophercloud"
+  "github.com/rackspace/gophercloud/openstack"
+  "github.com/rackspace/gophercloud/openstack/utils"
+)
+
+// Option 1: Pass in the values yourself
+opts := gophercloud.AuthOptions{
+  IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
+  Username: "{username}",
+  Password: "{password}",
+  TenantID: "{tenant_id}",
+}
+
+// Option 2: Use a utility function to retrieve all your environment variables
+opts, err := openstack.AuthOptionsFromEnv()
+```
+
+Once you have the `opts` variable, you can pass it in and get back a
+`ProviderClient` struct:
+
+```go
+provider, err := openstack.AuthenticatedClient(opts)
+```
+
+The `ProviderClient` is the top-level client that all of your OpenStack services
+derive from. The provider contains all of the authentication details that allow
+your Go code to access the API - such as the base URL and token ID.
+
+### Provision a server
+
+Once we have a base Provider, we inject it as a dependency into each OpenStack
+service. In order to work with the Compute API, we need a Compute service
+client; which can be created like so:
+
+```go
+client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
+  Region: os.Getenv("OS_REGION_NAME"),
+})
+```
+
+We then use this `client` for any Compute API operation we want. In our case,
+we want to provision a new server - so we invoke the `Create` method and pass
+in the flavor ID (hardware specification) and image ID (operating system) we're
+interested in:
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+server, err := servers.Create(client, servers.CreateOpts{
+  Name:      "My new server!",
+  FlavorRef: "flavor_id",
+  ImageRef:  "image_id",
+}).Extract()
+```
+
+If you are unsure about what images and flavors are, you can read our [Compute
+Getting Started guide](http://gophercloud.io/docs/compute). The above code
+sample creates a new server with the parameters, and embodies the new resource
+in the `server` variable (a
+[`servers.Server`](http://godoc.org/github.com/rackspace/gophercloud) struct).
+
+### Next steps
+
+Cool! You've handled authentication, got your `ProviderClient` and provisioned
+a new server. You're now ready to use more OpenStack services.
+
+* [Getting started with Compute](http://gophercloud.io/docs/compute)
+* [Getting started with Object Storage](http://gophercloud.io/docs/object-storage)
+* [Getting started with Networking](http://gophercloud.io/docs/networking)
+* [Getting started with Block Storage](http://gophercloud.io/docs/block-storage)
+* [Getting started with Identity](http://gophercloud.io/docs/identity)
+
+## Contributing
+
+Engaging the community and lowering barriers for contributors is something we
+care a lot about. For this reason, we've taken the time to write a [contributing
+guide](./CONTRIBUTING.md) for folks interested in getting involved in our project.
+If you're not sure how you can get involved, feel free to submit an issue or
+[e-mail us](mailto:sdk-support@rackspace.com) privately. You don't need to be a
+Go expert - all members of the community are welcome!
+
+## Help and feedback
+
+If you're struggling with something or have spotted a potential bug, feel free
+to submit an issue to our [bug tracker](/issues) or e-mail us directly at
+[sdk-support@rackspace.com](mailto:sdk-support@rackspace.com).
diff --git a/acceptance/00-authentication.go b/acceptance/00-authentication.go
deleted file mode 100644
index 6467203..0000000
--- a/acceptance/00-authentication.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"fmt"
-	"github.com/rackspace/gophercloud"
-	"os"
-	"strings"
-)
-
-func main() {
-	provider, username, _, apiKey := getCredentials()
-
-	if !strings.Contains(provider, "rackspace") {
-		fmt.Fprintf(os.Stdout, "Skipping test because provider doesn't support API_KEYs\n")
-		return
-	}
-
-	_, err := gophercloud.Authenticate(
-		provider,
-		gophercloud.AuthOptions{
-			Username: username,
-			ApiKey:   apiKey,
-		},
-	)
-	if err != nil {
-		panic(err)
-	}
-}
diff --git a/acceptance/01-authentication.go b/acceptance/01-authentication.go
deleted file mode 100644
index 5cc9d38..0000000
--- a/acceptance/01-authentication.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"github.com/rackspace/gophercloud"
-)
-
-func main() {
-	provider, username, password, _ := getCredentials()
-
-	_, err := gophercloud.Authenticate(
-		provider,
-		gophercloud.AuthOptions{
-			Username: username,
-			Password: password,
-		},
-	)
-	if err != nil {
-		panic(err)
-	}
-}
diff --git a/acceptance/02-list-servers.go b/acceptance/02-list-servers.go
deleted file mode 100644
index 772852e..0000000
--- a/acceptance/02-list-servers.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(api gophercloud.CloudServersProvider) {
-			tryFullDetails(api)
-			tryLinksOnly(api)
-		})
-	})
-}
-
-func tryLinksOnly(api gophercloud.CloudServersProvider) {
-	servers, err := api.ListServersLinksOnly()
-	if err != nil {
-		panic(err)
-	}
-
-	if !*quiet {
-		fmt.Println("Id,Name")
-		for _, s := range servers {
-			if s.AccessIPv4 != "" {
-				panic("IPv4 not expected")
-			}
-
-			if s.Status != "" {
-				panic("Status not expected")
-			}
-
-			if s.Progress != 0 {
-				panic("Progress not expected")
-			}
-
-			fmt.Printf("%s,\"%s\"\n", s.Id, s.Name)
-		}
-	}
-}
-
-func tryFullDetails(api gophercloud.CloudServersProvider) {
-	servers, err := api.ListServers()
-	if err != nil {
-		panic(err)
-	}
-
-	if !*quiet {
-		fmt.Println("Id,Name,AccessIPv4,Status,Progress")
-		for _, s := range servers {
-			fmt.Printf("%s,\"%s\",%s,%s,%d\n", s.Id, s.Name, s.AccessIPv4, s.Status, s.Progress)
-		}
-	}
-}
diff --git a/acceptance/03-get-server-details.go b/acceptance/03-get-server-details.go
deleted file mode 100644
index 01140a9..0000000
--- a/acceptance/03-get-server-details.go
+++ /dev/null
@@ -1,134 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-	"os"
-	"github.com/racker/perigee"
-)
-
-var id = flag.String("i", "", "Server ID to get info on.  Defaults to first server in your account if unspecified.")
-var rgn = flag.String("r", "", "Datacenter region.  Leave blank for default region.")
-var quiet = flag.Bool("quiet", false, "Run quietly, for acceptance testing.  $? non-zero if issue.")
-
-func main() {
-	flag.Parse()
-
-	resultCode := 0
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			var (
-				err              error
-				serverId         string
-				deleteAfterwards bool
-			)
-
-			// Figure out which server to provide server details for.
-			if *id == "" {
-				deleteAfterwards, serverId, err = locateAServer(servers)
-				if err != nil {
-					panic(err)
-				}
-				if deleteAfterwards {
-					defer servers.DeleteServerById(serverId)
-				}
-			} else {
-				serverId = *id
-			}
-
-			// Grab server details by ID, and provide a report.
-			s, err := servers.ServerById(serverId)
-			if err != nil {
-				panic(err)
-			}
-
-			configs := []string{
-				"Access IPv4: %s\n",
-				"Access IPv6: %s\n",
-				"    Created: %s\n",
-				"     Flavor: %s\n",
-				"    Host ID: %s\n",
-				"         ID: %s\n",
-				"      Image: %s\n",
-				"       Name: %s\n",
-				"   Progress: %s\n",
-				"     Status: %s\n",
-				"  Tenant ID: %s\n",
-				"    Updated: %s\n",
-				"    User ID: %s\n",
-			}
-
-			values := []string{
-				s.AccessIPv4,
-				s.AccessIPv6,
-				s.Created,
-				s.Flavor.Id,
-				s.HostId,
-				s.Id,
-				s.Image.Id,
-				s.Name,
-				fmt.Sprintf("%d", s.Progress),
-				s.Status,
-				s.TenantId,
-				s.Updated,
-				s.UserId,
-			}
-
-			if !*quiet {
-				fmt.Println("Server info:")
-				for i, _ := range configs {
-					fmt.Printf(configs[i], values[i])
-				}
-			}
-		})
-
-		// Negative test -- We should absolutely never panic for a server that doesn't exist.
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			_, err := servers.ServerById(randomString("garbage", 32))
-			if err == nil {
-				fmt.Printf("Expected a 404 response when looking for a server known not to exist\n")
-				resultCode = 1
-			}
-			perigeeError, ok := err.(*perigee.UnexpectedResponseCodeError)
-			if !ok {
-				fmt.Printf("Unexpected error type\n")
-				resultCode = 1
-			} else {
-				if perigeeError.Actual != 404 {
-					fmt.Printf("Expected a 404 error code\n")
-				}
-			}
-		})
-	})
-	os.Exit(resultCode)
-}
-
-// locateAServer queries the set of servers owned by the user.  If at least one
-// exists, the first found is picked, and its ID is returned.  Otherwise, a new
-// server will be created, and its ID returned.
-//
-// deleteAfter will be true if the caller should schedule a call to DeleteServerById()
-// to clean up.
-func locateAServer(servers gophercloud.CloudServersProvider) (deleteAfter bool, id string, err error) {
-	ss, err := servers.ListServers()
-	if err != nil {
-		return false, "", err
-	}
-
-	if len(ss) > 0 {
-		// We could just cheat and dump the server details from ss[0].
-		// But, that tests ListServers(), and not ServerById().  So, we
-		// elect not to cheat.
-		return false, ss[0].Id, nil
-	}
-
-	serverId, err := createServer(servers, "", "", "", "")
-	if err != nil {
-		return false, "", err
-	}
-	err = waitForServerState(servers, serverId, "ACTIVE")
-	return true, serverId, err
-}
diff --git a/acceptance/04-create-server.go b/acceptance/04-create-server.go
deleted file mode 100644
index 03fd606..0000000
--- a/acceptance/04-create-server.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var region, serverName, imageRef, flavorRef *string
-var adminPass = flag.String("a", "", "Administrator password (auto-assigned if none)")
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance tests.  $? non-zero if error.")
-
-func configure() {
-	region = flag.String("r", "", "Region in which to create the server.  Leave blank for provider-default region.")
-	serverName = flag.String("n", randomString("ACPTTEST--", 16), "Server name (what you see in the control panel)")
-	imageRef = flag.String("i", "", "ID of image to deploy onto the server")
-	flavorRef = flag.String("f", "", "Flavor of server to deploy image upon")
-
-	flag.Parse()
-}
-
-func main() {
-	configure()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			_, err := createServer(servers, *imageRef, *flavorRef, *serverName, *adminPass)
-			if err != nil {
-				panic(err)
-			}
-
-			allServers, err := servers.ListServers()
-			if err != nil {
-				panic(err)
-			}
-
-			if !*quiet {
-				fmt.Printf("ID,Name,Status,Progress\n")
-				for _, i := range allServers {
-					fmt.Printf("%s,\"%s\",%s,%d\n", i.Id, i.Name, i.Status, i.Progress)
-				}
-			}
-		})
-	})
-}
diff --git a/acceptance/05-list-images.go b/acceptance/05-list-images.go
deleted file mode 100644
index 5ead18b..0000000
--- a/acceptance/05-list-images.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing.  $? non-zero on error though.")
-var rgn = flag.String("r", "", "Datacenter region to interrogate.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			images, err := servers.ListImages()
-			if err != nil {
-				panic(err)
-			}
-
-			if !*quiet {
-				fmt.Println("ID,Name,MinRam,MinDisk")
-				for _, image := range images {
-					fmt.Printf("%s,\"%s\",%d,%d\n", image.Id, image.Name, image.MinRam, image.MinDisk)
-				}
-			}
-		})
-	})
-}
diff --git a/acceptance/06-list-flavors.go b/acceptance/06-list-flavors.go
deleted file mode 100644
index 65db7da..0000000
--- a/acceptance/06-list-flavors.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing.  $? non-zero on error though.")
-var rgn = flag.String("r", "", "Datacenter region to interrogate.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			flavors, err := servers.ListFlavors()
-			if err != nil {
-				panic(err)
-			}
-
-			if !*quiet {
-				fmt.Println("ID,Name,MinRam,MinDisk")
-				for _, f := range flavors {
-					fmt.Printf("%s,\"%s\",%d,%d\n", f.Id, f.Name, f.Ram, f.Disk)
-				}
-			}
-		})
-	})
-}
diff --git a/acceptance/07-change-admin-password.go b/acceptance/07-change-admin-password.go
deleted file mode 100644
index 880fbe8..0000000
--- a/acceptance/07-change-admin-password.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-var serverId = flag.String("i", "", "ID of server whose admin password is to be changed.")
-var newPass = flag.String("p", "", "New password for the server.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(api gophercloud.CloudServersProvider) {
-			// If user doesn't explicitly provide a server ID, create one dynamically.
-			if *serverId == "" {
-				var err error
-				*serverId, err = createServer(api, "", "", "", "")
-				if err != nil {
-					panic(err)
-				}
-				waitForServerState(api, *serverId, "ACTIVE")
-			}
-
-			// If no password is provided, create one dynamically.
-			if *newPass == "" {
-				*newPass = randomString("", 16)
-			}
-
-			// Submit the request for changing the admin password.
-			// Note that we don't verify this actually completes;
-			// doing so is beyond the scope of the SDK, and should be
-			// the responsibility of your specific OpenStack provider.
-			err := api.SetAdminPassword(*serverId, *newPass)
-			if err != nil {
-				panic(err)
-			}
-
-			if !*quiet {
-				fmt.Println("Password change request submitted.")
-			}
-		})
-	})
-}
diff --git a/acceptance/08-reauthentication.go b/acceptance/08-reauthentication.go
deleted file mode 100644
index c46f5bb..0000000
--- a/acceptance/08-reauthentication.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing.  $? non-zero on error though.")
-var rgn = flag.String("r", "", "Datacenter region to interrogate.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	// Invoke withIdentity such that re-auth is enabled.
-	withIdentity(true, func(auth gophercloud.AccessProvider) {
-		token1 := auth.AuthToken()
-
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			// Just to confirm everything works, we should be able to list images without error.
-			_, err := servers.ListImages()
-			if err != nil {
-				panic(err)
-			}
-
-			// Revoke our current authentication token.
-			auth.Revoke(auth.AuthToken())
-
-			// Attempt to list images again.  This should _succeed_, because we enabled re-authentication.
-			_, err = servers.ListImages()
-			if err != nil {
-				panic(err)
-			}
-
-			// However, our new authentication token should differ.
-			token2 := auth.AuthToken()
-
-			if !*quiet {
-				fmt.Println("Old authentication token: ", token1)
-				fmt.Println("New authentication token: ", token2)
-			}
-
-			if token1 == token2 {
-				panic("Tokens should differ")
-			}
-		})
-	})
-}
diff --git a/acceptance/09-resize-server.go b/acceptance/09-resize-server.go
deleted file mode 100644
index a2ef3c8..0000000
--- a/acceptance/09-resize-server.go
+++ /dev/null
@@ -1,102 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-	"time"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(api gophercloud.CloudServersProvider) {
-			// These tests are going to take some time to complete.
-			// So, we'll do two tests at the same time to help amortize test time.
-			done := make(chan bool)
-			go resizeRejectTest(api, done)
-			go resizeAcceptTest(api, done)
-			_ = <-done
-			_ = <-done
-
-			if !*quiet {
-				fmt.Println("Done.")
-			}
-		})
-	})
-}
-
-// Perform the resize test, but reject the resize request.
-func resizeRejectTest(api gophercloud.CloudServersProvider, done chan bool) {
-	withServer(api, func(id string) {
-		newFlavorId := findAlternativeFlavor()
-		err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "")
-		if err != nil {
-			panic(err)
-		}
-
-		waitForServerState(api, id, "VERIFY_RESIZE")
-
-		err = api.RevertResize(id)
-		if err != nil {
-			panic(err)
-		}
-	})
-	done <- true
-}
-
-// Perform the resize test, but accept the resize request.
-func resizeAcceptTest(api gophercloud.CloudServersProvider, done chan bool) {
-	withServer(api, func(id string) {
-		newFlavorId := findAlternativeFlavor()
-		err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "")
-		if err != nil {
-			panic(err)
-		}
-
-		waitForServerState(api, id, "VERIFY_RESIZE")
-
-		err = api.ConfirmResize(id)
-		if err != nil {
-			panic(err)
-		}
-	})
-	done <- true
-}
-
-func withServer(api gophercloud.CloudServersProvider, f func(string)) {
-	id, err := createServer(api, "", "", "", "")
-	if err != nil {
-		panic(err)
-	}
-
-	for {
-		s, err := api.ServerById(id)
-		if err != nil {
-			panic(err)
-		}
-		if s.Status == "ACTIVE" {
-			break
-		}
-		time.Sleep(10 * time.Second)
-	}
-
-	f(id)
-
-	// I've learned that resizing an instance can fail if a delete request
-	// comes in prior to its completion.  This ends up leaving the server
-	// in an error state, and neither the resize NOR the delete complete.
-	// This is a bug in OpenStack, as far as I'm concerned, but thankfully,
-	// there's an easy work-around -- just wait for your server to return to
-	// active state first!
-	waitForServerState(api, id, "ACTIVE")
-	err = api.DeleteServerById(id)
-	if err != nil {
-		panic(err)
-	}
-}
diff --git a/acceptance/10-reboot-server.go b/acceptance/10-reboot-server.go
deleted file mode 100644
index ba6215a..0000000
--- a/acceptance/10-reboot-server.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(servers gophercloud.CloudServersProvider) {
-			log("Creating server")
-			serverId, err := createServer(servers, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, serverId, "ACTIVE")
-
-			log("Soft-rebooting server")
-			servers.RebootServer(serverId, false)
-			waitForServerState(servers, serverId, "REBOOT")
-			waitForServerState(servers, serverId, "ACTIVE")
-
-			log("Hard-rebooting server")
-			servers.RebootServer(serverId, true)
-			waitForServerState(servers, serverId, "HARD_REBOOT")
-			waitForServerState(servers, serverId, "ACTIVE")
-
-			log("Done")
-			servers.DeleteServerById(serverId)
-		})
-	})
-}
-
-func log(s string) {
-	if !*quiet {
-		fmt.Println(s)
-	}
-}
diff --git a/acceptance/11-rescue-unrescue-server.go b/acceptance/11-rescue-unrescue-server.go
deleted file mode 100644
index 008ad9d..0000000
--- a/acceptance/11-rescue-unrescue-server.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(servers gophercloud.CloudServersProvider) {
-			log("Creating server")
-			id, err := createServer(servers, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, id, "ACTIVE")
-			defer servers.DeleteServerById(id)
-
-			log("Rescuing server")
-			adminPass, err := servers.RescueServer(id)
-			if err != nil {
-				panic(err)
-			}
-			log("  Admin password = " + adminPass)
-			if len(adminPass) < 1 {
-				panic("Empty admin password")
-			}
-			waitForServerState(servers, id, "RESCUE")
-
-			log("Unrescuing server")
-			err = servers.UnrescueServer(id)
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, id, "ACTIVE")
-
-			log("Done")
-		})
-	})
-}
-
-func log(s string) {
-	if !*quiet {
-		fmt.Println(s)
-	}
-}
diff --git a/acceptance/12-update-server.go b/acceptance/12-update-server.go
deleted file mode 100644
index c0191f1..0000000
--- a/acceptance/12-update-server.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(servers gophercloud.CloudServersProvider) {
-			log("Creating server")
-			id, err := createServer(servers, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, id, "ACTIVE")
-			defer servers.DeleteServerById(id)
-
-			log("Updating name of server")
-			newName := randomString("ACPTTEST", 32)
-			newDetails, err := servers.UpdateServer(id, gophercloud.NewServerSettings{
-				Name: newName,
-			})
-			if err != nil {
-				panic(err)
-			}
-			if newDetails.Name != newName {
-				panic("Name change didn't appear to take")
-			}
-
-			log("Done")
-		})
-	})
-}
-
-func log(s string) {
-	if !*quiet {
-		fmt.Println(s)
-	}
-}
diff --git a/acceptance/13-rebuild-server.go b/acceptance/13-rebuild-server.go
deleted file mode 100644
index ae7e19f..0000000
--- a/acceptance/13-rebuild-server.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(servers gophercloud.CloudServersProvider) {
-			log("Creating server")
-			id, err := createServer(servers, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, id, "ACTIVE")
-			defer servers.DeleteServerById(id)
-
-			log("Rebuilding server")
-			newDetails, err := servers.RebuildServer(id, gophercloud.NewServer{
-				Name:      randomString("ACPTTEST", 32),
-				ImageRef:  findAlternativeImage(),
-				FlavorRef: findAlternativeFlavor(),
-				AdminPass: randomString("", 16),
-			})
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, newDetails.Id, "ACTIVE")
-
-			log("Done")
-		})
-	})
-}
-
-func log(s string) {
-	if !*quiet {
-		fmt.Println(s)
-	}
-}
diff --git a/acceptance/14-list-addresses.go b/acceptance/14-list-addresses.go
deleted file mode 100644
index 1d7d26b..0000000
--- a/acceptance/14-list-addresses.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(api gophercloud.CloudServersProvider) {
-			log("Creating server")
-			id, err := createServer(api, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(api, id, "ACTIVE")
-			defer api.DeleteServerById(id)
-
-			tryAllAddresses(id, api)
-			tryAddressesByNetwork("private", id, api)
-
-			log("Done")
-		})
-	})
-}
-
-func tryAllAddresses(id string, api gophercloud.CloudServersProvider) {
-	log("Getting list of all addresses...")
-	addresses, err := api.ListAddresses(id)
-	if (err != nil) && (err != gophercloud.WarnUnauthoritative) {
-		panic(err)
-	}
-	if err == gophercloud.WarnUnauthoritative {
-		log("Uh oh -- got a response back, but it's not authoritative for some reason.")
-	}
-	if !*quiet {
-		fmt.Println("Addresses:")
-		fmt.Printf("%+v\n", addresses)
-	}
-}
-
-func tryAddressesByNetwork(networkLabel string, id string, api gophercloud.CloudServersProvider) {
-	log("Getting list of addresses on", networkLabel, "network...")
-	network, err := api.ListAddressesByNetwork(id, networkLabel)
-	if (err != nil) && (err != gophercloud.WarnUnauthoritative) {
-		panic(err)
-	}
-	if err == gophercloud.WarnUnauthoritative {
-		log("Uh oh -- got a response back, but it's not authoritative for some reason.")
-	}
-	for _, addr := range network[networkLabel] {
-		log("Address:", addr.Addr, "  IPv", addr.Version)
-	}
-}
-
-func log(s ...interface{}) {
-	if !*quiet {
-		fmt.Println(s...)
-	}
-}
diff --git a/acceptance/15-list-keypairs.go b/acceptance/15-list-keypairs.go
deleted file mode 100644
index 1a617ed..0000000
--- a/acceptance/15-list-keypairs.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing.  $? non-zero on error though.")
-var rgn = flag.String("r", "", "Datacenter region to interrogate.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			keypairs, err := servers.ListKeyPairs()
-			if err != nil {
-				panic(err)
-			}
-
-			if !*quiet {
-				fmt.Println("name,fingerprint,publickey")
-				for _, key := range keypairs {
-					fmt.Printf("%s,%s,%s\n", key.Name, key.FingerPrint, key.PublicKey)
-				}
-			}
-		})
-	})
-}
diff --git a/acceptance/16-create-delete-keypair.go b/acceptance/16-create-delete-keypair.go
deleted file mode 100644
index f59e51c..0000000
--- a/acceptance/16-create-delete-keypair.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing.  $? non-zero on error though.")
-var rgn = flag.String("r", "", "Datacenter region to interrogate.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			name := randomString("ACPTTEST", 16)
-			kp := gophercloud.NewKeyPair{
-				Name: name,
-			}
-			keypair, err := servers.CreateKeyPair(kp)
-			if err != nil {
-				panic(err)
-			}
-			if !*quiet {
-				fmt.Printf("%s,%s,%s\n", keypair.Name, keypair.FingerPrint, keypair.PublicKey)
-			}
-
-			keypair, err = servers.ShowKeyPair(name)
-			if err != nil {
-				panic(err)
-			}
-			if !*quiet {
-				fmt.Printf("%s,%s,%s\n", keypair.Name, keypair.FingerPrint, keypair.PublicKey)
-			}
-
-			err = servers.DeleteKeyPair(name)
-			if err != nil {
-				panic(err)
-			}
-		})
-	})
-}
diff --git a/acceptance/17-create-delete-image.go b/acceptance/17-create-delete-image.go
deleted file mode 100644
index b3d80a3..0000000
--- a/acceptance/17-create-delete-image.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode for acceptance testing.  $? non-zero on error though.")
-var rgn = flag.String("r", "", "Datacenter region to interrogate.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			log("Creating server")
-			serverId, err := createServer(servers, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(servers, serverId, "ACTIVE")
-
-			log("Creating image")
-			name := randomString("ACPTTEST", 16)
-			createImage := gophercloud.CreateImage{
-				Name: name,
-			}
-			imageId, err := servers.CreateImage(serverId, createImage)
-			if err != nil {
-				panic(err)
-			}
-			waitForImageState(servers, imageId, "ACTIVE")
-
-			log("Deleting server")
-			servers.DeleteServerById(serverId)
-
-			log("Deleting image")
-			servers.DeleteImageById(imageId)
-
-			log("Done")
-		})
-	})
-}
-
-func log(s string) {
-	if !*quiet {
-		fmt.Println(s)
-	}
-}
diff --git a/acceptance/18-osutil-authentication.go b/acceptance/18-osutil-authentication.go
deleted file mode 100644
index 01ff4e9..0000000
--- a/acceptance/18-osutil-authentication.go
+++ /dev/null
@@ -1,19 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/osutil"
-)
-
-func main() {
-	provider, authOptions, err := osutil.AuthOptions()
-	if err != nil {
-		panic(err)
-	}
-	_, err = gophercloud.Authenticate(provider, authOptions)
-	if err != nil {
-		panic(err)
-	}
-}
diff --git a/acceptance/19-list-addresses-0.1.go b/acceptance/19-list-addresses-0.1.go
deleted file mode 100644
index d60557b..0000000
--- a/acceptance/19-list-addresses-0.1.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
-
-func main() {
-	flag.Parse()
-	withIdentity(false, func(acc gophercloud.AccessProvider) {
-		withServerApi(acc, func(api gophercloud.CloudServersProvider) {
-			log("Creating server")
-			id, err := createServer(api, "", "", "", "")
-			if err != nil {
-				panic(err)
-			}
-			waitForServerState(api, id, "ACTIVE")
-			defer api.DeleteServerById(id)
-
-			tryAllAddresses(id, api)
-
-			log("Done")
-		})
-	})
-}
-
-func tryAllAddresses(id string, api gophercloud.CloudServersProvider) {
-	log("Getting the server instance")
-	s, err := api.ServerById(id)
-	if err != nil {
-		panic(err)
-	}
-
-	log("Getting the complete set of pools")
-	ps, err := s.AllAddressPools()
-	if err != nil {
-		panic(err)
-	}
-
-	log("Listing IPs for each pool")
-	for k, v := range ps {
-		log(fmt.Sprintf("  Pool %s", k))
-		for _, a := range v {
-			log(fmt.Sprintf("    IP: %s, Version: %d", a.Addr, a.Version))
-		}
-	}
-}
-
-func log(s ...interface{}) {
-	if !*quiet {
-		fmt.Println(s...)
-	}
-}
diff --git a/acceptance/99-delete-server.go b/acceptance/99-delete-server.go
deleted file mode 100644
index 3e38ba4..0000000
--- a/acceptance/99-delete-server.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-)
-
-var quiet = flag.Bool("quiet", false, "Quiet operation for acceptance tests.  $? non-zero if problem.")
-var region = flag.String("r", "", "Datacenter region.  Leave blank for provider-default region.")
-
-func main() {
-	flag.Parse()
-
-	withIdentity(false, func(auth gophercloud.AccessProvider) {
-		withServerApi(auth, func(servers gophercloud.CloudServersProvider) {
-			// Grab a listing of all servers.
-			ss, err := servers.ListServers()
-			if err != nil {
-				panic(err)
-			}
-
-			// And for each one that starts with the ACPTTEST prefix, delete it.
-			// These are likely left-overs from previously running acceptance tests.
-			// Note that 04-create-servers.go is intended to leak servers by intention,
-			// so as to test this code.  :)
-			n := 0
-			for _, s := range ss {
-				if len(s.Name) < 8 {
-					continue
-				}
-				if s.Name[0:8] == "ACPTTEST" {
-					err := servers.DeleteServerById(s.Id)
-					if err != nil {
-						panic(err)
-					}
-					n++
-				}
-			}
-
-			if !*quiet {
-				fmt.Printf("%d servers removed.\n", n)
-			}
-		})
-	})
-}
diff --git a/acceptance/README.md b/acceptance/README.md
new file mode 100644
index 0000000..3199837
--- /dev/null
+++ b/acceptance/README.md
@@ -0,0 +1,57 @@
+# Gophercloud Acceptance tests
+
+The purpose of these acceptance tests is to validate that SDK features meet
+the requirements of a contract - to consumers, other parts of the library, and
+to a remote API.
+
+> **Note:** Because every test will be run against a real API endpoint, you
+> may incur bandwidth and service charges for all the resource usage. These
+> tests *should* remove their remote products automatically. However, there may
+> be certain cases where this does not happen; always double-check to make sure
+> you have no stragglers left behind.
+
+### Step 1. Set environment variables
+
+A lot of tests rely on environment variables for configuration - so you will need
+to set them before running the suite. If you're testing against pure OpenStack APIs,
+you can download a file that contains all of these variables for you: just visit
+the `project/access_and_security` page in your control panel and click the "Download
+OpenStack RC File" button at the top right. For all other providers, you will need
+to set them manually.
+
+#### Authentication
+
+|Name|Description|
+|---|---|
+|`OS_USERNAME`|Your API username|
+|`OS_PASSWORD`|Your API password|
+|`OS_AUTH_URL`|The identity URL you need to authenticate|
+|`OS_TENANT_NAME`|Your API tenant name|
+|`OS_TENANT_ID`|Your API tenant ID|
+|`RS_USERNAME`|Your Rackspace username|
+|`RS_API_KEY`|Your Rackspace API key|
+
+#### General
+
+|Name|Description|
+|---|---|
+|`OS_REGION_NAME`|The region you want your resources to reside in|
+|`RS_REGION`|Rackspace region you want your resource to reside in|
+
+#### Compute
+
+|Name|Description|
+|---|---|
+|`OS_IMAGE_ID`|The ID of the image your want your server to be based on|
+|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on|
+|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to|
+|`RS_IMAGE_ID`|The ID of the image you want servers to be created with|
+|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with|
+
+### 2. Run the test suite
+
+From the root directory, run:
+
+```
+./script/acceptancetest
+```
diff --git a/acceptance/libargs.go b/acceptance/libargs.go
deleted file mode 100644
index cf234e7..0000000
--- a/acceptance/libargs.go
+++ /dev/null
@@ -1,239 +0,0 @@
-// +build acceptance,old
-
-package main
-
-import (
-	"crypto/rand"
-	"fmt"
-	"github.com/rackspace/gophercloud"
-	"os"
-	"strings"
-	"time"
-)
-
-// getCredentials will verify existence of needed credential information
-// provided through environment variables.  This function will not return
-// if at least one piece of required information is missing.
-func getCredentials() (provider, username, password, apiKey string) {
-	provider = os.Getenv("SDK_PROVIDER")
-	username = os.Getenv("SDK_USERNAME")
-	password = os.Getenv("SDK_PASSWORD")
-	apiKey = os.Getenv("SDK_API_KEY")
-	var authURL = os.Getenv("OS_AUTH_URL")
-
-	if (provider == "") || (username == "") || (password == "") {
-		fmt.Fprintf(os.Stderr, "One or more of the following environment variables aren't set:\n")
-		fmt.Fprintf(os.Stderr, "  SDK_PROVIDER=\"%s\"\n", provider)
-		fmt.Fprintf(os.Stderr, "  SDK_USERNAME=\"%s\"\n", username)
-		fmt.Fprintf(os.Stderr, "  SDK_PASSWORD=\"%s\"\n", password)
-		os.Exit(1)
-	}
-
-	if strings.Contains(provider, "rackspace") && (authURL != "") {
-		provider = authURL + "/v2.0/tokens"
-	}
-
-	return
-}
-
-// randomString generates a string of given length, but random content.
-// All content will be within the ASCII graphic character set.
-// (Implementation from Even Shaw's contribution on
-// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
-func randomString(prefix string, n int) string {
-	const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-	var bytes = make([]byte, n)
-	rand.Read(bytes)
-	for i, b := range bytes {
-		bytes[i] = alphanum[b%byte(len(alphanum))]
-	}
-	return prefix + string(bytes)
-}
-
-// aSuitableImage finds a minimal image for use in dynamically creating servers.
-// If none can be found, this function will panic.
-func aSuitableImage(api gophercloud.CloudServersProvider) string {
-	images, err := api.ListImages()
-	if err != nil {
-		panic(err)
-	}
-
-	// TODO(sfalvo):
-	// Works for Rackspace, might not work for your provider!
-	// Need to figure out why ListImages() provides 0 values for
-	// Ram and Disk fields.
-	//
-	// Until then, just return Ubuntu 12.04 LTS.
-	for i := 0; i < len(images); i++ {
-		if strings.Contains(images[i].Name, "Ubuntu 12.04 LTS") {
-			return images[i].Id
-		}
-	}
-	panic("Image for Ubuntu 12.04 LTS not found.")
-}
-
-// aSuitableFlavor finds the minimum flavor capable of running the test image
-// chosen by aSuitableImage.  If none can be found, this function will panic.
-func aSuitableFlavor(api gophercloud.CloudServersProvider) string {
-	flavors, err := api.ListFlavors()
-	if err != nil {
-		panic(err)
-	}
-
-	// TODO(sfalvo):
-	// Works for Rackspace, might not work for your provider!
-	// Need to figure out why ListFlavors() provides 0 values for
-	// Ram and Disk fields.
-	//
-	// Until then, just return Ubuntu 12.04 LTS.
-	for i := 0; i < len(flavors); i++ {
-		if flavors[i].Id == "2" {
-			return flavors[i].Id
-		}
-	}
-	panic("Flavor 2 (512MB 1-core 20GB machine) not found.")
-}
-
-// createServer creates a new server in a manner compatible with acceptance testing.
-// In particular, it ensures that the name of the server always starts with "ACPTTEST--",
-// which the delete servers acceptance test relies on to identify servers to delete.
-// Passing in empty image and flavor references will force the use of reasonable defaults.
-// An empty name string will result in a dynamically created name prefixed with "ACPTTEST--".
-// A blank admin password will cause a password to be automatically generated; however,
-// at present no means of recovering this password exists, as no acceptance tests yet require
-// this data.
-func createServer(servers gophercloud.CloudServersProvider, imageRef, flavorRef, name, adminPass string) (string, error) {
-	if imageRef == "" {
-		imageRef = aSuitableImage(servers)
-	}
-
-	if flavorRef == "" {
-		flavorRef = aSuitableFlavor(servers)
-	}
-
-	if len(name) < 1 {
-		name = randomString("ACPTTEST", 16)
-	}
-
-	if (len(name) < 8) || (name[0:8] != "ACPTTEST") {
-		name = fmt.Sprintf("ACPTTEST--%s", name)
-	}
-
-	newServer, err := servers.CreateServer(gophercloud.NewServer{
-		Name:      name,
-		ImageRef:  imageRef,
-		FlavorRef: flavorRef,
-		AdminPass: adminPass,
-	})
-
-	if err != nil {
-		return "", err
-	}
-
-	return newServer.Id, nil
-}
-
-// findAlternativeFlavor locates a flavor to resize a server to.  It is guaranteed to be different
-// than what aSuitableFlavor() returns.  If none could be found, this function will panic.
-func findAlternativeFlavor() string {
-	return "3" // 1GB image, up from 512MB image
-}
-
-// findAlternativeImage locates an image to resize or rebuild a server with.  It is guaranteed to be
-// different than what aSuitableImage() returns.  If none could be found, this function will panic.
-func findAlternativeImage() string {
-	return "c6f9c411-e708-4952-91e5-62ded5ea4d3e"
-}
-
-// withIdentity authenticates the user against the provider's identity service, and provides an
-// accessor for additional services.
-func withIdentity(ar bool, f func(gophercloud.AccessProvider)) {
-	_, _, _, apiKey := getCredentials()
-	if len(apiKey) == 0 {
-		withPasswordIdentity(ar, f)
-	} else {
-		withAPIKeyIdentity(ar, f)
-	}
-}
-
-func withPasswordIdentity(ar bool, f func(gophercloud.AccessProvider)) {
-	provider, username, password, _ := getCredentials()
-	acc, err := gophercloud.Authenticate(
-		provider,
-		gophercloud.AuthOptions{
-			Username:    username,
-			Password:    password,
-			AllowReauth: ar,
-		},
-	)
-	if err != nil {
-		panic(err)
-	}
-
-	f(acc)
-}
-
-func withAPIKeyIdentity(ar bool, f func(gophercloud.AccessProvider)) {
-	provider, username, _, apiKey := getCredentials()
-	acc, err := gophercloud.Authenticate(
-		provider,
-		gophercloud.AuthOptions{
-			Username:    username,
-			ApiKey:      apiKey,
-			AllowReauth: ar,
-		},
-	)
-	if err != nil {
-		panic(err)
-	}
-
-	f(acc)
-}
-
-// withServerApi acquires the cloud servers API.
-func withServerApi(acc gophercloud.AccessProvider, f func(gophercloud.CloudServersProvider)) {
-	api, err := gophercloud.ServersApi(acc, gophercloud.ApiCriteria{
-		Name:      "cloudServersOpenStack",
-		VersionId: "2",
-		UrlChoice: gophercloud.PublicURL,
-	})
-	if err != nil {
-		panic(err)
-	}
-
-	f(api)
-}
-
-// waitForServerState polls, every 10 seconds, for a given server to appear in the indicated state.
-// This call will block forever if it never appears in the desired state, so if a timeout is required,
-// make sure to call this function in a goroutine.
-func waitForServerState(api gophercloud.CloudServersProvider, id, state string) error {
-	for {
-		s, err := api.ServerById(id)
-		if err != nil {
-			return err
-		}
-		if s.Status == state {
-			return nil
-		}
-		time.Sleep(10 * time.Second)
-	}
-	panic("Impossible")
-}
-
-// waitForImageState polls, every 10 seconds, for a given image to appear in the indicated state.
-// This call will block forever if it never appears in the desired state, so if a timeout is required,
-// make sure to call this function in a goroutine.
-func waitForImageState(api gophercloud.CloudServersProvider, id, state string) error {
-	for {
-		s, err := api.ImageById(id)
-		if err != nil {
-			return err
-		}
-		if s.Status == state {
-			return nil
-		}
-		time.Sleep(10 * time.Second)
-	}
-	panic("Impossible")
-}
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..5835048
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,87 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestSnapshots(t *testing.T) {
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+	}
+
+	v, err := volumes.Create(client, &volumes.CreateOpts{
+		Name: "gophercloud-test-volume",
+		Size: 1,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Failed to create volume: %v\n", err)
+	}
+
+	err = volumes.WaitForStatus(client, v.ID, "available", 120)
+	if err != nil {
+		t.Fatalf("Failed to create volume: %v\n", err)
+	}
+
+	t.Logf("Created volume: %v\n", v)
+
+	ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+		Name:     "gophercloud-test-snapshot",
+		VolumeID: v.ID,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Failed to create snapshot: %v\n", err)
+	}
+
+	err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+	if err != nil {
+		t.Fatalf("Failed to create snapshot: %v\n", err)
+	}
+
+	t.Logf("Created snapshot: %+v\n", ss)
+
+	err = snapshots.Delete(client, ss.ID)
+	if err != nil {
+		t.Fatalf("Failed to delete snapshot: %v", err)
+	}
+
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := snapshots.Get(client, ss.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	if err != nil {
+		t.Fatalf("Failed to delete snapshot: %v", err)
+	}
+
+	t.Log("Deleted snapshot\n")
+
+	err = volumes.Delete(client, v.ID)
+	if err != nil {
+		t.Errorf("Failed to delete volume: %v", err)
+	}
+
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := volumes.Get(client, v.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	if err != nil {
+		t.Errorf("Failed to delete volume: %v", err)
+	}
+
+	t.Log("Deleted volume\n")
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..6739a99
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,86 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.AuthenticatedClient(ao)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func TestVolumes(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+	}
+
+	cv, err := volumes.Create(client, &volumes.CreateOpts{
+		Size: 1,
+		Name: "gophercloud-test-volume",
+	}).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer func() {
+		err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+		if err != nil {
+			t.Error(err)
+		}
+		err = volumes.Delete(client, cv.ID)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+
+	_, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+		Name: "gophercloud-updated-volume",
+	}).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	v, err := volumes.Get(client, cv.ID).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	t.Logf("Got volume: %+v\n", v)
+
+	if v.Name != "gophercloud-updated-volume" {
+		t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name)
+	}
+
+	err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+		vols, err := volumes.ExtractVolumes(page)
+		if len(vols) != 1 {
+			t.Errorf("Expected 1 volume, got %d", len(vols))
+		}
+		return true, err
+	})
+	if err != nil {
+		t.Errorf("Error listing volumes: %v", err)
+	}
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..416e341
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestVolumeTypes(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+	}
+
+	vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+		ExtraSpecs: map[string]interface{}{
+			"capabilities": "gpu",
+			"priority":     3,
+		},
+		Name: "gophercloud-test-volumeType",
+	}).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer func() {
+		time.Sleep(10000 * time.Millisecond)
+		err = volumetypes.Delete(client, vt.ID)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+	t.Logf("Created volume type: %+v\n", vt)
+
+	vt, err = volumetypes.Get(client, vt.ID).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	t.Logf("Got volume type: %+v\n", vt)
+
+	err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		volTypes, err := volumetypes.ExtractVolumeTypes(page)
+		if len(volTypes) != 1 {
+			t.Errorf("Expected 1 volume type, got %d", len(volTypes))
+		}
+		t.Logf("Listing volume types: %+v\n", volTypes)
+		return true, err
+	})
+	if err != nil {
+		t.Errorf("Error trying to list volume types: %v", err)
+	}
+}
diff --git a/acceptance/openstack/client_test.go b/acceptance/openstack/client_test.go
new file mode 100644
index 0000000..6e88819
--- /dev/null
+++ b/acceptance/openstack/client_test.go
@@ -0,0 +1,40 @@
+// +build acceptance
+
+package openstack
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+)
+
+func TestAuthenticatedClient(t *testing.T) {
+	// Obtain credentials from the environment.
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to acquire credentials: %v", err)
+	}
+
+	client, err := openstack.AuthenticatedClient(ao)
+	if err != nil {
+		t.Fatalf("Unable to authenticate: %v", err)
+	}
+
+	if client.TokenID == "" {
+		t.Errorf("No token ID assigned to the client")
+	}
+
+	t.Logf("Client successfully acquired a token: %v", client.TokenID)
+
+	// Find the storage service in the service catalog.
+	storage, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+	if err != nil {
+		t.Errorf("Unable to locate a storage service: %v", err)
+	} else {
+		t.Logf("Located a storage service at endpoint: [%s]", storage.Endpoint)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
new file mode 100644
index 0000000..d08abe6
--- /dev/null
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -0,0 +1,50 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/smashwilson/gophercloud/acceptance/tools"
+)
+
+func TestBootFromVolume(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	name := tools.RandomString("Gophercloud-", 8)
+	t.Logf("Creating server [%s].", name)
+
+	bd := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			UUID:       choices.ImageID,
+			SourceType: bootfromvolume.Image,
+			VolumeSize: 10,
+		},
+	}
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: "3",
+	}
+	server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
+		serverCreateOpts,
+		bd,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created server: %+v\n", server)
+	//defer deleteServer(t, client, server)
+	t.Logf("Deleting server [%s]...", name)
+}
diff --git a/acceptance/openstack/compute/v2/compute_test.go b/acceptance/openstack/compute/v2/compute_test.go
new file mode 100644
index 0000000..46eb9ff
--- /dev/null
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -0,0 +1,97 @@
+// +build acceptance
+
+package v2
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.AuthenticatedClient(ao)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func waitForStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error {
+	return tools.WaitFor(func() (bool, error) {
+		latest, err := servers.Get(client, server.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if latest.Status == status {
+			// Success!
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
+
+// ComputeChoices contains image and flavor selections for use by the acceptance tests.
+type ComputeChoices struct {
+	// ImageID contains the ID of a valid image.
+	ImageID string
+
+	// FlavorID contains the ID of a valid flavor.
+	FlavorID string
+
+	// FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct
+	// from FlavorID.
+	FlavorIDResize string
+}
+
+// ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables.
+// If any required state is missing, an `error` will be returned that enumerates the missing properties.
+func ComputeChoicesFromEnv() (*ComputeChoices, error) {
+	imageID := os.Getenv("OS_IMAGE_ID")
+	flavorID := os.Getenv("OS_FLAVOR_ID")
+	flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE")
+
+	missing := make([]string, 0, 3)
+	if imageID == "" {
+		missing = append(missing, "OS_IMAGE_ID")
+	}
+	if flavorID == "" {
+		missing = append(missing, "OS_FLAVOR_ID")
+	}
+	if flavorIDResize == "" {
+		missing = append(missing, "OS_FLAVOR_ID_RESIZE")
+	}
+
+	notDistinct := ""
+	if flavorID == flavorIDResize {
+		notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct."
+	}
+
+	if len(missing) > 0 || notDistinct != "" {
+		text := "You're missing some important setup:\n"
+		if len(missing) > 0 {
+			text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n"
+		}
+		if notDistinct != "" {
+			text += " * " + notDistinct + "\n"
+		}
+
+		return nil, fmt.Errorf(text)
+	}
+
+	return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize}, nil
+}
diff --git a/acceptance/openstack/compute/v2/extension_test.go b/acceptance/openstack/compute/v2/extension_test.go
new file mode 100644
index 0000000..1356ffa
--- /dev/null
+++ b/acceptance/openstack/compute/v2/extension_test.go
@@ -0,0 +1,47 @@
+// +build acceptance compute extensionss
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListExtensions(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	err = extensions.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		exts, err := extensions.ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+
+		for i, ext := range exts {
+			t.Logf("[%02d]    name=[%s]\n", i, ext.Name)
+			t.Logf("       alias=[%s]\n", ext.Alias)
+			t.Logf(" description=[%s]\n", ext.Description)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func TestGetExtension(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	ext, err := extensions.Get(client, "os-admin-actions").Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Extension details:")
+	t.Logf("        name=[%s]\n", ext.Name)
+	t.Logf("   namespace=[%s]\n", ext.Namespace)
+	t.Logf("       alias=[%s]\n", ext.Alias)
+	t.Logf(" description=[%s]\n", ext.Description)
+	t.Logf("     updated=[%s]\n", ext.Updated)
+}
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
new file mode 100644
index 0000000..9f51b12
--- /dev/null
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -0,0 +1,57 @@
+// +build acceptance compute flavors
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListFlavors(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+	pager := flavors.ListDetail(client, nil)
+	count, pages := 0, 0
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("---")
+		pages++
+		flavors, err := flavors.ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, f := range flavors {
+			t.Logf("%s\t%s\t%d\t%d\t%d", f.ID, f.Name, f.RAM, f.Disk, f.VCPUs)
+		}
+
+		return true, nil
+	})
+
+	t.Logf("--------\n%d flavors listed on %d pages.", count, pages)
+}
+
+func TestGetFlavor(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	flavor, err := flavors.Get(client, choices.FlavorID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get flavor information: %v", err)
+	}
+
+	t.Logf("Flavor: %#v", flavor)
+}
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
new file mode 100644
index 0000000..ceab22f
--- /dev/null
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -0,0 +1,37 @@
+// +build acceptance compute images
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/images"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListImages(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute: client: %v", err)
+	}
+
+	t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+	pager := images.ListDetail(client, nil)
+	count, pages := 0, 0
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		images, err := images.ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, i := range images {
+			t.Logf("%s\t%s\t%s\t%s", i.ID, i.Name, i.Status, i.Created)
+		}
+
+		return true, nil
+	})
+
+	t.Logf("--------\n%d images listed on %d pages.", count, pages)
+}
diff --git a/acceptance/openstack/compute/v2/pkg.go b/acceptance/openstack/compute/v2/pkg.go
new file mode 100644
index 0000000..bb158c3
--- /dev/null
+++ b/acceptance/openstack/compute/v2/pkg.go
@@ -0,0 +1,3 @@
+// The v2 package contains acceptance tests for the Openstack Compute V2 service.
+
+package v2
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
new file mode 100644
index 0000000..e223c18
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -0,0 +1,393 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListServers(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6")
+
+	pager := servers.List(client, servers.ListOpts{})
+	count, pages := 0, 0
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		t.Logf("---")
+
+		servers, err := servers.ExtractServers(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range servers {
+			t.Logf("%s\t%s\t%s\t%s\t%s\t\n", s.ID, s.Name, s.Status, s.AccessIPv4, s.AccessIPv6)
+			count++
+		}
+
+		return true, nil
+	})
+
+	t.Logf("--------\n%d servers listed on %d pages.\n", count, pages)
+}
+
+func networkingClient() (*gophercloud.ServiceClient, error) {
+	opts, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	provider, err := openstack.AuthenticatedClient(opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
+		Name:   "neutron",
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var network networks.Network
+
+	networkingClient, err := networkingClient()
+	if err != nil {
+		t.Fatalf("Unable to create a networking client: %v", err)
+	}
+
+	pager := networks.List(networkingClient, networks.ListOpts{Name: "public", Limit: 1})
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		networks, err := networks.ExtractNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		if len(networks) == 0 {
+			t.Fatalf("No networks to attach to server")
+			return false, err
+		}
+
+		network = networks[0]
+
+		return false, nil
+	})
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	server, err := servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		Networks: []servers.Network{
+			servers.Network{UUID: network.ID},
+		},
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	return server, err
+}
+
+func TestCreateDestroyServer(t *testing.T) {
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer func() {
+		servers.Delete(client, server.ID)
+		t.Logf("Server deleted.")
+	}()
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+}
+
+func TestUpdateServer(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	alternateName := tools.RandomString("ACPTTEST", 16)
+	for alternateName == server.Name {
+		alternateName = tools.RandomString("ACPTTEST", 16)
+	}
+
+	t.Logf("Attempting to rename the server to %s.", alternateName)
+
+	updated, err := servers.Update(client, server.ID, servers.UpdateOpts{Name: alternateName}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to rename server: %v", err)
+	}
+
+	if updated.ID != server.ID {
+		t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID)
+	}
+
+	err = tools.WaitFor(func() (bool, error) {
+		latest, err := servers.Get(client, updated.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		return latest.Name == alternateName, nil
+	})
+}
+
+func TestActionChangeAdminPassword(t *testing.T) {
+	t.Parallel()
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	randomPassword := tools.MakeNewPassword(server.AdminPass)
+	res := servers.ChangeAdminPassword(client, server.ID, randomPassword)
+	if res.Err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "PASSWORD"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionReboot(t *testing.T) {
+	t.Parallel()
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	res := servers.Reboot(client, server.ID, "aldhjflaskhjf")
+	if res.Err == nil {
+		t.Fatal("Expected the SDK to provide an ArgumentError here")
+	}
+
+	t.Logf("Attempting reboot of server %s", server.ID)
+	res = servers.Reboot(client, server.ID, servers.OSReboot)
+	if res.Err != nil {
+		t.Fatalf("Unable to reboot server: %v", err)
+	}
+
+	if err = waitForStatus(client, server, "REBOOT"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionRebuild(t *testing.T) {
+	t.Parallel()
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("Attempting to rebuild server %s", server.ID)
+
+	rebuildOpts := servers.RebuildOpts{
+		Name:      tools.RandomString("ACPTTEST", 16),
+		AdminPass: tools.MakeNewPassword(server.AdminPass),
+		ImageID:   choices.ImageID,
+	}
+
+	rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if rebuilt.ID != server.ID {
+		t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID)
+	}
+
+	if err = waitForStatus(client, rebuilt, "REBUILD"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, rebuilt, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func resizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server, choices *ComputeChoices) {
+	if err := waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("Attempting to resize server [%s]", server.ID)
+
+	opts := &servers.ResizeOpts{
+		FlavorRef: choices.FlavorIDResize,
+	}
+	if res := servers.Resize(client, server.ID, opts); res.Err != nil {
+		t.Fatal(res.Err)
+	}
+
+	if err := waitForStatus(client, server, "VERIFY_RESIZE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionResizeConfirm(t *testing.T) {
+	t.Parallel()
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+	resizeServer(t, client, server, choices)
+
+	t.Logf("Attempting to confirm resize for server %s", server.ID)
+
+	if res := servers.ConfirmResize(client, server.ID); res.Err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionResizeRevert(t *testing.T) {
+	t.Parallel()
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+	resizeServer(t, client, server, choices)
+
+	t.Logf("Attempting to revert resize for server %s", server.ID)
+
+	if res := servers.RevertResize(client, server.ID); res.Err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
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
new file mode 100644
index 0000000..feae233
--- /dev/null
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -0,0 +1,47 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func v2AuthOptions(t *testing.T) gophercloud.AuthOptions {
+	// Obtain credentials from the environment.
+	ao, err := openstack.AuthOptionsFromEnv()
+	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 = ""
+	}
+
+	return ao
+}
+
+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)
+	}
+
+	return openstack.NewIdentityV2(provider)
+}
+
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	return createClient(t, true)
+}
diff --git a/acceptance/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..0632a48
--- /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, tokens2.WrapOptions(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/endpoint_test.go b/acceptance/openstack/identity/v3/endpoint_test.go
new file mode 100644
index 0000000..ea893c2
--- /dev/null
+++ b/acceptance/openstack/identity/v3/endpoint_test.go
@@ -0,0 +1,111 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
+	services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListEndpoints(t *testing.T) {
+	// Create a service client.
+	serviceClient := createAuthenticatedClient(t)
+	if serviceClient == nil {
+		return
+	}
+
+	// Use the service to list all available endpoints.
+	pager := endpoints3.List(serviceClient, endpoints3.ListOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		endpoints, err := endpoints3.ExtractEndpoints(page)
+		if err != nil {
+			t.Fatalf("Error extracting endpoings: %v", err)
+		}
+
+		for _, endpoint := range endpoints {
+			t.Logf("Endpoint: %8s %10s %9s %s",
+				endpoint.ID,
+				endpoint.Availability,
+				endpoint.Name,
+				endpoint.URL)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Errorf("Unexpected error while iterating endpoint pages: %v", err)
+	}
+}
+
+func TestNavigateCatalog(t *testing.T) {
+	// Create a service client.
+	client := createAuthenticatedClient(t)
+	if client == nil {
+		return
+	}
+
+	var compute *services3.Service
+	var endpoint *endpoints3.Endpoint
+
+	// Discover the service we're interested in.
+	servicePager := services3.List(client, services3.ListOpts{ServiceType: "compute"})
+	err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
+		part, err := services3.ExtractServices(page)
+		if err != nil {
+			return false, err
+		}
+		if compute != nil {
+			t.Fatalf("Expected one service, got more than one page")
+			return false, nil
+		}
+		if len(part) != 1 {
+			t.Fatalf("Expected one service, got %d", len(part))
+			return false, nil
+		}
+
+		compute = &part[0]
+		return true, nil
+	})
+	if err != nil {
+		t.Fatalf("Unexpected error iterating pages: %v", err)
+	}
+
+	if compute == nil {
+		t.Fatalf("No compute service found.")
+	}
+
+	// Enumerate the endpoints available for this service.
+	computePager := endpoints3.List(client, endpoints3.ListOpts{
+		Availability: gophercloud.AvailabilityPublic,
+		ServiceID:    compute.ID,
+	})
+	err = computePager.EachPage(func(page pagination.Page) (bool, error) {
+		part, err := endpoints3.ExtractEndpoints(page)
+		if err != nil {
+			return false, err
+		}
+		if endpoint != nil {
+			t.Fatalf("Expected one endpoint, got more than one page")
+			return false, nil
+		}
+		if len(part) != 1 {
+			t.Fatalf("Expected one endpoint, got %d", len(part))
+			return false, nil
+		}
+
+		endpoint = &part[0]
+		return true, nil
+	})
+
+	if endpoint == nil {
+		t.Fatalf("No endpoint found.")
+	}
+
+	t.Logf("Success. The compute endpoint is at %s.", endpoint.URL)
+}
diff --git a/acceptance/openstack/identity/v3/identity_test.go b/acceptance/openstack/identity/v3/identity_test.go
new file mode 100644
index 0000000..ce64345
--- /dev/null
+++ b/acceptance/openstack/identity/v3/identity_test.go
@@ -0,0 +1,39 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createAuthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	// Obtain credentials from the environment.
+	ao, err := openstack.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	// Trim out unused fields.
+	ao.Username, ao.TenantID, ao.TenantName = "", "", ""
+
+	if ao.UserID == "" {
+		t.Logf("Skipping identity v3 tests because no OS_USERID is present.")
+		return nil
+	}
+
+	// Create a client and manually authenticate against v3.
+	providerClient, err := openstack.NewClient(ao.IdentityEndpoint)
+	if err != nil {
+		t.Fatalf("Unable to instantiate client: %v", err)
+	}
+
+	err = openstack.AuthenticateV3(providerClient, ao)
+	if err != nil {
+		t.Fatalf("Unable to authenticate against identity v3: %v", err)
+	}
+
+	// Create a service client.
+	return openstack.NewIdentityV3(providerClient)
+}
diff --git a/acceptance/openstack/identity/v3/pkg.go b/acceptance/openstack/identity/v3/pkg.go
new file mode 100644
index 0000000..eac3ae9
--- /dev/null
+++ b/acceptance/openstack/identity/v3/pkg.go
@@ -0,0 +1 @@
+package v3
diff --git a/acceptance/openstack/identity/v3/service_test.go b/acceptance/openstack/identity/v3/service_test.go
new file mode 100644
index 0000000..082bd11
--- /dev/null
+++ b/acceptance/openstack/identity/v3/service_test.go
@@ -0,0 +1,36 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListServices(t *testing.T) {
+	// Create a service client.
+	serviceClient := createAuthenticatedClient(t)
+	if serviceClient == nil {
+		return
+	}
+
+	// Use the client to list all available services.
+	pager := services3.List(serviceClient, services3.ListOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		parts, err := services3.ExtractServices(page)
+		if err != nil {
+			return false, err
+		}
+
+		t.Logf("--- Page ---")
+		for _, service := range parts {
+			t.Logf("Service: %32s %15s %10s %s", service.ID, service.Type, service.Name, *service.Description)
+		}
+		return true, nil
+	})
+	if err != nil {
+		t.Errorf("Unexpected error traversing pages: %v", err)
+	}
+}
diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go
new file mode 100644
index 0000000..4342ade
--- /dev/null
+++ b/acceptance/openstack/identity/v3/token_test.go
@@ -0,0 +1,42 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack"
+	tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
+)
+
+func TestGetToken(t *testing.T) {
+	// Obtain credentials from the environment.
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to acquire credentials: %v", err)
+	}
+
+	// Trim out unused fields. Skip if we don't have a UserID.
+	ao.Username, ao.TenantID, ao.TenantName = "", "", ""
+	if ao.UserID == "" {
+		t.Logf("Skipping identity v3 tests because no OS_USERID is present.")
+		return
+	}
+
+	// Create an unauthenticated client.
+	provider, err := openstack.NewClient(ao.IdentityEndpoint)
+	if err != nil {
+		t.Fatalf("Unable to instantiate client: %v", err)
+	}
+
+	// Create a service client.
+	service := openstack.NewIdentityV3(provider)
+
+	// Use the service to create a token.
+	token, err := tokens3.Create(service, ao, nil).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get token: %v", err)
+	}
+
+	t.Logf("Acquired token: %s", token.ID)
+}
diff --git a/acceptance/openstack/networking/v2/apiversion_test.go b/acceptance/openstack/networking/v2/apiversion_test.go
new file mode 100644
index 0000000..99e1d01
--- /dev/null
+++ b/acceptance/openstack/networking/v2/apiversion_test.go
@@ -0,0 +1,51 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/networking/v2/apiversions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListAPIVersions(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	pager := apiversions.ListVersions(Client)
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		versions, err := apiversions.ExtractAPIVersions(page)
+		th.AssertNoErr(t, err)
+
+		for _, v := range versions {
+			t.Logf("API Version: ID [%s] Status [%s]", v.ID, v.Status)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
+
+func TestListAPIResources(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	pager := apiversions.ListVersionResources(Client, "v2.0")
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		vrs, err := apiversions.ExtractVersionResources(page)
+		th.AssertNoErr(t, err)
+
+		for _, vr := range vrs {
+			t.Logf("Network: Name [%s] Collection [%s]", vr.Name, vr.Collection)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
diff --git a/acceptance/openstack/networking/v2/common.go b/acceptance/openstack/networking/v2/common.go
new file mode 100644
index 0000000..1efac2c
--- /dev/null
+++ b/acceptance/openstack/networking/v2/common.go
@@ -0,0 +1,39 @@
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var Client *gophercloud.ServiceClient
+
+func NewClient() (*gophercloud.ServiceClient, error) {
+	opts, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	provider, err := openstack.AuthenticatedClient(opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
+		Name:   "neutron",
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func Setup(t *testing.T) {
+	client, err := NewClient()
+	th.AssertNoErr(t, err)
+	Client = client
+}
+
+func Teardown() {
+	Client = nil
+}
diff --git a/acceptance/openstack/networking/v2/extension_test.go b/acceptance/openstack/networking/v2/extension_test.go
new file mode 100644
index 0000000..edcbba4
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extension_test.go
@@ -0,0 +1,45 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListExts(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	pager := extensions.List(Client)
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		exts, err := extensions.ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+
+		for _, ext := range exts {
+			t.Logf("Extension: Name [%s] Description [%s]", ext.Name, ext.Description)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
+
+func TestGetExt(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	ext, err := extensions.Get(Client, "service-type").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, ext.Updated, "2013-01-20T00:00:00-00:00")
+	th.AssertEquals(t, ext.Name, "Neutron Service Type Management")
+	th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/neutron/service-type/api/v1.0")
+	th.AssertEquals(t, ext.Alias, "service-type")
+	th.AssertEquals(t, ext.Description, "API for retrieving service providers for Neutron advanced services")
+}
diff --git a/acceptance/openstack/networking/v2/extensions/layer3_test.go b/acceptance/openstack/networking/v2/extensions/layer3_test.go
new file mode 100644
index 0000000..63e0be3
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/layer3_test.go
@@ -0,0 +1,300 @@
+// +build acceptance networking layer3ext
+
+package extensions
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+	cidr1 = "10.0.0.1/24"
+	cidr2 = "20.0.0.1/24"
+)
+
+func TestAll(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	testRouter(t)
+	testFloatingIP(t)
+}
+
+func testRouter(t *testing.T) {
+	// Setup: Create network
+	networkID := createNetwork(t)
+
+	// Create router
+	routerID := createRouter(t, networkID)
+
+	// Lists routers
+	listRouters(t)
+
+	// Update router
+	updateRouter(t, routerID)
+
+	// Get router
+	getRouter(t, routerID)
+
+	// Create new subnet. Note: this subnet will be deleted when networkID is deleted
+	subnetID := createSubnet(t, networkID, cidr2)
+
+	// Add interface
+	addInterface(t, routerID, subnetID)
+
+	// Remove interface
+	removeInterface(t, routerID, subnetID)
+
+	// Delete router
+	deleteRouter(t, routerID)
+
+	// Cleanup
+	deleteNetwork(t, networkID)
+}
+
+func testFloatingIP(t *testing.T) {
+	// Setup external network
+	extNetworkID := createNetwork(t)
+
+	// Setup internal network, subnet and port
+	intNetworkID, subnetID, portID := createInternalTopology(t)
+
+	// Now the important part: we need to allow the external network to talk to
+	// the internal subnet. For this we need a router that has an interface to
+	// the internal subnet.
+	routerID := bridgeIntSubnetWithExtNetwork(t, extNetworkID, subnetID)
+
+	// Create floating IP
+	ipID := createFloatingIP(t, extNetworkID, portID)
+
+	// Get floating IP
+	getFloatingIP(t, ipID)
+
+	// Update floating IP
+	updateFloatingIP(t, ipID, portID)
+
+	// Delete floating IP
+	deleteFloatingIP(t, ipID)
+
+	// Remove the internal subnet interface
+	removeInterface(t, routerID, subnetID)
+
+	// Delete router and external network
+	deleteRouter(t, routerID)
+	deleteNetwork(t, extNetworkID)
+
+	// Delete internal port and network
+	deletePort(t, portID)
+	deleteNetwork(t, intNetworkID)
+}
+
+func createNetwork(t *testing.T) string {
+	t.Logf("Creating a network")
+
+	asu := true
+	opts := external.CreateOpts{
+		Parent:   networks.CreateOpts{Name: "sample_network", AdminStateUp: &asu},
+		External: true,
+	}
+	n, err := networks.Create(base.Client, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	if n.ID == "" {
+		t.Fatalf("No ID returned when creating a network")
+	}
+
+	createSubnet(t, n.ID, cidr1)
+
+	t.Logf("Network created: ID [%s]", n.ID)
+
+	return n.ID
+}
+
+func deleteNetwork(t *testing.T, networkID string) {
+	t.Logf("Deleting network %s", networkID)
+	networks.Delete(base.Client, networkID)
+}
+
+func deletePort(t *testing.T, portID string) {
+	t.Logf("Deleting port %s", portID)
+	ports.Delete(base.Client, portID)
+}
+
+func createInternalTopology(t *testing.T) (string, string, string) {
+	t.Logf("Creating an internal network (for port)")
+	opts := networks.CreateOpts{Name: "internal_network"}
+	n, err := networks.Create(base.Client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	// A subnet is also needed
+	subnetID := createSubnet(t, n.ID, cidr2)
+
+	t.Logf("Creating an internal port on network %s", n.ID)
+	p, err := ports.Create(base.Client, ports.CreateOpts{
+		NetworkID: n.ID,
+		Name:      "fixed_internal_port",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	return n.ID, subnetID, p.ID
+}
+
+func bridgeIntSubnetWithExtNetwork(t *testing.T, networkID, subnetID string) string {
+	// Create router with external gateway info
+	routerID := createRouter(t, networkID)
+
+	// Add interface for internal subnet
+	addInterface(t, routerID, subnetID)
+
+	return routerID
+}
+
+func createSubnet(t *testing.T, networkID, cidr string) string {
+	t.Logf("Creating a subnet for network %s", networkID)
+
+	iFalse := false
+	s, err := subnets.Create(base.Client, subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       cidr,
+		IPVersion:  subnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: &iFalse,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Subnet created: ID [%s]", s.ID)
+
+	return s.ID
+}
+
+func createRouter(t *testing.T, networkID string) string {
+	t.Logf("Creating a router for network %s", networkID)
+
+	asu := false
+	gwi := routers.GatewayInfo{NetworkID: networkID}
+	r, err := routers.Create(base.Client, routers.CreateOpts{
+		Name:         "foo_router",
+		AdminStateUp: &asu,
+		GatewayInfo:  &gwi,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	if r.ID == "" {
+		t.Fatalf("No ID returned when creating a router")
+	}
+
+	t.Logf("Router created: ID [%s]", r.ID)
+
+	return r.ID
+}
+
+func listRouters(t *testing.T) {
+	pager := routers.List(base.Client, routers.ListOpts{})
+
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		routerList, err := routers.ExtractRouters(page)
+		th.AssertNoErr(t, err)
+
+		for _, r := range routerList {
+			t.Logf("Listing router: ID [%s] Name [%s] Status [%s] GatewayInfo [%#v]",
+				r.ID, r.Name, r.Status, r.GatewayInfo)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updateRouter(t *testing.T, routerID string) {
+	_, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
+		Name: "another_name",
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+}
+
+func getRouter(t *testing.T, routerID string) {
+	r, err := routers.Get(base.Client, routerID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting router: ID [%s] Name [%s] Status [%s]", r.ID, r.Name, r.Status)
+}
+
+func addInterface(t *testing.T, routerID, subnetID string) {
+	ir, err := routers.AddInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Interface added to router %s: SubnetID [%s] PortID [%s]", routerID, ir.SubnetID, ir.PortID)
+}
+
+func removeInterface(t *testing.T, routerID, subnetID string) {
+	ir, err := routers.RemoveInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Interface %s removed from %s", ir.ID, routerID)
+}
+
+func deleteRouter(t *testing.T, routerID string) {
+	t.Logf("Deleting router %s", routerID)
+
+	res := routers.Delete(base.Client, routerID)
+
+	th.AssertNoErr(t, res.Err)
+}
+
+func createFloatingIP(t *testing.T, networkID, portID string) string {
+	t.Logf("Creating floating IP on network [%s] with port [%s]", networkID, portID)
+
+	opts := floatingips.CreateOpts{
+		FloatingNetworkID: networkID,
+		PortID:            portID,
+	}
+
+	ip, err := floatingips.Create(base.Client, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Floating IP created: ID [%s] Status [%s] Fixed (internal) IP: [%s] Floating (external) IP: [%s]",
+		ip.ID, ip.Status, ip.FixedIP, ip.FloatingIP)
+
+	return ip.ID
+}
+
+func getFloatingIP(t *testing.T, ipID string) {
+	ip, err := floatingips.Get(base.Client, ipID).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting floating IP: ID [%s] Status [%s]", ip.ID, ip.Status)
+}
+
+func updateFloatingIP(t *testing.T, ipID, portID string) {
+	t.Logf("Disassociate all ports from IP %s", ipID)
+	_, err := floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: ""}).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Re-associate the port %s", portID)
+	_, err = floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: portID}).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func deleteFloatingIP(t *testing.T, ipID string) {
+	t.Logf("Deleting IP %s", ipID)
+	res := floatingips.Delete(base.Client, ipID)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
new file mode 100644
index 0000000..27dfe5f
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
@@ -0,0 +1,78 @@
+package lbaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func SetupTopology(t *testing.T) (string, string) {
+	// create network
+	n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created network %s", n.ID)
+
+	// create subnet
+	s, err := subnets.Create(base.Client, subnets.CreateOpts{
+		NetworkID: n.ID,
+		CIDR:      "192.168.199.0/24",
+		IPVersion: subnets.IPv4,
+		Name:      "tmp_subnet",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created subnet %s", s.ID)
+
+	return n.ID, s.ID
+}
+
+func DeleteTopology(t *testing.T, networkID string) {
+	res := networks.Delete(base.Client, networkID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted network %s", networkID)
+}
+
+func CreatePool(t *testing.T, subnetID string) string {
+	p, err := pools.Create(base.Client, pools.CreateOpts{
+		LBMethod: pools.LBMethodRoundRobin,
+		Protocol: "HTTP",
+		Name:     "tmp_pool",
+		SubnetID: subnetID,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created pool %s", p.ID)
+
+	return p.ID
+}
+
+func DeletePool(t *testing.T, poolID string) {
+	res := pools.Delete(base.Client, poolID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted pool %s", poolID)
+}
+
+func CreateMonitor(t *testing.T) string {
+	m, err := monitors.Create(base.Client, monitors.CreateOpts{
+		Delay:         10,
+		Timeout:       10,
+		MaxRetries:    3,
+		Type:          monitors.TypeHTTP,
+		ExpectedCodes: "200",
+		URLPath:       "/login",
+		HTTPMethod:    "GET",
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created monitor ID [%s]", m.ID)
+
+	return m.ID
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go
new file mode 100644
index 0000000..9b60582
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go
@@ -0,0 +1,95 @@
+// +build acceptance networking lbaas lbaasmember
+
+package lbaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMembers(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// setup
+	networkID, subnetID := SetupTopology(t)
+	poolID := CreatePool(t, subnetID)
+
+	// create member
+	memberID := createMember(t, poolID)
+
+	// list members
+	listMembers(t)
+
+	// update member
+	updateMember(t, memberID)
+
+	// get member
+	getMember(t, memberID)
+
+	// delete member
+	deleteMember(t, memberID)
+
+	// teardown
+	DeletePool(t, poolID)
+	DeleteTopology(t, networkID)
+}
+
+func createMember(t *testing.T, poolID string) string {
+	m, err := members.Create(base.Client, members.CreateOpts{
+		Address:      "192.168.199.1",
+		ProtocolPort: 8080,
+		PoolID:       poolID,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created member: ID [%s] Status [%s] Weight [%d] Address [%s] Port [%d]",
+		m.ID, m.Status, m.Weight, m.Address, m.ProtocolPort)
+
+	return m.ID
+}
+
+func listMembers(t *testing.T) {
+	err := members.List(base.Client, members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		memberList, err := members.ExtractMembers(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		for _, m := range memberList {
+			t.Logf("Listing member: ID [%s] Status [%s]", m.ID, m.Status)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updateMember(t *testing.T, memberID string) {
+	m, err := members.Update(base.Client, memberID, members.UpdateOpts{AdminStateUp: true}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated member ID [%s]", m.ID)
+}
+
+func getMember(t *testing.T, memberID string) {
+	m, err := members.Get(base.Client, memberID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting member ID [%s]", m.ID)
+}
+
+func deleteMember(t *testing.T, memberID string) {
+	res := members.Delete(base.Client, memberID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted member %s", memberID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go
new file mode 100644
index 0000000..9056fff
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go
@@ -0,0 +1,77 @@
+// +build acceptance networking lbaas lbaasmonitor
+
+package lbaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMonitors(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// create monitor
+	monitorID := CreateMonitor(t)
+
+	// list monitors
+	listMonitors(t)
+
+	// update monitor
+	updateMonitor(t, monitorID)
+
+	// get monitor
+	getMonitor(t, monitorID)
+
+	// delete monitor
+	deleteMonitor(t, monitorID)
+}
+
+func listMonitors(t *testing.T) {
+	err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		monitorList, err := monitors.ExtractMonitors(page)
+		if err != nil {
+			t.Errorf("Failed to extract monitors: %v", err)
+			return false, err
+		}
+
+		for _, m := range monitorList {
+			t.Logf("Listing monitor: ID [%s] Type [%s] Delay [%ds] Timeout [%d] Retries [%d] Status [%s]",
+				m.ID, m.Type, m.Delay, m.Timeout, m.MaxRetries, m.Status)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updateMonitor(t *testing.T, monitorID string) {
+	opts := monitors.UpdateOpts{Delay: 10, Timeout: 10, MaxRetries: 3}
+	m, err := monitors.Update(base.Client, monitorID, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated monitor ID [%s]", m.ID)
+}
+
+func getMonitor(t *testing.T, monitorID string) {
+	m, err := monitors.Get(base.Client, monitorID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting monitor ID [%s]: URL path [%s] HTTP Method [%s] Accepted codes [%s]",
+		m.ID, m.URLPath, m.HTTPMethod, m.ExpectedCodes)
+}
+
+func deleteMonitor(t *testing.T, monitorID string) {
+	res := monitors.Delete(base.Client, monitorID)
+
+	th.AssertNoErr(t, res.Err)
+
+	t.Logf("Deleted monitor %s", monitorID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go
new file mode 100644
index 0000000..f5a7df7
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go
@@ -0,0 +1 @@
+package lbaas
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
new file mode 100644
index 0000000..8194064
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
@@ -0,0 +1,98 @@
+// +build acceptance networking lbaas lbaaspool
+
+package lbaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPools(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// setup
+	networkID, subnetID := SetupTopology(t)
+
+	// create pool
+	poolID := CreatePool(t, subnetID)
+
+	// list pools
+	listPools(t)
+
+	// update pool
+	updatePool(t, poolID)
+
+	// get pool
+	getPool(t, poolID)
+
+	// create monitor
+	monitorID := CreateMonitor(t)
+
+	// associate health monitor
+	associateMonitor(t, poolID, monitorID)
+
+	// disassociate health monitor
+	disassociateMonitor(t, poolID, monitorID)
+
+	// delete pool
+	DeletePool(t, poolID)
+
+	// teardown
+	DeleteTopology(t, networkID)
+}
+
+func listPools(t *testing.T) {
+	err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		poolList, err := pools.ExtractPools(page)
+		if err != nil {
+			t.Errorf("Failed to extract pools: %v", err)
+			return false, err
+		}
+
+		for _, p := range poolList {
+			t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s]", p.ID, p.Name, p.Status, p.LBMethod)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updatePool(t *testing.T, poolID string) {
+	opts := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections}
+	p, err := pools.Update(base.Client, poolID, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated pool ID [%s]", p.ID)
+}
+
+func getPool(t *testing.T, poolID string) {
+	p, err := pools.Get(base.Client, poolID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting pool ID [%s]", p.ID)
+}
+
+func associateMonitor(t *testing.T, poolID, monitorID string) {
+	res := pools.AssociateMonitor(base.Client, poolID, monitorID)
+
+	th.AssertNoErr(t, res.Err)
+
+	t.Logf("Associated pool %s with monitor %s", poolID, monitorID)
+}
+
+func disassociateMonitor(t *testing.T, poolID, monitorID string) {
+	res := pools.DisassociateMonitor(base.Client, poolID, monitorID)
+
+	th.AssertNoErr(t, res.Err)
+
+	t.Logf("Disassociated pool %s with monitor %s", poolID, monitorID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go
new file mode 100644
index 0000000..c8dff2d
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go
@@ -0,0 +1,101 @@
+// +build acceptance networking lbaas lbaasvip
+
+package lbaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVIPs(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// setup
+	networkID, subnetID := SetupTopology(t)
+	poolID := CreatePool(t, subnetID)
+
+	// create VIP
+	VIPID := createVIP(t, subnetID, poolID)
+
+	// list VIPs
+	listVIPs(t)
+
+	// update VIP
+	updateVIP(t, VIPID)
+
+	// get VIP
+	getVIP(t, VIPID)
+
+	// delete VIP
+	deleteVIP(t, VIPID)
+
+	// teardown
+	DeletePool(t, poolID)
+	DeleteTopology(t, networkID)
+}
+
+func createVIP(t *testing.T, subnetID, poolID string) string {
+	p, err := vips.Create(base.Client, vips.CreateOpts{
+		Protocol:     "HTTP",
+		Name:         "New_VIP",
+		AdminStateUp: vips.Up,
+		SubnetID:     subnetID,
+		PoolID:       poolID,
+		ProtocolPort: 80,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created pool %s", p.ID)
+
+	return p.ID
+}
+
+func listVIPs(t *testing.T) {
+	err := vips.List(base.Client, vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		vipList, err := vips.ExtractVIPs(page)
+		if err != nil {
+			t.Errorf("Failed to extract VIPs: %v", err)
+			return false, err
+		}
+
+		for _, vip := range vipList {
+			t.Logf("Listing VIP: ID [%s] Name [%s] Address [%s] Port [%s] Connection Limit [%d]",
+				vip.ID, vip.Name, vip.Address, vip.ProtocolPort, vip.ConnLimit)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updateVIP(t *testing.T, VIPID string) {
+	i1000 := 1000
+	_, err := vips.Update(base.Client, VIPID, vips.UpdateOpts{ConnLimit: &i1000}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated VIP ID [%s]", VIPID)
+}
+
+func getVIP(t *testing.T, VIPID string) {
+	vip, err := vips.Get(base.Client, VIPID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting VIP ID [%s]: Status [%s]", vip.ID, vip.Status)
+}
+
+func deleteVIP(t *testing.T, VIPID string) {
+	res := vips.Delete(base.Client, VIPID)
+
+	th.AssertNoErr(t, res.Err)
+
+	t.Logf("Deleted VIP %s", VIPID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/pkg.go b/acceptance/openstack/networking/v2/extensions/pkg.go
new file mode 100644
index 0000000..aeec0fa
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/pkg.go
@@ -0,0 +1 @@
+package extensions
diff --git a/acceptance/openstack/networking/v2/extensions/provider_test.go b/acceptance/openstack/networking/v2/extensions/provider_test.go
new file mode 100644
index 0000000..f10c9d9
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/provider_test.go
@@ -0,0 +1,68 @@
+// +build acceptance networking
+
+package extensions
+
+import (
+	"strconv"
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// Create a network
+	n, err := networks.Create(base.Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Name, "sample_network")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	networkID := n.ID
+
+	// List networks
+	pager := networks.List(base.Client, networks.ListOpts{Limit: 2})
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		networkList, err := networks.ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+
+		for _, n := range networkList {
+			t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+				n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+
+	// Get a network
+	if networkID == "" {
+		t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+	}
+	n, err = networks.Get(base.Client, networkID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.Subnets, []string{})
+	th.AssertEquals(t, n.Name, "sample_network")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.Shared, false)
+	th.AssertEquals(t, n.ID, networkID)
+
+	// Update network
+	n, err = networks.Update(base.Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Name, "new_network_name")
+
+	// Delete network
+	res := networks.Delete(base.Client, networkID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateMultipleNetworks(t *testing.T) {
+	//networks.CreateMany()
+}
diff --git a/acceptance/openstack/networking/v2/extensions/security_test.go b/acceptance/openstack/networking/v2/extensions/security_test.go
new file mode 100644
index 0000000..7d75292
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -0,0 +1,171 @@
+// +build acceptance networking security
+
+package extensions
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecurityGroups(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// create security group
+	groupID := createSecGroup(t)
+
+	// delete security group
+	defer deleteSecGroup(t, groupID)
+
+	// list security group
+	listSecGroups(t)
+
+	// get security group
+	getSecGroup(t, groupID)
+
+	// create port with security group
+	networkID, portID := createPort(t, groupID)
+
+	// teardown
+	defer deleteNetwork(t, networkID)
+
+	// delete port
+	defer deletePort(t, portID)
+}
+
+func TestSecurityGroupRules(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// create security group
+	groupID := createSecGroup(t)
+
+	defer deleteSecGroup(t, groupID)
+
+	// create security group rule
+	ruleID := createSecRule(t, groupID)
+
+	// delete security group rule
+	defer deleteSecRule(t, ruleID)
+
+	// list security group rule
+	listSecRules(t)
+
+	// get security group rule
+	getSecRule(t, ruleID)
+}
+
+func createSecGroup(t *testing.T) string {
+	sg, err := groups.Create(base.Client, groups.CreateOpts{
+		Name:        "new-webservers",
+		Description: "security group for webservers",
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created security group %s", sg.ID)
+
+	return sg.ID
+}
+
+func listSecGroups(t *testing.T) {
+	err := groups.List(base.Client, groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		list, err := groups.ExtractGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract secgroups: %v", err)
+			return false, err
+		}
+
+		for _, sg := range list {
+			t.Logf("Listing security group: ID [%s] Name [%s]", sg.ID, sg.Name)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getSecGroup(t *testing.T, id string) {
+	sg, err := groups.Get(base.Client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting security group: ID [%s] Name [%s] Description [%s]", sg.ID, sg.Name, sg.Description)
+}
+
+func createPort(t *testing.T, groupID string) (string, string) {
+	n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created network %s", n.ID)
+
+	opts := ports.CreateOpts{
+		NetworkID:      n.ID,
+		Name:           "my_port",
+		SecurityGroups: []string{groupID},
+	}
+	p, err := ports.Create(base.Client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created port %s with security group %s", p.ID, groupID)
+
+	return n.ID, p.ID
+}
+
+func deleteSecGroup(t *testing.T, groupID string) {
+	res := groups.Delete(base.Client, groupID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted security group %s", groupID)
+}
+
+func createSecRule(t *testing.T, groupID string) string {
+	r, err := rules.Create(base.Client, rules.CreateOpts{
+		Direction:    "ingress",
+		PortRangeMin: 80,
+		EtherType:    "IPv4",
+		PortRangeMax: 80,
+		Protocol:     "tcp",
+		SecGroupID:   groupID,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created security group rule %s", r.ID)
+
+	return r.ID
+}
+
+func listSecRules(t *testing.T) {
+	err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		list, err := rules.ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract sec rules: %v", err)
+			return false, err
+		}
+
+		for _, r := range list {
+			t.Logf("Listing security rule: ID [%s]", r.ID)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getSecRule(t *testing.T, id string) {
+	r, err := rules.Get(base.Client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting security rule: ID [%s] Direction [%s] EtherType [%s] Protocol [%s]",
+		r.ID, r.Direction, r.EtherType, r.Protocol)
+}
+
+func deleteSecRule(t *testing.T, id string) {
+	res := rules.Delete(base.Client, id)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted security rule %s", id)
+}
diff --git a/acceptance/openstack/networking/v2/network_test.go b/acceptance/openstack/networking/v2/network_test.go
new file mode 100644
index 0000000..be8a3a1
--- /dev/null
+++ b/acceptance/openstack/networking/v2/network_test.go
@@ -0,0 +1,68 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Create a network
+	n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: networks.Up}).Extract()
+	th.AssertNoErr(t, err)
+	defer networks.Delete(Client, n.ID)
+	th.AssertEquals(t, n.Name, "sample_network")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	networkID := n.ID
+
+	// List networks
+	pager := networks.List(Client, networks.ListOpts{Limit: 2})
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		networkList, err := networks.ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+
+		for _, n := range networkList {
+			t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+				n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+
+	// Get a network
+	if networkID == "" {
+		t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+	}
+	n, err = networks.Get(Client, networkID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.Subnets, []string{})
+	th.AssertEquals(t, n.Name, "sample_network")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.Shared, false)
+	th.AssertEquals(t, n.ID, networkID)
+
+	// Update network
+	n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Name, "new_network_name")
+
+	// Delete network
+	res := networks.Delete(Client, networkID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateMultipleNetworks(t *testing.T) {
+	//networks.CreateMany()
+}
diff --git a/acceptance/openstack/networking/v2/pkg.go b/acceptance/openstack/networking/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/openstack/networking/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/openstack/networking/v2/port_test.go b/acceptance/openstack/networking/v2/port_test.go
new file mode 100644
index 0000000..7f22dbd
--- /dev/null
+++ b/acceptance/openstack/networking/v2/port_test.go
@@ -0,0 +1,117 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPortCRUD(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Setup network
+	t.Log("Setting up network")
+	networkID, err := createNetwork()
+	th.AssertNoErr(t, err)
+	defer networks.Delete(Client, networkID)
+
+	// Setup subnet
+	t.Logf("Setting up subnet on network %s", networkID)
+	subnetID, err := createSubnet(networkID)
+	th.AssertNoErr(t, err)
+	defer subnets.Delete(Client, subnetID)
+
+	// Create port
+	t.Logf("Create port based on subnet %s", subnetID)
+	portID := createPort(t, networkID, subnetID)
+
+	// List ports
+	t.Logf("Listing all ports")
+	listPorts(t)
+
+	// Get port
+	if portID == "" {
+		t.Fatalf("In order to retrieve a port, the portID must be set")
+	}
+	p, err := ports.Get(Client, portID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, p.ID, portID)
+
+	// Update port
+	p, err = ports.Update(Client, portID, ports.UpdateOpts{Name: "new_port_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, p.Name, "new_port_name")
+
+	// Delete port
+	res := ports.Delete(Client, portID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func createPort(t *testing.T, networkID, subnetID string) string {
+	enable := false
+	opts := ports.CreateOpts{
+		NetworkID:    networkID,
+		Name:         "my_port",
+		AdminStateUp: &enable,
+		FixedIPs:     []ports.IP{ports.IP{SubnetID: subnetID}},
+	}
+	p, err := ports.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, p.NetworkID, networkID)
+	th.AssertEquals(t, p.Name, "my_port")
+	th.AssertEquals(t, p.AdminStateUp, false)
+
+	return p.ID
+}
+
+func listPorts(t *testing.T) {
+	count := 0
+	pager := ports.List(Client, ports.ListOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- Page ---")
+
+		portList, err := ports.ExtractPorts(page)
+		th.AssertNoErr(t, err)
+
+		for _, p := range portList {
+			t.Logf("Port: ID [%s] Name [%s] Status [%d] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
+				p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups)
+		}
+
+		return true, nil
+	})
+
+	th.CheckNoErr(t, err)
+
+	if count == 0 {
+		t.Logf("No pages were iterated over when listing ports")
+	}
+}
+
+func createNetwork() (string, error) {
+	res, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
+	return res.ID, err
+}
+
+func createSubnet(networkID string) (string, error) {
+	s, err := subnets.Create(Client, subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  subnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: subnets.Down,
+	}).Extract()
+	return s.ID, err
+}
+
+func TestPortBatchCreate(t *testing.T) {
+	// todo
+}
diff --git a/acceptance/openstack/networking/v2/subnet_test.go b/acceptance/openstack/networking/v2/subnet_test.go
new file mode 100644
index 0000000..097a303
--- /dev/null
+++ b/acceptance/openstack/networking/v2/subnet_test.go
@@ -0,0 +1,86 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	pager := subnets.List(Client, subnets.ListOpts{Limit: 2})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		subnetList, err := subnets.ExtractSubnets(page)
+		th.AssertNoErr(t, err)
+
+		for _, s := range subnetList {
+			t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]",
+				s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
+
+func TestCRUD(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Setup network
+	t.Log("Setting up network")
+	n, err := networks.Create(Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
+	th.AssertNoErr(t, err)
+	networkID := n.ID
+	defer networks.Delete(Client, networkID)
+
+	// Create subnet
+	t.Log("Create subnet")
+	enable := false
+	opts := subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  subnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: &enable,
+	}
+	s, err := subnets.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.NetworkID, networkID)
+	th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.Name, "my_subnet")
+	th.AssertEquals(t, s.EnableDHCP, false)
+	subnetID := s.ID
+
+	// Get subnet
+	t.Log("Getting subnet")
+	s, err = subnets.Get(Client, subnetID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, s.ID, subnetID)
+
+	// Update subnet
+	t.Log("Update subnet")
+	s, err = subnets.Update(Client, subnetID, subnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, s.Name, "new_subnet_name")
+
+	// Delete subnet
+	t.Log("Delete subnet")
+	res := subnets.Delete(Client, subnetID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestBatchCreate(t *testing.T) {
+	// todo
+}
diff --git a/acceptance/openstack/objectstorage/v1/accounts_test.go b/acceptance/openstack/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..f7c01a7
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/accounts_test.go
@@ -0,0 +1,44 @@
+// +build acceptance
+
+package v1
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAccounts(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	// Update an account's metadata.
+	updateres := accounts.Update(client, accounts.UpdateOpts{Metadata: metadata})
+	th.AssertNoErr(t, updateres.Err)
+
+	// Defer the deletion of the metadata set above.
+	defer func() {
+		tempMap := make(map[string]string)
+		for k := range metadata {
+			tempMap[k] = ""
+		}
+		updateres = accounts.Update(client, accounts.UpdateOpts{Metadata: tempMap})
+		th.AssertNoErr(t, updateres.Err)
+	}()
+
+	// Retrieve account metadata.
+	getres := accounts.Get(client, nil)
+	th.AssertNoErr(t, getres.Err)
+	// Extract the custom metadata from the 'Get' response.
+	am, err := getres.ExtractMetadata()
+	th.AssertNoErr(t, err)
+	for k := range metadata {
+		if am[k] != metadata[strings.Title(k)] {
+			t.Errorf("Expected custom metadata with key: %s", k)
+			return
+		}
+	}
+}
diff --git a/acceptance/openstack/objectstorage/v1/common.go b/acceptance/openstack/objectstorage/v1/common.go
new file mode 100644
index 0000000..1eac681
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/common.go
@@ -0,0 +1,28 @@
+// +build acceptance
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var metadata = map[string]string{"gopher": "cloud"}
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+	ao, err := openstack.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := openstack.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	c, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+	th.AssertNoErr(t, err)
+	return c
+}
diff --git a/acceptance/openstack/objectstorage/v1/containers_test.go b/acceptance/openstack/objectstorage/v1/containers_test.go
new file mode 100644
index 0000000..d6832f1
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/containers_test.go
@@ -0,0 +1,89 @@
+// +build acceptance
+
+package v1
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// numContainers is the number of containers to create for testing.
+var numContainers = 2
+
+func TestContainers(t *testing.T) {
+	// Create a new client to execute the HTTP requests. See common.go for newClient body.
+	client := newClient(t)
+
+	// Create a slice of random container names.
+	cNames := make([]string, numContainers)
+	for i := 0; i < numContainers; i++ {
+		cNames[i] = tools.RandomString("gophercloud-test-container-", 8)
+	}
+
+	// Create numContainers containers.
+	for i := 0; i < len(cNames); i++ {
+		res := containers.Create(client, cNames[i], nil)
+		th.AssertNoErr(t, res.Err)
+	}
+	// Delete the numContainers containers after function completion.
+	defer func() {
+		for i := 0; i < len(cNames); i++ {
+			res := containers.Delete(client, cNames[i])
+			th.AssertNoErr(t, res.Err)
+		}
+	}()
+
+	// List the numContainer names that were just created. To just list those,
+	// the 'prefix' parameter is used.
+	err := containers.List(client, &containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+		containerList, err := containers.ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		for _, n := range containerList {
+			t.Logf("Container: Name [%s] Count [%d] Bytes [%d]",
+				n.Name, n.Count, n.Bytes)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	// List the info for the numContainer containers that were created.
+	err = containers.List(client, &containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"}).EachPage(func(page pagination.Page) (bool, error) {
+		containerList, err := containers.ExtractNames(page)
+		th.AssertNoErr(t, err)
+		for _, n := range containerList {
+			t.Logf("Container: Name [%s]", n)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	// Update one of the numContainer container metadata.
+	updateres := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: metadata})
+	th.AssertNoErr(t, updateres.Err)
+	// After the tests are done, delete the metadata that was set.
+	defer func() {
+		tempMap := make(map[string]string)
+		for k := range metadata {
+			tempMap[k] = ""
+		}
+		res := containers.Update(client, cNames[0], &containers.UpdateOpts{Metadata: tempMap})
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	// Retrieve a container's metadata.
+	cm, err := containers.Get(client, cNames[0]).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	for k := range metadata {
+		if cm[k] != metadata[strings.Title(k)] {
+			t.Errorf("Expected custom metadata with key: %s", k)
+		}
+	}
+}
diff --git a/acceptance/openstack/objectstorage/v1/objects_test.go b/acceptance/openstack/objectstorage/v1/objects_test.go
new file mode 100644
index 0000000..987f733
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/objects_test.go
@@ -0,0 +1,118 @@
+// +build acceptance
+
+package v1
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// numObjects is the number of objects to create for testing.
+var numObjects = 2
+
+func TestObjects(t *testing.T) {
+	// Create a provider client for executing the HTTP request.
+	// See common.go for more information.
+	client := newClient(t)
+
+	// Make a slice of length numObjects to hold the random object names.
+	oNames := make([]string, numObjects)
+	for i := 0; i < len(oNames); i++ {
+		oNames[i] = tools.RandomString("test-object-", 8)
+	}
+
+	// Create a container to hold the test objects.
+	cName := tools.RandomString("test-container-", 8)
+	createres := containers.Create(client, cName, nil)
+	th.AssertNoErr(t, createres.Err)
+
+	// Defer deletion of the container until after testing.
+	defer func() {
+		res := containers.Delete(client, cName)
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	// Create a slice of buffers to hold the test object content.
+	oContents := make([]*bytes.Buffer, numObjects)
+	for i := 0; i < numObjects; i++ {
+		oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
+		res := objects.Create(client, cName, oNames[i], oContents[i], nil)
+		th.AssertNoErr(t, res.Err)
+	}
+	// Delete the objects after testing.
+	defer func() {
+		for i := 0; i < numObjects; i++ {
+			res := objects.Delete(client, cName, oNames[i], nil)
+			th.AssertNoErr(t, res.Err)
+		}
+	}()
+
+	ons := make([]string, 0, len(oNames))
+	err := objects.List(client, cName, &objects.ListOpts{Full: false, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+		names, err := objects.ExtractNames(page)
+		th.AssertNoErr(t, err)
+		ons = append(ons, names...)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, len(ons), len(oNames))
+
+	ois := make([]objects.Object, 0, len(oNames))
+	err = objects.List(client, cName, &objects.ListOpts{Full: true, Prefix: "test-object-"}).EachPage(func(page pagination.Page) (bool, error) {
+		info, err := objects.ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		ois = append(ois, info...)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, len(ois), len(oNames))
+
+	// Copy the contents of one object to another.
+	copyres := objects.Copy(client, cName, oNames[0], &objects.CopyOpts{Destination: cName + "/" + oNames[1]})
+	th.AssertNoErr(t, copyres.Err)
+
+	// Download one of the objects that was created above.
+	o1Content, err := objects.Download(client, cName, oNames[0], nil).ExtractContent()
+	th.AssertNoErr(t, err)
+
+	// Download the another object that was create above.
+	o2Content, err := objects.Download(client, cName, oNames[1], nil).ExtractContent()
+	th.AssertNoErr(t, err)
+
+	// Compare the two object's contents to test that the copy worked.
+	th.AssertEquals(t, string(o2Content), string(o1Content))
+
+	// Update an object's metadata.
+	updateres := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: metadata})
+	th.AssertNoErr(t, updateres.Err)
+
+	// Delete the object's metadata after testing.
+	defer func() {
+		tempMap := make(map[string]string)
+		for k := range metadata {
+			tempMap[k] = ""
+		}
+		res := objects.Update(client, cName, oNames[0], &objects.UpdateOpts{Metadata: tempMap})
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	// Retrieve an object's metadata.
+	om, err := objects.Get(client, cName, oNames[0], nil).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	for k := range metadata {
+		if om[k] != metadata[strings.Title(k)] {
+			t.Errorf("Expected custom metadata with key: %s", k)
+			return
+		}
+	}
+}
diff --git a/acceptance/openstack/pkg.go b/acceptance/openstack/pkg.go
new file mode 100644
index 0000000..3a8ecdb
--- /dev/null
+++ b/acceptance/openstack/pkg.go
@@ -0,0 +1,4 @@
+// +build acceptance
+
+package openstack
+
diff --git a/acceptance/rackspace/blockstorage/v1/common.go b/acceptance/rackspace/blockstorage/v1/common.go
new file mode 100644
index 0000000..e9fdd99
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/common.go
@@ -0,0 +1,38 @@
+// +build acceptance
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	opts, err := rackspace.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+	opts = tools.OnlyRS(opts)
+	region := os.Getenv("RS_REGION")
+
+	provider, err := rackspace.AuthenticatedClient(opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewBlockStorageV1(provider, gophercloud.EndpointOpts{
+		Region: region,
+	})
+}
+
+func setup(t *testing.T) *gophercloud.ServiceClient {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	return client
+}
diff --git a/acceptance/rackspace/blockstorage/v1/snapshot_test.go b/acceptance/rackspace/blockstorage/v1/snapshot_test.go
new file mode 100644
index 0000000..be1314b
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/snapshot_test.go
@@ -0,0 +1,82 @@
+// +build acceptance blockstorage snapshots
+
+package v1
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSnapshots(t *testing.T) {
+	client := setup(t)
+	volID := testVolumeCreate(t, client)
+
+	t.Log("Creating snapshots")
+	s := testSnapshotCreate(t, client, volID)
+	id := s.ID
+
+	t.Log("Listing snapshots")
+	testSnapshotList(t, client)
+
+	t.Logf("Getting snapshot %s", id)
+	testSnapshotGet(t, client, id)
+
+	t.Logf("Updating snapshot %s", id)
+	testSnapshotUpdate(t, client, id)
+
+	t.Logf("Deleting snapshot %s", id)
+	testSnapshotDelete(t, client, id)
+	s.WaitUntilDeleted(client, -1)
+
+	t.Logf("Deleting volume %s", volID)
+	testVolumeDelete(t, client, volID)
+}
+
+func testSnapshotCreate(t *testing.T, client *gophercloud.ServiceClient, volID string) *snapshots.Snapshot {
+	opts := snapshots.CreateOpts{VolumeID: volID, Name: "snapshot-001"}
+	s, err := snapshots.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created snapshot %s", s.ID)
+
+	t.Logf("Waiting for new snapshot to become available...")
+	start := time.Now().Second()
+	s.WaitUntilComplete(client, -1)
+	t.Logf("Snapshot completed after %ds", time.Now().Second()-start)
+
+	return s
+}
+
+func testSnapshotList(t *testing.T, client *gophercloud.ServiceClient) {
+	snapshots.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		sList, err := snapshots.ExtractSnapshots(page)
+		th.AssertNoErr(t, err)
+
+		for _, s := range sList {
+			t.Logf("Snapshot: ID [%s] Name [%s] Volume ID [%s] Progress [%s] Created [%s]",
+				s.ID, s.Name, s.VolumeID, s.Progress, s.CreatedAt)
+		}
+
+		return true, nil
+	})
+}
+
+func testSnapshotGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	_, err := snapshots.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func testSnapshotUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	_, err := snapshots.Update(client, id, snapshots.UpdateOpts{Name: "new_name"}).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func testSnapshotDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := snapshots.Delete(client, id)
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted snapshot %s", id)
+}
diff --git a/acceptance/rackspace/blockstorage/v1/volume_test.go b/acceptance/rackspace/blockstorage/v1/volume_test.go
new file mode 100644
index 0000000..5a52ac7
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/volume_test.go
@@ -0,0 +1,71 @@
+// +build acceptance blockstorage volumes
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVolumes(t *testing.T) {
+	client := setup(t)
+
+	t.Logf("Listing volumes")
+	testVolumeList(t, client)
+
+	t.Logf("Creating volume")
+	volumeID := testVolumeCreate(t, client)
+
+	t.Logf("Getting volume %s", volumeID)
+	testVolumeGet(t, client, volumeID)
+
+	t.Logf("Updating volume %s", volumeID)
+	testVolumeUpdate(t, client, volumeID)
+
+	t.Logf("Deleting volume %s", volumeID)
+	testVolumeDelete(t, client, volumeID)
+}
+
+func testVolumeList(t *testing.T, client *gophercloud.ServiceClient) {
+	volumes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		vList, err := volumes.ExtractVolumes(page)
+		th.AssertNoErr(t, err)
+
+		for _, v := range vList {
+			t.Logf("Volume: ID [%s] Name [%s] Type [%s] Created [%s]", v.ID, v.Name,
+				v.VolumeType, v.CreatedAt)
+		}
+
+		return true, nil
+	})
+}
+
+func testVolumeCreate(t *testing.T, client *gophercloud.ServiceClient) string {
+	vol, err := volumes.Create(client, os.CreateOpts{Size: 75}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size)
+	return vol.ID
+}
+
+func testVolumeGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	vol, err := volumes.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size)
+}
+
+func testVolumeUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	vol, err := volumes.Update(client, id, volumes.UpdateOpts{Name: "new_name"}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created volume: ID [%s] Name [%s]", vol.ID, vol.Name)
+}
+
+func testVolumeDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := volumes.Delete(client, id)
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted volume %s", id)
+}
diff --git a/acceptance/rackspace/blockstorage/v1/volume_type_test.go b/acceptance/rackspace/blockstorage/v1/volume_type_test.go
new file mode 100644
index 0000000..716f2b9
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/volume_type_test.go
@@ -0,0 +1,46 @@
+// +build acceptance blockstorage volumetypes
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAll(t *testing.T) {
+	client := setup(t)
+
+	t.Logf("Listing volume types")
+	id := testList(t, client)
+
+	t.Logf("Getting volume type %s", id)
+	testGet(t, client, id)
+}
+
+func testList(t *testing.T, client *gophercloud.ServiceClient) string {
+	var lastID string
+
+	volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		typeList, err := volumetypes.ExtractVolumeTypes(page)
+		th.AssertNoErr(t, err)
+
+		for _, vt := range typeList {
+			t.Logf("Volume type: ID [%s] Name [%s]", vt.ID, vt.Name)
+			lastID = vt.ID
+		}
+
+		return true, nil
+	})
+
+	return lastID
+}
+
+func testGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	vt, err := volumetypes.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Volume: ID [%s] Name [%s]", vt.ID, vt.Name)
+}
diff --git a/acceptance/rackspace/client_test.go b/acceptance/rackspace/client_test.go
new file mode 100644
index 0000000..61214c0
--- /dev/null
+++ b/acceptance/rackspace/client_test.go
@@ -0,0 +1,28 @@
+// +build acceptance
+
+package rackspace
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClient(t *testing.T) {
+	// Obtain credentials from the environment.
+	ao, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := rackspace.AuthenticatedClient(tools.OnlyRS(ao))
+	if err != nil {
+		t.Fatalf("Unable to authenticate: %v", err)
+	}
+
+	if client.TokenID == "" {
+		t.Errorf("No token ID assigned to the client")
+	}
+
+	t.Logf("Client successfully acquired a token: %v", client.TokenID)
+}
diff --git a/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
new file mode 100644
index 0000000..010bf42
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
@@ -0,0 +1,46 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/smashwilson/gophercloud/acceptance/tools"
+)
+
+func TestBootFromVolume(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("Gophercloud-", 8)
+	t.Logf("Creating server [%s].", name)
+
+	bd := []osBFV.BlockDevice{
+		osBFV.BlockDevice{
+			UUID:       options.imageID,
+			SourceType: osBFV.Image,
+			VolumeSize: 10,
+		},
+	}
+
+	server, err := bootfromvolume.Create(client, servers.CreateOpts{
+		Name:        name,
+		FlavorRef:   "performance1-1",
+		BlockDevice: bd,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created server: %+v\n", server)
+	//defer deleteServer(t, client, server)
+	t.Logf("Deleting server [%s]...", name)
+}
diff --git a/acceptance/rackspace/compute/v2/compute_test.go b/acceptance/rackspace/compute/v2/compute_test.go
new file mode 100644
index 0000000..3ca6dc9
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/compute_test.go
@@ -0,0 +1,60 @@
+// +build acceptance
+
+package v2
+
+import (
+	"errors"
+	"os"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	// Obtain credentials from the environment.
+	options, err := rackspace.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+	options = tools.OnlyRS(options)
+	region := os.Getenv("RS_REGION")
+
+	if options.Username == "" {
+		return nil, errors.New("Please provide a Rackspace username as RS_USERNAME.")
+	}
+	if options.APIKey == "" {
+		return nil, errors.New("Please provide a Rackspace API key as RS_API_KEY.")
+	}
+	if region == "" {
+		return nil, errors.New("Please provide a Rackspace region as RS_REGION.")
+	}
+
+	client, err := rackspace.AuthenticatedClient(options)
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewComputeV2(client, gophercloud.EndpointOpts{
+		Region: region,
+	})
+}
+
+type serverOpts struct {
+	imageID  string
+	flavorID string
+}
+
+func optionsFromEnv() (*serverOpts, error) {
+	options := &serverOpts{
+		imageID:  os.Getenv("RS_IMAGE_ID"),
+		flavorID: os.Getenv("RS_FLAVOR_ID"),
+	}
+	if options.imageID == "" {
+		return nil, errors.New("Please provide a valid Rackspace image ID as RS_IMAGE_ID")
+	}
+	if options.flavorID == "" {
+		return nil, errors.New("Please provide a valid Rackspace flavor ID as RS_FLAVOR_ID")
+	}
+	return options, nil
+}
diff --git a/acceptance/rackspace/compute/v2/flavors_test.go b/acceptance/rackspace/compute/v2/flavors_test.go
new file mode 100644
index 0000000..4618ecc
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/flavors_test.go
@@ -0,0 +1,61 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/flavors"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListFlavors(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	count := 0
+	err = flavors.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("-- Page %0d --", count)
+
+		fs, err := flavors.ExtractFlavors(page)
+		th.AssertNoErr(t, err)
+
+		for i, flavor := range fs {
+			t.Logf("[%02d]      id=[%s]", i, flavor.ID)
+			t.Logf("        name=[%s]", flavor.Name)
+			t.Logf("        disk=[%d]", flavor.Disk)
+			t.Logf("         RAM=[%d]", flavor.RAM)
+			t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor)
+			t.Logf("        swap=[%d]", flavor.Swap)
+			t.Logf("       VCPUs=[%d]", flavor.VCPUs)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No flavors listed!")
+	}
+}
+
+func TestGetFlavor(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	flavor, err := flavors.Get(client, options.flavorID).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Requested flavor:")
+	t.Logf("          id=[%s]", flavor.ID)
+	t.Logf("        name=[%s]", flavor.Name)
+	t.Logf("        disk=[%d]", flavor.Disk)
+	t.Logf("         RAM=[%d]", flavor.RAM)
+	t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor)
+	t.Logf("        swap=[%d]", flavor.Swap)
+	t.Logf("       VCPUs=[%d]", flavor.VCPUs)
+}
diff --git a/acceptance/rackspace/compute/v2/images_test.go b/acceptance/rackspace/compute/v2/images_test.go
new file mode 100644
index 0000000..5e36c2e
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/images_test.go
@@ -0,0 +1,63 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/images"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListImages(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	count := 0
+	err = images.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("-- Page %02d --", count)
+
+		is, err := images.ExtractImages(page)
+		th.AssertNoErr(t, err)
+
+		for i, image := range is {
+			t.Logf("[%02d]   id=[%s]", i, image.ID)
+			t.Logf("     name=[%s]", image.Name)
+			t.Logf("  created=[%s]", image.Created)
+			t.Logf("  updated=[%s]", image.Updated)
+			t.Logf(" min disk=[%d]", image.MinDisk)
+			t.Logf("  min RAM=[%d]", image.MinRAM)
+			t.Logf(" progress=[%d]", image.Progress)
+			t.Logf("   status=[%s]", image.Status)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count < 1 {
+		t.Errorf("Expected at least one page of images.")
+	}
+}
+
+func TestGetImage(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	image, err := images.Get(client, options.imageID).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Requested image:")
+	t.Logf("       id=[%s]", image.ID)
+	t.Logf("     name=[%s]", image.Name)
+	t.Logf("  created=[%s]", image.Created)
+	t.Logf("  updated=[%s]", image.Updated)
+	t.Logf(" min disk=[%d]", image.MinDisk)
+	t.Logf("  min RAM=[%d]", image.MinRAM)
+	t.Logf(" progress=[%d]", image.Progress)
+	t.Logf("   status=[%s]", image.Status)
+}
diff --git a/acceptance/rackspace/compute/v2/keypairs_test.go b/acceptance/rackspace/compute/v2/keypairs_test.go
new file mode 100644
index 0000000..ac5a1f2
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/keypairs_test.go
@@ -0,0 +1,87 @@
+// +build acceptance rackspace
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func deleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, name string) {
+	err := keypairs.Delete(client, name).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully deleted key [%s].", name)
+}
+
+func TestCreateKeyPair(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("createdkey-", 8)
+	k, err := keypairs.Create(client, os.CreateOpts{Name: name}).Extract()
+	th.AssertNoErr(t, err)
+	defer deleteKeyPair(t, client, name)
+
+	t.Logf("Created a new keypair:")
+	t.Logf("        name=[%s]", k.Name)
+	t.Logf(" fingerprint=[%s]", k.Fingerprint)
+	t.Logf("   publickey=[%s]", tools.Elide(k.PublicKey))
+	t.Logf("  privatekey=[%s]", tools.Elide(k.PrivateKey))
+	t.Logf("      userid=[%s]", k.UserID)
+}
+
+func TestImportKeyPair(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("importedkey-", 8)
+	pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter"
+
+	k, err := keypairs.Create(client, os.CreateOpts{
+		Name:      name,
+		PublicKey: pubkey,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer deleteKeyPair(t, client, name)
+
+	th.CheckEquals(t, pubkey, k.PublicKey)
+	th.CheckEquals(t, "", k.PrivateKey)
+
+	t.Logf("Imported an existing keypair:")
+	t.Logf("        name=[%s]", k.Name)
+	t.Logf(" fingerprint=[%s]", k.Fingerprint)
+	t.Logf("   publickey=[%s]", tools.Elide(k.PublicKey))
+	t.Logf("  privatekey=[%s]", tools.Elide(k.PrivateKey))
+	t.Logf("      userid=[%s]", k.UserID)
+}
+
+func TestListKeyPairs(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	count := 0
+	err = keypairs.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- %02d ---", count)
+
+		ks, err := keypairs.ExtractKeyPairs(page)
+		th.AssertNoErr(t, err)
+
+		for i, keypair := range ks {
+			t.Logf("[%02d]    name=[%s]", i, keypair.Name)
+			t.Logf(" fingerprint=[%s]", keypair.Fingerprint)
+			t.Logf("   publickey=[%s]", tools.Elide(keypair.PublicKey))
+			t.Logf("  privatekey=[%s]", tools.Elide(keypair.PrivateKey))
+			t.Logf("      userid=[%s]", keypair.UserID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/rackspace/compute/v2/networks_test.go b/acceptance/rackspace/compute/v2/networks_test.go
new file mode 100644
index 0000000..e8fc4d3
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/networks_test.go
@@ -0,0 +1,53 @@
+// +build acceptance rackspace
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/networks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworks(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	// Create a network
+	n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created network: %+v\n", n)
+	defer networks.Delete(client, n.ID)
+	th.AssertEquals(t, n.Label, "sample_network")
+	th.AssertEquals(t, n.CIDR, "172.20.0.0/24")
+	networkID := n.ID
+
+	// List networks
+	pager := networks.List(client)
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		networkList, err := networks.ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+
+		for _, n := range networkList {
+			t.Logf("Network: ID [%s] Label [%s] CIDR [%s]",
+				n.ID, n.Label, n.CIDR)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+
+	// Get a network
+	if networkID == "" {
+		t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+	}
+	n, err = networks.Get(client, networkID).Extract()
+	t.Logf("Retrieved Network: %+v\n", n)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.CIDR, "172.20.0.0/24")
+	th.AssertEquals(t, n.Label, "sample_network")
+	th.AssertEquals(t, n.ID, networkID)
+}
diff --git a/acceptance/rackspace/compute/v2/pkg.go b/acceptance/rackspace/compute/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
new file mode 100644
index 0000000..5359450
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -0,0 +1,199 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+	oskey "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createServerKeyPair(t *testing.T, client *gophercloud.ServiceClient) *oskey.KeyPair {
+	name := tools.RandomString("importedkey-", 8)
+	pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter"
+
+	k, err := keypairs.Create(client, oskey.CreateOpts{
+		Name:      name,
+		PublicKey: pubkey,
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	return k
+}
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName string) *os.Server {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("Gophercloud-", 8)
+
+	opts := &servers.CreateOpts{
+		Name:       name,
+		ImageRef:   options.imageID,
+		FlavorRef:  options.flavorID,
+		DiskConfig: diskconfig.Manual,
+	}
+
+	if keyName != "" {
+		opts.KeyPair = keyName
+	}
+
+	t.Logf("Creating server [%s].", name)
+	s, err := servers.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Creating server.")
+
+	err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+	t.Logf("Server created successfully.")
+
+	return s
+}
+
+func logServer(t *testing.T, server *os.Server, index int) {
+	if index == -1 {
+		t.Logf("             id=[%s]", server.ID)
+	} else {
+		t.Logf("[%02d]             id=[%s]", index, server.ID)
+	}
+	t.Logf("           name=[%s]", server.Name)
+	t.Logf("      tenant ID=[%s]", server.TenantID)
+	t.Logf("        user ID=[%s]", server.UserID)
+	t.Logf("        updated=[%s]", server.Updated)
+	t.Logf("        created=[%s]", server.Created)
+	t.Logf("        host ID=[%s]", server.HostID)
+	t.Logf("    access IPv4=[%s]", server.AccessIPv4)
+	t.Logf("    access IPv6=[%s]", server.AccessIPv6)
+	t.Logf("          image=[%v]", server.Image)
+	t.Logf("         flavor=[%v]", server.Flavor)
+	t.Logf("      addresses=[%v]", server.Addresses)
+	t.Logf("       metadata=[%v]", server.Metadata)
+	t.Logf("          links=[%v]", server.Links)
+	t.Logf("        keyname=[%s]", server.KeyName)
+	t.Logf(" admin password=[%s]", server.AdminPass)
+	t.Logf("         status=[%s]", server.Status)
+	t.Logf("       progress=[%d]", server.Progress)
+}
+
+func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Get")
+
+	details, err := servers.Get(client, server.ID).Extract()
+	th.AssertNoErr(t, err)
+	logServer(t, details, -1)
+}
+
+func listServers(t *testing.T, client *gophercloud.ServiceClient) {
+	t.Logf("> servers.List")
+
+	count := 0
+	err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- Page %02d ---", count)
+
+		s, err := servers.ExtractServers(page)
+		th.AssertNoErr(t, err)
+		for index, server := range s {
+			logServer(t, &server, index)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.ChangeAdminPassword")
+
+	original := server.AdminPass
+
+	t.Logf("Changing server password.")
+	err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract()
+	th.AssertNoErr(t, err)
+
+	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+	t.Logf("Password changed successfully.")
+}
+
+func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Reboot")
+
+	err := servers.Reboot(client, server.ID, os.HardReboot).Extract()
+	th.AssertNoErr(t, err)
+
+	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Server successfully rebooted.")
+}
+
+func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Rebuild")
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	opts := servers.RebuildOpts{
+		Name:       tools.RandomString("RenamedGopher", 16),
+		AdminPass:  tools.MakeNewPassword(server.AdminPass),
+		ImageID:    options.imageID,
+		DiskConfig: diskconfig.Manual,
+	}
+	after, err := servers.Rebuild(client, server.ID, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, after.ID, server.ID)
+
+	err = servers.WaitForStatus(client, after.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Server successfully rebuilt.")
+	logServer(t, after, -1)
+}
+
+func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Delete")
+
+	err := servers.Delete(client, server.ID)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Server deleted successfully.")
+}
+
+func deleteServerKeyPair(t *testing.T, client *gophercloud.ServiceClient, k *oskey.KeyPair) {
+	t.Logf("> keypairs.Delete")
+
+	err := keypairs.Delete(client, k.Name).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Keypair deleted successfully.")
+}
+
+func TestServerOperations(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	kp := createServerKeyPair(t, client)
+	defer deleteServerKeyPair(t, client, kp)
+
+	server := createServer(t, client, kp.Name)
+	defer deleteServer(t, client, server)
+
+	getServer(t, client, server)
+	listServers(t, client)
+	changeAdminPassword(t, client, server)
+	rebootServer(t, client, server)
+	rebuildServer(t, client, server)
+}
diff --git a/acceptance/rackspace/compute/v2/virtualinterfaces_test.go b/acceptance/rackspace/compute/v2/virtualinterfaces_test.go
new file mode 100644
index 0000000..39475e1
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/virtualinterfaces_test.go
@@ -0,0 +1,53 @@
+// +build acceptance rackspace
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/networks"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/virtualinterfaces"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVirtualInterfaces(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	// Create a server
+	server := createServer(t, client, "")
+	t.Logf("Created Server: %v\n", server)
+	defer deleteServer(t, client, server)
+	serverID := server.ID
+
+	// Create a network
+	n, err := networks.Create(client, networks.CreateOpts{Label: "sample_network", CIDR: "172.20.0.0/24"}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created Network: %v\n", n)
+	defer networks.Delete(client, n.ID)
+	networkID := n.ID
+
+	// Create a virtual interface
+	vi, err := virtualinterfaces.Create(client, serverID, networkID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created virtual interface: %+v\n", vi)
+	defer virtualinterfaces.Delete(client, serverID, vi.ID)
+
+	// List virtual interfaces
+	pager := virtualinterfaces.List(client, serverID)
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		virtualinterfacesList, err := virtualinterfaces.ExtractVirtualInterfaces(page)
+		th.AssertNoErr(t, err)
+
+		for _, vi := range virtualinterfacesList {
+			t.Logf("Virtual Interface: ID [%s] MAC Address [%s] IP Addresses [%v]",
+				vi.ID, vi.MACAddress, vi.IPAddresses)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
diff --git a/acceptance/rackspace/identity/v2/extension_test.go b/acceptance/rackspace/identity/v2/extension_test.go
new file mode 100644
index 0000000..a50e015
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/extension_test.go
@@ -0,0 +1,54 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	extensions2 "github.com/rackspace/gophercloud/rackspace/identity/v2/extensions"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestExtensions(t *testing.T) {
+	service := authenticatedClient(t)
+
+	t.Logf("Extensions available on this identity endpoint:")
+	count := 0
+	var chosen string
+	err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		extensions, err := extensions2.ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+
+		for i, ext := range extensions {
+			if chosen == "" {
+				chosen = ext.Alias
+			}
+
+			t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace)
+			t.Logf("     alias=[%s] updated=[%s]", ext.Alias, ext.Updated)
+			t.Logf("     description=[%s]", ext.Description)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if chosen == "" {
+		t.Logf("No extensions found.")
+		return
+	}
+
+	ext, err := extensions2.Get(service, chosen).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Detail for extension [%s]:", chosen)
+	t.Logf("        name=[%s]", ext.Name)
+	t.Logf("   namespace=[%s]", ext.Namespace)
+	t.Logf("       alias=[%s]", ext.Alias)
+	t.Logf("     updated=[%s]", ext.Updated)
+	t.Logf(" description=[%s]", ext.Description)
+}
diff --git a/acceptance/rackspace/identity/v2/identity_test.go b/acceptance/rackspace/identity/v2/identity_test.go
new file mode 100644
index 0000000..1182982
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/identity_test.go
@@ -0,0 +1,50 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
+	// Obtain credentials from the environment.
+	options, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+	options = tools.OnlyRS(options)
+
+	if options.Username == "" {
+		t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
+	}
+	if options.APIKey == "" {
+		t.Fatal("Please provide a Rackspace API key as RS_API_KEY.")
+	}
+
+	return options
+}
+
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+	ao := rackspaceAuthOptions(t)
+
+	provider, err := rackspace.NewClient(ao.IdentityEndpoint)
+	th.AssertNoErr(t, err)
+
+	if auth {
+		err = rackspace.Authenticate(provider, ao)
+		th.AssertNoErr(t, err)
+	}
+
+	return rackspace.NewIdentityV2(provider)
+}
+
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+	return createClient(t, true)
+}
diff --git a/acceptance/rackspace/identity/v2/tenant_test.go b/acceptance/rackspace/identity/v2/tenant_test.go
new file mode 100644
index 0000000..6081a49
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/tenant_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	rstenants "github.com/rackspace/gophercloud/rackspace/identity/v2/tenants"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTenants(t *testing.T) {
+	service := authenticatedClient(t)
+
+	t.Logf("Tenants available to the currently issued token:")
+	count := 0
+	err := rstenants.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		tenants, err := rstenants.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		for i, tenant := range tenants {
+			t.Logf("[%02d]      id=[%s]", i, tenant.ID)
+			t.Logf("        name=[%s] enabled=[%v]", i, tenant.Name, tenant.Enabled)
+			t.Logf(" description=[%s]", tenant.Description)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No tenants listed for your current token.")
+	}
+}
diff --git a/acceptance/rackspace/objectstorage/v1/accounts_test.go b/acceptance/rackspace/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..145e4e0
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/accounts_test.go
@@ -0,0 +1,33 @@
+// +build acceptance rackspace
+
+package v1
+
+import (
+	"testing"
+
+	raxAccounts "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/accounts"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAccounts(t *testing.T) {
+	c, err := createClient(t, false)
+	th.AssertNoErr(t, err)
+
+	updateres := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}})
+	th.AssertNoErr(t, updateres.Err)
+	t.Logf("Headers from Update Account request: %+v\n", updateres.Header)
+	defer func() {
+		updateres = raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": ""}})
+		th.AssertNoErr(t, updateres.Err)
+		metadata, err := raxAccounts.Get(c).ExtractMetadata()
+		th.AssertNoErr(t, err)
+		t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata)
+		th.CheckEquals(t, metadata["White"], "")
+	}()
+
+	metadata, err := raxAccounts.Get(c).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadata from Get Account request (after update): %+v\n", metadata)
+
+	th.CheckEquals(t, metadata["White"], "mountains")
+}
diff --git a/acceptance/rackspace/objectstorage/v1/bulk_test.go b/acceptance/rackspace/objectstorage/v1/bulk_test.go
new file mode 100644
index 0000000..79013a5
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/bulk_test.go
@@ -0,0 +1,23 @@
+// +build acceptance rackspace objectstorage v1
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/rackspace/objectstorage/v1/bulk"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBulk(t *testing.T) {
+	c, err := createClient(t, false)
+	th.AssertNoErr(t, err)
+
+	var options bulk.DeleteOpts
+	options = append(options, "container/object1")
+	res := bulk.Delete(c, options)
+	th.AssertNoErr(t, res.Err)
+	body, err := res.ExtractBody()
+	th.AssertNoErr(t, err)
+	t.Logf("Response body from Bulk Delete Request: %+v\n", body)
+}
diff --git a/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go b/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go
new file mode 100644
index 0000000..e1bf38b
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go
@@ -0,0 +1,61 @@
+// +build acceptance rackspace objectstorage v1
+
+package v1
+
+import (
+	"testing"
+
+	osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers"
+	raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCDNContainers(t *testing.T) {
+	raxClient, err := createClient(t, false)
+	th.AssertNoErr(t, err)
+
+	createres := raxContainers.Create(raxClient, "gophercloud-test", nil)
+	th.AssertNoErr(t, createres.Err)
+	t.Logf("Headers from Create Container request: %+v\n", createres.Header)
+	defer func() {
+		res := raxContainers.Delete(raxClient, "gophercloud-test")
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	raxCDNClient, err := createClient(t, true)
+	th.AssertNoErr(t, err)
+
+	r := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900})
+	th.AssertNoErr(t, r.Err)
+	t.Logf("Headers from Enable CDN Container request: %+v\n", r.Header)
+
+	t.Logf("Container Names available to the currently issued token:")
+	count := 0
+	err = raxCDNContainers.List(raxCDNClient, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		names, err := raxCDNContainers.ExtractNames(page)
+		th.AssertNoErr(t, err)
+
+		for i, name := range names {
+			t.Logf("[%02d] %s", i, name)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No CDN containers listed for your current token.")
+	}
+
+	updateres := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", raxCDNContainers.UpdateOpts{CDNEnabled: false})
+	th.AssertNoErr(t, updateres.Err)
+	t.Logf("Headers from Update CDN Container request: %+v\n", updateres.Header)
+
+	metadata, err := raxCDNContainers.Get(raxCDNClient, "gophercloud-test").ExtractMetadata()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Get CDN Container request (after update): %+v\n", metadata)
+}
diff --git a/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go b/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go
new file mode 100644
index 0000000..dfc2dca
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go
@@ -0,0 +1,46 @@
+// +build acceptance rackspace objectstorage v1
+
+package v1
+
+import (
+	"bytes"
+	"testing"
+
+	raxCDNContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers"
+	raxCDNObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdnobjects"
+	raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers"
+	raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCDNObjects(t *testing.T) {
+	raxClient, err := createClient(t, false)
+	th.AssertNoErr(t, err)
+
+	createContResult := raxContainers.Create(raxClient, "gophercloud-test", nil)
+	th.AssertNoErr(t, createContResult.Err)
+	t.Logf("Headers from Create Container request: %+v\n", createContResult.Header)
+	defer func() {
+		deleteResult := raxContainers.Delete(raxClient, "gophercloud-test")
+		th.AssertNoErr(t, deleteResult.Err)
+	}()
+
+	createObjResult := raxObjects.Create(raxClient, "gophercloud-test", "test-object", bytes.NewBufferString("gophercloud cdn test"), nil)
+	th.AssertNoErr(t, createObjResult.Err)
+	t.Logf("Headers from Create Object request: %+v\n", createObjResult.Header)
+	defer func() {
+		deleteResult := raxObjects.Delete(raxClient, "gophercloud-test", "test-object", nil)
+		th.AssertNoErr(t, deleteResult.Err)
+	}()
+
+	raxCDNClient, err := createClient(t, true)
+	th.AssertNoErr(t, err)
+
+	enableResult := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900})
+	th.AssertNoErr(t, enableResult.Err)
+	t.Logf("Headers from Enable CDN Container request: %+v\n", enableResult.Header)
+
+	deleteResult := raxCDNObjects.Delete(raxCDNClient, "gophercloud-test", "test-object", nil)
+	th.AssertNoErr(t, deleteResult.Err)
+	t.Logf("Headers from Delete CDN Object request: %+v\n", deleteResult.Err)
+}
diff --git a/acceptance/rackspace/objectstorage/v1/common.go b/acceptance/rackspace/objectstorage/v1/common.go
new file mode 100644
index 0000000..1ae0727
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/common.go
@@ -0,0 +1,54 @@
+// +build acceptance rackspace objectstorage v1
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func rackspaceAuthOptions(t *testing.T) gophercloud.AuthOptions {
+	// Obtain credentials from the environment.
+	options, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+	options = tools.OnlyRS(options)
+
+	if options.Username == "" {
+		t.Fatal("Please provide a Rackspace username as RS_USERNAME.")
+	}
+	if options.APIKey == "" {
+		t.Fatal("Please provide a Rackspace API key as RS_API_KEY.")
+	}
+
+	return options
+}
+
+func createClient(t *testing.T, cdn bool) (*gophercloud.ServiceClient, error) {
+	region := os.Getenv("RS_REGION")
+	if region == "" {
+		t.Fatal("Please provide a Rackspace region as RS_REGION")
+	}
+
+	ao := rackspaceAuthOptions(t)
+
+	provider, err := rackspace.NewClient(ao.IdentityEndpoint)
+	th.AssertNoErr(t, err)
+
+	err = rackspace.Authenticate(provider, ao)
+	th.AssertNoErr(t, err)
+
+	if cdn {
+		return rackspace.NewObjectCDNV1(provider, gophercloud.EndpointOpts{
+			Region: region,
+		})
+	}
+
+	return rackspace.NewObjectStorageV1(provider, gophercloud.EndpointOpts{
+		Region: region,
+	})
+}
diff --git a/acceptance/rackspace/objectstorage/v1/containers_test.go b/acceptance/rackspace/objectstorage/v1/containers_test.go
new file mode 100644
index 0000000..a7339cf
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/containers_test.go
@@ -0,0 +1,85 @@
+// +build acceptance rackspace objectstorage v1
+
+package v1
+
+import (
+	"testing"
+
+	osContainers "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestContainers(t *testing.T) {
+	c, err := createClient(t, false)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Containers Info available to the currently issued token:")
+	count := 0
+	err = raxContainers.List(c, &osContainers.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		containers, err := raxContainers.ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		for i, container := range containers {
+			t.Logf("[%02d]      name=[%s]", i, container.Name)
+			t.Logf("            count=[%d]", container.Count)
+			t.Logf("            bytes=[%d]", container.Bytes)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No containers listed for your current token.")
+	}
+
+	t.Logf("Container Names available to the currently issued token:")
+	count = 0
+	err = raxContainers.List(c, &osContainers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		names, err := raxContainers.ExtractNames(page)
+		th.AssertNoErr(t, err)
+
+		for i, name := range names {
+			t.Logf("[%02d] %s", i, name)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No containers listed for your current token.")
+	}
+
+	createres := raxContainers.Create(c, "gophercloud-test", nil)
+	th.AssertNoErr(t, createres.Err)
+	defer func() {
+		res := raxContainers.Delete(c, "gophercloud-test")
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	updateres := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}})
+	th.AssertNoErr(t, updateres.Err)
+	t.Logf("Headers from Update Account request: %+v\n", updateres.Header)
+	defer func() {
+		res := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": ""}})
+		th.AssertNoErr(t, res.Err)
+		metadata, err := raxContainers.Get(c, "gophercloud-test").ExtractMetadata()
+		th.AssertNoErr(t, err)
+		t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata)
+		th.CheckEquals(t, metadata["White"], "")
+	}()
+
+	getres := raxContainers.Get(c, "gophercloud-test")
+	t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header)
+	metadata, err := getres.ExtractMetadata()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadata from Get Account request (after update): %+v\n", metadata)
+	th.CheckEquals(t, metadata["White"], "mountains")
+}
diff --git a/acceptance/rackspace/objectstorage/v1/objects_test.go b/acceptance/rackspace/objectstorage/v1/objects_test.go
new file mode 100644
index 0000000..462f284
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/objects_test.go
@@ -0,0 +1,112 @@
+// +build acceptance rackspace objectstorage v1
+
+package v1
+
+import (
+	"bytes"
+	"testing"
+
+	osObjects "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+	raxContainers "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/containers"
+	raxObjects "github.com/rackspace/gophercloud/rackspace/objectstorage/v1/objects"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestObjects(t *testing.T) {
+	c, err := createClient(t, false)
+	th.AssertNoErr(t, err)
+
+	res := raxContainers.Create(c, "gophercloud-test", nil)
+	th.AssertNoErr(t, res.Err)
+
+	defer func() {
+		res := raxContainers.Delete(c, "gophercloud-test")
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	content := bytes.NewBufferString("Lewis Carroll")
+	options := &osObjects.CreateOpts{ContentType: "text/plain"}
+	createres := raxObjects.Create(c, "gophercloud-test", "o1", content, options)
+	th.AssertNoErr(t, createres.Err)
+	defer func() {
+		res := raxObjects.Delete(c, "gophercloud-test", "o1", nil)
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	t.Logf("Objects Info available to the currently issued token:")
+	count := 0
+	err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		objects, err := raxObjects.ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		for i, object := range objects {
+			t.Logf("[%02d]      name=[%s]", i, object.Name)
+			t.Logf("            content-type=[%s]", object.ContentType)
+			t.Logf("            bytes=[%d]", object.Bytes)
+			t.Logf("            last-modified=[%s]", object.LastModified)
+			t.Logf("            hash=[%s]", object.Hash)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No objects listed for your current token.")
+	}
+	t.Logf("Container Names available to the currently issued token:")
+	count = 0
+	err = raxObjects.List(c, "gophercloud-test", &osObjects.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page %02d ---", count)
+
+		names, err := raxObjects.ExtractNames(page)
+		th.AssertNoErr(t, err)
+
+		for i, name := range names {
+			t.Logf("[%02d] %s", i, name)
+		}
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No objects listed for your current token.")
+	}
+
+	copyres := raxObjects.Copy(c, "gophercloud-test", "o1", &raxObjects.CopyOpts{Destination: "gophercloud-test/o2"})
+	th.AssertNoErr(t, copyres.Err)
+	defer func() {
+		res := raxObjects.Delete(c, "gophercloud-test", "o2", nil)
+		th.AssertNoErr(t, res.Err)
+	}()
+
+	o1Content, err := raxObjects.Download(c, "gophercloud-test", "o1", nil).ExtractContent()
+	th.AssertNoErr(t, err)
+	o2Content, err := raxObjects.Download(c, "gophercloud-test", "o2", nil).ExtractContent()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, string(o2Content), string(o1Content))
+
+	updateres := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": "mountains"}})
+	th.AssertNoErr(t, updateres.Err)
+	t.Logf("Headers from Update Account request: %+v\n", updateres.Header)
+	defer func() {
+		res := raxObjects.Update(c, "gophercloud-test", "o2", osObjects.UpdateOpts{Metadata: map[string]string{"white": ""}})
+		th.AssertNoErr(t, res.Err)
+		metadata, err := raxObjects.Get(c, "gophercloud-test", "o2", nil).ExtractMetadata()
+		th.AssertNoErr(t, err)
+		t.Logf("Metadata from Get Account request (after update reverted): %+v\n", metadata)
+		th.CheckEquals(t, metadata["White"], "")
+	}()
+
+	getres := raxObjects.Get(c, "gophercloud-test", "o2", nil)
+	th.AssertNoErr(t, getres.Err)
+	t.Logf("Headers from Get Account request (after update): %+v\n", getres.Header)
+	metadata, err := getres.ExtractMetadata()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadata from Get Account request (after update): %+v\n", metadata)
+	th.CheckEquals(t, metadata["White"], "mountains")
+}
diff --git a/acceptance/rackspace/pkg.go b/acceptance/rackspace/pkg.go
new file mode 100644
index 0000000..5d17b32
--- /dev/null
+++ b/acceptance/rackspace/pkg.go
@@ -0,0 +1 @@
+package rackspace
diff --git a/acceptance/tools/pkg.go b/acceptance/tools/pkg.go
new file mode 100644
index 0000000..f7eca12
--- /dev/null
+++ b/acceptance/tools/pkg.go
@@ -0,0 +1 @@
+package tools
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
new file mode 100644
index 0000000..61b1d7a
--- /dev/null
+++ b/acceptance/tools/tools.go
@@ -0,0 +1,82 @@
+// +build acceptance
+
+package tools
+
+import (
+	"crypto/rand"
+	"errors"
+	"os"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// ErrTimeout is returned if WaitFor takes longer than 300 second to happen.
+var ErrTimeout = errors.New("Timed out")
+
+// OnlyRS overrides the default Gophercloud behavior of using OS_-prefixed environment variables
+// if RS_ variables aren't present. Otherwise, they'll stomp over each other here in the acceptance
+// tests, where you need to have both defined.
+func OnlyRS(original gophercloud.AuthOptions) gophercloud.AuthOptions {
+	if os.Getenv("RS_AUTH_URL") == "" {
+		original.IdentityEndpoint = ""
+	}
+	if os.Getenv("RS_USERNAME") == "" {
+		original.Username = ""
+	}
+	if os.Getenv("RS_PASSWORD") == "" {
+		original.Password = ""
+	}
+	if os.Getenv("RS_API_KEY") == "" {
+		original.APIKey = ""
+	}
+	return original
+}
+
+// WaitFor polls a predicate function once per second to wait for a certain state to arrive.
+func WaitFor(predicate func() (bool, error)) error {
+	for i := 0; i < 300; i++ {
+		time.Sleep(1 * time.Second)
+
+		satisfied, err := predicate()
+		if err != nil {
+			return err
+		}
+		if satisfied {
+			return nil
+		}
+	}
+	return ErrTimeout
+}
+
+// MakeNewPassword generates a new string that's guaranteed to be different than the given one.
+func MakeNewPassword(oldPass string) string {
+	randomPassword := RandomString("", 16)
+	for randomPassword == oldPass {
+		randomPassword = RandomString("", 16)
+	}
+	return randomPassword
+}
+
+// RandomString generates a string of given length, but random content.
+// All content will be within the ASCII graphic character set.
+// (Implementation from Even Shaw's contribution on
+// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
+func RandomString(prefix string, n int) string {
+	const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+	var bytes = make([]byte, n)
+	rand.Read(bytes)
+	for i, b := range bytes {
+		bytes[i] = alphanum[b%byte(len(alphanum))]
+	}
+	return prefix + string(bytes)
+}
+
+// Elide returns the first bit of its input string with a suffix of "..." if it's longer than
+// a comfortable 40 characters.
+func Elide(value string) string {
+	if len(value) > 40 {
+		return value[0:37] + "..."
+	}
+	return value
+}
diff --git a/api_fetch.go b/api_fetch.go
deleted file mode 100644
index 196047e..0000000
--- a/api_fetch.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package gophercloud
-
-import(
- "fmt"
- "github.com/mitchellh/mapstructure"
-)
-
-//The default generic openstack api
-var OpenstackApi = map[string]interface{}{
-	"Type": "compute",
-	"UrlChoice": PublicURL,
-}
-
-// Api for use with rackspace
-var RackspaceApi = map[string]interface{}{
-	"Name":      "cloudServersOpenStack",
-	"VersionId": "2",
-	"UrlChoice": PublicURL,
-}
-
-
-//Populates an ApiCriteria struct with the api values
-//from one of the api maps 
-func PopulateApi(variant string) (ApiCriteria, error){
-	var Api ApiCriteria
-	var variantMap map[string]interface{}
-
-	switch variant {
-	case "":
-		variantMap = OpenstackApi
-
-	case "openstack":
-		variantMap = OpenstackApi
-
-	case "rackspace": 
-		variantMap = RackspaceApi
-
-	default:
-		var err = fmt.Errorf(
-			"PopulateApi: Unknown variant %# v; legal values: \"openstack\", \"rackspace\"", variant)
-		return Api, err
-	}
-
-	err := mapstructure.Decode(variantMap,&Api)
-		if err != nil{
-			return Api,err
-		}
-	return Api, err 
-}
diff --git a/auth_options.go b/auth_options.go
new file mode 100644
index 0000000..bc0ef65
--- /dev/null
+++ b/auth_options.go
@@ -0,0 +1,38 @@
+package gophercloud
+
+// AuthOptions allows anyone calling Authenticate to supply the required access
+// credentials. Its fields are the union of those recognized by each identity
+// implementation and provider.
+type AuthOptions struct {
+	// IdentityEndpoint specifies the HTTP endpoint that is required to work with
+	// the Identity API of the appropriate version. Required by the identity
+	// services, but often populated by a provider Client.
+	IdentityEndpoint string
+
+	// Username is required if using Identity V2 API. Consult with your provider's
+	// control panel to discover your account's username. In Identity V3, either
+	// UserID or a combination of Username and DomainID or DomainName.
+	Username, UserID string
+
+	// Exactly one of Password or ApiKey is required for the Identity V2 and V3
+	// APIs. Consult with your provider's control panel to discover your account's
+	// preferred method of authentication.
+	Password, APIKey string
+
+	// At most one of DomainID and DomainName must be provided if using Username
+	// with Identity V3. Otherwise, either are optional.
+	DomainID, DomainName string
+
+	// The TenantID and TenantName fields are optional for the Identity V2 API.
+	// Some providers allow you to specify a TenantName instead of the TenantId.
+	// Some require both.  Your provider's authentication policies will determine
+	// how these fields influence authentication.
+	TenantID, TenantName string
+
+	// AllowReauth should be set to true if you grant permission for Gophercloud to
+	// cache your credentials in memory, and to allow Gophercloud to attempt to
+	// re-authenticate automatically if/when your token expires.  If you set it to
+	// false, it will not cache these settings, but re-authentication will not be
+	// possible.  This setting defaults to false.
+	AllowReauth bool
+}
diff --git a/auth_results.go b/auth_results.go
new file mode 100644
index 0000000..1a1faa5
--- /dev/null
+++ b/auth_results.go
@@ -0,0 +1,15 @@
+package gophercloud
+
+import "time"
+
+// 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 TokenID.
+type AuthResults interface {
+	// TokenID returns the token's ID value from the authentication response.
+	TokenID() (string, error)
+
+	// ExpiresAt retrieves the token's expiration time.
+	ExpiresAt() (time.Time, error)
+}
diff --git a/authenticate.go b/authenticate.go
deleted file mode 100644
index ff609aa..0000000
--- a/authenticate.go
+++ /dev/null
@@ -1,257 +0,0 @@
-package gophercloud
-
-import (
-	"fmt"
-	"github.com/racker/perigee"
-)
-
-// AuthOptions lets anyone calling Authenticate() supply the required access credentials.
-// At present, only Identity V2 API support exists; therefore, only Username, Password,
-// and optionally, TenantId are provided.  If future Identity API versions become available,
-// alternative fields unique to those versions may appear here.
-type AuthOptions struct {
-	// Username and Password are required if using Identity V2 API.
-	// Consult with your provider's control panel to discover your
-	// account's username and password.
-	Username, Password string
-
-	// ApiKey used for providers that support Api Key authentication
-	ApiKey string
-
-	// The TenantId field is optional for the Identity V2 API.
-	TenantId string
-
-	// The TenantName can be specified instead of the TenantId
-	TenantName string
-
-	// AllowReauth should be set to true if you grant permission for Gophercloud to cache
-	// your credentials in memory, and to allow Gophercloud to attempt to re-authenticate
-	// automatically if/when your token expires.  If you set it to false, it will not cache
-	// these settings, but re-authentication will not be possible.  This setting defaults
-	// to false.
-	AllowReauth bool
-}
-
-// AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity
-// service.  You will not work with this structure directly.
-type AuthContainer struct {
-	Auth Auth `json:"auth"`
-}
-
-// Auth provides a JSON encoding wrapper for passing credentials to the Identity
-// service.  You will not work with this structure directly.
-type Auth struct {
-	PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty"`
-	ApiKeyCredentials   *ApiKeyCredentials   `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
-	TenantId            string               `json:"tenantId,omitempty"`
-	TenantName          string               `json:"tenantName,omitempty"`
-}
-
-// PasswordCredentials provides a JSON encoding wrapper for passing credentials to the Identity
-// service.  You will not work with this structure directly.
-type PasswordCredentials struct {
-	Username string `json:"username"`
-	Password string `json:"password"`
-}
-
-type ApiKeyCredentials struct {
-	Username string `json:"username"`
-	ApiKey   string `json:"apiKey"`
-}
-
-// Access encapsulates the API token and its relevant fields, as well as the
-// services catalog that Identity API returns once authenticated.
-type Access struct {
-	Token          Token
-	ServiceCatalog []CatalogEntry
-	User           User
-	provider       Provider    `json:"-"`
-	options        AuthOptions `json:"-"`
-	context        *Context    `json:"-"`
-}
-
-// Token encapsulates an authentication token and when it expires.  It also includes
-// tenant information if available.
-type Token struct {
-	Id, Expires string
-	Tenant      Tenant
-}
-
-// Tenant encapsulates tenant authentication information.  If, after authentication,
-// no tenant information is supplied, both Id and Name will be "".
-type Tenant struct {
-	Id, Name string
-}
-
-// User encapsulates the user credentials, and provides visibility in what
-// the user can do through its role assignments.
-type User struct {
-	Id, Name          string
-	XRaxDefaultRegion string `json:"RAX-AUTH:defaultRegion"`
-	Roles             []Role
-}
-
-// Role encapsulates a permission that a user can rely on.
-type Role struct {
-	Description, Id, Name string
-}
-
-// CatalogEntry encapsulates a service catalog record.
-type CatalogEntry struct {
-	Name, Type string
-	Endpoints  []EntryEndpoint
-}
-
-// EntryEndpoint encapsulates how to get to the API of some service.
-type EntryEndpoint struct {
-	Region, TenantId                    string
-	PublicURL, InternalURL              string
-	VersionId, VersionInfo, VersionList string
-}
-
-type AuthError struct {
-	StatusCode int
-}
-
-func (ae *AuthError) Error() string {
-	switch ae.StatusCode {
-	case 401:
-		return "Auth failed. Bad credentials."
-
-	default:
-		return fmt.Sprintf("Auth failed. Status code is: %s.", ae.StatusCode)
-	}
-}
-
-//
-func getAuthCredentials(options AuthOptions) Auth {
-	if options.ApiKey == "" {
-		return Auth{
-			PasswordCredentials: &PasswordCredentials{
-				Username: options.Username,
-				Password: options.Password,
-			},
-			TenantId:   options.TenantId,
-			TenantName: options.TenantName,
-		}
-	} else {
-		return Auth{
-			ApiKeyCredentials: &ApiKeyCredentials{
-				Username: options.Username,
-				ApiKey:   options.ApiKey,
-			},
-			TenantId:   options.TenantId,
-			TenantName: options.TenantName,
-		}
-	}
-}
-
-// papersPlease contains the common logic between authentication and re-authentication.
-// The name, obviously a joke on the process of authentication, was chosen because
-// of how many other entities exist in the program containing the word Auth or Authorization.
-// I didn't need another one.
-func (c *Context) papersPlease(p Provider, options AuthOptions) (*Access, error) {
-	var access *Access
-	access = new(Access)
-
-	if (options.Username == "") || (options.Password == "" && options.ApiKey == "") {
-		return nil, ErrCredentials
-	}
-
-	resp, err := perigee.Request("POST", p.AuthEndpoint, perigee.Options{
-		CustomClient: c.httpClient,
-		ReqBody: &AuthContainer{
-			Auth: getAuthCredentials(options),
-		},
-		Results: &struct {
-			Access **Access `json:"access"`
-		}{
-			&access,
-		},
-	})
-
-	if err == nil {
-		switch resp.StatusCode {
-		case 200:
-			access.options = options
-			access.provider = p
-			access.context = c
-
-		default:
-			err = &AuthError {
-				StatusCode: resp.StatusCode,
-			}
-		}
-	}
-
-	return access, err
-}
-
-// Authenticate() grants access to the OpenStack-compatible provider API.
-//
-// Providers are identified through a unique key string.
-// See the RegisterProvider() method for more details.
-//
-// The supplied AuthOptions instance allows the client to specify only those credentials
-// relevant for the authentication request.  At present, support exists for OpenStack
-// Identity V2 API only; support for V3 will become available as soon as documentation for it
-// becomes readily available.
-//
-// For Identity V2 API requirements, you must provide at least the Username and Password
-// options.  The TenantId field is optional, and defaults to "".
-func (c *Context) Authenticate(provider string, options AuthOptions) (*Access, error) {
-	p, err := c.ProviderByName(provider)
-	if err != nil {
-		return nil, err
-	}
-	return c.papersPlease(p, options)
-}
-
-// Reauthenticate attempts to reauthenticate using the configured access credentials, if
-// allowed.  This method takes no action unless your AuthOptions has the AllowReauth flag
-// set to true.
-func (a *Access) Reauthenticate() error {
-	var other *Access
-	var err error
-
-	if a.options.AllowReauth {
-		other, err = a.context.papersPlease(a.provider, a.options)
-		if err == nil {
-			*a = *other
-		}
-	}
-	return err
-}
-
-// See AccessProvider interface definition for details.
-func (a *Access) FirstEndpointUrlByCriteria(ac ApiCriteria) string {
-	ep := FindFirstEndpointByCriteria(a.ServiceCatalog, ac)
-	urls := []string{ep.PublicURL, ep.InternalURL}
-	return urls[ac.UrlChoice]
-}
-
-// See AccessProvider interface definition for details.
-func (a *Access) AuthToken() string {
-	return a.Token.Id
-}
-
-// See AccessProvider interface definition for details.
-func (a *Access) Revoke(tok string) error {
-	url := a.provider.AuthEndpoint + "/" + tok
-	err := perigee.Delete(url, perigee.Options{
-		MoreHeaders: map[string]string{
-			"X-Auth-Token": a.AuthToken(),
-		},
-		OkCodes: []int{204},
-	})
-	return err
-}
-
-// See ServiceCatalogerForIdentityV2 interface definition for details.
-// Note that the raw slice is returend; be careful not to alter the fields of any members,
-// for other components of Gophercloud may depend upon them.
-// If this becomes a problem in the future,
-// a future revision may return a deep-copy of the service catalog instead.
-func (a *Access) V2ServiceCatalog() []CatalogEntry {
-	return a.ServiceCatalog
-}
diff --git a/authenticate_test.go b/authenticate_test.go
deleted file mode 100644
index b05c780..0000000
--- a/authenticate_test.go
+++ /dev/null
@@ -1,264 +0,0 @@
-package gophercloud
-
-import (
-	"net/http"
-	"testing"
-)
-
-const SUCCESSFUL_RESPONSE = `{
-	"access": {
-		"serviceCatalog": [{
-			"endpoints": [{
-				"publicURL": "https://ord.servers.api.rackspacecloud.com/v2/12345",
-				"region": "ORD",
-				"tenantId": "12345",
-				"versionId": "2",
-				"versionInfo": "https://ord.servers.api.rackspacecloud.com/v2",
-				"versionList": "https://ord.servers.api.rackspacecloud.com/"
-			},{
-				"publicURL": "https://dfw.servers.api.rackspacecloud.com/v2/12345",
-				"region": "DFW",
-				"tenantId": "12345",
-				"versionId": "2",
-				"versionInfo": "https://dfw.servers.api.rackspacecloud.com/v2",
-				"versionList": "https://dfw.servers.api.rackspacecloud.com/"
-			}],
-			"name": "cloudServersOpenStack",
-			"type": "compute"
-		},{
-			"endpoints": [{
-				"publicURL": "https://ord.databases.api.rackspacecloud.com/v1.0/12345",
-				"region": "ORD",
-				"tenantId": "12345"
-			}],
-			"name": "cloudDatabases",
-			"type": "rax:database"
-		}],
-		"token": {
-			"expires": "2012-04-13T13:15:00.000-05:00",
-			"id": "aaaaa-bbbbb-ccccc-dddd"
-		},
-		"user": {
-			"RAX-AUTH:defaultRegion": "DFW",
-			"id": "161418",
-			"name": "demoauthor",
-			"roles": [{
-				"description": "User Admin Role.",
-				"id": "3",
-				"name": "identity:user-admin"
-			}]
-		}
-	}
-}
-`
-
-func TestAuthProvider(t *testing.T) {
-	tt := newTransport().WithResponse(SUCCESSFUL_RESPONSE)
-	c := TestContext().UseCustomClient(&http.Client{
-		Transport: tt,
-	})
-
-	_, err := c.Authenticate("", AuthOptions{})
-	if err == nil {
-		t.Error("Expected error for empty provider string")
-		return
-	}
-	_, err = c.Authenticate("unknown-provider", AuthOptions{Username: "u", Password: "p"})
-	if err == nil {
-		t.Error("Expected error for unknown service provider")
-		return
-	}
-
-	err = c.RegisterProvider("provider", Provider{AuthEndpoint: "/"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	_, err = c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if tt.called != 1 {
-		t.Error("Expected transport to be called once.")
-		return
-	}
-}
-
-func TestTenantIdEncoding(t *testing.T) {
-	tt := newTransport().WithResponse(SUCCESSFUL_RESPONSE)
-	c := TestContext().
-		UseCustomClient(&http.Client{
-		Transport: tt,
-	}).
-		WithProvider("provider", Provider{AuthEndpoint: "/"})
-
-	tt.IgnoreTenantId()
-	_, err := c.Authenticate("provider", AuthOptions{
-		Username: "u",
-		Password: "p",
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if tt.tenantIdFound {
-		t.Error("Tenant ID should not have been encoded")
-		return
-	}
-
-	tt.ExpectTenantId()
-	_, err = c.Authenticate("provider", AuthOptions{
-		Username: "u",
-		Password: "p",
-		TenantId: "t",
-	})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	if !tt.tenantIdFound {
-		t.Error("Tenant ID should have been encoded")
-		return
-	}
-}
-
-func TestUserNameAndPassword(t *testing.T) {
-	c := TestContext().
-		WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}).
-		UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)})
-
-	credentials := []AuthOptions{
-		{},
-		{Username: "u"},
-		{Password: "p"},
-	}
-	for i, auth := range credentials {
-		_, err := c.Authenticate("provider", auth)
-		if err == nil {
-			t.Error("Expected error from missing credentials (%d)", i)
-			return
-		}
-	}
-
-	_, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-}
-
-func TestUserNameAndApiKey(t *testing.T) {
-	c := TestContext().
-		WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"}).
-		UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)})
-
-	credentials := []AuthOptions{
-		{},
-		{Username: "u"},
-		{ApiKey: "a"},
-	}
-	for i, auth := range credentials {
-		_, err := c.Authenticate("provider", auth)
-		if err == nil {
-			t.Error("Expected error from missing credentials (%d)", i)
-			return
-		}
-	}
-
-	_, err := c.Authenticate("provider", AuthOptions{Username: "u", ApiKey: "a"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-}
-
-func TestTokenAcquisition(t *testing.T) {
-	c := TestContext().
-		UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}).
-		WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
-
-	acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	tok := acc.Token
-	if (tok.Id == "") || (tok.Expires == "") {
-		t.Error("Expected a valid token for successful login; got %s, %s", tok.Id, tok.Expires)
-		return
-	}
-}
-
-func TestServiceCatalogAcquisition(t *testing.T) {
-	c := TestContext().
-		UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}).
-		WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
-
-	acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	svcs := acc.ServiceCatalog
-	if len(svcs) < 2 {
-		t.Error("Expected 2 service catalog entries; got %d", len(svcs))
-		return
-	}
-
-	types := map[string]bool{
-		"compute":      true,
-		"rax:database": true,
-	}
-	for _, entry := range svcs {
-		if !types[entry.Type] {
-			t.Error("Expected to find type %s.", entry.Type)
-			return
-		}
-	}
-}
-
-func TestUserAcquisition(t *testing.T) {
-	c := TestContext().
-		UseCustomClient(&http.Client{Transport: newTransport().WithResponse(SUCCESSFUL_RESPONSE)}).
-		WithProvider("provider", Provider{AuthEndpoint: "http://localhost/"})
-
-	acc, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	u := acc.User
-	if u.Id != "161418" {
-		t.Error("Expected user ID of 16148; got", u.Id)
-		return
-	}
-}
-
-func TestAuthenticationNeverReauths(t *testing.T) {
-	tt := newTransport().WithError(401)
-	c := TestContext().
-		UseCustomClient(&http.Client{Transport: tt}).
-		WithProvider("provider", Provider{AuthEndpoint: "http://localhost"})
-
-	_, err := c.Authenticate("provider", AuthOptions{Username: "u", Password: "p"})
-	if err == nil {
-		t.Error("Expected an error from a 401 Unauthorized response")
-		return
-	}
-
-	rc, _ := ActualResponseCode(err)
-	if rc != 401 {
-		t.Error("Expected a 401 error code")
-		return
-	}
-
-	err = tt.VerifyCalls(t, 1)
-	if err != nil {
-		// Test object already flagged.
-		return
-	}
-}
diff --git a/common_types.go b/common_types.go
deleted file mode 100644
index 044b308..0000000
--- a/common_types.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package gophercloud
-
-// Link is used for JSON (un)marshalling.
-// It provides RESTful links to a resource.
-type Link struct {
-	Href string `json:"href"`
-	Rel  string `json:"rel"`
-	Type string `json:"type"`
-}
-
-// FileConfig structures represent a blob of data which must appear at a
-// a specific location in a server's filesystem.  The file contents are
-// base-64 encoded.
-type FileConfig struct {
-	Path     string `json:"path"`
-	Contents string `json:"contents"`
-}
-
-// NetworkConfig structures represent an affinity between a server and a
-// specific, uniquely identified network.  Networks are identified through
-// universally unique IDs.
-type NetworkConfig struct {
-	Uuid string `json:"uuid"`
-}
diff --git a/context.go b/context.go
deleted file mode 100644
index e753c8b..0000000
--- a/context.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package gophercloud
-
-import (
-	"net/http"
-	"strings"
-	"fmt"
-	"github.com/tonnerre/golang-pretty"
-)
-
-// Provider structures exist for each tangible provider of OpenStack service.
-// For example, Rackspace, Hewlett-Packard, and NASA might have their own instance of this structure.
-//
-// At a minimum, a provider must expose an authentication endpoint.
-type Provider struct {
-	AuthEndpoint string
-}
-
-// ReauthHandlerFunc functions are responsible for somehow performing the task of
-// reauthentication.
-type ReauthHandlerFunc func(AccessProvider) error
-
-// Context structures encapsulate Gophercloud-global state in a manner which
-// facilitates easier unit testing.  As a user of this SDK, you'll never
-// have to use this structure, except when contributing new code to the SDK.
-type Context struct {
-	// providerMap serves as a directory of supported providers.
-	providerMap map[string]Provider
-
-	// httpClient refers to the current HTTP client interface to use.
-	httpClient *http.Client
-
-	// reauthHandler provides the functionality needed to re-authenticate
-	// if that feature is enabled.  Note: in order to allow for automatic
-	// re-authentication, the Context object will need to remember your
-	// username, password, and tenant ID as provided in the initial call
-	// to Authenticate().  If you do not desire this, you'll need to handle
-	// reauthentication yourself through other means.  Two methods exist:
-	// the first approach is to just handle errors yourself at the application
-	// layer, and the other is through a custom reauthentication handler
-	// set through the WithReauthHandler() method.
-	reauthHandler ReauthHandlerFunc
-}
-
-// TestContext yields a new Context instance, pre-initialized with a barren
-// state suitable for per-unit-test customization.  This configuration consists
-// of:
-//
-// * An empty provider map.
-//
-// * An HTTP client built by the net/http package (see http://godoc.org/net/http#Client).
-func TestContext() *Context {
-	return &Context{
-		providerMap: make(map[string]Provider),
-		httpClient:  &http.Client{},
-		reauthHandler: func(acc AccessProvider) error {
-			return acc.Reauthenticate()
-		},
-	}
-}
-
-// UseCustomClient configures the context to use a customized HTTP client
-// instance.  By default, TestContext() will return a Context which uses
-// the net/http package's default client instance.
-func (c *Context) UseCustomClient(hc *http.Client) *Context {
-	c.httpClient = hc
-	return c
-}
-
-// RegisterProvider allows a unit test to register a mythical provider convenient for testing.
-// If the provider structure lacks adequate configuration, or the configuration given has some
-// detectable error, an ErrConfiguration error will result.
-func (c *Context) RegisterProvider(name string, p Provider) error {
-	if p.AuthEndpoint == "" {
-		return ErrConfiguration
-	}
-
-	c.providerMap[name] = p
-	return nil
-}
-
-// WithProvider offers convenience for unit tests.
-func (c *Context) WithProvider(name string, p Provider) *Context {
-	err := c.RegisterProvider(name, p)
-	if err != nil {
-		panic(err)
-	}
-	return c
-}
-
-// ProviderByName will locate a provider amongst those previously registered, if it exists.
-// If the named provider has not been registered, an ErrProvider error will result.
-//
-// You may also specify a custom Identity API URL.
-// Any provider name that contains the characters "://", in that order, will be treated as a custom Identity API URL.
-// Custom URLs, important for private cloud deployments, overrides all provider configurations.
-func (c *Context) ProviderByName(name string) (p Provider, err error) {
-	for provider, descriptor := range c.providerMap {
-		if name == provider {
-			return descriptor, nil
-		}
-	}
-	if strings.Contains(name, "://") {
-		p = Provider{
-			AuthEndpoint: name,
-		}
-		return p, nil
-	}
-	return Provider{}, ErrProvider
-}
-
-func getServiceCatalogFromAccessProvider(provider AccessProvider) ([]CatalogEntry) {
-	access, found := provider.(*Access)
-	if found {
-		return access.ServiceCatalog
-	} else {
-		return nil
-	}
-}
-
-// Instantiates a Cloud Servers API for the provider given.
-func (c *Context) ServersApi(provider AccessProvider, criteria ApiCriteria) (CloudServersProvider, error) {
-	url := provider.FirstEndpointUrlByCriteria(criteria)
-	if url == "" {
-		var err = fmt.Errorf(
-			"Missing endpoint, or insufficient privileges to access endpoint; criteria = %# v; serviceCatalog = %# v",
-			pretty.Formatter(criteria),
-			pretty.Formatter(getServiceCatalogFromAccessProvider(provider)))
-		return nil, err
-	}
-
-	gcp := &genericServersProvider{
-		endpoint: url,
-		context:  c,
-		access:   provider,
-	}
-
-	return gcp, nil
-}
-
-// WithReauthHandler configures the context to handle reauthentication attempts using the supplied
-// funtion.  By default, reauthentication happens by invoking Authenticate(), which is unlikely to be
-// useful in a unit test.
-//
-// Do not confuse this function with WithReauth()!  Although they work together to support reauthentication,
-// WithReauth() actually contains the decision-making logic to determine when to perform a reauth,
-// while WithReauthHandler() is used to configure what a reauth actually entails.
-func (c *Context) WithReauthHandler(f ReauthHandlerFunc) *Context {
-	c.reauthHandler = f
-	return c
-}
diff --git a/context_test.go b/context_test.go
deleted file mode 100644
index 2936526..0000000
--- a/context_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package gophercloud
-
-import (
-	"testing"
-)
-
-func TestProviderRegistry(t *testing.T) {
-	c := TestContext()
-
-	_, err := c.ProviderByName("aProvider")
-	if err == nil {
-		t.Error("Expected error when looking for a provider by non-existant name")
-		return
-	}
-
-	err = c.RegisterProvider("aProvider", Provider{})
-	if err != ErrConfiguration {
-		t.Error("Unexpected error/nil when registering a provider w/out an auth endpoint\n  %s", err)
-		return
-	}
-
-	_ = c.RegisterProvider("aProvider", Provider{AuthEndpoint: "http://localhost/auth"})
-	_, err = c.ProviderByName("aProvider")
-	if err != nil {
-		t.Error(err)
-		return
-	}
-}
diff --git a/endpoint_search.go b/endpoint_search.go
new file mode 100644
index 0000000..b6f6b48
--- /dev/null
+++ b/endpoint_search.go
@@ -0,0 +1,65 @@
+package gophercloud
+
+import "errors"
+
+var (
+	// ErrServiceNotFound is returned when no service matches the EndpointOpts.
+	ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
+
+	// ErrEndpointNotFound is returned when no available endpoints match the provided EndpointOpts.
+	ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
+)
+
+// Availability indicates whether a specific service endpoint is accessible.
+// Identity v2 lists these as different kinds of URLs ("adminURL",
+// "internalURL", and "publicURL"), while v3 lists them as "Interfaces".
+type Availability string
+
+const (
+	// AvailabilityAdmin makes an endpoint only available to administrators.
+	AvailabilityAdmin Availability = "admin"
+
+	// AvailabilityPublic makes an endpoint available to everyone.
+	AvailabilityPublic Availability = "public"
+
+	// AvailabilityInternal makes an endpoint only available within the cluster.
+	AvailabilityInternal Availability = "internal"
+)
+
+// 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").
+	// Required.
+	Type string
+
+	// Name is the service name for the client (e.g., "nova") as it appears in
+	// the service catalog. Services can have the same Type but a different Name,
+	// which is why both Type and Name are sometimes needed. Optional.
+	Name string
+
+	// Region is the geographic region in which the service resides. Required only
+	// for services that span multiple regions.
+	Region string
+
+	// Availability is the visibility of the endpoint to be returned. Valid types
+	// are: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin.
+	// Availability is not required, and defaults to AvailabilityPublic.
+	// Not all providers or services offer all Availability options.
+	Availability Availability
+}
+
+// EndpointLocator is a function that describes how to locate a single endpoint
+// from a service catalog for a specific ProviderClient. It should be set
+// during ProviderClient authentication and used to discover related ServiceClients.
+type EndpointLocator func(EndpointOpts) (string, error)
+
+// ApplyDefaults sets EndpointOpts fields if not already set. Currently,
+// EndpointOpts.Availability defaults to the public endpoint.
+func (eo *EndpointOpts) ApplyDefaults(t string) {
+	if eo.Type == "" {
+		eo.Type = t
+	}
+	if eo.Availability == "" {
+		eo.Availability = AvailabilityPublic
+	}
+}
diff --git a/endpoint_search_test.go b/endpoint_search_test.go
new file mode 100644
index 0000000..3457453
--- /dev/null
+++ b/endpoint_search_test.go
@@ -0,0 +1,19 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestApplyDefaultsToEndpointOpts(t *testing.T) {
+	eo := EndpointOpts{Availability: AvailabilityPublic}
+	eo.ApplyDefaults("compute")
+	expected := EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+	th.CheckDeepEquals(t, expected, eo)
+
+	eo = EndpointOpts{Type: "compute"}
+	eo.ApplyDefaults("object-store")
+	expected = EndpointOpts{Availability: AvailabilityPublic, Type: "compute"}
+	th.CheckDeepEquals(t, expected, eo)
+}
diff --git a/errors.go b/errors.go
deleted file mode 100644
index 726ba7e..0000000
--- a/errors.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package gophercloud
-
-import (
-	"fmt"
-)
-
-// ErrNotImplemented should be used only while developing new SDK features.
-// No established function or method will ever produce this error.
-var ErrNotImplemented = fmt.Errorf("Not implemented")
-
-// ErrProvider errors occur when attempting to reference an unsupported
-// provider.  More often than not, this error happens due to a typo in
-// the name.
-var ErrProvider = fmt.Errorf("Missing or incorrect provider")
-
-// ErrCredentials errors happen when attempting to authenticate using a
-// set of credentials not recognized by the Authenticate() method.
-// For example, not providing a username or password when attempting to
-// authenticate against an Identity V2 API.
-var ErrCredentials = fmt.Errorf("Missing or incomplete credentials")
-
-// ErrConfiguration errors happen when attempting to add a new provider, and
-// the provider added lacks a correct or consistent configuration.
-// For example, all providers must expose at least an Identity V2 API
-// for authentication; if this endpoint isn't specified, you may receive
-// this error when attempting to register it against a context.
-var ErrConfiguration = fmt.Errorf("Missing or incomplete configuration")
-
-// ErrError errors happen when you attempt to discover the response code
-// responsible for a previous request bombing with an error, but pass in an
-// error interface which doesn't belong to the web client.
-var ErrError = fmt.Errorf("Attempt to solicit actual HTTP response code from error entity which doesn't know")
-
-// WarnUnauthoritative warnings happen when a service believes its response
-// to be correct, but is not in a position of knowing for sure at the moment.
-// For example, the service could be responding with cached data that has
-// exceeded its time-to-live setting, but which has not yet received an official
-// update from an authoritative source.
-var WarnUnauthoritative = fmt.Errorf("Unauthoritative data")
diff --git a/flavors.go b/flavors.go
deleted file mode 100644
index eb864d5..0000000
--- a/flavors.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package gophercloud
-
-import (
-	"github.com/racker/perigee"
-)
-
-// See CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ListFlavors() ([]Flavor, error) {
-	var fs []Flavor
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/flavors/detail"
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			Results:      &struct{ Flavors *[]Flavor }{&fs},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return fs, err
-}
-
-// FlavorLink provides a reference to a flavor by either ID or by direct URL.
-// Some services use just the ID, others use just the URL.
-// This structure provides a common means of expressing both in a single field.
-type FlavorLink struct {
-	Id    string `json:"id"`
-	Links []Link `json:"links"`
-}
-
-// Flavor records represent (virtual) hardware configurations for server resources in a region.
-//
-// The Id field contains the flavor's unique identifier.
-// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
-//
-// The Disk and Ram fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
-//
-// The Name field provides a human-readable moniker for the flavor.
-//
-// Swap indicates how much space is reserved for swap.
-// If not provided, this field will be set to 0.
-//
-// VCpus indicates how many (virtual) CPUs are available for this flavor.
-type Flavor struct {
-	OsFlvDisabled bool    `json:"OS-FLV-DISABLED:disabled"`
-	Disk          int     `json:"disk"`
-	Id            string  `json:"id"`
-	Links         []Link  `json:"links"`
-	Name          string  `json:"name"`
-	Ram           int     `json:"ram"`
-	RxTxFactor    float64 `json:"rxtx_factor"`
-	Swap          int     `json:"swap"`
-	VCpus         int     `json:"vcpus"`
-}
diff --git a/floating_ips.go b/floating_ips.go
deleted file mode 100644
index 1163667..0000000
--- a/floating_ips.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package gophercloud
-
-import (
-	"errors"
-	"fmt"
-	"github.com/racker/perigee"
-)
-
-func (gsp *genericServersProvider) ListFloatingIps() ([]FloatingIp, error) {
-	var fips []FloatingIp
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/os-floating-ips"
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			Results: &struct {
-				FloatingIps *[]FloatingIp `json:"floating_ips"`
-			}{&fips},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return fips, err
-}
-
-func (gsp *genericServersProvider) CreateFloatingIp(pool string) (FloatingIp, error) {
-	fip := new(FloatingIp)
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/os-floating-ips"
-		return perigee.Post(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			ReqBody: map[string]string{
-				"pool": pool,
-			},
-			Results: &struct {
-				FloatingIp **FloatingIp `json:"floating_ip"`
-			}{&fip},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-
-	if fip.Ip == "" {
-		return *fip, errors.New("Error creating floating IP")
-	}
-
-	return *fip, err
-}
-
-func (gsp *genericServersProvider) AssociateFloatingIp(serverId string, ip FloatingIp) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, serverId)
-		return perigee.Post(ep, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			ReqBody: map[string](map[string]string){
-				"addFloatingIp": map[string]string{"address": ip.Ip},
-			},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-func (gsp *genericServersProvider) DeleteFloatingIp(ip FloatingIp) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-floating-ips/%d", gsp.endpoint, ip.Id)
-		return perigee.Delete(ep, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-type FloatingIp struct {
-	Id         int    `json:"id"`
-	Pool       string `json:"pool"`
-	Ip         string `json:"ip"`
-	FixedIp    string `json:"fixed_ip"`
-	InstanceId string `json:"instance_id"`
-}
diff --git a/global_context.go b/global_context.go
deleted file mode 100644
index 89d283b..0000000
--- a/global_context.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package gophercloud
-
-import (
-	"github.com/racker/perigee"
-)
-
-// globalContext is the, well, "global context."
-// Most of this SDK is written in a manner to facilitate easier testing,
-// which doesn't require all the configuration a real-world application would require.
-// However, for real-world deployments, applications should be able to rely on a consistent configuration of providers, etc.
-var globalContext *Context
-
-// providers is the set of supported providers.
-var providers = map[string]Provider{
-	"rackspace-us": {
-		AuthEndpoint: "https://identity.api.rackspacecloud.com/v2.0/tokens",
-	},
-	"rackspace-uk": {
-		AuthEndpoint: "https://lon.identity.api.rackspacecloud.com/v2.0/tokens",
-	},
-}
-
-// Initialize the global context to sane configuration.
-// The Go runtime ensures this function is called before main(),
-// thus guaranteeing proper configuration before your application ever runs.
-func init() {
-	globalContext = TestContext()
-	for name, descriptor := range providers {
-		globalContext.RegisterProvider(name, descriptor)
-	}
-}
-
-// Authenticate() grants access to the OpenStack-compatible provider API.
-//
-// Providers are identified through a unique key string.
-// Specifying an unsupported provider will result in an ErrProvider error.
-// However, you may also specify a custom Identity API URL.
-// Any provider name that contains the characters "://", in that order, will be treated as a custom Identity API URL.
-// Custom URLs, important for private cloud deployments, overrides all provider configurations.
-//
-// The supplied AuthOptions instance allows the client to specify only those credentials
-// relevant for the authentication request.  At present, support exists for OpenStack
-// Identity V2 API only; support for V3 will become available as soon as documentation for it
-// becomes readily available.
-//
-// For Identity V2 API requirements, you must provide at least the Username and Password
-// options.  The TenantId field is optional, and defaults to "".
-func Authenticate(provider string, options AuthOptions) (*Access, error) {
-	return globalContext.Authenticate(provider, options)
-}
-
-// Instantiates a Cloud Servers object for the provider given.
-func ServersApi(acc AccessProvider, criteria ApiCriteria) (CloudServersProvider, error) {
-	return globalContext.ServersApi(acc, criteria)
-}
-
-// ActualResponseCode inspects a returned error, and discovers the actual response actual
-// response code that caused the error to be raised.
-func ActualResponseCode(e error) (int, error) {
-	if err, typeOk := e.(*perigee.UnexpectedResponseCodeError); typeOk {
-		return err.Actual, nil
-	} else if err, typeOk := e.(*AuthError); typeOk{
-		return err.StatusCode, nil
-	}
-
-	return 0, ErrError
-}
diff --git a/images.go b/images.go
deleted file mode 100644
index a23e0bb..0000000
--- a/images.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package gophercloud
-
-import (
-	"github.com/racker/perigee"
-)
-
-// See the CloudImagesProvider interface for details.
-func (gsp *genericServersProvider) ListImages() ([]Image, error) {
-	var is []Image
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/images/detail"
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			Results:      &struct{ Images *[]Image }{&is},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return is, err
-}
-
-func (gsp *genericServersProvider) ImageById(id string) (*Image, error) {
-	var is *Image
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/images/" + id
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			Results:      &struct{ Image **Image }{&is},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return is, err
-}
-
-func (gsp *genericServersProvider) DeleteImageById(id string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/images/" + id
-		_, err := perigee.Request("DELETE", url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-		return err
-	})
-	return err
-}
-
-// ImageLink provides a reference to a image by either ID or by direct URL.
-// Some services use just the ID, others use just the URL.
-// This structure provides a common means of expressing both in a single field.
-type ImageLink struct {
-	Id    string `json:"id"`
-	Links []Link `json:"links"`
-}
-
-// Image is used for JSON (un)marshalling.
-// It provides a description of an OS image.
-//
-// The Id field contains the image's unique identifier.
-// For example, this identifier will be useful for specifying which operating system to install on a new server instance.
-//
-// The MinDisk and MinRam fields specify the minimum resources a server must provide to be able to install the image.
-//
-// The Name field provides a human-readable moniker for the OS image.
-//
-// The Progress and Status fields indicate image-creation status.
-// Any usable image will have 100% progress.
-//
-// The Updated field indicates the last time this image was changed.
-//
-// OsDcfDiskConfig indicates the server's boot volume configuration.
-// Valid values are:
-//     AUTO
-//     ----
-//     The server is built with a single partition the size of the target flavor disk.
-//     The file system is automatically adjusted to fit the entire partition.
-//     This keeps things simple and automated.
-//     AUTO is valid only for images and servers with a single partition that use the EXT3 file system.
-//     This is the default setting for applicable Rackspace base images.
-//
-//     MANUAL
-//     ------
-//     The server is built using whatever partition scheme and file system is in the source image.
-//     If the target flavor disk is larger,
-//     the remaining disk space is left unpartitioned.
-//     This enables images to have non-EXT3 file systems, multiple partitions, and so on,
-//     and enables you to manage the disk configuration.
-//
-type Image struct {
-	Created         string `json:"created"`
-	Id              string `json:"id"`
-	Links           []Link `json:"links"`
-	MinDisk         int    `json:"minDisk"`
-	MinRam          int    `json:"minRam"`
-	Name            string `json:"name"`
-	Progress        int    `json:"progress"`
-	Status          string `json:"status"`
-	Updated         string `json:"updated"`
-	OsDcfDiskConfig string `json:"OS-DCF:diskConfig"`
-}
diff --git a/interfaces.go b/interfaces.go
deleted file mode 100644
index d73f11d..0000000
--- a/interfaces.go
+++ /dev/null
@@ -1,272 +0,0 @@
-package gophercloud
-
-import "net/url"
-
-// AccessProvider instances encapsulate a Keystone authentication interface.
-type AccessProvider interface {
-	// FirstEndpointUrlByCriteria searches through the service catalog for the first
-	// matching entry endpoint fulfilling the provided criteria.  If nothing found,
-	// return "".  Otherwise, return either the public or internal URL for the
-	// endpoint, depending on both its existence and the setting of the ApiCriteria.UrlChoice
-	// field.
-	FirstEndpointUrlByCriteria(ApiCriteria) string
-
-	// AuthToken provides a copy of the current authentication token for the user's credentials.
-	// Note that AuthToken() will not automatically refresh an expired token.
-	AuthToken() string
-
-	// Revoke allows you to terminate any program's access to the OpenStack API by token ID.
-	Revoke(string) error
-
-	// Reauthenticate attempts to acquire a new authentication token, if the feature is enabled by
-	// AuthOptions.AllowReauth.
-	Reauthenticate() error
-}
-
-// ServiceCatalogerIdentityV2 interface provides direct access to the service catalog as offered by the Identity V2 API.
-// We regret we need to fracture the namespace of what should otherwise be a simple concept; however,
-// the OpenStack community saw fit to render V3's service catalog completely incompatible with V2.
-type ServiceCatalogerForIdentityV2 interface {
-	V2ServiceCatalog() []CatalogEntry
-}
-
-// CloudServersProvider instances encapsulate a Cloud Servers API, should one exist in the service catalog
-// for your provider.
-type CloudServersProvider interface {
-	// Servers
-
-	// ListServers provides a complete list of servers hosted by the user
-	// in a given region.  This function differs from ListServersLinksOnly()
-	// in that it returns all available details for each server returned.
-	ListServers() ([]Server, error)
-
-	// ListServersByFilters provides a list of servers hosted by the user in a
-	// given region. This function let you requests servers by certain URI
-	// paramaters defined by the API endpoint.  This is sometimes more suitable
-	// if you have many servers and you only want to pick servers on certain
-	// criterias. An example usage could be :
-	//
-	//	   filter := url.Values{}
-	//	   filter.Set("name", "MyServer")
-	//	   filter.Set("status", "ACTIVE")
-	//
-	//	   filteredServers, err := c.ListServersByFilters(filter)
-	//
-	// Here, filteredServers only contains servers whose name started with
-	// "MyServer" and are in "ACTIVE" status.
-	ListServersByFilter(filter url.Values) ([]Server, error)
-
-	// ListServers provides a complete list of servers hosted by the user
-	// in a given region.  This function differs from ListServers() in that
-	// it returns only IDs and links to each server returned.
-	//
-	// This function should be used only under certain circumstances.
-	// It's most useful for checking to see if a server with a given ID exists,
-	// or that you have permission to work with that server.  It's also useful
-	// when the cost of retrieving the server link list plus the overhead of manually
-	// invoking ServerById() for each of the servers you're interested in is less than
-	// just calling ListServers() to begin with.  This may be a consideration, for
-	// example, with mobile applications.
-	//
-	// In other cases, you probably should just call ListServers() and cache the
-	// results to conserve overall bandwidth and reduce your access rate on the API.
-	ListServersLinksOnly() ([]Server, error)
-
-	// ServerById will retrieve a detailed server description given the unique ID
-	// of a server.  The ID can be returned by either ListServers() or by ListServersLinksOnly().
-	ServerById(id string) (*Server, error)
-
-	// CreateServer requests a new server to be created by the cloud server provider.
-	// The user must pass in a pointer to an initialized NewServerContainer structure.
-	// Please refer to the NewServerContainer documentation for more details.
-	//
-	// If the NewServer structure's AdminPass is empty (""), a password will be
-	// automatically generated by your OpenStack provider, and returned through the
-	// AdminPass field of the result.  Take care, however; this will be the only time
-	// this happens.  No other means exists in the public API to acquire a password
-	// for a pre-existing server.  If you lose it, you'll need to call SetAdminPassword()
-	// to set a new one.
-	CreateServer(ns NewServer) (*NewServer, error)
-
-	// DeleteServerById requests that the server with the assigned ID be removed
-	// from your account.  The delete happens asynchronously.
-	DeleteServerById(id string) error
-
-	// SetAdminPassword requests that the server with the specified ID have its
-	// administrative password changed.  For Linux, BSD, or other POSIX-like
-	// system, this password corresponds to the root user.  For Windows machines,
-	// the Administrator password will be affected instead.
-	SetAdminPassword(id string, pw string) error
-
-	// ResizeServer can be a short-hand for RebuildServer where only the size of the server
-	// changes.  Note that after the resize operation is requested, you will need to confirm
-	// the resize has completed for changes to take effect permanently.  Changes will assume
-	// to be confirmed even without an explicit confirmation after 24 hours from the initial
-	// request.
-	ResizeServer(id, newName, newFlavor, newDiskConfig string) error
-
-	// RevertResize will reject a server's resized configuration, thus
-	// rolling back to the original server.
-	RevertResize(id string) error
-
-	// ConfirmResizeServer will acknowledge a server's resized configuration.
-	ConfirmResize(id string) error
-
-	// RebootServer requests that the server with the specified ID be rebooted.
-	// Two reboot mechanisms exist.
-	//
-	// - Hard.  This will physically power-cycle the unit.
-	// - Soft.  This will attempt to use the server's software-based mechanisms to restart
-	//           the machine.  E.g., "shutdown -r now" on Linux.
-	RebootServer(id string, hard bool) error
-
-	// RescueServer requests that the server with the specified ID be placed into
-	// a state of maintenance.  The server instance is replaced with a new instance,
-	// of the same flavor and image.  This new image will have the boot volume of the
-	// original machine mounted as a secondary device, so that repair and administration
-	// may occur.  Use UnrescueServer() to restore the server to its previous state.
-	// Note also that many providers will impose a time limit for how long a server may
-	// exist in rescue mode!  Consult the API documentation for your provider for
-	// details.
-	RescueServer(id string) (string, error)
-
-	// UnrescueServer requests that a server in rescue state be placed into its nominal
-	// operating state.
-	UnrescueServer(id string) error
-
-	// UpdateServer alters one or more fields of the identified server's Server record.
-	// However, not all fields may be altered.  Presently, only Name, AccessIPv4, and
-	// AccessIPv6 fields may be altered.   If unspecified, or set to an empty or zero
-	// value, the corresponding field remains unaltered.
-	//
-	// This function returns the new set of server details if successful.
-	UpdateServer(id string, newValues NewServerSettings) (*Server, error)
-
-	// RebuildServer reprovisions a server to the specifications given by the
-	// NewServer structure.  The following fields are guaranteed to be recognized:
-	//
-	//		Name (required)				AccessIPv4
-	//		imageRef (required)			AccessIPv6
-	//		AdminPass (required)		Metadata
-	//		Personality
-	//
-	// Other providers may reserve the right to act on additional fields.
-	RebuildServer(id string, ns NewServer) (*Server, error)
-
-	// CreateImage will create a new image from the specified server id returning the id of the new image.
-	CreateImage(id string, ci CreateImage) (string, error)
-
-	// Addresses
-
-	// ListAddresses yields the list of available addresses for the server.
-	// This information is also returned by ServerById() in the Server.Addresses
-	// field.  However, if you have a lot of servers and all you need are addresses,
-	// this function might be more efficient.
-	ListAddresses(id string) (AddressSet, error)
-
-	// ListAddressesByNetwork yields the list of available addresses for a given server id and networkLabel.
-	// Example: ListAddressesByNetwork("234-4353-4jfrj-43j2s", "private")
-	ListAddressesByNetwork(id, networkLabel string) (NetworkAddress, error)
-
-	// ListFloatingIps yields the list of all floating IP addresses allocated to the current project.
-	ListFloatingIps() ([]FloatingIp, error)
-
-	// CreateFloatingIp allocates a new IP from the named pool to the current project.
-	CreateFloatingIp(pool string) (FloatingIp, error)
-
-	// DeleteFloatingIp returns the specified IP from the current project to the pool.
-	DeleteFloatingIp(ip FloatingIp) error
-
-	// AssociateFloatingIp associates the given floating IP to the given server id.
-	AssociateFloatingIp(serverId string, ip FloatingIp) error
-
-	// Images
-
-	// ListImages yields the list of available operating system images.  This function
-	// returns full details for each image, if available.
-	ListImages() ([]Image, error)
-
-	// ImageById yields details about a specific image.
-	ImageById(id string) (*Image, error)
-
-	// DeleteImageById will delete the specific image.
-	DeleteImageById(id string) error
-
-	// Flavors
-
-	// ListFlavors yields the list of available system flavors.  This function
-	// returns full details for each flavor, if available.
-	ListFlavors() ([]Flavor, error)
-
-	// KeyPairs
-
-	// ListKeyPairs yields the list of available keypairs.
-	ListKeyPairs() ([]KeyPair, error)
-
-	// CreateKeyPairs will create or generate a new keypair.
-	CreateKeyPair(nkp NewKeyPair) (KeyPair, error)
-
-	// DeleteKeyPair wil delete a keypair.
-	DeleteKeyPair(name string) error
-
-	// ShowKeyPair will yield the named keypair.
-	ShowKeyPair(name string) (KeyPair, error)
-
-	// ListSecurityGroups provides a listing of security groups for the tenant.
-	// This method works only if the provider supports the os-security-groups extension.
-	ListSecurityGroups() ([]SecurityGroup, error)
-
-	// CreateSecurityGroup lets a tenant create a new security group.
-	// Only the SecurityGroup fields which are specified will be marshalled to the API.
-	// This method works only if the provider supports the os-security-groups extension.
-	CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error)
-
-	// ListSecurityGroupsByServerId provides a list of security groups which apply to the indicated server.
-	// This method works only if the provider supports the os-security-groups extension.
-	ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error)
-
-	// SecurityGroupById returns a security group corresponding to the provided ID number.
-	// This method works only if the provider supports the os-security-groups extension.
-	SecurityGroupById(id int) (*SecurityGroup, error)
-
-	// DeleteSecurityGroupById disposes of a security group corresponding to the provided ID number.
-	// This method works only if the provider supports the os-security-groups extension.
-	DeleteSecurityGroupById(id int) error
-
-	// ListDefaultSGRules lists default security group rules.
-	// This method only works if the provider supports the os-security-groups-default-rules extension.
-	ListDefaultSGRules() ([]SGRule, error)
-
-	// CreateDefaultSGRule creates a default security group rule.
-	// This method only works if the provider supports the os-security-groups-default-rules extension.
-	CreateDefaultSGRule(SGRule) (*SGRule, error)
-
-	// GetSGRule obtains information for a specified security group rule.
-	// This method only works if the provider supports the os-security-groups-default-rules extension.
-	GetSGRule(string) (*SGRule, error)
-
-	// Pause stores the state of the server in RAM.  A paused instance continues to run in a frozen state.
-	Pause(id string) error
-
-	// Unpause restores normal behavior of a previously paused server.
-	Unpause(id string) error
-
-	// Suspend stores the state of the server on disk or other non-volatile medium.
-	// Administrative users might suspend an infrequently used instance or suspend an instance to perform system maintenance.
-	// Suspending an instance is similar to placing a device in hibernation;
-	// memory and vCPUs become available to create other instances.
-	Suspend(id string) error
-
-	// Resume restores normal behavior of a previously suspended server.
-	Resume(id string) error
-
-	// Start restores a previously stopped server to normal operation.
-	// WARNING: This function depends on a Nova action which remains undocumented on the OpenStack API Reference website,
-	// and may be deprecated in favor of Pause/Unpause, or Suspend/Resume.
-	Start(id string) error
-
-	// Stop stops a server.
-	// WARNING: This function depends on a Nova action which remains undocumented on the OpenStack API Reference website,
-	// and may be deprecated in favor of Pause/Unpause, or Suspend/Resume.
-	Stop(id string) error
-}
diff --git a/keypairs.go b/keypairs.go
deleted file mode 100644
index 8ae8cd3..0000000
--- a/keypairs.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package gophercloud
-
-import (
-	"github.com/racker/perigee"
-)
-
-// See the CloudImagesProvider interface for details.
-func (gsp *genericServersProvider) ListKeyPairs() ([]KeyPair, error) {
-	type KeyPairs struct {
-		KeyPairs []struct {
-			KeyPair KeyPair `json:"keypair"`
-		} `json:"keypairs"`
-	}
-
-	var kp KeyPairs
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/os-keypairs"
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			Results:      &kp,
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-
-	// Flatten out the list of keypairs
-	var keypairs []KeyPair
-	for _, k := range kp.KeyPairs {
-		keypairs = append(keypairs, k.KeyPair)
-	}
-	return keypairs, err
-}
-
-func (gsp *genericServersProvider) CreateKeyPair(nkp NewKeyPair) (KeyPair, error) {
-	var kp KeyPair
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/os-keypairs"
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				KeyPair *NewKeyPair `json:"keypair"`
-			}{&nkp},
-			CustomClient: gsp.context.httpClient,
-			Results:      &struct{ KeyPair *KeyPair }{&kp},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{200},
-		})
-	})
-	return kp, err
-}
-
-// See the CloudImagesProvider interface for details.
-func (gsp *genericServersProvider) DeleteKeyPair(name string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/os-keypairs/" + name
-		return perigee.Delete(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-	return err
-}
-
-func (gsp *genericServersProvider) ShowKeyPair(name string) (KeyPair, error) {
-	var kp KeyPair
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/os-keypairs/" + name
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gsp.context.httpClient,
-			Results:      &struct{ KeyPair *KeyPair }{&kp},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return kp, err
-}
-
-type KeyPair struct {
-	FingerPrint string `json:"fingerprint"`
-	Name        string `json:"name"`
-	PrivateKey  string `json:"private_key,omitempty"`
-	PublicKey   string `json:"public_key"`
-	UserID      string `json:"user_id,omitempty"`
-}
-
-type NewKeyPair struct {
-	Name      string `json:"name"`
-	PublicKey string `json:"public_key,omitempty"`
-}
diff --git a/openstack/auth_env.go b/openstack/auth_env.go
new file mode 100644
index 0000000..a4402b6
--- /dev/null
+++ b/openstack/auth_env.go
@@ -0,0 +1,58 @@
+package openstack
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/rackspace/gophercloud"
+)
+
+var nilOptions = gophercloud.AuthOptions{}
+
+// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD
+// environment variables, respectively, remain undefined.  See the AuthOptions() function for more details.
+var (
+	ErrNoAuthURL  = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
+	ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
+	ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.")
+)
+
+// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack
+// OS_* environment variables.  The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
+// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME.  Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
+// have settings, or an error will result.  OS_TENANT_ID and OS_TENANT_NAME are optional.
+func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
+	authURL := os.Getenv("OS_AUTH_URL")
+	username := os.Getenv("OS_USERNAME")
+	userID := os.Getenv("OS_USERID")
+	password := os.Getenv("OS_PASSWORD")
+	tenantID := os.Getenv("OS_TENANT_ID")
+	tenantName := os.Getenv("OS_TENANT_NAME")
+	domainID := os.Getenv("OS_DOMAIN_ID")
+	domainName := os.Getenv("OS_DOMAIN_NAME")
+
+	if authURL == "" {
+		return nilOptions, ErrNoAuthURL
+	}
+
+	if username == "" && userID == "" {
+		return nilOptions, ErrNoUsername
+	}
+
+	if password == "" {
+		return nilOptions, ErrNoPassword
+	}
+
+	ao := gophercloud.AuthOptions{
+		IdentityEndpoint: authURL,
+		UserID:           userID,
+		Username:         username,
+		Password:         password,
+		TenantID:         tenantID,
+		TenantName:       tenantName,
+		DomainID:         domainID,
+		DomainName:       domainName,
+	}
+
+	return ao, nil
+}
diff --git a/openstack/blockstorage/v1/apiversions/doc.go b/openstack/blockstorage/v1/apiversions/doc.go
new file mode 100644
index 0000000..e3af39f
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/doc.go
@@ -0,0 +1,3 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Block Storage service, code-named Cinder.
+package apiversions
diff --git a/openstack/blockstorage/v1/apiversions/requests.go b/openstack/blockstorage/v1/apiversions/requests.go
new file mode 100644
index 0000000..016bf37
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,28 @@
+package apiversions
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// List lists all the Cinder API versions available to end-users.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page {
+		return APIVersionPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, v string) GetResult {
+	var res GetResult
+	_, err := perigee.Request("GET", getURL(client, v), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		Results:     &res.Body,
+	})
+	res.Err = err
+	return res
+}
diff --git a/openstack/blockstorage/v1/apiversions/requests_test.go b/openstack/blockstorage/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..56b5e4f
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests_test.go
@@ -0,0 +1,145 @@
+package apiversions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `{
+			"versions": [
+				{
+					"status": "CURRENT",
+					"updated": "2012-01-04T11:33:21Z",
+					"id": "v1.0",
+					"links": [
+						{
+							"href": "http://23.253.228.211:8776/v1/",
+							"rel": "self"
+						}
+					]
+			    },
+				{
+					"status": "CURRENT",
+					"updated": "2012-11-21T11:33:21Z",
+					"id": "v2.0",
+					"links": [
+						{
+							"href": "http://23.253.228.211:8776/v2/",
+							"rel": "self"
+						}
+					]
+				}
+			]
+		}`)
+	})
+
+	count := 0
+
+	List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractAPIVersions(page)
+		if err != nil {
+			t.Errorf("Failed to extract API versions: %v", err)
+			return false, err
+		}
+
+		expected := []APIVersion{
+			APIVersion{
+				ID:      "v1.0",
+				Status:  "CURRENT",
+				Updated: "2012-01-04T11:33:21Z",
+			},
+			APIVersion{
+				ID:      "v2.0",
+				Status:  "CURRENT",
+				Updated: "2012-11-21T11:33:21Z",
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestAPIInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `{
+			"version": {
+				"status": "CURRENT",
+				"updated": "2012-01-04T11:33:21Z",
+				"media-types": [
+					{
+						"base": "application/xml",
+						"type": "application/vnd.openstack.volume+xml;version=1"
+					},
+					{
+						"base": "application/json",
+						"type": "application/vnd.openstack.volume+json;version=1"
+					}
+				],
+				"id": "v1.0",
+				"links": [
+					{
+						"href": "http://23.253.228.211:8776/v1/",
+						"rel": "self"
+					},
+					{
+						"href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf",
+						"type": "application/pdf",
+						"rel": "describedby"
+					},
+					{
+						"href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl",
+						"type": "application/vnd.sun.wadl+xml",
+						"rel": "describedby"
+					}
+				]
+			}
+		}`)
+	})
+
+	actual, err := Get(client.ServiceClient(), "v1").Extract()
+	if err != nil {
+		t.Errorf("Failed to extract version: %v", err)
+	}
+
+	expected := APIVersion{
+		ID:      "v1.0",
+		Status:  "CURRENT",
+		Updated: "2012-01-04T11:33:21Z",
+	}
+
+	th.AssertEquals(t, actual.ID, expected.ID)
+	th.AssertEquals(t, actual.Status, expected.Status)
+	th.AssertEquals(t, actual.Updated, expected.Updated)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..7b0df11
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -0,0 +1,58 @@
+package apiversions
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// APIVersion represents an API version for Cinder.
+type APIVersion struct {
+	ID      string `json:"id" mapstructure:"id"`           // unique identifier
+	Status  string `json:"status" mapstructure:"status"`   // current status
+	Updated string `json:"updated" mapstructure:"updated"` // date last updated
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+	is, err := ExtractAPIVersions(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+	var resp struct {
+		Versions []APIVersion `mapstructure:"versions"`
+	}
+
+	err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+
+	return resp.Versions, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts an API version resource.
+func (r GetResult) Extract() (*APIVersion, error) {
+	var resp struct {
+		Version *APIVersion `mapstructure:"version"`
+	}
+
+	err := mapstructure.Decode(r.Body, &resp)
+
+	return resp.Version, err
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..56f8260
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+	return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("")
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls_test.go b/openstack/blockstorage/v1/apiversions/urls_test.go
new file mode 100644
index 0000000..37e9142
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "v1")
+	expected := endpoint + "v1/"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/doc.go b/openstack/blockstorage/v1/snapshots/doc.go
new file mode 100644
index 0000000..198f830
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/doc.go
@@ -0,0 +1,5 @@
+// Package snapshots provides information and interaction with snapshots in the
+// OpenStack Block Storage service. A snapshot is a point in time copy of the
+// data contained in an external storage volume, and can be controlled
+// programmatically.
+package snapshots
diff --git a/openstack/blockstorage/v1/snapshots/fixtures.go b/openstack/blockstorage/v1/snapshots/fixtures.go
new file mode 100644
index 0000000..d1461fb
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/fixtures.go
@@ -0,0 +1,114 @@
+package snapshots
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+    {
+      "snapshots": [
+        {
+          "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+          "display_name": "snapshot-001"
+        },
+        {
+          "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+          "display_name": "snapshot-002"
+        }
+      ]
+    }
+    `)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "snapshot": {
+        "display_name": "snapshot-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+      `)
+	})
+}
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "snapshot": {
+        "volume_id": "1234",
+        "display_name": "snapshot-001"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "snapshot": {
+        "volume_id": "1234",
+        "display_name": "snapshot-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+    `)
+	})
+}
+
+func MockUpdateMetadataResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots/123/metadata", 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")
+		th.TestJSONRequest(t, r, `
+    {
+      "metadata": {
+        "key": "v1"
+      }
+    }
+    `)
+
+		fmt.Fprintf(w, `
+      {
+        "metadata": {
+          "key": "v1"
+        }
+      }
+    `)
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..8cb130d
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,187 @@
+package snapshots
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToSnapshotCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains options for creating a Snapshot. This object is passed to
+// the snapshots.Create function. For more information about these parameters,
+// see the Snapshot object.
+type CreateOpts struct {
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Force bool
+	// OPTIONAL
+	Metadata map[string]interface{}
+	// OPTIONAL
+	Name string
+	// REQUIRED
+	VolumeID string
+}
+
+// ToSnapshotCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.VolumeID == "" {
+		return nil, fmt.Errorf("Required CreateOpts field 'VolumeID' not set.")
+	}
+	s["volume_id"] = opts.VolumeID
+
+	if opts.Description != "" {
+		s["display_description"] = opts.Description
+	}
+	if opts.Force == true {
+		s["force"] = opts.Force
+	}
+	if opts.Metadata != nil {
+		s["metadata"] = opts.Metadata
+	}
+	if opts.Name != "" {
+		s["display_name"] = opts.Name
+	}
+
+	return map[string]interface{}{"snapshot": s}, nil
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To
+// extract the Snapshot object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToSnapshotCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+	})
+	return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202, 204},
+	})
+	return err
+}
+
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot
+// object from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToSnapshotListQuery() (string, error)
+}
+
+// ListOpts hold options for listing Snapshots. It is passed to the
+// snapshots.List function.
+type ListOpts struct {
+	Name     string `q:"display_name"`
+	Status   string `q:"status"`
+	VolumeID string `q:"volume_id"`
+}
+
+// ToSnapshotListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSnapshotListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns Snapshots optionally limited by the conditions provided in
+// ListOpts.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToSnapshotListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return ListResult{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to
+// the Update request.
+type UpdateMetadataOptsBuilder interface {
+	ToSnapshotUpdateMetadataMap() (map[string]interface{}, error)
+}
+
+// UpdateMetadataOpts contain options for updating an existing Snapshot. This
+// object is passed to the snapshots.Update function. For more information
+// about the parameters, see the Snapshot object.
+type UpdateMetadataOpts struct {
+	Metadata map[string]interface{}
+}
+
+// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of
+// an UpdateMetadataOpts.
+func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Metadata != nil {
+		v["metadata"] = opts.Metadata
+	}
+
+	return v, nil
+}
+
+// UpdateMetadata will update the Snapshot with provided information. To
+// extract the updated Snapshot from the response, call the ExtractMetadata
+// method on the UpdateMetadataResult.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
+	var res UpdateMetadataResult
+
+	reqBody, err := opts.ToSnapshotUpdateMetadataMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+	})
+	return res
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..8db55d9
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -0,0 +1,104 @@
+package snapshots
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSnapshots(page)
+		if err != nil {
+			t.Errorf("Failed to extract snapshots: %v", err)
+			return false, err
+		}
+
+		expected := []Snapshot{
+			Snapshot{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "snapshot-001",
+			},
+			Snapshot{
+				ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name: "snapshot-002",
+			},
+		}
+
+		th.CheckDeepEquals(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()
+
+	MockGetResponse(t)
+
+	v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "snapshot-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
+	n, err := Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.VolumeID, "1234")
+	th.AssertEquals(t, n.Name, "snapshot-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestUpdateMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUpdateMetadataResponse(t)
+
+	expected := map[string]interface{}{"key": "v1"}
+
+	options := &UpdateMetadataOpts{
+		Metadata: map[string]interface{}{
+			"key": "v1",
+		},
+	}
+
+	actual, err := UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, actual, expected)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..d414a7d
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,118 @@
+package snapshots
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Snapshot contains all the information associated with an OpenStack Snapshot.
+type Snapshot struct {
+	// Currect status of the Snapshot.
+	Status string `mapstructure:"status"`
+
+	// Display name.
+	Name string `mapstructure:"display_name"`
+
+	// Instances onto which the Snapshot is attached.
+	Attachments []string `mapstructure:"attachments"`
+
+	// Logical group.
+	AvailabilityZone string `mapstructure:"availability_zone"`
+
+	// Is the Snapshot bootable?
+	Bootable string `mapstructure:"bootable"`
+
+	// Date created.
+	CreatedAt string `mapstructure:"created_at"`
+
+	// Display description.
+	Description string `mapstructure:"display_discription"`
+
+	// See VolumeType object for more information.
+	VolumeType string `mapstructure:"volume_type"`
+
+	// ID of the Snapshot from which this Snapshot was created.
+	SnapshotID string `mapstructure:"snapshot_id"`
+
+	// ID of the Volume from which this Snapshot was created.
+	VolumeID string `mapstructure:"volume_id"`
+
+	// User-defined key-value pairs.
+	Metadata map[string]string `mapstructure:"metadata"`
+
+	// Unique identifier.
+	ID string `mapstructure:"id"`
+
+	// Size of the Snapshot, in GB.
+	Size int `mapstructure:"size"`
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Snapshots.
+func (r ListResult) IsEmpty() (bool, error) {
+	volumes, err := ExtractSnapshots(r)
+	if err != nil {
+		return true, err
+	}
+	return len(volumes) == 0, nil
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
+	var response struct {
+		Snapshots []Snapshot `json:"snapshots"`
+	}
+
+	err := mapstructure.Decode(page.(ListResult).Body, &response)
+	return response.Snapshots, err
+}
+
+// UpdateMetadataResult contains the response body and error from an UpdateMetadata request.
+type UpdateMetadataResult struct {
+	commonResult
+}
+
+// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
+func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	m := r.Body.(map[string]interface{})["metadata"]
+	return m.(map[string]interface{}), nil
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the Snapshot object out of the commonResult object.
+func (r commonResult) Extract() (*Snapshot, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Snapshot *Snapshot `json:"snapshot"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Snapshot, err
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls.go b/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..4d635e8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls.go
@@ -0,0 +1,27 @@
+package snapshots
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("snapshots")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("snapshots", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return deleteURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("snapshots", id, "metadata")
+}
+
+func updateMetadataURL(c *gophercloud.ServiceClient, id string) string {
+	return metadataURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls_test.go b/openstack/blockstorage/v1/snapshots/urls_test.go
new file mode 100644
index 0000000..feacf7f
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls_test.go
@@ -0,0 +1,50 @@
+package snapshots
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "snapshots"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "snapshots"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+	actual := metadataURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo/metadata"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateMetadataURL(t *testing.T) {
+	actual := updateMetadataURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo/metadata"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/util.go b/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..64cdc60
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util.go
@@ -0,0 +1,22 @@
+package snapshots
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+// WaitForStatus will continually poll the resource, checking for a particular
+// status. It will do this for the amount of seconds defined.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/openstack/blockstorage/v1/snapshots/util_test.go b/openstack/blockstorage/v1/snapshots/util_test.go
new file mode 100644
index 0000000..a4c4c82
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util_test.go
@@ -0,0 +1,38 @@
+package snapshots
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestWaitForStatus(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(2 * time.Second)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+		{
+			"snapshot": {
+				"display_name": "snapshot-001",
+				"id": "1234",
+				"status":"available"
+			}
+		}`)
+	})
+
+	err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
+	if err == nil {
+		t.Errorf("Expected error: 'Time Out in WaitFor'")
+	}
+
+	err = WaitForStatus(client.ServiceClient(), "1234", "available", 3)
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/doc.go b/openstack/blockstorage/v1/volumes/doc.go
new file mode 100644
index 0000000..307b8b1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/doc.go
@@ -0,0 +1,5 @@
+// Package volumes provides information and interaction with volumes in the
+// OpenStack Block Storage service. A volume is a detachable block storage
+// device, akin to a USB hard drive. It can only be attached to one instance at
+// a time.
+package volumes
diff --git a/openstack/blockstorage/v1/volumes/fixtures.go b/openstack/blockstorage/v1/volumes/fixtures.go
new file mode 100644
index 0000000..a01ad05
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/fixtures.go
@@ -0,0 +1,105 @@
+package volumes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+  {
+    "volumes": [
+      {
+        "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+        "display_name": "vol-001"
+      },
+      {
+        "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+        "display_name": "vol-002"
+      }
+    ]
+  }
+  `)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "volume": {
+        "display_name": "vol-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+      `)
+	})
+}
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "volume": {
+        "size": 75
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "volume": {
+        "size": 4,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+    `)
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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)
+	})
+}
+
+func MockUpdateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+    {
+      "volume": {
+        "display_name": "vol-002",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+        }
+    }
+    `)
+	})
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..fa2202c
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,216 @@
+package volumes
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToVolumeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains options for creating a Volume. This object is passed to
+// the volumes.Create function. For more information about these parameters,
+// see the Volume object.
+type CreateOpts struct {
+	// OPTIONAL
+	Availability string
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Metadata map[string]string
+	// OPTIONAL
+	Name string
+	// REQUIRED
+	Size int
+	// OPTIONAL
+	SnapshotID, SourceVolID, ImageID string
+	// OPTIONAL
+	VolumeType string
+}
+
+// ToVolumeCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Size == 0 {
+		return nil, fmt.Errorf("Required CreateOpts field 'Size' not set.")
+	}
+	v["size"] = opts.Size
+
+	if opts.Availability != "" {
+		v["availability_zone"] = opts.Availability
+	}
+	if opts.Description != "" {
+		v["display_description"] = opts.Description
+	}
+	if opts.ImageID != "" {
+		v["imageRef"] = opts.ImageID
+	}
+	if opts.Metadata != nil {
+		v["metadata"] = opts.Metadata
+	}
+	if opts.Name != "" {
+		v["display_name"] = opts.Name
+	}
+	if opts.SourceVolID != "" {
+		v["source_volid"] = opts.SourceVolID
+	}
+	if opts.SnapshotID != "" {
+		v["snapshot_id"] = opts.SnapshotID
+	}
+	if opts.VolumeType != "" {
+		v["volume_type"] = opts.VolumeType
+	}
+
+	return map[string]interface{}{"volume": v}, nil
+}
+
+// Create will create a new Volume based on the values in CreateOpts. To extract
+// the Volume object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToVolumeCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201},
+	})
+	return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202, 204},
+	})
+	return err
+}
+
+// Get retrieves the Volume with the provided ID. To extract the Volume object
+// from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToVolumeListQuery() (string, error)
+}
+
+// ListOpts holds options for listing Volumes. It is passed to the volumes.List
+// function.
+type ListOpts struct {
+	// admin-only option. Set it to true to see all tenant volumes.
+	AllTenants bool `q:"all_tenants"`
+	// List only volumes that contain Metadata.
+	Metadata map[string]string `q:"metadata"`
+	// List only volumes that have Name as the display name.
+	Name string `q:"name"`
+	// List only volumes that have a status of Status.
+	Status string `q:"status"`
+}
+
+// ToVolumeListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToVolumeListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns Volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToVolumeListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return ListResult{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToVolumeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+	// OPTIONAL
+	Name string
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Metadata map[string]string
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Description != "" {
+		v["display_description"] = opts.Description
+	}
+	if opts.Metadata != nil {
+		v["metadata"] = opts.Metadata
+	}
+	if opts.Name != "" {
+		v["display_name"] = opts.Name
+	}
+
+	return map[string]interface{}{"volume": v}, nil
+}
+
+// Update will update the Volume with provided information. To extract the updated
+// Volume from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToVolumeUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+	})
+	return res
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..11b950e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,95 @@
+package volumes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volumes: %v", err)
+			return false, err
+		}
+
+		expected := []Volume{
+			Volume{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-001",
+			},
+			Volume{
+				ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name: "vol-002",
+			},
+		}
+
+		th.CheckDeepEquals(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()
+
+	MockGetResponse(t)
+
+	v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "vol-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := &CreateOpts{Size: 75}
+	n, err := Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Size, 4)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUpdateResponse(t)
+
+	options := UpdateOpts{Name: "vol-002"}
+	v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "vol-002", v.Name)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..ca322d1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,108 @@
+package volumes
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+	// Current status of the volume.
+	Status string `mapstructure:"status"`
+
+	// Human-readable display name for the volume.
+	Name string `mapstructure:"display_name"`
+
+	// Instances onto which the volume is attached.
+	Attachments []string `mapstructure:"attachments"`
+
+	// This parameter is no longer used.
+	AvailabilityZone string `mapstructure:"availability_zone"`
+
+	// Indicates whether this is a bootable volume.
+	Bootable string `mapstructure:"bootable"`
+
+	// The date when this volume was created.
+	CreatedAt string `mapstructure:"created_at"`
+
+	// Human-readable description for the volume.
+	Description string `mapstructure:"display_discription"`
+
+	// The type of volume to create, either SATA or SSD.
+	VolumeType string `mapstructure:"volume_type"`
+
+	// The ID of the snapshot from which the volume was created
+	SnapshotID string `mapstructure:"snapshot_id"`
+
+	// The ID of another block storage volume from which the current volume was created
+	SourceVolID string `mapstructure:"source_volid"`
+
+	// Arbitrary key-value pairs defined by the user.
+	Metadata map[string]string `mapstructure:"metadata"`
+
+	// Unique identifier for the volume.
+	ID string `mapstructure:"id"`
+
+	// Size of the volume in GB.
+	Size int `mapstructure:"size"`
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// ListResult is a pagination.pager that is returned from a call to the List function.
+type ListResult struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volumes.
+func (r ListResult) IsEmpty() (bool, error) {
+	volumes, err := ExtractVolumes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(volumes) == 0, nil
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(page pagination.Page) ([]Volume, error) {
+	var response struct {
+		Volumes []Volume `json:"volumes"`
+	}
+
+	err := mapstructure.Decode(page.(ListResult).Body, &response)
+	return response.Volumes, err
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+	commonResult
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (*Volume, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Volume *Volume `json:"volume"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Volume, err
+}
diff --git a/openstack/blockstorage/v1/volumes/urls.go b/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..29629a1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("volumes")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("volumes", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return deleteURL(c, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return deleteURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumes/urls_test.go b/openstack/blockstorage/v1/volumes/urls_test.go
new file mode 100644
index 0000000..a95270e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls_test.go
@@ -0,0 +1,44 @@
+package volumes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "volumes"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "volumes"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "volumes/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "volumes/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "volumes/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/volumes/util.go b/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..1dda695
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util.go
@@ -0,0 +1,22 @@
+package volumes
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+// WaitForStatus will continually poll the resource, checking for a particular
+// status. It will do this for the amount of seconds defined.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/openstack/blockstorage/v1/volumes/util_test.go b/openstack/blockstorage/v1/volumes/util_test.go
new file mode 100644
index 0000000..24ef3b6
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util_test.go
@@ -0,0 +1,38 @@
+package volumes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestWaitForStatus(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(3 * time.Second)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+		{
+			"volume": {
+				"display_name": "vol-001",
+				"id": "1234",
+				"status":"available"
+			}
+		}`)
+	})
+
+	err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
+	if err == nil {
+		t.Errorf("Expected error: 'Time Out in WaitFor'")
+	}
+
+	err = WaitForStatus(client.ServiceClient(), "1234", "available", 6)
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/doc.go b/openstack/blockstorage/v1/volumetypes/doc.go
new file mode 100644
index 0000000..793084f
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/doc.go
@@ -0,0 +1,9 @@
+// Package volumetypes provides information and interaction with volume types
+// in the OpenStack Block Storage service. A volume type indicates the type of
+// a block storage volume, such as SATA, SCSCI, SSD, etc. These can be
+// customized or defined by the OpenStack admin.
+//
+// You can also define extra_specs associated with your volume types. For
+// instance, you could have a VolumeType=SATA, with extra_specs (RPM=10000,
+// RAID-Level=5) . Extra_specs are defined and customized by the admin.
+package volumetypes
diff --git a/openstack/blockstorage/v1/volumetypes/fixtures.go b/openstack/blockstorage/v1/volumetypes/fixtures.go
new file mode 100644
index 0000000..e3326ea
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/fixtures.go
@@ -0,0 +1,60 @@
+package volumetypes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+    {
+      "volume_types": [
+        {
+          "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+          "name": "vol-type-001",
+          "extra_specs": {
+            "capabilities": "gpu"
+            }
+        },
+        {
+          "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+          "name": "vol-type-002",
+          "extra_specs": {}
+        }
+      ]
+    }
+    `)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "volume_type": {
+        "name": "vol-type-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+    "extra_specs": {
+      "serverNumber": "2"
+    }
+    }
+}
+      `)
+	})
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..32d323d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,86 @@
+package volumetypes
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToVolumeTypeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts are options for creating a volume type.
+type CreateOpts struct {
+	// OPTIONAL. See VolumeType.
+	ExtraSpecs map[string]interface{}
+	// OPTIONAL. See VolumeType.
+	Name string
+}
+
+// ToVolumeTypeCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) {
+	vt := make(map[string]interface{})
+
+	if opts.ExtraSpecs != nil {
+		vt["extra_specs"] = opts.ExtraSpecs
+	}
+	if opts.Name != "" {
+		vt["name"] = opts.Name
+	}
+
+	return map[string]interface{}{"volume_type": vt}, nil
+}
+
+// Create will create a new volume. To extract the created volume type object,
+// call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToVolumeTypeCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+	})
+	return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+	return err
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, err := perigee.Request("GET", getURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		Results:     &res.Body,
+	})
+	res.Err = err
+	return res
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return ListResult{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, listURL(client), createPage)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests_test.go b/openstack/blockstorage/v1/volumetypes/requests_test.go
new file mode 100644
index 0000000..8b11786
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -0,0 +1,118 @@
+package volumetypes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumeTypes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volume types: %v", err)
+			return false, err
+		}
+
+		expected := []VolumeType{
+			VolumeType{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-type-001",
+				ExtraSpecs: map[string]interface{}{
+					"capabilities": "gpu",
+				},
+			},
+			VolumeType{
+				ID:         "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name:       "vol-type-002",
+				ExtraSpecs: map[string]interface{}{},
+			},
+		}
+
+		th.CheckDeepEquals(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()
+
+	MockGetResponse(t)
+
+	vt, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+	th.AssertEquals(t, vt.Name, "vol-type-001")
+	th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "volume_type": {
+        "name": "vol-type-001"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "volume_type": {
+        "name": "vol-type-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+		`)
+	})
+
+	options := &CreateOpts{Name: "vol-type-001"}
+	n, err := Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "vol-type-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..a13f7c1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,67 @@
+package volumetypes
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// VolumeType contains all information associated with an OpenStack Volume Type.
+type VolumeType struct {
+	ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata
+	ID         string                 `json:"id" mapstructure:"id"`                   // unique identifier
+	Name       string                 `json:"name" mapstructure:"name"`               // display name
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volume Types.
+func (r ListResult) IsEmpty() (bool, error) {
+	volumeTypes, err := ExtractVolumeTypes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(volumeTypes) == 0, nil
+}
+
+// ExtractVolumeTypes extracts and returns Volume Types.
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+	var response struct {
+		VolumeTypes []VolumeType `mapstructure:"volume_types"`
+	}
+
+	err := mapstructure.Decode(page.(ListResult).Body, &response)
+	return response.VolumeTypes, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the Volume Type object out of the commonResult object.
+func (r commonResult) Extract() (*VolumeType, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.VolumeType, err
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls.go b/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..cf8367b
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls.go
@@ -0,0 +1,19 @@
+package volumetypes
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("types")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("types", id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls_test.go b/openstack/blockstorage/v1/volumetypes/urls_test.go
new file mode 100644
index 0000000..44016e2
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls_test.go
@@ -0,0 +1,38 @@
+package volumetypes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "types"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "types"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "types/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "types/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/client.go b/openstack/client.go
new file mode 100644
index 0000000..99b3d46
--- /dev/null
+++ b/openstack/client.go
@@ -0,0 +1,205 @@
+package openstack
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
+	"github.com/rackspace/gophercloud/openstack/utils"
+)
+
+const (
+	v20 = "v2.0"
+	v30 = "v3.0"
+)
+
+// NewClient prepares an unauthenticated ProviderClient instance.
+// Most users will probably prefer using the AuthenticatedClient function instead.
+// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
+// for example.
+func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+	u, err := url.Parse(endpoint)
+	if err != nil {
+		return nil, err
+	}
+	hadPath := u.Path != ""
+	u.Path, u.RawQuery, u.Fragment = "", "", ""
+	base := u.String()
+
+	endpoint = gophercloud.NormalizeURL(endpoint)
+	base = gophercloud.NormalizeURL(base)
+
+	if hadPath {
+		return &gophercloud.ProviderClient{
+			IdentityBase:     base,
+			IdentityEndpoint: endpoint,
+		}, nil
+	}
+
+	return &gophercloud.ProviderClient{
+		IdentityBase:     base,
+		IdentityEndpoint: "",
+	}, nil
+}
+
+// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
+// returns a Client instance that's ready to operate.
+// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses
+// the most recent identity service available to proceed.
+func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
+	client, err := NewClient(options.IdentityEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	err = Authenticate(client, options)
+	if err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
+func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	versions := []*utils.Version{
+		&utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+		&utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"},
+	}
+
+	chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
+	if err != nil {
+		return err
+	}
+
+	switch chosen.ID {
+	case v20:
+		return v2auth(client, endpoint, options)
+	case v30:
+		return v3auth(client, endpoint, options)
+	default:
+		// The switch statement must be out of date from the versions list.
+		return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
+	}
+}
+
+// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
+func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	return v2auth(client, "", options)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+	v2Client := NewIdentityV2(client)
+	if endpoint != "" {
+		v2Client.Endpoint = endpoint
+	}
+
+	result := tokens2.Create(v2Client, tokens2.AuthOptions{AuthOptions: options})
+
+	token, err := result.ExtractToken()
+	if err != nil {
+		return err
+	}
+
+	catalog, err := result.ExtractServiceCatalog()
+	if err != nil {
+		return err
+	}
+
+	client.TokenID = token.ID
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return V2EndpointURL(catalog, opts)
+	}
+
+	return nil
+}
+
+// AuthenticateV3 explicitly authenticates against the identity v3 service.
+func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	return v3auth(client, "", options)
+}
+
+func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+	// Override the generated service endpoint with the one returned by the version endpoint.
+	v3Client := NewIdentityV3(client)
+	if endpoint != "" {
+		v3Client.Endpoint = endpoint
+	}
+
+	token, err := tokens3.Create(v3Client, options, nil).Extract()
+	if err != nil {
+		return err
+	}
+	client.TokenID = token.ID
+
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return V3EndpointURL(v3Client, opts)
+	}
+
+	return nil
+}
+
+// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
+func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
+	v2Endpoint := client.IdentityBase + "v2.0/"
+
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       v2Endpoint,
+	}
+}
+
+// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
+func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
+	v3Endpoint := client.IdentityBase + "v3/"
+
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       v3Endpoint,
+	}
+}
+
+// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
+func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("object-store")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
+func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("compute")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
+func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("network")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       url,
+		ResourceBase:   url + "v2.0/",
+	}, nil
+}
+
+// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
+func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("volume")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/openstack/client_test.go b/openstack/client_test.go
new file mode 100644
index 0000000..257260c
--- /dev/null
+++ b/openstack/client_test.go
@@ -0,0 +1,161 @@
+package openstack
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClientV3(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	const ID = "0123456789"
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `
+			{
+				"versions": {
+					"values": [
+						{
+							"status": "stable",
+							"id": "v3.0",
+							"links": [
+								{ "href": "%s", "rel": "self" }
+							]
+						},
+						{
+							"status": "stable",
+							"id": "v2.0",
+							"links": [
+								{ "href": "%s", "rel": "self" }
+							]
+						}
+					]
+				}
+			}
+		`, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
+	})
+
+	th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("X-Subject-Token", ID)
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `{ "token": { "expires_at": "2013-02-02T18:30:59.000000Z" } }`)
+	})
+
+	options := gophercloud.AuthOptions{
+		UserID:           "me",
+		Password:         "secret",
+		IdentityEndpoint: th.Endpoint(),
+	}
+	client, err := AuthenticatedClient(options)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, ID, client.TokenID)
+}
+
+func TestAuthenticatedClientV2(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `
+			{
+				"versions": {
+					"values": [
+						{
+							"status": "experimental",
+							"id": "v3.0",
+							"links": [
+								{ "href": "%s", "rel": "self" }
+							]
+						},
+						{
+							"status": "stable",
+							"id": "v2.0",
+							"links": [
+								{ "href": "%s", "rel": "self" }
+							]
+						}
+					]
+				}
+			}
+		`, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
+	})
+
+	th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `
+			{
+				"access": {
+					"token": {
+						"id": "01234567890",
+						"expires": "2014-10-01T10:00:00.000000Z"
+					},
+					"serviceCatalog": [
+						{
+							"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/"
+								}
+							]
+						}
+					]
+				}
+			}
+		`)
+	})
+
+	options := gophercloud.AuthOptions{
+		Username:         "me",
+		Password:         "secret",
+		IdentityEndpoint: th.Endpoint(),
+	}
+	client, err := AuthenticatedClient(options)
+	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/common/extensions/errors.go b/openstack/common/extensions/errors.go
new file mode 100755
index 0000000..aeec0fa
--- /dev/null
+++ b/openstack/common/extensions/errors.go
@@ -0,0 +1 @@
+package extensions
diff --git a/openstack/common/extensions/fixtures.go b/openstack/common/extensions/fixtures.go
new file mode 100644
index 0000000..0ed7de9
--- /dev/null
+++ b/openstack/common/extensions/fixtures.go
@@ -0,0 +1,91 @@
+// +build fixtures
+
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Extension results.
+const ListOutput = `
+{
+	"extensions": [
+		{
+			"updated": "2013-01-20T00:00:00-00:00",
+			"name": "Neutron Service Type Management",
+			"links": [],
+			"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+			"alias": "service-type",
+			"description": "API for retrieving service providers for Neutron advanced services"
+		}
+	]
+}`
+
+// GetOutput provides a single Extension result.
+const GetOutput = `
+{
+	"extension": {
+		"updated": "2013-02-03T10:00:00-00:00",
+		"name": "agent",
+		"links": [],
+		"namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
+		"alias": "agent",
+		"description": "The agent management extension."
+	}
+}
+`
+
+// ListedExtension is the Extension that should be parsed from ListOutput.
+var ListedExtension = Extension{
+	Updated:     "2013-01-20T00:00:00-00:00",
+	Name:        "Neutron Service Type Management",
+	Links:       []interface{}{},
+	Namespace:   "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+	Alias:       "service-type",
+	Description: "API for retrieving service providers for Neutron advanced services",
+}
+
+// ExpectedExtensions is a slice containing the Extension that should be parsed from ListOutput.
+var ExpectedExtensions = []Extension{ListedExtension}
+
+// SingleExtension is the Extension that should be parsed from GetOutput.
+var SingleExtension = &Extension{
+	Updated:     "2013-02-03T10:00:00-00:00",
+	Name:        "agent",
+	Links:       []interface{}{},
+	Namespace:   "http://docs.openstack.org/ext/agent/api/v2.0",
+	Alias:       "agent",
+	Description: "The agent management extension.",
+}
+
+// HandleListExtensionsSuccessfully creates an HTTP handler at `/extensions` on the test handler
+// mux that response with a list containing a single tenant.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+
+		fmt.Fprintf(w, ListOutput)
+	})
+}
+
+// HandleGetExtensionSuccessfully creates an HTTP handler at `/extensions/agent` that responds with
+// a JSON payload corresponding to SingleExtension.
+func HandleGetExtensionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, GetOutput)
+	})
+}
diff --git a/openstack/common/extensions/requests.go b/openstack/common/extensions/requests.go
new file mode 100755
index 0000000..3ca6e12
--- /dev/null
+++ b/openstack/common/extensions/requests.go
@@ -0,0 +1,26 @@
+package extensions
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// 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.PageResult) 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..6550283
--- /dev/null
+++ b/openstack/common/extensions/requests_test.go
@@ -0,0 +1,38 @@
+package extensions
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListExtensionsSuccessfully(t)
+
+	count := 0
+
+	List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+		th.AssertDeepEquals(t, ExpectedExtensions, actual)
+
+		return true, nil
+	})
+
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetExtensionSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, SingleExtension, actual)
+}
diff --git a/openstack/common/extensions/results.go b/openstack/common/extensions/results.go
new file mode 100755
index 0000000..777d083
--- /dev/null
+++ b/openstack/common/extensions/results.go
@@ -0,0 +1,65 @@
+package extensions
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily stores the result of a Get call.
+// Use its Extract() method to interpret it as an Extension.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult as an Extension.
+func (r GetResult) Extract() (*Extension, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Extension *Extension `json:"extension"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Extension, err
+}
+
+// Extension is a struct that represents an OpenStack extension.
+type Extension struct {
+	Updated     string        `json:"updated" mapstructure:"updated"`
+	Name        string        `json:"name" mapstructure:"name"`
+	Links       []interface{} `json:"links" mapstructure:"links"`
+	Namespace   string        `json:"namespace" mapstructure:"namespace"`
+	Alias       string        `json:"alias" mapstructure:"alias"`
+	Description string        `json:"description" mapstructure:"description"`
+}
+
+// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
+type ExtensionPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an ExtensionPage struct is empty.
+func (r ExtensionPage) IsEmpty() (bool, error) {
+	is, err := ExtractExtensions(r)
+	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.
+// 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"`
+	}
+
+	err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
+
+	return resp.Extensions, err
+}
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/common/extensions/urls_test.go b/openstack/common/extensions/urls_test.go
new file mode 100755
index 0000000..3223b1c
--- /dev/null
+++ b/openstack/common/extensions/urls_test.go
@@ -0,0 +1,26 @@
+package extensions
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestExtensionURL(t *testing.T) {
+	actual := ExtensionURL(endpointClient(), "agent")
+	expected := endpoint + "extensions/agent"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListExtensionURL(t *testing.T) {
+	actual := ListExtensionURL(endpointClient())
+	expected := endpoint + "extensions"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/requests.go b/openstack/compute/v2/extensions/bootfromvolume/requests.go
new file mode 100644
index 0000000..5a976d1
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests.go
@@ -0,0 +1,111 @@
+package bootfromvolume
+
+import (
+	"errors"
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+	"github.com/racker/perigee"
+)
+
+// SourceType represents the type of medium being used to create the volume.
+type SourceType string
+
+const (
+	Volume   SourceType = "volume"
+	Snapshot SourceType = "snapshot"
+	Image    SourceType = "image"
+)
+
+// BlockDevice is a structure with options for booting a server instance
+// from a volume. The volume may be created from an image, snapshot, or another
+// volume.
+type BlockDevice struct {
+	// BootIndex [optional] is the boot index. It defaults to 0.
+	BootIndex int `json:"boot_index"`
+
+	// DeleteOnTermination [optional] specifies whether or not to delete the attached volume
+	// when the server is deleted. Defaults to `false`.
+	DeleteOnTermination bool `json:"delete_on_termination"`
+
+	// DestinationType [optional] is the type that gets created. Possible values are "volume"
+	// and "local".
+	DestinationType string `json:"destination_type"`
+
+	// SourceType [required] must be one of: "volume", "snapshot", "image".
+	SourceType SourceType `json:"source_type"`
+
+	// UUID [required] is the unique identifier for the volume, snapshot, or image (see above)
+	UUID string `json:"uuid"`
+
+	// VolumeSize [optional] is the size of the volume to create (in gigabytes).
+	VolumeSize int `json:"volume_size"`
+}
+
+// CreateOptsExt is a structure that extends the server `CreateOpts` structure
+// by allowing for a block device mapping.
+type CreateOptsExt struct {
+	servers.CreateOptsBuilder
+	BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"`
+}
+
+// ToServerCreateMap adds the block device mapping option to the base server
+// creation options.
+func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
+	base, err := opts.CreateOptsBuilder.ToServerCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(opts.BlockDevice) == 0 {
+		return nil, errors.New("Required fields UUID and SourceType not set.")
+	}
+
+	serverMap := base["server"].(map[string]interface{})
+
+	blockDevice := make([]map[string]interface{}, len(opts.BlockDevice))
+
+	for i, bd := range opts.BlockDevice {
+		if string(bd.SourceType) == "" {
+			return nil, errors.New("SourceType must be one of: volume, image, snapshot.")
+		}
+
+		blockDevice[i] = make(map[string]interface{})
+
+		blockDevice[i]["source_type"] = bd.SourceType
+		blockDevice[i]["boot_index"] = strconv.Itoa(bd.BootIndex)
+		blockDevice[i]["delete_on_termination"] = strconv.FormatBool(bd.DeleteOnTermination)
+		blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize)
+		if bd.UUID != "" {
+			blockDevice[i]["uuid"] = bd.UUID
+		}
+		if bd.DestinationType != "" {
+			blockDevice[i]["destination_type"] = bd.DestinationType
+		}
+
+	}
+	serverMap["block_device_mapping_v2"] = blockDevice
+
+	return base, nil
+}
+
+// Create requests the creation of a server from the given block device mapping.
+func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) servers.CreateResult {
+	var res servers.CreateResult
+
+	reqBody, err := opts.ToServerCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 202},
+	})
+	return res
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/requests_test.go b/openstack/compute/v2/extensions/bootfromvolume/requests_test.go
new file mode 100644
index 0000000..5bf9137
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests_test.go
@@ -0,0 +1,51 @@
+package bootfromvolume
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []BlockDevice{
+			BlockDevice{
+				UUID:            "123456",
+				SourceType:      Image,
+				DestinationType: "volume",
+				VolumeSize:      10,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name": "createdserver",
+        "imageRef": "asdfasdfasdf",
+        "flavorRef": "performance1-1",
+        "block_device_mapping_v2":[
+          {
+            "uuid":"123456",
+            "source_type":"image",
+            "destination_type":"volume",
+            "boot_index": "0",
+            "delete_on_termination": "false",
+            "volume_size": "10"
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/results.go b/openstack/compute/v2/extensions/bootfromvolume/results.go
new file mode 100644
index 0000000..f60329f
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/results.go
@@ -0,0 +1,10 @@
+package bootfromvolume
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// CreateResult temporarily contains the response from a Create call.
+type CreateResult struct {
+	os.CreateResult
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/urls.go b/openstack/compute/v2/extensions/bootfromvolume/urls.go
new file mode 100644
index 0000000..0cffe25
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/urls.go
@@ -0,0 +1,7 @@
+package bootfromvolume
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("os-volumes_boot")
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/urls_test.go b/openstack/compute/v2/extensions/bootfromvolume/urls_test.go
new file mode 100644
index 0000000..6ee6477
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/urls_test.go
@@ -0,0 +1,16 @@
+package bootfromvolume
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-volumes_boot", createURL(c))
+}
diff --git a/openstack/compute/v2/extensions/delegate.go b/openstack/compute/v2/extensions/delegate.go
new file mode 100644
index 0000000..1007909
--- /dev/null
+++ b/openstack/compute/v2/extensions/delegate.go
@@ -0,0 +1,23 @@
+package extensions
+
+import (
+	"github.com/rackspace/gophercloud"
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractExtensions interprets a Page as a slice of Extensions.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+	return common.ExtractExtensions(page)
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+	return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return common.List(c)
+}
diff --git a/openstack/compute/v2/extensions/delegate_test.go b/openstack/compute/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..c3c525f
--- /dev/null
+++ b/openstack/compute/v2/extensions/delegate_test.go
@@ -0,0 +1,96 @@
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+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", client.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(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+
+		expected := []common.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.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", client.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(client.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/compute/v2/extensions/diskconfig/doc.go b/openstack/compute/v2/extensions/diskconfig/doc.go
new file mode 100644
index 0000000..a971b35
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/doc.go
@@ -0,0 +1,3 @@
+// Package diskconfig provides information and interaction with the the Disk
+// Config extension that works with the OpenStack Compute service.
+package diskconfig
diff --git a/openstack/compute/v2/extensions/diskconfig/requests.go b/openstack/compute/v2/extensions/diskconfig/requests.go
new file mode 100644
index 0000000..7407e0d
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/requests.go
@@ -0,0 +1,114 @@
+package diskconfig
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// DiskConfig represents one of the two possible settings for the DiskConfig option when creating,
+// rebuilding, or resizing servers: Auto or Manual.
+type DiskConfig string
+
+const (
+	// Auto builds a server with a single partition the size of the target flavor disk and
+	// automatically adjusts the filesystem to fit the entire partition. Auto may only be used with
+	// images and servers that use a single EXT3 partition.
+	Auto DiskConfig = "AUTO"
+
+	// Manual builds a server using whatever partition scheme and filesystem are present in the source
+	// image. If the target flavor disk is larger, the remaining space is left unpartitioned. This
+	// enables images to have non-EXT3 filesystems, multiple partitions, and so on, and enables you
+	// to manage the disk configuration. It also results in slightly shorter boot times.
+	Manual DiskConfig = "MANUAL"
+)
+
+// ErrInvalidDiskConfig is returned if an invalid string is specified for a DiskConfig option.
+var ErrInvalidDiskConfig = errors.New("DiskConfig must be either diskconfig.Auto or diskconfig.Manual.")
+
+// Validate ensures that a DiskConfig contains an appropriate value.
+func (config DiskConfig) validate() error {
+	switch config {
+	case Auto, Manual:
+		return nil
+	default:
+		return ErrInvalidDiskConfig
+	}
+}
+
+// CreateOptsExt adds a DiskConfig option to the base CreateOpts.
+type CreateOptsExt struct {
+	servers.CreateOptsBuilder
+
+	// DiskConfig [optional] controls how the created server's disk is partitioned.
+	DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"`
+}
+
+// ToServerCreateMap adds the diskconfig option to the base server creation options.
+func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
+	base, err := opts.CreateOptsBuilder.ToServerCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if string(opts.DiskConfig) == "" {
+		return base, nil
+	}
+
+	serverMap := base["server"].(map[string]interface{})
+	serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig)
+
+	return base, nil
+}
+
+// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts.
+type RebuildOptsExt struct {
+	servers.RebuildOptsBuilder
+
+	// DiskConfig [optional] controls how the rebuilt server's disk is partitioned.
+	DiskConfig DiskConfig
+}
+
+// ToServerRebuildMap adds the diskconfig option to the base server rebuild options.
+func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) {
+	err := opts.DiskConfig.validate()
+	if err != nil {
+		return nil, err
+	}
+
+	base, err := opts.RebuildOptsBuilder.ToServerRebuildMap()
+	if err != nil {
+		return nil, err
+	}
+
+	serverMap := base["rebuild"].(map[string]interface{})
+	serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig)
+
+	return base, nil
+}
+
+// ResizeOptsExt adds a DiskConfig option to the base server resize options.
+type ResizeOptsExt struct {
+	servers.ResizeOptsBuilder
+
+	// DiskConfig [optional] controls how the resized server's disk is partitioned.
+	DiskConfig DiskConfig
+}
+
+// ToServerResizeMap adds the diskconfig option to the base server creation options.
+func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) {
+	err := opts.DiskConfig.validate()
+	if err != nil {
+		return nil, err
+	}
+
+	base, err := opts.ResizeOptsBuilder.ToServerResizeMap()
+	if err != nil {
+		return nil, err
+	}
+
+	serverMap := base["resize"].(map[string]interface{})
+	serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig)
+
+	return base, nil
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/requests_test.go b/openstack/compute/v2/extensions/diskconfig/requests_test.go
new file mode 100644
index 0000000..e3c26d4
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/requests_test.go
@@ -0,0 +1,87 @@
+package diskconfig
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := CreateOptsExt{
+		CreateOptsBuilder: base,
+		DiskConfig:        Manual,
+	}
+
+	expected := `
+		{
+			"server": {
+				"name": "createdserver",
+				"imageRef": "asdfasdfasdf",
+				"flavorRef": "performance1-1",
+				"OS-DCF:diskConfig": "MANUAL"
+			}
+		}
+	`
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestRebuildOpts(t *testing.T) {
+	base := servers.RebuildOpts{
+		Name:      "rebuiltserver",
+		AdminPass: "swordfish",
+		ImageID:   "asdfasdfasdf",
+	}
+
+	ext := RebuildOptsExt{
+		RebuildOptsBuilder: base,
+		DiskConfig:         Auto,
+	}
+
+	actual, err := ext.ToServerRebuildMap()
+	th.AssertNoErr(t, err)
+
+	expected := `
+		{
+			"rebuild": {
+				"name": "rebuiltserver",
+				"imageRef": "asdfasdfasdf",
+				"adminPass": "swordfish",
+				"OS-DCF:diskConfig": "AUTO"
+			}
+		}
+	`
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestResizeOpts(t *testing.T) {
+	base := servers.ResizeOpts{
+		FlavorRef: "performance1-8",
+	}
+
+	ext := ResizeOptsExt{
+		ResizeOptsBuilder: base,
+		DiskConfig:        Auto,
+	}
+
+	actual, err := ext.ToServerResizeMap()
+	th.AssertNoErr(t, err)
+
+	expected := `
+		{
+			"resize": {
+				"flavorRef": "performance1-8",
+				"OS-DCF:diskConfig": "AUTO"
+			}
+		}
+	`
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/results.go b/openstack/compute/v2/extensions/diskconfig/results.go
new file mode 100644
index 0000000..10ec2da
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/results.go
@@ -0,0 +1,60 @@
+package diskconfig
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func commonExtract(result gophercloud.Result) (*DiskConfig, error) {
+	var resp struct {
+		Server struct {
+			DiskConfig string `mapstructure:"OS-DCF:diskConfig"`
+		} `mapstructure:"server"`
+	}
+
+	err := mapstructure.Decode(result.Body, &resp)
+	if err != nil {
+		return nil, err
+	}
+
+	config := DiskConfig(resp.Server.DiskConfig)
+	return &config, nil
+}
+
+// ExtractGet returns the disk configuration from a servers.Get call.
+func ExtractGet(result servers.GetResult) (*DiskConfig, error) {
+	return commonExtract(result.Result)
+}
+
+// ExtractUpdate returns the disk configuration from a servers.Update call.
+func ExtractUpdate(result servers.UpdateResult) (*DiskConfig, error) {
+	return commonExtract(result.Result)
+}
+
+// ExtractRebuild returns the disk configuration from a servers.Rebuild call.
+func ExtractRebuild(result servers.RebuildResult) (*DiskConfig, error) {
+	return commonExtract(result.Result)
+}
+
+// ExtractDiskConfig returns the DiskConfig setting for a specific server acquired from an
+// servers.ExtractServers call, while iterating through a Pager.
+func ExtractDiskConfig(page pagination.Page, index int) (*DiskConfig, error) {
+	casted := page.(servers.ServerPage).Body
+
+	type server struct {
+		DiskConfig string `mapstructure:"OS-DCF:diskConfig"`
+	}
+	var response struct {
+		Servers []server `mapstructure:"servers"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	config := DiskConfig(response.Servers[index].DiskConfig)
+	return &config, nil
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/results_test.go b/openstack/compute/v2/extensions/diskconfig/results_test.go
new file mode 100644
index 0000000..dd8d2b7
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/results_test.go
@@ -0,0 +1,68 @@
+package diskconfig
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestExtractGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	servers.HandleServerGetSuccessfully(t)
+
+	config, err := ExtractGet(servers.Get(client.ServiceClient(), "1234asdf"))
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, Manual, *config)
+}
+
+func TestExtractUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	servers.HandleServerUpdateSuccessfully(t)
+
+	r := servers.Update(client.ServiceClient(), "1234asdf", servers.UpdateOpts{
+		Name: "new-name",
+	})
+	config, err := ExtractUpdate(r)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, Manual, *config)
+}
+
+func TestExtractRebuild(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	servers.HandleRebuildSuccessfully(t, servers.SingleServerBody)
+
+	r := servers.Rebuild(client.ServiceClient(), "1234asdf", servers.RebuildOpts{
+		Name:       "new-name",
+		AdminPass:  "swordfish",
+		ImageID:    "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+		AccessIPv4: "1.2.3.4",
+	})
+	config, err := ExtractRebuild(r)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, Manual, *config)
+}
+
+func TestExtractList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	servers.HandleServerListSuccessfully(t)
+
+	pages := 0
+	err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		config, err := ExtractDiskConfig(page, 0)
+		th.AssertNoErr(t, err)
+		th.CheckEquals(t, Manual, *config)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, pages, 1)
+}
diff --git a/openstack/compute/v2/extensions/doc.go b/openstack/compute/v2/extensions/doc.go
new file mode 100644
index 0000000..2b447da
--- /dev/null
+++ b/openstack/compute/v2/extensions/doc.go
@@ -0,0 +1,3 @@
+// Package extensions provides information and interaction with the
+// different extensions available for the OpenStack Compute service.
+package extensions
diff --git a/openstack/compute/v2/extensions/keypairs/doc.go b/openstack/compute/v2/extensions/keypairs/doc.go
new file mode 100644
index 0000000..856f41b
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/doc.go
@@ -0,0 +1,3 @@
+// Package keypairs provides information and interaction with the Keypairs
+// extension for the OpenStack Compute service.
+package keypairs
diff --git a/openstack/compute/v2/extensions/keypairs/fixtures.go b/openstack/compute/v2/extensions/keypairs/fixtures.go
new file mode 100644
index 0000000..d10af99
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/fixtures.go
@@ -0,0 +1,171 @@
+// +build fixtures
+
+package keypairs
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput is a sample response to a List call.
+const ListOutput = `
+{
+	"keypairs": [
+		{
+			"keypair": {
+				"fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a",
+				"name": "firstkey",
+				"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n"
+			}
+		},
+		{
+			"keypair": {
+				"fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
+				"name": "secondkey",
+				"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n"
+			}
+		}
+	]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+	"keypair": {
+		"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n",
+		"name": "firstkey",
+		"fingerprint": "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a"
+	}
+}
+`
+
+// CreateOutput is a sample response to a Create call.
+const CreateOutput = `
+{
+	"keypair": {
+		"fingerprint": "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
+		"name": "createdkey",
+		"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n",
+		"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n",
+		"user_id": "fake"
+	}
+}
+`
+
+// ImportOutput is a sample response to a Create call that provides its own public key.
+const ImportOutput = `
+{
+	"keypair": {
+		"fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c",
+		"name": "importedkey",
+		"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova",
+		"user_id": "fake"
+	}
+}
+`
+
+// FirstKeyPair is the first result in ListOutput.
+var FirstKeyPair = KeyPair{
+	Name:        "firstkey",
+	Fingerprint: "15:b0:f8:b3:f9:48:63:71:cf:7b:5b:38:6d:44:2d:4a",
+	PublicKey:   "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+Eo/RZRngaGTkFs7I62ZjsIlO79KklKbMXi8F+KITD4bVQHHn+kV+4gRgkgCRbdoDqoGfpaDFs877DYX9n4z6FrAIZ4PES8TNKhatifpn9NdQYWA+IkU8CuvlEKGuFpKRi/k7JLos/gHi2hy7QUwgtRvcefvD/vgQZOVw/mGR9Q== Generated by Nova\n",
+}
+
+// SecondKeyPair is the second result in ListOutput.
+var SecondKeyPair = KeyPair{
+	Name:        "secondkey",
+	Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
+	PublicKey:   "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n",
+}
+
+// ExpectedKeyPairSlice is the slice of results that should be parsed from ListOutput, in the expected
+// order.
+var ExpectedKeyPairSlice = []KeyPair{FirstKeyPair, SecondKeyPair}
+
+// CreatedKeyPair is the parsed result from CreatedOutput.
+var CreatedKeyPair = KeyPair{
+	Name:        "createdkey",
+	Fingerprint: "35:9d:d0:c3:4a:80:d3:d8:86:f1:ca:f7:df:c4:f9:d8",
+	PublicKey:   "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7DUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5Q== Generated by Nova\n",
+	PrivateKey:  "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQC9mC3WZN9UGLxgPBpP7H5jZMc6pKwOoSgre8yun6REFktn/Kz7\nDUt9jaR1UJyRzHxITfCfAIgSxPdGqB/oF1suMyWgu5i0625vavLB5z5kC8Hq3qZJ\n9zJO1poE1kyD+htiTtPWJ88e12xuH2XB/CZN9OpEiF98hAagiOE0EnOS5QIDAQAB\nAoGAE5XO1mDhORy9COvsg+kYPUhB1GsCYxh+v88wG7HeFDKBY6KUc/Kxo6yoGn5T\nTjRjekyi2KoDZHz4VlIzyZPwFS4I1bf3oCunVoAKzgLdmnTtvRNMC5jFOGc2vUgP\n9bSyRj3S1R4ClVk2g0IDeagko/jc8zzLEYuIK+fbkds79YECQQDt3vcevgegnkga\ntF4NsDmmBPRkcSHCqrANP/7vFcBQN3czxeYYWX3DK07alu6GhH1Y4sHbdm616uU0\nll7xbDzxAkEAzAtN2IyftNygV2EGiaGgqLyo/tD9+Vui2qCQplqe4jvWh/5Sparl\nOjmKo+uAW+hLrLVMnHzRWxbWU8hirH5FNQJATO+ZxCK4etXXAnQmG41NCAqANWB2\nB+2HJbH2NcQ2QHvAHUm741JGn/KI/aBlo7KEjFRDWUVUB5ji64BbUwCsMQJBAIku\nLGcjnBf/oLk+XSPZC2eGd2Ph5G5qYmH0Q2vkTx+wtTn3DV+eNsDfgMtWAJVJ5t61\ngU1QSXyhLPVlKpnnxuUCQC+xvvWjWtsLaFtAsZywJiqLxQzHts8XLGZptYJ5tLWV\nrtmYtBcJCN48RrgQHry/xWYeA4K/AFQpXfNPgprQ96Q=\n-----END RSA PRIVATE KEY-----\n",
+	UserID:      "fake",
+}
+
+// ImportedKeyPair is the parsed result from ImportOutput.
+var ImportedKeyPair = KeyPair{
+	Name:        "importedkey",
+	Fingerprint: "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c",
+	PublicKey:   "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova",
+	UserID:      "fake",
+}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, ListOutput)
+	})
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request for "firstkey".
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-keypairs/firstkey", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+}
+
+// HandleCreateSuccessfully configures the test server to respond to a Create request for a new
+// keypair called "createdkey".
+func HandleCreateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "keypair": { "name": "createdkey" } }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, CreateOutput)
+	})
+}
+
+// HandleImportSuccessfully configures the test server to respond to an Import request for an
+// existing keypair called "importedkey".
+func HandleImportSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-keypairs", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `
+			{
+				"keypair": {
+					"name": "importedkey",
+					"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova"
+				}
+			}
+		`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, ImportOutput)
+	})
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
+// keypair called "deletedkey".
+func HandleDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-keypairs/deletedkey", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/compute/v2/extensions/keypairs/requests.go b/openstack/compute/v2/extensions/keypairs/requests.go
new file mode 100644
index 0000000..01ee12a
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/requests.go
@@ -0,0 +1,89 @@
+package keypairs
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of KeyPairs.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return KeyPairPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the
+// CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+	ToKeyPairCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts species keypair creation or import parameters.
+type CreateOpts struct {
+	// Name [required] is a friendly name to refer to this KeyPair in other services.
+	Name string
+
+	// PublicKey [optional] is a pregenerated OpenSSH-formatted public key. If provided, this key
+	// will be imported and no new key will be created.
+	PublicKey string
+}
+
+// ToKeyPairCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) {
+	if opts.Name == "" {
+		return nil, errors.New("Missing field required for keypair creation: Name")
+	}
+
+	keypair := make(map[string]interface{})
+	keypair["name"] = opts.Name
+	if opts.PublicKey != "" {
+		keypair["public_key"] = opts.PublicKey
+	}
+
+	return map[string]interface{}{"keypair": keypair}, nil
+}
+
+// Create requests the creation of a new keypair on the server, or to import a pre-existing
+// keypair.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToKeyPairCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Get returns public data about a previously uploaded KeyPair.
+func Get(client *gophercloud.ServiceClient, name string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(client, name), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Delete requests the deletion of a previous stored KeyPair from the server.
+func Delete(client *gophercloud.ServiceClient, name string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", deleteURL(client, name), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{202},
+	})
+	return res
+}
diff --git a/openstack/compute/v2/extensions/keypairs/requests_test.go b/openstack/compute/v2/extensions/keypairs/requests_test.go
new file mode 100644
index 0000000..502a154
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/requests_test.go
@@ -0,0 +1,71 @@
+package keypairs
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractKeyPairs(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedKeyPairSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t)
+
+	actual, err := Create(client.ServiceClient(), CreateOpts{
+		Name: "createdkey",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &CreatedKeyPair, actual)
+}
+
+func TestImport(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleImportSuccessfully(t)
+
+	actual, err := Create(client.ServiceClient(), CreateOpts{
+		Name:      "importedkey",
+		PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &ImportedKeyPair, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "firstkey").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstKeyPair, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "deletedkey").Extract()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/keypairs/results.go b/openstack/compute/v2/extensions/keypairs/results.go
new file mode 100644
index 0000000..96b96ea
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/results.go
@@ -0,0 +1,99 @@
+package keypairs
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// KeyPair is an SSH key known to the OpenStack cluster that is available to be injected into
+// servers.
+type KeyPair struct {
+	// Name is used to refer to this keypair from other services within this region.
+	Name string `mapstructure:"name"`
+
+	// Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer
+	// public key.
+	Fingerprint string `mapstructure:"fingerprint"`
+
+	// PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..."
+	PublicKey string `mapstructure:"public_key"`
+
+	// PrivateKey is the private key from this pair, in PEM format.
+	// "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..." It is only present if this keypair was just
+	// returned from a Create call
+	PrivateKey string `mapstructure:"private_key"`
+
+	// UserID is the user who owns this keypair.
+	UserID string `mapstructure:"user_id"`
+}
+
+// KeyPairPage stores a single, only page of KeyPair results from a List call.
+type KeyPairPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a KeyPairPage is empty.
+func (page KeyPairPage) IsEmpty() (bool, error) {
+	ks, err := ExtractKeyPairs(page)
+	return len(ks) == 0, err
+}
+
+// ExtractKeyPairs interprets a page of results as a slice of KeyPairs.
+func ExtractKeyPairs(page pagination.Page) ([]KeyPair, error) {
+	type pair struct {
+		KeyPair KeyPair `mapstructure:"keypair"`
+	}
+
+	var resp struct {
+		KeyPairs []pair `mapstructure:"keypairs"`
+	}
+
+	err := mapstructure.Decode(page.(KeyPairPage).Body, &resp)
+	results := make([]KeyPair, len(resp.KeyPairs))
+	for i, pair := range resp.KeyPairs {
+		results[i] = pair.KeyPair
+	}
+	return results, err
+}
+
+type keyPairResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any KeyPair resource response as a KeyPair struct.
+func (r keyPairResult) Extract() (*KeyPair, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		KeyPair *KeyPair `json:"keypair" mapstructure:"keypair"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+	return res.KeyPair, err
+}
+
+// CreateResult is the response from a Create operation. Call its Extract method to interpret it
+// as a KeyPair.
+type CreateResult struct {
+	keyPairResult
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a KeyPair.
+type GetResult struct {
+	keyPairResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
+// the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.Result
+}
+
+// Extract determines whether or not a deletion request was accepted.
+func (r DeleteResult) Extract() error {
+	return r.Err
+}
diff --git a/openstack/compute/v2/extensions/keypairs/urls.go b/openstack/compute/v2/extensions/keypairs/urls.go
new file mode 100644
index 0000000..702f532
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/urls.go
@@ -0,0 +1,25 @@
+package keypairs
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-keypairs"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return resourceURL(c)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return resourceURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, name string) string {
+	return c.ServiceURL(resourcePath, name)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, name string) string {
+	return getURL(c, name)
+}
diff --git a/openstack/compute/v2/extensions/keypairs/urls_test.go b/openstack/compute/v2/extensions/keypairs/urls_test.go
new file mode 100644
index 0000000..60efd2a
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/urls_test.go
@@ -0,0 +1,40 @@
+package keypairs
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-keypairs", listURL(c))
+}
+
+func TestCreateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-keypairs", createURL(c))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", getURL(c, "wat"))
+}
+
+func TestDeleteURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-keypairs/wat", deleteURL(c, "wat"))
+}
diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go
new file mode 100644
index 0000000..5822e1b
--- /dev/null
+++ b/openstack/compute/v2/flavors/doc.go
@@ -0,0 +1,7 @@
+// Package flavors provides information and interaction with the flavor API
+// resource in the OpenStack Compute service.
+//
+// A flavor is an available hardware configuration for a server. Each flavor
+// has a unique combination of disk space, memory capacity and priority for CPU
+// time.
+package flavors
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
new file mode 100644
index 0000000..065a2ec
--- /dev/null
+++ b/openstack/compute/v2/flavors/requests.go
@@ -0,0 +1,72 @@
+package flavors
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToFlavorListQuery() (string, error)
+}
+
+// ListOpts helps control the results returned by the List() function.
+// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
+// Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
+type ListOpts struct {
+
+	// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
+	ChangesSince string `q:"changes-since"`
+
+	// MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
+	MinDisk int `q:"minDisk"`
+	MinRAM  int `q:"minRam"`
+
+	// Marker and Limit control paging.
+	// Marker instructs List where to start listing from.
+	Marker string `q:"marker"`
+
+	// Limit instructs List to refrain from sending excessively large lists of flavors.
+	Limit int `q:"limit"`
+}
+
+// ToFlavorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListDetail instructs OpenStack to provide a list of flavors.
+// You may provide criteria by which List curtails its results for easier processing.
+// See ListOpts for more details.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToFlavorListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, url, createPage)
+}
+
+// Get instructs OpenStack to provide details on a single flavor, identified by its ID.
+// Use ExtractFlavor to convert its result into a Flavor.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var gr GetResult
+	gr.Err = perigee.Get(getURL(client, id), perigee.Options{
+		Results:     &gr.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return gr
+}
diff --git a/openstack/compute/v2/flavors/requests_test.go b/openstack/compute/v2/flavors/requests_test.go
new file mode 100644
index 0000000..fbd7c33
--- /dev/null
+++ b/openstack/compute/v2/flavors/requests_test.go
@@ -0,0 +1,129 @@
+package flavors
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const tokenID = "blerb"
+
+func TestListFlavors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+					{
+						"flavors": [
+							{
+								"id": "1",
+								"name": "m1.tiny",
+								"disk": 1,
+								"ram": 512,
+								"vcpus": 1
+							},
+							{
+								"id": "2",
+								"name": "m2.small",
+								"disk": 10,
+								"ram": 1024,
+								"vcpus": 2
+							}
+						],
+						"flavors_links": [
+							{
+								"href": "%s/flavors/detail?marker=2",
+								"rel": "next"
+							}
+						]
+					}
+				`, th.Server.URL)
+		case "2":
+			fmt.Fprintf(w, `{ "flavors": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	pages := 0
+	err := ListDetail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Flavor{
+			Flavor{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1},
+			Flavor{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Expected %#v, but was %#v", expected, actual)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if pages != 1 {
+		t.Errorf("Expected one page, got %d", pages)
+	}
+}
+
+func TestGetFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"flavor": {
+					"id": "1",
+					"name": "m1.tiny",
+					"disk": 1,
+					"ram": 512,
+					"vcpus": 1,
+					"rxtx_factor": 1
+				}
+			}
+		`)
+	})
+
+	actual, err := Get(fake.ServiceClient(), "12345").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get flavor: %v", err)
+	}
+
+	expected := &Flavor{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       1,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1,
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but was %#v", expected, actual)
+	}
+}
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
new file mode 100644
index 0000000..389bd7f
--- /dev/null
+++ b/openstack/compute/v2/flavors/results.go
@@ -0,0 +1,122 @@
+package flavors
+
+import (
+	"errors"
+	"reflect"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure.
+var ErrCannotInterpet = errors.New("Unable to interpret a response body.")
+
+// GetResult temporarily holds the reponse from a Get call.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract provides access to the individual Flavor returned by the Get function.
+func (gr GetResult) Extract() (*Flavor, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+
+	var result struct {
+		Flavor Flavor `mapstructure:"flavor"`
+	}
+
+	cfg := &mapstructure.DecoderConfig{
+		DecodeHook: defaulter,
+		Result:     &result,
+	}
+	decoder, err := mapstructure.NewDecoder(cfg)
+	if err != nil {
+		return nil, err
+	}
+	err = decoder.Decode(gr.Body)
+	return &result.Flavor, err
+}
+
+// Flavor records represent (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+	// The Id field contains the flavor's unique identifier.
+	// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
+	ID string `mapstructure:"id"`
+
+	// The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
+	Disk int `mapstructure:"disk"`
+	RAM  int `mapstructure:"ram"`
+
+	// The Name field provides a human-readable moniker for the flavor.
+	Name string `mapstructure:"name"`
+
+	RxTxFactor float64 `mapstructure:"rxtx_factor"`
+
+	// Swap indicates how much space is reserved for swap.
+	// If not provided, this field will be set to 0.
+	Swap int `mapstructure:"swap"`
+
+	// VCPUs indicates how many (virtual) CPUs are available for this flavor.
+	VCPUs int `mapstructure:"vcpus"`
+}
+
+// FlavorPage contains a single page of the response from a List call.
+type FlavorPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty determines if a page contains any results.
+func (p FlavorPage) IsEmpty() (bool, error) {
+	flavors, err := ExtractFlavors(p)
+	if err != nil {
+		return true, err
+	}
+	return len(flavors) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (p FlavorPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"flavors_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
+	if (from == reflect.String) && (to == reflect.Int) {
+		return 0, nil
+	}
+	return v, nil
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+	casted := page.(FlavorPage).Body
+	var container struct {
+		Flavors []Flavor `mapstructure:"flavors"`
+	}
+
+	cfg := &mapstructure.DecoderConfig{
+		DecodeHook: defaulter,
+		Result:     &container,
+	}
+	decoder, err := mapstructure.NewDecoder(cfg)
+	if err != nil {
+		return container.Flavors, err
+	}
+	err = decoder.Decode(casted)
+	if err != nil {
+		return container.Flavors, err
+	}
+
+	return container.Flavors, nil
+}
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
new file mode 100644
index 0000000..683c107
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls.go
@@ -0,0 +1,13 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("flavors", id)
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("flavors", "detail")
+}
diff --git a/openstack/compute/v2/flavors/urls_test.go b/openstack/compute/v2/flavors/urls_test.go
new file mode 100644
index 0000000..069da24
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls_test.go
@@ -0,0 +1,26 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "flavors/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "flavors/detail"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/images/doc.go b/openstack/compute/v2/images/doc.go
new file mode 100644
index 0000000..0edaa3f
--- /dev/null
+++ b/openstack/compute/v2/images/doc.go
@@ -0,0 +1,7 @@
+// Package images provides information and interaction with the image API
+// resource in the OpenStack Compute service.
+//
+// An image is a collection of files used to create or rebuild a server.
+// Operators provide a number of pre-built OS images by default. You may also
+// create custom images from cloud servers you have launched.
+package images
diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go
new file mode 100644
index 0000000..1422cd0
--- /dev/null
+++ b/openstack/compute/v2/images/requests.go
@@ -0,0 +1,71 @@
+package images
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToImageListQuery() (string, error)
+}
+
+// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
+type ListOpts struct {
+	// When the image last changed status (in date-time format).
+	ChangesSince string `q:"changes-since"`
+	// The number of Images to return.
+	Limit int `q:"limit"`
+	// UUID of the Image at which to set a marker.
+	Marker string `q:"marker"`
+	// The name of the Image.
+	Name string `q:"name:"`
+	// The name of the Server (in URL format).
+	Server string `q:"server"`
+	// The current status of the Image.
+	Status string `q:"status"`
+	// The value of the type of image (e.g. BASE, SERVER, ALL)
+	Type string `q:"type"`
+}
+
+// ToImageListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToImageListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListDetail enumerates the available images.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listDetailURL(client)
+	if opts != nil {
+		query, err := opts.ToImageListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return ImagePage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, url, createPage)
+}
+
+// Get acquires additional detail about a specific image by ID.
+// Use ExtractImage() to intepret the result as an openstack Image.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		Results:     &result.Body,
+		OkCodes:     []int{200},
+	})
+	return result
+}
diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go
new file mode 100644
index 0000000..9a05f97
--- /dev/null
+++ b/openstack/compute/v2/images/requests_test.go
@@ -0,0 +1,175 @@
+package images
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListImages(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+				{
+					"images": [
+						{
+							"status": "ACTIVE",
+							"updated": "2014-09-23T12:54:56Z",
+							"id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+							"OS-EXT-IMG-SIZE:size": 476704768,
+							"name": "F17-x86_64-cfntools",
+							"created": "2014-09-23T12:54:52Z",
+							"minDisk": 0,
+							"progress": 100,
+							"minRam": 0,
+							"metadata": {}
+						},
+						{
+							"status": "ACTIVE",
+							"updated": "2014-09-23T12:51:43Z",
+							"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+							"OS-EXT-IMG-SIZE:size": 13167616,
+							"name": "cirros-0.3.2-x86_64-disk",
+							"created": "2014-09-23T12:51:42Z",
+							"minDisk": 0,
+							"progress": 100,
+							"minRam": 0,
+							"metadata": {}
+						}
+					]
+				}
+			`)
+		case "2":
+			fmt.Fprintf(w, `{ "images": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	pages := 0
+	options := &ListOpts{Limit: 2}
+	err := ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Image{
+			Image{
+				ID:       "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+				Name:     "F17-x86_64-cfntools",
+				Created:  "2014-09-23T12:54:52Z",
+				Updated:  "2014-09-23T12:54:56Z",
+				MinDisk:  0,
+				MinRAM:   0,
+				Progress: 100,
+				Status:   "ACTIVE",
+			},
+			Image{
+				ID:       "f90f6034-2570-4974-8351-6b49732ef2eb",
+				Name:     "cirros-0.3.2-x86_64-disk",
+				Created:  "2014-09-23T12:51:42Z",
+				Updated:  "2014-09-23T12:51:43Z",
+				MinDisk:  0,
+				MinRAM:   0,
+				Progress: 100,
+				Status:   "ACTIVE",
+			},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual)
+		}
+
+		return false, nil
+	})
+
+	if err != nil {
+		t.Fatalf("EachPage error: %v", err)
+	}
+	if pages != 1 {
+		t.Errorf("Expected one page, got %d", pages)
+	}
+}
+
+func TestGetImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"image": {
+					"status": "ACTIVE",
+					"updated": "2014-09-23T12:54:56Z",
+					"id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+					"OS-EXT-IMG-SIZE:size": 476704768,
+					"name": "F17-x86_64-cfntools",
+					"created": "2014-09-23T12:54:52Z",
+					"minDisk": 0,
+					"progress": 100,
+					"minRam": 0,
+					"metadata": {}
+				}
+			}
+		`)
+	})
+
+	actual, err := Get(fake.ServiceClient(), "12345678").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Get: %v", err)
+	}
+
+	expected := &Image{
+		Status:   "ACTIVE",
+		Updated:  "2014-09-23T12:54:56Z",
+		ID:       "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+		Name:     "F17-x86_64-cfntools",
+		Created:  "2014-09-23T12:54:52Z",
+		MinDisk:  0,
+		Progress: 100,
+		MinRAM:   0,
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but got %#v", expected, actual)
+	}
+}
+
+func TestNextPageURL(t *testing.T) {
+	var page ImagePage
+	var body map[string]interface{}
+	bodyString := []byte(`{"images":{"links":[{"href":"http://192.154.23.87/12345/images/image3","rel":"bookmark"}]}, "images_links":[{"href":"http://192.154.23.87/12345/images/image4","rel":"next"}]}`)
+	err := json.Unmarshal(bodyString, &body)
+	if err != nil {
+		t.Fatalf("Error unmarshaling data into page body: %v", err)
+	}
+	page.Body = body
+
+	expected := "http://192.154.23.87/12345/images/image4"
+	actual, err := page.NextPageURL()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..493d511
--- /dev/null
+++ b/openstack/compute/v2/images/results.go
@@ -0,0 +1,90 @@
+package images
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily stores a Get response.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult as an Image.
+func (gr GetResult) Extract() (*Image, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+
+	var decoded struct {
+		Image Image `mapstructure:"image"`
+	}
+
+	err := mapstructure.Decode(gr.Body, &decoded)
+	return &decoded.Image, err
+}
+
+// Image is used for JSON (un)marshalling.
+// It provides a description of an OS image.
+type Image struct {
+	// ID contains the image's unique identifier.
+	ID string
+
+	Created string
+
+	// MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
+	MinDisk int
+	MinRAM  int
+
+	// Name provides a human-readable moniker for the OS image.
+	Name string
+
+	// The Progress and Status fields indicate image-creation status.
+	// Any usable image will have 100% progress.
+	Progress int
+	Status   string
+
+	Updated string
+}
+
+// ImagePage contains a single page of results from a List operation.
+// Use ExtractImages to convert it into a slice of usable structs.
+type ImagePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Image results.
+func (page ImagePage) IsEmpty() (bool, error) {
+	images, err := ExtractImages(page)
+	if err != nil {
+		return true, err
+	}
+	return len(images) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ImagePage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"images_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractImages converts a page of List results into a slice of usable Image structs.
+func ExtractImages(page pagination.Page) ([]Image, error) {
+	casted := page.(ImagePage).Body
+	var results struct {
+		Images []Image `mapstructure:"images"`
+	}
+
+	err := mapstructure.Decode(casted, &results)
+	return results.Images, err
+}
diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go
new file mode 100644
index 0000000..9b3c86d
--- /dev/null
+++ b/openstack/compute/v2/images/urls.go
@@ -0,0 +1,11 @@
+package images
+
+import "github.com/rackspace/gophercloud"
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("images", "detail")
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("images", id)
+}
diff --git a/openstack/compute/v2/images/urls_test.go b/openstack/compute/v2/images/urls_test.go
new file mode 100644
index 0000000..b1ab3d6
--- /dev/null
+++ b/openstack/compute/v2/images/urls_test.go
@@ -0,0 +1,26 @@
+package images
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "images/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+	actual := listDetailURL(endpointClient())
+	expected := endpoint + "images/detail"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/doc.go b/openstack/compute/v2/servers/doc.go
new file mode 100644
index 0000000..fe45671
--- /dev/null
+++ b/openstack/compute/v2/servers/doc.go
@@ -0,0 +1,6 @@
+// Package servers provides information and interaction with the server API
+// resource in the OpenStack Compute service.
+//
+// A server is a virtual machine instance in the compute system. In order for
+// one to be provisioned, a valid flavor and image are required.
+package servers
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
new file mode 100644
index 0000000..e872b07
--- /dev/null
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -0,0 +1,459 @@
+// +build fixtures
+
+package servers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ServerListBody contains the canned body of a servers.List response.
+const ServerListBody = `
+{
+	"servers": [
+		{
+			"status": "ACTIVE",
+			"updated": "2014-09-25T13:10:10Z",
+			"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+			"OS-EXT-SRV-ATTR:host": "devstack",
+			"addresses": {
+				"private": [
+					{
+						"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+						"version": 4,
+						"addr": "10.0.0.32",
+						"OS-EXT-IPS:type": "fixed"
+					}
+				]
+			},
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+					"rel": "self"
+				},
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+					"rel": "bookmark"
+				}
+			],
+			"key_name": null,
+			"image": {
+				"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
+			"OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
+			"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+			"flavor": {
+				"id": "1",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+			"security_groups": [
+				{
+					"name": "default"
+				}
+			],
+			"OS-SRV-USG:terminated_at": null,
+			"OS-EXT-AZ:availability_zone": "nova",
+			"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+			"name": "herp",
+			"created": "2014-09-25T13:10:02Z",
+			"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+			"OS-DCF:diskConfig": "MANUAL",
+			"os-extended-volumes:volumes_attached": [],
+			"accessIPv4": "",
+			"accessIPv6": "",
+			"progress": 0,
+			"OS-EXT-STS:power_state": 1,
+			"config_drive": "",
+			"metadata": {}
+		},
+		{
+			"status": "ACTIVE",
+			"updated": "2014-09-25T13:04:49Z",
+			"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+			"OS-EXT-SRV-ATTR:host": "devstack",
+			"addresses": {
+				"private": [
+					{
+						"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+						"version": 4,
+						"addr": "10.0.0.31",
+						"OS-EXT-IPS:type": "fixed"
+					}
+				]
+			},
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+					"rel": "self"
+				},
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+					"rel": "bookmark"
+				}
+			],
+			"key_name": null,
+			"image": {
+				"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+			"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+			"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+			"flavor": {
+				"id": "1",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+			"security_groups": [
+				{
+					"name": "default"
+				}
+			],
+			"OS-SRV-USG:terminated_at": null,
+			"OS-EXT-AZ:availability_zone": "nova",
+			"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+			"name": "derp",
+			"created": "2014-09-25T13:04:41Z",
+			"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+			"OS-DCF:diskConfig": "MANUAL",
+			"os-extended-volumes:volumes_attached": [],
+			"accessIPv4": "",
+			"accessIPv6": "",
+			"progress": 0,
+			"OS-EXT-STS:power_state": 1,
+			"config_drive": "",
+			"metadata": {}
+		}
+	]
+}
+`
+
+// SingleServerBody is the canned body of a Get request on an existing server.
+const SingleServerBody = `
+{
+	"server": {
+		"status": "ACTIVE",
+		"updated": "2014-09-25T13:04:49Z",
+		"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+		"OS-EXT-SRV-ATTR:host": "devstack",
+		"addresses": {
+			"private": [
+				{
+					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+					"version": 4,
+					"addr": "10.0.0.31",
+					"OS-EXT-IPS:type": "fixed"
+				}
+			]
+		},
+		"links": [
+			{
+				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel": "self"
+			},
+			{
+				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel": "bookmark"
+			}
+		],
+		"key_name": null,
+		"image": {
+			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"OS-EXT-STS:task_state": null,
+		"OS-EXT-STS:vm_state": "active",
+		"OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+		"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+		"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+		"flavor": {
+			"id": "1",
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+		"security_groups": [
+			{
+				"name": "default"
+			}
+		],
+		"OS-SRV-USG:terminated_at": null,
+		"OS-EXT-AZ:availability_zone": "nova",
+		"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+		"name": "derp",
+		"created": "2014-09-25T13:04:41Z",
+		"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+		"OS-DCF:diskConfig": "MANUAL",
+		"os-extended-volumes:volumes_attached": [],
+		"accessIPv4": "",
+		"accessIPv6": "",
+		"progress": 0,
+		"OS-EXT-STS:power_state": 1,
+		"config_drive": "",
+		"metadata": {}
+	}
+}
+`
+
+var (
+	// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
+	ServerHerp = Server{
+		Status:  "ACTIVE",
+		Updated: "2014-09-25T13:10:10Z",
+		HostID:  "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+		Addresses: map[string]interface{}{
+			"private": []interface{}{
+				map[string]interface{}{
+					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+					"version":                 float64(4),
+					"addr":                    "10.0.0.32",
+					"OS-EXT-IPS:type":         "fixed",
+				},
+			},
+		},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+				"rel":  "bookmark",
+			},
+		},
+		Image: map[string]interface{}{
+			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		Flavor: map[string]interface{}{
+			"id": "1",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		ID:       "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
+		Name:     "herp",
+		Created:  "2014-09-25T13:10:02Z",
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]interface{}{},
+	}
+
+	// ServerDerp is a Server struct that should correspond to the second server in ServerListBody.
+	ServerDerp = Server{
+		Status:  "ACTIVE",
+		Updated: "2014-09-25T13:04:49Z",
+		HostID:  "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+		Addresses: map[string]interface{}{
+			"private": []interface{}{
+				map[string]interface{}{
+					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+					"version":                 float64(4),
+					"addr":                    "10.0.0.31",
+					"OS-EXT-IPS:type":         "fixed",
+				},
+			},
+		},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel":  "bookmark",
+			},
+		},
+		Image: map[string]interface{}{
+			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		Flavor: map[string]interface{}{
+			"id": "1",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		ID:       "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
+		Name:     "derp",
+		Created:  "2014-09-25T13:04:41Z",
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]interface{}{},
+	}
+)
+
+// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request
+// with a given response.
+func HandleServerCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"server": {
+				"name": "derp",
+				"imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
+				"flavorRef": "1"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleServerListSuccessfully sets up the test server to respond to a server List request.
+func HandleServerListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, ServerListBody)
+		case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
+			fmt.Fprintf(w, `{ "servers": [] }`)
+		default:
+			t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request.
+func HandleServerDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleServerGetSuccessfully sets up the test server to respond to a server Get request.
+func HandleServerGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, SingleServerBody)
+	})
+}
+
+// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request.
+func HandleServerUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+
+		fmt.Fprintf(w, SingleServerBody)
+	})
+}
+
+// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
+// change request.
+func HandleAdminPasswordChangeSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success.
+func HandleRebootSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success.
+func HandleRebuildSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `
+			{
+				"rebuild": {
+					"name": "new-name",
+					"adminPass": "swordfish",
+					"imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"accessIPv4": "1.2.3.4"
+				}
+			}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
new file mode 100644
index 0000000..544f816
--- /dev/null
+++ b/openstack/compute/v2/servers/requests.go
@@ -0,0 +1,538 @@
+package servers
+
+import (
+	"encoding/base64"
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToServerListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the server attributes you want to see returned. Marker and Limit are used
+// for pagination.
+type ListOpts struct {
+	// A time/date stamp for when the server last changed status.
+	ChangesSince string `q:"changes-since"`
+
+	// Name of the image in URL format.
+	Image string `q:"image"`
+
+	// Name of the flavor in URL format.
+	Flavor string `q:"flavor"`
+
+	// Name of the server as a string; can be queried with regular expressions.
+	// Realize that ?name=bob returns both bob and bobb. If you need to match bob
+	// only, you can use a regular expression matching the syntax of the
+	// underlying database server implemented for Compute.
+	Name string `q:"name"`
+
+	// Value of the status of the server so that you can filter on "ACTIVE" for example.
+	Status string `q:"status"`
+
+	// Name of the host as a string.
+	Host string `q:"host"`
+
+	// UUID of the server at which you want to set a marker.
+	Marker string `q:"marker"`
+
+	// Integer value for the limit of values to return.
+	Limit int `q:"limit"`
+}
+
+// ToServerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToServerListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List makes a request against the API to list servers accessible to you.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listDetailURL(client)
+
+	if opts != nil {
+		query, err := opts.ToServerListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return ServerPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+// The CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+	ToServerCreateMap() (map[string]interface{}, error)
+}
+
+// Network is used within CreateOpts to control a new server's network attachments.
+type Network struct {
+	// UUID of a nova-network to attach to the newly provisioned server.
+	// Required unless Port is provided.
+	UUID string
+
+	// Port of a neutron network to attach to the newly provisioned server.
+	// Required unless UUID is provided.
+	Port string
+
+	// FixedIP [optional] specifies a fixed IPv4 address to be used on this network.
+	FixedIP string
+}
+
+// CreateOpts specifies server creation parameters.
+type CreateOpts struct {
+	// Name [required] is the name to assign to the newly launched server.
+	Name string
+
+	// ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+	// Optional if using the boot-from-volume extension.
+	ImageRef string
+
+	// FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+	FlavorRef string
+
+	// SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+	SecurityGroups []string
+
+	// UserData [optional] contains configuration information or scripts to use upon launch.
+	// Create will base64-encode it for you.
+	UserData []byte
+
+	// AvailabilityZone [optional] in which to launch the server.
+	AvailabilityZone string
+
+	// Networks [optional] dictates how this server will be attached to available networks.
+	// By default, the server will be attached to all isolated networks for the tenant.
+	Networks []Network
+
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string
+
+	// Personality [optional] includes the path and contents of a file to inject into the server at launch.
+	// The maximum size of the file is 255 bytes (decoded).
+	Personality []byte
+
+	// ConfigDrive [optional] enables metadata injection through a configuration drive.
+	ConfigDrive bool
+}
+
+// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
+	server := make(map[string]interface{})
+
+	server["name"] = opts.Name
+	server["imageRef"] = opts.ImageRef
+	server["flavorRef"] = opts.FlavorRef
+
+	if opts.UserData != nil {
+		encoded := base64.StdEncoding.EncodeToString(opts.UserData)
+		server["user_data"] = &encoded
+	}
+	if opts.Personality != nil {
+		encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+		server["personality"] = &encoded
+	}
+	if opts.ConfigDrive {
+		server["config_drive"] = "true"
+	}
+	if opts.AvailabilityZone != "" {
+		server["availability_zone"] = opts.AvailabilityZone
+	}
+	if opts.Metadata != nil {
+		server["metadata"] = opts.Metadata
+	}
+
+	if len(opts.SecurityGroups) > 0 {
+		securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
+		for i, groupName := range opts.SecurityGroups {
+			securityGroups[i] = map[string]interface{}{"name": groupName}
+		}
+	}
+
+	if len(opts.Networks) > 0 {
+		networks := make([]map[string]interface{}, len(opts.Networks))
+		for i, net := range opts.Networks {
+			networks[i] = make(map[string]interface{})
+			if net.UUID != "" {
+				networks[i]["uuid"] = net.UUID
+			}
+			if net.Port != "" {
+				networks[i]["port"] = net.Port
+			}
+			if net.FixedIP != "" {
+				networks[i]["fixed_ip"] = net.FixedIP
+			}
+		}
+		server["networks"] = networks
+	}
+
+	return map[string]interface{}{"server": server}, nil
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToServerCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", listURL(client), perigee.Options{
+		Results:     &res.Body,
+		ReqBody:     reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+	return res
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return err
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return result
+}
+
+// UpdateOptsBuilder allows extentions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToServerUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts struct {
+	// Name [optional] changes the displayed name of the server.
+	// The server host name will *not* change.
+	// Server names are not constrained to be unique, even within the same tenant.
+	Name string
+
+	// AccessIPv4 [optional] provides a new IPv4 address for the instance.
+	AccessIPv4 string
+
+	// AccessIPv6 [optional] provides a new IPv6 address for the instance.
+	AccessIPv6 string
+}
+
+// ToServerUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} {
+	server := make(map[string]string)
+	if opts.Name != "" {
+		server["name"] = opts.Name
+	}
+	if opts.AccessIPv4 != "" {
+		server["accessIPv4"] = opts.AccessIPv4
+	}
+	if opts.AccessIPv6 != "" {
+		server["accessIPv6"] = opts.AccessIPv6
+	}
+	return map[string]interface{}{"server": server}
+}
+
+// Update requests that various attributes of the indicated server be changed.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+	_, result.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     opts.ToServerUpdateMap(),
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return result
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) ActionResult {
+	var req struct {
+		ChangePassword struct {
+			AdminPass string `json:"adminPass"`
+		} `json:"changePassword"`
+	}
+
+	req.ChangePassword.AdminPass = newPassword
+
+	var res ActionResult
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody:     req,
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return res
+}
+
+// ErrArgument errors occur when an argument supplied to a package function
+// fails to fall within acceptable values.  For example, the Reboot() function
+// expects the "how" parameter to be one of HardReboot or SoftReboot.  These
+// constants are (currently) strings, leading someone to wonder if they can pass
+// other string values instead, perhaps in an effort to break the API of their
+// provider.  Reboot() returns this error in this situation.
+//
+// Function identifies which function was called/which function is generating
+// the error.
+// Argument identifies which formal argument was responsible for producing the
+// error.
+// Value provides the value as it was passed into the function.
+type ErrArgument struct {
+	Function, Argument string
+	Value              interface{}
+}
+
+// Error yields a useful diagnostic for debugging purposes.
+func (e *ErrArgument) Error() string {
+	return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value)
+}
+
+func (e *ErrArgument) String() string {
+	return e.Error()
+}
+
+// RebootMethod describes the mechanisms by which a server reboot can be requested.
+type RebootMethod string
+
+// These constants determine how a server should be rebooted.
+// See the Reboot() function for further details.
+const (
+	SoftReboot RebootMethod = "SOFT"
+	HardReboot RebootMethod = "HARD"
+	OSReboot                = SoftReboot
+	PowerCycle              = HardReboot
+)
+
+// Reboot requests that a given server reboot.
+// Two methods exist for rebooting a server:
+//
+// HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the machine, or if a VM,
+// terminating it at the hypervisor level.
+// It's done. Caput. Full stop.
+// Then, after a brief while, power is restored or the VM instance restarted.
+//
+// SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures.
+// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, how RebootMethod) ActionResult {
+	var res ActionResult
+
+	if (how != SoftReboot) && (how != HardReboot) {
+		res.Err = &ErrArgument{
+			Function: "Reboot",
+			Argument: "how",
+			Value:    how,
+		}
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody: struct {
+			C map[string]string `json:"reboot"`
+		}{
+			map[string]string{"type": string(how)},
+		},
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return res
+}
+
+// RebuildOptsBuilder is an interface that allows extensions to override the
+// default behaviour of rebuild options
+type RebuildOptsBuilder interface {
+	ToServerRebuildMap() (map[string]interface{}, error)
+}
+
+// RebuildOpts represents the configuration options used in a server rebuild
+// operation
+type RebuildOpts struct {
+	// Required. The ID of the image you want your server to be provisioned on
+	ImageID string
+
+	// Name to set the server to
+	Name string
+
+	// Required. The server's admin password
+	AdminPass string
+
+	// AccessIPv4 [optional] provides a new IPv4 address for the instance.
+	AccessIPv4 string
+
+	// AccessIPv6 [optional] provides a new IPv6 address for the instance.
+	AccessIPv6 string
+
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string
+
+	// Personality [optional] includes the path and contents of a file to inject into the server at launch.
+	// The maximum size of the file is 255 bytes (decoded).
+	Personality []byte
+}
+
+// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
+func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
+	var err error
+	server := make(map[string]interface{})
+
+	if opts.AdminPass == "" {
+		err = fmt.Errorf("AdminPass is required")
+	}
+
+	if opts.ImageID == "" {
+		err = fmt.Errorf("ImageID is required")
+	}
+
+	if err != nil {
+		return server, err
+	}
+
+	server["name"] = opts.Name
+	server["adminPass"] = opts.AdminPass
+	server["imageRef"] = opts.ImageID
+
+	if opts.AccessIPv4 != "" {
+		server["accessIPv4"] = opts.AccessIPv4
+	}
+
+	if opts.AccessIPv6 != "" {
+		server["accessIPv6"] = opts.AccessIPv6
+	}
+
+	if opts.Metadata != nil {
+		server["metadata"] = opts.Metadata
+	}
+
+	if opts.Personality != nil {
+		encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+		server["personality"] = &encoded
+	}
+
+	return map[string]interface{}{"rebuild": server}, nil
+}
+
+// Rebuild will reprovision the server according to the configuration options
+// provided in the RebuildOpts struct.
+func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) RebuildResult {
+	var result RebuildResult
+
+	if id == "" {
+		result.Err = fmt.Errorf("ID is required")
+		return result
+	}
+
+	reqBody, err := opts.ToServerRebuildMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody:     &reqBody,
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return result
+}
+
+// ResizeOptsBuilder is an interface that allows extensions to override the default structure of
+// a Resize request.
+type ResizeOptsBuilder interface {
+	ToServerResizeMap() (map[string]interface{}, error)
+}
+
+// ResizeOpts represents the configuration options used to control a Resize operation.
+type ResizeOpts struct {
+	// FlavorRef is the ID of the flavor you wish your server to become.
+	FlavorRef string
+}
+
+// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body to the
+// Resize request.
+func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
+	resize := map[string]interface{}{
+		"flavorRef": opts.FlavorRef,
+	}
+
+	return map[string]interface{}{"resize": resize}, nil
+}
+
+// Resize instructs the provider to change the flavor of the server.
+// Note that this implies rebuilding it.
+// Unfortunately, one cannot pass rebuild parameters to the resize function.
+// When the resize completes, the server will be in RESIZE_VERIFY state.
+// While in this state, you can explore the use of the new server's configuration.
+// If you like it, call ConfirmResize() to commit the resize permanently.
+// Otherwise, call RevertResize() to restore the old configuration.
+func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult {
+	var res ActionResult
+	reqBody, err := opts.ToServerResizeMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody:     reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return res
+}
+
+// ConfirmResize confirms a previous resize operation on a server.
+// See Resize() for more details.
+func ConfirmResize(client *gophercloud.ServiceClient, id string) ActionResult {
+	var res ActionResult
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody:     map[string]interface{}{"confirmResize": nil},
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return res
+}
+
+// RevertResize cancels a previous resize operation on a server.
+// See Resize() for more details.
+func RevertResize(client *gophercloud.ServiceClient, id string) ActionResult {
+	var res ActionResult
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody:     map[string]interface{}{"revertResize": nil},
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return res
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..23fe781
--- /dev/null
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -0,0 +1,176 @@
+package servers
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListServers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerListSuccessfully(t)
+
+	pages := 0
+	err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractServers(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 servers, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, ServerHerp, actual[0])
+		th.CheckDeepEquals(t, ServerDerp, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestCreateServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationSuccessfully(t, SingleServerBody)
+
+	actual, err := Create(client.ServiceClient(), CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerDeletionSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, err)
+}
+
+func TestGetServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerGetSuccessfully(t)
+
+	client := client.ServiceClient()
+	actual, err := Get(client, "1234asdf").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestUpdateServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerUpdateSuccessfully(t)
+
+	client := client.ServiceClient()
+	actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestChangeServerAdminPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAdminPasswordChangeSuccessfully(t)
+
+	res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRebootServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRebootSuccessfully(t)
+
+	res := Reboot(client.ServiceClient(), "1234asdf", SoftReboot)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRebuildServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRebuildSuccessfully(t, SingleServerBody)
+
+	opts := RebuildOpts{
+		Name:       "new-name",
+		AdminPass:  "swordfish",
+		ImageID:    "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+		AccessIPv4: "1.2.3.4",
+	}
+
+	actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestResizeServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	res := Resize(client.ServiceClient(), "1234asdf", ResizeOpts{FlavorRef: "2"})
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestConfirmResize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := ConfirmResize(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRevertResize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "revertResize": null }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	res := RevertResize(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..74a221f
--- /dev/null
+++ b/openstack/compute/v2/servers/results.go
@@ -0,0 +1,151 @@
+package servers
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type serverResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any serverResult as a Server, if possible.
+func (r serverResult) Extract() (*Server, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Server Server `mapstructure:"server"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return &response.Server, err
+}
+
+// CreateResult temporarily contains the response from a Create call.
+type CreateResult struct {
+	serverResult
+}
+
+// GetResult temporarily contains the response from a Get call.
+type GetResult struct {
+	serverResult
+}
+
+// UpdateResult temporarily contains the response from an Update call.
+type UpdateResult struct {
+	serverResult
+}
+
+// RebuildResult temporarily contains the response from a Rebuild call.
+type RebuildResult struct {
+	serverResult
+}
+
+// ActionResult represents the result of server action operations, like reboot
+type ActionResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts error information from a result
+func (r ActionResult) Extract() error {
+	return r.Err
+}
+
+// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
+type Server struct {
+	// ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
+	ID string
+
+	// TenantID identifies the tenant owning this server resource.
+	TenantID string `mapstructure:"tenant_id"`
+
+	// UserID uniquely identifies the user account owning the tenant.
+	UserID string `mapstructure:"user_id"`
+
+	// Name contains the human-readable name for the server.
+	Name string
+
+	// Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
+	Updated string
+	Created string
+
+	HostID string
+
+	// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
+	Status string
+
+	// Progress ranges from 0..100.
+	// A request made against the server completes only once Progress reaches 100.
+	Progress int
+
+	// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
+	AccessIPv4 string
+	AccessIPv6 string
+
+	// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
+	Image map[string]interface{}
+
+	// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
+	Flavor map[string]interface{}
+
+	// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
+	Addresses map[string]interface{}
+
+	// Metadata includes a list of all user-specified key-value pairs attached to the server.
+	Metadata map[string]interface{}
+
+	// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+	Links []interface{}
+
+	// KeyName indicates which public key was injected into the server on launch.
+	KeyName string `json:"key_name" mapstructure:"key_name"`
+
+	// AdminPass will generally be empty ("").  However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place.
+	// Note that this is the ONLY time this field will be valid.
+	AdminPass string `json:"adminPass" mapstructure:"adminPass"`
+}
+
+// ServerPage abstracts the raw results of making a List() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractServers call.
+type ServerPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (page ServerPage) IsEmpty() (bool, error) {
+	servers, err := ExtractServers(page)
+	if err != nil {
+		return true, err
+	}
+	return len(servers) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ServerPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"servers_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]Server, error) {
+	casted := page.(ServerPage).Body
+
+	var response struct {
+		Servers []Server `mapstructure:"servers"`
+	}
+	err := mapstructure.Decode(casted, &response)
+	return response.Servers, err
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..57587ab
--- /dev/null
+++ b/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,31 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("servers")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return createURL(client)
+}
+
+func listDetailURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("servers", "detail")
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return deleteURL(client, id)
+}
+
+func updateURL(client *gophercloud.ServiceClient, id string) string {
+	return deleteURL(client, id)
+}
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "action")
+}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
new file mode 100644
index 0000000..cc895c9
--- /dev/null
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -0,0 +1,56 @@
+package servers
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "servers"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "servers"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestListDetailURL(t *testing.T) {
+	actual := listDetailURL(endpointClient())
+	expected := endpoint + "servers/detail"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestActionURL(t *testing.T) {
+	actual := actionURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo/action"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/util.go b/openstack/compute/v2/servers/util.go
new file mode 100644
index 0000000..e6baf74
--- /dev/null
+++ b/openstack/compute/v2/servers/util.go
@@ -0,0 +1,20 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+// WaitForStatus will continually poll a server until it successfully transitions to a specified
+// status. It will do this for at most the number of seconds specified.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/openstack/compute/v2/servers/util_test.go b/openstack/compute/v2/servers/util_test.go
new file mode 100644
index 0000000..e192ae3
--- /dev/null
+++ b/openstack/compute/v2/servers/util_test.go
@@ -0,0 +1,38 @@
+package servers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestWaitForStatus(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/4321", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(2 * time.Second)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+		{
+			"server": {
+				"name": "the-server",
+				"id": "4321",
+				"status": "ACTIVE"
+			}
+		}`)
+	})
+
+	err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0)
+	if err == nil {
+		t.Errorf("Expected error: 'Time Out in WaitFor'")
+	}
+
+	err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3)
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
new file mode 100644
index 0000000..5a311e4
--- /dev/null
+++ b/openstack/endpoint_location.go
@@ -0,0 +1,124 @@
+package openstack
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
+	services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired
+// during the v2 identity service. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
+	// Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
+	var endpoints = make([]tokens2.Endpoint, 0, 1)
+	for _, entry := range catalog.Entries {
+		if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
+			for _, endpoint := range entry.Endpoints {
+				if opts.Region == "" || endpoint.Region == opts.Region {
+					endpoints = append(endpoints, endpoint)
+				}
+			}
+		}
+	}
+
+	// Report an error if the options were ambiguous.
+	if len(endpoints) > 1 {
+		return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+	}
+
+	// Extract the appropriate URL from the matching Endpoint.
+	for _, endpoint := range endpoints {
+		switch opts.Availability {
+		case gophercloud.AvailabilityPublic:
+			return gophercloud.NormalizeURL(endpoint.PublicURL), nil
+		case gophercloud.AvailabilityInternal:
+			return gophercloud.NormalizeURL(endpoint.InternalURL), nil
+		case gophercloud.AvailabilityAdmin:
+			return gophercloud.NormalizeURL(endpoint.AdminURL), nil
+		default:
+			return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
+		}
+	}
+
+	// Report an error if there were no matching endpoints.
+	return "", gophercloud.ErrEndpointNotFound
+}
+
+// V3EndpointURL discovers the endpoint URL for a specific service using multiple calls against
+// an identity v3 service endpoint. The specified EndpointOpts are used to identify a unique,
+// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided
+// criteria and when none do. The minimum that can be specified is a Type, but you will also often
+// need to specify a Name and/or a Region depending on what's available on your OpenStack
+// deployment.
+func V3EndpointURL(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
+	// Discover the service we're interested in.
+	var services = make([]services3.Service, 0, 1)
+	servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
+	err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
+		part, err := services3.ExtractServices(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, service := range part {
+			if service.Name == opts.Name {
+				services = append(services, service)
+			}
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		return "", err
+	}
+
+	if len(services) == 0 {
+		return "", gophercloud.ErrServiceNotFound
+	}
+	if len(services) > 1 {
+		return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
+	}
+	service := services[0]
+
+	// Enumerate the endpoints available for this service.
+	var endpoints []endpoints3.Endpoint
+	endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
+		Availability: opts.Availability,
+		ServiceID:    service.ID,
+	})
+	err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
+		part, err := endpoints3.ExtractEndpoints(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, endpoint := range part {
+			if opts.Region == "" || endpoint.Region == opts.Region {
+				endpoints = append(endpoints, endpoint)
+			}
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		return "", err
+	}
+
+	if len(endpoints) == 0 {
+		return "", gophercloud.ErrEndpointNotFound
+	}
+	if len(endpoints) > 1 {
+		return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
+	}
+	endpoint := endpoints[0]
+
+	return gophercloud.NormalizeURL(endpoint.URL), nil
+}
diff --git a/openstack/endpoint_location_test.go b/openstack/endpoint_location_test.go
new file mode 100644
index 0000000..4e0569a
--- /dev/null
+++ b/openstack/endpoint_location_test.go
@@ -0,0 +1,225 @@
+package openstack
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// Service catalog fixtures take too much vertical space!
+var catalog2 = tokens2.ServiceCatalog{
+	Entries: []tokens2.CatalogEntry{
+		tokens2.CatalogEntry{
+			Type: "same",
+			Name: "same",
+			Endpoints: []tokens2.Endpoint{
+				tokens2.Endpoint{
+					Region:      "same",
+					PublicURL:   "https://public.correct.com/",
+					InternalURL: "https://internal.correct.com/",
+					AdminURL:    "https://admin.correct.com/",
+				},
+				tokens2.Endpoint{
+					Region:    "different",
+					PublicURL: "https://badregion.com/",
+				},
+			},
+		},
+		tokens2.CatalogEntry{
+			Type: "same",
+			Name: "different",
+			Endpoints: []tokens2.Endpoint{
+				tokens2.Endpoint{
+					Region:    "same",
+					PublicURL: "https://badname.com/",
+				},
+				tokens2.Endpoint{
+					Region:    "different",
+					PublicURL: "https://badname.com/+badregion",
+				},
+			},
+		},
+		tokens2.CatalogEntry{
+			Type: "different",
+			Name: "different",
+			Endpoints: []tokens2.Endpoint{
+				tokens2.Endpoint{
+					Region:    "same",
+					PublicURL: "https://badtype.com/+badname",
+				},
+				tokens2.Endpoint{
+					Region:    "different",
+					PublicURL: "https://badtype.com/+badregion+badname",
+				},
+			},
+		},
+	},
+}
+
+func TestV2EndpointExact(t *testing.T) {
+	expectedURLs := map[gophercloud.Availability]string{
+		gophercloud.AvailabilityPublic:   "https://public.correct.com/",
+		gophercloud.AvailabilityAdmin:    "https://admin.correct.com/",
+		gophercloud.AvailabilityInternal: "https://internal.correct.com/",
+	}
+
+	for availability, expected := range expectedURLs {
+		actual, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+			Type:         "same",
+			Name:         "same",
+			Region:       "same",
+			Availability: availability,
+		})
+		th.AssertNoErr(t, err)
+		th.CheckEquals(t, expected, actual)
+	}
+}
+
+func TestV2EndpointNone(t *testing.T) {
+	_, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "nope",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err)
+}
+
+func TestV2EndpointMultiple(t *testing.T) {
+	_, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "same",
+		Region:       "same",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	if !strings.HasPrefix(err.Error(), "Discovered 2 matching endpoints:") {
+		t.Errorf("Received unexpected error: %v", err)
+	}
+}
+
+func TestV2EndpointBadAvailability(t *testing.T) {
+	_, err := V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "same",
+		Name:         "same",
+		Region:       "same",
+		Availability: "wat",
+	})
+	th.CheckEquals(t, err.Error(), "Unexpected availability in endpoint query: wat")
+}
+
+func setupV3Responses(t *testing.T) {
+	// Mock the service query.
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"links": {
+					"next": null,
+					"previous": null
+				},
+				"services": [
+					{
+						"description": "Correct",
+						"id": "1234",
+						"name": "same",
+						"type": "same"
+					},
+					{
+						"description": "Bad Name",
+						"id": "9876",
+						"name": "different",
+						"type": "same"
+					}
+				]
+			}
+		`)
+	})
+
+	// Mock the endpoint query.
+	th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestFormValues(t, r, map[string]string{
+			"service_id": "1234",
+			"interface":  "public",
+		})
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"endpoints": [
+					{
+						"id": "12",
+						"interface": "public",
+						"name": "the-right-one",
+						"region": "same",
+						"service_id": "1234",
+						"url": "https://correct:9000/"
+					},
+					{
+						"id": "14",
+						"interface": "public",
+						"name": "bad-region",
+						"region": "different",
+						"service_id": "1234",
+						"url": "https://bad-region:9001/"
+					}
+				],
+				"links": {
+					"next": null,
+					"previous": null
+				}
+			}
+    `)
+	})
+}
+
+func TestV3EndpointExact(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	setupV3Responses(t)
+
+	actual, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+		Type:         "same",
+		Name:         "same",
+		Region:       "same",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, actual, "https://correct:9000/")
+}
+
+func TestV3EndpointNoService(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+      {
+        "links": {
+          "next": null,
+          "previous": null
+        },
+        "services": []
+      }
+    `)
+	})
+
+	_, err := V3EndpointURL(fake.ServiceClient(), gophercloud.EndpointOpts{
+		Type:         "nope",
+		Name:         "same",
+		Region:       "same",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.CheckEquals(t, gophercloud.ErrServiceNotFound, err)
+}
diff --git a/openstack/identity/v2/extensions/delegate.go b/openstack/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..fd6e80e
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,52 @@
+package extensions
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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) ([]common.Extension, error) {
+	// Identity v2 adds an intermediate "values" object.
+
+	var resp struct {
+		Extensions struct {
+			Values []common.Extension `mapstructure:"values"`
+		} `mapstructure:"extensions"`
+	}
+
+	err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
+	return resp.Extensions.Values, err
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+	return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return common.List(c).WithPageCreator(func(r pagination.PageResult) pagination.Page {
+		return ExtensionPage{
+			ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
+		}
+	})
+}
diff --git a/openstack/identity/v2/extensions/delegate_test.go b/openstack/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..504118a
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,38 @@
+package extensions
+
+import (
+	"testing"
+
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListExtensionsSuccessfully(t)
+
+	count := 0
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, common.ExpectedExtensions, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	common.HandleGetExtensionSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/openstack/identity/v2/extensions/doc.go b/openstack/identity/v2/extensions/doc.go
new file mode 100644
index 0000000..791e4e3
--- /dev/null
+++ b/openstack/identity/v2/extensions/doc.go
@@ -0,0 +1,3 @@
+// Package extensions provides information and interaction with the
+// different extensions available for the OpenStack Identity service.
+package extensions
diff --git a/openstack/identity/v2/extensions/fixtures.go b/openstack/identity/v2/extensions/fixtures.go
new file mode 100644
index 0000000..96cb7d2
--- /dev/null
+++ b/openstack/identity/v2/extensions/fixtures.go
@@ -0,0 +1,60 @@
+// +build fixtures
+
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single Extension result. It differs from the delegated implementation
+// by the introduction of an intermediate "values" member.
+const ListOutput = `
+{
+	"extensions": {
+		"values": [
+			{
+				"updated": "2013-01-20T00:00:00-00:00",
+				"name": "Neutron Service Type Management",
+				"links": [],
+				"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+				"alias": "service-type",
+				"description": "API for retrieving service providers for Neutron advanced services"
+			}
+		]
+	}
+}
+`
+
+// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
+// call.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+
+		fmt.Fprintf(w, `
+{
+  "extensions": {
+    "values": [
+      {
+        "updated": "2013-01-20T00:00:00-00:00",
+        "name": "Neutron Service Type Management",
+        "links": [],
+        "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+        "alias": "service-type",
+        "description": "API for retrieving service providers for Neutron advanced services"
+      }
+    ]
+  }
+}
+    `)
+	})
+
+}
diff --git a/openstack/identity/v2/tenants/doc.go b/openstack/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..0c2d49d
--- /dev/null
+++ b/openstack/identity/v2/tenants/doc.go
@@ -0,0 +1,7 @@
+// Package tenants provides information and interaction with the
+// tenants API resource for the OpenStack Identity service.
+//
+// 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
+// for more information.
+package tenants
diff --git a/openstack/identity/v2/tenants/fixtures.go b/openstack/identity/v2/tenants/fixtures.go
new file mode 100644
index 0000000..7f044ac
--- /dev/null
+++ b/openstack/identity/v2/tenants/fixtures.go
@@ -0,0 +1,65 @@
+// +build fixtures
+
+package tenants
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Tenant results.
+const ListOutput = `
+{
+	"tenants": [
+		{
+			"id": "1234",
+			"name": "Red Team",
+			"description": "The team that is red",
+			"enabled": true
+		},
+		{
+			"id": "9876",
+			"name": "Blue Team",
+			"description": "The team that is blue",
+			"enabled": false
+		}
+	]
+}
+`
+
+// RedTeam is a Tenant fixture.
+var RedTeam = Tenant{
+	ID:          "1234",
+	Name:        "Red Team",
+	Description: "The team that is red",
+	Enabled:     true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = Tenant{
+	ID:          "9876",
+	Name:        "Blue Team",
+	Description: "The team that is blue",
+	Enabled:     false,
+}
+
+// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
+var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
+
+// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
+// responds with a list of two tenants.
+func HandleListTenantsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, ListOutput)
+	})
+}
diff --git a/openstack/identity/v2/tenants/requests.go b/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..5a359f5
--- /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.PageResult) pagination.Page {
+		return TenantPage{pagination.LinkedPageBase{PageResult: 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..e8f172d
--- /dev/null
+++ b/openstack/identity/v2/tenants/requests_test.go
@@ -0,0 +1,29 @@
+package tenants
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListTenantsSuccessfully(t)
+
+	count := 0
+	err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+
+		actual, err := ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ExpectedTenantSlice, 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..c1220c3
--- /dev/null
+++ b/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,62 @@
+package tenants
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"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 resp struct {
+		Links []gophercloud.Link `mapstructure:"tenants_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// 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/tokens/doc.go b/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..31cacc5
--- /dev/null
+++ b/openstack/identity/v2/tokens/doc.go
@@ -0,0 +1,5 @@
+// Package tokens provides information and interaction with the token API
+// resource for the OpenStack Identity service.
+// For more information, see:
+// 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..3a9172e
--- /dev/null
+++ b/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,30 @@
+package tokens
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	// ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+	ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+	// ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
+	ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+	// ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
+	ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
+
+	// 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.")
+
+	// ErrPasswordRequired is returned if you don't provide a password.
+	ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
+)
+
+func unacceptedAttributeErr(attribute string) error {
+	return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
+}
diff --git a/openstack/identity/v2/tokens/fixtures.go b/openstack/identity/v2/tokens/fixtures.go
new file mode 100644
index 0000000..1cb0d05
--- /dev/null
+++ b/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+
+package tokens
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &Token{
+	ID:        "aaaabbbbccccdddd",
+	ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
+	Tenant: tenants.Tenant{
+		ID:          "fc394f2ab2df4114bde39905f800dc57",
+		Name:        "test",
+		Description: "There are many tenants. This one is yours.",
+		Enabled:     true,
+	},
+}
+
+// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
+var ExpectedServiceCatalog = &ServiceCatalog{
+	Entries: []CatalogEntry{
+		CatalogEntry{
+			Name: "inscrutablewalrus",
+			Type: "something",
+			Endpoints: []Endpoint{
+				Endpoint{
+					PublicURL: "http://something0:1234/v2/",
+					Region:    "region0",
+				},
+				Endpoint{
+					PublicURL: "http://something1:1234/v2/",
+					Region:    "region1",
+				},
+			},
+		},
+		CatalogEntry{
+			Name: "arbitrarypenguin",
+			Type: "else",
+			Endpoints: []Endpoint{
+				Endpoint{
+					PublicURL: "http://else0:4321/v3/",
+					Region:    "region0",
+				},
+			},
+		},
+	},
+}
+
+// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
+const TokenCreationResponse = `
+{
+	"access": {
+		"token": {
+			"issued_at": "2014-01-30T15:30:58.000000Z",
+			"expires": "2014-01-31T15:30:58Z",
+			"id": "aaaabbbbccccdddd",
+			"tenant": {
+				"description": "There are many tenants. This one is yours.",
+				"enabled": true,
+				"id": "fc394f2ab2df4114bde39905f800dc57",
+				"name": "test"
+			}
+		},
+		"serviceCatalog": [
+			{
+				"endpoints": [
+					{
+						"publicURL": "http://something0:1234/v2/",
+						"region": "region0"
+					},
+					{
+						"publicURL": "http://something1:1234/v2/",
+						"region": "region1"
+					}
+				],
+				"type": "something",
+				"name": "inscrutablewalrus"
+			},
+			{
+				"endpoints": [
+					{
+						"publicURL": "http://else0:4321/v3/",
+						"region": "region0"
+					}
+				],
+				"type": "else",
+				"name": "arbitrarypenguin"
+			}
+		]
+	}
+}
+`
+
+// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenPost(t *testing.T, requestJSON string) {
+	th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		if requestJSON != "" {
+			th.TestJSONRequest(t, r, requestJSON)
+		}
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, TokenCreationResponse)
+	})
+}
+
+// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
+// service catalog.
+func IsSuccessful(t *testing.T, result CreateResult) {
+	token, err := result.ExtractToken()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedToken, token)
+
+	serviceCatalog, err := result.ExtractServiceCatalog()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..87c923a
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,87 @@
+package tokens
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+
+	// ToTokenCreateMap assembles the Create request body, returning an error if parameters are
+	// missing or inconsistent.
+	ToTokenCreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptions struct {
+	gophercloud.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+	return AuthOptions{AuthOptions: original}
+}
+
+// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
+// request.
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+	// Error out if an unsupported auth option is present.
+	if auth.UserID != "" {
+		return nil, ErrUserIDProvided
+	}
+	if auth.APIKey != "" {
+		return nil, ErrAPIKeyProvided
+	}
+	if auth.DomainID != "" {
+		return nil, ErrDomainIDProvided
+	}
+	if auth.DomainName != "" {
+		return nil, ErrDomainNameProvided
+	}
+
+	// Username and Password are always required.
+	if auth.Username == "" {
+		return nil, ErrUsernameRequired
+	}
+	if auth.Password == "" {
+		return nil, ErrPasswordRequired
+	}
+
+	// Populate the request map.
+	authMap := make(map[string]interface{})
+
+	authMap["passwordCredentials"] = map[string]interface{}{
+		"username": auth.Username,
+		"password": auth.Password,
+	}
+
+	if auth.TenantID != "" {
+		authMap["tenantId"] = auth.TenantID
+	}
+	if auth.TenantName != "" {
+		authMap["tenantName"] = auth.TenantName
+	}
+
+	return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to the identity service and attempts to acquire a Token.
+// If successful, the CreateResult
+// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
+// which abstracts all of the gory details about navigating service catalogs and such.
+func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
+	request, err := auth.ToTokenCreateMap()
+	if err != nil {
+		return CreateResult{gophercloud.Result{Err: err}}
+	}
+
+	var result CreateResult
+	_, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{
+		ReqBody: &request,
+		Results: &result.Body,
+		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..2f02825
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,140 @@
+package tokens
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleTokenPost(t, requestJSON)
+
+	return Create(client.ServiceClient(), AuthOptions{options})
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleTokenPost(t, "")
+
+	actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
+	th.CheckEquals(t, expectedErr, actualErr)
+}
+
+func TestCreateWithPassword(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		Password: "swordfish",
+	}
+
+	IsSuccessful(t, tokenPost(t, options, `
+    {
+      "auth": {
+        "passwordCredentials": {
+          "username": "me",
+          "password": "swordfish"
+        }
+      }
+    }
+  `))
+}
+
+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 TestProhibitAPIKey(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		Password: "thing",
+		APIKey:   "123412341234",
+	}
+
+	tokenPostErr(t, options, ErrAPIKeyProvided)
+}
+
+func TestProhibitDomainID(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		Password: "thing",
+		DomainID: "1234",
+	}
+
+	tokenPostErr(t, options, ErrDomainIDProvided)
+}
+
+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 TestRequirePassword(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+	}
+
+	tokenPostErr(t, options, ErrPasswordRequired)
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..1eddb9d
--- /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.Result
+}
+
+// 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.Body, &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.Body, &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.Result{Err: err}}
+}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..cd4c696
--- /dev/null
+++ b/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,8 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+// CreateURL generates the URL used to create new Tokens.
+func CreateURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("tokens")
+}
diff --git a/openstack/identity/v3/endpoints/doc.go b/openstack/identity/v3/endpoints/doc.go
new file mode 100644
index 0000000..8516394
--- /dev/null
+++ b/openstack/identity/v3/endpoints/doc.go
@@ -0,0 +1,6 @@
+// Package endpoints provides information and interaction with the service
+// endpoints API resource in the OpenStack Identity service.
+//
+// For more information, see:
+// http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3
+package endpoints
diff --git a/openstack/identity/v3/endpoints/errors.go b/openstack/identity/v3/endpoints/errors.go
new file mode 100644
index 0000000..854957f
--- /dev/null
+++ b/openstack/identity/v3/endpoints/errors.go
@@ -0,0 +1,21 @@
+package endpoints
+
+import "fmt"
+
+func requiredAttribute(attribute string) error {
+	return fmt.Errorf("You must specify %s for this endpoint.", attribute)
+}
+
+var (
+	// ErrAvailabilityRequired is reported if an Endpoint is created without an Availability.
+	ErrAvailabilityRequired = requiredAttribute("an availability")
+
+	// ErrNameRequired is reported if an Endpoint is created without a Name.
+	ErrNameRequired = requiredAttribute("a name")
+
+	// ErrURLRequired is reported if an Endpoint is created without a URL.
+	ErrURLRequired = requiredAttribute("a URL")
+
+	// ErrServiceIDRequired is reported if an Endpoint is created without a ServiceID.
+	ErrServiceIDRequired = requiredAttribute("a serviceID")
+)
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
new file mode 100644
index 0000000..4bec427
--- /dev/null
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -0,0 +1,143 @@
+package endpoints
+
+import (
+	"strconv"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
+type EndpointOpts struct {
+	Availability gophercloud.Availability
+	Name         string
+	Region       string
+	URL          string
+	ServiceID    string
+}
+
+// Create inserts a new Endpoint into the service catalog.
+// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
+func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
+	// Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
+	type endpoint struct {
+		Interface string  `json:"interface"`
+		Name      string  `json:"name"`
+		Region    *string `json:"region,omitempty"`
+		URL       string  `json:"url"`
+		ServiceID string  `json:"service_id"`
+	}
+
+	type request struct {
+		Endpoint endpoint `json:"endpoint"`
+	}
+
+	// Ensure that EndpointOpts is fully populated.
+	if opts.Availability == "" {
+		return createErr(ErrAvailabilityRequired)
+	}
+	if opts.Name == "" {
+		return createErr(ErrNameRequired)
+	}
+	if opts.URL == "" {
+		return createErr(ErrURLRequired)
+	}
+	if opts.ServiceID == "" {
+		return createErr(ErrServiceIDRequired)
+	}
+
+	// Populate the request body.
+	reqBody := request{
+		Endpoint: endpoint{
+			Interface: string(opts.Availability),
+			Name:      opts.Name,
+			URL:       opts.URL,
+			ServiceID: opts.ServiceID,
+		},
+	}
+	reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+
+	var result CreateResult
+	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &result.Body,
+		OkCodes:     []int{201},
+	})
+	return result
+}
+
+// ListOpts allows finer control over the the endpoints returned by a List call.
+// All fields are optional.
+type ListOpts struct {
+	Availability gophercloud.Availability
+	ServiceID    string
+	Page         int
+	PerPage      int
+}
+
+// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q := make(map[string]string)
+	if opts.Availability != "" {
+		q["interface"] = string(opts.Availability)
+	}
+	if opts.ServiceID != "" {
+		q["service_id"] = opts.ServiceID
+	}
+	if opts.Page != 0 {
+		q["page"] = strconv.Itoa(opts.Page)
+	}
+	if opts.PerPage != 0 {
+		q["per_page"] = strconv.Itoa(opts.Page)
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return EndpointPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	u := listURL(client) + gophercloud.BuildQuery(q)
+	return pagination.NewPager(client, u, createPage)
+}
+
+// Update changes an existing endpoint with new data.
+// All fields are optional in the provided EndpointOpts.
+func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
+	type endpoint struct {
+		Interface *string `json:"interface,omitempty"`
+		Name      *string `json:"name,omitempty"`
+		Region    *string `json:"region,omitempty"`
+		URL       *string `json:"url,omitempty"`
+		ServiceID *string `json:"service_id,omitempty"`
+	}
+
+	type request struct {
+		Endpoint endpoint `json:"endpoint"`
+	}
+
+	reqBody := request{Endpoint: endpoint{}}
+	reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
+	reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
+	reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+	reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
+	reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
+
+	var result UpdateResult
+	_, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &result.Body,
+		OkCodes:     []int{200},
+	})
+	return result
+}
+
+// Delete removes an endpoint from the service catalog.
+func Delete(client *gophercloud.ServiceClient, endpointID string) error {
+	_, err := perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return err
+}
diff --git a/openstack/identity/v3/endpoints/requests_test.go b/openstack/identity/v3/endpoints/requests_test.go
new file mode 100644
index 0000000..381461c
--- /dev/null
+++ b/openstack/identity/v3/endpoints/requests_test.go
@@ -0,0 +1,228 @@
+package endpoints
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		testhelper.TestJSONRequest(t, r, `
+      {
+        "endpoint": {
+          "interface": "public",
+          "name": "the-endiest-of-points",
+          "region": "underground",
+          "url": "https://1.2.3.4:9000/",
+          "service_id": "asdfasdfasdfasdf"
+        }
+      }
+    `)
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `
+      {
+        "endpoint": {
+          "id": "12",
+          "interface": "public",
+          "links": {
+            "self": "https://localhost:5000/v3/endpoints/12"
+          },
+          "name": "the-endiest-of-points",
+          "region": "underground",
+          "service_id": "asdfasdfasdfasdf",
+          "url": "https://1.2.3.4:9000/"
+        }
+      }
+    `)
+	})
+
+	actual, err := Create(client.ServiceClient(), EndpointOpts{
+		Availability: gophercloud.AvailabilityPublic,
+		Name:         "the-endiest-of-points",
+		Region:       "underground",
+		URL:          "https://1.2.3.4:9000/",
+		ServiceID:    "asdfasdfasdfasdf",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create an endpoint: %v", err)
+	}
+
+	expected := &Endpoint{
+		ID:           "12",
+		Availability: gophercloud.AvailabilityPublic,
+		Name:         "the-endiest-of-points",
+		Region:       "underground",
+		ServiceID:    "asdfasdfasdfasdf",
+		URL:          "https://1.2.3.4:9000/",
+	}
+
+	if !reflect.DeepEqual(actual, expected) {
+		t.Errorf("Expected %#v, was %#v", expected, actual)
+	}
+}
+
+func TestListEndpoints(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"endpoints": [
+					{
+						"id": "12",
+						"interface": "public",
+						"links": {
+							"self": "https://localhost:5000/v3/endpoints/12"
+						},
+						"name": "the-endiest-of-points",
+						"region": "underground",
+						"service_id": "asdfasdfasdfasdf",
+						"url": "https://1.2.3.4:9000/"
+					},
+					{
+						"id": "13",
+						"interface": "internal",
+						"links": {
+							"self": "https://localhost:5000/v3/endpoints/13"
+						},
+						"name": "shhhh",
+						"region": "underground",
+						"service_id": "asdfasdfasdfasdf",
+						"url": "https://1.2.3.4:9001/"
+					}
+				],
+				"links": {
+					"next": null,
+					"previous": null
+				}
+			}
+		`)
+	})
+
+	count := 0
+	List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractEndpoints(page)
+		if err != nil {
+			t.Errorf("Failed to extract endpoints: %v", err)
+			return false, err
+		}
+
+		expected := []Endpoint{
+			Endpoint{
+				ID:           "12",
+				Availability: gophercloud.AvailabilityPublic,
+				Name:         "the-endiest-of-points",
+				Region:       "underground",
+				ServiceID:    "asdfasdfasdfasdf",
+				URL:          "https://1.2.3.4:9000/",
+			},
+			Endpoint{
+				ID:           "13",
+				Availability: gophercloud.AvailabilityInternal,
+				Name:         "shhhh",
+				Region:       "underground",
+				ServiceID:    "asdfasdfasdfasdf",
+				URL:          "https://1.2.3.4:9001/",
+			},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Expected %#v, got %#v", expected, actual)
+		}
+
+		return true, nil
+	})
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestUpdateEndpoint(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "PATCH")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		testhelper.TestJSONRequest(t, r, `
+		{
+	    "endpoint": {
+	      "name": "renamed",
+				"region": "somewhere-else"
+	    }
+		}
+	`)
+
+		fmt.Fprintf(w, `
+		{
+			"endpoint": {
+				"id": "12",
+				"interface": "public",
+				"links": {
+					"self": "https://localhost:5000/v3/endpoints/12"
+				},
+				"name": "renamed",
+				"region": "somewhere-else",
+				"service_id": "asdfasdfasdfasdf",
+				"url": "https://1.2.3.4:9000/"
+			}
+		}
+	`)
+	})
+
+	actual, err := Update(client.ServiceClient(), "12", EndpointOpts{
+		Name:   "renamed",
+		Region: "somewhere-else",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Update: %v", err)
+	}
+
+	expected := &Endpoint{
+		ID:           "12",
+		Availability: gophercloud.AvailabilityPublic,
+		Name:         "renamed",
+		Region:       "somewhere-else",
+		ServiceID:    "asdfasdfasdfasdf",
+		URL:          "https://1.2.3.4:9000/",
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, was %#v", expected, actual)
+	}
+}
+
+func TestDeleteEndpoint(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/endpoints/34", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "DELETE")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := Delete(client.ServiceClient(), "34")
+	if err != nil {
+		t.Fatalf("Unexpected error from Delete: %v", err)
+	}
+}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
new file mode 100644
index 0000000..f559b9a
--- /dev/null
+++ b/openstack/identity/v3/endpoints/results.go
@@ -0,0 +1,77 @@
+package endpoints
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Endpoint, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Endpoint `json:"endpoint"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res.Endpoint, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// createErr quickly wraps an error in a CreateResult.
+func createErr(err error) CreateResult {
+	return CreateResult{commonResult{gophercloud.Result{Err: err}}}
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+	commonResult
+}
+
+// Endpoint describes the entry point for another service's API.
+type Endpoint struct {
+	ID           string                   `mapstructure:"id" json:"id"`
+	Availability gophercloud.Availability `mapstructure:"interface" json:"interface"`
+	Name         string                   `mapstructure:"name" json:"name"`
+	Region       string                   `mapstructure:"region" json:"region"`
+	ServiceID    string                   `mapstructure:"service_id" json:"service_id"`
+	URL          string                   `mapstructure:"url" json:"url"`
+}
+
+// EndpointPage is a single page of Endpoint results.
+type EndpointPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if no Endpoints were returned.
+func (p EndpointPage) IsEmpty() (bool, error) {
+	es, err := ExtractEndpoints(p)
+	if err != nil {
+		return true, err
+	}
+	return len(es) == 0, nil
+}
+
+// ExtractEndpoints extracts an Endpoint slice from a Page.
+func ExtractEndpoints(page pagination.Page) ([]Endpoint, error) {
+	var response struct {
+		Endpoints []Endpoint `mapstructure:"endpoints"`
+	}
+
+	err := mapstructure.Decode(page.(EndpointPage).Body, &response)
+
+	return response.Endpoints, err
+}
diff --git a/openstack/identity/v3/endpoints/urls.go b/openstack/identity/v3/endpoints/urls.go
new file mode 100644
index 0000000..547d7b1
--- /dev/null
+++ b/openstack/identity/v3/endpoints/urls.go
@@ -0,0 +1,11 @@
+package endpoints
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("endpoints")
+}
+
+func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
+	return client.ServiceURL("endpoints", endpointID)
+}
diff --git a/openstack/identity/v3/endpoints/urls_test.go b/openstack/identity/v3/endpoints/urls_test.go
new file mode 100644
index 0000000..0b183b7
--- /dev/null
+++ b/openstack/identity/v3/endpoints/urls_test.go
@@ -0,0 +1,23 @@
+package endpoints
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func TestGetListURL(t *testing.T) {
+	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+	url := listURL(&client)
+	if url != "http://localhost:5000/v3/endpoints" {
+		t.Errorf("Unexpected list URL generated: [%s]", url)
+	}
+}
+
+func TestGetEndpointURL(t *testing.T) {
+	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+	url := endpointURL(&client, "1234")
+	if url != "http://localhost:5000/v3/endpoints/1234" {
+		t.Errorf("Unexpected service URL generated: [%s]", url)
+	}
+}
diff --git a/openstack/identity/v3/services/doc.go b/openstack/identity/v3/services/doc.go
new file mode 100644
index 0000000..fa56411
--- /dev/null
+++ b/openstack/identity/v3/services/doc.go
@@ -0,0 +1,3 @@
+// Package services provides information and interaction with the services API
+// resource for the OpenStack Identity service.
+package services
diff --git a/openstack/identity/v3/services/requests.go b/openstack/identity/v3/services/requests.go
new file mode 100644
index 0000000..425a67c
--- /dev/null
+++ b/openstack/identity/v3/services/requests.go
@@ -0,0 +1,98 @@
+package services
+
+import (
+	"strconv"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type response struct {
+	Service Service `json:"service"`
+}
+
+// Create adds a new service of the requested type to the catalog.
+func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
+	type request struct {
+		Type string `json:"type"`
+	}
+
+	req := request{Type: serviceType}
+
+	var result CreateResult
+	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     &req,
+		Results:     &result.Body,
+		OkCodes:     []int{201},
+	})
+	return result
+}
+
+// ListOpts allows you to query the List method.
+type ListOpts struct {
+	ServiceType string
+	PerPage     int
+	Page        int
+}
+
+// List enumerates the services available to a specific user.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q := make(map[string]string)
+	if opts.ServiceType != "" {
+		q["type"] = opts.ServiceType
+	}
+	if opts.Page != 0 {
+		q["page"] = strconv.Itoa(opts.Page)
+	}
+	if opts.PerPage != 0 {
+		q["perPage"] = strconv.Itoa(opts.PerPage)
+	}
+	u := listURL(client) + gophercloud.BuildQuery(q)
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return ServicePage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, u, createPage)
+}
+
+// Get returns additional information about a service, given its ID.
+func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
+	var result GetResult
+	_, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		Results:     &result.Body,
+		OkCodes:     []int{200},
+	})
+	return result
+}
+
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
+	type request struct {
+		Type string `json:"type"`
+	}
+
+	req := request{Type: serviceType}
+
+	var result UpdateResult
+	_, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     &req,
+		Results:     &result.Body,
+		OkCodes:     []int{200},
+	})
+	return result
+}
+
+// Delete removes an existing service.
+// It either deletes all associated endpoints, or fails until all endpoints are deleted.
+func Delete(client *gophercloud.ServiceClient, serviceID string) error {
+	_, err := perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return err
+}
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
new file mode 100644
index 0000000..337647c
--- /dev/null
+++ b/openstack/identity/v3/services/requests_test.go
@@ -0,0 +1,211 @@
+package services
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		testhelper.TestJSONRequest(t, r, `{ "type": "compute" }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `{
+        "service": {
+          "description": "Here's your service",
+          "id": "1234",
+          "name": "InscrutableOpenStackProjectName",
+          "type": "compute"
+        }
+    }`)
+	})
+
+	result, err := Create(client.ServiceClient(), "compute").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Create: %v", err)
+	}
+
+	if result.Description == nil || *result.Description != "Here's your service" {
+		t.Errorf("Service description was unexpected [%s]", result.Description)
+	}
+	if result.ID != "1234" {
+		t.Errorf("Service ID was unexpected [%s]", result.ID)
+	}
+	if result.Name != "InscrutableOpenStackProjectName" {
+		t.Errorf("Service name was unexpected [%s]", result.Name)
+	}
+	if result.Type != "compute" {
+		t.Errorf("Service type was unexpected [%s]", result.Type)
+	}
+}
+
+func TestListSinglePage(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"links": {
+					"next": null,
+					"previous": null
+				},
+				"services": [
+					{
+						"description": "Service One",
+						"id": "1234",
+						"name": "service-one",
+						"type": "identity"
+					},
+					{
+						"description": "Service Two",
+						"id": "9876",
+						"name": "service-two",
+						"type": "compute"
+					}
+				]
+			}
+		`)
+	})
+
+	count := 0
+	err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractServices(page)
+		if err != nil {
+			return false, err
+		}
+
+		desc0 := "Service One"
+		desc1 := "Service Two"
+		expected := []Service{
+			Service{
+				Description: &desc0,
+				ID:          "1234",
+				Name:        "service-one",
+				Type:        "identity",
+			},
+			Service{
+				Description: &desc1,
+				ID:          "9876",
+				Name:        "service-two",
+				Type:        "compute",
+			},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Expected %#v, got %#v", expected, actual)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Errorf("Unexpected error while paging: %v", err)
+	}
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGetSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"service": {
+						"description": "Service One",
+						"id": "12345",
+						"name": "service-one",
+						"type": "identity"
+				}
+			}
+		`)
+	})
+
+	result, err := Get(client.ServiceClient(), "12345").Extract()
+	if err != nil {
+		t.Fatalf("Error fetching service information: %v", err)
+	}
+
+	if result.ID != "12345" {
+		t.Errorf("Unexpected service ID: %s", result.ID)
+	}
+	if *result.Description != "Service One" {
+		t.Errorf("Unexpected service description: [%s]", *result.Description)
+	}
+	if result.Name != "service-one" {
+		t.Errorf("Unexpected service name: [%s]", result.Name)
+	}
+	if result.Type != "identity" {
+		t.Errorf("Unexpected service type: [%s]", result.Type)
+	}
+}
+
+func TestUpdateSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "PATCH")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		testhelper.TestJSONRequest(t, r, `{ "type": "lasermagic" }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"service": {
+						"id": "12345",
+						"type": "lasermagic"
+				}
+			}
+		`)
+	})
+
+	result, err := Update(client.ServiceClient(), "12345", "lasermagic").Extract()
+	if err != nil {
+		t.Fatalf("Unable to update service: %v", err)
+	}
+
+	if result.ID != "12345" {
+		t.Fatalf("Expected ID 12345, was %s", result.ID)
+	}
+}
+
+func TestDeleteSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "DELETE")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := Delete(client.ServiceClient(), "12345")
+	if err != nil {
+		t.Fatalf("Unable to delete service: %v", err)
+	}
+}
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
new file mode 100644
index 0000000..dcd7fe2
--- /dev/null
+++ b/openstack/identity/v3/services/results.go
@@ -0,0 +1,75 @@
+package services
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Service, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Service `json:"service"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res.Service, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+	commonResult
+}
+
+// Service is the result of a list or information query.
+type Service struct {
+	Description *string `json:"description,omitempty"`
+	ID          string  `json:"id"`
+	Name        string  `json:"name"`
+	Type        string  `json:"type"`
+}
+
+// ServicePage is a single page of Service results.
+type ServicePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if the page contains no results.
+func (p ServicePage) IsEmpty() (bool, error) {
+	services, err := ExtractServices(p)
+	if err != nil {
+		return true, err
+	}
+	return len(services) == 0, nil
+}
+
+// ExtractServices extracts a slice of Services from a Collection acquired from List.
+func ExtractServices(page pagination.Page) ([]Service, error) {
+	var response struct {
+		Services []Service `mapstructure:"services"`
+	}
+
+	err := mapstructure.Decode(page.(ServicePage).Body, &response)
+	return response.Services, err
+}
diff --git a/openstack/identity/v3/services/urls.go b/openstack/identity/v3/services/urls.go
new file mode 100644
index 0000000..85443a4
--- /dev/null
+++ b/openstack/identity/v3/services/urls.go
@@ -0,0 +1,11 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("services")
+}
+
+func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
+	return client.ServiceURL("services", serviceID)
+}
diff --git a/openstack/identity/v3/services/urls_test.go b/openstack/identity/v3/services/urls_test.go
new file mode 100644
index 0000000..5a31b32
--- /dev/null
+++ b/openstack/identity/v3/services/urls_test.go
@@ -0,0 +1,23 @@
+package services
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func TestListURL(t *testing.T) {
+	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+	url := listURL(&client)
+	if url != "http://localhost:5000/v3/services" {
+		t.Errorf("Unexpected list URL generated: [%s]", url)
+	}
+}
+
+func TestServiceURL(t *testing.T) {
+	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+	url := serviceURL(&client, "1234")
+	if url != "http://localhost:5000/v3/services/1234" {
+		t.Errorf("Unexpected service URL generated: [%s]", url)
+	}
+}
diff --git a/openstack/identity/v3/tokens/doc.go b/openstack/identity/v3/tokens/doc.go
new file mode 100644
index 0000000..76ff5f4
--- /dev/null
+++ b/openstack/identity/v3/tokens/doc.go
@@ -0,0 +1,6 @@
+// Package tokens provides information and interaction with the token API
+// resource for the OpenStack Identity service.
+//
+// For more information, see:
+// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
+package tokens
diff --git a/openstack/identity/v3/tokens/errors.go b/openstack/identity/v3/tokens/errors.go
new file mode 100644
index 0000000..4476109
--- /dev/null
+++ b/openstack/identity/v3/tokens/errors.go
@@ -0,0 +1,72 @@
+package tokens
+
+import (
+	"errors"
+	"fmt"
+)
+
+func unacceptedAttributeErr(attribute string) error {
+	return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute)
+}
+
+func redundantWithTokenErr(attribute string) error {
+	return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute)
+}
+
+func redundantWithUserID(attribute string) error {
+	return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute)
+}
+
+var (
+	// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
+	ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+	// ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
+	ErrTenantIDProvided = unacceptedAttributeErr("TenantID")
+
+	// ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
+	ErrTenantNameProvided = unacceptedAttributeErr("TenantName")
+
+	// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
+	ErrUsernameWithToken = redundantWithTokenErr("Username")
+
+	// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
+	ErrUserIDWithToken = redundantWithTokenErr("UserID")
+
+	// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
+	ErrDomainIDWithToken = redundantWithTokenErr("DomainID")
+
+	// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
+	ErrDomainNameWithToken = redundantWithTokenErr("DomainName")
+
+	// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
+	ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication")
+
+	// ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used.
+	ErrDomainIDWithUserID = redundantWithUserID("DomainID")
+
+	// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
+	ErrDomainNameWithUserID = redundantWithUserID("DomainName")
+
+	// ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it.
+	// It may also indicate that both a DomainID and a DomainName were provided at once.
+	ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
+
+	// ErrMissingPassword indicates that no password was provided and no token is available.
+	ErrMissingPassword = errors.New("You must provide a password to authenticate")
+
+	// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
+	ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")
+
+	// ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope.
+	ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope")
+
+	// ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope.
+	ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope")
+
+	// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
+	ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.")
+
+	// ErrScopeEmpty indicates that no credentials were provided in a Scope.
+	ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope")
+)
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
new file mode 100644
index 0000000..351d7d6
--- /dev/null
+++ b/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,285 @@
+package tokens
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// Scope allows a created token to be limited to a specific domain or project.
+type Scope struct {
+	ProjectID   string
+	ProjectName string
+	DomainID    string
+	DomainName  string
+}
+
+func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
+	h := c.AuthenticatedHeaders()
+	h["X-Subject-Token"] = subjectToken
+	return h
+}
+
+// Create authenticates and either generates a new token, or changes the Scope of an existing token.
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
+	type domainReq struct {
+		ID   *string `json:"id,omitempty"`
+		Name *string `json:"name,omitempty"`
+	}
+
+	type projectReq struct {
+		Domain *domainReq `json:"domain,omitempty"`
+		Name   *string    `json:"name,omitempty"`
+		ID     *string    `json:"id,omitempty"`
+	}
+
+	type userReq struct {
+		ID       *string    `json:"id,omitempty"`
+		Name     *string    `json:"name,omitempty"`
+		Password string     `json:"password"`
+		Domain   *domainReq `json:"domain,omitempty"`
+	}
+
+	type passwordReq struct {
+		User userReq `json:"user"`
+	}
+
+	type tokenReq struct {
+		ID string `json:"id"`
+	}
+
+	type identityReq struct {
+		Methods  []string     `json:"methods"`
+		Password *passwordReq `json:"password,omitempty"`
+		Token    *tokenReq    `json:"token,omitempty"`
+	}
+
+	type scopeReq struct {
+		Domain  *domainReq  `json:"domain,omitempty"`
+		Project *projectReq `json:"project,omitempty"`
+	}
+
+	type authReq struct {
+		Identity identityReq `json:"identity"`
+		Scope    *scopeReq   `json:"scope,omitempty"`
+	}
+
+	type request struct {
+		Auth authReq `json:"auth"`
+	}
+
+	// Populate the request structure based on the provided arguments. Create and return an error
+	// if insufficient or incompatible information is present.
+	var req request
+
+	// Test first for unrecognized arguments.
+	if options.APIKey != "" {
+		return createErr(ErrAPIKeyProvided)
+	}
+	if options.TenantID != "" {
+		return createErr(ErrTenantIDProvided)
+	}
+	if options.TenantName != "" {
+		return createErr(ErrTenantNameProvided)
+	}
+
+	if options.Password == "" {
+		if c.TokenID != "" {
+			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
+			// parameters.
+			if options.Username != "" {
+				return createErr(ErrUsernameWithToken)
+			}
+			if options.UserID != "" {
+				return createErr(ErrUserIDWithToken)
+			}
+			if options.DomainID != "" {
+				return createErr(ErrDomainIDWithToken)
+			}
+			if options.DomainName != "" {
+				return createErr(ErrDomainNameWithToken)
+			}
+
+			// Configure the request for Token authentication.
+			req.Auth.Identity.Methods = []string{"token"}
+			req.Auth.Identity.Token = &tokenReq{
+				ID: c.TokenID,
+			}
+		} else {
+			// If no password or token ID are available, authentication can't continue.
+			return createErr(ErrMissingPassword)
+		}
+	} else {
+		// Password authentication.
+		req.Auth.Identity.Methods = []string{"password"}
+
+		// At least one of Username and UserID must be specified.
+		if options.Username == "" && options.UserID == "" {
+			return createErr(ErrUsernameOrUserID)
+		}
+
+		if options.Username != "" {
+			// If Username is provided, UserID may not be provided.
+			if options.UserID != "" {
+				return createErr(ErrUsernameOrUserID)
+			}
+
+			// Either DomainID or DomainName must also be specified.
+			if options.DomainID == "" && options.DomainName == "" {
+				return createErr(ErrDomainIDOrDomainName)
+			}
+
+			if options.DomainID != "" {
+				if options.DomainName != "" {
+					return createErr(ErrDomainIDOrDomainName)
+				}
+
+				// Configure the request for Username and Password authentication with a DomainID.
+				req.Auth.Identity.Password = &passwordReq{
+					User: userReq{
+						Name:     &options.Username,
+						Password: options.Password,
+						Domain:   &domainReq{ID: &options.DomainID},
+					},
+				}
+			}
+
+			if options.DomainName != "" {
+				// Configure the request for Username and Password authentication with a DomainName.
+				req.Auth.Identity.Password = &passwordReq{
+					User: userReq{
+						Name:     &options.Username,
+						Password: options.Password,
+						Domain:   &domainReq{Name: &options.DomainName},
+					},
+				}
+			}
+		}
+
+		if options.UserID != "" {
+			// If UserID is specified, neither DomainID nor DomainName may be.
+			if options.DomainID != "" {
+				return createErr(ErrDomainIDWithUserID)
+			}
+			if options.DomainName != "" {
+				return createErr(ErrDomainNameWithUserID)
+			}
+
+			// Configure the request for UserID and Password authentication.
+			req.Auth.Identity.Password = &passwordReq{
+				User: userReq{ID: &options.UserID, Password: options.Password},
+			}
+		}
+	}
+
+	// Add a "scope" element if a Scope has been provided.
+	if scope != nil {
+		if scope.ProjectName != "" {
+			// ProjectName provided: either DomainID or DomainName must also be supplied.
+			// ProjectID may not be supplied.
+			if scope.DomainID == "" && scope.DomainName == "" {
+				return createErr(ErrScopeDomainIDOrDomainName)
+			}
+			if scope.ProjectID != "" {
+				return createErr(ErrScopeProjectIDOrProjectName)
+			}
+
+			if scope.DomainID != "" {
+				// ProjectName + DomainID
+				req.Auth.Scope = &scopeReq{
+					Project: &projectReq{
+						Name:   &scope.ProjectName,
+						Domain: &domainReq{ID: &scope.DomainID},
+					},
+				}
+			}
+
+			if scope.DomainName != "" {
+				// ProjectName + DomainName
+				req.Auth.Scope = &scopeReq{
+					Project: &projectReq{
+						Name:   &scope.ProjectName,
+						Domain: &domainReq{Name: &scope.DomainName},
+					},
+				}
+			}
+		} else if scope.ProjectID != "" {
+			// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+			if scope.DomainID != "" {
+				return createErr(ErrScopeProjectIDAlone)
+			}
+			if scope.DomainName != "" {
+				return createErr(ErrScopeProjectIDAlone)
+			}
+
+			// ProjectID
+			req.Auth.Scope = &scopeReq{
+				Project: &projectReq{ID: &scope.ProjectID},
+			}
+		} else if scope.DomainID != "" {
+			// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+			if scope.DomainName != "" {
+				return createErr(ErrScopeDomainIDOrDomainName)
+			}
+
+			// DomainID
+			req.Auth.Scope = &scopeReq{
+				Domain: &domainReq{ID: &scope.DomainID},
+			}
+		} else if scope.DomainName != "" {
+			return createErr(ErrScopeDomainName)
+		} else {
+			return createErr(ErrScopeEmpty)
+		}
+	}
+
+	var result CreateResult
+	var response *perigee.Response
+	response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
+		ReqBody: &req,
+		Results: &result.Body,
+		OkCodes: []int{201},
+	})
+	if result.Err != nil {
+		return result
+	}
+	result.Header = response.HttpResponse.Header
+	return result
+}
+
+// Get validates and retrieves information about another token.
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+	var result GetResult
+	var response *perigee.Response
+	response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
+		MoreHeaders: subjectTokenHeaders(c, token),
+		Results:     &result.Body,
+		OkCodes:     []int{200, 203},
+	})
+	if result.Err != nil {
+		return result
+	}
+	result.Header = response.HttpResponse.Header
+	return result
+}
+
+// Validate determines if a specified token is valid or not.
+func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
+	response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
+		MoreHeaders: subjectTokenHeaders(c, token),
+		OkCodes:     []int{204, 404},
+	})
+	if err != nil {
+		return false, err
+	}
+
+	return response.StatusCode == 204, nil
+}
+
+// Revoke immediately makes specified token invalid.
+func Revoke(c *gophercloud.ServiceClient, token string) error {
+	_, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
+		MoreHeaders: subjectTokenHeaders(c, token),
+		OkCodes:     []int{204},
+	})
+	return err
+}
diff --git a/openstack/identity/v3/tokens/requests_test.go b/openstack/identity/v3/tokens/requests_test.go
new file mode 100644
index 0000000..a61bb2c
--- /dev/null
+++ b/openstack/identity/v3/tokens/requests_test.go
@@ -0,0 +1,516 @@
+package tokens
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure.
+func authTokenPost(t *testing.T, options gophercloud.AuthOptions, scope *Scope, requestJSON string) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{
+			TokenID: "12345abcdef",
+		},
+		Endpoint: testhelper.Endpoint(),
+	}
+
+	testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "Content-Type", "application/json")
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		testhelper.TestJSONRequest(t, r, requestJSON)
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `{
+			"token": {
+				"expires_at": "2014-10-02T13:45:00.000000Z"
+			}
+		}`)
+	})
+
+	_, err := Create(&client, options, scope).Extract()
+	if err != nil {
+		t.Errorf("Create returned an error: %v", err)
+	}
+}
+
+func authTokenPostErr(t *testing.T, options gophercloud.AuthOptions, scope *Scope, includeToken bool, expectedErr error) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{},
+		Endpoint:       testhelper.Endpoint(),
+	}
+	if includeToken {
+		client.TokenID = "abcdef123456"
+	}
+
+	_, err := Create(&client, options, scope).Extract()
+	if err == nil {
+		t.Errorf("Create did NOT return an error")
+	}
+	if err != expectedErr {
+		t.Errorf("Create returned an unexpected error: wanted %v, got %v", expectedErr, err)
+	}
+}
+
+func TestCreateUserIDAndPassword(t *testing.T) {
+	authTokenPost(t, gophercloud.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": { "id": "me", "password": "squirrel!" }
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateUsernameDomainIDPassword(t *testing.T) {
+	authTokenPost(t, gophercloud.AuthOptions{Username: "fakey", Password: "notpassword", DomainID: "abc123"}, nil, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": {
+							"domain": {
+								"id": "abc123"
+							},
+							"name": "fakey",
+							"password": "notpassword"
+						}
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateUsernameDomainNamePassword(t *testing.T) {
+	authTokenPost(t, gophercloud.AuthOptions{Username: "frank", Password: "swordfish", DomainName: "spork.net"}, nil, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": {
+							"domain": {
+								"name": "spork.net"
+							},
+							"name": "frank",
+							"password": "swordfish"
+						}
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateTokenID(t *testing.T) {
+	authTokenPost(t, gophercloud.AuthOptions{}, nil, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["token"],
+					"token": {
+						"id": "12345abcdef"
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateProjectIDScope(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &Scope{ProjectID: "123456"}
+	authTokenPost(t, options, scope, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": {
+							"id": "fenris",
+							"password": "g0t0h311"
+						}
+					}
+				},
+				"scope": {
+					"project": {
+						"id": "123456"
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateDomainIDScope(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &Scope{DomainID: "1000"}
+	authTokenPost(t, options, scope, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": {
+							"id": "fenris",
+							"password": "g0t0h311"
+						}
+					}
+				},
+				"scope": {
+					"domain": {
+						"id": "1000"
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateProjectNameAndDomainIDScope(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &Scope{ProjectName: "world-domination", DomainID: "1000"}
+	authTokenPost(t, options, scope, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": {
+							"id": "fenris",
+							"password": "g0t0h311"
+						}
+					}
+				},
+				"scope": {
+					"project": {
+						"domain": {
+							"id": "1000"
+						},
+						"name": "world-domination"
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateProjectNameAndDomainNameScope(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &Scope{ProjectName: "world-domination", DomainName: "evil-plans"}
+	authTokenPost(t, options, scope, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": {
+							"id": "fenris",
+							"password": "g0t0h311"
+						}
+					}
+				},
+				"scope": {
+					"project": {
+						"domain": {
+							"name": "evil-plans"
+						},
+						"name": "world-domination"
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateExtractsTokenFromResponse(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{},
+		Endpoint:       testhelper.Endpoint(),
+	}
+
+	testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("X-Subject-Token", "aaa111")
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `{
+			"token": {
+				"expires_at": "2014-10-02T13:45:00.000000Z"
+			}
+		}`)
+	})
+
+	options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
+	token, err := Create(&client, options, nil).Extract()
+	if err != nil {
+		t.Fatalf("Create returned an error: %v", err)
+	}
+
+	if token.ID != "aaa111" {
+		t.Errorf("Expected token to be aaa111, but was %s", token.ID)
+	}
+}
+
+func TestCreateFailureEmptyAuth(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{}, nil, false, ErrMissingPassword)
+}
+
+func TestCreateFailureAPIKey(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{APIKey: "something"}, nil, false, ErrAPIKeyProvided)
+}
+
+func TestCreateFailureTenantID(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{TenantID: "something"}, nil, false, ErrTenantIDProvided)
+}
+
+func TestCreateFailureTenantName(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{TenantName: "something"}, nil, false, ErrTenantNameProvided)
+}
+
+func TestCreateFailureTokenIDUsername(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{Username: "something"}, nil, true, ErrUsernameWithToken)
+}
+
+func TestCreateFailureTokenIDUserID(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{UserID: "something"}, nil, true, ErrUserIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainID(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{DomainID: "something"}, nil, true, ErrDomainIDWithToken)
+}
+
+func TestCreateFailureTokenIDDomainName(t *testing.T) {
+	authTokenPostErr(t, gophercloud.AuthOptions{DomainName: "something"}, nil, true, ErrDomainNameWithToken)
+}
+
+func TestCreateFailureMissingUser(t *testing.T) {
+	options := gophercloud.AuthOptions{Password: "supersecure"}
+	authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureBothUser(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Password: "supersecure",
+		Username: "oops",
+		UserID:   "redundancy",
+	}
+	authTokenPostErr(t, options, nil, false, ErrUsernameOrUserID)
+}
+
+func TestCreateFailureMissingDomain(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Password: "supersecure",
+		Username: "notuniqueenough",
+	}
+	authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureBothDomain(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Password:   "supersecure",
+		Username:   "someone",
+		DomainID:   "hurf",
+		DomainName: "durf",
+	}
+	authTokenPostErr(t, options, nil, false, ErrDomainIDOrDomainName)
+}
+
+func TestCreateFailureUserIDDomainID(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		UserID:   "100",
+		Password: "stuff",
+		DomainID: "oops",
+	}
+	authTokenPostErr(t, options, nil, false, ErrDomainIDWithUserID)
+}
+
+func TestCreateFailureUserIDDomainName(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		UserID:     "100",
+		Password:   "sssh",
+		DomainName: "oops",
+	}
+	authTokenPostErr(t, options, nil, false, ErrDomainNameWithUserID)
+}
+
+func TestCreateFailureScopeProjectNameAlone(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{ProjectName: "notenough"}
+	authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeProjectNameAndID(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"}
+	authTokenPostErr(t, options, scope, false, ErrScopeProjectIDOrProjectName)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{ProjectID: "toomuch", DomainID: "notneeded"}
+	authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{ProjectID: "toomuch", DomainName: "notneeded"}
+	authTokenPostErr(t, options, scope, false, ErrScopeProjectIDAlone)
+}
+
+func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{DomainID: "toomuch", DomainName: "notneeded"}
+	authTokenPostErr(t, options, scope, false, ErrScopeDomainIDOrDomainName)
+}
+
+func TestCreateFailureScopeDomainNameAlone(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{DomainName: "notenough"}
+	authTokenPostErr(t, options, scope, false, ErrScopeDomainName)
+}
+
+func TestCreateFailureEmptyScope(t *testing.T) {
+	options := gophercloud.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &Scope{}
+	authTokenPostErr(t, options, scope, false, ErrScopeEmpty)
+}
+
+func TestGetRequest(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{
+			TokenID: "12345abcdef",
+		},
+		Endpoint: testhelper.Endpoint(),
+	}
+
+	testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "Content-Type", "")
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+		testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+			{ "token": { "expires_at": "2014-08-29T13:10:01.000000Z" } }
+		`)
+	})
+
+	token, err := Get(&client, "abcdef12345").Extract()
+	if err != nil {
+		t.Errorf("Info returned an error: %v", err)
+	}
+
+	expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
+	if token.ExpiresAt != expected {
+		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
+	}
+}
+
+func prepareAuthTokenHandler(t *testing.T, expectedMethod string, status int) gophercloud.ServiceClient {
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{
+			TokenID: "12345abcdef",
+		},
+		Endpoint: testhelper.Endpoint(),
+	}
+
+	testhelper.Mux.HandleFunc("/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, expectedMethod)
+		testhelper.TestHeader(t, r, "Content-Type", "")
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		testhelper.TestHeader(t, r, "X-Auth-Token", "12345abcdef")
+		testhelper.TestHeader(t, r, "X-Subject-Token", "abcdef12345")
+
+		w.WriteHeader(status)
+	})
+
+	return client
+}
+
+func TestValidateRequestSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	client := prepareAuthTokenHandler(t, "HEAD", http.StatusNoContent)
+
+	ok, err := Validate(&client, "abcdef12345")
+	if err != nil {
+		t.Errorf("Unexpected error from Validate: %v", err)
+	}
+
+	if !ok {
+		t.Errorf("Validate returned false for a valid token")
+	}
+}
+
+func TestValidateRequestFailure(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	client := prepareAuthTokenHandler(t, "HEAD", http.StatusNotFound)
+
+	ok, err := Validate(&client, "abcdef12345")
+	if err != nil {
+		t.Errorf("Unexpected error from Validate: %v", err)
+	}
+
+	if ok {
+		t.Errorf("Validate returned true for an invalid token")
+	}
+}
+
+func TestValidateRequestError(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	client := prepareAuthTokenHandler(t, "HEAD", http.StatusUnauthorized)
+
+	_, err := Validate(&client, "abcdef12345")
+	if err == nil {
+		t.Errorf("Missing expected error from Validate")
+	}
+}
+
+func TestRevokeRequestSuccessful(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	client := prepareAuthTokenHandler(t, "DELETE", http.StatusNoContent)
+
+	err := Revoke(&client, "abcdef12345")
+	if err != nil {
+		t.Errorf("Unexpected error from Revoke: %v", err)
+	}
+}
+
+func TestRevokeRequestError(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound)
+
+	err := Revoke(&client, "abcdef12345")
+	if err == nil {
+		t.Errorf("Missing expected error from Revoke")
+	}
+}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
new file mode 100644
index 0000000..c31c52f
--- /dev/null
+++ b/openstack/identity/v3/tokens/results.go
@@ -0,0 +1,68 @@
+package tokens
+
+import (
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+)
+
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Token struct {
+			ExpiresAt string `mapstructure:"expires_at"`
+		} `mapstructure:"token"`
+	}
+
+	var token Token
+
+	// Parse the token itself from the stored headers.
+	token.ID = r.Header.Get("X-Subject-Token")
+
+	err := mapstructure.Decode(r.Body, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	// Attempt to parse the timestamp.
+	token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
+
+	return &token, err
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+	return CreateResult{
+		commonResult: commonResult{Result: gophercloud.Result{Err: err}},
+	}
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+	// ID is the issued token.
+	ID string
+
+	// ExpiresAt is the timestamp at which this token will no longer be accepted.
+	ExpiresAt time.Time
+}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
new file mode 100644
index 0000000..360b60a
--- /dev/null
+++ b/openstack/identity/v3/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func tokenURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("auth", "tokens")
+}
diff --git a/openstack/identity/v3/tokens/urls_test.go b/openstack/identity/v3/tokens/urls_test.go
new file mode 100644
index 0000000..549c398
--- /dev/null
+++ b/openstack/identity/v3/tokens/urls_test.go
@@ -0,0 +1,21 @@
+package tokens
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTokenURL(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
+
+	expected := testhelper.Endpoint() + "auth/tokens"
+	actual := tokenURL(&client)
+	if actual != expected {
+		t.Errorf("Expected URL %s, but was %s", expected, actual)
+	}
+}
diff --git a/openstack/networking/v2/apiversions/doc.go b/openstack/networking/v2/apiversions/doc.go
new file mode 100644
index 0000000..0208ee2
--- /dev/null
+++ b/openstack/networking/v2/apiversions/doc.go
@@ -0,0 +1,4 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Neutron service. This functionality is not
+// restricted to this particular version.
+package apiversions
diff --git a/openstack/networking/v2/apiversions/errors.go b/openstack/networking/v2/apiversions/errors.go
new file mode 100644
index 0000000..76bdb14
--- /dev/null
+++ b/openstack/networking/v2/apiversions/errors.go
@@ -0,0 +1 @@
+package apiversions
diff --git a/openstack/networking/v2/apiversions/requests.go b/openstack/networking/v2/apiversions/requests.go
new file mode 100644
index 0000000..9fb6de1
--- /dev/null
+++ b/openstack/networking/v2/apiversions/requests.go
@@ -0,0 +1,21 @@
+package apiversions
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListVersions lists all the Neutron API versions available to end-users
+func ListVersions(c *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page {
+		return APIVersionPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// ListVersionResources lists all of the different API resources for a particular
+// API versions. Typical resources for Neutron might be: networks, subnets, etc.
+func ListVersionResources(c *gophercloud.ServiceClient, v string) pagination.Pager {
+	return pagination.NewPager(c, apiInfoURL(c, v), func(r pagination.PageResult) pagination.Page {
+		return APIVersionResourcePage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/openstack/networking/v2/apiversions/requests_test.go b/openstack/networking/v2/apiversions/requests_test.go
new file mode 100644
index 0000000..d35af9f
--- /dev/null
+++ b/openstack/networking/v2/apiversions/requests_test.go
@@ -0,0 +1,182 @@
+package apiversions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "versions": [
+        {
+            "status": "CURRENT",
+            "id": "v2.0",
+            "links": [
+                {
+                    "href": "http://23.253.228.211:9696/v2.0",
+                    "rel": "self"
+                }
+            ]
+        }
+    ]
+}`)
+	})
+
+	count := 0
+
+	ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractAPIVersions(page)
+		if err != nil {
+			t.Errorf("Failed to extract API versions: %v", err)
+			return false, err
+		}
+
+		expected := []APIVersion{
+			APIVersion{
+				Status: "CURRENT",
+				ID:     "v2.0",
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+
+	ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := ExtractAPIVersions(page); err == nil {
+			t.Fatalf("Expected error, got nil")
+		}
+		return true, nil
+	})
+}
+
+func TestAPIInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "resources": [
+        {
+            "links": [
+                {
+                    "href": "http://23.253.228.211:9696/v2.0/subnets",
+                    "rel": "self"
+                }
+            ],
+            "name": "subnet",
+            "collection": "subnets"
+        },
+        {
+            "links": [
+                {
+                    "href": "http://23.253.228.211:9696/v2.0/networks",
+                    "rel": "self"
+                }
+            ],
+            "name": "network",
+            "collection": "networks"
+        },
+        {
+            "links": [
+                {
+                    "href": "http://23.253.228.211:9696/v2.0/ports",
+                    "rel": "self"
+                }
+            ],
+            "name": "port",
+            "collection": "ports"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVersionResources(page)
+		if err != nil {
+			t.Errorf("Failed to extract version resources: %v", err)
+			return false, err
+		}
+
+		expected := []APIVersionResource{
+			APIVersionResource{
+				Name:       "subnet",
+				Collection: "subnets",
+			},
+			APIVersionResource{
+				Name:       "network",
+				Collection: "networks",
+			},
+			APIVersionResource{
+				Name:       "port",
+				Collection: "ports",
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestNonJSONCannotBeExtractedIntoAPIVersionResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+
+	ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := ExtractVersionResources(page); err == nil {
+			t.Fatalf("Expected error, got nil")
+		}
+		return true, nil
+	})
+}
diff --git a/openstack/networking/v2/apiversions/results.go b/openstack/networking/v2/apiversions/results.go
new file mode 100644
index 0000000..9715934
--- /dev/null
+++ b/openstack/networking/v2/apiversions/results.go
@@ -0,0 +1,77 @@
+package apiversions
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// APIVersion represents an API version for Neutron. It contains the status of
+// the API, and its unique ID.
+type APIVersion struct {
+	Status string `mapstructure:"status" json:"status"`
+	ID     string `mapstructure:"id" json:"id"`
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+	is, err := ExtractAPIVersions(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+	var resp struct {
+		Versions []APIVersion `mapstructure:"versions"`
+	}
+
+	err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+
+	return resp.Versions, err
+}
+
+// APIVersionResource represents a generic API resource. It contains the name
+// of the resource and its plural collection name.
+type APIVersionResource struct {
+	Name       string `mapstructure:"name" json:"name"`
+	Collection string `mapstructure:"collection" json:"collection"`
+}
+
+// APIVersionResourcePage is a concrete type which embeds the common
+// SinglePageBase struct, and is used when traversing API versions collections.
+type APIVersionResourcePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty is a concrete function which indicates whether an
+// APIVersionResourcePage is empty or not.
+func (r APIVersionResourcePage) IsEmpty() (bool, error) {
+	is, err := ExtractVersionResources(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractVersionResources accepts a Page struct, specifically a
+// APIVersionResourcePage struct, and extracts the elements into a slice of
+// APIVersionResource structs. In other words, the collection is mapped into
+// a relevant slice.
+func ExtractVersionResources(page pagination.Page) ([]APIVersionResource, error) {
+	var resp struct {
+		APIVersionResources []APIVersionResource `mapstructure:"resources"`
+	}
+
+	err := mapstructure.Decode(page.(APIVersionResourcePage).Body, &resp)
+
+	return resp.APIVersionResources, err
+}
diff --git a/openstack/networking/v2/apiversions/urls.go b/openstack/networking/v2/apiversions/urls.go
new file mode 100644
index 0000000..58aa2b6
--- /dev/null
+++ b/openstack/networking/v2/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func apiVersionsURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint
+}
+
+func apiInfoURL(c *gophercloud.ServiceClient, version string) string {
+	return c.Endpoint + strings.TrimRight(version, "/") + "/"
+}
diff --git a/openstack/networking/v2/apiversions/urls_test.go b/openstack/networking/v2/apiversions/urls_test.go
new file mode 100644
index 0000000..7dd069c
--- /dev/null
+++ b/openstack/networking/v2/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestAPIVersionsURL(t *testing.T) {
+	actual := apiVersionsURL(endpointClient())
+	expected := endpoint
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestAPIInfoURL(t *testing.T) {
+	actual := apiInfoURL(endpointClient(), "v2.0")
+	expected := endpoint + "v2.0/"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/networking/v2/common/common_tests.go b/openstack/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..4160351
--- /dev/null
+++ b/openstack/networking/v2/common/common_tests.go
@@ -0,0 +1,14 @@
+package common
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+	sc := client.ServiceClient()
+	sc.ResourceBase = sc.Endpoint + "v2.0/"
+	return sc
+}
diff --git a/openstack/networking/v2/extensions/delegate.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/delegate_test.go b/openstack/networking/v2/extensions/delegate_test.go
new file mode 100755
index 0000000..3d2ac78
--- /dev/null
+++ b/openstack/networking/v2/extensions/delegate_test.go
@@ -0,0 +1,105 @@
+package extensions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		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(fake.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{
+				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
+	})
+
+	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", fake.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(fake.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
+	th.AssertEquals(t, ext.Name, "agent")
+	th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
+	th.AssertEquals(t, ext.Alias, "agent")
+	th.AssertEquals(t, ext.Description, "The agent management extension.")
+}
diff --git a/openstack/networking/v2/extensions/external/doc.go b/openstack/networking/v2/extensions/external/doc.go
new file mode 100755
index 0000000..dad3a84
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/doc.go
@@ -0,0 +1,3 @@
+// Package external provides information and interaction with the external
+// extension for the OpenStack Networking service.
+package external
diff --git a/openstack/networking/v2/extensions/external/requests.go b/openstack/networking/v2/extensions/external/requests.go
new file mode 100644
index 0000000..2f04593
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/requests.go
@@ -0,0 +1,56 @@
+package external
+
+import "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// CreateOpts is the structure used when creating new external network
+// resources. It embeds networks.CreateOpts and so inherits all of its required
+// and optional fields, with the addition of the External field.
+type CreateOpts struct {
+	Parent   networks.CreateOpts
+	External bool
+}
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (o CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	outer, err := o.Parent.ToNetworkCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	outer["network"].(map[string]interface{})["router:external"] = o.External
+
+	return outer, nil
+}
+
+// UpdateOpts is the structure used when updating existing external network
+// resources. It embeds networks.UpdateOpts and so inherits all of its required
+// and optional fields, with the addition of the External field.
+type UpdateOpts struct {
+	Parent   networks.UpdateOpts
+	External bool
+}
+
+// ToNetworkUpdateMap casts an UpdateOpts struct to a map.
+func (o UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+	outer, err := o.Parent.ToNetworkUpdateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	outer["network"].(map[string]interface{})["router:external"] = o.External
+
+	return outer, nil
+}
diff --git a/openstack/networking/v2/extensions/external/results.go b/openstack/networking/v2/extensions/external/results.go
new file mode 100644
index 0000000..1c173c0
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/results.go
@@ -0,0 +1,81 @@
+package external
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// NetworkExternal represents a decorated form of a Network with based on the
+// "external-net" extension.
+type NetworkExternal struct {
+	// UUID for the network
+	ID string `mapstructure:"id" json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Name string `mapstructure:"name" json:"name"`
+
+	// The administrative state of network. If false (down), the network does not forward packets.
+	AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+	// Indicates whether network is currently operational. Possible values include
+	// `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+	Status string `mapstructure:"status" json:"status"`
+
+	// Subnets associated with this network.
+	Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+	// Specifies whether the network resource can be accessed by any tenant or not.
+	Shared bool `mapstructure:"shared" json:"shared"`
+
+	// Specifies whether the network is an external network or not.
+	External bool `mapstructure:"router:external" json:"router:external"`
+}
+
+func commonExtract(e error, response interface{}) (*NetworkExternal, error) {
+	if e != nil {
+		return nil, e
+	}
+
+	var res struct {
+		Network *NetworkExternal `json:"network"`
+	}
+
+	err := mapstructure.Decode(response, &res)
+
+	return res.Network, err
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
+	return commonExtract(r.Err, r.Body)
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) {
+	return commonExtract(r.Err, r.Body)
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) {
+	return commonExtract(r.Err, r.Body)
+}
+
+// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
+// extracts the elements into a slice of NetworkExtAttrs structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractList(page pagination.Page) ([]NetworkExternal, error) {
+	var resp struct {
+		Networks []NetworkExternal `mapstructure:"networks" json:"networks"`
+	}
+
+	err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
+
+	return resp.Networks, err
+}
diff --git a/openstack/networking/v2/extensions/external/results_test.go b/openstack/networking/v2/extensions/external/results_test.go
new file mode 100644
index 0000000..916cd2c
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/results_test.go
@@ -0,0 +1,254 @@
+package external
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "networks": [
+        {
+            "admin_state_up": true,
+            "id": "0f38d5ad-10a6-428f-a5fc-825cfe0f1970",
+            "name": "net1",
+            "router:external": false,
+            "shared": false,
+            "status": "ACTIVE",
+            "subnets": [
+                "25778974-48a8-46e7-8998-9dc8c70d2f06"
+            ],
+            "tenant_id": "b575417a6c444a6eb5cc3a58eb4f714a"
+        },
+        {
+            "admin_state_up": true,
+            "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+            "name": "ext_net",
+            "router:external": true,
+            "shared": false,
+            "status": "ACTIVE",
+            "subnets": [
+                "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+            ],
+            "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractList(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []NetworkExternal{
+			NetworkExternal{
+				Status:       "ACTIVE",
+				Subnets:      []string{"25778974-48a8-46e7-8998-9dc8c70d2f06"},
+				Name:         "net1",
+				AdminStateUp: true,
+				TenantID:     "b575417a6c444a6eb5cc3a58eb4f714a",
+				Shared:       false,
+				ID:           "0f38d5ad-10a6-428f-a5fc-825cfe0f1970",
+				External:     false,
+			},
+			NetworkExternal{
+				Status:       "ACTIVE",
+				Subnets:      []string{"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"},
+				Name:         "ext_net",
+				AdminStateUp: true,
+				TenantID:     "5eb8995cf717462c9df8d1edfa498010",
+				Shared:       false,
+				ID:           "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+				External:     true,
+			},
+		}
+
+		th.CheckDeepEquals(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("/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "admin_state_up": true,
+        "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+        "name": "ext_net",
+        "router:external": true,
+        "shared": false,
+        "status": "ACTIVE",
+        "subnets": [
+            "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+        ],
+        "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+    }
+}
+			`)
+	})
+
+	res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	n, err := ExtractGet(res)
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, n.External)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "network": {
+        "admin_state_up": true,
+        "name": "ext_net",
+        "router:external": true
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+	"network": {
+			"admin_state_up": true,
+			"id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+			"name": "ext_net",
+			"router:external": true,
+			"shared": false,
+			"status": "ACTIVE",
+			"subnets": [
+					"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+			],
+			"tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+	}
+}
+		`)
+	})
+
+	options := CreateOpts{networks.CreateOpts{Name: "ext_net", AdminStateUp: Up}, true}
+	res := networks.Create(fake.ServiceClient(), options)
+
+	n, err := ExtractCreate(res)
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, n.External)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"network": {
+				"router:external": true,
+				"name": "new_name"
+		}
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"network": {
+			"admin_state_up": true,
+			"id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+			"name": "new_name",
+			"router:external": true,
+			"shared": false,
+			"status": "ACTIVE",
+			"subnets": [
+					"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+			],
+			"tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+	}
+}
+		`)
+	})
+
+	options := UpdateOpts{networks.UpdateOpts{Name: "new_name"}, true}
+	res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+	n, err := ExtractUpdate(res)
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, n.External)
+}
+
+func TestExtractFnsReturnsErrWhenResultContainsErr(t *testing.T) {
+	gr := networks.GetResult{}
+	gr.Err = errors.New("")
+
+	if _, err := ExtractGet(gr); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+
+	ur := networks.UpdateResult{}
+	ur.Err = errors.New("")
+
+	if _, err := ExtractUpdate(ur); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+
+	cr := networks.CreateResult{}
+	cr.Err = errors.New("")
+
+	if _, err := ExtractCreate(cr); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+}
diff --git a/openstack/networking/v2/extensions/layer3/doc.go b/openstack/networking/v2/extensions/layer3/doc.go
new file mode 100644
index 0000000..d533458
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/doc.go
@@ -0,0 +1,5 @@
+// Package layer3 provides access to the Layer-3 networking extension for the
+// OpenStack Neutron service. This extension allows API users to route packets
+// between subnets, forward packets from internal networks to external ones,
+// and access instances from external networks through floating IPs.
+package layer3
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
new file mode 100644
index 0000000..d23f9e2
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -0,0 +1,190 @@
+package floatingips
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	ID                string `q:"id"`
+	FloatingNetworkID string `q:"floating_network_id"`
+	PortID            string `q:"port_id"`
+	FixedIP           string `q:"fixed_ip_address"`
+	FloatingIP        string `q:"floating_ip_address"`
+	TenantID          string `q:"tenant_id"`
+	Limit             int    `q:"limit"`
+	Marker            string `q:"marker"`
+	SortKey           string `q:"sort_key"`
+	SortDir           string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// floating IP resources. It accepts a ListOpts struct, which allows you to
+// filter and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOpts contains all the values needed to create a new floating IP
+// resource. The only required fields are FloatingNetworkID and PortID which
+// refer to the external network and internal port respectively.
+type CreateOpts struct {
+	FloatingNetworkID string
+	FloatingIP        string
+	PortID            string
+	FixedIP           string
+	TenantID          string
+}
+
+var (
+	errFloatingNetworkIDRequired = fmt.Errorf("A NetworkID is required")
+	errPortIDRequired            = fmt.Errorf("A PortID is required")
+)
+
+// Create accepts a CreateOpts struct and uses the values provided to create a
+// new floating IP resource. You can create floating IPs on external networks
+// only. If you provide a FloatingNetworkID which refers to a network that is
+// not external (i.e. its `router:external' attribute is False), the operation
+// will fail and return a 400 error.
+//
+// If you do not specify a FloatingIP address value, the operation will
+// automatically allocate an available address for the new resource. If you do
+// choose to specify one, it must fall within the subnet range for the external
+// network - otherwise the operation returns a 400 error. If the FloatingIP
+// address is already in use, the operation returns a 409 error code.
+//
+// You can associate the new resource with an internal port by using the PortID
+// field. If you specify a PortID that is not valid, the operation will fail and
+// return 404 error code.
+//
+// You must also configure an IP address for the port associated with the PortID
+// you have provided - this is what the FixedIP refers to: an IP fixed to a port.
+// Because a port might be associated with multiple IP addresses, you can use
+// the FixedIP field to associate a particular IP address rather than have the
+// API assume for you. If you specify an IP address that is not valid, the
+// operation will fail and return a 400 error code. If the PortID and FixedIP
+// are already associated with another resource, the operation will fail and
+// returns a 409 error code.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	var res CreateResult
+
+	// Validate
+	if opts.FloatingNetworkID == "" {
+		res.Err = errFloatingNetworkIDRequired
+		return res
+	}
+	if opts.PortID == "" {
+		res.Err = errPortIDRequired
+		return res
+	}
+
+	// Define structures
+	type floatingIP struct {
+		FloatingNetworkID string `json:"floating_network_id"`
+		FloatingIP        string `json:"floating_ip_address,omitempty"`
+		PortID            string `json:"port_id"`
+		FixedIP           string `json:"fixed_ip_address,omitempty"`
+		TenantID          string `json:"tenant_id,omitempty"`
+	}
+	type request struct {
+		FloatingIP floatingIP `json:"floatingip"`
+	}
+
+	// Populate request body
+	reqBody := request{FloatingIP: floatingIP{
+		FloatingNetworkID: opts.FloatingNetworkID,
+		PortID:            opts.PortID,
+		FixedIP:           opts.FixedIP,
+		TenantID:          opts.TenantID,
+	}}
+
+	// Send request to API
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+
+	return res
+}
+
+// Get retrieves a particular floating IP resource based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// UpdateOpts contains the values used when updating a floating IP resource. The
+// only value that can be updated is which internal port the floating IP is
+// linked to. To associate the floating IP with a new internal port, provide its
+// ID. To disassociate the floating IP from all ports, provide an empty string.
+type UpdateOpts struct {
+	PortID string
+}
+
+// Update allows floating IP resources to be updated. Currently, the only way to
+// "update" a floating IP is to associate it with a new internal port, or
+// disassociated it from all ports. See UpdateOpts for instructions of how to
+// do this.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type floatingIP struct {
+		PortID *string `json:"port_id"`
+	}
+
+	type request struct {
+		FloatingIP floatingIP `json:"floatingip"`
+	}
+
+	var portID *string
+	if opts.PortID == "" {
+		portID = nil
+	} else {
+		portID = &opts.PortID
+	}
+
+	reqBody := request{FloatingIP: floatingIP{PortID: portID}}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a particular floating IP resource. Please
+// ensure this is what you want - you can also disassociate the IP from existing
+// internal ports.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
new file mode 100644
index 0000000..19614be
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
@@ -0,0 +1,306 @@
+package floatingips
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "floatingips": [
+        {
+            "floating_network_id": "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+            "router_id": null,
+            "fixed_ip_address": null,
+            "floating_ip_address": "192.0.0.4",
+            "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+            "status": "DOWN",
+            "port_id": null,
+            "id": "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e"
+        },
+        {
+            "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64",
+            "router_id": "0a24cb83-faf5-4d7f-b723-3144ed8a2167",
+            "fixed_ip_address": "192.0.0.2",
+            "floating_ip_address": "10.0.0.3",
+            "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+            "status": "DOWN",
+            "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25",
+            "id": "ada25a95-f321-4f59-b0e0-f3a970dd3d63"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractFloatingIPs(page)
+		if err != nil {
+			t.Errorf("Failed to extract floating IPs: %v", err)
+			return false, err
+		}
+
+		expected := []FloatingIP{
+			FloatingIP{
+				FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+				FixedIP:           "",
+				FloatingIP:        "192.0.0.4",
+				TenantID:          "017d8de156df4177889f31a9bd6edc00",
+				Status:            "DOWN",
+				PortID:            "",
+				ID:                "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e",
+			},
+			FloatingIP{
+				FloatingNetworkID: "90f742b1-6d17-487b-ba95-71881dbc0b64",
+				FixedIP:           "192.0.0.2",
+				FloatingIP:        "10.0.0.3",
+				TenantID:          "017d8de156df4177889f31a9bd6edc00",
+				Status:            "DOWN",
+				PortID:            "74a342ce-8e07-4e91-880c-9f834b68fa25",
+				ID:                "ada25a95-f321-4f59-b0e0-f3a970dd3d63",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestInvalidNextPageURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{"floatingips": [{}], "floatingips_links": {}}`)
+	})
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		ExtractFloatingIPs(page)
+		return true, nil
+	})
+}
+
+func TestRequiredFieldsForCreate(t *testing.T) {
+	res1 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: ""})
+	if res1.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res2 := Create(fake.ServiceClient(), CreateOpts{FloatingNetworkID: "foo", PortID: ""})
+	if res2.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "floatingip": {
+        "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+        "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "floatingip": {
+        "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+        "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+        "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+        "fixed_ip_address": "10.0.0.3",
+        "floating_ip_address": "",
+        "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+        "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+    }
+}
+		`)
+	})
+
+	options := CreateOpts{
+		FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+		PortID:            "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+	}
+
+	ip, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID)
+	th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID)
+	th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID)
+	th.AssertEquals(t, "", ip.FloatingIP)
+	th.AssertEquals(t, "ce705c24-c1ef-408a-bda3-7bbd946164ab", ip.PortID)
+	th.AssertEquals(t, "10.0.0.3", ip.FixedIP)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "floatingip": {
+        "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64",
+        "fixed_ip_address": "192.0.0.2",
+        "floating_ip_address": "10.0.0.3",
+        "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+        "status": "DOWN",
+        "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25",
+        "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+    }
+}
+      `)
+	})
+
+	ip, err := Get(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "90f742b1-6d17-487b-ba95-71881dbc0b64", ip.FloatingNetworkID)
+	th.AssertEquals(t, "10.0.0.3", ip.FloatingIP)
+	th.AssertEquals(t, "74a342ce-8e07-4e91-880c-9f834b68fa25", ip.PortID)
+	th.AssertEquals(t, "192.0.0.2", ip.FixedIP)
+	th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", ip.TenantID)
+	th.AssertEquals(t, "DOWN", ip.Status)
+	th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID)
+}
+
+func TestAssociate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+	"floatingip": {
+		"port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e"
+	}
+}
+		`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"floatingip": {
+			"router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+			"tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+			"floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+			"fixed_ip_address": null,
+			"floating_ip_address": "172.24.4.228",
+			"port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e",
+			"id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+	}
+}
+	`)
+	})
+
+	ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{PortID: "423abc8d-2991-4a55-ba98-2aaea84cc72e"}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, "423abc8d-2991-4a55-ba98-2aaea84cc72e", ip.PortID)
+}
+
+func TestDisassociate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "floatingip": {
+      "port_id": null
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "floatingip": {
+        "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+        "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+        "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+        "fixed_ip_address": null,
+        "floating_ip_address": "172.24.4.228",
+        "port_id": null,
+        "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+    }
+}
+    `)
+	})
+
+	ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, "", ip.FixedIP)
+	th.AssertDeepEquals(t, "", ip.PortID)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/results.go b/openstack/networking/v2/extensions/layer3/floatingips/results.go
new file mode 100644
index 0000000..df3a63f
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -0,0 +1,125 @@
+package floatingips
+
+import (
+	"fmt"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// FloatingIP represents a floating IP resource. A floating IP is an external
+// IP address that is mapped to an internal port and, optionally, a specific
+// IP address on a private network. In other words, it enables access to an
+// instance on a private network from an external network. For thsi reason,
+// floating IPs can only be defined on networks where the `router:external'
+// attribute (provided by the external network extension) is set to True.
+type FloatingIP struct {
+	// Unique identifier for the floating IP instance.
+	ID string `json:"id" mapstructure:"id"`
+
+	// UUID of the external network where the floating IP is to be created.
+	FloatingNetworkID string `json:"floating_network_id" mapstructure:"floating_network_id"`
+
+	// Address of the floating IP on the external network.
+	FloatingIP string `json:"floating_ip_address" mapstructure:"floating_ip_address"`
+
+	// UUID of the port on an internal network that is associated with the floating IP.
+	PortID string `json:"port_id" mapstructure:"port_id"`
+
+	// The specific IP address of the internal port which should be associated
+	// with the floating IP.
+	FixedIP string `json:"fixed_ip_address" mapstructure:"fixed_ip_address"`
+
+	// Owner of the floating IP. Only admin users can specify a tenant identifier
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+	// The condition of the API resource.
+	Status string `json:"status" mapstructure:"status"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract a result and extracts a FloatingIP resource.
+func (r commonResult) Extract() (*FloatingIP, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		FloatingIP *FloatingIP `json:"floatingip"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+	if err != nil {
+		return nil, fmt.Errorf("Error decoding Neutron floating IP: %v", err)
+	}
+
+	return res.FloatingIP, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of an update operation.
+type DeleteResult commonResult
+
+// FloatingIPPage is the page returned by a pager when traversing over a
+// collection of floating IPs.
+type FloatingIPPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of floating IPs has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p FloatingIPPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"floatingips_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p FloatingIPPage) IsEmpty() (bool, error) {
+	is, err := ExtractFloatingIPs(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct,
+// and extracts the elements into a slice of FloatingIP structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) {
+	var resp struct {
+		FloatingIPs []FloatingIP `mapstructure:"floatingips" json:"floatingips"`
+	}
+
+	err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp)
+
+	return resp.FloatingIPs, err
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
new file mode 100644
index 0000000..355f20d
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
@@ -0,0 +1,13 @@
+package floatingips
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "floatingips"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
new file mode 100755
index 0000000..e3a1441
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -0,0 +1,246 @@
+package routers
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	ID           string `q:"id"`
+	Name         string `q:"name"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	Status       string `q:"status"`
+	TenantID     string `q:"tenant_id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return RouterPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOpts contains all the values needed to create a new router. There are
+// no required values.
+type CreateOpts struct {
+	Name         string
+	AdminStateUp *bool
+	TenantID     string
+	GatewayInfo  *GatewayInfo
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// logical router. When it is created, the router does not have an internal
+// interface - it is not associated to any subnet.
+//
+// You can optionally specify an external gateway for a router using the
+// GatewayInfo struct. The external gateway for the router must be plugged into
+// an external network (it is external if its `router:external' field is set to
+// true).
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	type router struct {
+		Name         *string      `json:"name,omitempty"`
+		AdminStateUp *bool        `json:"admin_state_up,omitempty"`
+		TenantID     *string      `json:"tenant_id,omitempty"`
+		GatewayInfo  *GatewayInfo `json:"external_gateway_info,omitempty"`
+	}
+
+	type request struct {
+		Router router `json:"router"`
+	}
+
+	reqBody := request{Router: router{
+		Name:         gophercloud.MaybeString(opts.Name),
+		AdminStateUp: opts.AdminStateUp,
+		TenantID:     gophercloud.MaybeString(opts.TenantID),
+	}}
+
+	if opts.GatewayInfo != nil {
+		reqBody.Router.GatewayInfo = opts.GatewayInfo
+	}
+
+	var res CreateResult
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+	return res
+}
+
+// Get retrieves a particular router based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// UpdateOpts contains the values used when updating a router.
+type UpdateOpts struct {
+	Name         string
+	AdminStateUp *bool
+	GatewayInfo  *GatewayInfo
+}
+
+// Update allows routers to be updated. You can update the name, administrative
+// state, and the external gateway. For more information about how to set the
+// external gateway for a router, see Create. This operation does not enable
+// the update of router interfaces. To do this, use the AddInterface and
+// RemoveInterface functions.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type router struct {
+		Name         *string      `json:"name,omitempty"`
+		AdminStateUp *bool        `json:"admin_state_up,omitempty"`
+		GatewayInfo  *GatewayInfo `json:"external_gateway_info,omitempty"`
+	}
+
+	type request struct {
+		Router router `json:"router"`
+	}
+
+	reqBody := request{Router: router{
+		Name:         gophercloud.MaybeString(opts.Name),
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	if opts.GatewayInfo != nil {
+		reqBody.Router.GatewayInfo = opts.GatewayInfo
+	}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a particular router based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
+
+var errInvalidInterfaceOpts = errors.New("When adding a router interface you must provide either a subnet ID or a port ID")
+
+// InterfaceOpts allow you to work with operations that either add or remote
+// an internal interface from a router.
+type InterfaceOpts struct {
+	SubnetID string
+	PortID   string
+}
+
+// AddInterface attaches a subnet to an internal router interface. You must
+// specify either a SubnetID or PortID in the request body. If you specify both,
+// the operation will fail and an error will be returned.
+//
+// If you specify a SubnetID, the gateway IP address for that particular subnet
+// is used to create the router interface. Alternatively, if you specify a
+// PortID, the IP address associated with the port is used to create the router
+// interface.
+//
+// If you reference a port that is associated with multiple IP addresses, or
+// if the port is associated with zero IP addresses, the operation will fail and
+// a 400 Bad Request error will be returned.
+//
+// If you reference a port already in use, the operation will fail and a 409
+// Conflict error will be returned.
+//
+// The PortID that is returned after using Extract() on the result of this
+// operation can either be the same PortID passed in or, on the other hand, the
+// identifier of a new port created by this operation. After the operation
+// completes, the device ID of the port is set to the router ID, and the
+// device owner attribute is set to `network:router_interface'.
+func AddInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult {
+	var res InterfaceResult
+
+	// Validate
+	if (opts.SubnetID == "" && opts.PortID == "") || (opts.SubnetID != "" && opts.PortID != "") {
+		res.Err = errInvalidInterfaceOpts
+		return res
+	}
+
+	type request struct {
+		SubnetID string `json:"subnet_id,omitempty"`
+		PortID   string `json:"port_id,omitempty"`
+	}
+
+	body := request{SubnetID: opts.SubnetID, PortID: opts.PortID}
+
+	_, res.Err = perigee.Request("PUT", addInterfaceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &body,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+
+	return res
+}
+
+// RemoveInterface removes an internal router interface, which detaches a
+// subnet from the router. You must specify either a SubnetID or PortID, since
+// these values are used to identify the router interface to remove.
+//
+// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you
+// choose to specify both, the subnet ID must correspond to the subnet ID of
+// the first IP address on the port specified by the port ID. Otherwise, the
+// operation will fail and return a 409 Conflict error.
+//
+// If the router, subnet or port which are referenced do not exist or are not
+// visible to you, the operation will fail and a 404 Not Found error will be
+// returned. After this operation completes, the port connecting the router
+// with the subnet is removed from the subnet for the network.
+func RemoveInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult {
+	var res InterfaceResult
+
+	type request struct {
+		SubnetID string `json:"subnet_id,omitempty"`
+		PortID   string `json:"port_id,omitempty"`
+	}
+
+	body := request{SubnetID: opts.SubnetID, PortID: opts.PortID}
+
+	_, res.Err = perigee.Request("PUT", removeInterfaceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &body,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+
+	return res
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
new file mode 100755
index 0000000..c34264d
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -0,0 +1,338 @@
+package routers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/routers", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "routers": [
+        {
+            "status": "ACTIVE",
+            "external_gateway_info": null,
+            "name": "second_routers",
+            "admin_state_up": true,
+            "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+            "id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b"
+        },
+        {
+            "status": "ACTIVE",
+            "external_gateway_info": {
+                "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"
+            },
+            "name": "router1",
+            "admin_state_up": true,
+            "tenant_id": "33a40233088643acb66ff6eb0ebea679",
+            "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRouters(page)
+		if err != nil {
+			t.Errorf("Failed to extract routers: %v", err)
+			return false, err
+		}
+
+		expected := []Router{
+			Router{
+				Status:       "ACTIVE",
+				GatewayInfo:  GatewayInfo{NetworkID: ""},
+				AdminStateUp: true,
+				Name:         "second_routers",
+				ID:           "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b",
+				TenantID:     "6b96ff0cb17a4b859e1e575d221683d3",
+			},
+			Router{
+				Status:       "ACTIVE",
+				GatewayInfo:  GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"},
+				AdminStateUp: true,
+				Name:         "router1",
+				ID:           "a9254bdb-2613-4a13-ac4c-adc581fba50d",
+				TenantID:     "33a40233088643acb66ff6eb0ebea679",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "router":{
+      "name": "foo_router",
+      "admin_state_up": false,
+      "external_gateway_info":{
+         "network_id":"8ca37218-28ff-41cb-9b10-039601ea7e6b"
+      }
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "router": {
+        "status": "ACTIVE",
+        "external_gateway_info": {
+            "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+        },
+        "name": "foo_router",
+        "admin_state_up": false,
+        "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+        "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+    }
+}
+		`)
+	})
+
+	asu := false
+	gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+
+	options := CreateOpts{
+		Name:         "foo_router",
+		AdminStateUp: &asu,
+		GatewayInfo:  &gwi,
+	}
+	r, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "foo_router", r.Name)
+	th.AssertEquals(t, false, r.AdminStateUp)
+	th.AssertDeepEquals(t, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}, r.GatewayInfo)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers/a07eea83-7710-4860-931b-5fe220fae533", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "router": {
+        "status": "ACTIVE",
+        "external_gateway_info": {
+            "network_id": "85d76829-6415-48ff-9c63-5c5ca8c61ac6"
+        },
+        "name": "router1",
+        "admin_state_up": true,
+        "tenant_id": "d6554fe62e2f41efbb6e026fad5c1542",
+        "id": "a07eea83-7710-4860-931b-5fe220fae533"
+    }
+}
+			`)
+	})
+
+	n, err := Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "85d76829-6415-48ff-9c63-5c5ca8c61ac6"})
+	th.AssertEquals(t, n.Name, "router1")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.TenantID, "d6554fe62e2f41efbb6e026fad5c1542")
+	th.AssertEquals(t, n.ID, "a07eea83-7710-4860-931b-5fe220fae533")
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers/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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "router": {
+			"name": "new_name",
+        "external_gateway_info": {
+            "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+        }
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "router": {
+        "status": "ACTIVE",
+        "external_gateway_info": {
+            "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+        },
+        "name": "new_name",
+        "admin_state_up": true,
+        "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+        "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+    }
+}
+		`)
+	})
+
+	gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+	options := UpdateOpts{Name: "new_name", GatewayInfo: &gwi}
+
+	n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "new_name")
+	th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"})
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers/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)
+	})
+
+	res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestAddInterface(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/add_router_interface", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188",
+    "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+    "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31",
+    "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770"
+}
+`)
+	})
+
+	opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+	res, err := AddInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID)
+	th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID)
+	th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID)
+	th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
+}
+
+func TestAddInterfaceRequiredOpts(t *testing.T) {
+	_, err := AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	_, err = AddInterface(fake.ServiceClient(), "foo", InterfaceOpts{SubnetID: "bar", PortID: "baz"}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestRemoveInterface(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/remove_router_interface", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188",
+		"tenant_id": "017d8de156df4177889f31a9bd6edc00",
+		"port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31",
+		"id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770"
+}
+`)
+	})
+
+	opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+	res, err := RemoveInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID)
+	th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID)
+	th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID)
+	th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go
new file mode 100755
index 0000000..ba2f757
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -0,0 +1,159 @@
+package routers
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// GatewayInfo represents the information of an external gateway for any
+// particular network router.
+type GatewayInfo struct {
+	NetworkID string `json:"network_id" mapstructure:"network_id"`
+}
+
+// Router represents a Neutron router. A router is a logical entity that
+// forwards packets across internal subnets and NATs (network address
+// translation) them on external networks through an appropriate gateway.
+//
+// A router has an interface for each subnet with which it is associated. By
+// default, the IP address of such interface is the subnet's gateway IP. Also,
+// whenever a router is associated with a subnet, a port for that router
+// interface is added to the subnet's network.
+type Router struct {
+	// Indicates whether or not a router is currently operational.
+	Status string `json:"status" mapstructure:"status"`
+
+	// Information on external gateway for the router.
+	GatewayInfo GatewayInfo `json:"external_gateway_info" mapstructure:"external_gateway_info"`
+
+	// Administrative state of the router.
+	AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+	// Human readable name for the router. Does not have to be unique.
+	Name string `json:"name" mapstructure:"name"`
+
+	// Unique identifier for the router.
+	ID string `json:"id" mapstructure:"id"`
+
+	// Owner of the router. Only admin users can specify a tenant identifier
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// RouterPage is the page returned by a pager when traversing over a
+// collection of routers.
+type RouterPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of routers has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p RouterPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"routers_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p RouterPage) IsEmpty() (bool, error) {
+	is, err := ExtractRouters(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractRouters accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRouters(page pagination.Page) ([]Router, error) {
+	var resp struct {
+		Routers []Router `mapstructure:"routers" json:"routers"`
+	}
+
+	err := mapstructure.Decode(page.(RouterPage).Body, &resp)
+
+	return resp.Routers, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Router, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Router *Router `json:"router"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Router, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// InterfaceInfo represents information about a particular router interface. As
+// mentioned above, in order for a router to forward to a subnet, it needs an
+// interface.
+type InterfaceInfo struct {
+	// The ID of the subnet which this interface is associated with.
+	SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+	// The ID of the port that is a part of the subnet.
+	PortID string `json:"port_id" mapstructure:"port_id"`
+
+	// The UUID of the interface.
+	ID string `json:"id" mapstructure:"id"`
+
+	// Owner of the interface.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// InterfaceResult represents the result of interface operations, such as
+// AddInterface() and RemoveInterface().
+type InterfaceResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts an information struct.
+func (r InterfaceResult) Extract() (*InterfaceInfo, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res *InterfaceInfo
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res, err
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/urls.go b/openstack/networking/v2/extensions/layer3/routers/urls.go
new file mode 100644
index 0000000..bc22c2a
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/urls.go
@@ -0,0 +1,21 @@
+package routers
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "routers"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
+
+func addInterfaceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id, "add_router_interface")
+}
+
+func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id, "remove_router_interface")
+}
diff --git a/openstack/networking/v2/extensions/lbaas/doc.go b/openstack/networking/v2/extensions/lbaas/doc.go
new file mode 100644
index 0000000..bc1fc28
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/doc.go
@@ -0,0 +1,3 @@
+// Package lbaas provides information and interaction with the Load Balancer
+// as a Service extension for the OpenStack Networking service.
+package lbaas
diff --git a/openstack/networking/v2/extensions/lbaas/members/requests.go b/openstack/networking/v2/extensions/lbaas/members/requests.go
new file mode 100644
index 0000000..58ec580
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/requests.go
@@ -0,0 +1,139 @@
+package members
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Status       string `q:"status"`
+	Weight       int    `q:"weight"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	TenantID     string `q:"tenant_id"`
+	PoolID       string `q:"pool_id"`
+	Address      string `q:"address"`
+	ProtocolPort int    `q:"protocol_port"`
+	ID           string `q:"id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// pools. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those pools that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return MemberPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOpts contains all the values needed to create a new pool member.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a pool
+	// for another tenant.
+	TenantID string
+
+	// Required. The IP address of the member.
+	Address string
+
+	// Required. The port on which the application is hosted.
+	ProtocolPort int
+
+	// Required. The pool to which this member will belong.
+	PoolID string
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool member.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	type member struct {
+		TenantID     string `json:"tenant_id"`
+		ProtocolPort int    `json:"protocol_port"`
+		Address      string `json:"address"`
+		PoolID       string `json:"pool_id"`
+	}
+	type request struct {
+		Member member `json:"member"`
+	}
+
+	reqBody := request{Member: member{
+		Address:      opts.Address,
+		TenantID:     opts.TenantID,
+		ProtocolPort: opts.ProtocolPort,
+		PoolID:       opts.PoolID,
+	}}
+
+	var res CreateResult
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+	return res
+}
+
+// Get retrieves a particular pool member based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// UpdateOpts contains the values used when updating a pool member.
+type UpdateOpts struct {
+	// The administrative state of the member, which is up (true) or down (false).
+	AdminStateUp bool
+}
+
+// Update allows members to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type member struct {
+		AdminStateUp bool `json:"admin_state_up"`
+	}
+	type request struct {
+		Member member `json:"member"`
+	}
+
+	reqBody := request{Member: member{AdminStateUp: opts.AdminStateUp}}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular member based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
new file mode 100644
index 0000000..dc1ece3
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
@@ -0,0 +1,243 @@
+package members
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lb/members", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "members":[
+      {
+         "status":"ACTIVE",
+         "weight":1,
+         "admin_state_up":true,
+         "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+         "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab",
+         "address":"10.0.0.4",
+         "protocol_port":80,
+         "id":"701b531b-111a-4f21-ad85-4795b7b12af6"
+      },
+      {
+         "status":"ACTIVE",
+         "weight":1,
+         "admin_state_up":true,
+         "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+         "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab",
+         "address":"10.0.0.3",
+         "protocol_port":80,
+         "id":"beb53b4d-230b-4abd-8118-575b8fa006ef"
+      }
+   ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractMembers(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Member{
+			Member{
+				Status:       "ACTIVE",
+				Weight:       1,
+				AdminStateUp: true,
+				TenantID:     "83657cfcdfe44cd5920adaf26c48ceea",
+				PoolID:       "72741b06-df4d-4715-b142-276b6bce75ab",
+				Address:      "10.0.0.4",
+				ProtocolPort: 80,
+				ID:           "701b531b-111a-4f21-ad85-4795b7b12af6",
+			},
+			Member{
+				Status:       "ACTIVE",
+				Weight:       1,
+				AdminStateUp: true,
+				TenantID:     "83657cfcdfe44cd5920adaf26c48ceea",
+				PoolID:       "72741b06-df4d-4715-b142-276b6bce75ab",
+				Address:      "10.0.0.3",
+				ProtocolPort: 80,
+				ID:           "beb53b4d-230b-4abd-8118-575b8fa006ef",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+  "member": {
+    "tenant_id": "453105b9-1754-413f-aab1-55f1af620750",
+		"pool_id": "foo",
+    "address": "192.0.2.14",
+    "protocol_port":8080
+  }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+  "member": {
+    "id": "975592ca-e308-48ad-8298-731935ee9f45",
+    "address": "192.0.2.14",
+    "protocol_port": 8080,
+    "tenant_id": "453105b9-1754-413f-aab1-55f1af620750",
+    "admin_state_up":true,
+    "weight": 1,
+    "status": "DOWN"
+  }
+}
+    `)
+	})
+
+	options := CreateOpts{
+		TenantID:     "453105b9-1754-413f-aab1-55f1af620750",
+		Address:      "192.0.2.14",
+		ProtocolPort: 8080,
+		PoolID:       "foo",
+	}
+	_, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/members/975592ca-e308-48ad-8298-731935ee9f45", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "member":{
+      "id":"975592ca-e308-48ad-8298-731935ee9f45",
+      "address":"192.0.2.14",
+      "protocol_port":8080,
+      "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+      "admin_state_up":true,
+      "weight":1,
+      "status":"DOWN"
+   }
+}
+      `)
+	})
+
+	m, err := Get(fake.ServiceClient(), "975592ca-e308-48ad-8298-731935ee9f45").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "975592ca-e308-48ad-8298-731935ee9f45", m.ID)
+	th.AssertEquals(t, "192.0.2.14", m.Address)
+	th.AssertEquals(t, 8080, m.ProtocolPort)
+	th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", m.TenantID)
+	th.AssertEquals(t, true, m.AdminStateUp)
+	th.AssertEquals(t, 1, m.Weight)
+	th.AssertEquals(t, "DOWN", m.Status)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "member":{
+      "admin_state_up":false
+   }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "member":{
+      "status":"PENDING_UPDATE",
+      "protocol_port":8080,
+      "weight":1,
+      "admin_state_up":false,
+      "tenant_id":"4fd44f30292945e481c7b8a0c8908869",
+      "pool_id":"7803631d-f181-4500-b3a2-1b68ba2a75fd",
+      "address":"10.0.0.5",
+      "status_description":null,
+      "id":"48a471ea-64f1-4eb6-9be7-dae6bbe40a0f"
+   }
+}
+    `)
+	})
+
+	options := UpdateOpts{AdminStateUp: false}
+
+	_, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/members/results.go b/openstack/networking/v2/extensions/lbaas/members/results.go
new file mode 100644
index 0000000..a4408e1
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -0,0 +1,120 @@
+package members
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Member represents the application running on a backend server.
+type Member struct {
+	// The status of the member. Indicates whether the member is operational.
+	Status string
+
+	// Weight of member.
+	Weight int
+
+	// The administrative state of the member, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+	// Owner of the member. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+	// The pool to which the member belongs.
+	PoolID string `json:"pool_id" mapstructure:"pool_id"`
+
+	// The IP address of the member.
+	Address string
+
+	// The port on which the application is hosted.
+	ProtocolPort int `json:"protocol_port" mapstructure:"protocol_port"`
+
+	// The unique ID for the member.
+	ID string
+}
+
+// MemberPage is the page returned by a pager when traversing over a
+// collection of pool members.
+type MemberPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of members has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p MemberPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"members_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a MemberPage struct is empty.
+func (p MemberPage) IsEmpty() (bool, error) {
+	is, err := ExtractMembers(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractMembers accepts a Page struct, specifically a MemberPage struct,
+// and extracts the elements into a slice of Member structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMembers(page pagination.Page) ([]Member, error) {
+	var resp struct {
+		Members []Member `mapstructure:"members" json:"members"`
+	}
+
+	err := mapstructure.Decode(page.(MemberPage).Body, &resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Members, nil
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Member, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Member *Member `json:"member"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Member, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/lbaas/members/urls.go b/openstack/networking/v2/extensions/lbaas/members/urls.go
new file mode 100644
index 0000000..94b57e4
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/urls.go
@@ -0,0 +1,16 @@
+package members
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "lb"
+	resourcePath = "members"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/openstack/networking/v2/extensions/lbaas/monitors/requests.go
new file mode 100644
index 0000000..e2b590e
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests.go
@@ -0,0 +1,282 @@
+package monitors
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	ID            string `q:"id"`
+	TenantID      string `q:"tenant_id"`
+	Type          string `q:"type"`
+	Delay         int    `q:"delay"`
+	Timeout       int    `q:"timeout"`
+	MaxRetries    int    `q:"max_retries"`
+	HTTPMethod    string `q:"http_method"`
+	URLPath       string `q:"url_path"`
+	ExpectedCodes string `q:"expected_codes"`
+	AdminStateUp  *bool  `q:"admin_state_up"`
+	Status        string `q:"status"`
+	Limit         int    `q:"limit"`
+	Marker        string `q:"marker"`
+	SortKey       string `q:"sort_key"`
+	SortDir       string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return MonitorPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Constants that represent approved monitoring types.
+const (
+	TypePING  = "PING"
+	TypeTCP   = "TCP"
+	TypeHTTP  = "HTTP"
+	TypeHTTPS = "HTTPS"
+)
+
+var (
+	errValidTypeRequired     = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS")
+	errDelayRequired         = fmt.Errorf("Delay is required")
+	errTimeoutRequired       = fmt.Errorf("Timeout is required")
+	errMaxRetriesRequired    = fmt.Errorf("MaxRetries is required")
+	errURLPathRequired       = fmt.Errorf("URL path is required")
+	errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required")
+	errDelayMustGETimeout    = fmt.Errorf("Delay must be greater than or equal to timeout")
+)
+
+// CreateOpts contains all the values needed to create a new health monitor.
+type CreateOpts struct {
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID string
+
+	// Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is
+	// sent by the load balancer to verify the member state.
+	Type string
+
+	// Required. The time, in seconds, between sending probes to members.
+	Delay int
+
+	// Required. Maximum number of seconds for a monitor to wait for a ping reply
+	// before it times out. The value must be less than the delay value.
+	Timeout int
+
+	// Required. Number of permissible ping failures before changing the member's
+	// status to INACTIVE. Must be a number between 1 and 10.
+	MaxRetries int
+
+	// Required for HTTP(S) types. URI path that will be accessed if monitor type
+	// is HTTP or HTTPS.
+	URLPath string
+
+	// Required for HTTP(S) types. The HTTP method used for requests by the
+	// monitor. If this attribute is not specified, it defaults to "GET".
+	HTTPMethod string
+
+	// Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S)
+	// monitor. You can either specify a single status like "200", or a range
+	// like "200-202".
+	ExpectedCodes string
+
+	AdminStateUp *bool
+}
+
+// Create is an operation which provisions a new health monitor. There are
+// different types of monitor you can provision: PING, TCP or HTTP(S). Below
+// are examples of how to create each one.
+//
+// Here is an example config struct to use when creating a PING or TCP monitor:
+//
+// CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3}
+// CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3}
+//
+// Here is an example config struct to use when creating a HTTP(S) monitor:
+//
+// CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3,
+//  HttpMethod: "HEAD", ExpectedCodes: "200"}
+//
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	var res CreateResult
+
+	// Validate inputs
+	allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true}
+	if opts.Type == "" || allowed[opts.Type] == false {
+		res.Err = errValidTypeRequired
+	}
+	if opts.Delay == 0 {
+		res.Err = errDelayRequired
+	}
+	if opts.Timeout == 0 {
+		res.Err = errTimeoutRequired
+	}
+	if opts.MaxRetries == 0 {
+		res.Err = errMaxRetriesRequired
+	}
+	if opts.Type == TypeHTTP || opts.Type == TypeHTTPS {
+		if opts.URLPath == "" {
+			res.Err = errURLPathRequired
+		}
+		if opts.ExpectedCodes == "" {
+			res.Err = errExpectedCodesRequired
+		}
+	}
+	if opts.Delay < opts.Timeout {
+		res.Err = errDelayMustGETimeout
+	}
+	if res.Err != nil {
+		return res
+	}
+
+	type monitor struct {
+		Type          string  `json:"type"`
+		Delay         int     `json:"delay"`
+		Timeout       int     `json:"timeout"`
+		MaxRetries    int     `json:"max_retries"`
+		TenantID      *string `json:"tenant_id,omitempty"`
+		URLPath       *string `json:"url_path,omitempty"`
+		ExpectedCodes *string `json:"expected_codes,omitempty"`
+		HTTPMethod    *string `json:"http_method,omitempty"`
+		AdminStateUp  *bool   `json:"admin_state_up,omitempty"`
+	}
+
+	type request struct {
+		Monitor monitor `json:"health_monitor"`
+	}
+
+	reqBody := request{Monitor: monitor{
+		Type:          opts.Type,
+		Delay:         opts.Delay,
+		Timeout:       opts.Timeout,
+		MaxRetries:    opts.MaxRetries,
+		TenantID:      gophercloud.MaybeString(opts.TenantID),
+		URLPath:       gophercloud.MaybeString(opts.URLPath),
+		ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes),
+		HTTPMethod:    gophercloud.MaybeString(opts.HTTPMethod),
+		AdminStateUp:  opts.AdminStateUp,
+	}}
+
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+
+	return res
+}
+
+// Get retrieves a particular health monitor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// UpdateOpts contains all the values needed to update an existing virtual IP.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+	// Required. The time, in seconds, between sending probes to members.
+	Delay int
+
+	// Required. Maximum number of seconds for a monitor to wait for a ping reply
+	// before it times out. The value must be less than the delay value.
+	Timeout int
+
+	// Required. Number of permissible ping failures before changing the member's
+	// status to INACTIVE. Must be a number between 1 and 10.
+	MaxRetries int
+
+	// Required for HTTP(S) types. URI path that will be accessed if monitor type
+	// is HTTP or HTTPS.
+	URLPath string
+
+	// Required for HTTP(S) types. The HTTP method used for requests by the
+	// monitor. If this attribute is not specified, it defaults to "GET".
+	HTTPMethod string
+
+	// Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S)
+	// monitor. You can either specify a single status like "200", or a range
+	// like "200-202".
+	ExpectedCodes string
+
+	AdminStateUp *bool
+}
+
+// Update is an operation which modifies the attributes of the specified monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	var res UpdateResult
+
+	if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout {
+		res.Err = errDelayMustGETimeout
+	}
+
+	type monitor struct {
+		Delay         int     `json:"delay"`
+		Timeout       int     `json:"timeout"`
+		MaxRetries    int     `json:"max_retries"`
+		URLPath       *string `json:"url_path,omitempty"`
+		ExpectedCodes *string `json:"expected_codes,omitempty"`
+		HTTPMethod    *string `json:"http_method,omitempty"`
+		AdminStateUp  *bool   `json:"admin_state_up,omitempty"`
+	}
+
+	type request struct {
+		Monitor monitor `json:"health_monitor"`
+	}
+
+	reqBody := request{Monitor: monitor{
+		Delay:         opts.Delay,
+		Timeout:       opts.Timeout,
+		MaxRetries:    opts.MaxRetries,
+		URLPath:       gophercloud.MaybeString(opts.URLPath),
+		ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes),
+		HTTPMethod:    gophercloud.MaybeString(opts.HTTPMethod),
+		AdminStateUp:  opts.AdminStateUp,
+	}}
+
+	_, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 202},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a particular monitor based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
new file mode 100644
index 0000000..79a99bf
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
@@ -0,0 +1,312 @@
+package monitors
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lb/health_monitors", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "health_monitors":[
+      {
+         "admin_state_up":true,
+         "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+         "delay":10,
+         "max_retries":1,
+         "timeout":1,
+         "type":"PING",
+         "id":"466c8345-28d8-4f84-a246-e04380b0461d"
+      },
+      {
+         "admin_state_up":true,
+         "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+         "delay":5,
+         "expected_codes":"200",
+         "max_retries":2,
+         "http_method":"GET",
+         "timeout":2,
+         "url_path":"/",
+         "type":"HTTP",
+         "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+      }
+   ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractMonitors(page)
+		if err != nil {
+			t.Errorf("Failed to extract monitors: %v", err)
+			return false, err
+		}
+
+		expected := []Monitor{
+			Monitor{
+				AdminStateUp: true,
+				TenantID:     "83657cfcdfe44cd5920adaf26c48ceea",
+				Delay:        10,
+				MaxRetries:   1,
+				Timeout:      1,
+				Type:         "PING",
+				ID:           "466c8345-28d8-4f84-a246-e04380b0461d",
+			},
+			Monitor{
+				AdminStateUp:  true,
+				TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+				Delay:         5,
+				ExpectedCodes: "200",
+				MaxRetries:    2,
+				Timeout:       2,
+				URLPath:       "/",
+				Type:          "HTTP",
+				HTTPMethod:    "GET",
+				ID:            "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) {
+	_, err := Create(fake.ServiceClient(), CreateOpts{
+		Type:          "HTTP",
+		Delay:         1,
+		Timeout:       10,
+		MaxRetries:    5,
+		URLPath:       "/check",
+		ExpectedCodes: "200-299",
+	}).Extract()
+
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	_, err = Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", UpdateOpts{
+		Delay:   1,
+		Timeout: 10,
+	}).Extract()
+
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "health_monitor":{
+      "type":"HTTP",
+      "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+      "delay":20,
+      "timeout":10,
+      "max_retries":5,
+      "url_path":"/check",
+      "expected_codes":"200-299"
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+   "health_monitor":{
+      "id":"f3eeab00-8367-4524-b662-55e64d4cacb5",
+      "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+      "type":"HTTP",
+      "delay":20,
+      "timeout":10,
+      "max_retries":5,
+      "http_method":"GET",
+      "url_path":"/check",
+      "expected_codes":"200-299",
+      "admin_state_up":true,
+      "status":"ACTIVE"
+   }
+}
+		`)
+	})
+
+	_, err := Create(fake.ServiceClient(), CreateOpts{
+		Type:          "HTTP",
+		TenantID:      "453105b9-1754-413f-aab1-55f1af620750",
+		Delay:         20,
+		Timeout:       10,
+		MaxRetries:    5,
+		URLPath:       "/check",
+		ExpectedCodes: "200-299",
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "health_monitor":{
+      "id":"f3eeab00-8367-4524-b662-55e64d4cacb5",
+      "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+      "type":"HTTP",
+      "delay":20,
+      "timeout":10,
+      "max_retries":5,
+      "http_method":"GET",
+      "url_path":"/check",
+      "expected_codes":"200-299",
+      "admin_state_up":true,
+      "status":"ACTIVE"
+   }
+}
+			`)
+	})
+
+	hm, err := Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID)
+	th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID)
+	th.AssertEquals(t, "HTTP", hm.Type)
+	th.AssertEquals(t, 20, hm.Delay)
+	th.AssertEquals(t, 10, hm.Timeout)
+	th.AssertEquals(t, 5, hm.MaxRetries)
+	th.AssertEquals(t, "GET", hm.HTTPMethod)
+	th.AssertEquals(t, "/check", hm.URLPath)
+	th.AssertEquals(t, "200-299", hm.ExpectedCodes)
+	th.AssertEquals(t, true, hm.AdminStateUp)
+	th.AssertEquals(t, "ACTIVE", hm.Status)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "health_monitor":{
+      "delay": 3,
+      "timeout": 20,
+      "max_retries": 10,
+      "url_path": "/another_check",
+      "expected_codes": "301"
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+    "health_monitor": {
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "delay": 3,
+        "max_retries": 10,
+        "http_method": "GET",
+        "timeout": 20,
+        "pools": [
+            {
+                "status": "PENDING_CREATE",
+                "status_description": null,
+                "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df"
+            }
+        ],
+        "type": "PING",
+        "id": "b05e44b5-81f9-4551-b474-711a722698f7"
+    }
+}
+		`)
+	})
+
+	_, err := Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", UpdateOpts{
+		Delay:         3,
+		Timeout:       20,
+		MaxRetries:    10,
+		URLPath:       "/another_check",
+		ExpectedCodes: "301",
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/results.go b/openstack/networking/v2/extensions/lbaas/monitors/results.go
new file mode 100644
index 0000000..bd711f7
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -0,0 +1,145 @@
+package monitors
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Monitor represents a load balancer health monitor. A health monitor is used
+// to determine whether or not back-end members of the VIP's pool are usable
+// for processing a request. A pool can have several health monitors associated
+// with it. There are different types of health monitors supported:
+//
+// PING: used to ping the members using ICMP.
+// TCP: used to connect to the members using TCP.
+// HTTP: used to send an HTTP request to the member.
+// HTTPS: used to send a secure HTTP request to the member.
+//
+// When a pool has several monitors associated with it, each member of the pool
+// is monitored by all these monitors. If any monitor declares the member as
+// unhealthy, then the member status is changed to INACTIVE and the member
+// won't participate in its pool's load balancing. In other words, ALL monitors
+// must declare the member to be healthy for it to stay ACTIVE.
+type Monitor struct {
+	// The unique ID for the VIP.
+	ID string
+
+	// Owner of the VIP. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+	// The type of probe sent by the load balancer to verify the member state,
+	// which is PING, TCP, HTTP, or HTTPS.
+	Type string
+
+	// The time, in seconds, between sending probes to members.
+	Delay int
+
+	// The maximum number of seconds for a monitor to wait for a connection to be
+	// established before it times out. This value must be less than the delay value.
+	Timeout int
+
+	// Number of allowed connection failures before changing the status of the
+	// member to INACTIVE. A valid value is from 1 to 10.
+	MaxRetries int `json:"max_retries" mapstructure:"max_retries"`
+
+	// The HTTP method that the monitor uses for requests.
+	HTTPMethod string `json:"http_method" mapstructure:"http_method"`
+
+	// The HTTP path of the request sent by the monitor to test the health of a
+	// member. Must be a string beginning with a forward slash (/).
+	URLPath string `json:"url_path" mapstructure:"url_path"`
+
+	// Expected HTTP codes for a passing HTTP(S) monitor.
+	ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"`
+
+	// The administrative state of the health monitor, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+	// The status of the health monitor. Indicates whether the health monitor is
+	// operational.
+	Status string
+}
+
+// MonitorPage is the page returned by a pager when traversing over a
+// collection of health monitors.
+type MonitorPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of monitors has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p MonitorPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"health_monitors_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p MonitorPage) IsEmpty() (bool, error) {
+	is, err := ExtractMonitors(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct,
+// and extracts the elements into a slice of Monitor structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMonitors(page pagination.Page) ([]Monitor, error) {
+	var resp struct {
+		Monitors []Monitor `mapstructure:"health_monitors" json:"health_monitors"`
+	}
+
+	err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
+
+	return resp.Monitors, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a monitor.
+func (r commonResult) Extract() (*Monitor, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Monitor *Monitor `json:"health_monitor" mapstructure:"health_monitor"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Monitor, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
new file mode 100644
index 0000000..46e84bb
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "lb"
+	resourcePath = "health_monitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests.go b/openstack/networking/v2/extensions/lbaas/pools/requests.go
new file mode 100644
index 0000000..ca8d33b
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests.go
@@ -0,0 +1,205 @@
+package pools
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Status       string `q:"status"`
+	LBMethod     string `q:"lb_method"`
+	Protocol     string `q:"protocol"`
+	SubnetID     string `q:"subnet_id"`
+	TenantID     string `q:"tenant_id"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	Name         string `q:"name"`
+	ID           string `q:"id"`
+	VIPID        string `q:"vip_id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// pools. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those pools that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return PoolPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Supported attributes for create/update operations.
+const (
+	LBMethodRoundRobin       = "ROUND_ROBIN"
+	LBMethodLeastConnections = "LEAST_CONNECTIONS"
+
+	ProtocolTCP   = "TCP"
+	ProtocolHTTP  = "HTTP"
+	ProtocolHTTPS = "HTTPS"
+)
+
+// CreateOpts contains all the values needed to create a new pool.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a pool
+	// for another tenant.
+	TenantID string
+
+	// Required. Name of the pool.
+	Name string
+
+	// Required. The protocol used by the pool members, you can use either
+	// ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+	Protocol string
+
+	// The network on which the members of the pool will be located. Only members
+	// that are on this network can be added to the pool.
+	SubnetID string
+
+	// The algorithm used to distribute load between the members of the pool. The
+	// current specification supports LBMethodRoundRobin and
+	// LBMethodLeastConnections as valid values for this attribute.
+	LBMethod string
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	type pool struct {
+		Name     string `json:"name"`
+		TenantID string `json:"tenant_id,omitempty"`
+		Protocol string `json:"protocol"`
+		SubnetID string `json:"subnet_id"`
+		LBMethod string `json:"lb_method"`
+	}
+	type request struct {
+		Pool pool `json:"pool"`
+	}
+
+	reqBody := request{Pool: pool{
+		Name:     opts.Name,
+		TenantID: opts.TenantID,
+		Protocol: opts.Protocol,
+		SubnetID: opts.SubnetID,
+		LBMethod: opts.LBMethod,
+	}}
+
+	var res CreateResult
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+	return res
+}
+
+// Get retrieves a particular pool based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// UpdateOpts contains the values used when updating a pool.
+type UpdateOpts struct {
+	// Required. Name of the pool.
+	Name string
+
+	// The algorithm used to distribute load between the members of the pool. The
+	// current specification supports LBMethodRoundRobin and
+	// LBMethodLeastConnections as valid values for this attribute.
+	LBMethod string
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type pool struct {
+		Name     string `json:"name,"`
+		LBMethod string `json:"lb_method"`
+	}
+	type request struct {
+		Pool pool `json:"pool"`
+	}
+
+	reqBody := request{Pool: pool{
+		Name:     opts.Name,
+		LBMethod: opts.LBMethod,
+	}}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular pool based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
+
+// AssociateMonitor will associate a health monitor with a particular pool.
+// Once associated, the health monitor will start monitoring the members of the
+// pool and will deactivate these members if they are deemed unhealthy. A
+// member can be deactivated (status set to INACTIVE) if any of health monitors
+// finds it unhealthy.
+func AssociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult {
+	type hm struct {
+		ID string `json:"id"`
+	}
+	type request struct {
+		Monitor hm `json:"health_monitor"`
+	}
+
+	reqBody := request{hm{ID: monitorID}}
+
+	var res AssociateResult
+	_, res.Err = perigee.Request("POST", associateURL(c, poolID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+	return res
+}
+
+// DisassociateMonitor will disassociate a health monitor with a particular
+// pool. When dissociation is successful, the health monitor will no longer
+// check for the health of the members of the pool.
+func DisassociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult {
+	var res AssociateResult
+	_, res.Err = perigee.Request("DELETE", disassociateURL(c, poolID, monitorID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
new file mode 100644
index 0000000..6da29a6
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
@@ -0,0 +1,317 @@
+package pools
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lb/pools", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "pools":[
+      {
+         "status":"ACTIVE",
+         "lb_method":"ROUND_ROBIN",
+         "protocol":"HTTP",
+         "description":"",
+         "health_monitors":[
+            "466c8345-28d8-4f84-a246-e04380b0461d",
+            "5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+         ],
+         "members":[
+            "701b531b-111a-4f21-ad85-4795b7b12af6",
+            "beb53b4d-230b-4abd-8118-575b8fa006ef"
+         ],
+         "status_description": null,
+         "id":"72741b06-df4d-4715-b142-276b6bce75ab",
+         "vip_id":"4ec89087-d057-4e2c-911f-60a3b47ee304",
+         "name":"app_pool",
+         "admin_state_up":true,
+         "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+         "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+         "health_monitors_status": [],
+         "provider": "haproxy"
+      }
+   ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPools(page)
+		if err != nil {
+			t.Errorf("Failed to extract pools: %v", err)
+			return false, err
+		}
+
+		expected := []Pool{
+			Pool{
+				Status:      "ACTIVE",
+				LBMethod:    "ROUND_ROBIN",
+				Protocol:    "HTTP",
+				Description: "",
+				MonitorIDs: []string{
+					"466c8345-28d8-4f84-a246-e04380b0461d",
+					"5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+				},
+				SubnetID:     "8032909d-47a1-4715-90af-5153ffe39861",
+				TenantID:     "83657cfcdfe44cd5920adaf26c48ceea",
+				AdminStateUp: true,
+				Name:         "app_pool",
+				MemberIDs: []string{
+					"701b531b-111a-4f21-ad85-4795b7b12af6",
+					"beb53b4d-230b-4abd-8118-575b8fa006ef",
+				},
+				ID:       "72741b06-df4d-4715-b142-276b6bce75ab",
+				VIPID:    "4ec89087-d057-4e2c-911f-60a3b47ee304",
+				Provider: "haproxy",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "pool": {
+        "lb_method": "ROUND_ROBIN",
+        "protocol": "HTTP",
+        "name": "Example pool",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "pool": {
+        "status": "PENDING_CREATE",
+        "lb_method": "ROUND_ROBIN",
+        "protocol": "HTTP",
+        "description": "",
+        "health_monitors": [],
+        "members": [],
+        "status_description": null,
+        "id": "69055154-f603-4a28-8951-7cc2d9e54a9a",
+        "vip_id": null,
+        "name": "Example pool",
+        "admin_state_up": true,
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "health_monitors_status": []
+    }
+}
+		`)
+	})
+
+	options := CreateOpts{
+		LBMethod: LBMethodRoundRobin,
+		Protocol: "HTTP",
+		Name:     "Example pool",
+		SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+	}
+	p, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "PENDING_CREATE", p.Status)
+	th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod)
+	th.AssertEquals(t, "HTTP", p.Protocol)
+	th.AssertEquals(t, "", p.Description)
+	th.AssertDeepEquals(t, []string{}, p.MonitorIDs)
+	th.AssertDeepEquals(t, []string{}, p.MemberIDs)
+	th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID)
+	th.AssertEquals(t, "Example pool", p.Name)
+	th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID)
+	th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "pool":{
+      "id":"332abe93-f488-41ba-870b-2ac66be7f853",
+      "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995",
+      "name":"Example pool",
+      "description":"",
+      "protocol":"tcp",
+      "lb_algorithm":"ROUND_ROBIN",
+      "session_persistence":{
+      },
+      "healthmonitor_id":null,
+      "members":[
+      ],
+      "admin_state_up":true,
+      "status":"ACTIVE"
+   }
+}
+			`)
+	})
+
+	n, err := Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853")
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "pool":{
+      "name":"SuperPool",
+      "lb_method": "LEAST_CONNECTIONS"
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "pool":{
+      "status":"PENDING_UPDATE",
+      "lb_method":"LEAST_CONNECTIONS",
+      "protocol":"TCP",
+      "description":"",
+      "health_monitors":[
+
+      ],
+      "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+      "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+      "admin_state_up":true,
+      "name":"SuperPool",
+      "members":[
+
+      ],
+      "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+      "vip_id":null
+   }
+}
+		`)
+	})
+
+	options := UpdateOpts{Name: "SuperPool", LBMethod: LBMethodLeastConnections}
+
+	n, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "SuperPool", n.Name)
+	th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestAssociateHealthMonitor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "health_monitor":{
+      "id":"b624decf-d5d3-4c66-9a3d-f047e7786181"
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	_, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisassociateHealthMonitor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors/b624decf-d5d3-4c66-9a3d-f047e7786181", 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)
+	})
+
+	res := DisassociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/results.go b/openstack/networking/v2/extensions/lbaas/pools/results.go
new file mode 100644
index 0000000..3b1c66c
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -0,0 +1,144 @@
+package pools
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Pool represents a logical set of devices, such as web servers, that you
+// group together to receive and process traffic. The load balancing function
+// chooses a member of the pool according to the configured load balancing
+// method to handle the new requests or connections received on the VIP address.
+// There is only one pool per virtual IP.
+type Pool struct {
+	// The status of the pool. Indicates whether the pool is operational.
+	Status string
+
+	// The load-balancer algorithm, which is round-robin, least-connections, and
+	// so on. This value, which must be supported, is dependent on the provider.
+	// Round-robin must be supported.
+	LBMethod string `json:"lb_method" mapstructure:"lb_method"`
+
+	// The protocol of the pool, which is TCP, HTTP, or HTTPS.
+	Protocol string
+
+	// Description for the pool.
+	Description string
+
+	// The IDs of associated monitors which check the health of the pool members.
+	MonitorIDs []string `json:"health_monitors" mapstructure:"health_monitors"`
+
+	// The network on which the members of the pool will be located. Only members
+	// that are on this network can be added to the pool.
+	SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+	// Owner of the pool. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+	// The administrative state of the pool, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+	// Pool name. Does not have to be unique.
+	Name string
+
+	// List of member IDs that belong to the pool.
+	MemberIDs []string `json:"members" mapstructure:"members"`
+
+	// The unique ID for the pool.
+	ID string
+
+	// The ID of the virtual IP associated with this pool
+	VIPID string `json:"vip_id" mapstructure:"vip_id"`
+
+	// The provider
+	Provider string
+}
+
+// PoolPage is the page returned by a pager when traversing over a
+// collection of pools.
+type PoolPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of pools has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p PoolPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"pools_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p PoolPage) IsEmpty() (bool, error) {
+	is, err := ExtractPools(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractPools accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPools(page pagination.Page) ([]Pool, error) {
+	var resp struct {
+		Pools []Pool `mapstructure:"pools" json:"pools"`
+	}
+
+	err := mapstructure.Decode(page.(PoolPage).Body, &resp)
+
+	return resp.Pools, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Pool, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Pool *Pool `json:"pool"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Pool, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// AssociateResult represents the result of an association operation.
+type AssociateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/urls.go b/openstack/networking/v2/extensions/lbaas/pools/urls.go
new file mode 100644
index 0000000..6cd15b0
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/urls.go
@@ -0,0 +1,25 @@
+package pools
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "lb"
+	resourcePath = "pools"
+	monitorPath  = "health_monitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
+
+func associateURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id, monitorPath)
+}
+
+func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string {
+	return c.ServiceURL(rootPath, resourcePath, poolID, monitorPath, monitorID)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/requests.go b/openstack/networking/v2/extensions/lbaas/vips/requests.go
new file mode 100644
index 0000000..ec929d6
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests.go
@@ -0,0 +1,273 @@
+package vips
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	ID              string `q:"id"`
+	Name            string `q:"name"`
+	AdminStateUp    *bool  `q:"admin_state_up"`
+	Status          string `q:"status"`
+	TenantID        string `q:"tenant_id"`
+	SubnetID        string `q:"subnet_id"`
+	Address         string `q:"address"`
+	PortID          string `q:"port_id"`
+	Protocol        string `q:"protocol"`
+	ProtocolPort    int    `q:"protocol_port"`
+	ConnectionLimit int    `q:"connection_limit"`
+	Limit           int    `q:"limit"`
+	Marker          string `q:"marker"`
+	SortKey         string `q:"sort_key"`
+	SortDir         string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return VIPPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+var (
+	errNameRequired         = fmt.Errorf("Name is required")
+	errSubnetIDRequried     = fmt.Errorf("SubnetID is required")
+	errProtocolRequired     = fmt.Errorf("Protocol is required")
+	errProtocolPortRequired = fmt.Errorf("Protocol port is required")
+	errPoolIDRequired       = fmt.Errorf("PoolID is required")
+)
+
+// CreateOpts contains all the values needed to create a new virtual IP.
+type CreateOpts struct {
+	// Required. Human-readable name for the VIP. Does not have to be unique.
+	Name string
+
+	// Required. The network on which to allocate the VIP's address. A tenant can
+	// only create VIPs on networks authorized by policy (e.g. networks that
+	// belong to them or networks that are shared).
+	SubnetID string
+
+	// Required. The protocol - can either be TCP, HTTP or HTTPS.
+	Protocol string
+
+	// Required. The port on which to listen for client traffic.
+	ProtocolPort int
+
+	// Required. The ID of the pool with which the VIP is associated.
+	PoolID string
+
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID string
+
+	// Optional. The IP address of the VIP.
+	Address string
+
+	// Optional. Human-readable description for the VIP.
+	Description string
+
+	// Optional. Omit this field to prevent session persistence.
+	Persistence *SessionPersistence
+
+	// Optional. The maximum number of connections allowed for the VIP.
+	ConnLimit *int
+
+	// Optional. The administrative state of the VIP. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool
+}
+
+// Create is an operation which provisions a new virtual IP based on the
+// configuration defined in the CreateOpts struct. Once the request is
+// validated and progress has started on the provisioning process, a
+// CreateResult will be returned.
+//
+// Please note that the PoolID should refer to a pool that is not already
+// associated with another vip. If the pool is already used by another vip,
+// then the operation will fail with a 409 Conflict error will be returned.
+//
+// Users with an admin role can create VIPs on behalf of other tenants by
+// specifying a TenantID attribute different than their own.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	var res CreateResult
+
+	// Validate required opts
+	if opts.Name == "" {
+		res.Err = errNameRequired
+		return res
+	}
+	if opts.SubnetID == "" {
+		res.Err = errSubnetIDRequried
+		return res
+	}
+	if opts.Protocol == "" {
+		res.Err = errProtocolRequired
+		return res
+	}
+	if opts.ProtocolPort == 0 {
+		res.Err = errProtocolPortRequired
+		return res
+	}
+	if opts.PoolID == "" {
+		res.Err = errPoolIDRequired
+		return res
+	}
+
+	type vip struct {
+		Name         string              `json:"name"`
+		SubnetID     string              `json:"subnet_id"`
+		Protocol     string              `json:"protocol"`
+		ProtocolPort int                 `json:"protocol_port"`
+		PoolID       string              `json:"pool_id"`
+		Description  *string             `json:"description,omitempty"`
+		TenantID     *string             `json:"tenant_id,omitempty"`
+		Address      *string             `json:"address,omitempty"`
+		Persistence  *SessionPersistence `json:"session_persistence,omitempty"`
+		ConnLimit    *int                `json:"connection_limit,omitempty"`
+		AdminStateUp *bool               `json:"admin_state_up,omitempty"`
+	}
+
+	type request struct {
+		VirtualIP vip `json:"vip"`
+	}
+
+	reqBody := request{VirtualIP: vip{
+		Name:         opts.Name,
+		SubnetID:     opts.SubnetID,
+		Protocol:     opts.Protocol,
+		ProtocolPort: opts.ProtocolPort,
+		PoolID:       opts.PoolID,
+		Description:  gophercloud.MaybeString(opts.Description),
+		TenantID:     gophercloud.MaybeString(opts.TenantID),
+		Address:      gophercloud.MaybeString(opts.Address),
+		ConnLimit:    opts.ConnLimit,
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	if opts.Persistence != nil {
+		reqBody.VirtualIP.Persistence = opts.Persistence
+	}
+
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+
+	return res
+}
+
+// Get retrieves a particular virtual IP based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// UpdateOpts contains all the values needed to update an existing virtual IP.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+	// Human-readable name for the VIP. Does not have to be unique.
+	Name string
+
+	// Required. The ID of the pool with which the VIP is associated.
+	PoolID string
+
+	// Optional. Human-readable description for the VIP.
+	Description string
+
+	// Optional. Omit this field to prevent session persistence.
+	Persistence *SessionPersistence
+
+	// Optional. The maximum number of connections allowed for the VIP.
+	ConnLimit *int
+
+	// Optional. The administrative state of the VIP. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool
+}
+
+// Update is an operation which modifies the attributes of the specified VIP.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type vip struct {
+		Name         string              `json:"name,omitempty"`
+		PoolID       string              `json:"pool_id,omitempty"`
+		Description  *string             `json:"description,omitempty"`
+		Persistence  *SessionPersistence `json:"session_persistence,omitempty"`
+		ConnLimit    *int                `json:"connection_limit,omitempty"`
+		AdminStateUp *bool               `json:"admin_state_up,omitempty"`
+	}
+
+	type request struct {
+		VirtualIP vip `json:"vip"`
+	}
+
+	reqBody := request{VirtualIP: vip{
+		Name:         opts.Name,
+		PoolID:       opts.PoolID,
+		Description:  gophercloud.MaybeString(opts.Description),
+		ConnLimit:    opts.ConnLimit,
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	if opts.Persistence != nil {
+		reqBody.VirtualIP.Persistence = opts.Persistence
+	}
+
+	var res UpdateResult
+	_, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 202},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a particular virtual IP based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
new file mode 100644
index 0000000..430f1a1
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
@@ -0,0 +1,336 @@
+package vips
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips", rootURL(fake.ServiceClient()))
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "vips":[
+         {
+           "id": "db902c0c-d5ff-4753-b465-668ad9656918",
+           "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+           "name": "web_vip",
+           "description": "lb config for the web tier",
+           "subnet_id": "96a4386a-f8c3-42ed-afce-d7954eee77b3",
+           "address" : "10.30.176.47",
+           "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+           "protocol": "HTTP",
+           "protocol_port": 80,
+           "pool_id" : "cfc6589d-f949-4c66-99d2-c2da56ef3764",
+           "admin_state_up": true,
+           "status": "ACTIVE"
+         },
+         {
+           "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+           "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+           "name": "db_vip",
+					 "description": "lb config for the db tier",
+           "subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+           "address" : "10.30.176.48",
+           "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+           "protocol": "TCP",
+           "protocol_port": 3306,
+           "pool_id" : "41efe233-7591-43c5-9cf7-923964759f9e",
+           "session_persistence" : {"type" : "SOURCE_IP"},
+           "connection_limit" : 2000,
+           "admin_state_up": true,
+           "status": "INACTIVE"
+         }
+      ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVIPs(page)
+		if err != nil {
+			t.Errorf("Failed to extract LBs: %v", err)
+			return false, err
+		}
+
+		expected := []VirtualIP{
+			VirtualIP{
+				ID:           "db902c0c-d5ff-4753-b465-668ad9656918",
+				TenantID:     "310df60f-2a10-4ee5-9554-98393092194c",
+				Name:         "web_vip",
+				Description:  "lb config for the web tier",
+				SubnetID:     "96a4386a-f8c3-42ed-afce-d7954eee77b3",
+				Address:      "10.30.176.47",
+				PortID:       "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+				Protocol:     "HTTP",
+				ProtocolPort: 80,
+				PoolID:       "cfc6589d-f949-4c66-99d2-c2da56ef3764",
+				Persistence:  SessionPersistence{},
+				ConnLimit:    0,
+				AdminStateUp: true,
+				Status:       "ACTIVE",
+			},
+			VirtualIP{
+				ID:           "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+				TenantID:     "310df60f-2a10-4ee5-9554-98393092194c",
+				Name:         "db_vip",
+				Description:  "lb config for the db tier",
+				SubnetID:     "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+				Address:      "10.30.176.48",
+				PortID:       "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+				Protocol:     "TCP",
+				ProtocolPort: 3306,
+				PoolID:       "41efe233-7591-43c5-9cf7-923964759f9e",
+				Persistence:  SessionPersistence{Type: "SOURCE_IP"},
+				ConnLimit:    2000,
+				AdminStateUp: true,
+				Status:       "INACTIVE",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "vip": {
+        "protocol": "HTTP",
+        "name": "NewVip",
+        "admin_state_up": true,
+        "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+        "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+        "protocol_port": 80,
+				"session_persistence": {"type": "SOURCE_IP"}
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "vip": {
+        "status": "PENDING_CREATE",
+        "protocol": "HTTP",
+        "description": "",
+        "admin_state_up": true,
+        "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+        "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+        "connection_limit": -1,
+        "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+        "address": "10.0.0.11",
+        "protocol_port": 80,
+        "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5",
+        "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2",
+        "name": "NewVip"
+    }
+}
+		`)
+	})
+
+	opts := CreateOpts{
+		Protocol:     "HTTP",
+		Name:         "NewVip",
+		AdminStateUp: Up,
+		SubnetID:     "8032909d-47a1-4715-90af-5153ffe39861",
+		PoolID:       "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+		ProtocolPort: 80,
+		Persistence:  &SessionPersistence{Type: "SOURCE_IP"},
+	}
+
+	r, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "PENDING_CREATE", r.Status)
+	th.AssertEquals(t, "HTTP", r.Protocol)
+	th.AssertEquals(t, "", r.Description)
+	th.AssertEquals(t, true, r.AdminStateUp)
+	th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.SubnetID)
+	th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID)
+	th.AssertEquals(t, -1, r.ConnLimit)
+	th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.PoolID)
+	th.AssertEquals(t, "10.0.0.11", r.Address)
+	th.AssertEquals(t, 80, r.ProtocolPort)
+	th.AssertEquals(t, "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", r.PortID)
+	th.AssertEquals(t, "c987d2be-9a3c-4ac9-a046-e8716b1350e2", r.ID)
+	th.AssertEquals(t, "NewVip", r.Name)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar", ProtocolPort: 80})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "vip": {
+        "status": "ACTIVE",
+        "protocol": "HTTP",
+        "description": "",
+        "admin_state_up": true,
+        "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+        "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+        "connection_limit": 1000,
+        "pool_id": "72741b06-df4d-4715-b142-276b6bce75ab",
+        "session_persistence": {
+            "cookie_name": "MyAppCookie",
+            "type": "APP_COOKIE"
+        },
+        "address": "10.0.0.10",
+        "protocol_port": 80,
+        "port_id": "b5a743d6-056b-468b-862d-fb13a9aa694e",
+        "id": "4ec89087-d057-4e2c-911f-60a3b47ee304",
+        "name": "my-vip"
+    }
+}
+			`)
+	})
+
+	vip, err := Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "ACTIVE", vip.Status)
+	th.AssertEquals(t, "HTTP", vip.Protocol)
+	th.AssertEquals(t, "", vip.Description)
+	th.AssertEquals(t, true, vip.AdminStateUp)
+	th.AssertEquals(t, 1000, vip.ConnLimit)
+	th.AssertEquals(t, SessionPersistence{Type: "APP_COOKIE", CookieName: "MyAppCookie"}, vip.Persistence)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "vip": {
+        "connection_limit": 1000,
+				"session_persistence": {"type": "SOURCE_IP"}
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+    "vip": {
+        "status": "PENDING_UPDATE",
+        "protocol": "HTTP",
+        "description": "",
+        "admin_state_up": true,
+        "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+        "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+        "connection_limit": 1000,
+        "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+        "address": "10.0.0.11",
+        "protocol_port": 80,
+        "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5",
+        "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2",
+        "name": "NewVip"
+    }
+}
+		`)
+	})
+
+	i1000 := 1000
+	options := UpdateOpts{
+		ConnLimit:   &i1000,
+		Persistence: &SessionPersistence{Type: "SOURCE_IP"},
+	}
+	vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "PENDING_UPDATE", vip.Status)
+	th.AssertEquals(t, 1000, vip.ConnLimit)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/results.go b/openstack/networking/v2/extensions/lbaas/vips/results.go
new file mode 100644
index 0000000..731edef
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -0,0 +1,164 @@
+package vips
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SessionPersistence represents the session persistence feature of the load
+// balancing service. It attempts to force connections or requests in the same
+// session to be processed by the same member as long as it is ative. Three
+// types of persistence are supported:
+//
+// SOURCE_IP:   With this mode, all connections originating from the same source
+//              IP address, will be handled by the same member of the pool.
+// HTTP_COOKIE: With this persistence mode, the load balancing function will
+//              create a cookie on the first request from a client. Subsequent
+//              requests containing the same cookie value will be handled by
+//              the same member of the pool.
+// APP_COOKIE:  With this persistence mode, the load balancing function will
+//              rely on a cookie established by the backend application. All
+//              requests carrying the same cookie value will be handled by the
+//              same member of the pool.
+type SessionPersistence struct {
+	// The type of persistence mode
+	Type string `mapstructure:"type" json:"type"`
+
+	// Name of cookie if persistence mode is set appropriately
+	CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"`
+}
+
+// VirtualIP is the primary load balancing configuration object that specifies
+// the virtual IP address and port on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+// This entity is sometimes known in LB products under the name of a "virtual
+// server", a "vserver" or a "listener".
+type VirtualIP struct {
+	// The unique ID for the VIP.
+	ID string `mapstructure:"id" json:"id"`
+
+	// Owner of the VIP. Only an admin user can specify a tenant ID other than its own.
+	TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+	// Human-readable name for the VIP. Does not have to be unique.
+	Name string `mapstructure:"name" json:"name"`
+
+	// Human-readable description for the VIP.
+	Description string `mapstructure:"description" json:"description"`
+
+	// The ID of the subnet on which to allocate the VIP address.
+	SubnetID string `mapstructure:"subnet_id" json:"subnet_id"`
+
+	// The IP address of the VIP.
+	Address string `mapstructure:"address" json:"address"`
+
+	// The protocol of the VIP address. A valid value is TCP, HTTP, or HTTPS.
+	Protocol string `mapstructure:"protocol" json:"protocol"`
+
+	// The port on which to listen to client traffic that is associated with the
+	// VIP address. A valid value is from 0 to 65535.
+	ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"`
+
+	// The ID of the pool with which the VIP is associated.
+	PoolID string `mapstructure:"pool_id" json:"pool_id"`
+
+	// The ID of the port which belongs to the load balancer
+	PortID string `mapstructure:"port_id" json:"port_id"`
+
+	// Indicates whether connections in the same session will be processed by the
+	// same pool member or not.
+	Persistence SessionPersistence `mapstructure:"session_persistence" json:"session_persistence"`
+
+	// The maximum number of connections allowed for the VIP. Default is -1,
+	// meaning no limit.
+	ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"`
+
+	// The administrative state of the VIP. A valid value is true (UP) or false (DOWN).
+	AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+	// The status of the VIP. Indicates whether the VIP is operational.
+	Status string `mapstructure:"status" json:"status"`
+}
+
+// VIPPage is the page returned by a pager when traversing over a
+// collection of routers.
+type VIPPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of routers has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p VIPPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"vips_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p VIPPage) IsEmpty() (bool, error) {
+	is, err := ExtractVIPs(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractVIPs accepts a Page struct, specifically a VIPPage struct,
+// and extracts the elements into a slice of VirtualIP structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractVIPs(page pagination.Page) ([]VirtualIP, error) {
+	var resp struct {
+		VIPs []VirtualIP `mapstructure:"vips" json:"vips"`
+	}
+
+	err := mapstructure.Decode(page.(VIPPage).Body, &resp)
+
+	return resp.VIPs, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*VirtualIP, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		VirtualIP *VirtualIP `mapstructure:"vip" json:"vip"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.VirtualIP, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/lbaas/vips/urls.go b/openstack/networking/v2/extensions/lbaas/vips/urls.go
new file mode 100644
index 0000000..2b6f67e
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/urls.go
@@ -0,0 +1,16 @@
+package vips
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "lb"
+	resourcePath = "vips"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/provider/doc.go b/openstack/networking/v2/extensions/provider/doc.go
new file mode 100755
index 0000000..373da44
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/doc.go
@@ -0,0 +1,21 @@
+// Package provider gives access to the provider Neutron plugin, allowing
+// network extended attributes. The provider extended attributes for networks
+// enable administrative users to specify how network objects map to the
+// underlying networking infrastructure. These extended attributes also appear
+// when administrative users query networks.
+//
+// For more information about extended attributes, see the NetworkExtAttrs
+// struct. The actual semantics of these attributes depend on the technology
+// back end of the particular plug-in. See the plug-in documentation and the
+// OpenStack Cloud Administrator Guide to understand which values should be
+// specific for each of these attributes when OpenStack Networking is deployed
+// with a particular plug-in. The examples shown in this chapter refer to the
+// Open vSwitch plug-in.
+//
+// The default policy settings enable only users with administrative rights to
+// specify these parameters in requests and to see their values in responses. By
+// default, the provider network extension attributes are completely hidden from
+// regular tenants. As a rule of thumb, if these attributes are not visible in a
+// GET /networks/<network-id> operation, this implies the user submitting the
+// request is not authorized to view or manipulate provider network attributes.
+package provider
diff --git a/openstack/networking/v2/extensions/provider/results.go b/openstack/networking/v2/extensions/provider/results.go
new file mode 100755
index 0000000..3453584
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/results.go
@@ -0,0 +1,124 @@
+package provider
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// NetworkExtAttrs represents an extended form of a Network with additional fields.
+type NetworkExtAttrs struct {
+	// UUID for the network
+	ID string `mapstructure:"id" json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Name string `mapstructure:"name" json:"name"`
+
+	// The administrative state of network. If false (down), the network does not forward packets.
+	AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+	// Indicates whether network is currently operational. Possible values include
+	// `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+	Status string `mapstructure:"status" json:"status"`
+
+	// Subnets associated with this network.
+	Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+	// Specifies whether the network resource can be accessed by any tenant or not.
+	Shared bool `mapstructure:"shared" json:"shared"`
+
+	// Specifies the nature of the physical network mapped to this network
+	// resource. Examples are flat, vlan, or gre.
+	NetworkType string `json:"provider:network_type" mapstructure:"provider:network_type"`
+
+	// Identifies the physical network on top of which this network object is
+	// being implemented. The OpenStack Networking API does not expose any facility
+	// for retrieving the list of available physical networks. As an example, in
+	// the Open vSwitch plug-in this is a symbolic name which is then mapped to
+	// specific bridges on each compute host through the Open vSwitch plug-in
+	// configuration file.
+	PhysicalNetwork string `json:"provider:physical_network" mapstructure:"provider:physical_network"`
+
+	// Identifies an isolated segment on the physical network; the nature of the
+	// segment depends on the segmentation model defined by network_type. For
+	// instance, if network_type is vlan, then this is a vlan identifier;
+	// otherwise, if network_type is gre, then this will be a gre key.
+	SegmentationID string `json:"provider:segmentation_id" mapstructure:"provider:segmentation_id"`
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *NetworkExtAttrs `json:"network"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Network, err
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *NetworkExtAttrs `json:"network"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Network, err
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *NetworkExtAttrs `json:"network"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Network, err
+}
+
+// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
+// extracts the elements into a slice of NetworkExtAttrs structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractList(page pagination.Page) ([]NetworkExtAttrs, error) {
+	var resp struct {
+		Networks []NetworkExtAttrs `mapstructure:"networks" json:"networks"`
+	}
+
+	err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
+
+	return resp.Networks, err
+}
diff --git a/openstack/networking/v2/extensions/provider/results_test.go b/openstack/networking/v2/extensions/provider/results_test.go
new file mode 100644
index 0000000..9801b2e
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/results_test.go
@@ -0,0 +1,253 @@
+package provider
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "networks": [
+        {
+            "status": "ACTIVE",
+            "subnets": [
+                "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+            ],
+            "name": "private-network",
+            "admin_state_up": true,
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "shared": true,
+            "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+            "provider:segmentation_id": null,
+            "provider:physical_network": null,
+            "provider:network_type": "local"
+        },
+        {
+            "status": "ACTIVE",
+            "subnets": [
+                "08eae331-0402-425a-923c-34f7cfe39c1b"
+            ],
+            "name": "private",
+            "admin_state_up": true,
+            "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+            "shared": true,
+            "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+            "provider:segmentation_id": null,
+            "provider:physical_network": null,
+            "provider:network_type": "local"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractList(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []NetworkExtAttrs{
+			NetworkExtAttrs{
+				Status:          "ACTIVE",
+				Subnets:         []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+				Name:            "private-network",
+				AdminStateUp:    true,
+				TenantID:        "4fd44f30292945e481c7b8a0c8908869",
+				Shared:          true,
+				ID:              "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+				NetworkType:     "local",
+				PhysicalNetwork: "",
+				SegmentationID:  "",
+			},
+			NetworkExtAttrs{
+				Status:          "ACTIVE",
+				Subnets:         []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+				Name:            "private",
+				AdminStateUp:    true,
+				TenantID:        "26a7980765d0414dbc1fc1f88cdb7e6e",
+				Shared:          true,
+				ID:              "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+				NetworkType:     "local",
+				PhysicalNetwork: "",
+				SegmentationID:  "",
+			},
+		}
+
+		th.CheckDeepEquals(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/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "subnets": [
+            "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        ],
+        "name": "private-network",
+        "provider:physical_network": null,
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "provider:network_type": "local",
+        "shared": true,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "provider:segmentation_id": null
+    }
+}
+			`)
+	})
+
+	res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	n, err := ExtractGet(res)
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "", n.PhysicalNetwork)
+	th.AssertEquals(t, "local", n.NetworkType)
+	th.AssertEquals(t, "", n.SegmentationID)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "network": {
+        "name": "sample_network",
+        "admin_state_up": true
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "subnets": [
+            "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        ],
+        "name": "private-network",
+        "provider:physical_network": null,
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "provider:network_type": "local",
+        "shared": true,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "provider:segmentation_id": null
+    }
+}
+		`)
+	})
+
+	options := networks.CreateOpts{Name: "sample_network", AdminStateUp: Up}
+	res := networks.Create(fake.ServiceClient(), options)
+	n, err := ExtractCreate(res)
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "", n.PhysicalNetwork)
+	th.AssertEquals(t, "local", n.NetworkType)
+	th.AssertEquals(t, "", n.SegmentationID)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"network": {
+				"name": "new_network_name",
+				"admin_state_up": false,
+				"shared": true
+		}
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "subnets": [
+            "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        ],
+        "name": "private-network",
+        "provider:physical_network": null,
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "provider:network_type": "local",
+        "shared": true,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "provider:segmentation_id": null
+    }
+}
+		`)
+	})
+
+	iTrue := true
+	options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: Down, Shared: &iTrue}
+	res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+	n, err := ExtractUpdate(res)
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "", n.PhysicalNetwork)
+	th.AssertEquals(t, "local", n.NetworkType)
+	th.AssertEquals(t, "", n.SegmentationID)
+}
diff --git a/openstack/networking/v2/extensions/security/doc.go b/openstack/networking/v2/extensions/security/doc.go
new file mode 100644
index 0000000..8ef455f
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/doc.go
@@ -0,0 +1,32 @@
+// Package security contains functionality to work with security group and
+// security group rules Neutron resources.
+//
+// Security groups and security group rules allows administrators and tenants
+// the ability to specify the type of traffic and direction (ingress/egress)
+// that is allowed to pass through a port. A security group is a container for
+// security group rules.
+//
+// When a port is created in Networking it is associated with a security group.
+// If a security group is not specified the port is associated with a 'default'
+// security group. By default, this group drops all ingress traffic and allows
+// all egress. Rules can be added to this group in order to change the behaviour.
+//
+// The basic characteristics of Neutron Security Groups are:
+//
+// For ingress traffic (to an instance)
+//  - Only traffic matched with security group rules are allowed.
+//  - When there is no rule defined, all traffic are dropped.
+//
+// For egress traffic (from an instance)
+//  - Only traffic matched with security group rules are allowed.
+//  - When there is no rule defined, all egress traffic are dropped.
+//  - When a new security group is created, rules to allow all egress traffic
+//    are automatically added.
+//
+// "default security group" is defined for each tenant.
+//  - For the default security group a rule which allows intercommunication
+//    among hosts associated with the default security group is defined by default.
+//  - As a result, all egress traffic and intercommunication in the default
+//    group are allowed and all ingress from outside of the default group is
+//    dropped by default (in the default security group).
+package security
diff --git a/openstack/networking/v2/extensions/security/groups/requests.go b/openstack/networking/v2/extensions/security/groups/requests.go
new file mode 100644
index 0000000..0c970ae
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests.go
@@ -0,0 +1,107 @@
+package groups
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	ID       string `q:"id"`
+	Name     string `q:"name"`
+	TenantID string `q:"tenant_id"`
+	Limit    int    `q:"limit"`
+	Marker   string `q:"marker"`
+	SortKey  string `q:"sort_key"`
+	SortDir  string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security groups. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return SecGroupPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+var (
+	errNameRequired = fmt.Errorf("Name is required")
+)
+
+// CreateOpts contains all the values needed to create a new security group.
+type CreateOpts struct {
+	// Required. Human-readable name for the VIP. Does not have to be unique.
+	Name string
+
+	// Optional. Describes the security group.
+	Description string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	var res CreateResult
+
+	// Validate required opts
+	if opts.Name == "" {
+		res.Err = errNameRequired
+		return res
+	}
+
+	type secgroup struct {
+		Name        string `json:"name"`
+		Description string `json:"description,omitempty"`
+	}
+
+	type request struct {
+		SecGroup secgroup `json:"security_group"`
+	}
+
+	reqBody := request{SecGroup: secgroup{
+		Name:        opts.Name,
+		Description: opts.Description,
+	}}
+
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+
+	return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/security/groups/requests_test.go b/openstack/networking/v2/extensions/security/groups/requests_test.go
new file mode 100644
index 0000000..5f074c7
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests_test.go
@@ -0,0 +1,213 @@
+package groups
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups", rootURL(fake.ServiceClient()))
+	th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "security_groups": [
+        {
+            "description": "default",
+            "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+            "name": "default",
+            "security_group_rules": [],
+            "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract secgroups: %v", err)
+			return false, err
+		}
+
+		expected := []SecGroup{
+			SecGroup{
+				Description: "default",
+				ID:          "85cc3048-abc3-43cc-89b3-377341426ac5",
+				Name:        "default",
+				Rules:       []rules.SecGroupRule{},
+				TenantID:    "e4f50856753b4dc6afee5fa6b9b6c550",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "security_group": {
+        "name": "new-webservers",
+        "description": "security group for webservers"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "security_group": {
+        "description": "security group for webservers",
+        "id": "2076db17-a522-4506-91de-c6dd8e837028",
+        "name": "new-webservers",
+        "security_group_rules": [
+            {
+                "direction": "egress",
+                "ethertype": "IPv4",
+                "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d",
+                "port_range_max": null,
+                "port_range_min": null,
+                "protocol": null,
+                "remote_group_id": null,
+                "remote_ip_prefix": null,
+                "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+                "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+            },
+            {
+                "direction": "egress",
+                "ethertype": "IPv6",
+                "id": "565b9502-12de-4ffd-91e9-68885cff6ae1",
+                "port_range_max": null,
+                "port_range_min": null,
+                "protocol": null,
+                "remote_group_id": null,
+                "remote_ip_prefix": null,
+                "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+                "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+            }
+        ],
+        "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+    }
+}
+    `)
+	})
+
+	opts := CreateOpts{Name: "new-webservers", Description: "security group for webservers"}
+	_, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "security_group": {
+        "description": "default",
+        "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+        "name": "default",
+        "security_group_rules": [
+            {
+                "direction": "egress",
+                "ethertype": "IPv6",
+                "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+                "port_range_max": null,
+                "port_range_min": null,
+                "protocol": null,
+                "remote_group_id": null,
+                "remote_ip_prefix": null,
+                "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+                "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+            },
+            {
+                "direction": "egress",
+                "ethertype": "IPv4",
+                "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+                "port_range_max": null,
+                "port_range_min": null,
+                "protocol": null,
+                "remote_group_id": null,
+                "remote_ip_prefix": null,
+                "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+                "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+            }
+        ],
+        "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+    }
+}
+      `)
+	})
+
+	sg, err := Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "default", sg.Description)
+	th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID)
+	th.AssertEquals(t, "default", sg.Name)
+	th.AssertEquals(t, 2, len(sg.Rules))
+	th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/security/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go
new file mode 100644
index 0000000..3b256ef
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -0,0 +1,106 @@
+package groups
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroup represents a container for security group rules.
+type SecGroup struct {
+	// The UUID for the security group.
+	ID string
+
+	// Human-readable name for the security group. Might not be unique. Cannot be
+	// named "default" as that is automatically created for a tenant.
+	Name string
+
+	// The security group description.
+	Description string
+
+	// A slice of security group rules that dictate the permitted behaviour for
+	// traffic entering and leaving the group.
+	Rules []rules.SecGroupRule `json:"security_group_rules" mapstructure:"security_group_rules"`
+
+	// Owner of the security group. Only admin users can specify a TenantID
+	// other than their own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupPage is the page returned by a pager when traversing over a
+// collection of security groups.
+type SecGroupPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security groups has
+// reached the end of a page and the pager seeks to traverse over a new one. In
+// order to do this, it needs to construct the next page's URL.
+func (p SecGroupPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"security_groups_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a SecGroupPage struct is empty.
+func (p SecGroupPage) IsEmpty() (bool, error) {
+	is, err := ExtractGroups(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct,
+// and extracts the elements into a slice of SecGroup structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractGroups(page pagination.Page) ([]SecGroup, error) {
+	var resp struct {
+		SecGroups []SecGroup `mapstructure:"security_groups" json:"security_groups"`
+	}
+
+	err := mapstructure.Decode(page.(SecGroupPage).Body, &resp)
+
+	return resp.SecGroups, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a security group.
+func (r commonResult) Extract() (*SecGroup, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		SecGroup *SecGroup `mapstructure:"security_group" json:"security_group"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.SecGroup, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/security/groups/urls.go b/openstack/networking/v2/extensions/security/groups/urls.go
new file mode 100644
index 0000000..84f7324
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/urls.go
@@ -0,0 +1,13 @@
+package groups
+
+import "github.com/rackspace/gophercloud"
+
+const rootPath = "security-groups"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, id)
+}
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
new file mode 100644
index 0000000..edaebe8
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -0,0 +1,183 @@
+package rules
+
+import (
+	"fmt"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the security group attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Direction      string `q:"direction"`
+	EtherType      string `q:"ethertype"`
+	ID             string `q:"id"`
+	PortRangeMax   int    `q:"port_range_max"`
+	PortRangeMin   int    `q:"port_range_min"`
+	Protocol       string `q:"protocol"`
+	RemoteGroupID  string `q:"remote_group_id"`
+	RemoteIPPrefix string `q:"remote_ip_prefix"`
+	SecGroupID     string `q:"security_group_id"`
+	TenantID       string `q:"tenant_id"`
+	Limit          int    `q:"limit"`
+	Marker         string `q:"marker"`
+	SortKey        string `q:"sort_key"`
+	SortDir        string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security group rules. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Errors
+var (
+	errValidDirectionRequired = fmt.Errorf("A valid Direction is required")
+	errValidEtherTypeRequired = fmt.Errorf("A valid EtherType is required")
+	errSecGroupIDRequired     = fmt.Errorf("A valid SecGroupID is required")
+	errValidProtocolRequired  = fmt.Errorf("A valid Protocol is required")
+)
+
+// Constants useful for CreateOpts
+const (
+	DirIngress   = "ingress"
+	DirEgress    = "egress"
+	Ether4       = "IPv4"
+	Ether6       = "IPv6"
+	ProtocolTCP  = "tcp"
+	ProtocolUDP  = "udp"
+	ProtocolICMP = "icmp"
+)
+
+// CreateOpts contains all the values needed to create a new security group rule.
+type CreateOpts struct {
+	// Required. Must be either "ingress" or "egress": the direction in which the
+	// security group rule is applied.
+	Direction string
+
+	// Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must
+	// match the ingress or egress rules.
+	EtherType string
+
+	// Required. The security group ID to associate with this security group rule.
+	SecGroupID string
+
+	// Optional. The maximum port number in the range that is matched by the
+	// security group rule. The PortRangeMin attribute constrains the PortRangeMax
+	// attribute. If the protocol is ICMP, this value must be an ICMP type.
+	PortRangeMax int
+
+	// Optional. The minimum port number in the range that is matched by the
+	// security group rule. If the protocol is TCP or UDP, this value must be
+	// less than or equal to the value of the PortRangeMax attribute. If the
+	// protocol is ICMP, this value must be an ICMP type.
+	PortRangeMin int
+
+	// Optional. The protocol that is matched by the security group rule. Valid
+	// values are "tcp", "udp", "icmp" or an empty string.
+	Protocol string
+
+	// Optional. The remote group ID to be associated with this security group
+	// rule. You can specify either RemoteGroupID or RemoteIPPrefix.
+	RemoteGroupID string
+
+	// Optional. The remote IP prefix to be associated with this security group
+	// rule. You can specify either RemoteGroupID or RemoteIPPrefix. This
+	// attribute matches the specified IP prefix as the source IP address of the
+	// IP packet.
+	RemoteIPPrefix string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	var res CreateResult
+
+	// Validate required opts
+	if opts.Direction != DirIngress && opts.Direction != DirEgress {
+		res.Err = errValidDirectionRequired
+		return res
+	}
+	if opts.EtherType != Ether4 && opts.EtherType != Ether6 {
+		res.Err = errValidEtherTypeRequired
+		return res
+	}
+	if opts.SecGroupID == "" {
+		res.Err = errSecGroupIDRequired
+		return res
+	}
+	if opts.Protocol != "" && opts.Protocol != ProtocolTCP && opts.Protocol != ProtocolUDP && opts.Protocol != ProtocolICMP {
+		res.Err = errValidProtocolRequired
+		return res
+	}
+
+	type secrule struct {
+		Direction      string `json:"direction"`
+		EtherType      string `json:"ethertype"`
+		SecGroupID     string `json:"security_group_id"`
+		PortRangeMax   int    `json:"port_range_max,omitempty"`
+		PortRangeMin   int    `json:"port_range_min,omitempty"`
+		Protocol       string `json:"protocol,omitempty"`
+		RemoteGroupID  string `json:"remote_group_id,omitempty"`
+		RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
+	}
+
+	type request struct {
+		SecRule secrule `json:"security_group_rule"`
+	}
+
+	reqBody := request{SecRule: secrule{
+		Direction:      opts.Direction,
+		EtherType:      opts.EtherType,
+		SecGroupID:     opts.SecGroupID,
+		PortRangeMax:   opts.PortRangeMax,
+		PortRangeMin:   opts.PortRangeMin,
+		Protocol:       opts.Protocol,
+		RemoteGroupID:  opts.RemoteGroupID,
+		RemoteIPPrefix: opts.RemoteIPPrefix,
+	}}
+
+	_, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+
+	return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/security/rules/requests_test.go b/openstack/networking/v2/extensions/security/rules/requests_test.go
new file mode 100644
index 0000000..b5afef3
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -0,0 +1,243 @@
+package rules
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules", rootURL(fake.ServiceClient()))
+	th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "security_group_rules": [
+        {
+            "direction": "egress",
+            "ethertype": "IPv6",
+            "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+            "port_range_max": null,
+            "port_range_min": null,
+            "protocol": null,
+            "remote_group_id": null,
+            "remote_ip_prefix": null,
+            "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+            "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+        },
+        {
+            "direction": "egress",
+            "ethertype": "IPv4",
+            "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+            "port_range_max": null,
+            "port_range_min": null,
+            "protocol": null,
+            "remote_group_id": null,
+            "remote_ip_prefix": null,
+            "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+            "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract secrules: %v", err)
+			return false, err
+		}
+
+		expected := []SecGroupRule{
+			SecGroupRule{
+				Direction:      "egress",
+				EtherType:      "IPv6",
+				ID:             "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+				PortRangeMax:   0,
+				PortRangeMin:   0,
+				Protocol:       "",
+				RemoteGroupID:  "",
+				RemoteIPPrefix: "",
+				SecGroupID:     "85cc3048-abc3-43cc-89b3-377341426ac5",
+				TenantID:       "e4f50856753b4dc6afee5fa6b9b6c550",
+			},
+			SecGroupRule{
+				Direction:      "egress",
+				EtherType:      "IPv4",
+				ID:             "93aa42e5-80db-4581-9391-3a608bd0e448",
+				PortRangeMax:   0,
+				PortRangeMin:   0,
+				Protocol:       "",
+				RemoteGroupID:  "",
+				RemoteIPPrefix: "",
+				SecGroupID:     "85cc3048-abc3-43cc-89b3-377341426ac5",
+				TenantID:       "e4f50856753b4dc6afee5fa6b9b6c550",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "security_group_rule": {
+        "direction": "ingress",
+        "port_range_min": 80,
+        "ethertype": "IPv4",
+        "port_range_max": 80,
+        "protocol": "tcp",
+        "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+        "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "security_group_rule": {
+        "direction": "ingress",
+        "ethertype": "IPv4",
+        "id": "2bc0accf-312e-429a-956e-e4407625eb62",
+        "port_range_max": 80,
+        "port_range_min": 80,
+        "protocol": "tcp",
+        "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+        "remote_ip_prefix": null,
+        "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
+        "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+    }
+}
+    `)
+	})
+
+	opts := CreateOpts{
+		Direction:     "ingress",
+		PortRangeMin:  80,
+		EtherType:     "IPv4",
+		PortRangeMax:  80,
+		Protocol:      "tcp",
+		RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+		SecGroupID:    "a7734e61-b545-452d-a3cd-0189cbd9747a",
+	}
+	_, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{Direction: "something"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: "something"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), CreateOpts{Direction: DirIngress, EtherType: Ether4, SecGroupID: "something", Protocol: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "security_group_rule": {
+        "direction": "egress",
+        "ethertype": "IPv6",
+        "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+        "port_range_max": null,
+        "port_range_min": null,
+        "protocol": null,
+        "remote_group_id": null,
+        "remote_ip_prefix": null,
+        "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+        "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+    }
+}
+      `)
+	})
+
+	sr, err := Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "egress", sr.Direction)
+	th.AssertEquals(t, "IPv6", sr.EtherType)
+	th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID)
+	th.AssertEquals(t, 0, sr.PortRangeMax)
+	th.AssertEquals(t, 0, sr.PortRangeMin)
+	th.AssertEquals(t, "", sr.Protocol)
+	th.AssertEquals(t, "", sr.RemoteGroupID)
+	th.AssertEquals(t, "", sr.RemoteIPPrefix)
+	th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID)
+	th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
new file mode 100644
index 0000000..29e8676
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -0,0 +1,131 @@
+package rules
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroupRule represents a rule to dictate the behaviour of incoming or
+// outgoing traffic for a particular security group.
+type SecGroupRule struct {
+	// The UUID for this security group rule.
+	ID string
+
+	// The direction in which the security group rule is applied. The only values
+	// allowed are "ingress" or "egress". For a compute instance, an ingress
+	// security group rule is applied to incoming (ingress) traffic for that
+	// instance. An egress rule is applied to traffic leaving the instance.
+	Direction string
+
+	// Must be IPv4 or IPv6, and addresses represented in CIDR must match the
+	// ingress or egress rules.
+	EtherType string `json:"ethertype" mapstructure:"ethertype"`
+
+	// The security group ID to associate with this security group rule.
+	SecGroupID string `json:"security_group_id" mapstructure:"security_group_id"`
+
+	// The minimum port number in the range that is matched by the security group
+	// rule. If the protocol is TCP or UDP, this value must be less than or equal
+	// to the value of the PortRangeMax attribute. If the protocol is ICMP, this
+	// value must be an ICMP type.
+	PortRangeMin int `json:"port_range_min" mapstructure:"port_range_min"`
+
+	// The maximum port number in the range that is matched by the security group
+	// rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If
+	// the protocol is ICMP, this value must be an ICMP type.
+	PortRangeMax int `json:"port_range_max" mapstructure:"port_range_max"`
+
+	// The protocol that is matched by the security group rule. Valid values are
+	// "tcp", "udp", "icmp" or an empty string.
+	Protocol string
+
+	// The remote group ID to be associated with this security group rule. You
+	// can specify either RemoteGroupID or RemoteIPPrefix.
+	RemoteGroupID string `json:"remote_group_id" mapstructure:"remote_group_id"`
+
+	// The remote IP prefix to be associated with this security group rule. You
+	// can specify either RemoteGroupID or RemoteIPPrefix . This attribute
+	// matches the specified IP prefix as the source IP address of the IP packet.
+	RemoteIPPrefix string `json:"remote_ip_prefix" mapstructure:"remote_ip_prefix"`
+
+	// The owner of this security group rule.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupRulePage is the page returned by a pager when traversing over a
+// collection of security group rules.
+type SecGroupRulePage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security group rules has
+// reached the end of a page and the pager seeks to traverse over a new one. In
+// order to do this, it needs to construct the next page's URL.
+func (p SecGroupRulePage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"security_group_rules_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a SecGroupRulePage struct is empty.
+func (p SecGroupRulePage) IsEmpty() (bool, error) {
+	is, err := ExtractRules(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct,
+// and extracts the elements into a slice of SecGroupRule structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRules(page pagination.Page) ([]SecGroupRule, error) {
+	var resp struct {
+		SecGroupRules []SecGroupRule `mapstructure:"security_group_rules" json:"security_group_rules"`
+	}
+
+	err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp)
+
+	return resp.SecGroupRules, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a security rule.
+func (r commonResult) Extract() (*SecGroupRule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		SecGroupRule *SecGroupRule `mapstructure:"security_group_rule" json:"security_group_rule"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.SecGroupRule, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/security/rules/urls.go b/openstack/networking/v2/extensions/security/rules/urls.go
new file mode 100644
index 0000000..8e2b2bb
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -0,0 +1,13 @@
+package rules
+
+import "github.com/rackspace/gophercloud"
+
+const rootPath = "security-group-rules"
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, id)
+}
diff --git a/openstack/networking/v2/networks/doc.go b/openstack/networking/v2/networks/doc.go
new file mode 100644
index 0000000..c87a7ce
--- /dev/null
+++ b/openstack/networking/v2/networks/doc.go
@@ -0,0 +1,9 @@
+// Package networks contains functionality for working with Neutron network
+// resources. A network is an isolated virtual layer-2 broadcast domain that is
+// typically reserved for the tenant who created it (unless you configure the
+// network to be shared). Tenants can create multiple networks until the
+// thresholds per-tenant quota is reached.
+//
+// In the v2.0 Networking API, the network is the main entity. Ports and subnets
+// are always associated with a network.
+package networks
diff --git a/openstack/networking/v2/networks/errors.go b/openstack/networking/v2/networks/errors.go
new file mode 100644
index 0000000..83c4a6a
--- /dev/null
+++ b/openstack/networking/v2/networks/errors.go
@@ -0,0 +1 @@
+package networks
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
new file mode 100644
index 0000000..eaa7136
--- /dev/null
+++ b/openstack/networking/v2/networks/requests.go
@@ -0,0 +1,209 @@
+package networks
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+type networkOpts struct {
+	AdminStateUp *bool
+	Name         string
+	Shared       *bool
+	TenantID     string
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToNetworkListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the network attributes you want to see returned. SortKey allows you to sort
+// by a particular network attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Status       string `q:"status"`
+	Name         string `q:"name"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	TenantID     string `q:"tenant_id"`
+	Shared       *bool  `q:"shared"`
+	ID           string `q:"id"`
+	Marker       string `q:"marker"`
+	Limit        int    `q:"limit"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// ToNetworkListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToNetworkListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// networks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToNetworkListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get retrieves a specific network based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToNetworkCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts networkOpts
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	n := make(map[string]interface{})
+
+	if opts.AdminStateUp != nil {
+		n["admin_state_up"] = &opts.AdminStateUp
+	}
+	if opts.Name != "" {
+		n["name"] = opts.Name
+	}
+	if opts.Shared != nil {
+		n["shared"] = &opts.Shared
+	}
+	if opts.TenantID != "" {
+		n["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"network": n}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. This operation does not actually require a request body, i.e. the
+// CreateOpts struct argument can be empty.
+//
+// The tenant ID that is contained in the URI is the tenant that creates the
+// network. An admin user, however, has the option of specifying another tenant
+// ID in the CreateOpts struct.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToNetworkCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+	return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToNetworkUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts networkOpts
+
+// ToNetworkUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+	n := make(map[string]interface{})
+
+	if opts.AdminStateUp != nil {
+		n["admin_state_up"] = &opts.AdminStateUp
+	}
+	if opts.Name != "" {
+		n["name"] = opts.Name
+	}
+	if opts.Shared != nil {
+		n["shared"] = &opts.Shared
+	}
+
+	return map[string]interface{}{"network": n}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing network using the
+// values provided. For more information, see the Create function.
+func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToNetworkUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201},
+	})
+
+	return res
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
new file mode 100644
index 0000000..a263b7b
--- /dev/null
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -0,0 +1,275 @@
+package networks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "networks": [
+        {
+            "status": "ACTIVE",
+            "subnets": [
+                "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+            ],
+            "name": "private-network",
+            "admin_state_up": true,
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "shared": true,
+            "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+        },
+        {
+            "status": "ACTIVE",
+            "subnets": [
+                "08eae331-0402-425a-923c-34f7cfe39c1b"
+            ],
+            "name": "private",
+            "admin_state_up": true,
+            "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+            "shared": true,
+            "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324"
+        }
+    ]
+}
+			`)
+	})
+
+	client := fake.ServiceClient()
+	count := 0
+
+	List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []Network{
+			Network{
+				Status:       "ACTIVE",
+				Subnets:      []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+				Name:         "private-network",
+				AdminStateUp: true,
+				TenantID:     "4fd44f30292945e481c7b8a0c8908869",
+				Shared:       true,
+				ID:           "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+			},
+			Network{
+				Status:       "ACTIVE",
+				Subnets:      []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+				Name:         "private",
+				AdminStateUp: true,
+				TenantID:     "26a7980765d0414dbc1fc1f88cdb7e6e",
+				Shared:       true,
+				ID:           "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+			},
+		}
+
+		th.CheckDeepEquals(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/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "subnets": [
+            "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        ],
+        "name": "private-network",
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "shared": true,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+			`)
+	})
+
+	n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"})
+	th.AssertEquals(t, n.Name, "private-network")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertEquals(t, n.Shared, true)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "network": {
+        "name": "sample_network",
+        "admin_state_up": true
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "subnets": [],
+        "name": "net1",
+        "admin_state_up": true,
+        "tenant_id": "9bacb3c5d39d41a79512987f338cf177",
+        "shared": false,
+        "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+    }
+}
+		`)
+	})
+
+	iTrue := true
+	options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.Subnets, []string{})
+	th.AssertEquals(t, n.Name, "net1")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177")
+	th.AssertEquals(t, n.Shared, false)
+	th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestCreateWithOptionalFields(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+	"network": {
+			"name": "sample_network",
+			"admin_state_up": true,
+			"shared": true,
+			"tenant_id": "12345"
+	}
+}
+		`)
+
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	iTrue := true
+	options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"}
+	_, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"network": {
+				"name": "new_network_name",
+				"admin_state_up": false,
+				"shared": true
+		}
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "status": "ACTIVE",
+        "subnets": [],
+        "name": "new_network_name",
+        "admin_state_up": false,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "shared": true,
+        "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+    }
+}
+		`)
+	})
+
+	iTrue, iFalse := true, false
+	options := UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, Shared: &iTrue}
+	n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "new_network_name")
+	th.AssertEquals(t, n.AdminStateUp, false)
+	th.AssertEquals(t, n.Shared, true)
+	th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/networks/results.go b/openstack/networking/v2/networks/results.go
new file mode 100644
index 0000000..6c32ebe
--- /dev/null
+++ b/openstack/networking/v2/networks/results.go
@@ -0,0 +1,114 @@
+package networks
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a network resource.
+func (r commonResult) Extract() (*Network, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *Network `json:"network"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Network, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// Network represents, well, a network.
+type Network struct {
+	// UUID for the network
+	ID string `mapstructure:"id" json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Name string `mapstructure:"name" json:"name"`
+
+	// The administrative state of network. If false (down), the network does not forward packets.
+	AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+	// Indicates whether network is currently operational. Possible values include
+	// `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+	Status string `mapstructure:"status" json:"status"`
+
+	// Subnets associated with this network.
+	Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+	// Specifies whether the network resource can be accessed by any tenant or not.
+	Shared bool `mapstructure:"shared" json:"shared"`
+}
+
+// NetworkPage is the page returned by a pager when traversing over a
+// collection of networks.
+type NetworkPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of networks has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p NetworkPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"networks_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p NetworkPage) IsEmpty() (bool, error) {
+	is, err := ExtractNetworks(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct,
+// and extracts the elements into a slice of Network structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractNetworks(page pagination.Page) ([]Network, error) {
+	var resp struct {
+		Networks []Network `mapstructure:"networks" json:"networks"`
+	}
+
+	err := mapstructure.Decode(page.(NetworkPage).Body, &resp)
+
+	return resp.Networks, err
+}
diff --git a/openstack/networking/v2/networks/urls.go b/openstack/networking/v2/networks/urls.go
new file mode 100644
index 0000000..33c2387
--- /dev/null
+++ b/openstack/networking/v2/networks/urls.go
@@ -0,0 +1,27 @@
+package networks
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("networks", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("networks")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
diff --git a/openstack/networking/v2/networks/urls_test.go b/openstack/networking/v2/networks/urls_test.go
new file mode 100644
index 0000000..caf77db
--- /dev/null
+++ b/openstack/networking/v2/networks/urls_test.go
@@ -0,0 +1,38 @@
+package networks
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/networks/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "v2.0/networks"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "v2.0/networks"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/networks/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/networking/v2/ports/doc.go b/openstack/networking/v2/ports/doc.go
new file mode 100644
index 0000000..f16a4bb
--- /dev/null
+++ b/openstack/networking/v2/ports/doc.go
@@ -0,0 +1,8 @@
+// Package ports contains functionality for working with Neutron port resources.
+// A port represents a virtual switch port on a logical network switch. Virtual
+// instances attach their interfaces into ports. The logical port also defines
+// the MAC address and the IP address(es) to be assigned to the interfaces
+// plugged into them. When IP addresses are associated to a port, this also
+// implies the port is associated with a subnet, as the IP address was taken
+// from the allocation pool for a specific subnet.
+package ports
diff --git a/openstack/networking/v2/ports/errors.go b/openstack/networking/v2/ports/errors.go
new file mode 100644
index 0000000..111d977
--- /dev/null
+++ b/openstack/networking/v2/ports/errors.go
@@ -0,0 +1,11 @@
+package ports
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var (
+	errNetworkIDRequired = err("A Network ID is required")
+)
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
new file mode 100644
index 0000000..8210801
--- /dev/null
+++ b/openstack/networking/v2/ports/requests.go
@@ -0,0 +1,245 @@
+package ports
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToPortListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the port attributes you want to see returned. SortKey allows you to sort
+// by a particular port attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Status       string `q:"status"`
+	Name         string `q:"name"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	NetworkID    string `q:"network_id"`
+	TenantID     string `q:"tenant_id"`
+	DeviceOwner  string `q:"device_owner"`
+	MACAddress   string `q:"mac_address"`
+	ID           string `q:"id"`
+	DeviceID     string `q:"device_id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// ToPortListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPortListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// ports. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those ports that are owned by the tenant
+// who submits the request, unless the request is submitted by an user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToPortListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return PortPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToPortCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts represents the attributes used when creating a new port.
+type CreateOpts struct {
+	NetworkID      string
+	Name           string
+	AdminStateUp   *bool
+	MACAddress     string
+	FixedIPs       interface{}
+	DeviceID       string
+	DeviceOwner    string
+	TenantID       string
+	SecurityGroups []string
+}
+
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.NetworkID == "" {
+		return nil, errNetworkIDRequired
+	}
+	p["network_id"] = opts.NetworkID
+
+	if opts.DeviceID != "" {
+		p["device_id"] = opts.DeviceID
+	}
+	if opts.DeviceOwner != "" {
+		p["device_owner"] = opts.DeviceOwner
+	}
+	if opts.FixedIPs != nil {
+		p["fixed_ips"] = opts.FixedIPs
+	}
+	if opts.SecurityGroups != nil {
+		p["security_groups"] = opts.SecurityGroups
+	}
+	if opts.TenantID != "" {
+		p["tenant_id"] = opts.TenantID
+	}
+	if opts.AdminStateUp != nil {
+		p["admin_state_up"] = &opts.AdminStateUp
+	}
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+	if opts.MACAddress != "" {
+		p["mac_address"] = opts.MACAddress
+	}
+
+	return map[string]interface{}{"port": p}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. You must remember to provide a NetworkID value.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToPortCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Response
+	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+		DumpReqJson: true,
+	})
+
+	return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToPortUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the attributes used when updating an existing port.
+type UpdateOpts struct {
+	Name           string
+	AdminStateUp   *bool
+	FixedIPs       interface{}
+	DeviceID       string
+	DeviceOwner    string
+	SecurityGroups []string
+}
+
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.DeviceID != "" {
+		p["device_id"] = opts.DeviceID
+	}
+	if opts.DeviceOwner != "" {
+		p["device_owner"] = opts.DeviceOwner
+	}
+	if opts.FixedIPs != nil {
+		p["fixed_ips"] = opts.FixedIPs
+	}
+	if opts.SecurityGroups != nil {
+		p["security_groups"] = opts.SecurityGroups
+	}
+	if opts.AdminStateUp != nil {
+		p["admin_state_up"] = &opts.AdminStateUp
+	}
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+
+	return map[string]interface{}{"port": p}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToPortUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201},
+	})
+	return res
+}
+
+// Delete accepts a unique ID and deletes the port associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
new file mode 100644
index 0000000..9e323ef
--- /dev/null
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -0,0 +1,321 @@
+package ports
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "ports": [
+        {
+            "status": "ACTIVE",
+            "binding:host_id": "devstack",
+            "name": "",
+            "admin_state_up": true,
+            "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+            "tenant_id": "",
+            "device_owner": "network:router_gateway",
+            "mac_address": "fa:16:3e:58:42:ed",
+            "fixed_ips": [
+                {
+                    "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062",
+                    "ip_address": "172.24.4.2"
+                }
+            ],
+            "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+            "security_groups": [],
+            "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPorts(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []Port{
+			Port{
+				Status:       "ACTIVE",
+				Name:         "",
+				AdminStateUp: true,
+				NetworkID:    "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+				TenantID:     "",
+				DeviceOwner:  "network:router_gateway",
+				MACAddress:   "fa:16:3e:58:42:ed",
+				FixedIPs: []IP{
+					IP{
+						SubnetID:  "008ba151-0b8c-4a67-98b5-0d2b87666062",
+						IPAddress: "172.24.4.2",
+					},
+				},
+				ID:             "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+				SecurityGroups: []string{},
+				DeviceID:       "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+			},
+		}
+
+		th.CheckDeepEquals(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/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "ACTIVE",
+        "name": "",
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "7e02058126cc4950b75f9970368ba177",
+        "device_owner": "network:router_interface",
+        "mac_address": "fa:16:3e:23:fd:d7",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.1"
+            }
+        ],
+        "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2",
+        "security_groups": [],
+        "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e"
+    }
+}
+			`)
+	})
+
+	n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertEquals(t, n.Name, "")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+	th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177")
+	th.AssertEquals(t, n.DeviceOwner, "network:router_interface")
+	th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7")
+	th.AssertDeepEquals(t, n.FixedIPs, []IP{
+		IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"},
+	})
+	th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2")
+	th.AssertDeepEquals(t, n.SecurityGroups, []string{})
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "port": {
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "name": "private-port",
+        "admin_state_up": true,
+				"fixed_ips": [
+						{
+								"subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+								"ip_address": "10.0.0.2"
+						}
+				],
+				"security_groups": ["foo"]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "private-port",
+        "allowed_address_pairs": [],
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+        "device_owner": "",
+        "mac_address": "fa:16:3e:c9:cb:f0",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.2"
+            }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	asu := true
+	options := CreateOpts{
+		Name:         "private-port",
+		AdminStateUp: &asu,
+		NetworkID:    "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+		FixedIPs: []IP{
+			IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+		},
+		SecurityGroups: []string{"foo"},
+	}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "DOWN")
+	th.AssertEquals(t, n.Name, "private-port")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+	th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+	th.AssertEquals(t, n.DeviceOwner, "")
+	th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0")
+	th.AssertDeepEquals(t, n.FixedIPs, []IP{
+		IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+	})
+	th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+	th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"port": {
+				"name": "new_port_name",
+				"fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.3"
+            }
+        ],
+				"security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ]
+		}
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "new_port_name",
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+        "device_owner": "",
+        "mac_address": "fa:16:3e:c9:cb:f0",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.3"
+            }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	options := UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []IP{
+			IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+	}
+
+	s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "new_port_name")
+	th.AssertDeepEquals(t, s.FixedIPs, []IP{
+		IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
new file mode 100644
index 0000000..6681123
--- /dev/null
+++ b/openstack/networking/v2/ports/results.go
@@ -0,0 +1,124 @@
+package ports
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a port resource.
+func (r commonResult) Extract() (*Port, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Port *Port `json:"port"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Port, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// IP is a sub-struct that represents an individual IP.
+type IP struct {
+	SubnetID  string `mapstructure:"subnet_id" json:"subnet_id"`
+	IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
+}
+
+// Port represents a Neutron port. See package documentation for a top-level
+// description of what this is.
+type Port struct {
+	// UUID for the port.
+	ID string `mapstructure:"id" json:"id"`
+	// Network that this port is associated with.
+	NetworkID string `mapstructure:"network_id" json:"network_id"`
+	// Human-readable name for the port. Might not be unique.
+	Name string `mapstructure:"name" json:"name"`
+	// Administrative state of port. If false (down), port does not forward packets.
+	AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+	// Indicates whether network is currently operational. Possible values include
+	// `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
+	Status string `mapstructure:"status" json:"status"`
+	// Mac address to use on this port.
+	MACAddress string `mapstructure:"mac_address" json:"mac_address"`
+	// Specifies IP addresses for the port thus associating the port itself with
+	// the subnets where the IP addresses are picked from
+	FixedIPs []IP `mapstructure:"fixed_ips" json:"fixed_ips"`
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+	// Identifies the entity (e.g.: dhcp agent) using this port.
+	DeviceOwner string `mapstructure:"device_owner" json:"device_owner"`
+	// Specifies the IDs of any security groups associated with a port.
+	SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"`
+	// Identifies the device (e.g., virtual server) using this port.
+	DeviceID string `mapstructure:"device_id" json:"device_id"`
+}
+
+// PortPage is the page returned by a pager when traversing over a collection
+// of network ports.
+type PortPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of ports has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p PortPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"ports_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PortPage struct is empty.
+func (p PortPage) IsEmpty() (bool, error) {
+	is, err := ExtractPorts(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractPorts accepts a Page struct, specifically a PortPage struct,
+// and extracts the elements into a slice of Port structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPorts(page pagination.Page) ([]Port, error) {
+	var resp struct {
+		Ports []Port `mapstructure:"ports" json:"ports"`
+	}
+
+	err := mapstructure.Decode(page.(PortPage).Body, &resp)
+
+	return resp.Ports, err
+}
diff --git a/openstack/networking/v2/ports/urls.go b/openstack/networking/v2/ports/urls.go
new file mode 100644
index 0000000..6d0572f
--- /dev/null
+++ b/openstack/networking/v2/ports/urls.go
@@ -0,0 +1,31 @@
+package ports
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("ports", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("ports")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
diff --git a/openstack/networking/v2/ports/urls_test.go b/openstack/networking/v2/ports/urls_test.go
new file mode 100644
index 0000000..7fadd4d
--- /dev/null
+++ b/openstack/networking/v2/ports/urls_test.go
@@ -0,0 +1,44 @@
+package ports
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "v2.0/ports"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/ports/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "v2.0/ports"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/ports/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/ports/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/networking/v2/subnets/doc.go b/openstack/networking/v2/subnets/doc.go
new file mode 100644
index 0000000..43e8296
--- /dev/null
+++ b/openstack/networking/v2/subnets/doc.go
@@ -0,0 +1,10 @@
+// Package subnets contains functionality for working with Neutron subnet
+// resources. A subnet represents an IP address block that can be used to
+// assign IP addresses to virtual instances. Each subnet must have a CIDR and
+// must be associated with a network. IPs can either be selected from the whole
+// subnet CIDR or from allocation pools specified by the user.
+//
+// A subnet can also have a gateway, a list of DNS name servers, and host routes.
+// This information is pushed to instances whose interfaces are associated with
+// the subnet.
+package subnets
diff --git a/openstack/networking/v2/subnets/errors.go b/openstack/networking/v2/subnets/errors.go
new file mode 100644
index 0000000..0db0a6e
--- /dev/null
+++ b/openstack/networking/v2/subnets/errors.go
@@ -0,0 +1,13 @@
+package subnets
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var (
+	errNetworkIDRequired = err("A network ID is required")
+	errCIDRRequired      = err("A valid CIDR is required")
+	errInvalidIPType     = err("An IP type must either be 4 or 6")
+)
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
new file mode 100644
index 0000000..2e6d670
--- /dev/null
+++ b/openstack/networking/v2/subnets/requests.go
@@ -0,0 +1,254 @@
+package subnets
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToSubnetListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the subnet attributes you want to see returned. SortKey allows you to sort
+// by a particular subnet attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Name       string `q:"name"`
+	EnableDHCP *bool  `q:"enable_dhcp"`
+	NetworkID  string `q:"network_id"`
+	TenantID   string `q:"tenant_id"`
+	IPVersion  int    `q:"ip_version"`
+	GatewayIP  string `q:"gateway_ip"`
+	CIDR       string `q:"cidr"`
+	ID         string `q:"id"`
+	Limit      int    `q:"limit"`
+	Marker     string `q:"marker"`
+	SortKey    string `q:"sort_key"`
+	SortDir    string `q:"sort_dir"`
+}
+
+// ToSubnetListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSubnetListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// subnets. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those subnets that are owned by the tenant
+// who submits the request, unless the request is submitted by an user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToSubnetListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return SubnetPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get retrieves a specific subnet based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Valid IP types
+const (
+	IPv4 = 4
+	IPv6 = 6
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToSubnetCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts represents the attributes used when creating a new subnet.
+type CreateOpts struct {
+	// Required
+	NetworkID string
+	CIDR      string
+	// Optional
+	Name            string
+	TenantID        string
+	AllocationPools []AllocationPool
+	GatewayIP       string
+	IPVersion       int
+	EnableDHCP      *bool
+	DNSNameservers  []string
+	HostRoutes      []HostRoute
+}
+
+// ToSubnetCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.NetworkID == "" {
+		return nil, errNetworkIDRequired
+	}
+	if opts.CIDR == "" {
+		return nil, errCIDRRequired
+	}
+	if opts.IPVersion != 0 && opts.IPVersion != IPv4 && opts.IPVersion != IPv6 {
+		return nil, errInvalidIPType
+	}
+
+	s["network_id"] = opts.NetworkID
+	s["cidr"] = opts.CIDR
+
+	if opts.EnableDHCP != nil {
+		s["enable_dhcp"] = &opts.EnableDHCP
+	}
+	if opts.Name != "" {
+		s["name"] = opts.Name
+	}
+	if opts.GatewayIP != "" {
+		s["gateway_ip"] = opts.GatewayIP
+	}
+	if opts.TenantID != "" {
+		s["tenant_id"] = opts.TenantID
+	}
+	if opts.IPVersion != 0 {
+		s["ip_version"] = opts.IPVersion
+	}
+	if len(opts.AllocationPools) != 0 {
+		s["allocation_pools"] = opts.AllocationPools
+	}
+	if len(opts.DNSNameservers) != 0 {
+		s["dns_nameservers"] = opts.DNSNameservers
+	}
+	if len(opts.HostRoutes) != 0 {
+		s["host_routes"] = opts.HostRoutes
+	}
+
+	return map[string]interface{}{"subnet": s}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new subnet using the values
+// provided. You must remember to provide a valid NetworkID, CIDR and IP version.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToSubnetCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{201},
+	})
+
+	return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToSubnetUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the attributes used when updating an existing subnet.
+type UpdateOpts struct {
+	Name           string
+	GatewayIP      string
+	DNSNameservers []string
+	HostRoutes     []HostRoute
+	EnableDHCP     *bool
+}
+
+// ToSubnetUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.EnableDHCP != nil {
+		s["enable_dhcp"] = &opts.EnableDHCP
+	}
+	if opts.Name != "" {
+		s["name"] = opts.Name
+	}
+	if opts.GatewayIP != "" {
+		s["gateway_ip"] = opts.GatewayIP
+	}
+	if len(opts.DNSNameservers) != 0 {
+		s["dns_nameservers"] = opts.DNSNameservers
+	}
+	if len(opts.HostRoutes) != 0 {
+		s["host_routes"] = opts.HostRoutes
+	}
+
+	return map[string]interface{}{"subnet": s}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnet using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToSubnetUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201},
+	})
+
+	return res
+}
+
+// Delete accepts a unique ID and deletes the subnet associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
new file mode 100644
index 0000000..987064a
--- /dev/null
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -0,0 +1,362 @@
+package subnets
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "subnets": [
+        {
+            "name": "private-subnet",
+            "enable_dhcp": true,
+            "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+            "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+            "dns_nameservers": [],
+            "allocation_pools": [
+                {
+                    "start": "10.0.0.2",
+                    "end": "10.0.0.254"
+                }
+            ],
+            "host_routes": [],
+            "ip_version": 4,
+            "gateway_ip": "10.0.0.1",
+            "cidr": "10.0.0.0/24",
+            "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+        },
+        {
+            "name": "my_subnet",
+            "enable_dhcp": true,
+            "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "dns_nameservers": [],
+            "allocation_pools": [
+                {
+                    "start": "192.0.0.2",
+                    "end": "192.255.255.254"
+                }
+            ],
+            "host_routes": [],
+            "ip_version": 4,
+            "gateway_ip": "192.0.0.1",
+            "cidr": "192.0.0.0/8",
+            "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSubnets(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []Subnet{
+			Subnet{
+				Name:           "private-subnet",
+				EnableDHCP:     true,
+				NetworkID:      "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+				TenantID:       "26a7980765d0414dbc1fc1f88cdb7e6e",
+				DNSNameservers: []string{},
+				AllocationPools: []AllocationPool{
+					AllocationPool{
+						Start: "10.0.0.2",
+						End:   "10.0.0.254",
+					},
+				},
+				HostRoutes: []HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "10.0.0.1",
+				CIDR:       "10.0.0.0/24",
+				ID:         "08eae331-0402-425a-923c-34f7cfe39c1b",
+			},
+			Subnet{
+				Name:           "my_subnet",
+				EnableDHCP:     true,
+				NetworkID:      "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+				TenantID:       "4fd44f30292945e481c7b8a0c8908869",
+				DNSNameservers: []string{},
+				AllocationPools: []AllocationPool{
+					AllocationPool{
+						Start: "192.0.0.2",
+						End:   "192.255.255.254",
+					},
+				},
+				HostRoutes: []HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "192.0.0.1",
+				CIDR:       "192.0.0.0/8",
+				ID:         "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+			},
+		}
+
+		th.CheckDeepEquals(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/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "my_subnet",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "dns_nameservers": [],
+        "allocation_pools": [
+            {
+                "start": "192.0.0.2",
+                "end": "192.255.255.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "192.0.0.1",
+        "cidr": "192.0.0.0/8",
+        "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+    }
+}
+			`)
+	})
+
+	s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "my_subnet")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+	th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{
+		AllocationPool{
+			Start: "192.0.0.2",
+			End:   "192.255.255.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "192.0.0.1")
+	th.AssertEquals(t, s.CIDR, "192.0.0.0/8")
+	th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet": {
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "ip_version": 4,
+        "cidr": "192.168.199.0/24",
+				"dns_nameservers": ["foo"],
+				"allocation_pools": [
+						{
+								"start": "192.168.199.2",
+								"end": "192.168.199.254"
+						}
+				],
+				"host_routes": [{"destination":"","nexthop": "bar"}]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "dns_nameservers": [],
+        "allocation_pools": [
+            {
+                "start": "192.168.199.2",
+                "end": "192.168.199.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "192.168.199.1",
+        "cidr": "192.168.199.0/24",
+        "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126"
+    }
+}
+		`)
+	})
+
+	opts := CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+		IPVersion: 4,
+		CIDR:      "192.168.199.0/24",
+		AllocationPools: []AllocationPool{
+			AllocationPool{
+				Start: "192.168.199.2",
+				End:   "192.168.199.254",
+			},
+		},
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []HostRoute{
+			HostRoute{NextHop: "bar"},
+		},
+	}
+	s, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+	th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{
+		AllocationPool{
+			Start: "192.168.199.2",
+			End:   "192.168.199.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
+	th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+	th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet": {
+        "name": "my_new_subnet",
+				"dns_nameservers": ["foo"],
+				"host_routes": [{"destination":"","nexthop": "bar"}]
+    }
+}
+		`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "my_new_subnet",
+        "enable_dhcp": true,
+        "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+        "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+        "dns_nameservers": [],
+        "allocation_pools": [
+            {
+                "start": "10.0.0.2",
+                "end": "10.0.0.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "10.0.0.1",
+        "cidr": "10.0.0.0/24",
+        "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+    }
+}
+	`)
+	})
+
+	opts := UpdateOpts{
+		Name:           "my_new_subnet",
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []HostRoute{
+			HostRoute{NextHop: "bar"},
+		},
+	}
+	s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "my_new_subnet")
+	th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go
new file mode 100644
index 0000000..c87504c
--- /dev/null
+++ b/openstack/networking/v2/subnets/results.go
@@ -0,0 +1,130 @@
+package subnets
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a subnet resource.
+func (r commonResult) Extract() (*Subnet, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Subnet *Subnet `json:"subnet"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Subnet, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// AllocationPool represents a sub-range of cidr available for dynamic
+// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"}
+type AllocationPool struct {
+	Start string `json:"start"`
+	End   string `json:"end"`
+}
+
+// HostRoute represents a route that should be used by devices with IPs from
+// a subnet (not including local subnet route).
+type HostRoute struct {
+	DestinationCIDR string `json:"destination"`
+	NextHop         string `json:"nexthop"`
+}
+
+// Subnet represents a subnet. See package documentation for a top-level
+// description of what this is.
+type Subnet struct {
+	// UUID representing the subnet
+	ID string `mapstructure:"id" json:"id"`
+	// UUID of the parent network
+	NetworkID string `mapstructure:"network_id" json:"network_id"`
+	// Human-readable name for the subnet. Might not be unique.
+	Name string `mapstructure:"name" json:"name"`
+	// IP version, either `4' or `6'
+	IPVersion int `mapstructure:"ip_version" json:"ip_version"`
+	// CIDR representing IP range for this subnet, based on IP version
+	CIDR string `mapstructure:"cidr" json:"cidr"`
+	// Default gateway used by devices in this subnet
+	GatewayIP string `mapstructure:"gateway_ip" json:"gateway_ip"`
+	// DNS name servers used by hosts in this subnet.
+	DNSNameservers []string `mapstructure:"dns_nameservers" json:"dns_nameservers"`
+	// Sub-ranges of CIDR available for dynamic allocation to ports. See AllocationPool.
+	AllocationPools []AllocationPool `mapstructure:"allocation_pools" json:"allocation_pools"`
+	// Routes that should be used by devices with IPs from this subnet (not including local subnet route).
+	HostRoutes []HostRoute `mapstructure:"host_routes" json:"host_routes"`
+	// Specifies whether DHCP is enabled for this subnet or not.
+	EnableDHCP bool `mapstructure:"enable_dhcp" json:"enable_dhcp"`
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+}
+
+// SubnetPage is the page returned by a pager when traversing over a collection
+// of subnets.
+type SubnetPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of subnets has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p SubnetPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"subnets_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a SubnetPage struct is empty.
+func (p SubnetPage) IsEmpty() (bool, error) {
+	is, err := ExtractSubnets(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct,
+// and extracts the elements into a slice of Subnet structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractSubnets(page pagination.Page) ([]Subnet, error) {
+	var resp struct {
+		Subnets []Subnet `mapstructure:"subnets" json:"subnets"`
+	}
+
+	err := mapstructure.Decode(page.(SubnetPage).Body, &resp)
+
+	return resp.Subnets, err
+}
diff --git a/openstack/networking/v2/subnets/urls.go b/openstack/networking/v2/subnets/urls.go
new file mode 100644
index 0000000..0d02368
--- /dev/null
+++ b/openstack/networking/v2/subnets/urls.go
@@ -0,0 +1,31 @@
+package subnets
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("subnets", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("subnets")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
diff --git a/openstack/networking/v2/subnets/urls_test.go b/openstack/networking/v2/subnets/urls_test.go
new file mode 100644
index 0000000..aeeddf3
--- /dev/null
+++ b/openstack/networking/v2/subnets/urls_test.go
@@ -0,0 +1,44 @@
+package subnets
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "v2.0/subnets"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/subnets/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "v2.0/subnets"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/subnets/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "v2.0/subnets/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/accounts/doc.go b/openstack/objectstorage/v1/accounts/doc.go
new file mode 100644
index 0000000..f5f894a
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/doc.go
@@ -0,0 +1,8 @@
+// Package accounts contains functionality for working with Object Storage
+// account resources. An account is the top-level resource the object storage
+// hierarchy: containers belong to accounts, objects belong to containers.
+//
+// Another way of thinking of an account is like a namespace for all your
+// resources. It is synonymous with a project or tenant in other OpenStack
+// services.
+package accounts
diff --git a/openstack/objectstorage/v1/accounts/fixtures.go b/openstack/objectstorage/v1/accounts/fixtures.go
new file mode 100644
index 0000000..3dad0c5
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/fixtures.go
@@ -0,0 +1,38 @@
+// +build fixtures
+
+package accounts
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleGetAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that
+// responds with a `Get` response.
+func HandleGetAccountSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+
+		w.Header().Set("X-Account-Container-Count", "2")
+		w.Header().Set("X-Account-Bytes-Used", "14")
+		w.Header().Set("X-Account-Meta-Subject", "books")
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleUpdateAccountSuccessfully creates an HTTP handler at `/` on the test handler mux that
+// responds with a `Update` response.
+func HandleUpdateAccountSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Set("X-Account-Meta-Foo", "bar")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/objectstorage/v1/accounts/requests.go b/openstack/objectstorage/v1/accounts/requests.go
new file mode 100644
index 0000000..55b4217
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -0,0 +1,106 @@
+package accounts
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// GetOptsBuilder allows extensions to add additional headers to the Get
+// request.
+type GetOptsBuilder interface {
+	ToAccountGetMap() (map[string]string, error)
+}
+
+// GetOpts is a structure that contains parameters for getting an account's
+// metadata.
+type GetOpts struct {
+	Newest bool `h:"X-Newest"`
+}
+
+// ToAccountGetMap formats a GetOpts into a map[string]string of headers.
+func (opts GetOpts) ToAccountGetMap() (map[string]string, error) {
+	return gophercloud.BuildHeaders(opts)
+}
+
+// Get is a function that retrieves an account's metadata. To extract just the
+// custom metadata, call the ExtractMetadata method on the GetResult. To extract
+// all the headers that are returned (including the metadata), call the
+// ExtractHeaders method on the GetResult.
+func Get(c *gophercloud.ServiceClient, opts GetOptsBuilder) GetResult {
+	var res GetResult
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToAccountGetMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("HEAD", getURL(c), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional headers to the Update
+// request.
+type UpdateOptsBuilder interface {
+	ToAccountUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or
+// deleting an account's metadata.
+type UpdateOpts struct {
+	Metadata          map[string]string
+	ContentType       string `h:"Content-Type"`
+	DetectContentType bool   `h:"X-Detect-Content-Type"`
+	TempURLKey        string `h:"X-Account-Meta-Temp-URL-Key"`
+	TempURLKey2       string `h:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers.
+func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
+	headers, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		headers["X-Account-Meta-"+k] = v
+	}
+	return headers, err
+}
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+// To extract the headers returned, call the Extract method on the UpdateResult.
+func Update(c *gophercloud.ServiceClient, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToAccountUpdateMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("POST", updateURL(c), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/openstack/objectstorage/v1/accounts/requests_test.go b/openstack/objectstorage/v1/accounts/requests_test.go
new file mode 100644
index 0000000..d6dc26b
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests_test.go
@@ -0,0 +1,33 @@
+package accounts
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var metadata = map[string]string{"gophercloud-test": "accounts"}
+
+func TestUpdateAccount(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetAccountSuccessfully(t)
+
+	options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+	res := Update(fake.ServiceClient(), options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetAccount(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateAccountSuccessfully(t)
+
+	expected := map[string]string{"Foo": "bar"}
+	actual, err := Get(fake.ServiceClient(), &GetOpts{}).ExtractMetadata()
+	if err != nil {
+		t.Fatalf("Unable to get account metadata: %v", err)
+	}
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/accounts/results.go b/openstack/objectstorage/v1/accounts/results.go
new file mode 100644
index 0000000..ba379eb
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/results.go
@@ -0,0 +1,34 @@
+package accounts
+
+import (
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// GetResult is returned from a call to the Get function.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metatdata associated with the account.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+
+	metadata := make(map[string]string)
+	for k, v := range gr.Header {
+		if strings.HasPrefix(k, "X-Account-Meta-") {
+			key := strings.TrimPrefix(k, "X-Account-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata, nil
+}
+
+// UpdateResult is returned from a call to the Update function.
+type UpdateResult struct {
+	gophercloud.Result
+}
diff --git a/openstack/objectstorage/v1/accounts/urls.go b/openstack/objectstorage/v1/accounts/urls.go
new file mode 100644
index 0000000..9952fe4
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls.go
@@ -0,0 +1,11 @@
+package accounts
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint
+}
+
+func updateURL(c *gophercloud.ServiceClient) string {
+	return getURL(c)
+}
diff --git a/openstack/objectstorage/v1/accounts/urls_test.go b/openstack/objectstorage/v1/accounts/urls_test.go
new file mode 100644
index 0000000..074d52d
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls_test.go
@@ -0,0 +1,26 @@
+package accounts
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient())
+	expected := endpoint
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient())
+	expected := endpoint
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/containers/doc.go b/openstack/objectstorage/v1/containers/doc.go
new file mode 100644
index 0000000..5fed553
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/doc.go
@@ -0,0 +1,8 @@
+// Package containers contains functionality for working with Object Storage
+// container resources. A container serves as a logical namespace for objects
+// that are placed inside it - an object with the same name in two different
+// containers represents two different objects.
+//
+// In addition to containing objects, you can also use the container to control
+// access to objects by using an access control list (ACL).
+package containers
diff --git a/openstack/objectstorage/v1/containers/fixtures.go b/openstack/objectstorage/v1/containers/fixtures.go
new file mode 100644
index 0000000..1c0a915
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/fixtures.go
@@ -0,0 +1,132 @@
+// +build fixtures
+
+package containers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ExpectedListInfo is the result expected from a call to `List` when full
+// info is requested.
+var ExpectedListInfo = []Container{
+	Container{
+		Count: 0,
+		Bytes: 0,
+		Name:  "janeausten",
+	},
+	Container{
+		Count: 1,
+		Bytes: 14,
+		Name:  "marktwain",
+	},
+}
+
+// ExpectedListNames is the result expected from a call to `List` when just
+// container names are requested.
+var ExpectedListNames = []string{"janeausten", "marktwain"}
+
+// HandleListContainerInfoSuccessfully creates an HTTP handler at `/` on the test handler mux that
+// responds with a `List` response when full info is requested.
+func HandleListContainerInfoSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `[
+        {
+          "count": 0,
+          "bytes": 0,
+          "name": "janeausten"
+        },
+        {
+          "count": 1,
+          "bytes": 14,
+          "name": "marktwain"
+        }
+      ]`)
+		case "marktwain":
+			fmt.Fprintf(w, `[]`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// HandleListContainerNamesSuccessfully creates an HTTP handler at `/` on the test handler mux that
+// responds with a `ListNames` response when only container names are requested.
+func HandleListContainerNamesSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "text/plain")
+
+		w.Header().Set("Content-Type", "text/plain")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, "janeausten\nmarktwain\n")
+		case "marktwain":
+			fmt.Fprintf(w, ``)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// HandleCreateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
+// responds with a `Create` response.
+func HandleCreateContainerSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Add("X-Container-Meta-Foo", "bar")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleDeleteContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
+// responds with a `Delete` response.
+func HandleDeleteContainerSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
+// responds with a `Update` response.
+func HandleUpdateContainerSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleGetContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
+// responds with a `Get` response.
+func HandleGetContainerSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/objectstorage/v1/containers/requests.go b/openstack/objectstorage/v1/containers/requests.go
new file mode 100644
index 0000000..3fae4d9
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -0,0 +1,206 @@
+package containers
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToContainerListParams() (bool, string, error)
+}
+
+// ListOpts is a structure that holds options for listing containers.
+type ListOpts struct {
+	Full      bool
+	Limit     int    `q:"limit"`
+	Marker    string `q:"marker"`
+	EndMarker string `q:"end_marker"`
+	Format    string `q:"format"`
+	Prefix    string `q:"prefix"`
+	Delimiter string `q:"delimiter"`
+}
+
+// ToContainerListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each container.
+func (opts ListOpts) ToContainerListParams() (bool, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return false, "", err
+	}
+	return opts.Full, q.String(), nil
+}
+
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+
+	url := listURL(c)
+	if opts != nil {
+		full, query, err := opts.ToContainerListParams()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+
+		if full {
+			headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
+		}
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		p := ContainerPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	pager := pagination.NewPager(c, url, createPage)
+	pager.Headers = headers
+	return pager
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToContainerCreateMap() (map[string]string, error)
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+	Metadata          map[string]string
+	ContainerRead     string `h:"X-Container-Read"`
+	ContainerSyncTo   string `h:"X-Container-Sync-To"`
+	ContainerSyncKey  string `h:"X-Container-Sync-Key"`
+	ContainerWrite    string `h:"X-Container-Write"`
+	ContentType       string `h:"Content-Type"`
+	DetectContentType bool   `h:"X-Detect-Content-Type"`
+	IfNoneMatch       string `h:"If-None-Match"`
+	VersionsLocation  string `h:"X-Versions-Location"`
+}
+
+// ToContainerCreateMap formats a CreateOpts into a map of headers.
+func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToContainerCreateMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("PUT", createURL(c, containerName), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) DeleteResult {
+	var res DeleteResult
+	resp, err := perigee.Request("DELETE", deleteURL(c, containerName), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{202, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToContainerUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or
+// deleting a container's metadata.
+type UpdateOpts struct {
+	Metadata               map[string]string
+	ContainerRead          string `h:"X-Container-Read"`
+	ContainerSyncTo        string `h:"X-Container-Sync-To"`
+	ContainerSyncKey       string `h:"X-Container-Sync-Key"`
+	ContainerWrite         string `h:"X-Container-Write"`
+	ContentType            string `h:"Content-Type"`
+	DetectContentType      bool   `h:"X-Detect-Content-Type"`
+	RemoveVersionsLocation string `h:"X-Remove-Versions-Location"`
+	VersionsLocation       string `h:"X-Versions-Location"`
+}
+
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToContainerUpdateMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("POST", updateURL(c, containerName), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{202, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
+func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
+	var res GetResult
+	resp, err := perigee.Request("HEAD", getURL(c, containerName), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/openstack/objectstorage/v1/containers/requests_test.go b/openstack/objectstorage/v1/containers/requests_test.go
new file mode 100644
index 0000000..d0ce7f1
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests_test.go
@@ -0,0 +1,91 @@
+package containers
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var metadata = map[string]string{"gophercloud-test": "containers"}
+
+func TestListContainerInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListContainerInfoSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), &ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ExpectedListInfo, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListContainerNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListContainerNamesSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), &ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateContainerSuccessfully(t)
+
+	options := CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+	res := Create(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+	th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0])
+}
+
+func TestDeleteContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteContainerSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer")
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestUpateContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateContainerSuccessfully(t)
+
+	options := &UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+	res := Update(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestGetContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetContainerSuccessfully(t)
+
+	_, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata()
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
new file mode 100644
index 0000000..c00a4bc
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -0,0 +1,143 @@
+package containers
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Container represents a container resource.
+type Container struct {
+	// The total number of bytes stored in the container.
+	Bytes int `json:"bytes" mapstructure:"bytes"`
+
+	// The total number of objects stored in the container.
+	Count int `json:"count" mapstructure:"count"`
+
+	// The name of the container.
+	Name string `json:"name" mapstructure:"name"`
+}
+
+// ContainerPage is the page returned by a pager when traversing over a
+// collection of containers.
+type ContainerPage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no container names.
+func (r ContainerPage) IsEmpty() (bool, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return true, err
+	}
+	return len(names) == 0, nil
+}
+
+// LastMarker returns the last container name in a ListResult.
+func (r ContainerPage) LastMarker() (string, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return "", err
+	}
+	if len(names) == 0 {
+		return "", nil
+	}
+	return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a ListResult and returns the containers' information.
+func ExtractInfo(page pagination.Page) ([]Container, error) {
+	untyped := page.(ContainerPage).Body.([]interface{})
+	results := make([]Container, len(untyped))
+	for index, each := range untyped {
+		container := each.(map[string]interface{})
+		err := mapstructure.Decode(container, &results[index])
+		if err != nil {
+			return results, err
+		}
+	}
+	return results, nil
+}
+
+// ExtractNames is a function that takes a ListResult and returns the containers' names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	casted := page.(ContainerPage)
+	ct := casted.Header.Get("Content-Type")
+
+	switch {
+	case strings.HasPrefix(ct, "application/json"):
+		parsed, err := ExtractInfo(page)
+		if err != nil {
+			return nil, err
+		}
+
+		names := make([]string, 0, len(parsed))
+		for _, container := range parsed {
+			names = append(names, container.Name)
+		}
+		return names, nil
+	case strings.HasPrefix(ct, "text/plain"):
+		names := make([]string, 0, 50)
+
+		body := string(page.(ContainerPage).Body.([]uint8))
+		for _, name := range strings.Split(body, "\n") {
+			if len(name) > 0 {
+				names = append(names, name)
+			}
+		}
+
+		return names, nil
+	default:
+		return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+	}
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the container.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+	metadata := make(map[string]string)
+	for k, v := range gr.Header {
+		if strings.HasPrefix(k, "X-Container-Meta-") {
+			key := strings.TrimPrefix(k, "X-Container-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata, nil
+}
+
+// CreateResult represents the result of a create operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
+// method on the result struct.
+type CreateResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
+// method on the result struct.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeaders'
+// method on the result struct.
+type DeleteResult struct {
+	commonResult
+}
diff --git a/openstack/objectstorage/v1/containers/urls.go b/openstack/objectstorage/v1/containers/urls.go
new file mode 100644
index 0000000..f864f84
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls.go
@@ -0,0 +1,23 @@
+package containers
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint
+}
+
+func createURL(c *gophercloud.ServiceClient, container string) string {
+	return c.ServiceURL(container)
+}
+
+func getURL(c *gophercloud.ServiceClient, container string) string {
+	return createURL(c, container)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container string) string {
+	return createURL(c, container)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container string) string {
+	return createURL(c, container)
+}
diff --git a/openstack/objectstorage/v1/containers/urls_test.go b/openstack/objectstorage/v1/containers/urls_test.go
new file mode 100644
index 0000000..d043a2a
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls_test.go
@@ -0,0 +1,43 @@
+package containers
+
+import (
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"testing"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/objects/doc.go b/openstack/objectstorage/v1/objects/doc.go
new file mode 100644
index 0000000..30a9add
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/doc.go
@@ -0,0 +1,5 @@
+// Package objects contains functionality for working with Object Storage
+// object resources. An object is a resource that represents and contains data
+// - such as documents, images, and so on. You can also store custom metadata
+// with an object.
+package objects
diff --git a/openstack/objectstorage/v1/objects/fixtures.go b/openstack/objectstorage/v1/objects/fixtures.go
new file mode 100644
index 0000000..d951160
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/fixtures.go
@@ -0,0 +1,164 @@
+// +build fixtures
+
+package objects
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleDownloadObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
+// responds with a `Download` response.
+func HandleDownloadObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, "Successful download with Gophercloud")
+	})
+}
+
+// ExpectedListInfo is the result expected from a call to `List` when full
+// info is requested.
+var ExpectedListInfo = []Object{
+	Object{
+		Hash:         "451e372e48e0f6b1114fa0724aa79fa1",
+		LastModified: "2009-11-10 23:00:00 +0000 UTC",
+		Bytes:        14,
+		Name:         "goodbye",
+		ContentType:  "application/octet-stream",
+	},
+	Object{
+		Hash:         "451e372e48e0f6b1114fa0724aa79fa1",
+		LastModified: "2009-11-10 23:00:00 +0000 UTC",
+		Bytes:        14,
+		Name:         "hello",
+		ContentType:  "application/octet-stream",
+	},
+}
+
+// ExpectedListNames is the result expected from a call to `List` when just
+// object names are requested.
+var ExpectedListNames = []string{"hello", "goodbye"}
+
+// HandleListObjectsInfoSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
+// responds with a `List` response when full info is requested.
+func HandleListObjectsInfoSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Set("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `[
+      {
+        "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+        "last_modified": "2009-11-10 23:00:00 +0000 UTC",
+        "bytes": 14,
+        "name": "goodbye",
+        "content_type": "application/octet-stream"
+      },
+      {
+        "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+        "last_modified": "2009-11-10 23:00:00 +0000 UTC",
+        "bytes": 14,
+        "name": "hello",
+        "content_type": "application/octet-stream"
+      }
+    ]`)
+		case "hello":
+			fmt.Fprintf(w, `[]`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// HandleListObjectNamesSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
+// responds with a `List` response when only object names are requested.
+func HandleListObjectNamesSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "text/plain")
+
+		w.Header().Set("Content-Type", "text/plain")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, "hello\ngoodbye\n")
+		case "goodbye":
+			fmt.Fprintf(w, "")
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// HandleCreateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
+// responds with a `Create` response.
+func HandleCreateObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+// HandleCopyObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
+// responds with a `Copy` response.
+func HandleCopyObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "COPY")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject")
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+// HandleDeleteObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
+// responds with a `Delete` response.
+func HandleDeleteObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleUpdateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
+// responds with a `Update` response.
+func HandleUpdateObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleGetObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
+// responds with a `Get` response.
+func HandleGetObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
new file mode 100644
index 0000000..0ad0315
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -0,0 +1,416 @@
+package objects
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"time"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToObjectListParams() (bool, string, error)
+}
+
+// ListOpts is a structure that holds parameters for listing objects.
+type ListOpts struct {
+	Full      bool
+	Limit     int    `q:"limit"`
+	Marker    string `q:"marker"`
+	EndMarker string `q:"end_marker"`
+	Format    string `q:"format"`
+	Prefix    string `q:"prefix"`
+	Delimiter string `q:"delimiter"`
+	Path      string `q:"path"`
+}
+
+// ToObjectListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each object.
+func (opts ListOpts) ToObjectListParams() (bool, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return false, "", err
+	}
+	return opts.Full, q.String(), nil
+}
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the container. To extract only the object information or names, pass the ListResult
+// response to the ExtractInfo or ExtractNames function, respectively.
+func List(c *gophercloud.ServiceClient, containerName string, opts ListOptsBuilder) pagination.Pager {
+	headers := map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+
+	url := listURL(c, containerName)
+	if opts != nil {
+		full, query, err := opts.ToObjectListParams()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+
+		if full {
+			headers = map[string]string{"Accept": "application/json", "Content-Type": "application/json"}
+		}
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		p := ObjectPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	pager := pagination.NewPager(c, url, createPage)
+	pager.Headers = headers
+	return pager
+}
+
+// DownloadOptsBuilder allows extensions to add additional parameters to the
+// Download request.
+type DownloadOptsBuilder interface {
+	ToObjectDownloadParams() (map[string]string, string, error)
+}
+
+// DownloadOpts is a structure that holds parameters for downloading an object.
+type DownloadOpts struct {
+	IfMatch           string    `h:"If-Match"`
+	IfModifiedSince   time.Time `h:"If-Modified-Since"`
+	IfNoneMatch       string    `h:"If-None-Match"`
+	IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"`
+	Range             string    `h:"Range"`
+	Expires           string    `q:"expires"`
+	MultipartManifest string    `q:"multipart-manifest"`
+	Signature         string    `q:"signature"`
+}
+
+// ToObjectDownloadParams formats a DownloadOpts into a query string and map of
+// headers.
+func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return nil, "", err
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, q.String(), err
+	}
+	return h, q.String(), nil
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the
+// ExtractContent function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOptsBuilder) DownloadResult {
+	var res DownloadResult
+
+	url := downloadURL(c, containerName, objectName)
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, query, err := opts.ToObjectDownloadParams()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		url += query
+	}
+
+	resp, err := perigee.Request("GET", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{200},
+	})
+	defer resp.HttpResponse.Body.Close()
+	body, err := ioutil.ReadAll(resp.HttpResponse.Body)
+	res.Body = body
+	res.Err = err
+	res.Header = resp.HttpResponse.Header
+	return res
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToObjectCreateParams() (map[string]string, string, error)
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentLength      int    `h:"Content-Length"`
+	ContentType        string `h:"Content-Type"`
+	CopyFrom           string `h:"X-Copy-From"`
+	DeleteAfter        int    `h:"X-Delete-After"`
+	DeleteAt           int    `h:"X-Delete-At"`
+	DetectContentType  string `h:"X-Detect-Content-Type"`
+	ETag               string `h:"ETag"`
+	IfNoneMatch        string `h:"If-None-Match"`
+	ObjectManifest     string `h:"X-Object-Manifest"`
+	TransferEncoding   string `h:"Transfer-Encoding"`
+	Expires            string `q:"expires"`
+	MultipartManifest  string `q:"multiple-manifest"`
+	Signature          string `q:"signature"`
+}
+
+// ToObjectCreateParams formats a CreateOpts into a query string and map of
+// headers.
+func (opts CreateOpts) ToObjectCreateParams() (map[string]string, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return nil, "", err
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, q.String(), err
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	return h, q.String(), nil
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	url := createURL(c, containerName, objectName)
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, query, err := opts.ToObjectCreateParams()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+
+		url += query
+	}
+
+	contentType := h["Content-Type"]
+
+	resp, err := perigee.Request("PUT", url, perigee.Options{
+		ContentType: contentType,
+		ReqBody:     content,
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// CopyOptsBuilder allows extensions to add additional parameters to the
+// Copy request.
+type CopyOptsBuilder interface {
+	ToObjectCopyMap() (map[string]string, error)
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to
+// another.
+type CopyOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentType        string `h:"Content-Type"`
+	Destination        string `h:"Destination,required"`
+}
+
+// ToObjectCopyMap formats a CopyOpts into a map of headers.
+func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
+	if opts.Destination == "" {
+		return nil, fmt.Errorf("Required CopyOpts field 'Destination' not set.")
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) CopyResult {
+	var res CopyResult
+	h := c.AuthenticatedHeaders()
+
+	headers, err := opts.ToObjectCopyMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	for k, v := range headers {
+		h[k] = v
+	}
+
+	url := copyURL(c, containerName, objectName)
+	resp, err := perigee.Request("COPY", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201},
+	})
+	res.Header = resp.HttpResponse.Header
+	return res
+}
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+	ToObjectDeleteQuery() (string, error)
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts struct {
+	MultipartManifest string `q:"multipart-manifest"`
+}
+
+// ToObjectDeleteQuery formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToObjectDeleteQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) DeleteResult {
+	var res DeleteResult
+	url := deleteURL(c, containerName, objectName)
+
+	if opts != nil {
+		query, err := opts.ToObjectDeleteQuery()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		url += query
+	}
+
+	resp, err := perigee.Request("DELETE", url, perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// GetOptsBuilder allows extensions to add additional parameters to the
+// Get request.
+type GetOptsBuilder interface {
+	ToObjectGetQuery() (string, error)
+}
+
+// GetOpts is a structure that holds parameters for getting an object's metadata.
+type GetOpts struct {
+	Expires   string `q:"expires"`
+	Signature string `q:"signature"`
+}
+
+// ToObjectGetQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToObjectGetQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOptsBuilder) GetResult {
+	var res GetResult
+	url := getURL(c, containerName, objectName)
+
+	if opts != nil {
+		query, err := opts.ToObjectGetQuery()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		url += query
+	}
+
+	resp, err := perigee.Request("HEAD", url, perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToObjectUpdateMap() (map[string]string, error)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
+// object's metadata.
+type UpdateOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentType        string `h:"Content-Type"`
+	DeleteAfter        int    `h:"X-Delete-After"`
+	DeleteAt           int    `h:"X-Delete-At"`
+	DetectContentType  bool   `h:"X-Detect-Content-Type"`
+}
+
+// ToObjectUpdateMap formats a UpdateOpts into a map of headers.
+func (opts UpdateOpts) ToObjectUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToObjectUpdateMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	url := updateURL(c, containerName, objectName)
+	resp, err := perigee.Request("POST", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{202},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
new file mode 100644
index 0000000..7ab40f2
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -0,0 +1,114 @@
+package objects
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDownloadObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDownloadObjectSuccessfully(t)
+
+	content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, string(content), "Successful download with Gophercloud")
+}
+
+func TestListObjectInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListObjectsInfoSuccessfully(t)
+
+	count := 0
+	options := &ListOpts{Full: true}
+	err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ExpectedListInfo, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListObjectNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListObjectNamesSuccessfully(t)
+
+	count := 0
+	options := &ListOpts{Full: false}
+	err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateObjectSuccessfully(t)
+
+	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+	options := &CreateOpts{ContentType: "application/json"}
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCopyObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCopyObjectSuccessfully(t)
+
+	options := &CopyOpts{Destination: "/newTestContainer/newTestObject"}
+	res := Copy(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestDeleteObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteObjectSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpateObjectMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateObjectSuccessfully(t)
+
+	options := &UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}}
+	res := Update(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetObjectSuccessfully(t)
+
+	expected := map[string]string{"Gophercloud-Test": "objects"}
+	actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
new file mode 100644
index 0000000..f85f1ac
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -0,0 +1,155 @@
+package objects
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object struct {
+	Bytes        int    `json:"bytes" mapstructure:"bytes"`
+	ContentType  string `json:"content_type" mapstructure:"content_type"`
+	Hash         string `json:"hash" mapstructure:"hash"`
+	LastModified string `json:"last_modified" mapstructure:"last_modified"`
+	Name         string `json:"name" mapstructure:"name"`
+}
+
+// ObjectPage is a single page of objects that is returned from a call to the
+// List function.
+type ObjectPage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no object names.
+func (r ObjectPage) IsEmpty() (bool, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return true, err
+	}
+	return len(names) == 0, nil
+}
+
+// LastMarker returns the last object name in a ListResult.
+func (r ObjectPage) LastMarker() (string, error) {
+	names, err := ExtractNames(r)
+	if err != nil {
+		return "", err
+	}
+	if len(names) == 0 {
+		return "", nil
+	}
+	return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]Object, error) {
+	untyped := page.(ObjectPage).Body.([]interface{})
+	results := make([]Object, len(untyped))
+	for index, each := range untyped {
+		object := each.(map[string]interface{})
+		err := mapstructure.Decode(object, &results[index])
+		if err != nil {
+			return results, err
+		}
+	}
+	return results, nil
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	casted := page.(ObjectPage)
+	ct := casted.Header.Get("Content-Type")
+	switch {
+	case strings.HasPrefix(ct, "application/json"):
+		parsed, err := ExtractInfo(page)
+		if err != nil {
+			return nil, err
+		}
+
+		names := make([]string, 0, len(parsed))
+		for _, object := range parsed {
+			names = append(names, object.Name)
+		}
+
+		return names, nil
+	case strings.HasPrefix(ct, "text/plain"):
+		names := make([]string, 0, 50)
+
+		body := string(page.(ObjectPage).Body.([]uint8))
+		for _, name := range strings.Split(body, "\n") {
+			if len(name) > 0 {
+				names = append(names, name)
+			}
+		}
+
+		return names, nil
+	case strings.HasPrefix(ct, "text/html"):
+		return []string{}, nil
+	default:
+		return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+	}
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult struct {
+	commonResult
+}
+
+// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
+// and returns the object's content.
+func (dr DownloadResult) ExtractContent() ([]byte, error) {
+	if dr.Err != nil {
+		return nil, dr.Err
+	}
+	return dr.Body.([]byte), nil
+}
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult struct {
+	commonResult
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+	metadata := make(map[string]string)
+	for k, v := range gr.Header {
+		if strings.HasPrefix(k, "X-Object-Meta-") {
+			key := strings.TrimPrefix(k, "X-Object-Meta-")
+			metadata[key] = v[0]
+		}
+	}
+	return metadata, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	commonResult
+}
+
+// CopyResult represents the result of a copy operation.
+type CopyResult struct {
+	commonResult
+}
diff --git a/openstack/objectstorage/v1/objects/urls.go b/openstack/objectstorage/v1/objects/urls.go
new file mode 100644
index 0000000..d2ec62c
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls.go
@@ -0,0 +1,33 @@
+package objects
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+func listURL(c *gophercloud.ServiceClient, container string) string {
+	return c.ServiceURL(container)
+}
+
+func copyURL(c *gophercloud.ServiceClient, container, object string) string {
+	return c.ServiceURL(container, object)
+}
+
+func createURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func getURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func downloadURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container, object string) string {
+	return copyURL(c, container, object)
+}
diff --git a/openstack/objectstorage/v1/objects/urls_test.go b/openstack/objectstorage/v1/objects/urls_test.go
new file mode 100644
index 0000000..1dcfe35
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls_test.go
@@ -0,0 +1,56 @@
+package objects
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCopyURL(t *testing.T) {
+	actual := copyURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDownloadURL(t *testing.T) {
+	actual := downloadURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "foo/bar"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/utils/choose_version.go b/openstack/utils/choose_version.go
new file mode 100644
index 0000000..753f8f8
--- /dev/null
+++ b/openstack/utils/choose_version.go
@@ -0,0 +1,114 @@
+package utils
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/racker/perigee"
+)
+
+// Version is a supported API version, corresponding to a vN package within the appropriate service.
+type Version struct {
+	ID       string
+	Suffix   string
+	Priority int
+}
+
+var goodStatus = map[string]bool{
+	"current":   true,
+	"supported": true,
+	"stable":    true,
+}
+
+// ChooseVersion queries the base endpoint of a API to choose the most recent non-experimental alternative from a service's
+// published versions.
+// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint.
+func ChooseVersion(identityBase string, identityEndpoint string, recognized []*Version) (*Version, string, error) {
+	type linkResp struct {
+		Href string `json:"href"`
+		Rel  string `json:"rel"`
+	}
+
+	type valueResp struct {
+		ID     string     `json:"id"`
+		Status string     `json:"status"`
+		Links  []linkResp `json:"links"`
+	}
+
+	type versionsResp struct {
+		Values []valueResp `json:"values"`
+	}
+
+	type response struct {
+		Versions versionsResp `json:"versions"`
+	}
+
+	normalize := func(endpoint string) string {
+		if !strings.HasSuffix(endpoint, "/") {
+			return endpoint + "/"
+		}
+		return endpoint
+	}
+	identityEndpoint = normalize(identityEndpoint)
+
+	// If a full endpoint is specified, check version suffixes for a match first.
+	for _, v := range recognized {
+		if strings.HasSuffix(identityEndpoint, v.Suffix) {
+			return v, identityEndpoint, nil
+		}
+	}
+
+	var resp response
+	_, err := perigee.Request("GET", identityBase, perigee.Options{
+		Results: &resp,
+		OkCodes: []int{200, 300},
+	})
+
+	if err != nil {
+		return nil, "", err
+	}
+
+	byID := make(map[string]*Version)
+	for _, version := range recognized {
+		byID[version.ID] = version
+	}
+
+	var highest *Version
+	var endpoint string
+
+	for _, value := range resp.Versions.Values {
+		href := ""
+		for _, link := range value.Links {
+			if link.Rel == "self" {
+				href = normalize(link.Href)
+			}
+		}
+
+		if matching, ok := byID[value.ID]; ok {
+			// Prefer a version that exactly matches the provided endpoint.
+			if href == identityEndpoint {
+				if href == "" {
+					return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, identityBase)
+				}
+				return matching, href, nil
+			}
+
+			// Otherwise, find the highest-priority version with a whitelisted status.
+			if goodStatus[strings.ToLower(value.Status)] {
+				if highest == nil || matching.Priority > highest.Priority {
+					highest = matching
+					endpoint = href
+				}
+			}
+		}
+	}
+
+	if highest == nil {
+		return nil, "", fmt.Errorf("No supported version available from endpoint %s", identityBase)
+	}
+	if endpoint == "" {
+		return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, identityBase)
+	}
+
+	return highest, endpoint, nil
+}
diff --git a/openstack/utils/choose_version_test.go b/openstack/utils/choose_version_test.go
new file mode 100644
index 0000000..9552696
--- /dev/null
+++ b/openstack/utils/choose_version_test.go
@@ -0,0 +1,105 @@
+package utils
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+func setupVersionHandler() {
+	testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `
+			{
+				"versions": {
+					"values": [
+						{
+							"status": "stable",
+							"id": "v3.0",
+							"links": [
+								{ "href": "%s/v3.0", "rel": "self" }
+							]
+						},
+						{
+							"status": "stable",
+							"id": "v2.0",
+							"links": [
+								{ "href": "%s/v2.0", "rel": "self" }
+							]
+						}
+					]
+				}
+			}
+		`, testhelper.Server.URL, testhelper.Server.URL)
+	})
+}
+
+func TestChooseVersion(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	setupVersionHandler()
+
+	v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "blarg"}
+	v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "hargl"}
+
+	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), "", []*Version{v2, v3})
+
+	if err != nil {
+		t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+	}
+
+	if v != v3 {
+		t.Errorf("Expected %#v to win, but %#v did instead", v3, v)
+	}
+
+	expected := testhelper.Endpoint() + "v3.0/"
+	if endpoint != expected {
+		t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+	}
+}
+
+func TestChooseVersionOpinionatedLink(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	setupVersionHandler()
+
+	v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "nope"}
+	v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "northis"}
+
+	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+	if err != nil {
+		t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+	}
+
+	if v != v2 {
+		t.Errorf("Expected %#v to win, but %#v did instead", v2, v)
+	}
+
+	expected := testhelper.Endpoint() + "v2.0/"
+	if endpoint != expected {
+		t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+	}
+}
+
+func TestChooseVersionFromSuffix(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	v2 := &Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"}
+	v3 := &Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"}
+
+	v, endpoint, err := ChooseVersion(testhelper.Endpoint(), testhelper.Endpoint()+"v2.0/", []*Version{v2, v3})
+	if err != nil {
+		t.Fatalf("Unexpected error from ChooseVersion: %v", err)
+	}
+
+	if v != v2 {
+		t.Errorf("Expected %#v to win, but %#v did instead", v2, v)
+	}
+
+	expected := testhelper.Endpoint() + "v2.0/"
+	if endpoint != expected {
+		t.Errorf("Expected endpoint [%s], but was [%s] instead", expected, endpoint)
+	}
+}
diff --git a/osutil/auth.go b/osutil/auth.go
deleted file mode 100644
index a411b63..0000000
--- a/osutil/auth.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package osutil
-
-import (
-	"fmt"
-	"github.com/rackspace/gophercloud"
-	"os"
-	"strings"
-)
-
-var (
-	nilOptions = gophercloud.AuthOptions{}
-
-	// ErrNoAuthUrl errors occur when the value of the OS_AUTH_URL environment variable cannot be determined.
-	ErrNoAuthUrl = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
-
-	// ErrNoUsername errors occur when the value of the OS_USERNAME environment variable cannot be determined.
-	ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
-
-	// ErrNoPassword errors occur when the value of the OS_PASSWORD environment variable cannot be determined.
-	ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_API_KEY needs to be set.")
-)
-
-// AuthOptions fills out a gophercloud.AuthOptions structure with the settings found on the various OpenStack
-// OS_* environment variables.  The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
-// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME.  Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
-// have settings, or an error will result.  OS_TENANT_ID and OS_TENANT_NAME are optional.
-//
-// The value of OS_AUTH_URL will be returned directly to the caller, for subsequent use in
-// gophercloud.Authenticate()'s Provider parameter.  This function will not interpret the value of OS_AUTH_URL,
-// so as a convenient extention, you may set OS_AUTH_URL to, e.g., "rackspace-uk", or any other Gophercloud-recognized
-// provider shortcuts.  For broad compatibility, especially with local installations, you should probably
-// avoid the temptation to do this.
-func AuthOptions() (string, gophercloud.AuthOptions, error) {
-	provider := os.Getenv("OS_AUTH_URL")
-	username := os.Getenv("OS_USERNAME")
-	password := os.Getenv("OS_PASSWORD")
-	tenantId := os.Getenv("OS_TENANT_ID")
-	tenantName := os.Getenv("OS_TENANT_NAME")
-
-	if provider == "" {
-		return "", nilOptions, ErrNoAuthUrl
-	}
-
-	if username == "" {
-		return "", nilOptions, ErrNoUsername
-	}
-
-	if password == "" {
-		return "", nilOptions, ErrNoPassword
-	}
-
-	ao := gophercloud.AuthOptions{
-		Username:   username,
-		Password:   password,
-		TenantId:   tenantId,
-		TenantName: tenantName,
-	}
-
-	if !strings.HasSuffix(provider, "/tokens") {
-		provider += "/tokens"
-	}
-
-	return provider, ao, nil
-}
diff --git a/osutil/region.go b/osutil/region.go
deleted file mode 100644
index f7df507..0000000
--- a/osutil/region.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package osutil
-
-import "os"
-
-// Region provides a means of querying the OS_REGION_NAME environment variable.
-// At present, you may also use os.Getenv("OS_REGION_NAME") as well.
-func Region() string {
-	return os.Getenv("OS_REGION_NAME")
-}
diff --git a/package.go b/package.go
index 396e523..e8c2e82 100644
--- a/package.go
+++ b/package.go
@@ -1,7 +1,38 @@
-// Gophercloud provides a multi-vendor interface to OpenStack-compatible clouds which attempts to follow
-// established Go community coding standards and social norms.
-//
-// Unless you intend on contributing code to the SDK, you will almost certainly never have to use any
-// Context structures or any of its methods.  Contextual methods exist for easier unit testing only.
-// Stick with the global functions unless you know exactly what you're doing, and why.
+/*
+Package gophercloud provides a multi-vendor interface to OpenStack-compatible
+clouds. The library has a three-level hierarchy: providers, services, and
+resources.
+
+Provider structs represent the service providers that offer and manage a
+collection of services. Examples of providers include: OpenStack, Rackspace,
+HP. These are defined like so:
+
+  opts := gophercloud.AuthOptions{
+    IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
+    Username: "{username}",
+    Password: "{password}",
+    TenantID: "{tenant_id}",
+  }
+
+  provider, err := openstack.AuthenticatedClient(opts)
+
+Service structs are specific to a provider and handle all of the logic and
+operations for a particular OpenStack service. Examples of services include:
+Compute, Object Storage, Block Storage. In order to define one, you need to
+pass in the parent provider, like so:
+
+  opts := gophercloud.EndpointOpts{Region: "RegionOne"}
+
+  client := openstack.NewComputeV2(provider, opts)
+
+Resource structs are the domain models that services make use of in order
+to work with and represent the state of API resources:
+
+  server, err := servers.Get(client, "{serverId}").Extract()
+
+Another convention is to return Result structs for API operations, which allow
+you to access the HTTP headers, response body, and associated errors with the
+network transaction. To get a resource struct, you then call the Extract
+method which is chained to the response.
+*/
 package gophercloud
diff --git a/pagination/http.go b/pagination/http.go
new file mode 100644
index 0000000..1e108c8
--- /dev/null
+++ b/pagination/http.go
@@ -0,0 +1,64 @@
+package pagination
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// PageResult stores the HTTP response that returned the current page of results.
+type PageResult struct {
+	gophercloud.Result
+	url.URL
+}
+
+// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the
+// results, interpreting it as JSON if the content type indicates.
+func PageResultFrom(resp http.Response) (PageResult, error) {
+	var parsedBody interface{}
+
+	defer resp.Body.Close()
+	rawBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return PageResult{}, err
+	}
+
+	if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
+		err = json.Unmarshal(rawBody, &parsedBody)
+		if err != nil {
+			return PageResult{}, err
+		}
+	} else {
+		parsedBody = rawBody
+	}
+
+	return PageResult{
+		Result: gophercloud.Result{
+			Body:   parsedBody,
+			Header: resp.Header,
+		},
+		URL: *resp.Request.URL,
+	}, err
+}
+
+// Request performs a Perigee request and extracts the http.Response from the result.
+func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (http.Response, error) {
+	h := client.AuthenticatedHeaders()
+	for key, value := range headers {
+		h[key] = value
+	}
+
+	resp, err := perigee.Request("GET", url, perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{200, 204},
+	})
+	if err != nil {
+		return http.Response{}, err
+	}
+	return resp.HttpResponse, nil
+}
diff --git a/pagination/linked.go b/pagination/linked.go
new file mode 100644
index 0000000..461fa49
--- /dev/null
+++ b/pagination/linked.go
@@ -0,0 +1,61 @@
+package pagination
+
+import "fmt"
+
+// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result.
+type LinkedPageBase struct {
+	PageResult
+
+	// LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer.
+	// If any link along the path is missing, an empty URL will be returned.
+	// If any link results in an unexpected value type, an error will be returned.
+	// When left as "nil", []string{"links", "next"} will be used as a default.
+	LinkPath []string
+}
+
+// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
+// It assumes that the links are available in a "links" element of the top-level response object.
+// If this is not the case, override NextPageURL on your result type.
+func (current LinkedPageBase) NextPageURL() (string, error) {
+	var path []string
+	var key string
+
+	if current.LinkPath == nil {
+		path = []string{"links", "next"}
+	} else {
+		path = current.LinkPath
+	}
+
+	submap, ok := current.Body.(map[string]interface{})
+	if !ok {
+		return "", fmt.Errorf("Expected an object, but was %#v", current.Body)
+	}
+
+	for {
+		key, path = path[0], path[1:len(path)]
+
+		value, ok := submap[key]
+		if !ok {
+			return "", nil
+		}
+
+		if len(path) > 0 {
+			submap, ok = value.(map[string]interface{})
+			if !ok {
+				return "", fmt.Errorf("Expected an object, but was %#v", value)
+			}
+		} else {
+			if value == nil {
+				// Actual null element.
+				return "", nil
+			}
+
+			url, ok := value.(string)
+			if !ok {
+				return "", fmt.Errorf("Expected a string, but was %#v", value)
+			}
+
+			return url, nil
+		}
+	}
+}
diff --git a/pagination/linked_test.go b/pagination/linked_test.go
new file mode 100644
index 0000000..4d3248e
--- /dev/null
+++ b/pagination/linked_test.go
@@ -0,0 +1,107 @@
+package pagination
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+// LinkedPager sample and test cases.
+
+type LinkedPageResult struct {
+	LinkedPageBase
+}
+
+func (r LinkedPageResult) IsEmpty() (bool, error) {
+	is, err := ExtractLinkedInts(r)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+func ExtractLinkedInts(page Page) ([]int, error) {
+	var response struct {
+		Ints []int `mapstructure:"ints"`
+	}
+
+	err := mapstructure.Decode(page.(LinkedPageResult).Body, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response.Ints, nil
+}
+
+func createLinked(t *testing.T) Pager {
+	testhelper.SetupHTTP()
+
+	testhelper.Mux.HandleFunc("/page1", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{ "ints": [1, 2, 3], "links": { "next": "%s/page2" } }`, testhelper.Server.URL)
+	})
+
+	testhelper.Mux.HandleFunc("/page2", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{ "ints": [4, 5, 6], "links": { "next": "%s/page3" } }`, testhelper.Server.URL)
+	})
+
+	testhelper.Mux.HandleFunc("/page3", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{ "ints": [7, 8, 9], "links": { "next": null } }`)
+	})
+
+	client := createClient()
+
+	createPage := func(r PageResult) Page {
+		return LinkedPageResult{LinkedPageBase{PageResult: r}}
+	}
+
+	return NewPager(client, testhelper.Server.URL+"/page1", createPage)
+}
+
+func TestEnumerateLinked(t *testing.T) {
+	pager := createLinked(t)
+	defer testhelper.TeardownHTTP()
+
+	callCount := 0
+	err := pager.EachPage(func(page Page) (bool, error) {
+		actual, err := ExtractLinkedInts(page)
+		if err != nil {
+			return false, err
+		}
+
+		t.Logf("Handler invoked with %v", actual)
+
+		var expected []int
+		switch callCount {
+		case 0:
+			expected = []int{1, 2, 3}
+		case 1:
+			expected = []int{4, 5, 6}
+		case 2:
+			expected = []int{7, 8, 9}
+		default:
+			t.Fatalf("Unexpected call count: %d", callCount)
+			return false, nil
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Call %d: Expected %#v, but was %#v", callCount, expected, actual)
+		}
+
+		callCount++
+		return true, nil
+	})
+	if err != nil {
+		t.Errorf("Unexpected error for page iteration: %v", err)
+	}
+
+	if callCount != 3 {
+		t.Errorf("Expected 3 calls, but was %d", callCount)
+	}
+}
diff --git a/pagination/marker.go b/pagination/marker.go
new file mode 100644
index 0000000..e7688c2
--- /dev/null
+++ b/pagination/marker.go
@@ -0,0 +1,34 @@
+package pagination
+
+// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager.
+// For convenience, embed the MarkedPageBase struct.
+type MarkerPage interface {
+	Page
+
+	// LastMarker returns the last "marker" value on this page.
+	LastMarker() (string, error)
+}
+
+// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters.
+type MarkerPageBase struct {
+	PageResult
+
+	// Owner is a reference to the embedding struct.
+	Owner MarkerPage
+}
+
+// NextPageURL generates the URL for the page of results after this one.
+func (current MarkerPageBase) NextPageURL() (string, error) {
+	currentURL := current.URL
+
+	mark, err := current.Owner.LastMarker()
+	if err != nil {
+		return "", err
+	}
+
+	q := currentURL.Query()
+	q.Set("marker", mark)
+	currentURL.RawQuery = q.Encode()
+
+	return currentURL.String(), nil
+}
diff --git a/pagination/marker_test.go b/pagination/marker_test.go
new file mode 100644
index 0000000..3b1df1d
--- /dev/null
+++ b/pagination/marker_test.go
@@ -0,0 +1,113 @@
+package pagination
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+// MarkerPager sample and test cases.
+
+type MarkerPageResult struct {
+	MarkerPageBase
+}
+
+func (r MarkerPageResult) IsEmpty() (bool, error) {
+	results, err := ExtractMarkerStrings(r)
+	if err != nil {
+		return true, err
+	}
+	return len(results) == 0, err
+}
+
+func (r MarkerPageResult) LastMarker() (string, error) {
+	results, err := ExtractMarkerStrings(r)
+	if err != nil {
+		return "", err
+	}
+	if len(results) == 0 {
+		return "", nil
+	}
+	return results[len(results)-1], nil
+}
+
+func createMarkerPaged(t *testing.T) Pager {
+	testhelper.SetupHTTP()
+
+	testhelper.Mux.HandleFunc("/page", func(w http.ResponseWriter, r *http.Request) {
+		r.ParseForm()
+		ms := r.Form["marker"]
+		switch {
+		case len(ms) == 0:
+			fmt.Fprintf(w, "aaa\nbbb\nccc")
+		case len(ms) == 1 && ms[0] == "ccc":
+			fmt.Fprintf(w, "ddd\neee\nfff")
+		case len(ms) == 1 && ms[0] == "fff":
+			fmt.Fprintf(w, "ggg\nhhh\niii")
+		case len(ms) == 1 && ms[0] == "iii":
+			w.WriteHeader(http.StatusNoContent)
+		default:
+			t.Errorf("Request with unexpected marker: [%v]", ms)
+		}
+	})
+
+	client := createClient()
+
+	createPage := func(r PageResult) Page {
+		p := MarkerPageResult{MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	return NewPager(client, testhelper.Server.URL+"/page", createPage)
+}
+
+func ExtractMarkerStrings(page Page) ([]string, error) {
+	content := page.(MarkerPageResult).Body.([]uint8)
+	parts := strings.Split(string(content), "\n")
+	results := make([]string, 0, len(parts))
+	for _, part := range parts {
+		if len(part) > 0 {
+			results = append(results, part)
+		}
+	}
+	return results, nil
+}
+
+func TestEnumerateMarker(t *testing.T) {
+	pager := createMarkerPaged(t)
+	defer testhelper.TeardownHTTP()
+
+	callCount := 0
+	err := pager.EachPage(func(page Page) (bool, error) {
+		actual, err := ExtractMarkerStrings(page)
+		if err != nil {
+			return false, err
+		}
+
+		t.Logf("Handler invoked with %v", actual)
+
+		var expected []string
+		switch callCount {
+		case 0:
+			expected = []string{"aaa", "bbb", "ccc"}
+		case 1:
+			expected = []string{"ddd", "eee", "fff"}
+		case 2:
+			expected = []string{"ggg", "hhh", "iii"}
+		default:
+			t.Fatalf("Unexpected call count: %d", callCount)
+			return false, nil
+		}
+
+		testhelper.CheckDeepEquals(t, expected, actual)
+
+		callCount++
+		return true, nil
+	})
+	testhelper.AssertNoErr(t, err)
+	testhelper.AssertEquals(t, callCount, 3)
+}
diff --git a/pagination/null.go b/pagination/null.go
new file mode 100644
index 0000000..ae57e18
--- /dev/null
+++ b/pagination/null.go
@@ -0,0 +1,20 @@
+package pagination
+
+// nullPage is an always-empty page that trivially satisfies all Page interfacts.
+// It's useful to be returned along with an error.
+type nullPage struct{}
+
+// NextPageURL always returns "" to indicate that there are no more pages to return.
+func (p nullPage) NextPageURL() (string, error) {
+	return "", nil
+}
+
+// IsEmpty always returns true to prevent iteration over nullPages.
+func (p nullPage) IsEmpty() (bool, error) {
+	return true, nil
+}
+
+// LastMark always returns "" because the nullPage contains no items to have a mark.
+func (p nullPage) LastMark() (string, error) {
+	return "", nil
+}
diff --git a/pagination/pager.go b/pagination/pager.go
new file mode 100644
index 0000000..5c20e16
--- /dev/null
+++ b/pagination/pager.go
@@ -0,0 +1,115 @@
+package pagination
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+)
+
+var (
+	// ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
+	ErrPageNotAvailable = errors.New("The requested page does not exist.")
+)
+
+// Page must be satisfied by the result type of any resource collection.
+// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
+// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs,
+// instead.
+// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type
+// will need to implement.
+type Page interface {
+
+	// NextPageURL generates the URL for the page of data that follows this collection.
+	// Return "" if no such page exists.
+	NextPageURL() (string, error)
+
+	// IsEmpty returns true if this Page has no items in it.
+	IsEmpty() (bool, error)
+}
+
+// Pager knows how to advance through a specific resource collection, one page at a time.
+type Pager struct {
+	client *gophercloud.ServiceClient
+
+	initialURL string
+
+	createPage func(r PageResult) Page
+
+	Err error
+
+	// Headers supplies additional HTTP headers to populate on each paged request.
+	Headers map[string]string
+}
+
+// NewPager constructs a manually-configured pager.
+// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
+func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager {
+	return Pager{
+		client:     client,
+		initialURL: initialURL,
+		createPage: createPage,
+	}
+}
+
+// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
+// useful for overriding List functions in delegation.
+func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager {
+	return Pager{
+		client:     p.client,
+		initialURL: p.initialURL,
+		createPage: createPage,
+	}
+}
+
+func (p Pager) fetchNextPage(url string) (Page, error) {
+	resp, err := Request(p.client, p.Headers, url)
+	if err != nil {
+		return nil, err
+	}
+
+	remembered, err := PageResultFrom(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return p.createPage(remembered), nil
+}
+
+// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
+// Return "false" from the handler to prematurely stop iterating.
+func (p Pager) EachPage(handler func(Page) (bool, error)) error {
+	if p.Err != nil {
+		return p.Err
+	}
+	currentURL := p.initialURL
+	for {
+		currentPage, err := p.fetchNextPage(currentURL)
+		if err != nil {
+			return err
+		}
+
+		empty, err := currentPage.IsEmpty()
+		if err != nil {
+			return err
+		}
+		if empty {
+			return nil
+		}
+
+		ok, err := handler(currentPage)
+		if err != nil {
+			return err
+		}
+		if !ok {
+			return nil
+		}
+
+		currentURL, err = currentPage.NextPageURL()
+		if err != nil {
+			return err
+		}
+		if currentURL == "" {
+			return nil
+		}
+	}
+}
diff --git a/pagination/pagination_test.go b/pagination/pagination_test.go
new file mode 100644
index 0000000..f3e4de1
--- /dev/null
+++ b/pagination/pagination_test.go
@@ -0,0 +1,13 @@
+package pagination
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+func createClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{TokenID: "abc123"},
+		Endpoint:       testhelper.Endpoint(),
+	}
+}
diff --git a/pagination/pkg.go b/pagination/pkg.go
new file mode 100644
index 0000000..912daea
--- /dev/null
+++ b/pagination/pkg.go
@@ -0,0 +1,4 @@
+/*
+Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs.
+*/
+package pagination
diff --git a/pagination/single.go b/pagination/single.go
new file mode 100644
index 0000000..4dd3c5c
--- /dev/null
+++ b/pagination/single.go
@@ -0,0 +1,9 @@
+package pagination
+
+// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once.
+type SinglePageBase PageResult
+
+// NextPageURL always returns "" to indicate that there are no more pages to return.
+func (current SinglePageBase) NextPageURL() (string, error) {
+	return "", nil
+}
diff --git a/pagination/single_test.go b/pagination/single_test.go
new file mode 100644
index 0000000..8817d57
--- /dev/null
+++ b/pagination/single_test.go
@@ -0,0 +1,71 @@
+package pagination
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+// SinglePage sample and test cases.
+
+type SinglePageResult struct {
+	SinglePageBase
+}
+
+func (r SinglePageResult) IsEmpty() (bool, error) {
+	is, err := ExtractSingleInts(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+func ExtractSingleInts(page Page) ([]int, error) {
+	var response struct {
+		Ints []int `mapstructure:"ints"`
+	}
+
+	err := mapstructure.Decode(page.(SinglePageResult).Body, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response.Ints, nil
+}
+
+func setupSinglePaged() Pager {
+	testhelper.SetupHTTP()
+	client := createClient()
+
+	testhelper.Mux.HandleFunc("/only", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{ "ints": [1, 2, 3] }`)
+	})
+
+	createPage := func(r PageResult) Page {
+		return SinglePageResult{SinglePageBase(r)}
+	}
+
+	return NewPager(client, testhelper.Server.URL+"/only", createPage)
+}
+
+func TestEnumerateSinglePaged(t *testing.T) {
+	callCount := 0
+	pager := setupSinglePaged()
+	defer testhelper.TeardownHTTP()
+
+	err := pager.EachPage(func(page Page) (bool, error) {
+		callCount++
+
+		expected := []int{1, 2, 3}
+		actual, err := ExtractSingleInts(page)
+		testhelper.AssertNoErr(t, err)
+		testhelper.CheckDeepEquals(t, expected, actual)
+		return true, nil
+	})
+	testhelper.CheckNoErr(t, err)
+	testhelper.CheckEquals(t, 1, callCount)
+}
diff --git a/params.go b/params.go
new file mode 100644
index 0000000..5fe3c2c
--- /dev/null
+++ b/params.go
@@ -0,0 +1,184 @@
+package gophercloud
+
+import (
+	"fmt"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// MaybeString takes a string that might be a zero-value, and either returns a
+// pointer to its address or a nil value (i.e. empty pointer). This is useful
+// for converting zero values in options structs when the end-user hasn't
+// defined values. Those zero values need to be nil in order for the JSON
+// serialization to ignore them.
+func MaybeString(original string) *string {
+	if original != "" {
+		return &original
+	}
+	return nil
+}
+
+// MaybeInt takes an int that might be a zero-value, and either returns a
+// pointer to its address or a nil value (i.e. empty pointer).
+func MaybeInt(original int) *int {
+	if original != 0 {
+		return &original
+	}
+	return nil
+}
+
+var t time.Time
+
+func isZero(v reflect.Value) bool {
+	switch v.Kind() {
+	case reflect.Func, reflect.Map, reflect.Slice:
+		return v.IsNil()
+	case reflect.Array:
+		z := true
+		for i := 0; i < v.Len(); i++ {
+			z = z && isZero(v.Index(i))
+		}
+		return z
+	case reflect.Struct:
+		if v.Type() == reflect.TypeOf(t) {
+			if v.Interface().(time.Time).IsZero() {
+				return true
+			}
+			return false
+		}
+		z := true
+		for i := 0; i < v.NumField(); i++ {
+			z = z && isZero(v.Field(i))
+		}
+		return z
+	}
+	// Compare other types directly:
+	z := reflect.Zero(v.Type())
+	return v.Interface() == z.Interface()
+}
+
+/*
+BuildQueryString accepts a generic structure and parses it URL struct. It
+converts field names into query names based on "q" tags. So for example, this
+type:
+
+	struct {
+	   Bar string `q:"x_bar"`
+	   Baz int    `q:"lorem_ipsum"`
+	}{
+	   Bar: "XXX",
+	   Baz: "YYY",
+	}
+
+will be converted into ?x_bar=XXX&lorem_ipsum=YYYY
+*/
+func BuildQueryString(opts interface{}) (*url.URL, error) {
+	optsValue := reflect.ValueOf(opts)
+	if optsValue.Kind() == reflect.Ptr {
+		optsValue = optsValue.Elem()
+	}
+
+	optsType := reflect.TypeOf(opts)
+	if optsType.Kind() == reflect.Ptr {
+		optsType = optsType.Elem()
+	}
+
+	var optsSlice []string
+	if optsValue.Kind() == reflect.Struct {
+		for i := 0; i < optsValue.NumField(); i++ {
+			v := optsValue.Field(i)
+			f := optsType.Field(i)
+			qTag := f.Tag.Get("q")
+
+			// if the field has a 'q' tag, it goes in the query string
+			if qTag != "" {
+				tags := strings.Split(qTag, ",")
+
+				// if the field is set, add it to the slice of query pieces
+				if !isZero(v) {
+					switch v.Kind() {
+					case reflect.String:
+						optsSlice = append(optsSlice, tags[0]+"="+v.String())
+					case reflect.Int:
+						optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10))
+					case reflect.Bool:
+						optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool()))
+					}
+				} else {
+					// Otherwise, the field is not set.
+					if len(tags) == 2 && tags[1] == "required" {
+						// And the field is required. Return an error.
+						return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
+					}
+				}
+			}
+
+		}
+		// URL encode the string for safety.
+		s := strings.Join(optsSlice, "&")
+		if s != "" {
+			s = "?" + s
+		}
+		u, err := url.Parse(s)
+		if err != nil {
+			return nil, err
+		}
+		return u, nil
+	}
+	// Return an error if the underlying type of 'opts' isn't a struct.
+	return nil, fmt.Errorf("Options type is not a struct.")
+}
+
+// BuildHeaders accepts a generic structure and parses it into a string map. It
+// converts field names into header names based on "h" tags, and field values
+// into header values by a simple one-to-one mapping.
+func BuildHeaders(opts interface{}) (map[string]string, error) {
+	optsValue := reflect.ValueOf(opts)
+	if optsValue.Kind() == reflect.Ptr {
+		optsValue = optsValue.Elem()
+	}
+
+	optsType := reflect.TypeOf(opts)
+	if optsType.Kind() == reflect.Ptr {
+		optsType = optsType.Elem()
+	}
+
+	optsMap := make(map[string]string)
+	if optsValue.Kind() == reflect.Struct {
+		for i := 0; i < optsValue.NumField(); i++ {
+			v := optsValue.Field(i)
+			f := optsType.Field(i)
+			hTag := f.Tag.Get("h")
+
+			// if the field has a 'h' tag, it goes in the header
+			if hTag != "" {
+				tags := strings.Split(hTag, ",")
+
+				// if the field is set, add it to the slice of query pieces
+				if !isZero(v) {
+					switch v.Kind() {
+					case reflect.String:
+						optsMap[tags[0]] = v.String()
+					case reflect.Int:
+						optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
+					case reflect.Bool:
+						optsMap[tags[0]] = strconv.FormatBool(v.Bool())
+					}
+				} else {
+					// Otherwise, the field is not set.
+					if len(tags) == 2 && tags[1] == "required" {
+						// And the field is required. Return an error.
+						return optsMap, fmt.Errorf("Required header not set.")
+					}
+				}
+			}
+
+		}
+		return optsMap, nil
+	}
+	// Return an error if the underlying type of 'opts' isn't a struct.
+	return optsMap, fmt.Errorf("Options type is not a struct.")
+}
diff --git a/params_test.go b/params_test.go
new file mode 100644
index 0000000..9f1d3bd
--- /dev/null
+++ b/params_test.go
@@ -0,0 +1,142 @@
+package gophercloud
+
+import (
+	"net/url"
+	"reflect"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMaybeString(t *testing.T) {
+	testString := ""
+	var expected *string
+	actual := MaybeString(testString)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testString = "carol"
+	expected = &testString
+	actual = MaybeString(testString)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestMaybeInt(t *testing.T) {
+	testInt := 0
+	var expected *int
+	actual := MaybeInt(testInt)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testInt = 4
+	expected = &testInt
+	actual = MaybeInt(testInt)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBuildQueryString(t *testing.T) {
+	opts := struct {
+		J int    `q:"j"`
+		R string `q:"r,required"`
+		C bool   `q:"c"`
+	}{
+		J: 2,
+		R: "red",
+		C: true,
+	}
+	expected := &url.URL{RawQuery: "j=2&r=red&c=true"}
+	actual, err := BuildQueryString(&opts)
+	if err != nil {
+		t.Errorf("Error building query string: %v", err)
+	}
+	th.CheckDeepEquals(t, expected, actual)
+
+	opts = struct {
+		J int    `q:"j"`
+		R string `q:"r,required"`
+		C bool   `q:"c"`
+	}{
+		J: 2,
+		C: true,
+	}
+	_, err = BuildQueryString(&opts)
+	if err == nil {
+		t.Errorf("Expected error: 'Required field not set'")
+	}
+	th.CheckDeepEquals(t, expected, actual)
+
+	_, err = BuildQueryString(map[string]interface{}{"Number": 4})
+	if err == nil {
+		t.Errorf("Expected error: 'Options type is not a struct'")
+	}
+}
+
+func TestBuildHeaders(t *testing.T) {
+	testStruct := struct {
+		Accept string `h:"Accept"`
+		Num    int    `h:"Number,required"`
+		Style  bool   `h:"Style"`
+	}{
+		Accept: "application/json",
+		Num:    4,
+		Style:  true,
+	}
+	expected := map[string]string{"Accept": "application/json", "Number": "4", "Style": "true"}
+	actual, err := BuildHeaders(&testStruct)
+	th.CheckNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testStruct.Num = 0
+	_, err = BuildHeaders(&testStruct)
+	if err == nil {
+		t.Errorf("Expected error: 'Required header not set'")
+	}
+
+	_, err = BuildHeaders(map[string]interface{}{"Number": 4})
+	if err == nil {
+		t.Errorf("Expected error: 'Options type is not a struct'")
+	}
+}
+
+func TestIsZero(t *testing.T) {
+	var testMap map[string]interface{}
+	testMapValue := reflect.ValueOf(testMap)
+	expected := true
+	actual := isZero(testMapValue)
+	th.CheckEquals(t, expected, actual)
+	testMap = map[string]interface{}{"empty": false}
+	testMapValue = reflect.ValueOf(testMap)
+	expected = false
+	actual = isZero(testMapValue)
+	th.CheckEquals(t, expected, actual)
+
+	var testArray [2]string
+	testArrayValue := reflect.ValueOf(testArray)
+	expected = true
+	actual = isZero(testArrayValue)
+	th.CheckEquals(t, expected, actual)
+	testArray = [2]string{"one", "two"}
+	testArrayValue = reflect.ValueOf(testArray)
+	expected = false
+	actual = isZero(testArrayValue)
+	th.CheckEquals(t, expected, actual)
+
+	var testStruct struct {
+		A string
+		B time.Time
+	}
+	testStructValue := reflect.ValueOf(testStruct)
+	expected = true
+	actual = isZero(testStructValue)
+	th.CheckEquals(t, expected, actual)
+	testStruct = struct {
+		A string
+		B time.Time
+	}{
+		B: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
+	}
+	testStructValue = reflect.ValueOf(testStruct)
+	expected = false
+	actual = isZero(testStructValue)
+	th.CheckEquals(t, expected, actual)
+
+}
diff --git a/provider_client.go b/provider_client.go
new file mode 100644
index 0000000..7754c20
--- /dev/null
+++ b/provider_client.go
@@ -0,0 +1,33 @@
+package gophercloud
+
+// ProviderClient stores details that are required to interact with any
+// services within a specific provider's API.
+//
+// Generally, you acquire a ProviderClient by calling the NewClient method in
+// the appropriate provider's child package, providing whatever authentication
+// credentials are required.
+type ProviderClient struct {
+	// IdentityBase is the base URL used for a particular provider's identity
+	// service - it will be used when issuing authenticatation requests. It
+	// should point to the root resource of the identity service, not a specific
+	// identity version.
+	IdentityBase string
+
+	// IdentityEndpoint is the identity endpoint. This may be a specific version
+	// of the identity service. If this is the case, this endpoint is used rather
+	// than querying versions first.
+	IdentityEndpoint string
+
+	// TokenID is the ID of the most recently issued valid token.
+	TokenID string
+
+	// EndpointLocator describes how this provider discovers the endpoints for
+	// its constituent services.
+	EndpointLocator EndpointLocator
+}
+
+// AuthenticatedHeaders returns a map of HTTP headers that are common for all
+// authenticated service requests.
+func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
+	return map[string]string{"X-Auth-Token": client.TokenID}
+}
diff --git a/provider_client_test.go b/provider_client_test.go
new file mode 100644
index 0000000..b260246
--- /dev/null
+++ b/provider_client_test.go
@@ -0,0 +1,16 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedHeaders(t *testing.T) {
+	p := &ProviderClient{
+		TokenID: "1234",
+	}
+	expected := map[string]string{"X-Auth-Token": "1234"}
+	actual := p.AuthenticatedHeaders()
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/auth_env.go b/rackspace/auth_env.go
new file mode 100644
index 0000000..5852c3c
--- /dev/null
+++ b/rackspace/auth_env.go
@@ -0,0 +1,57 @@
+package rackspace
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/rackspace/gophercloud"
+)
+
+var nilOptions = gophercloud.AuthOptions{}
+
+// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the
+// required RS_AUTH_URL, RS_USERNAME, or RS_PASSWORD environment variables,
+// respectively, remain undefined.  See the AuthOptions() function for more details.
+var (
+	ErrNoAuthURL  = fmt.Errorf("Environment variable RS_AUTH_URL or OS_AUTH_URL need to be set.")
+	ErrNoUsername = fmt.Errorf("Environment variable RS_USERNAME or OS_USERNAME need to be set.")
+	ErrNoPassword = fmt.Errorf("Environment variable RS_API_KEY or RS_PASSWORD needs to be set.")
+)
+
+func prefixedEnv(base string) string {
+	value := os.Getenv("RS_" + base)
+	if value == "" {
+		value = os.Getenv("OS_" + base)
+	}
+	return value
+}
+
+// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the
+// settings found on the various Rackspace RS_* environment variables.
+func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
+	authURL := prefixedEnv("AUTH_URL")
+	username := prefixedEnv("USERNAME")
+	password := prefixedEnv("PASSWORD")
+	apiKey := prefixedEnv("API_KEY")
+
+	if authURL == "" {
+		return nilOptions, ErrNoAuthURL
+	}
+
+	if username == "" {
+		return nilOptions, ErrNoUsername
+	}
+
+	if password == "" && apiKey == "" {
+		return nilOptions, ErrNoPassword
+	}
+
+	ao := gophercloud.AuthOptions{
+		IdentityEndpoint: authURL,
+		Username:         username,
+		Password:         password,
+		APIKey:           apiKey,
+	}
+
+	return ao, nil
+}
diff --git a/rackspace/blockstorage/v1/snapshots/delegate.go b/rackspace/blockstorage/v1/snapshots/delegate.go
new file mode 100644
index 0000000..3ae2438
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/delegate.go
@@ -0,0 +1,134 @@
+package snapshots
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+)
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("snapshots", id)
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToSnapshotCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains options for creating a Snapshot. This object is passed to
+// the snapshots.Create function. For more information about these parameters,
+// see the Snapshot object.
+type CreateOpts struct {
+	// REQUIRED
+	VolumeID string
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Force bool
+	// OPTIONAL
+	Name string
+}
+
+// ToSnapshotCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.VolumeID == "" {
+		return nil, errors.New("Required CreateOpts field 'VolumeID' not set.")
+	}
+
+	s["volume_id"] = opts.VolumeID
+
+	if opts.Description != "" {
+		s["display_description"] = opts.Description
+	}
+	if opts.Name != "" {
+		s["display_name"] = opts.Name
+	}
+	if opts.Force {
+		s["force"] = opts.Force
+	}
+
+	return map[string]interface{}{"snapshot": s}, nil
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To
+// extract the Snapshot object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	return os.Delete(client, id)
+}
+
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot
+// object from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
+
+// List returns Snapshots.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client, os.ListOpts{})
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToSnapshotUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	Name        string
+	Description string
+}
+
+// ToSnapshotUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name != "" {
+		s["display_name"] = opts.Name
+	}
+	if opts.Description != "" {
+		s["display_description"] = opts.Description
+	}
+
+	return map[string]interface{}{"snapshot": s}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing snapshot using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, snapshotID string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToSnapshotUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = perigee.Request("PUT", updateURL(c, snapshotID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201},
+	})
+
+	return res
+}
diff --git a/rackspace/blockstorage/v1/snapshots/delegate_test.go b/rackspace/blockstorage/v1/snapshots/delegate_test.go
new file mode 100644
index 0000000..fad7636
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/delegate_test.go
@@ -0,0 +1,97 @@
+package snapshots
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const endpoint = "http://localhost:57909/v1/12345"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockListResponse(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSnapshots(page)
+		if err != nil {
+			t.Errorf("Failed to extract snapshots: %v", err)
+			return false, err
+		}
+
+		expected := []Snapshot{
+			Snapshot{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "snapshot-001",
+			},
+			Snapshot{
+				ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name: "snapshot-002",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertEquals(t, 1, count)
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockGetResponse(t)
+
+	v, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "snapshot-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockCreateResponse(t)
+
+	options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.VolumeID, "1234")
+	th.AssertEquals(t, n.Name, "snapshot-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockDeleteResponse(t)
+
+	err := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/blockstorage/v1/snapshots/doc.go b/rackspace/blockstorage/v1/snapshots/doc.go
new file mode 100644
index 0000000..ad6064f
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/doc.go
@@ -0,0 +1,3 @@
+// Package snapshots provides information and interaction with the snapshot
+// API resource for the Rackspace Block Storage service.
+package snapshots
diff --git a/rackspace/blockstorage/v1/snapshots/results.go b/rackspace/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..0fab282
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,149 @@
+package snapshots
+
+import (
+	"github.com/racker/perigee"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Status is the type used to represent a snapshot's status
+type Status string
+
+// Constants to use for supported statuses
+const (
+	Creating    Status = "CREATING"
+	Available   Status = "AVAILABLE"
+	Deleting    Status = "DELETING"
+	Error       Status = "ERROR"
+	DeleteError Status = "ERROR_DELETING"
+)
+
+// Snapshot is the Rackspace representation of an external block storage device.
+type Snapshot struct {
+	// The timestamp when this snapshot was created.
+	CreatedAt string `mapstructure:"created_at"`
+
+	// The human-readable description for this snapshot.
+	Description string `mapstructure:"display_description"`
+
+	// The human-readable name for this snapshot.
+	Name string `mapstructure:"display_name"`
+
+	// The UUID for this snapshot.
+	ID string `mapstructure:"id"`
+
+	// The random metadata associated with this snapshot. Note: unlike standard
+	// OpenStack snapshots, this cannot actually be set.
+	Metadata map[string]string `mapstructure:"metadata"`
+
+	// Indicates the current progress of the snapshot's backup procedure.
+	Progress string `mapstructure:"os-extended-snapshot-attributes:progress"`
+
+	// The project ID.
+	ProjectID string `mapstructure:"os-extended-snapshot-attributes:project_id"`
+
+	// The size of the volume which this snapshot backs up.
+	Size int `mapstructure:"size"`
+
+	// The status of the snapshot.
+	Status Status `mapstructure:"status"`
+
+	// The ID of the volume which this snapshot seeks to back up.
+	VolumeID string `mapstructure:"volume_id"`
+}
+
+// CreateResult represents the result of a create operation
+type CreateResult struct {
+	os.CreateResult
+}
+
+// GetResult represents the result of a get operation
+type GetResult struct {
+	os.GetResult
+}
+
+// UpdateResult represents the result of an update operation
+type UpdateResult struct {
+	gophercloud.Result
+}
+
+func commonExtract(resp interface{}, err error) (*Snapshot, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		Snapshot *Snapshot `json:"snapshot"`
+	}
+
+	err = mapstructure.Decode(resp, &respStruct)
+
+	return respStruct.Snapshot, err
+}
+
+// Extract will get the Snapshot object out of the GetResult object.
+func (r GetResult) Extract() (*Snapshot, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the CreateResult object.
+func (r CreateResult) Extract() (*Snapshot, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*Snapshot, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
+	var response struct {
+		Snapshots []Snapshot `json:"snapshots"`
+	}
+
+	err := mapstructure.Decode(page.(os.ListResult).Body, &response)
+	return response.Snapshots, err
+}
+
+// WaitUntilComplete will continually poll a snapshot until it successfully
+// transitions to a specified state. It will do this for at most the number of
+// seconds specified.
+func (snapshot Snapshot) WaitUntilComplete(c *gophercloud.ServiceClient, timeout int) error {
+	return gophercloud.WaitFor(timeout, func() (bool, error) {
+		// Poll resource
+		current, err := Get(c, snapshot.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		// Has it been built yet?
+		if current.Progress == "100%" {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
+
+// WaitUntilDeleted will continually poll a snapshot until it has been
+// successfully deleted, i.e. returns a 404 status.
+func (snapshot Snapshot) WaitUntilDeleted(c *gophercloud.ServiceClient, timeout int) error {
+	return gophercloud.WaitFor(timeout, func() (bool, error) {
+		// Poll resource
+		_, err := Get(c, snapshot.ID).Extract()
+
+		// Check for a 404
+		if casted, ok := err.(*perigee.UnexpectedResponseCodeError); ok && casted.Actual == 404 {
+			return true, nil
+		} else if err != nil {
+			return false, err
+		}
+
+		return false, nil
+	})
+}
diff --git a/rackspace/blockstorage/v1/volumes/delegate.go b/rackspace/blockstorage/v1/volumes/delegate.go
new file mode 100644
index 0000000..4f14454
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/delegate.go
@@ -0,0 +1,75 @@
+package volumes
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type CreateOpts struct {
+	os.CreateOpts
+}
+
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+	if opts.Size < 75 || opts.Size > 1024 {
+		return nil, fmt.Errorf("Size field must be between 75 and 1024")
+	}
+
+	return opts.CreateOpts.ToVolumeCreateMap()
+}
+
+// Create will create a new Volume based on the values in CreateOpts. To extract
+// the Volume object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	return os.Delete(client, id)
+}
+
+// Get retrieves the Volume with the provided ID. To extract the Volume object
+// from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
+
+// List returns volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client, os.ListOpts{})
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+	// OPTIONAL
+	Name string
+	// OPTIONAL
+	Description string
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Description != "" {
+		v["display_description"] = opts.Description
+	}
+	if opts.Name != "" {
+		v["display_name"] = opts.Name
+	}
+
+	return map[string]interface{}{"volume": v}, nil
+}
+
+// Update will update the Volume with provided information. To extract the updated
+// Volume from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) UpdateResult {
+	return UpdateResult{os.Update(client, id, opts)}
+}
diff --git a/rackspace/blockstorage/v1/volumes/delegate_test.go b/rackspace/blockstorage/v1/volumes/delegate_test.go
new file mode 100644
index 0000000..2383c54
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/delegate_test.go
@@ -0,0 +1,106 @@
+package volumes
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockListResponse(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volumes: %v", err)
+			return false, err
+		}
+
+		expected := []Volume{
+			Volume{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-001",
+			},
+			Volume{
+				ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name: "vol-002",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertEquals(t, 1, count)
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockGetResponse(t)
+
+	v, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "vol-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockCreateResponse(t)
+
+	n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Size, 4)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestSizeRange(t *testing.T) {
+	_, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	_, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockDeleteResponse(t)
+
+	err := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockUpdateResponse(t)
+
+	options := &UpdateOpts{Name: "vol-002"}
+	v, err := Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "vol-002", v.Name)
+}
diff --git a/rackspace/blockstorage/v1/volumes/doc.go b/rackspace/blockstorage/v1/volumes/doc.go
new file mode 100644
index 0000000..b2be25c
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/doc.go
@@ -0,0 +1,3 @@
+// Package volumes provides information and interaction with the volume
+// API resource for the Rackspace Block Storage service.
+package volumes
diff --git a/rackspace/blockstorage/v1/volumes/results.go b/rackspace/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..c7c2cc4
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/results.go
@@ -0,0 +1,66 @@
+package volumes
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Volume wraps an Openstack volume
+type Volume os.Volume
+
+// CreateResult represents the result of a create operation
+type CreateResult struct {
+	os.CreateResult
+}
+
+// GetResult represents the result of a get operation
+type GetResult struct {
+	os.GetResult
+}
+
+// UpdateResult represents the result of an update operation
+type UpdateResult struct {
+	os.UpdateResult
+}
+
+func commonExtract(resp interface{}, err error) (*Volume, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		Volume *Volume `json:"volume"`
+	}
+
+	err = mapstructure.Decode(resp, &respStruct)
+
+	return respStruct.Volume, err
+}
+
+// Extract will get the Volume object out of the GetResult object.
+func (r GetResult) Extract() (*Volume, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Volume object out of the CreateResult object.
+func (r CreateResult) Extract() (*Volume, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Volume object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*Volume, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(page pagination.Page) ([]Volume, error) {
+	var response struct {
+		Volumes []Volume `json:"volumes"`
+	}
+
+	err := mapstructure.Decode(page.(os.ListResult).Body, &response)
+
+	return response.Volumes, err
+}
diff --git a/rackspace/blockstorage/v1/volumetypes/delegate.go b/rackspace/blockstorage/v1/volumetypes/delegate.go
new file mode 100644
index 0000000..c96b3e4
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/delegate.go
@@ -0,0 +1,18 @@
+package volumetypes
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
diff --git a/rackspace/blockstorage/v1/volumetypes/delegate_test.go b/rackspace/blockstorage/v1/volumetypes/delegate_test.go
new file mode 100644
index 0000000..6e65c90
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/delegate_test.go
@@ -0,0 +1,64 @@
+package volumetypes
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockListResponse(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumeTypes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volume types: %v", err)
+			return false, err
+		}
+
+		expected := []VolumeType{
+			VolumeType{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-type-001",
+				ExtraSpecs: map[string]interface{}{
+					"capabilities": "gpu",
+				},
+			},
+			VolumeType{
+				ID:         "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name:       "vol-type-002",
+				ExtraSpecs: map[string]interface{}{},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertEquals(t, 1, count)
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockGetResponse(t)
+
+	vt, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+	th.AssertEquals(t, vt.Name, "vol-type-001")
+	th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
diff --git a/rackspace/blockstorage/v1/volumetypes/doc.go b/rackspace/blockstorage/v1/volumetypes/doc.go
new file mode 100644
index 0000000..70122b7
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/doc.go
@@ -0,0 +1,3 @@
+// Package volumetypes provides information and interaction with the volume type
+// API resource for the Rackspace Block Storage service.
+package volumetypes
diff --git a/rackspace/blockstorage/v1/volumetypes/results.go b/rackspace/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..39c8d6f
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,37 @@
+package volumetypes
+
+import (
+	"github.com/mitchellh/mapstructure"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type VolumeType os.VolumeType
+
+type GetResult struct {
+	os.GetResult
+}
+
+// Extract will get the Volume Type struct out of the response.
+func (r GetResult) Extract() (*VolumeType, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.VolumeType, err
+}
+
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+	var response struct {
+		VolumeTypes []VolumeType `mapstructure:"volume_types"`
+	}
+
+	err := mapstructure.Decode(page.(os.ListResult).Body, &response)
+	return response.VolumeTypes, err
+}
diff --git a/rackspace/client.go b/rackspace/client.go
new file mode 100644
index 0000000..5f739a8
--- /dev/null
+++ b/rackspace/client.go
@@ -0,0 +1,156 @@
+package rackspace
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/utils"
+	tokens2 "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
+)
+
+const (
+	// RackspaceUSIdentity is an identity endpoint located in the United States.
+	RackspaceUSIdentity = "https://identity.api.rackspacecloud.com/v2.0/"
+
+	// RackspaceUKIdentity is an identity endpoint located in the UK.
+	RackspaceUKIdentity = "https://lon.identity.api.rackspacecloud.com/v2.0/"
+)
+
+const (
+	v20 = "v2.0"
+)
+
+// NewClient creates a client that's prepared to communicate with the Rackspace API, but is not
+// yet authenticated. Most users will probably prefer using the AuthenticatedClient function
+// instead.
+//
+// Provide the base URL of the identity endpoint you wish to authenticate against as "endpoint".
+// Often, this will be either RackspaceUSIdentity or RackspaceUKIdentity.
+func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
+	if endpoint == "" {
+		return os.NewClient(RackspaceUSIdentity)
+	}
+	return os.NewClient(endpoint)
+}
+
+// AuthenticatedClient logs in to Rackspace with the provided credentials and constructs a
+// ProviderClient that's ready to operate.
+//
+// If the provided AuthOptions does not specify an explicit IdentityEndpoint, it will default to
+// the canonical, production Rackspace US identity endpoint.
+func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
+	client, err := NewClient(options.IdentityEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	err = Authenticate(client, options)
+	if err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+// Authenticate or re-authenticate against the most recent identity service supported at the
+// provided endpoint.
+func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	versions := []*utils.Version{
+		&utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+	}
+
+	chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
+	if err != nil {
+		return err
+	}
+
+	switch chosen.ID {
+	case v20:
+		return v2auth(client, endpoint, options)
+	default:
+		// The switch statement must be out of date from the versions list.
+		return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
+	}
+}
+
+// AuthenticateV2 explicitly authenticates with v2 of the identity service.
+func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
+	return v2auth(client, "", options)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
+	v2Client := NewIdentityV2(client)
+	if endpoint != "" {
+		v2Client.Endpoint = endpoint
+	}
+
+	result := tokens2.Create(v2Client, tokens2.WrapOptions(options))
+
+	token, err := result.ExtractToken()
+	if err != nil {
+		return err
+	}
+
+	catalog, err := result.ExtractServiceCatalog()
+	if err != nil {
+		return err
+	}
+
+	client.TokenID = token.ID
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return os.V2EndpointURL(catalog, opts)
+	}
+
+	return nil
+}
+
+// NewIdentityV2 creates a ServiceClient that may be used to access the v2 identity service.
+func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
+	v2Endpoint := client.IdentityBase + "v2.0/"
+
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       v2Endpoint,
+	}
+}
+
+// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service.
+func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("compute")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       url,
+	}, nil
+}
+
+// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN.
+func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:object-cdn")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewObjectStorageV1 creates a ServiceClient that may be used with the Rackspace v1 object storage package.
+func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	return os.NewObjectStorageV1(client, eo)
+}
+
+// NewBlockStorageV1 creates a ServiceClient that can be used to access the
+// Rackspace Cloud Block Storage v1 API.
+func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("volume")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/rackspace/client_test.go b/rackspace/client_test.go
new file mode 100644
index 0000000..73b1c88
--- /dev/null
+++ b/rackspace/client_test.go
@@ -0,0 +1,38 @@
+package rackspace
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticatedClientV2(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `
+      {
+        "access": {
+          "token": {
+            "id": "01234567890",
+            "expires": "2014-10-01T10:00:00.000000Z"
+          },
+          "serviceCatalog": []
+        }
+      }
+    `)
+	})
+
+	options := gophercloud.AuthOptions{
+		Username:         "me",
+		APIKey:           "09876543210",
+		IdentityEndpoint: th.Endpoint() + "v2.0/",
+	}
+	client, err := AuthenticatedClient(options)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "01234567890", client.TokenID)
+}
diff --git a/rackspace/compute/v2/bootfromvolume/delegate.go b/rackspace/compute/v2/bootfromvolume/delegate.go
new file mode 100644
index 0000000..2580459
--- /dev/null
+++ b/rackspace/compute/v2/bootfromvolume/delegate.go
@@ -0,0 +1,12 @@
+package bootfromvolume
+
+import (
+	"github.com/rackspace/gophercloud"
+	osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// Create requests the creation of a server from the given block device mapping.
+func Create(client *gophercloud.ServiceClient, opts osServers.CreateOptsBuilder) osServers.CreateResult {
+	return osBFV.Create(client, opts)
+}
diff --git a/rackspace/compute/v2/bootfromvolume/delegate_test.go b/rackspace/compute/v2/bootfromvolume/delegate_test.go
new file mode 100644
index 0000000..0b53527
--- /dev/null
+++ b/rackspace/compute/v2/bootfromvolume/delegate_test.go
@@ -0,0 +1,52 @@
+package bootfromvolume
+
+import (
+	"testing"
+
+	osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := osBFV.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []osBFV.BlockDevice{
+			osBFV.BlockDevice{
+				UUID:            "123456",
+				SourceType:      osBFV.Image,
+				DestinationType: "volume",
+				VolumeSize:      10,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name": "createdserver",
+        "imageRef": "asdfasdfasdf",
+        "flavorRef": "performance1-1",
+        "block_device_mapping_v2":[
+          {
+            "uuid":"123456",
+            "source_type":"image",
+            "destination_type":"volume",
+            "boot_index": "0",
+            "delete_on_termination": "false",
+            "volume_size": "10"
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/rackspace/compute/v2/flavors/delegate.go b/rackspace/compute/v2/flavors/delegate.go
new file mode 100644
index 0000000..6bfc20c
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate.go
@@ -0,0 +1,46 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts helps control the results returned by the List() function. For example, a flavor with a
+// minDisk field of 10 will not be returned if you specify MinDisk set to 20.
+type ListOpts struct {
+
+	// MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria.
+	MinDisk int `q:"minDisk"`
+	MinRAM  int `q:"minRam"`
+
+	// Marker specifies the ID of the last flavor in the previous page.
+	Marker string `q:"marker"`
+
+	// Limit instructs List to refrain from sending excessively large lists of flavors.
+	Limit int `q:"limit"`
+}
+
+// ToFlavorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListDetail enumerates the server images available to your account.
+func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.ListDetail(client, opts)
+}
+
+// Get returns details about a single flavor, identity by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(client, id)
+}
+
+// ExtractFlavors interprets a page of List results as Flavors.
+func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) {
+	return os.ExtractFlavors(page)
+}
diff --git a/rackspace/compute/v2/flavors/delegate_test.go b/rackspace/compute/v2/flavors/delegate_test.go
new file mode 100644
index 0000000..204081d
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate_test.go
@@ -0,0 +1,62 @@
+package flavors
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, ListOutput)
+		case "performance1-2":
+			fmt.Fprintf(w, `{ "flavors": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	count := 0
+	err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractFlavors(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedFlavorSlice, actual)
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGetFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors/performance1-1", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+
+	actual, err := Get(client.ServiceClient(), "performance1-1").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &Performance1Flavor, actual)
+}
diff --git a/rackspace/compute/v2/flavors/doc.go b/rackspace/compute/v2/flavors/doc.go
new file mode 100644
index 0000000..278229a
--- /dev/null
+++ b/rackspace/compute/v2/flavors/doc.go
@@ -0,0 +1,3 @@
+// Package flavors provides information and interaction with the flavor
+// API resource for the Rackspace Cloud Servers service.
+package flavors
diff --git a/rackspace/compute/v2/flavors/fixtures.go b/rackspace/compute/v2/flavors/fixtures.go
new file mode 100644
index 0000000..b6dca93
--- /dev/null
+++ b/rackspace/compute/v2/flavors/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+package flavors
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+)
+
+// ListOutput is a sample response of a flavor List request.
+const ListOutput = `
+{
+  "flavors": [
+    {
+      "OS-FLV-EXT-DATA:ephemeral": 0,
+      "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+        "class": "performance1",
+        "disk_io_index": "40",
+        "number_of_data_disks": "0",
+        "policy_class": "performance_flavor",
+        "resize_policy_class": "performance_flavor"
+      },
+      "disk": 20,
+      "id": "performance1-1",
+      "links": [
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
+          "rel": "self"
+        },
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "1 GB Performance",
+      "ram": 1024,
+      "rxtx_factor": 200,
+      "swap": "",
+      "vcpus": 1
+    },
+    {
+      "OS-FLV-EXT-DATA:ephemeral": 20,
+      "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+        "class": "performance1",
+        "disk_io_index": "40",
+        "number_of_data_disks": "1",
+        "policy_class": "performance_flavor",
+        "resize_policy_class": "performance_flavor"
+      },
+      "disk": 40,
+      "id": "performance1-2",
+      "links": [
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2",
+          "rel": "self"
+        },
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "2 GB Performance",
+      "ram": 2048,
+      "rxtx_factor": 400,
+      "swap": "",
+      "vcpus": 2
+    }
+  ]
+}`
+
+// GetOutput is a sample response from a flavor Get request. Its contents correspond to the
+// Performance1Flavor struct.
+const GetOutput = `
+{
+  "flavor": {
+    "OS-FLV-EXT-DATA:ephemeral": 0,
+    "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+      "class": "performance1",
+      "disk_io_index": "40",
+      "number_of_data_disks": "0",
+      "policy_class": "performance_flavor",
+      "resize_policy_class": "performance_flavor"
+    },
+    "disk": 20,
+    "id": "performance1-1",
+    "links": [
+      {
+        "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
+        "rel": "self"
+      },
+      {
+        "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
+        "rel": "bookmark"
+      }
+    ],
+    "name": "1 GB Performance",
+    "ram": 1024,
+    "rxtx_factor": 200,
+    "swap": "",
+    "vcpus": 1
+  }
+}
+`
+
+// Performance1Flavor is the expected result of parsing GetOutput, or the first element of
+// ListOutput.
+var Performance1Flavor = os.Flavor{
+	ID:         "performance1-1",
+	Disk:       20,
+	RAM:        1024,
+	Name:       "1 GB Performance",
+	RxTxFactor: 200.0,
+	Swap:       0,
+	VCPUs:      1,
+}
+
+// Performance2Flavor is the second result expected from parsing ListOutput.
+var Performance2Flavor = os.Flavor{
+	ID:         "performance1-2",
+	Disk:       40,
+	RAM:        2048,
+	Name:       "2 GB Performance",
+	RxTxFactor: 400.0,
+	Swap:       0,
+	VCPUs:      2,
+}
+
+// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from
+// ListOutput.
+var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor}
diff --git a/rackspace/compute/v2/images/delegate.go b/rackspace/compute/v2/images/delegate.go
new file mode 100644
index 0000000..18e1f31
--- /dev/null
+++ b/rackspace/compute/v2/images/delegate.go
@@ -0,0 +1,22 @@
+package images
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListDetail enumerates the available server images.
+func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.ListDetail(client, opts)
+}
+
+// Get acquires additional detail about a specific image by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(client, id)
+}
+
+// ExtractImages interprets a page as a collection of server images.
+func ExtractImages(page pagination.Page) ([]os.Image, error) {
+	return os.ExtractImages(page)
+}
diff --git a/rackspace/compute/v2/images/delegate_test.go b/rackspace/compute/v2/images/delegate_test.go
new file mode 100644
index 0000000..db0a6e3
--- /dev/null
+++ b/rackspace/compute/v2/images/delegate_test.go
@@ -0,0 +1,62 @@
+package images
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListImageDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, ListOutput)
+		case "e19a734c-c7e6-443a-830c-242209c4d65d":
+			fmt.Fprintf(w, `{ "images": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	count := 0
+	err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractImages(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedImageSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGetImageDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/e19a734c-c7e6-443a-830c-242209c4d65d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+
+	actual, err := Get(client.ServiceClient(), "e19a734c-c7e6-443a-830c-242209c4d65d").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &UbuntuImage, actual)
+}
diff --git a/rackspace/compute/v2/images/doc.go b/rackspace/compute/v2/images/doc.go
new file mode 100644
index 0000000..cfae806
--- /dev/null
+++ b/rackspace/compute/v2/images/doc.go
@@ -0,0 +1,3 @@
+// Package images provides information and interaction with the image
+// API resource for the Rackspace Cloud Servers service.
+package images
diff --git a/rackspace/compute/v2/images/fixtures.go b/rackspace/compute/v2/images/fixtures.go
new file mode 100644
index 0000000..c46d196
--- /dev/null
+++ b/rackspace/compute/v2/images/fixtures.go
@@ -0,0 +1,199 @@
+// +build fixtures
+package images
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+)
+
+// ListOutput is an example response from an /images/detail request.
+const ListOutput = `
+{
+	"images": [
+		{
+			"OS-DCF:diskConfig": "MANUAL",
+			"OS-EXT-IMG-SIZE:size": 1.017415075e+09,
+			"created": "2014-10-01T15:49:02Z",
+			"id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+			"links": [
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+					"rel": "self"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+					"rel": "bookmark"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+					"rel": "alternate",
+					"type": "application/vnd.openstack.image"
+				}
+			],
+			"metadata": {
+				"auto_disk_config": "disabled",
+				"cache_in_nova": "True",
+				"com.rackspace__1__build_core": "1",
+				"com.rackspace__1__build_managed": "1",
+				"com.rackspace__1__build_rackconnect": "1",
+				"com.rackspace__1__options": "0",
+				"com.rackspace__1__platform_target": "PublicCloud",
+				"com.rackspace__1__release_build_date": "2014-10-01_15-46-08",
+				"com.rackspace__1__release_id": "100",
+				"com.rackspace__1__release_version": "10",
+				"com.rackspace__1__source": "kickstart",
+				"com.rackspace__1__visible_core": "1",
+				"com.rackspace__1__visible_managed": "0",
+				"com.rackspace__1__visible_rackconnect": "0",
+				"image_type": "base",
+				"org.openstack__1__architecture": "x64",
+				"org.openstack__1__os_distro": "org.archlinux",
+				"org.openstack__1__os_version": "2014.8",
+				"os_distro": "arch",
+				"os_type": "linux",
+				"vm_mode": "hvm"
+			},
+			"minDisk": 20,
+			"minRam": 512,
+			"name": "Arch 2014.10 (PVHVM)",
+			"progress": 100,
+			"status": "ACTIVE",
+			"updated": "2014-10-01T19:37:58Z"
+		},
+		{
+			"OS-DCF:diskConfig": "AUTO",
+			"OS-EXT-IMG-SIZE:size": 1.060306463e+09,
+			"created": "2014-10-01T12:58:11Z",
+			"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+			"links": [
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "self"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "bookmark"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "alternate",
+					"type": "application/vnd.openstack.image"
+				}
+			],
+			"metadata": {
+				"auto_disk_config": "True",
+				"cache_in_nova": "True",
+				"com.rackspace__1__build_core": "1",
+				"com.rackspace__1__build_managed": "1",
+				"com.rackspace__1__build_rackconnect": "1",
+				"com.rackspace__1__options": "0",
+				"com.rackspace__1__platform_target": "PublicCloud",
+				"com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
+				"com.rackspace__1__release_id": "1007",
+				"com.rackspace__1__release_version": "6",
+				"com.rackspace__1__source": "kickstart",
+				"com.rackspace__1__visible_core": "1",
+				"com.rackspace__1__visible_managed": "1",
+				"com.rackspace__1__visible_rackconnect": "1",
+				"image_type": "base",
+				"org.openstack__1__architecture": "x64",
+				"org.openstack__1__os_distro": "com.ubuntu",
+				"org.openstack__1__os_version": "14.04",
+				"os_distro": "ubuntu",
+				"os_type": "linux",
+				"vm_mode": "xen"
+			},
+			"minDisk": 20,
+			"minRam": 512,
+			"name": "Ubuntu 14.04 LTS (Trusty Tahr)",
+			"progress": 100,
+			"status": "ACTIVE",
+			"updated": "2014-10-01T15:51:44Z"
+		}
+	]
+}
+`
+
+// GetOutput is an example response from an /images request.
+const GetOutput = `
+{
+	"image": {
+		"OS-DCF:diskConfig": "AUTO",
+		"OS-EXT-IMG-SIZE:size": 1060306463,
+		"created": "2014-10-01T12:58:11Z",
+		"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+		"links": [
+			{
+				"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel": "self"
+			},
+			{
+				"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel": "bookmark"
+			},
+			{
+				"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel": "alternate",
+				"type": "application/vnd.openstack.image"
+			}
+		],
+		"metadata": {
+			"auto_disk_config": "True",
+			"cache_in_nova": "True",
+			"com.rackspace__1__build_core": "1",
+			"com.rackspace__1__build_managed": "1",
+			"com.rackspace__1__build_rackconnect": "1",
+			"com.rackspace__1__options": "0",
+			"com.rackspace__1__platform_target": "PublicCloud",
+			"com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
+			"com.rackspace__1__release_id": "1007",
+			"com.rackspace__1__release_version": "6",
+			"com.rackspace__1__source": "kickstart",
+			"com.rackspace__1__visible_core": "1",
+			"com.rackspace__1__visible_managed": "1",
+			"com.rackspace__1__visible_rackconnect": "1",
+			"image_type": "base",
+			"org.openstack__1__architecture": "x64",
+			"org.openstack__1__os_distro": "com.ubuntu",
+			"org.openstack__1__os_version": "14.04",
+			"os_distro": "ubuntu",
+			"os_type": "linux",
+			"vm_mode": "xen"
+		},
+		"minDisk": 20,
+		"minRam": 512,
+		"name": "Ubuntu 14.04 LTS (Trusty Tahr)",
+		"progress": 100,
+		"status": "ACTIVE",
+		"updated": "2014-10-01T15:51:44Z"
+	}
+}
+`
+
+// ArchImage is the first Image structure that should be parsed from ListOutput.
+var ArchImage = os.Image{
+	ID:       "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+	Name:     "Arch 2014.10 (PVHVM)",
+	Created:  "2014-10-01T15:49:02Z",
+	Updated:  "2014-10-01T19:37:58Z",
+	MinDisk:  20,
+	MinRAM:   512,
+	Progress: 100,
+	Status:   "ACTIVE",
+}
+
+// UbuntuImage is the second Image structure that should be parsed from ListOutput and
+// the only image that should be extracted from GetOutput.
+var UbuntuImage = os.Image{
+	ID:       "e19a734c-c7e6-443a-830c-242209c4d65d",
+	Name:     "Ubuntu 14.04 LTS (Trusty Tahr)",
+	Created:  "2014-10-01T12:58:11Z",
+	Updated:  "2014-10-01T15:51:44Z",
+	MinDisk:  20,
+	MinRAM:   512,
+	Progress: 100,
+	Status:   "ACTIVE",
+}
+
+// ExpectedImageSlice is the collection of images that should be parsed from ListOutput,
+// in order.
+var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage}
diff --git a/rackspace/compute/v2/keypairs/delegate.go b/rackspace/compute/v2/keypairs/delegate.go
new file mode 100644
index 0000000..3e53525
--- /dev/null
+++ b/rackspace/compute/v2/keypairs/delegate.go
@@ -0,0 +1,33 @@
+package keypairs
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of KeyPairs.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// Create requests the creation of a new keypair on the server, or to import a pre-existing
+// keypair.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, opts)
+}
+
+// Get returns public data about a previously uploaded KeyPair.
+func Get(client *gophercloud.ServiceClient, name string) os.GetResult {
+	return os.Get(client, name)
+}
+
+// Delete requests the deletion of a previous stored KeyPair from the server.
+func Delete(client *gophercloud.ServiceClient, name string) os.DeleteResult {
+	return os.Delete(client, name)
+}
+
+// ExtractKeyPairs interprets a page of results as a slice of KeyPairs.
+func ExtractKeyPairs(page pagination.Page) ([]os.KeyPair, error) {
+	return os.ExtractKeyPairs(page)
+}
diff --git a/rackspace/compute/v2/keypairs/delegate_test.go b/rackspace/compute/v2/keypairs/delegate_test.go
new file mode 100644
index 0000000..b72e69e
--- /dev/null
+++ b/rackspace/compute/v2/keypairs/delegate_test.go
@@ -0,0 +1,72 @@
+package keypairs
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListSuccessfully(t)
+
+	count := 0
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractKeyPairs(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, os.ExpectedKeyPairSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateSuccessfully(t)
+
+	actual, err := Create(client.ServiceClient(), os.CreateOpts{
+		Name: "createdkey",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &os.CreatedKeyPair, actual)
+}
+
+func TestImport(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleImportSuccessfully(t)
+
+	actual, err := Create(client.ServiceClient(), os.CreateOpts{
+		Name:      "importedkey",
+		PublicKey: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated by Nova",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &os.ImportedKeyPair, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "firstkey").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &os.FirstKeyPair, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "deletedkey").Extract()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/compute/v2/keypairs/doc.go b/rackspace/compute/v2/keypairs/doc.go
new file mode 100644
index 0000000..3171375
--- /dev/null
+++ b/rackspace/compute/v2/keypairs/doc.go
@@ -0,0 +1,3 @@
+// Package keypairs provides information and interaction with the keypair
+// API resource for the Rackspace Cloud Servers service.
+package keypairs
diff --git a/rackspace/compute/v2/networks/doc.go b/rackspace/compute/v2/networks/doc.go
new file mode 100644
index 0000000..8e5c773
--- /dev/null
+++ b/rackspace/compute/v2/networks/doc.go
@@ -0,0 +1,3 @@
+// Package networks provides information and interaction with the network
+// API resource for the Rackspace Cloud Servers service.
+package networks
diff --git a/rackspace/compute/v2/networks/requests.go b/rackspace/compute/v2/networks/requests.go
new file mode 100644
index 0000000..d3c973e
--- /dev/null
+++ b/rackspace/compute/v2/networks/requests.go
@@ -0,0 +1,101 @@
+package networks
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// networks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(c, listURL(c), createPage)
+}
+
+// Get retrieves a specific network based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToNetworkCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// REQUIRED. See Network object for more info.
+	CIDR string
+	// REQUIRED. See Network object for more info.
+	Label string
+}
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	n := make(map[string]interface{})
+
+	if opts.CIDR == "" {
+		return nil, errors.New("Required field CIDR not set.")
+	}
+	if opts.Label == "" {
+		return nil, errors.New("Required field Label not set.")
+	}
+
+	n["label"] = opts.Label
+	n["cidr"] = opts.CIDR
+	return map[string]interface{}{"network": n}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. This operation does not actually require a request body, i.e. the
+// CreateOpts struct argument can be empty.
+//
+// The tenant ID that is contained in the URI is the tenant that creates the
+// network. An admin user, however, has the option of specifying another tenant
+// ID in the CreateOpts struct.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToNetworkCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201, 202},
+	})
+	return res
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+	return res
+}
diff --git a/rackspace/compute/v2/networks/requests_test.go b/rackspace/compute/v2/networks/requests_test.go
new file mode 100644
index 0000000..6f44c1c
--- /dev/null
+++ b/rackspace/compute/v2/networks/requests_test.go
@@ -0,0 +1,156 @@
+package networks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/os-networksv2", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "networks": [
+        {
+            "label": "test-network-1",
+            "cidr": "192.168.100.0/24",
+            "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+        },
+        {
+            "label": "test-network-2",
+            "cidr": "192.30.250.00/18",
+            "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324"
+        }
+    ]
+}
+      `)
+	})
+
+	client := fake.ServiceClient()
+	count := 0
+
+	err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []Network{
+			Network{
+				Label: "test-network-1",
+				CIDR:  "192.168.100.0/24",
+				ID:    "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+			},
+			Network{
+				Label: "test-network-2",
+				CIDR:  "192.30.250.00/18",
+				ID:    "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+			},
+		}
+
+		th.CheckDeepEquals(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("/os-networksv2/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)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "label": "test-network-1",
+        "cidr": "192.168.100.0/24",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+      `)
+	})
+
+	n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.CIDR, "192.168.100.0/24")
+	th.AssertEquals(t, n.Label, "test-network-1")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/os-networksv2", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "network": {
+        "label": "test-network-1",
+        "cidr": "192.168.100.0/24"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "network": {
+        "label": "test-network-1",
+        "cidr": "192.168.100.0/24",
+        "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+    }
+}
+    `)
+	})
+
+	options := CreateOpts{Label: "test-network-1", CIDR: "192.168.100.0/24"}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Label, "test-network-1")
+	th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/os-networksv2/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)
+	})
+
+	res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/compute/v2/networks/results.go b/rackspace/compute/v2/networks/results.go
new file mode 100644
index 0000000..823c56e
--- /dev/null
+++ b/rackspace/compute/v2/networks/results.go
@@ -0,0 +1,79 @@
+package networks
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a network resource.
+func (r commonResult) Extract() (*Network, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *Network `json:"network"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Network, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// Network represents, well, a network.
+type Network struct {
+	// UUID for the network
+	ID string `mapstructure:"id" json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Label string `mapstructure:"label" json:"label"`
+
+	// Classless Inter-Domain Routing
+	CIDR string `mapstructure:"cidr" json:"cidr"`
+}
+
+// NetworkPage is the page returned by a pager when traversing over a
+// collection of networks.
+type NetworkPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if the NetworkPage contains no Networks.
+func (r NetworkPage) IsEmpty() (bool, error) {
+	networks, err := ExtractNetworks(r)
+	if err != nil {
+		return true, err
+	}
+	return len(networks) == 0, nil
+}
+
+// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct,
+// and extracts the elements into a slice of Network structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractNetworks(page pagination.Page) ([]Network, error) {
+	var resp struct {
+		Networks []Network `mapstructure:"networks" json:"networks"`
+	}
+
+	err := mapstructure.Decode(page.(NetworkPage).Body, &resp)
+
+	return resp.Networks, err
+}
diff --git a/rackspace/compute/v2/networks/urls.go b/rackspace/compute/v2/networks/urls.go
new file mode 100644
index 0000000..19a21aa
--- /dev/null
+++ b/rackspace/compute/v2/networks/urls.go
@@ -0,0 +1,27 @@
+package networks
+
+import "github.com/rackspace/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("os-networksv2", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("os-networksv2")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return rootURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
diff --git a/rackspace/compute/v2/networks/urls_test.go b/rackspace/compute/v2/networks/urls_test.go
new file mode 100644
index 0000000..983992e
--- /dev/null
+++ b/rackspace/compute/v2/networks/urls_test.go
@@ -0,0 +1,38 @@
+package networks
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "os-networksv2/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "os-networksv2"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "os-networksv2"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "os-networksv2/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/rackspace/compute/v2/servers/delegate.go b/rackspace/compute/v2/servers/delegate.go
new file mode 100644
index 0000000..cbf5384
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate.go
@@ -0,0 +1,61 @@
+package servers
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List makes a request against the API to list servers accessible to you.
+func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(client, opts)
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, opts)
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	return os.Delete(client, id)
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(client, id)
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult {
+	return os.ChangeAdminPassword(client, id, newPassword)
+}
+
+// Reboot requests that a given server reboot. Two methods exist for rebooting a server:
+//
+// os.HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the
+// machine, or if a VM, terminating it at the hypervisor level. It's done. Caput. Full stop. Then,
+// after a brief wait, power is restored or the VM instance restarted.
+//
+// os.SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. E.g., in
+// Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) os.ActionResult {
+	return os.Reboot(client, id, how)
+}
+
+// Rebuild will reprovision the server according to the configuration options provided in the
+// RebuildOpts struct.
+func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult {
+	return os.Rebuild(client, id, opts)
+}
+
+// WaitForStatus will continually poll a server until it successfully transitions to a specified
+// status. It will do this for at most the number of seconds specified.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return os.WaitForStatus(c, id, status, secs)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]os.Server, error) {
+	return os.ExtractServers(page)
+}
diff --git a/rackspace/compute/v2/servers/delegate_test.go b/rackspace/compute/v2/servers/delegate_test.go
new file mode 100644
index 0000000..0c331eb
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate_test.go
@@ -0,0 +1,112 @@
+package servers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListServers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, ListOutput)
+	})
+
+	count := 0
+	err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractServers(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedServerSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreateServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleServerCreationSuccessfully(t, CreateOutput)
+
+	actual, err := Create(client.ServiceClient(), os.CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, &CreatedServer, actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleServerDeletionSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, err)
+}
+
+func TestGetServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+
+	actual, err := Get(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &GophercloudServer, actual)
+}
+
+func TestChangeAdminPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleAdminPasswordChangeSuccessfully(t)
+
+	res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestReboot(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleRebootSuccessfully(t)
+
+	res := Reboot(client.ServiceClient(), "1234asdf", os.SoftReboot)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRebuildServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleRebuildSuccessfully(t, GetOutput)
+
+	opts := os.RebuildOpts{
+		Name:       "new-name",
+		AdminPass:  "swordfish",
+		ImageID:    "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+		AccessIPv4: "1.2.3.4",
+	}
+	actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &GophercloudServer, actual)
+}
diff --git a/rackspace/compute/v2/servers/doc.go b/rackspace/compute/v2/servers/doc.go
new file mode 100644
index 0000000..c9f77f6
--- /dev/null
+++ b/rackspace/compute/v2/servers/doc.go
@@ -0,0 +1,3 @@
+// Package servers provides information and interaction with the server
+// API resource for the Rackspace Cloud Servers service.
+package servers
diff --git a/rackspace/compute/v2/servers/fixtures.go b/rackspace/compute/v2/servers/fixtures.go
new file mode 100644
index 0000000..b22a289
--- /dev/null
+++ b/rackspace/compute/v2/servers/fixtures.go
@@ -0,0 +1,439 @@
+// +build fixtures
+
+package servers
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// ListOutput is the recorded output of a Rackspace servers.List request.
+const ListOutput = `
+{
+	"servers": [
+		{
+			"OS-DCF:diskConfig": "MANUAL",
+			"OS-EXT-STS:power_state": 1,
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"accessIPv4": "1.2.3.4",
+			"accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+			"addresses": {
+				"private": [
+					{
+						"addr": "10.208.230.113",
+						"version": 4
+					}
+				],
+				"public": [
+					{
+						"addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0",
+						"version": 6
+					},
+					{
+						"addr": "104.130.131.164",
+						"version": 4
+					}
+				]
+			},
+			"created": "2014-09-23T12:34:58Z",
+			"flavor": {
+				"id": "performance1-8",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
+			"id": "59818cee-bc8c-44eb-8073-673ee65105f7",
+			"image": {
+				"id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"key_name": "mykey",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+					"rel": "self"
+				},
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+					"rel": "bookmark"
+				}
+			],
+			"metadata": {},
+			"name": "devstack",
+			"progress": 100,
+			"status": "ACTIVE",
+			"tenant_id": "111111",
+			"updated": "2014-09-23T12:38:19Z",
+			"user_id": "14ae7bb21d81422694655f3cc30f2930"
+		},
+		{
+			"OS-DCF:diskConfig": "MANUAL",
+			"OS-EXT-STS:power_state": 1,
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"accessIPv4": "1.1.2.3",
+			"accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+			"addresses": {
+				"private": [
+					{
+						"addr": "10.10.20.30",
+						"version": 4
+					}
+				],
+				"public": [
+					{
+						"addr": "1.1.2.3",
+						"version": 4
+					},
+					{
+						"addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+						"version": 6
+					}
+				]
+			},
+			"created": "2014-07-21T19:32:55Z",
+			"flavor": {
+				"id": "performance1-2",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
+			"id": "25f1c7f5-e00a-4715-b354-16e24b2f4630",
+			"image": {
+				"id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"key_name": "otherkey",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+					"rel": "self"
+				},
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+					"rel": "bookmark"
+				}
+			],
+			"metadata": {},
+			"name": "peril-dfw",
+			"progress": 100,
+			"status": "ACTIVE",
+			"tenant_id": "111111",
+			"updated": "2014-07-21T19:34:24Z",
+			"user_id": "14ae7bb21d81422694655f3cc30f2930"
+		}
+	]
+}
+`
+
+// GetOutput is the recorded output of a Rackspace servers.Get request.
+const GetOutput = `
+{
+	"server": {
+		"OS-DCF:diskConfig": "AUTO",
+		"OS-EXT-STS:power_state": 1,
+		"OS-EXT-STS:task_state": null,
+		"OS-EXT-STS:vm_state": "active",
+		"accessIPv4": "1.2.4.8",
+		"accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777",
+		"addresses": {
+			"private": [
+				{
+					"addr": "10.20.40.80",
+					"version": 4
+				}
+			],
+			"public": [
+				{
+					"addr": "1.2.4.8",
+					"version": 4
+				},
+				{
+					"addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
+					"version": 6
+				}
+			]
+		},
+		"created": "2014-10-21T14:42:16Z",
+		"flavor": {
+			"id": "performance1-1",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
+		"id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+		"image": {
+			"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"key_name": null,
+		"links": [
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+				"rel": "self"
+			},
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+				"rel": "bookmark"
+			}
+		],
+		"metadata": {},
+		"name": "Gophercloud-pxpGGuey",
+		"progress": 100,
+		"status": "ACTIVE",
+		"tenant_id": "111111",
+		"updated": "2014-10-21T14:42:57Z",
+		"user_id": "14ae7bb21d81423694655f4dd30f2930"
+	}
+}
+`
+
+// CreateOutput contains a sample of Rackspace's response to a Create call.
+const CreateOutput = `
+{
+	"server": {
+		"OS-DCF:diskConfig": "AUTO",
+		"adminPass": "v7tADqbE5pr9",
+		"id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+		"links": [
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+				"rel": "self"
+			},
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+				"rel": "bookmark"
+			}
+		]
+	}
+}
+`
+
+// DevstackServer is the expected first result from parsing ListOutput.
+var DevstackServer = os.Server{
+	ID:         "59818cee-bc8c-44eb-8073-673ee65105f7",
+	Name:       "devstack",
+	TenantID:   "111111",
+	UserID:     "14ae7bb21d81422694655f3cc30f2930",
+	HostID:     "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
+	Updated:    "2014-09-23T12:38:19Z",
+	Created:    "2014-09-23T12:34:58Z",
+	AccessIPv4: "1.2.3.4",
+	AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+	Progress:   100,
+	Status:     "ACTIVE",
+	Image: map[string]interface{}{
+		"id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Flavor: map[string]interface{}{
+		"id": "performance1-8",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Addresses: map[string]interface{}{
+		"private": []interface{}{
+			map[string]interface{}{
+				"addr":    "10.20.30.40",
+				"version": float64(4.0),
+			},
+		},
+		"public": []interface{}{
+			map[string]interface{}{
+				"addr":    "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+				"version": float64(6.0),
+			},
+			map[string]interface{}{
+				"addr":    "1.2.3.4",
+				"version": float64(4.0),
+			},
+		},
+	},
+	Metadata: map[string]interface{}{},
+	Links: []interface{}{
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7",
+			"rel":  "self",
+		},
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+			"rel":  "bookmark",
+		},
+	},
+	KeyName:   "mykey",
+	AdminPass: "",
+}
+
+// PerilServer is the expected second result from parsing ListOutput.
+var PerilServer = os.Server{
+	ID:         "25f1c7f5-e00a-4715-b354-16e24b2f4630",
+	Name:       "peril-dfw",
+	TenantID:   "111111",
+	UserID:     "14ae7bb21d81422694655f3cc30f2930",
+	HostID:     "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
+	Updated:    "2014-07-21T19:34:24Z",
+	Created:    "2014-07-21T19:32:55Z",
+	AccessIPv4: "1.1.2.3",
+	AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+	Progress:   100,
+	Status:     "ACTIVE",
+	Image: map[string]interface{}{
+		"id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Flavor: map[string]interface{}{
+		"id": "performance1-2",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Addresses: map[string]interface{}{
+		"private": []interface{}{
+			map[string]interface{}{
+				"addr":    "10.10.20.30",
+				"version": float64(4.0),
+			},
+		},
+		"public": []interface{}{
+			map[string]interface{}{
+				"addr":    "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+				"version": float64(6.0),
+			},
+			map[string]interface{}{
+				"addr":    "1.1.2.3",
+				"version": float64(4.0),
+			},
+		},
+	},
+	Metadata: map[string]interface{}{},
+	Links: []interface{}{
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+			"rel":  "self",
+		},
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+			"rel":  "bookmark",
+		},
+	},
+	KeyName:   "otherkey",
+	AdminPass: "",
+}
+
+// GophercloudServer is the expected result from parsing GetOutput.
+var GophercloudServer = os.Server{
+	ID:         "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+	Name:       "Gophercloud-pxpGGuey",
+	TenantID:   "111111",
+	UserID:     "14ae7bb21d81423694655f4dd30f2930",
+	HostID:     "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
+	Updated:    "2014-10-21T14:42:57Z",
+	Created:    "2014-10-21T14:42:16Z",
+	AccessIPv4: "1.2.4.8",
+	AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777",
+	Progress:   100,
+	Status:     "ACTIVE",
+	Image: map[string]interface{}{
+		"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Flavor: map[string]interface{}{
+		"id": "performance1-1",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Addresses: map[string]interface{}{
+		"private": []interface{}{
+			map[string]interface{}{
+				"addr":    "10.20.40.80",
+				"version": float64(4.0),
+			},
+		},
+		"public": []interface{}{
+			map[string]interface{}{
+				"addr":    "2001:4800:6666:105:2a0f:c056:f594:7777",
+				"version": float64(6.0),
+			},
+			map[string]interface{}{
+				"addr":    "1.2.4.8",
+				"version": float64(4.0),
+			},
+		},
+	},
+	Metadata: map[string]interface{}{},
+	Links: []interface{}{
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+			"rel":  "self",
+		},
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+			"rel":  "bookmark",
+		},
+	},
+	KeyName:   "",
+	AdminPass: "",
+}
+
+// CreatedServer is the partial Server struct that can be parsed from CreateOutput.
+var CreatedServer = os.Server{
+	ID:        "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+	AdminPass: "v7tADqbE5pr9",
+	Links:     []interface{}{},
+}
+
+// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput.
+var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer}
diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go
new file mode 100644
index 0000000..884b9cb
--- /dev/null
+++ b/rackspace/compute/v2/servers/requests.go
@@ -0,0 +1,158 @@
+package servers
+
+import (
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// CreateOpts specifies all of the options that Rackspace accepts in its Create request, including
+// the union of all extensions that Rackspace supports.
+type CreateOpts struct {
+	// Name [required] is the name to assign to the newly launched server.
+	Name string
+
+	// ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+	// Optional if using the boot-from-volume extension.
+	ImageRef string
+
+	// FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+	FlavorRef string
+
+	// SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+	SecurityGroups []string
+
+	// UserData [optional] contains configuration information or scripts to use upon launch.
+	// Create will base64-encode it for you.
+	UserData []byte
+
+	// AvailabilityZone [optional] in which to launch the server.
+	AvailabilityZone string
+
+	// Networks [optional] dictates how this server will be attached to available networks.
+	// By default, the server will be attached to all isolated networks for the tenant.
+	Networks []os.Network
+
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string
+
+	// Personality [optional] includes the path and contents of a file to inject into the server at launch.
+	// The maximum size of the file is 255 bytes (decoded).
+	Personality []byte
+
+	// ConfigDrive [optional] enables metadata injection through a configuration drive.
+	ConfigDrive bool
+
+	// Rackspace-specific extensions begin here.
+
+	// KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched
+	// server. See the "keypairs" extension in OpenStack compute v2.
+	KeyPair string
+
+	// DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig"
+	// extension in OpenStack compute v2.
+	DiskConfig diskconfig.DiskConfig
+
+	// BlockDevice [optional] will create the server from a volume, which is created from an image,
+	// a snapshot, or an another volume.
+	BlockDevice []bootfromvolume.BlockDevice
+}
+
+// ToServerCreateMap constructs a request body using all of the OpenStack extensions that are
+// active on Rackspace.
+func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
+	base := os.CreateOpts{
+		Name:             opts.Name,
+		ImageRef:         opts.ImageRef,
+		FlavorRef:        opts.FlavorRef,
+		SecurityGroups:   opts.SecurityGroups,
+		UserData:         opts.UserData,
+		AvailabilityZone: opts.AvailabilityZone,
+		Networks:         opts.Networks,
+		Metadata:         opts.Metadata,
+		Personality:      opts.Personality,
+		ConfigDrive:      opts.ConfigDrive,
+	}
+
+	drive := diskconfig.CreateOptsExt{
+		CreateOptsBuilder: base,
+		DiskConfig:        opts.DiskConfig,
+	}
+
+	res, err := drive.ToServerCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(opts.BlockDevice) != 0 {
+		bfv := bootfromvolume.CreateOptsExt{
+			CreateOptsBuilder: drive,
+			BlockDevice:       opts.BlockDevice,
+		}
+
+		res, err = bfv.ToServerCreateMap()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// key_name doesn't actually come from the extension (or at least isn't documented there) so
+	// we need to add it manually.
+	serverMap := res["server"].(map[string]interface{})
+	serverMap["key_name"] = opts.KeyPair
+
+	return res, nil
+}
+
+// RebuildOpts represents all of the configuration options used in a server rebuild operation that
+// are supported by Rackspace.
+type RebuildOpts struct {
+	// Required. The ID of the image you want your server to be provisioned on
+	ImageID string
+
+	// Name to set the server to
+	Name string
+
+	// Required. The server's admin password
+	AdminPass string
+
+	// AccessIPv4 [optional] provides a new IPv4 address for the instance.
+	AccessIPv4 string
+
+	// AccessIPv6 [optional] provides a new IPv6 address for the instance.
+	AccessIPv6 string
+
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string
+
+	// Personality [optional] includes the path and contents of a file to inject into the server at launch.
+	// The maximum size of the file is 255 bytes (decoded).
+	Personality []byte
+
+	// Rackspace-specific stuff begins here.
+
+	// DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig"
+	// extension in OpenStack compute v2.
+	DiskConfig diskconfig.DiskConfig
+}
+
+// ToServerRebuildMap constructs a request body using all of the OpenStack extensions that are
+// active on Rackspace.
+func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
+	base := os.RebuildOpts{
+		ImageID:     opts.ImageID,
+		Name:        opts.Name,
+		AdminPass:   opts.AdminPass,
+		AccessIPv4:  opts.AccessIPv4,
+		AccessIPv6:  opts.AccessIPv6,
+		Metadata:    opts.Metadata,
+		Personality: opts.Personality,
+	}
+
+	drive := diskconfig.RebuildOptsExt{
+		RebuildOptsBuilder: base,
+		DiskConfig:         opts.DiskConfig,
+	}
+
+	return drive.ToServerRebuildMap()
+}
diff --git a/rackspace/compute/v2/servers/requests_test.go b/rackspace/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..3c0f806
--- /dev/null
+++ b/rackspace/compute/v2/servers/requests_test.go
@@ -0,0 +1,57 @@
+package servers
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+	opts := CreateOpts{
+		Name:       "createdserver",
+		ImageRef:   "image-id",
+		FlavorRef:  "flavor-id",
+		KeyPair:    "mykey",
+		DiskConfig: diskconfig.Manual,
+	}
+
+	expected := `
+	{
+		"server": {
+			"name": "createdserver",
+			"imageRef": "image-id",
+			"flavorRef": "flavor-id",
+			"key_name": "mykey",
+			"OS-DCF:diskConfig": "MANUAL"
+		}
+	}
+	`
+	actual, err := opts.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestRebuildOpts(t *testing.T) {
+	opts := RebuildOpts{
+		Name:       "rebuiltserver",
+		AdminPass:  "swordfish",
+		ImageID:    "asdfasdfasdf",
+		DiskConfig: diskconfig.Auto,
+	}
+
+	actual, err := opts.ToServerRebuildMap()
+	th.AssertNoErr(t, err)
+
+	expected := `
+	{
+		"rebuild": {
+			"name": "rebuiltserver",
+			"imageRef": "asdfasdfasdf",
+			"adminPass": "swordfish",
+			"OS-DCF:diskConfig": "AUTO"
+		}
+	}
+	`
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/rackspace/compute/v2/virtualinterfaces/requests.go b/rackspace/compute/v2/virtualinterfaces/requests.go
new file mode 100644
index 0000000..bfe3487
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/requests.go
@@ -0,0 +1,51 @@
+package virtualinterfaces
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// networks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return VirtualInterfacePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(c, listURL(c, instanceID), createPage)
+}
+
+// Create creates a new virtual interface for a network and attaches the network
+// to the server instance.
+func Create(c *gophercloud.ServiceClient, instanceID, networkID string) CreateResult {
+	var res CreateResult
+
+	reqBody := map[string]map[string]string{
+		"virtual_interface": {
+			"network_id": networkID,
+		},
+	}
+
+	// Send request to API
+	_, res.Err = perigee.Request("POST", createURL(c, instanceID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201, 202},
+	})
+	return res
+}
+
+// Delete deletes the interface with interfaceID attached to the instance with
+// instanceID.
+func Delete(c *gophercloud.ServiceClient, instanceID, interfaceID string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", deleteURL(c, instanceID, interfaceID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 204},
+	})
+	return res
+}
diff --git a/rackspace/compute/v2/virtualinterfaces/requests_test.go b/rackspace/compute/v2/virtualinterfaces/requests_test.go
new file mode 100644
index 0000000..d40af9c
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/requests_test.go
@@ -0,0 +1,165 @@
+package virtualinterfaces
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "virtual_interfaces": [
+        {
+            "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775",
+            "ip_addresses": [
+                {
+                    "address": "192.168.0.2",
+                    "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f",
+                    "network_label": "superprivate_xml"
+                }
+            ],
+            "mac_address": "BC:76:4E:04:85:20"
+        },
+        {
+            "id": "e14e789d-3b98-44a6-9c2d-c23eb1d1465c",
+            "ip_addresses": [
+                {
+                    "address": "10.181.1.30",
+                    "network_id": "3b324a1b-31b8-4db5-9fe5-4a2067f60297",
+                    "network_label": "private"
+                }
+            ],
+            "mac_address": "BC:76:4E:04:81:55"
+        }
+    ]
+}
+      `)
+	})
+
+	client := fake.ServiceClient()
+	count := 0
+
+	err := List(client, "12345").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVirtualInterfaces(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []VirtualInterface{
+			VirtualInterface{
+				MACAddress: "BC:76:4E:04:85:20",
+				IPAddresses: []IPAddress{
+					IPAddress{
+						Address:      "192.168.0.2",
+						NetworkID:    "f212726e-6321-4210-9bae-a13f5a33f83f",
+						NetworkLabel: "superprivate_xml",
+					},
+				},
+				ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775",
+			},
+			VirtualInterface{
+				MACAddress: "BC:76:4E:04:81:55",
+				IPAddresses: []IPAddress{
+					IPAddress{
+						Address:      "10.181.1.30",
+						NetworkID:    "3b324a1b-31b8-4db5-9fe5-4a2067f60297",
+						NetworkLabel: "private",
+					},
+				},
+				ID: "e14e789d-3b98-44a6-9c2d-c23eb1d1465c",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "virtual_interface": {
+        "network_id": "6789"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `{
+      "virtual_interfaces": [
+        {
+          "id": "de7c6d53-b895-4b4a-963c-517ccb0f0775",
+          "ip_addresses": [
+            {
+              "address": "192.168.0.2",
+              "network_id": "f212726e-6321-4210-9bae-a13f5a33f83f",
+              "network_label": "superprivate_xml"
+            }
+          ],
+          "mac_address": "BC:76:4E:04:85:20"
+        }
+      ]
+    }`)
+	})
+
+	expected := &VirtualInterface{
+		MACAddress: "BC:76:4E:04:85:20",
+		IPAddresses: []IPAddress{
+			IPAddress{
+				Address:      "192.168.0.2",
+				NetworkID:    "f212726e-6321-4210-9bae-a13f5a33f83f",
+				NetworkLabel: "superprivate_xml",
+			},
+		},
+		ID: "de7c6d53-b895-4b4a-963c-517ccb0f0775",
+	}
+
+	actual, err := Create(fake.ServiceClient(), "12345", "6789").Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/12345/os-virtual-interfacesv2/6789", 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)
+	})
+
+	res := Delete(fake.ServiceClient(), "12345", "6789")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/compute/v2/virtualinterfaces/results.go b/rackspace/compute/v2/virtualinterfaces/results.go
new file mode 100644
index 0000000..6818fcb
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/results.go
@@ -0,0 +1,79 @@
+package virtualinterfaces
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a network resource.
+func (r commonResult) Extract() (*VirtualInterface, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res.VirtualInterfaces[0], err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// IPAddress represents a vitual address attached to a VirtualInterface.
+type IPAddress struct {
+	Address      string `mapstructure:"address" json:"address"`
+	NetworkID    string `mapstructure:"network_id" json:"network_id"`
+	NetworkLabel string `mapstructure:"network_label" json:"network_label"`
+}
+
+// VirtualInterface represents a virtual interface.
+type VirtualInterface struct {
+	// UUID for the virtual interface
+	ID string `mapstructure:"id" json:"id"`
+
+	MACAddress string `mapstructure:"mac_address" json:"mac_address"`
+
+	IPAddresses []IPAddress `mapstructure:"ip_addresses" json:"ip_addresses"`
+}
+
+// VirtualInterfacePage is the page returned by a pager when traversing over a
+// collection of virtual interfaces.
+type VirtualInterfacePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if the NetworkPage contains no Networks.
+func (r VirtualInterfacePage) IsEmpty() (bool, error) {
+	networks, err := ExtractVirtualInterfaces(r)
+	if err != nil {
+		return true, err
+	}
+	return len(networks) == 0, nil
+}
+
+// ExtractVirtualInterfaces accepts a Page struct, specifically a VirtualInterfacePage struct,
+// and extracts the elements into a slice of VirtualInterface structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractVirtualInterfaces(page pagination.Page) ([]VirtualInterface, error) {
+	var resp struct {
+		VirtualInterfaces []VirtualInterface `mapstructure:"virtual_interfaces" json:"virtual_interfaces"`
+	}
+
+	err := mapstructure.Decode(page.(VirtualInterfacePage).Body, &resp)
+
+	return resp.VirtualInterfaces, err
+}
diff --git a/rackspace/compute/v2/virtualinterfaces/urls.go b/rackspace/compute/v2/virtualinterfaces/urls.go
new file mode 100644
index 0000000..9e5693e
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/urls.go
@@ -0,0 +1,15 @@
+package virtualinterfaces
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient, instanceID string) string {
+	return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2")
+}
+
+func createURL(c *gophercloud.ServiceClient, instanceID string) string {
+	return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, instanceID, interfaceID string) string {
+	return c.ServiceURL("servers", instanceID, "os-virtual-interfacesv2", interfaceID)
+}
diff --git a/rackspace/compute/v2/virtualinterfaces/urls_test.go b/rackspace/compute/v2/virtualinterfaces/urls_test.go
new file mode 100644
index 0000000..6732e4e
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/urls_test.go
@@ -0,0 +1,32 @@
+package virtualinterfaces
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient(), "12345")
+	expected := endpoint + "servers/12345/os-virtual-interfacesv2"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := createURL(endpointClient(), "12345")
+	expected := endpoint + "servers/12345/os-virtual-interfacesv2"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "12345", "6789")
+	expected := endpoint + "servers/12345/os-virtual-interfacesv2/6789"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/rackspace/identity/v2/extensions/delegate.go b/rackspace/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..fc547cd
--- /dev/null
+++ b/rackspace/identity/v2/extensions/delegate.go
@@ -0,0 +1,24 @@
+package extensions
+
+import (
+	"github.com/rackspace/gophercloud"
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of os.Extension structs.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+	return common.ExtractExtensions(page)
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+	return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return common.List(c)
+}
diff --git a/rackspace/identity/v2/extensions/delegate_test.go b/rackspace/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..e30f794
--- /dev/null
+++ b/rackspace/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,39 @@
+package extensions
+
+import (
+	"testing"
+
+	common "github.com/rackspace/gophercloud/openstack/common/extensions"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	common.HandleListExtensionsSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractExtensions(page)
+		th.AssertNoErr(t, err)
+		th.AssertDeepEquals(t, common.ExpectedExtensions, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	common.HandleGetExtensionSuccessfully(t)
+
+	actual, err := Get(fake.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/rackspace/identity/v2/extensions/doc.go b/rackspace/identity/v2/extensions/doc.go
new file mode 100644
index 0000000..b02a95b
--- /dev/null
+++ b/rackspace/identity/v2/extensions/doc.go
@@ -0,0 +1,3 @@
+// Package extensions provides information and interaction with the all the
+// extensions available for the Rackspace Identity service.
+package extensions
diff --git a/rackspace/identity/v2/tenants/delegate.go b/rackspace/identity/v2/tenants/delegate.go
new file mode 100644
index 0000000..6cdd0cf
--- /dev/null
+++ b/rackspace/identity/v2/tenants/delegate.go
@@ -0,0 +1,17 @@
+package tenants
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractTenants interprets a page of List results as a more usable slice of Tenant structs.
+func ExtractTenants(page pagination.Page) ([]os.Tenant, error) {
+	return os.ExtractTenants(page)
+}
+
+// List enumerates the tenants to which the current token grants access.
+func List(client *gophercloud.ServiceClient, opts *os.ListOpts) pagination.Pager {
+	return os.List(client, opts)
+}
diff --git a/rackspace/identity/v2/tenants/delegate_test.go b/rackspace/identity/v2/tenants/delegate_test.go
new file mode 100644
index 0000000..eccbfe2
--- /dev/null
+++ b/rackspace/identity/v2/tenants/delegate_test.go
@@ -0,0 +1,28 @@
+package tenants
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListTenantsSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractTenants(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, os.ExpectedTenantSlice, actual)
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
diff --git a/rackspace/identity/v2/tenants/doc.go b/rackspace/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..c1825c2
--- /dev/null
+++ b/rackspace/identity/v2/tenants/doc.go
@@ -0,0 +1,3 @@
+// Package tenants provides information and interaction with the tenant
+// API resource for the Rackspace Identity service.
+package tenants
diff --git a/rackspace/identity/v2/tokens/delegate.go b/rackspace/identity/v2/tokens/delegate.go
new file mode 100644
index 0000000..4f9885a
--- /dev/null
+++ b/rackspace/identity/v2/tokens/delegate.go
@@ -0,0 +1,60 @@
+package tokens
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+)
+
+var (
+	// ErrPasswordProvided is returned if both a password and an API key are provided to Create.
+	ErrPasswordProvided = errors.New("Please provide either a password or an API key.")
+)
+
+// AuthOptions wraps the OpenStack AuthOptions struct to be able to customize the request body
+// when API key authentication is used.
+type AuthOptions struct {
+	os.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+	return AuthOptions{AuthOptions: os.WrapOptions(original)}
+}
+
+// ToTokenCreateMap serializes an AuthOptions into a request body. If an API key is provided, it
+// will be used, otherwise
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+	if auth.APIKey == "" {
+		return auth.AuthOptions.ToTokenCreateMap()
+	}
+
+	// Verify that other required attributes are present.
+	if auth.Username == "" {
+		return nil, os.ErrUsernameRequired
+	}
+
+	authMap := make(map[string]interface{})
+
+	authMap["RAX-KSKEY:apiKeyCredentials"] = map[string]interface{}{
+		"username": auth.Username,
+		"apiKey":   auth.APIKey,
+	}
+
+	if auth.TenantID != "" {
+		authMap["tenantId"] = auth.TenantID
+	}
+	if auth.TenantName != "" {
+		authMap["tenantName"] = auth.TenantName
+	}
+
+	return map[string]interface{}{"auth": authMap}, nil
+}
+
+// Create authenticates to Rackspace's identity service and attempts to acquire a Token. Rather
+// than interact with this service directly, users should generally call
+// rackspace.AuthenticatedClient().
+func Create(client *gophercloud.ServiceClient, auth AuthOptions) os.CreateResult {
+	return os.Create(client, auth)
+}
diff --git a/rackspace/identity/v2/tokens/delegate_test.go b/rackspace/identity/v2/tokens/delegate_test.go
new file mode 100644
index 0000000..6678ff4
--- /dev/null
+++ b/rackspace/identity/v2/tokens/delegate_test.go
@@ -0,0 +1,36 @@
+package tokens
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) os.CreateResult {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleTokenPost(t, requestJSON)
+
+	return Create(client.ServiceClient(), WrapOptions(options))
+}
+
+func TestCreateTokenWithAPIKey(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+		APIKey:   "1234567890abcdef",
+	}
+
+	os.IsSuccessful(t, tokenPost(t, options, `
+    {
+      "auth": {
+        "RAX-KSKEY:apiKeyCredentials": {
+          "username": "me",
+          "apiKey": "1234567890abcdef"
+        }
+      }
+    }
+  `))
+}
diff --git a/rackspace/identity/v2/tokens/doc.go b/rackspace/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..44043e5
--- /dev/null
+++ b/rackspace/identity/v2/tokens/doc.go
@@ -0,0 +1,3 @@
+// Package tokens provides information and interaction with the token
+// API resource for the Rackspace Identity service.
+package tokens
diff --git a/rackspace/objectstorage/v1/accounts/delegate.go b/rackspace/objectstorage/v1/accounts/delegate.go
new file mode 100644
index 0000000..ae3de26
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/delegate.go
@@ -0,0 +1,38 @@
+package accounts
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+)
+
+// Get is a function that retrieves an account's metadata. To extract just the
+// custom metadata, call the ExtractMetadata method on the GetResult. To extract
+// all the headers that are returned (including the metadata), call the
+// ExtractHeaders method on the GetResult.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+	return os.Get(c, nil)
+}
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or
+// deleting an account's metadata.
+type UpdateOpts struct {
+	Metadata    map[string]string
+	TempURLKey  string `h:"X-Account-Meta-Temp-URL-Key"`
+	TempURLKey2 string `h:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// ToAccountUpdateMap formats an UpdateOpts into a map[string]string of headers.
+func (opts UpdateOpts) ToAccountUpdateMap() (map[string]string, error) {
+	headers, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		headers["X-Account-Meta-"+k] = v
+	}
+	return headers, err
+}
+
+func Update(c *gophercloud.ServiceClient, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, opts)
+}
diff --git a/rackspace/objectstorage/v1/accounts/delegate_test.go b/rackspace/objectstorage/v1/accounts/delegate_test.go
new file mode 100644
index 0000000..c568bd6
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/delegate_test.go
@@ -0,0 +1,30 @@
+package accounts
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetAccounts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetAccountSuccessfully(t)
+
+	options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+	res := Update(fake.ServiceClient(), options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestUpdateAccounts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateAccountSuccessfully(t)
+
+	expected := map[string]string{"Foo": "bar"}
+	actual, err := Get(fake.ServiceClient()).ExtractMetadata()
+	th.CheckNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/accounts/doc.go b/rackspace/objectstorage/v1/accounts/doc.go
new file mode 100644
index 0000000..293a930
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/doc.go
@@ -0,0 +1,3 @@
+// Package accounts provides information and interaction with the account
+// API resource for the Rackspace Cloud Files service.
+package accounts
diff --git a/rackspace/objectstorage/v1/bulk/doc.go b/rackspace/objectstorage/v1/bulk/doc.go
new file mode 100644
index 0000000..9c89e22
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/doc.go
@@ -0,0 +1,3 @@
+// Package bulk provides functionality for working with bulk operations in the
+// Rackspace Cloud Files service.
+package bulk
diff --git a/rackspace/objectstorage/v1/bulk/requests.go b/rackspace/objectstorage/v1/bulk/requests.go
new file mode 100644
index 0000000..d252609
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/requests.go
@@ -0,0 +1,51 @@
+package bulk
+
+import (
+	"net/url"
+	"strings"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+	ToBulkDeleteBody() (string, error)
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts []string
+
+// ToBulkDeleteBody formats a DeleteOpts into a request body.
+func (opts DeleteOpts) ToBulkDeleteBody() (string, error) {
+	return url.QueryEscape(strings.Join(opts, "\n")), nil
+}
+
+// Delete will delete objects or containers in bulk.
+func Delete(c *gophercloud.ServiceClient, opts DeleteOptsBuilder) DeleteResult {
+	var res DeleteResult
+
+	if opts == nil {
+		return res
+	}
+
+	reqString, err := opts.ToBulkDeleteBody()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	reqBody := strings.NewReader(reqString)
+
+	resp, err := perigee.Request("DELETE", deleteURL(c), perigee.Options{
+		ContentType: "text/plain",
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		ReqBody:     reqBody,
+		Results:     &res.Body,
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/rackspace/objectstorage/v1/bulk/requests_test.go b/rackspace/objectstorage/v1/bulk/requests_test.go
new file mode 100644
index 0000000..8b5578e
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/requests_test.go
@@ -0,0 +1,36 @@
+package bulk
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestBulkDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.AssertEquals(t, r.URL.RawQuery, "bulk-delete")
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+      {
+        "Number Not Found": 1,
+        "Response Status": "200 OK",
+        "Errors": [],
+        "Number Deleted": 1,
+        "Response Body": ""
+      }
+    `)
+	})
+
+	options := DeleteOpts{"gophercloud-testcontainer1", "gophercloud-testcontainer2"}
+	actual, err := Delete(fake.ServiceClient(), options).ExtractBody()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, actual.NumberDeleted, 1)
+}
diff --git a/rackspace/objectstorage/v1/bulk/results.go b/rackspace/objectstorage/v1/bulk/results.go
new file mode 100644
index 0000000..fddc125
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/results.go
@@ -0,0 +1,28 @@
+package bulk
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// DeleteResult represents the result of a bulk delete operation.
+type DeleteResult struct {
+	gophercloud.Result
+}
+
+// DeleteRespBody is the form of the response body returned by a bulk delete request.
+type DeleteRespBody struct {
+	NumberNotFound int      `mapstructure:"Number Not Found"`
+	ResponseStatus string   `mapstructure:"Response Status"`
+	Errors         []string `mapstructure:"Errors"`
+	NumberDeleted  int      `mapstructure:"Number Deleted"`
+	ResponseBody   string   `mapstructure:"Response Body"`
+}
+
+// ExtractBody will extract the body returned by the bulk extract request.
+func (dr DeleteResult) ExtractBody() (DeleteRespBody, error) {
+	var resp DeleteRespBody
+	err := mapstructure.Decode(dr.Body, &resp)
+	return resp, err
+}
diff --git a/rackspace/objectstorage/v1/bulk/urls.go b/rackspace/objectstorage/v1/bulk/urls.go
new file mode 100644
index 0000000..2e11203
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/urls.go
@@ -0,0 +1,11 @@
+package bulk
+
+import "github.com/rackspace/gophercloud"
+
+func deleteURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint + "?bulk-delete"
+}
+
+func extractURL(c *gophercloud.ServiceClient, ext string) string {
+	return c.Endpoint + "?extract-archive=" + ext
+}
diff --git a/rackspace/objectstorage/v1/bulk/urls_test.go b/rackspace/objectstorage/v1/bulk/urls_test.go
new file mode 100644
index 0000000..9169e52
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/urls_test.go
@@ -0,0 +1,26 @@
+package bulk
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient())
+	expected := endpoint + "?bulk-delete"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestExtractURL(t *testing.T) {
+	actual := extractURL(endpointClient(), "tar")
+	expected := endpoint + "?extract-archive=tar"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/delegate.go b/rackspace/objectstorage/v1/cdncontainers/delegate.go
new file mode 100644
index 0000000..d7eef20
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/delegate.go
@@ -0,0 +1,71 @@
+package cdncontainers
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractNames interprets a page of List results when just the container
+// names are requested.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	return os.ExtractNames(page)
+}
+
+// ListOpts are options for listing Rackspace CDN containers.
+type ListOpts struct {
+	EndMarker string `q:"end_marker"`
+	Format    string `q:"format"`
+	Limit     int    `q:"limit"`
+	Marker    string `q:"marker"`
+}
+
+// ToContainerListParams formats a ListOpts into a query string and boolean
+// representing whether to list complete information for each container.
+func (opts ListOpts) ToContainerListParams() (bool, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return false, "", err
+	}
+	return false, q.String(), nil
+}
+
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
+func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult {
+	return os.Get(c, containerName)
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or
+// deleting a container's metadata.
+type UpdateOpts struct {
+	CDNEnabled   bool `h:"X-Cdn-Enabled"`
+	LogRetention bool `h:"X-Log-Retention"`
+	TTL          int  `h:"X-Ttl"`
+}
+
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	h["X-Cdn-Enabled"] = strconv.FormatBool(opts.CDNEnabled)
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, containerName, opts)
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/delegate_test.go b/rackspace/objectstorage/v1/cdncontainers/delegate_test.go
new file mode 100644
index 0000000..02c3c5e
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/delegate_test.go
@@ -0,0 +1,50 @@
+package cdncontainers
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListCDNContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListContainerNamesSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetCDNContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetContainerSuccessfully(t)
+
+	_, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata()
+	th.CheckNoErr(t, err)
+
+}
+
+func TestUpdateCDNContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateContainerSuccessfully(t)
+
+	options := &UpdateOpts{TTL: 3600}
+	res := Update(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/doc.go b/rackspace/objectstorage/v1/cdncontainers/doc.go
new file mode 100644
index 0000000..7b0930e
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/doc.go
@@ -0,0 +1,3 @@
+// Package cdncontainers provides information and interaction with the CDN
+// Container API resource for the Rackspace Cloud Files service.
+package cdncontainers
diff --git a/rackspace/objectstorage/v1/cdncontainers/requests.go b/rackspace/objectstorage/v1/cdncontainers/requests.go
new file mode 100644
index 0000000..0567833
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/requests.go
@@ -0,0 +1,58 @@
+package cdncontainers
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// EnableOptsBuilder allows extensions to add additional parameters to the Enable
+// request.
+type EnableOptsBuilder interface {
+	ToCDNContainerEnableMap() (map[string]string, error)
+}
+
+// EnableOpts is a structure that holds options for enabling a CDN container.
+type EnableOpts struct {
+	// CDNEnabled indicates whether or not the container is CDN enabled. Set to
+	// `true` to enable the container. Note that changing this setting from true
+	// to false will disable the container in the CDN but only after the TTL has
+	// expired.
+	CDNEnabled bool `h:"X-Cdn-Enabled"`
+	// TTL is the time-to-live for the container (in seconds).
+	TTL int `h:"X-Ttl"`
+}
+
+// ToCDNContainerEnableMap formats an EnableOpts into a map of headers.
+func (opts EnableOpts) ToCDNContainerEnableMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	return h, nil
+}
+
+// Enable is a function that enables/disables a CDN container.
+func Enable(c *gophercloud.ServiceClient, containerName string, opts EnableOptsBuilder) EnableResult {
+	var res EnableResult
+	h := c.AuthenticatedHeaders()
+
+	if opts != nil {
+		headers, err := opts.ToCDNContainerEnableMap()
+		if err != nil {
+			res.Err = err
+			return res
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := perigee.Request("PUT", enableURL(c, containerName), perigee.Options{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/requests_test.go b/rackspace/objectstorage/v1/cdncontainers/requests_test.go
new file mode 100644
index 0000000..28b963d
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/requests_test.go
@@ -0,0 +1,29 @@
+package cdncontainers
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestEnableCDNContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.Header().Add("X-Ttl", "259200")
+		w.Header().Add("X-Cdn-Enabled", "True")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	options := &EnableOpts{CDNEnabled: true, TTL: 259200}
+	actual := Enable(fake.ServiceClient(), "testContainer", options)
+	th.AssertNoErr(t, actual.Err)
+	th.CheckEquals(t, actual.Header["X-Ttl"][0], "259200")
+	th.CheckEquals(t, actual.Header["X-Cdn-Enabled"][0], "True")
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/results.go b/rackspace/objectstorage/v1/cdncontainers/results.go
new file mode 100644
index 0000000..374d884
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/results.go
@@ -0,0 +1,8 @@
+package cdncontainers
+
+import "github.com/rackspace/gophercloud"
+
+// EnableResult represents the result of a get operation.
+type EnableResult struct {
+	gophercloud.Result
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/urls.go b/rackspace/objectstorage/v1/cdncontainers/urls.go
new file mode 100644
index 0000000..80653f2
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/urls.go
@@ -0,0 +1,7 @@
+package cdncontainers
+
+import "github.com/rackspace/gophercloud"
+
+func enableURL(c *gophercloud.ServiceClient, containerName string) string {
+	return c.ServiceURL(containerName)
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/urls_test.go b/rackspace/objectstorage/v1/cdncontainers/urls_test.go
new file mode 100644
index 0000000..aa5bfe6
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/urls_test.go
@@ -0,0 +1,20 @@
+package cdncontainers
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestEnableURL(t *testing.T) {
+	actual := enableURL(endpointClient(), "foo")
+	expected := endpoint + "foo"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/cdnobjects/delegate.go b/rackspace/objectstorage/v1/cdnobjects/delegate.go
new file mode 100644
index 0000000..e9d2ff1
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdnobjects/delegate.go
@@ -0,0 +1,11 @@
+package cdnobjects
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+)
+
+// Delete is a function that deletes an object from the CDN.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult {
+	return os.Delete(c, containerName, objectName, nil)
+}
diff --git a/rackspace/objectstorage/v1/cdnobjects/delegate_test.go b/rackspace/objectstorage/v1/cdnobjects/delegate_test.go
new file mode 100644
index 0000000..b5e04a9
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdnobjects/delegate_test.go
@@ -0,0 +1,19 @@
+package cdnobjects
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDeleteCDNObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteObjectSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil)
+	th.AssertNoErr(t, res.Err)
+
+}
diff --git a/rackspace/objectstorage/v1/cdnobjects/doc.go b/rackspace/objectstorage/v1/cdnobjects/doc.go
new file mode 100644
index 0000000..90cd5c9
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdnobjects/doc.go
@@ -0,0 +1,3 @@
+// Package cdnobjects provides information and interaction with the CDN
+// Object API resource for the Rackspace Cloud Files service.
+package cdnobjects
diff --git a/rackspace/objectstorage/v1/containers/delegate.go b/rackspace/objectstorage/v1/containers/delegate.go
new file mode 100644
index 0000000..77ed002
--- /dev/null
+++ b/rackspace/objectstorage/v1/containers/delegate.go
@@ -0,0 +1,93 @@
+package containers
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractInfo interprets a page of List results when full container info
+// is requested.
+func ExtractInfo(page pagination.Page) ([]os.Container, error) {
+	return os.ExtractInfo(page)
+}
+
+// ExtractNames interprets a page of List results when just the container
+// names are requested.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	return os.ExtractNames(page)
+}
+
+// List is a function that retrieves containers associated with the account as
+// well as account metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+	Metadata         map[string]string
+	ContainerRead    string `h:"X-Container-Read"`
+	ContainerWrite   string `h:"X-Container-Write"`
+	VersionsLocation string `h:"X-Versions-Location"`
+}
+
+// ToContainerCreateMap formats a CreateOpts into a map of headers.
+func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, containerName, opts)
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) os.DeleteResult {
+	return os.Delete(c, containerName)
+}
+
+// UpdateOpts is a structure that holds parameters for updating or creating a
+// container's metadata.
+type UpdateOpts struct {
+	Metadata               map[string]string
+	ContainerRead          string `h:"X-Container-Read"`
+	ContainerWrite         string `h:"X-Container-Write"`
+	ContentType            string `h:"Content-Type"`
+	DetectContentType      bool   `h:"X-Detect-Content-Type"`
+	RemoveVersionsLocation string `h:"X-Remove-Versions-Location"`
+	VersionsLocation       string `h:"X-Versions-Location"`
+}
+
+// ToContainerUpdateMap formats a CreateOpts into a map of headers.
+func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Container-Meta-"+k] = v
+	}
+	return h, nil
+}
+
+// Update is a function that creates, updates, or deletes a container's
+// metadata.
+func Update(c *gophercloud.ServiceClient, containerName string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, containerName, opts)
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just
+// the custom metadata, pass the GetResult response to the ExtractMetadata
+// function.
+func Get(c *gophercloud.ServiceClient, containerName string) os.GetResult {
+	return os.Get(c, containerName)
+}
diff --git a/rackspace/objectstorage/v1/containers/delegate_test.go b/rackspace/objectstorage/v1/containers/delegate_test.go
new file mode 100644
index 0000000..7ba4eb2
--- /dev/null
+++ b/rackspace/objectstorage/v1/containers/delegate_test.go
@@ -0,0 +1,91 @@
+package containers
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListContainerInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListContainerInfoSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), &os.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ExpectedListInfo, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListContainerNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListContainerNamesSuccessfully(t)
+
+	count := 0
+	err := List(fake.ServiceClient(), &os.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, os.ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateContainerSuccessfully(t)
+
+	options := os.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+	res := Create(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+	th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0])
+
+}
+
+func TestDeleteContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteContainerSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer")
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestUpdateContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateContainerSuccessfully(t)
+
+	options := &os.UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+	res := Update(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestGetContainers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetContainerSuccessfully(t)
+
+	_, err := Get(fake.ServiceClient(), "testContainer").ExtractMetadata()
+	th.CheckNoErr(t, err)
+}
diff --git a/rackspace/objectstorage/v1/containers/doc.go b/rackspace/objectstorage/v1/containers/doc.go
new file mode 100644
index 0000000..d132a07
--- /dev/null
+++ b/rackspace/objectstorage/v1/containers/doc.go
@@ -0,0 +1,3 @@
+// Package containers provides information and interaction with the Container
+// API resource for the Rackspace Cloud Files service.
+package containers
diff --git a/rackspace/objectstorage/v1/objects/delegate.go b/rackspace/objectstorage/v1/objects/delegate.go
new file mode 100644
index 0000000..bd4a4f0
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/delegate.go
@@ -0,0 +1,90 @@
+package objects
+
+import (
+	"io"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]os.Object, error) {
+	return os.ExtractInfo(page)
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+	return os.ExtractNames(page)
+}
+
+// List is a function that retrieves objects in the container as
+// well as container metadata. It returns a pager which can be iterated with the
+// EachPage function.
+func List(c *gophercloud.ServiceClient, containerName string, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, containerName, opts)
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the
+// ExtractContent function.
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DownloadOptsBuilder) os.DownloadResult {
+	return os.Download(c, containerName, objectName, opts)
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, containerName, objectName, content, opts)
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to
+// another.
+type CopyOpts struct {
+	Metadata           map[string]string
+	ContentDisposition string `h:"Content-Disposition"`
+	ContentEncoding    string `h:"Content-Encoding"`
+	ContentLength      int    `h:"Content-Length"`
+	ContentType        string `h:"Content-Type"`
+	CopyFrom           string `h:"X-Copy_From"`
+	Destination        string `h:"Destination"`
+	DetectContentType  bool   `h:"X-Detect-Content-Type"`
+}
+
+// ToObjectCopyMap formats a CopyOpts into a map of headers.
+func (opts CopyOpts) ToObjectCopyMap() (map[string]string, error) {
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+	// `Content-Length` is required and a value of "0" is acceptable, but calling `gophercloud.BuildHeaders`
+	// will remove the `Content-Length` header if it's set to 0 (or equivalently not set). This will add
+	// the header if it's not already set.
+	if _, ok := h["Content-Length"]; !ok {
+		h["Content-Length"] = "0"
+	}
+	return h, nil
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CopyOptsBuilder) os.CopyResult {
+	return os.Copy(c, containerName, objectName, opts)
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts os.DeleteOptsBuilder) os.DeleteResult {
+	return os.Delete(c, containerName, objectName, opts)
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts os.GetOptsBuilder) os.GetResult {
+	return os.Get(c, containerName, objectName, opts)
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, containerName, objectName, opts)
+}
diff --git a/rackspace/objectstorage/v1/objects/delegate_test.go b/rackspace/objectstorage/v1/objects/delegate_test.go
new file mode 100644
index 0000000..08831ec
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/delegate_test.go
@@ -0,0 +1,115 @@
+package objects
+
+import (
+	"bytes"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDownloadObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDownloadObjectSuccessfully(t)
+
+	content, err := Download(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractContent()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, string(content), "Successful download with Gophercloud")
+}
+
+func TestListObjectsInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListObjectsInfoSuccessfully(t)
+
+	count := 0
+	options := &os.ListOpts{Full: true}
+	err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractInfo(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ExpectedListInfo, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListObjectNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListObjectNamesSuccessfully(t)
+
+	count := 0
+	options := &os.ListOpts{Full: false}
+	err := List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNames(page)
+		if err != nil {
+			t.Errorf("Failed to extract container names: %v", err)
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, os.ExpectedListNames, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateObjectSuccessfully(t)
+
+	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+	options := &os.CreateOpts{ContentType: "application/json"}
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCopyObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCopyObjectSuccessfully(t)
+
+	options := &CopyOpts{Destination: "/newTestContainer/newTestObject"}
+	res := Copy(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestDeleteObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteObjectSuccessfully(t)
+
+	res := Delete(fake.ServiceClient(), "testContainer", "testObject", nil)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateObjectSuccessfully(t)
+
+	options := &os.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}}
+	res := Update(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetObjectSuccessfully(t)
+
+	expected := map[string]string{"Gophercloud-Test": "objects"}
+	actual, err := Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/objectstorage/v1/objects/doc.go b/rackspace/objectstorage/v1/objects/doc.go
new file mode 100644
index 0000000..781984b
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/doc.go
@@ -0,0 +1,3 @@
+// Package objects provides information and interaction with the Object
+// API resource for the Rackspace Cloud Files service.
+package objects
diff --git a/reauth.go b/reauth.go
deleted file mode 100644
index 342aca4..0000000
--- a/reauth.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package gophercloud
-
-import (
-	"github.com/racker/perigee"
-)
-
-// WithReauth wraps a Perigee request fragment with logic to perform re-authentication
-// if it's deemed necessary.
-//
-// Do not confuse this function with WithReauth()!  Although they work together to support reauthentication,
-// WithReauth() actually contains the decision-making logic to determine when to perform a reauth,
-// while WithReauthHandler() is used to configure what a reauth actually entails.
-func (c *Context) WithReauth(ap AccessProvider, f func() error) error {
-	err := f()
-	cause, ok := err.(*perigee.UnexpectedResponseCodeError)
-	if ok && cause.Actual == 401 {
-		err = c.reauthHandler(ap)
-		if err == nil {
-			err = f()
-		}
-	}
-	return err
-}
-
-// This is like WithReauth above but returns a perigee Response object
-func (c *Context) ResponseWithReauth(ap AccessProvider, f func() (*perigee.Response, error)) (*perigee.Response, error) {
-	response, err := f()
-	cause, ok := err.(*perigee.UnexpectedResponseCodeError)
-	if ok && cause.Actual == 401 {
-		err = c.reauthHandler(ap)
-		if err == nil {
-			response, err = f()
-		}
-	}
-	return response, err
-}
diff --git a/reauth_test.go b/reauth_test.go
deleted file mode 100644
index e3501b8..0000000
--- a/reauth_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package gophercloud
-
-import (
-	"github.com/racker/perigee"
-	"testing"
-)
-
-// This reauth-handler does nothing, and returns no error.
-func doNothing(_ AccessProvider) error {
-	return nil
-}
-
-func TestOtherErrorsPropegate(t *testing.T) {
-	calls := 0
-	c := TestContext().WithReauthHandler(doNothing)
-
-	err := c.WithReauth(nil, func() error {
-		calls++
-		return &perigee.UnexpectedResponseCodeError{
-			Expected: []int{204},
-			Actual:   404,
-		}
-	})
-
-	if err == nil {
-		t.Error("Expected MyError to be returned; got nil instead.")
-		return
-	}
-	if _, ok := err.(*perigee.UnexpectedResponseCodeError); !ok {
-		t.Error("Expected UnexpectedResponseCodeError; got %#v", err)
-		return
-	}
-	if calls != 1 {
-		t.Errorf("Expected the body to be invoked once; found %d calls instead", calls)
-		return
-	}
-}
-
-func Test401ErrorCausesBodyInvokation2ndTime(t *testing.T) {
-	calls := 0
-	c := TestContext().WithReauthHandler(doNothing)
-
-	err := c.WithReauth(nil, func() error {
-		calls++
-		return &perigee.UnexpectedResponseCodeError{
-			Expected: []int{204},
-			Actual:   401,
-		}
-	})
-
-	if err == nil {
-		t.Error("Expected MyError to be returned; got nil instead.")
-		return
-	}
-	if calls != 2 {
-		t.Errorf("Expected the body to be invoked once; found %d calls instead", calls)
-		return
-	}
-}
-
-func TestReauthAttemptShouldHappen(t *testing.T) {
-	calls := 0
-	c := TestContext().WithReauthHandler(func(_ AccessProvider) error {
-		calls++
-		return nil
-	})
-	c.WithReauth(nil, func() error {
-		return &perigee.UnexpectedResponseCodeError{
-			Expected: []int{204},
-			Actual:   401,
-		}
-	})
-
-	if calls != 1 {
-		t.Errorf("Expected Reauthenticator to be called once; found %d instead", calls)
-		return
-	}
-}
-
-type MyError struct{}
-
-func (*MyError) Error() string {
-	return "MyError instance"
-}
-
-func TestReauthErrorShouldPropegate(t *testing.T) {
-	c := TestContext().WithReauthHandler(func(_ AccessProvider) error {
-		return &MyError{}
-	})
-
-	err := c.WithReauth(nil, func() error {
-		return &perigee.UnexpectedResponseCodeError{
-			Expected: []int{204},
-			Actual:   401,
-		}
-	})
-
-	if _, ok := err.(*MyError); !ok {
-		t.Errorf("Expected a MyError; got %#v", err)
-		return
-	}
-}
-
-type MyAccess struct{}
-
-func (my *MyAccess) FirstEndpointUrlByCriteria(ApiCriteria) string {
-	return ""
-}
-func (my *MyAccess) AuthToken() string {
-	return ""
-}
-func (my *MyAccess) Revoke(string) error {
-	return nil
-}
-func (my *MyAccess) Reauthenticate() error {
-	return nil
-}
-
-func TestReauthHandlerUsesSameAccessProvider(t *testing.T) {
-	fakeAccess := &MyAccess{}
-	c := TestContext().WithReauthHandler(func(acc AccessProvider) error {
-		if acc != fakeAccess {
-			t.Errorf("Expected acc = fakeAccess")
-		}
-		return nil
-	})
-	c.WithReauth(fakeAccess, func() error {
-		return &perigee.UnexpectedResponseCodeError{
-			Expected: []int{204},
-			Actual:   401,
-		}
-	})
-}
diff --git a/results.go b/results.go
new file mode 100644
index 0000000..a8e3705
--- /dev/null
+++ b/results.go
@@ -0,0 +1,58 @@
+package gophercloud
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+// Result acts as a base struct that other results can embed.
+type Result struct {
+	// Body is the payload of the HTTP response from the server. In most cases, this will be the
+	// deserialized JSON structure.
+	Body interface{}
+
+	// Header contains the HTTP header structure from the original response.
+	Header http.Header
+
+	// Err is an error that occurred during the operation. It's deferred until extraction to make
+	// it easier to chain operations.
+	Err error
+}
+
+// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON.
+func (r Result) PrettyPrintJSON() string {
+	pretty, err := json.MarshalIndent(r.Body, "", "  ")
+	if err != nil {
+		panic(err.Error())
+	}
+	return string(pretty)
+}
+
+// RFC3339Milli describes a time format used by API responses.
+const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
+
+// Link represents a structure that enables paginated collections how to
+// traverse backward or forward. The "Rel" field is usually either "next".
+type Link struct {
+	Href string `mapstructure:"href"`
+	Rel  string `mapstructure:"rel"`
+}
+
+// ExtractNextURL attempts to extract the next URL from a JSON structure. It
+// follows the common convention of nesting back and next URLs in a "links"
+// JSON array.
+func ExtractNextURL(links []Link) (string, error) {
+	var url string
+
+	for _, l := range links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
diff --git a/script/acceptancetest b/script/acceptancetest
new file mode 100755
index 0000000..f9c89f4
--- /dev/null
+++ b/script/acceptancetest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the acceptance tests.
+
+exec go test -p=1 -tags 'acceptance fixtures' github.com/rackspace/gophercloud/acceptance/... $@
diff --git a/scripts/create-environment.sh b/script/bootstrap
old mode 100644
new mode 100755
similarity index 100%
rename from scripts/create-environment.sh
rename to script/bootstrap
diff --git a/script/cibuild b/script/cibuild
new file mode 100755
index 0000000..1cb389e
--- /dev/null
+++ b/script/cibuild
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Test script to be invoked by Travis.
+
+exec script/unittest -v
diff --git a/script/test b/script/test
new file mode 100755
index 0000000..1e03dff
--- /dev/null
+++ b/script/test
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run all the tests.
+
+exec go test -tags 'acceptance fixtures' ./... $@
diff --git a/script/unittest b/script/unittest
new file mode 100755
index 0000000..d3440a9
--- /dev/null
+++ b/script/unittest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the unit tests.
+
+exec go test -tags fixtures ./... $@
diff --git a/scripts/test-all.sh b/scripts/test-all.sh
deleted file mode 100755
index 096736f..0000000
--- a/scripts/test-all.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-#
-# This script is responsible for executing all the acceptance tests found in
-# the acceptance/ directory.
-
-# Find where _this_ script is running from.
-SCRIPTS=$(dirname $0)
-SCRIPTS=$(cd $SCRIPTS; pwd)
-
-# Locate the acceptance test / examples directory.
-ACCEPTANCE=$(cd $SCRIPTS/../acceptance; pwd)
-
-# Go workspace path
-WS=$(cd $SCRIPTS/..; pwd)
-
-# In order to run Go code interactively, we need the GOPATH environment
-# to be set.
-if [ "x$GOPATH" == "x" ]; then
-  export GOPATH=$WS
-  echo "WARNING: You didn't have your GOPATH environment variable set."
-  echo "         I'm assuming $GOPATH as its value."
-fi
-
-# Run all acceptance tests sequentially.
-# If any test fails, we fail fast.
-LIBS=$(ls $ACCEPTANCE/lib*.go)
-for T in $(ls -1 $ACCEPTANCE/[0-9][0-9]*.go); do
-  if ! [ -x $T ]; then
-    CMD="go run $T $LIBS -quiet"
-    echo "$CMD ..."
-    if ! $CMD ; then
-      echo "- FAILED.  Try re-running w/out the -quiet option to see output."
-      exit 1
-    fi
-  fi
-done
-
diff --git a/servers.go b/servers.go
deleted file mode 100644
index 0861fa2..0000000
--- a/servers.go
+++ /dev/null
@@ -1,905 +0,0 @@
-// TODO(sfalvo): Remove Rackspace-specific Server structure fields and refactor them into a provider-specific access method.
-// Be sure to update godocs accordingly.
-
-package gophercloud
-
-import (
-	"fmt"
-	"net/url"
-	"strings"
-
-	"github.com/mitchellh/mapstructure"
-	"github.com/racker/perigee"
-)
-
-// genericServersProvider structures provide the implementation for generic OpenStack-compatible
-// CloudServersProvider interfaces.
-type genericServersProvider struct {
-	// endpoint refers to the provider's API endpoint base URL.  This will be used to construct
-	// and issue queries.
-	endpoint string
-
-	// Test context (if any) in which to issue requests.
-	context *Context
-
-	// access associates this API provider with a set of credentials,
-	// which may be automatically renewed if they near expiration.
-	access AccessProvider
-}
-
-// See the CloudServersProvider interface for details.
-func (gcp *genericServersProvider) ListServersByFilter(filter url.Values) ([]Server, error) {
-	var ss []Server
-
-	err := gcp.context.WithReauth(gcp.access, func() error {
-		url := gcp.endpoint + "/servers/detail?" + filter.Encode()
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gcp.context.httpClient,
-			Results:      &struct{ Servers *[]Server }{&ss},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gcp.access.AuthToken(),
-			},
-		})
-	})
-	return ss, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gcp *genericServersProvider) ListServersLinksOnly() ([]Server, error) {
-	var ss []Server
-
-	err := gcp.context.WithReauth(gcp.access, func() error {
-		url := gcp.endpoint + "/servers"
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gcp.context.httpClient,
-			Results:      &struct{ Servers *[]Server }{&ss},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gcp.access.AuthToken(),
-			},
-		})
-	})
-	return ss, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gcp *genericServersProvider) ListServers() ([]Server, error) {
-	var ss []Server
-
-	err := gcp.context.WithReauth(gcp.access, func() error {
-		url := gcp.endpoint + "/servers/detail"
-		return perigee.Get(url, perigee.Options{
-			CustomClient: gcp.context.httpClient,
-			Results:      &struct{ Servers *[]Server }{&ss},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gcp.access.AuthToken(),
-			},
-		})
-	})
-
-	// Compatibility with v0.0.x -- we "map" our public and private
-	// addresses into a legacy structure field for the benefit of
-	// earlier software.
-
-	if err != nil {
-		return ss, err
-	}
-
-	for _, s := range ss {
-		err = mapstructure.Decode(s.RawAddresses, &s.Addresses)
-		if err != nil {
-			return ss, err
-		}
-	}
-
-	return ss, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ServerById(id string) (*Server, error) {
-	var s *Server
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/servers/" + id
-		return perigee.Get(url, perigee.Options{
-			Results: &struct{ Server **Server }{&s},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{200},
-		})
-	})
-
-	// Compatibility with v0.0.x -- we "map" our public and private
-	// addresses into a legacy structure field for the benefit of
-	// earlier software.
-
-	if err != nil {
-		return s, err
-	}
-
-	err = mapstructure.Decode(s.RawAddresses, &s.Addresses)
-
-	return s, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) CreateServer(ns NewServer) (*NewServer, error) {
-	var s *NewServer
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := gsp.endpoint + "/servers"
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Server *NewServer `json:"server"`
-			}{&ns},
-			Results: &struct{ Server **NewServer }{&s},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-
-	return s, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) DeleteServerById(id string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := gsp.endpoint + "/servers/" + id
-		return perigee.Delete(url, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{204},
-		})
-	})
-	return err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) SetAdminPassword(id, pw string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				ChangePassword struct {
-					AdminPass string `json:"adminPass"`
-				} `json:"changePassword"`
-			}{
-				struct {
-					AdminPass string `json:"adminPass"`
-				}{pw},
-			},
-			OkCodes: []int{202},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ResizeServer(id, newName, newFlavor, newDiskConfig string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		rr := ResizeRequest{
-			Name:       newName,
-			FlavorRef:  newFlavor,
-			DiskConfig: newDiskConfig,
-		}
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				Resize ResizeRequest `json:"resize"`
-			}{rr},
-			OkCodes: []int{202},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) RevertResize(id string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				RevertResize *int `json:"revertResize"`
-			}{nil},
-			OkCodes: []int{202},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ConfirmResize(id string) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				ConfirmResize *int `json:"confirmResize"`
-			}{nil},
-			OkCodes: []int{204},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-	return err
-}
-
-// See the CloudServersProvider interface for details
-func (gsp *genericServersProvider) RebootServer(id string, hard bool) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		types := map[bool]string{false: "SOFT", true: "HARD"}
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				Reboot struct {
-					Type string `json:"type"`
-				} `json:"reboot"`
-			}{
-				struct {
-					Type string `json:"type"`
-				}{types[hard]},
-			},
-			OkCodes: []int{202},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-		})
-	})
-}
-
-// See the CloudServersProvider interface for details
-func (gsp *genericServersProvider) RescueServer(id string) (string, error) {
-	var pw *string
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				Rescue string `json:"rescue"`
-			}{"none"},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct {
-				AdminPass **string `json:"adminPass"`
-			}{&pw},
-		})
-	})
-	return *pw, err
-}
-
-// See the CloudServersProvider interface for details
-func (gsp *genericServersProvider) UnrescueServer(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(url, perigee.Options{
-			ReqBody: &struct {
-				Unrescue *int `json:"unrescue"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// See the CloudServersProvider interface for details
-func (gsp *genericServersProvider) UpdateServer(id string, changes NewServerSettings) (*Server, error) {
-	var svr *Server
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		url := fmt.Sprintf("%s/servers/%s", gsp.endpoint, id)
-		return perigee.Put(url, perigee.Options{
-			ReqBody: &struct {
-				Server NewServerSettings `json:"server"`
-			}{changes},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct {
-				Server **Server `json:"server"`
-			}{&svr},
-		})
-	})
-	return svr, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) RebuildServer(id string, ns NewServer) (*Server, error) {
-	var s *Server
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Rebuild *NewServer `json:"rebuild"`
-			}{&ns},
-			Results: &struct{ Server **Server }{&s},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-
-	return s, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ListAddresses(id string) (AddressSet, error) {
-	var pas *AddressSet
-	var statusCode int
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/ips", gsp.endpoint, id)
-		return perigee.Get(ep, perigee.Options{
-			Results: &struct{ Addresses **AddressSet }{&pas},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes:    []int{200, 203},
-			StatusCode: &statusCode,
-		})
-	})
-
-	if err != nil {
-		if statusCode == 203 {
-			err = WarnUnauthoritative
-		}
-	}
-
-	return *pas, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ListAddressesByNetwork(id, networkLabel string) (NetworkAddress, error) {
-	pas := make(NetworkAddress)
-	var statusCode int
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/ips/%s", gsp.endpoint, id, networkLabel)
-		return perigee.Get(ep, perigee.Options{
-			Results: &pas,
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes:    []int{200, 203},
-			StatusCode: &statusCode,
-		})
-	})
-
-	if err != nil {
-		if statusCode == 203 {
-			err = WarnUnauthoritative
-		}
-	}
-
-	return pas, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) CreateImage(id string, ci CreateImage) (string, error) {
-	response, err := gsp.context.ResponseWithReauth(gsp.access, func() (*perigee.Response, error) {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Request("POST", ep, perigee.Options{
-			ReqBody: &struct {
-				CreateImage *CreateImage `json:"createImage"`
-			}{&ci},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{200, 202},
-		})
-	})
-
-	if err != nil {
-		return "", err
-	}
-	location, err := response.HttpResponse.Location()
-	if err != nil {
-		return "", err
-	}
-
-	// Return the last element of the location which is the image id
-	locationArr := strings.Split(location.Path, "/")
-	return locationArr[len(locationArr)-1], err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) Pause(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Pause *int `json:"pause"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) Unpause(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Unpause *int `json:"unpause"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) Suspend(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Suspend *int `json:"suspend"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) Resume(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Resume *int `json:"resume"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// WARNING: This function remains undocumented on the OpenStack API reference website, http://developer.openstack.org/api-ref-compute-v2-ext.html .
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) Start(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Start *int `json:"start"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// WARNING: This function remains undocumented on the OpenStack API reference website, http://developer.openstack.org/api-ref-compute-v2-ext.html .
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) Stop(id string) error {
-	return gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: &struct {
-				Stop *int `json:"stop"`
-			}{nil},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ListSecurityGroups() ([]SecurityGroup, error) {
-	var sgs []SecurityGroup
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint)
-		return perigee.Get(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct {
-				SecurityGroups *[]SecurityGroup `json:"security_groups"`
-			}{&sgs},
-		})
-	})
-	return sgs, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) CreateSecurityGroup(desired SecurityGroup) (*SecurityGroup, error) {
-	var actual *SecurityGroup
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-groups", gsp.endpoint)
-		return perigee.Post(ep, perigee.Options{
-			ReqBody: struct {
-				AddSecurityGroup SecurityGroup `json:"security_group"`
-			}{desired},
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct {
-				SecurityGroup **SecurityGroup `json:"security_group"`
-			}{&actual},
-		})
-	})
-	return actual, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ListSecurityGroupsByServerId(id string) ([]SecurityGroup, error) {
-	var sgs []SecurityGroup
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/servers/%s/os-security-groups", gsp.endpoint, id)
-		return perigee.Get(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct {
-				SecurityGroups *[]SecurityGroup `json:"security_groups"`
-			}{&sgs},
-		})
-	})
-	return sgs, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) SecurityGroupById(id int) (*SecurityGroup, error) {
-	var actual *SecurityGroup
-
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id)
-		return perigee.Get(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct {
-				SecurityGroup **SecurityGroup `json:"security_group"`
-			}{&actual},
-		})
-	})
-	return actual, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) DeleteSecurityGroupById(id int) error {
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-groups/%d", gsp.endpoint, id)
-		return perigee.Delete(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			OkCodes: []int{202},
-		})
-	})
-	return err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) ListDefaultSGRules() ([]SGRule, error) {
-	var sgrs []SGRule
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-group-default-rules", gsp.endpoint)
-		return perigee.Get(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct{ Security_group_default_rules *[]SGRule }{&sgrs},
-		})
-	})
-	return sgrs, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) CreateDefaultSGRule(r SGRule) (*SGRule, error) {
-	var sgr *SGRule
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-group-default-rules", gsp.endpoint)
-		return perigee.Post(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct{ Security_group_default_rule **SGRule }{&sgr},
-			ReqBody: struct {
-				Security_group_default_rule SGRule `json:"security_group_default_rule"`
-			}{r},
-		})
-	})
-	return sgr, err
-}
-
-// See the CloudServersProvider interface for details.
-func (gsp *genericServersProvider) GetSGRule(id string) (*SGRule, error) {
-	var sgr *SGRule
-	err := gsp.context.WithReauth(gsp.access, func() error {
-		ep := fmt.Sprintf("%s/os-security-group-default-rules/%s", gsp.endpoint, id)
-		return perigee.Get(ep, perigee.Options{
-			MoreHeaders: map[string]string{
-				"X-Auth-Token": gsp.access.AuthToken(),
-			},
-			Results: &struct{ Security_group_default_rule **SGRule }{&sgr},
-		})
-	})
-	return sgr, err
-}
-
-// SecurityGroup provides a description of a security group, including all its rules.
-type SecurityGroup struct {
-	Description string   `json:"description,omitempty"`
-	Id          int      `json:"id,omitempty"`
-	Name        string   `json:"name,omitempty"`
-	Rules       []SGRule `json:"rules,omitempty"`
-	TenantId    string   `json:"tenant_id,omitempty"`
-}
-
-// SGRule encapsulates a single rule which applies to a security group.
-// This definition is just a guess, based on the documentation found in another extension here: http://docs.openstack.org/api/openstack-compute/2/content/GET_os-security-group-default-rules-v2_listSecGroupDefaultRules_v2__tenant_id__os-security-group-rules_ext-os-security-group-default-rules.html
-type SGRule struct {
-	FromPort   int                    `json:"from_port,omitempty"`
-	Id         int                    `json:"id,omitempty"`
-	IpProtocol string                 `json:"ip_protocol,omitempty"`
-	IpRange    map[string]interface{} `json:"ip_range,omitempty"`
-	ToPort     int                    `json:"to_port,omitempty"`
-}
-
-// RaxBandwidth provides measurement of server bandwidth consumed over a given audit interval.
-type RaxBandwidth struct {
-	AuditPeriodEnd    string `json:"audit_period_end"`
-	AuditPeriodStart  string `json:"audit_period_start"`
-	BandwidthInbound  int64  `json:"bandwidth_inbound"`
-	BandwidthOutbound int64  `json:"bandwidth_outbound"`
-	Interface         string `json:"interface"`
-}
-
-// A VersionedAddress denotes either an IPv4 or IPv6 (depending on version indicated)
-// address.
-type VersionedAddress struct {
-	Addr    string `json:"addr"`
-	Version int    `json:"version"`
-}
-
-// An AddressSet provides a set of public and private IP addresses for a resource.
-// Each address has a version to identify if IPv4 or IPv6.
-type AddressSet struct {
-	Public  []VersionedAddress `json:"public"`
-	Private []VersionedAddress `json:"private"`
-}
-
-type NetworkAddress map[string][]VersionedAddress
-
-// Server records represent (virtual) hardware instances (not configurations) accessible by the user.
-//
-// The AccessIPv4 / AccessIPv6 fields provides IP addresses for the server in the IPv4 or IPv6 format, respectively.
-//
-// Addresses provides addresses for any attached isolated networks.
-// The version field indicates whether the IP address is version 4 or 6.
-// Note: only public and private pools appear here.
-// To get the complete set, use the AllAddressPools() method instead.
-//
-// Created tells when the server entity was created.
-//
-// The Flavor field includes the flavor ID and flavor links.
-//
-// The compute provisioning algorithm has an anti-affinity property that
-// attempts to spread customer VMs across hosts.
-// Under certain situations,
-// VMs from the same customer might be placed on the same host.
-// The HostId field represents the host your server runs on and
-// can be used to determine this scenario if it is relevant to your application.
-// Note that HostId is unique only per account; it is not globally unique.
-//
-// Id provides the server's unique identifier.
-// This field must be treated opaquely.
-//
-// Image indicates which image is installed on the server.
-//
-// Links provides one or more means of accessing the server.
-//
-// Metadata provides a small key-value store for application-specific information.
-//
-// Name provides a human-readable name for the server.
-//
-// Progress indicates how far along it is towards being provisioned.
-// 100 represents complete, while 0 represents just beginning.
-//
-// Status provides an indication of what the server's doing at the moment.
-// A server will be in ACTIVE state if it's ready for use.
-//
-// OsDcfDiskConfig indicates the server's boot volume configuration.
-// Valid values are:
-//     AUTO
-//     ----
-//     The server is built with a single partition the size of the target flavor disk.
-//     The file system is automatically adjusted to fit the entire partition.
-//     This keeps things simple and automated.
-//     AUTO is valid only for images and servers with a single partition that use the EXT3 file system.
-//     This is the default setting for applicable Rackspace base images.
-//
-//     MANUAL
-//     ------
-//     The server is built using whatever partition scheme and file system is in the source image.
-//     If the target flavor disk is larger,
-//     the remaining disk space is left unpartitioned.
-//     This enables images to have non-EXT3 file systems, multiple partitions, and so on,
-//     and enables you to manage the disk configuration.
-//
-// RaxBandwidth provides measures of the server's inbound and outbound bandwidth per interface.
-//
-// OsExtStsPowerState provides an indication of the server's power.
-// This field appears to be a set of flag bits:
-//
-//           ... 4  3   2   1   0
-//         +--//--+---+---+---+---+
-//         | .... | 0 | S | 0 | I |
-//         +--//--+---+---+---+---+
-//                      |       |
-//                      |       +---  0=Instance is down.
-//                      |             1=Instance is up.
-//                      |
-//                      +-----------  0=Server is switched ON.
-//                                    1=Server is switched OFF.
-//                                    (note reverse logic.)
-//
-// Unused bits should be ignored when read, and written as 0 for future compatibility.
-//
-// OsExtStsTaskState and OsExtStsVmState work together
-// to provide visibility in the provisioning process for the instance.
-// Consult Rackspace documentation at
-// http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ch_extensions.html#ext_status
-// for more details.  It's too lengthy to include here.
-type Server struct {
-	AccessIPv4         string `json:"accessIPv4"`
-	AccessIPv6         string `json:"accessIPv6"`
-	Addresses          AddressSet
-	Created            string            `json:"created"`
-	Flavor             FlavorLink        `json:"flavor"`
-	HostId             string            `json:"hostId"`
-	Id                 string            `json:"id"`
-	Image              ImageLink         `json:"image"`
-	Links              []Link            `json:"links"`
-	Metadata           map[string]string `json:"metadata"`
-	Name               string            `json:"name"`
-	Progress           int               `json:"progress"`
-	Status             string            `json:"status"`
-	TenantId           string            `json:"tenant_id"`
-	Updated            string            `json:"updated"`
-	UserId             string            `json:"user_id"`
-	OsDcfDiskConfig    string            `json:"OS-DCF:diskConfig"`
-	RaxBandwidth       []RaxBandwidth    `json:"rax-bandwidth:bandwidth"`
-	OsExtStsPowerState int               `json:"OS-EXT-STS:power_state"`
-	OsExtStsTaskState  string            `json:"OS-EXT-STS:task_state"`
-	OsExtStsVmState    string            `json:"OS-EXT-STS:vm_state"`
-
-	RawAddresses map[string]interface{} `json:"addresses"`
-}
-
-// AllAddressPools returns a complete set of address pools available on the server.
-// The name of each pool supported keys the map.
-// The value of the map contains the addresses provided in the corresponding pool.
-func (s *Server) AllAddressPools() (map[string][]VersionedAddress, error) {
-	pools := make(map[string][]VersionedAddress, 0)
-	for pool, subtree := range s.RawAddresses {
-		addresses := make([]VersionedAddress, 0)
-		err := mapstructure.Decode(subtree, &addresses)
-		if err != nil {
-			return nil, err
-		}
-		pools[pool] = addresses
-	}
-	return pools, nil
-}
-
-// NewServerSettings structures record those fields of the Server structure to change
-// when updating a server (see UpdateServer method).
-type NewServerSettings struct {
-	Name       string `json:"name,omitempty"`
-	AccessIPv4 string `json:"accessIPv4,omitempty"`
-	AccessIPv6 string `json:"accessIPv6,omitempty"`
-}
-
-// NewServer structures are used for both requests and responses.
-// The fields discussed below are relevent for server-creation purposes.
-//
-// The Name field contains the desired name of the server.
-// Note that (at present) Rackspace permits more than one server with the same name;
-// however, software should not depend on this.
-// Not only will Rackspace support thank you, so will your own devops engineers.
-// A name is required.
-//
-// The ImageRef field contains the ID of the desired software image to place on the server.
-// This ID must be found in the image slice returned by the Images() function.
-// This field is required.
-//
-// The FlavorRef field contains the ID of the server configuration desired for deployment.
-// This ID must be found in the flavor slice returned by the Flavors() function.
-// This field is required.
-//
-// For OsDcfDiskConfig, refer to the Image or Server structure documentation.
-// This field defaults to "AUTO" if not explicitly provided.
-//
-// Metadata contains a small key/value association of arbitrary data.
-// Neither Rackspace nor OpenStack places significance on this field in any way.
-// This field defaults to an empty map if not provided.
-//
-// Personality specifies the contents of certain files in the server's filesystem.
-// The files and their contents are mapped through a slice of FileConfig structures.
-// If not provided, all filesystem entities retain their image-specific configuration.
-//
-// Networks specifies an affinity for the server's various networks and interfaces.
-// Networks are identified through UUIDs; see NetworkConfig structure documentation for more details.
-// If not provided, network affinity is determined automatically.
-//
-// The AdminPass field may be used to provide a root- or administrator-password
-// during the server provisioning process.
-// If not provided, a random password will be automatically generated and returned in this field.
-//
-// The following fields are intended to be used to communicate certain results about the server being provisioned.
-// When attempting to create a new server, these fields MUST not be provided.
-// They'll be filled in by the response received from the Rackspace APIs.
-//
-// The Id field contains the server's unique identifier.
-// The identifier's scope is best assumed to be bound by the user's account, unless other arrangements have been made with Rackspace.
-//
-// The SecurityGroup field allows the user to specify a security group at launch.
-//
-// Any Links provided are used to refer to the server specifically by URL.
-// These links are useful for making additional REST calls not explicitly supported by Gorax.
-type NewServer struct {
-	Name            string                   `json:"name,omitempty"`
-	ImageRef        string                   `json:"imageRef,omitempty"`
-	FlavorRef       string                   `json:"flavorRef,omitempty"`
-	Metadata        map[string]string        `json:"metadata,omitempty"`
-	Personality     []FileConfig             `json:"personality,omitempty"`
-	Networks        []NetworkConfig          `json:"networks,omitempty"`
-	AdminPass       string                   `json:"adminPass,omitempty"`
-	KeyPairName     string                   `json:"key_name,omitempty"`
-	Id              string                   `json:"id,omitempty"`
-	Links           []Link                   `json:"links,omitempty"`
-	OsDcfDiskConfig string                   `json:"OS-DCF:diskConfig,omitempty"`
-	SecurityGroup   []map[string]interface{} `json:"security_groups,omitempty"`
-	ConfigDrive     bool                     `json:"config_drive"`
-	UserData        string                   `json:"user_data"`
-}
-
-// ResizeRequest structures are used internally to encode to JSON the parameters required to resize a server instance.
-// Client applications will not use this structure (no API accepts an instance of this structure).
-// See the Region method ResizeServer() for more details on how to resize a server.
-type ResizeRequest struct {
-	Name       string `json:"name,omitempty"`
-	FlavorRef  string `json:"flavorRef"`
-	DiskConfig string `json:"OS-DCF:diskConfig,omitempty"`
-}
-
-type CreateImage struct {
-	Name     string            `json:"name"`
-	Metadata map[string]string `json:"metadata,omitempty"`
-}
diff --git a/servers_test.go b/servers_test.go
deleted file mode 100644
index 60c71c8..0000000
--- a/servers_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package gophercloud
-
-import (
-	"net/http"
-	"testing"
-)
-
-type testAccess struct {
-	public, internal              string
-	calledFirstEndpointByCriteria int
-}
-
-func (ta *testAccess) FirstEndpointUrlByCriteria(ac ApiCriteria) string {
-	ta.calledFirstEndpointByCriteria++
-	urls := []string{ta.public, ta.internal}
-	return urls[ac.UrlChoice]
-}
-
-func (ta *testAccess) AuthToken() string {
-	return ""
-}
-
-func (ta *testAccess) Revoke(string) error {
-	return nil
-}
-
-func (ta *testAccess) Reauthenticate() error {
-	return nil
-}
-
-func TestGetServersApi(t *testing.T) {
-	c := TestContext().UseCustomClient(&http.Client{Transport: newTransport().WithResponse("Hello")})
-
-	acc := &testAccess{
-		public:   "http://localhost:8080",
-		internal: "http://localhost:8086",
-	}
-
-	_, err := c.ServersApi(acc, ApiCriteria{
-		Name:      "cloudComputeOpenStack",
-		Region:    "dfw",
-		VersionId: "2",
-	})
-
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	if acc.calledFirstEndpointByCriteria != 1 {
-		t.Error("Expected FirstEndpointByCriteria to be called")
-		return
-	}
-}
diff --git a/service_catalog.go b/service_catalog.go
deleted file mode 100644
index e6cf4a0..0000000
--- a/service_catalog.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package gophercloud
-
-import (
-	"os"
-	"strings"
-)
-
-// ApiCriteria provides one or more criteria for the SDK to look for appropriate endpoints.
-// Fields left unspecified or otherwise set to their zero-values are assumed to not be
-// relevant, and do not participate in the endpoint search.
-//
-// Name specifies the desired service catalog entry name.
-// Type specifies the desired service catalog entry type.
-// Region specifies the desired endpoint region.
-// If unset, Gophercloud will try to use the region set in the
-// OS_REGION_NAME environment variable.  If that's not set,
-// region comparison will not occur.  If OS_REGION_NAME is set
-// and IgnoreEnvVars is also set, OS_REGION_NAME will be ignored.
-// VersionId specifies the desired version of the endpoint.
-// Note that this field is matched exactly, and is (at present)
-// opaque to Gophercloud.  Thus, requesting a version 2
-// endpoint will _not_ match a version 3 endpoint.
-// The UrlChoice field inidicates whether or not gophercloud
-// should use the public or internal endpoint URL if a
-// candidate endpoint is found.
-// IgnoreEnvVars instructs Gophercloud to ignore helpful environment variables.
-type ApiCriteria struct {
-	Name          string
-	Type          string
-	Region        string
-	VersionId     string
-	UrlChoice     int
-	IgnoreEnvVars bool
-}
-
-// The choices available for UrlChoice.  See the ApiCriteria structure for details.
-const (
-	PublicURL = iota
-	InternalURL
-)
-
-// Given a set of criteria to match on, locate the first candidate endpoint
-// in the provided service catalog.
-//
-// If nothing found, the result will be a zero-valued EntryEndpoint (all URLs
-// set to "").
-func FindFirstEndpointByCriteria(entries []CatalogEntry, ac ApiCriteria) EntryEndpoint {
-	rgn := strings.ToUpper(ac.Region)
-	if (rgn == "") && !ac.IgnoreEnvVars {
-		rgn = os.Getenv("OS_REGION_NAME")
-	}
-
-	for _, entry := range entries {
-		if (ac.Name != "") && (ac.Name != entry.Name) {
-			continue
-		}
-
-		if (ac.Type != "") && (ac.Type != entry.Type) {
-			continue
-		}
-
-		for _, endpoint := range entry.Endpoints {
-			if (rgn != "") && (rgn != strings.ToUpper(endpoint.Region)) {
-				continue
-			}
-
-			if (ac.VersionId != "") && (ac.VersionId != endpoint.VersionId) {
-				continue
-			}
-
-			return endpoint
-		}
-	}
-	return EntryEndpoint{}
-}
diff --git a/service_catalog_test.go b/service_catalog_test.go
deleted file mode 100644
index b78f01f..0000000
--- a/service_catalog_test.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package gophercloud
-
-import (
-	"os"
-	"testing"
-)
-
-// TestFFEBCViaEnvVariable exercises only those calls where a region
-// parameter is required, but is provided by an environment variable.
-func TestFFEBCViaEnvVariable(t *testing.T) {
-	changeRegion("RGN")
-
-	endpoint := FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "", ""),
-		ApiCriteria{Name: "test"},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", ""),
-		ApiCriteria{Name: "test"},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("Regions are case insensitive.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", ""),
-		ApiCriteria{Name: "test", VersionId: "2"},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("Missing version ID means no match.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", "3"),
-		ApiCriteria{Name: "test", VersionId: "2"},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("Mismatched version ID means no match.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", "2"),
-		ApiCriteria{Name: "test", VersionId: "2"},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("All search criteria met; endpoint expected.")
-		return
-	}
-}
-
-// TestFFEBCViaRegionOption exercises only those calls where a region
-// parameter is specified explicitly.  The region option overrides
-// any defined OS_REGION_NAME environment setting.
-func TestFFEBCViaRegionOption(t *testing.T) {
-	changeRegion("Starfleet Command")
-
-	endpoint := FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "", ""),
-		ApiCriteria{Name: "test", Region: "RGN"},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("If provided, the Region qualifier must exclude endpoints with missing or mismatching regions.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", ""),
-		ApiCriteria{Name: "test", Region: "RGN"},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("Regions are case insensitive.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", ""),
-		ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("Missing version ID means no match.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", "3"),
-		ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("Mismatched version ID means no match.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "rgn", "2"),
-		ApiCriteria{Name: "test", Region: "RGN", VersionId: "2"},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("All search criteria met; endpoint expected.")
-		return
-	}
-}
-
-// TestFFEBCWithoutRegion exercises only those calls where a region
-// is irrelevant.  Just to make sure, though, we enforce Gophercloud
-// from paying any attention to OS_REGION_NAME if it happens to be set.
-func TestFindFirstEndpointByCriteria(t *testing.T) {
-	endpoint := FindFirstEndpointByCriteria([]CatalogEntry{}, ApiCriteria{Name: "test", IgnoreEnvVars: true})
-	if endpoint.PublicURL != "" {
-		t.Error("Not expecting to find anything in an empty service catalog.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		[]CatalogEntry{
-			{Name: "test"},
-		},
-		ApiCriteria{Name: "test", IgnoreEnvVars: true},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("Even though we have a matching entry, no endpoints exist")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "", ""),
-		ApiCriteria{Name: "test", IgnoreEnvVars: true},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("Looking for an endpoint by name but without region or version ID should match first entry endpoint.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "", ""),
-		ApiCriteria{Type: "compute", IgnoreEnvVars: true},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("Looking for an endpoint by type but without region or version ID should match first entry endpoint.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "", ""),
-		ApiCriteria{Type: "identity", IgnoreEnvVars: true},
-	)
-	if endpoint.PublicURL != "" {
-		t.Error("Returned mismatched type.")
-		return
-	}
-
-	endpoint = FindFirstEndpointByCriteria(
-		catalog("test", "compute", "http://localhost", "ord", "2"),
-		ApiCriteria{Name: "test", VersionId: "2", IgnoreEnvVars: true},
-	)
-	if endpoint.PublicURL != "http://localhost" {
-		t.Error("Sometimes, you might not care what region your stuff is in.")
-		return
-	}
-}
-
-func catalog(name, entry_type, url, region, version string) []CatalogEntry {
-	return []CatalogEntry{
-		{
-			Name: name,
-			Type: entry_type,
-			Endpoints: []EntryEndpoint{
-				{
-					PublicURL: url,
-					Region:    region,
-					VersionId: version,
-				},
-			},
-		},
-	}
-}
-
-func changeRegion(r string) {
-	err := os.Setenv("OS_REGION_NAME", r)
-	if err != nil {
-		panic(err)
-	}
-}
diff --git a/service_client.go b/service_client.go
new file mode 100644
index 0000000..3490da0
--- /dev/null
+++ b/service_client.go
@@ -0,0 +1,32 @@
+package gophercloud
+
+import "strings"
+
+// ServiceClient stores details required to interact with a specific service API implemented by a provider.
+// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient.
+type ServiceClient struct {
+	// ProviderClient is a reference to the provider that implements this service.
+	*ProviderClient
+
+	// 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.ResourceBaseURL() + strings.Join(parts, "/")
+}
diff --git a/service_client_test.go b/service_client_test.go
new file mode 100644
index 0000000..84beb3f
--- /dev/null
+++ b/service_client_test.go
@@ -0,0 +1,14 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceURL(t *testing.T) {
+	c := &ServiceClient{Endpoint: "http://123.45.67.8/"}
+	expected := "http://123.45.67.8/more/parts/here"
+	actual := c.ServiceURL("more", "parts", "here")
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/testhelper/client/fake.go b/testhelper/client/fake.go
new file mode 100644
index 0000000..5b69b05
--- /dev/null
+++ b/testhelper/client/fake.go
@@ -0,0 +1,17 @@
+package client
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+// Fake token to use.
+const TokenID = "cbc36478b0bd8e67e89469c7749d4127"
+
+// ServiceClient returns a generic service client for use in tests.
+func ServiceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{TokenID: TokenID},
+		Endpoint:       testhelper.Endpoint(),
+	}
+}
diff --git a/testhelper/convenience.go b/testhelper/convenience.go
new file mode 100644
index 0000000..cf33e1a
--- /dev/null
+++ b/testhelper/convenience.go
@@ -0,0 +1,329 @@
+package testhelper
+
+import (
+	"encoding/json"
+	"fmt"
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+const (
+	logBodyFmt = "\033[1;31m%s %s\033[0m"
+	greenCode  = "\033[0m\033[1;32m"
+	yellowCode = "\033[0m\033[1;33m"
+	resetCode  = "\033[0m\033[1;31m"
+)
+
+func prefix(depth int) string {
+	_, file, line, _ := runtime.Caller(depth)
+	return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
+}
+
+func green(str interface{}) string {
+	return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode)
+}
+
+func yellow(str interface{}) string {
+	return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode)
+}
+
+func logFatal(t *testing.T, str string) {
+	t.Fatalf(logBodyFmt, prefix(3), str)
+}
+
+func logError(t *testing.T, str string) {
+	t.Errorf(logBodyFmt, prefix(3), str)
+}
+
+type diffLogger func([]string, interface{}, interface{})
+
+type visit struct {
+	a1  uintptr
+	a2  uintptr
+	typ reflect.Type
+}
+
+// Recursively visits the structures of "expected" and "actual". The diffLogger function will be
+// invoked with each different value encountered, including the reference path that was followed
+// to get there.
+func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) {
+	defer func() {
+		// Fall back to the regular reflect.DeepEquals function.
+		if r := recover(); r != nil {
+			var e, a interface{}
+			if expected.IsValid() {
+				e = expected.Interface()
+			}
+			if actual.IsValid() {
+				a = actual.Interface()
+			}
+
+			if !reflect.DeepEqual(e, a) {
+				logDifference(path, e, a)
+			}
+		}
+	}()
+
+	if !expected.IsValid() && actual.IsValid() {
+		logDifference(path, nil, actual.Interface())
+		return
+	}
+	if expected.IsValid() && !actual.IsValid() {
+		logDifference(path, expected.Interface(), nil)
+		return
+	}
+	if !expected.IsValid() && !actual.IsValid() {
+		return
+	}
+
+	hard := func(k reflect.Kind) bool {
+		switch k {
+		case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
+			return true
+		}
+		return false
+	}
+
+	if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) {
+		addr1 := expected.UnsafeAddr()
+		addr2 := actual.UnsafeAddr()
+
+		if addr1 > addr2 {
+			addr1, addr2 = addr2, addr1
+		}
+
+		if addr1 == addr2 {
+			// References are identical. We can short-circuit
+			return
+		}
+
+		typ := expected.Type()
+		v := visit{addr1, addr2, typ}
+		if visited[v] {
+			// Already visited.
+			return
+		}
+
+		// Remember this visit for later.
+		visited[v] = true
+	}
+
+	switch expected.Kind() {
+	case reflect.Array:
+		for i := 0; i < expected.Len(); i++ {
+			hop := append(path, fmt.Sprintf("[%d]", i))
+			deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
+		}
+		return
+	case reflect.Slice:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+			return
+		}
+		if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
+			return
+		}
+		for i := 0; i < expected.Len(); i++ {
+			hop := append(path, fmt.Sprintf("[%d]", i))
+			deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
+		}
+		return
+	case reflect.Interface:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+			return
+		}
+		deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
+		return
+	case reflect.Ptr:
+		deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
+		return
+	case reflect.Struct:
+		for i, n := 0, expected.NumField(); i < n; i++ {
+			field := expected.Type().Field(i)
+			hop := append(path, "."+field.Name)
+			deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference)
+		}
+		return
+	case reflect.Map:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+			return
+		}
+		if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
+			return
+		}
+
+		var keys []reflect.Value
+		if expected.Len() >= actual.Len() {
+			keys = expected.MapKeys()
+		} else {
+			keys = actual.MapKeys()
+		}
+
+		for _, k := range keys {
+			expectedValue := expected.MapIndex(k)
+			actualValue := expected.MapIndex(k)
+
+			if !expectedValue.IsValid() {
+				logDifference(path, nil, actual.Interface())
+				return
+			}
+			if !actualValue.IsValid() {
+				logDifference(path, expected.Interface(), nil)
+				return
+			}
+
+			hop := append(path, fmt.Sprintf("[%v]", k))
+			deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference)
+		}
+		return
+	case reflect.Func:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+		}
+		return
+	default:
+		if expected.Interface() != actual.Interface() {
+			logDifference(path, expected.Interface(), actual.Interface())
+		}
+	}
+}
+
+func deepDiff(expected, actual interface{}, logDifference diffLogger) {
+	if expected == nil || actual == nil {
+		logDifference([]string{}, expected, actual)
+		return
+	}
+
+	expectedValue := reflect.ValueOf(expected)
+	actualValue := reflect.ValueOf(actual)
+
+	if expectedValue.Type() != actualValue.Type() {
+		logDifference([]string{}, expected, actual)
+		return
+	}
+	deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference)
+}
+
+// AssertEquals compares two arbitrary values and performs a comparison. If the
+// comparison fails, a fatal error is raised that will fail the test
+func AssertEquals(t *testing.T, expected, actual interface{}) {
+	if expected != actual {
+		logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+	}
+}
+
+// CheckEquals is similar to AssertEquals, except with a non-fatal error
+func CheckEquals(t *testing.T, expected, actual interface{}) {
+	if expected != actual {
+		logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+	}
+}
+
+// AssertDeepEquals - like Equals - performs a comparison - but on more complex
+// structures that requires deeper inspection
+func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
+	pre := prefix(2)
+
+	differed := false
+	deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
+		differed = true
+		t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m",
+			pre,
+			strings.Join(path, ""),
+			green(expected),
+			yellow(actual))
+	})
+	if differed {
+		logFatal(t, "The structures were different.")
+	}
+}
+
+// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error
+func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
+	pre := prefix(2)
+
+	deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
+		t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m",
+			pre,
+			strings.Join(path, ""),
+			green(expected),
+			yellow(actual))
+	})
+}
+
+// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and
+// CheckJSONEquals.
+func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool {
+	var parsedExpected, parsedActual interface{}
+	err := json.Unmarshal([]byte(expectedJSON), &parsedExpected)
+	if err != nil {
+		t.Errorf("Unable to parse expected value as JSON: %v", err)
+		return false
+	}
+
+	jsonActual, err := json.Marshal(actual)
+	AssertNoErr(t, err)
+	err = json.Unmarshal(jsonActual, &parsedActual)
+	AssertNoErr(t, err)
+
+	if !reflect.DeepEqual(parsedExpected, parsedActual) {
+		prettyExpected, err := json.MarshalIndent(parsedExpected, "", "  ")
+		if err != nil {
+			t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON)
+		} else {
+			// We can't use green() here because %#v prints prettyExpected as a byte array literal, which
+			// is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason.
+			t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode)
+		}
+
+		prettyActual, err := json.MarshalIndent(actual, "", "  ")
+		if err != nil {
+			t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual)
+		} else {
+			// We can't use yellow() for the same reason.
+			t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode)
+		}
+
+		return false
+	}
+	return true
+}
+
+// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that
+// both are consistent. If they aren't, the expected and actual structures are pretty-printed and
+// shown for comparison.
+//
+// This is useful for comparing structures that are built as nested map[string]interface{} values,
+// which are a pain to construct as literals.
+func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) {
+	if !isJSONEquals(t, expectedJSON, actual) {
+		logFatal(t, "The generated JSON structure differed.")
+	}
+}
+
+// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal.
+func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) {
+	if !isJSONEquals(t, expectedJSON, actual) {
+		logError(t, "The generated JSON structure differed.")
+	}
+}
+
+// AssertNoErr is a convenience function for checking whether an error value is
+// an actual error
+func AssertNoErr(t *testing.T, e error) {
+	if e != nil {
+		logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
+	}
+}
+
+// CheckNoErr is similar to AssertNoErr, except with a non-fatal error
+func CheckNoErr(t *testing.T, e error) {
+	if e != nil {
+		logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
+	}
+}
diff --git a/testhelper/doc.go b/testhelper/doc.go
new file mode 100644
index 0000000..25b4dfe
--- /dev/null
+++ b/testhelper/doc.go
@@ -0,0 +1,4 @@
+/*
+Package testhelper container methods that are useful for writing unit tests.
+*/
+package testhelper
diff --git a/testhelper/http_responses.go b/testhelper/http_responses.go
new file mode 100644
index 0000000..e1f1f9a
--- /dev/null
+++ b/testhelper/http_responses.go
@@ -0,0 +1,91 @@
+package testhelper
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"testing"
+)
+
+var (
+	// Mux is a multiplexer that can be used to register handlers.
+	Mux *http.ServeMux
+
+	// Server is an in-memory HTTP server for testing.
+	Server *httptest.Server
+)
+
+// SetupHTTP prepares the Mux and Server.
+func SetupHTTP() {
+	Mux = http.NewServeMux()
+	Server = httptest.NewServer(Mux)
+}
+
+// TeardownHTTP releases HTTP-related resources.
+func TeardownHTTP() {
+	Server.Close()
+}
+
+// Endpoint returns a fake endpoint that will actually target the Mux.
+func Endpoint() string {
+	return Server.URL + "/"
+}
+
+// TestFormValues ensures that all the URL parameters given to the http.Request are the same as values.
+func TestFormValues(t *testing.T, r *http.Request, values map[string]string) {
+	want := url.Values{}
+	for k, v := range values {
+		want.Add(k, v)
+	}
+
+	r.ParseForm()
+	if !reflect.DeepEqual(want, r.Form) {
+		t.Errorf("Request parameters = %v, want %v", r.Form, want)
+	}
+}
+
+// TestMethod checks that the Request has the expected method (e.g. GET, POST).
+func TestMethod(t *testing.T, r *http.Request, expected string) {
+	if expected != r.Method {
+		t.Errorf("Request method = %v, expected %v", r.Method, expected)
+	}
+}
+
+// TestHeader checks that the header on the http.Request matches the expected value.
+func TestHeader(t *testing.T, r *http.Request, header string, expected string) {
+	if actual := r.Header.Get(header); expected != actual {
+		t.Errorf("Header %s = %s, expected %s", header, actual, expected)
+	}
+}
+
+// TestBody verifies that the request body matches an expected body.
+func TestBody(t *testing.T, r *http.Request, expected string) {
+	b, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		t.Errorf("Unable to read body: %v", err)
+	}
+	str := string(b)
+	if expected != str {
+		t.Errorf("Body = %s, expected %s", str, expected)
+	}
+}
+
+// TestJSONRequest verifies that the JSON payload of a request matches an expected structure, without asserting things about
+// whitespace or ordering.
+func TestJSONRequest(t *testing.T, r *http.Request, expected string) {
+	b, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		t.Errorf("Unable to read request body: %v", err)
+	}
+
+	var actualJSON interface{}
+	err = json.Unmarshal(b, &actualJSON)
+	if err != nil {
+		t.Errorf("Unable to parse request body as JSON: %v", err)
+	}
+
+	CheckJSONEquals(t, expected, actualJSON)
+}
diff --git a/transport_double_test.go b/transport_double_test.go
deleted file mode 100644
index ef7f19a..0000000
--- a/transport_double_test.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package gophercloud
-
-import (
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"strings"
-	"testing"
-)
-
-type transport struct {
-	called         int
-	response       string
-	expectTenantId bool
-	tenantIdFound  bool
-	status         int
-}
-
-func (t *transport) RoundTrip(req *http.Request) (rsp *http.Response, err error) {
-	var authContainer *AuthContainer
-
-	t.called++
-
-	headers := make(http.Header)
-	headers.Add("Content-Type", "application/xml; charset=UTF-8")
-
-	body := ioutil.NopCloser(strings.NewReader(t.response))
-
-	if t.status == 0 {
-		t.status = 200
-	}
-	statusMsg := "OK"
-	if (t.status < 200) || (299 < t.status) {
-		statusMsg = "Error"
-	}
-
-	rsp = &http.Response{
-		Status:           fmt.Sprintf("%d %s", t.status, statusMsg),
-		StatusCode:       t.status,
-		Proto:            "HTTP/1.1",
-		ProtoMajor:       1,
-		ProtoMinor:       1,
-		Header:           headers,
-		Body:             body,
-		ContentLength:    -1,
-		TransferEncoding: nil,
-		Close:            true,
-		Trailer:          nil,
-		Request:          req,
-	}
-
-	bytes, err := ioutil.ReadAll(req.Body)
-	if err != nil {
-		return nil, err
-	}
-	err = json.Unmarshal(bytes, &authContainer)
-	if err != nil {
-		return nil, err
-	}
-	t.tenantIdFound = (authContainer.Auth.TenantId != "")
-
-	if t.tenantIdFound != t.expectTenantId {
-		rsp.Status = "500 Internal Server Error"
-		rsp.StatusCode = 500
-	}
-	return
-}
-
-func newTransport() *transport {
-	return &transport{}
-}
-
-func (t *transport) IgnoreTenantId() *transport {
-	t.expectTenantId = false
-	return t
-}
-
-func (t *transport) ExpectTenantId() *transport {
-	t.expectTenantId = true
-	return t
-}
-
-func (t *transport) WithResponse(r string) *transport {
-	t.response = r
-	t.status = 200
-	return t
-}
-
-func (t *transport) WithError(code int) *transport {
-	t.response = fmt.Sprintf("Error %d", code)
-	t.status = code
-	return t
-}
-
-func (t *transport) VerifyCalls(test *testing.T, n int) error {
-	if t.called != n {
-		err := fmt.Errorf("Expected Transport to be called %d times; found %d instead", n, t.called)
-		test.Error(err)
-		return err
-	}
-	return nil
-}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..7f5ead7
--- /dev/null
+++ b/util.go
@@ -0,0 +1,53 @@
+package gophercloud
+
+import (
+	"errors"
+	"strings"
+	"time"
+)
+
+// WaitFor polls a predicate function, once per second, up to a timeout limit.
+// It usually does this to wait for the resource to transition to a certain state.
+func WaitFor(timeout int, predicate func() (bool, error)) error {
+	start := time.Now().Second()
+	for {
+		// Force a 1s sleep
+		time.Sleep(1 * time.Second)
+
+		// If a timeout is set, and that's been exceeded, shut it down
+		if timeout >= 0 && time.Now().Second()-start >= timeout {
+			return errors.New("A timeout occurred")
+		}
+
+		// Execute the function
+		satisfied, err := predicate()
+		if err != nil {
+			return err
+		}
+		if satisfied {
+			return nil
+		}
+	}
+}
+
+// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+func NormalizeURL(url string) string {
+	if !strings.HasSuffix(url, "/") {
+		return url + "/"
+	}
+	return url
+}
+
+// BuildQuery constructs the query section of a URI from a map.
+func BuildQuery(params map[string]string) string {
+	if len(params) == 0 {
+		return ""
+	}
+
+	query := "?"
+	for k, v := range params {
+		query += k + "=" + v + "&"
+	}
+	query = query[:len(query)-1]
+	return query
+}
diff --git a/util_test.go b/util_test.go
new file mode 100644
index 0000000..6fbd920
--- /dev/null
+++ b/util_test.go
@@ -0,0 +1,21 @@
+package gophercloud
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitFor(t *testing.T) {
+	err := WaitFor(0, func() (bool, error) {
+		return true, nil
+	})
+	if err == nil {
+		t.Errorf("Expected error: 'Time out in WaitFor'")
+	}
+
+	err = WaitFor(5, func() (bool, error) {
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}