rackspace/gophercloud repo
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..325b90a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: go
+install:
+  - go get -v -tags 'fixtures acceptance' ./...
+go:
+  - 1.2
+  - 1.3
+  - 1.4
+  - 1.5
+script: script/cibuild
+after_success:
+  - go get golang.org/x/tools/cmd/cover
+  - go get github.com/axw/gocov/gocov
+  - go get github.com/mattn/goveralls
+  - export PATH=$PATH:$HOME/gopath/bin/
+  - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
+sudo: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6ba5beb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,274 @@
+# 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](/README.md#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:
+
+   ```bash
+   git checkout master
+   ```
+
+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](https://developer.rackspace.com/support/) 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
new file mode 100644
index 0000000..63beb30
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,13 @@
+Contributors
+============
+
+| 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>
+| Joe Topjian | <joe@topjian.net>
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fbbbc9e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,191 @@
+Copyright 2012-2013 Rackspace, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License.  You may obtain a copy of the
+License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations under the License.                                
+
+------
+ 
+				Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md
index a3e0070..0a0da59 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,160 @@
-# Gophercloud
-A Go SDK for interacting with OpenStack
+# Gophercloud: an OpenStack SDK for Go
+[![Build Status](https://travis-ci.org/rackspace/gophercloud.svg?branch=master)](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
+```
+
+To protect yourself against changes in your dependencies, we highly recommend choosing a
+[dependency management solution](https://github.com/golang/go/wiki/PackageManagementTools) for
+your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install
+Gophercloud as a dependency like so:
+
+```bash
+go get github.com/rackspace/gophercloud
+
+# Edit your code to import relevant packages from "github.com/rackspace/gophercloud"
+
+godep save ./...
+```
+
+This will install all the source files you need into a `Godeps/_workspace` directory, which is
+referenceable from your own source files when you use the `godep go` command.
+
+## 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
+[contact us](https://developer.rackspace.com/support/). 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 [contact us directly](https://developer.rackspace.com/support/).
diff --git a/UPGRADING.md b/UPGRADING.md
new file mode 100644
index 0000000..76a94d5
--- /dev/null
+++ b/UPGRADING.md
@@ -0,0 +1,338 @@
+# Upgrading to v1.0.0
+
+With the arrival of this new major version increment, the unfortunate news is
+that breaking changes have been introduced to existing services. The API
+has been completely rewritten from the ground up to make the library more
+extensible, maintainable and easy-to-use.
+
+Below we've compiled upgrade instructions for the various services that
+existed before. If you have a specific issue that is not addressed below,
+please [submit an issue](/issues/new) or
+[e-mail our support team](https://developer.rackspace.com/support/).
+
+* [Authentication](#authentication)
+* [Servers](#servers)
+  * [List servers](#list-servers)
+  * [Get server details](#get-server-details)
+  * [Create server](#create-server)
+  * [Resize server](#resize-server)
+  * [Reboot server](#reboot-server)
+  * [Update server](#update-server)
+  * [Rebuild server](#rebuild-server)
+  * [Change admin password](#change-admin-password)
+  * [Delete server](#delete-server)
+  * [Rescue server](#rescue-server)
+* [Images and flavors](#images-and-flavors)
+  * [List images](#list-images)
+  * [List flavors](#list-flavors)
+  * [Create/delete image](#createdelete-image)
+* [Other](#other)
+  * [List keypairs](#list-keypairs)
+  * [Create/delete keypair](#createdelete-keypair)
+  * [List IP addresses](#list-ip-addresses)
+
+# Authentication
+
+One of the major differences that this release introduces is the level of
+sub-packaging to differentiate between services and providers. You now have
+the option of authenticating with OpenStack and other providers (like Rackspace).
+
+To authenticate with a vanilla OpenStack installation, you can either specify
+your credentials like this:
+
+```go
+import (
+  "github.com/rackspace/gophercloud"
+  "github.com/rackspace/gophercloud/openstack"
+)
+
+opts := gophercloud.AuthOptions{
+  IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
+  Username: "{username}",
+  Password: "{password}",
+  TenantID: "{tenant_id}",
+}
+```
+
+Or have them pulled in through environment variables, like this:
+
+```go
+opts, err := openstack.AuthOptionsFromEnv()
+```
+
+Once you have your `AuthOptions` struct, you pass it in to get back a `Provider`,
+like so:
+
+```go
+provider, err := openstack.AuthenticatedClient(opts)
+```
+
+This provider is the top-level structure that all services are created from.
+
+# Servers
+
+Before you can interact with the Compute API, you need to retrieve a
+`gophercloud.ServiceClient`. To do this:
+
+```go
+// Define your region, etc.
+opts := gophercloud.EndpointOpts{Region: "RegionOne"}
+
+client, err := openstack.NewComputeV2(provider, opts)
+```
+
+## List servers
+
+All operations that involve API collections (servers, flavors, images) now use
+the `pagination.Pager` interface. This interface represents paginated entities
+that can be iterated over.
+
+Once you have a Pager, you can then pass a callback function into its `EachPage`
+method, and this will allow you to traverse over the collection and execute
+arbitrary functionality. So, an example with list servers:
+
+```go
+import (
+  "fmt"
+  "github.com/rackspace/gophercloud/pagination"
+  "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// We have the option of filtering the server list. If we want the full
+// collection, leave it as an empty struct or nil
+opts := servers.ListOpts{Name: "server_1"}
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := servers.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+  serverList, err := servers.ExtractServers(page)
+
+  // `s' will be a servers.Server struct
+  for _, s := range serverList {
+    fmt.Printf("We have a server. ID=%s, Name=%s", s.ID, s.Name)
+  }
+})
+```
+
+## Get server details
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+// Get the HTTP result
+response := servers.Get(client, "server_id")
+
+// Extract a Server struct from the response
+server, err := response.Extract()
+```
+
+## Create server
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+// Define our options
+opts := servers.CreateOpts{
+  Name: "new_server",
+  FlavorRef: "flavorID",
+  ImageRef: "imageID",
+}
+
+// Get our response
+response := servers.Create(client, opts)
+
+// Extract
+server, err := response.Extract()
+```
+
+## Change admin password
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+result := servers.ChangeAdminPassword(client, "server_id", "newPassword_&123")
+```
+
+## Resize server
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+result := servers.Resize(client, "server_id", "new_flavor_id")
+```
+
+## Reboot server
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+// You have a choice of two reboot methods: servers.SoftReboot or servers.HardReboot
+result := servers.Reboot(client, "server_id", servers.SoftReboot)
+```
+
+## Update server
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+opts := servers.UpdateOpts{Name: "new_name"}
+
+server, err := servers.Update(client, "server_id", opts).Extract()
+```
+
+## Rebuild server
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+// You have the option of specifying additional options
+opts := RebuildOpts{
+  Name:      "new_name",
+  AdminPass: "admin_password",
+  ImageID:   "image_id",
+  Metadata:  map[string]string{"owner": "me"},
+}
+
+result := servers.Rebuild(client, "server_id", opts)
+
+// You can extract a servers.Server struct from the HTTP response
+server, err := result.Extract()
+```
+
+## Delete server
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+
+response := servers.Delete(client, "server_id")
+```
+
+## Rescue server
+
+The server rescue extension for Compute is not currently supported.
+
+# Images and flavors
+
+## List images
+
+As with listing servers (see above), you first retrieve a Pager, and then pass
+in a callback over each page:
+
+```go
+import (
+  "github.com/rackspace/gophercloud/pagination"
+  "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+)
+
+// We have the option of filtering the image list. If we want the full
+// collection, leave it as an empty struct
+opts := images.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", Name: "Ubuntu 12.04"}
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := images.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+  imageList, err := images.ExtractImages(page)
+
+  for _, i := range imageList {
+    // "i" will be an images.Image
+  }
+})
+```
+
+## List flavors
+
+```go
+import (
+  "github.com/rackspace/gophercloud/pagination"
+  "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+)
+
+// We have the option of filtering the flavor list. If we want the full
+// collection, leave it as an empty struct
+opts := flavors.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", MinRAM: 4}
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := flavors.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+  flavorList, err := networks.ExtractFlavors(page)
+
+  for _, f := range flavorList {
+    // "f" will be a flavors.Flavor
+  }
+})
+```
+
+## Create/delete image
+
+Image management has been shifted to Glance, but unfortunately this service is
+not supported as of yet. You can, however, list Compute images like so:
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := images.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+  imageList, err := images.ExtractImages(page)
+
+  for _, i := range imageList {
+    // "i" will be an images.Image
+  }
+})
+```
+
+# Other
+
+## List keypairs
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := keypairs.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+  keyList, err := keypairs.ExtractKeyPairs(page)
+
+  for _, k := range keyList {
+    // "k" will be a keypairs.KeyPair
+  }
+})
+```
+
+## Create/delete keypairs
+
+To create a new keypair, you need to specify its name and, optionally, a
+pregenerated OpenSSH-formatted public key.
+
+```go
+import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+
+opts := keypairs.CreateOpts{
+  Name: "new_key",
+  PublicKey: "...",
+}
+
+response := keypairs.Create(client, opts)
+
+key, err := response.Extract()
+```
+
+To delete an existing keypair:
+
+```go
+response := keypairs.Delete(client, "keypair_id")
+```
+
+## List IP addresses
+
+This operation is not currently supported.
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/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..7741aa9
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,70 @@
+// +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"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSnapshots(t *testing.T) {
+
+	client, err := newClient(t)
+	th.AssertNoErr(t, err)
+
+	v, err := volumes.Create(client, &volumes.CreateOpts{
+		Name: "gophercloud-test-volume",
+		Size: 1,
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	err = volumes.WaitForStatus(client, v.ID, "available", 120)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created volume: %v\n", v)
+
+	ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+		Name:     "gophercloud-test-snapshot",
+		VolumeID: v.ID,
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created snapshot: %+v\n", ss)
+
+	err = snapshots.Delete(client, ss.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := snapshots.Get(client, ss.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+
+	t.Log("Deleted snapshot\n")
+
+	err = volumes.Delete(client, v.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := volumes.Get(client, v.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	th.AssertNoErr(t, 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..7760427
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,63 @@
+// +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"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := openstack.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func TestVolumes(t *testing.T) {
+	client, err := newClient(t)
+	th.AssertNoErr(t, err)
+
+	cv, err := volumes.Create(client, &volumes.CreateOpts{
+		Size: 1,
+		Name: "gophercloud-test-volume",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+		th.AssertNoErr(t, err)
+		err = volumes.Delete(client, cv.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+	}()
+
+	_, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+		Name: "gophercloud-updated-volume",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	v, err := volumes.Get(client, cv.ID).Extract()
+	th.AssertNoErr(t, err)
+	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)
+		th.CheckEquals(t, 1, len(vols))
+		return true, err
+	})
+	th.AssertNoErr(t, 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..000bc01
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,49 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVolumeTypes(t *testing.T) {
+	client, err := newClient(t)
+	th.AssertNoErr(t, err)
+
+	vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+		ExtraSpecs: map[string]interface{}{
+			"capabilities": "gpu",
+			"priority":     3,
+		},
+		Name: "gophercloud-test-volumeType",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		time.Sleep(10000 * time.Millisecond)
+		err = volumetypes.Delete(client, vt.ID).ExtractErr()
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+	t.Logf("Created volume type: %+v\n", vt)
+
+	vt, err = volumetypes.Get(client, vt.ID).Extract()
+	th.AssertNoErr(t, err)
+	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
+	})
+	th.AssertNoErr(t, 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..add0e5f
--- /dev/null
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -0,0 +1,55 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+	}
+	server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
+		serverCreateOpts,
+		bd,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("Created server: %+v\n", server)
+	defer servers.Delete(client, server.ID)
+	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..c1bbf79
--- /dev/null
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -0,0 +1,104 @@
+// +build acceptance common
+
+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
+
+	// NetworkName is the name of a network to launch the instance on.
+	NetworkName 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")
+	networkName := os.Getenv("OS_NETWORK_NAME")
+
+	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")
+	}
+	if networkName == "" {
+		networkName = "public"
+	}
+
+	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, NetworkName: networkName}, 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/floatingip_test.go b/acceptance/openstack/compute/v2/floatingip_test.go
new file mode 100644
index 0000000..de6efc9
--- /dev/null
+++ b/acceptance/openstack/compute/v2/floatingip_test.go
@@ -0,0 +1,168 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createFIPServer(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.")
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err := servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		AdminPass: pwd,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	th.AssertEquals(t, pwd, server.AdminPass)
+
+	return server, err
+}
+
+func createFloatingIP(t *testing.T, client *gophercloud.ServiceClient) (*floatingip.FloatingIP, error) {
+	pool := os.Getenv("OS_POOL_NAME")
+	fip, err := floatingip.Create(client, &floatingip.CreateOpts{
+		Pool: pool,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Obtained Floating IP: %v", fip.IP)
+
+	return fip, err
+}
+
+func associateFloatingIPDeprecated(t *testing.T, client *gophercloud.ServiceClient, serverId string, fip *floatingip.FloatingIP) {
+	// This form works, but is considered deprecated.
+	// See associateFloatingIP or associateFloatingIPFixed
+	err := floatingip.Associate(client, serverId, fip.IP).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Associated floating IP %v from instance %v", fip.IP, serverId)
+	defer func() {
+		err = floatingip.Disassociate(client, serverId, fip.IP).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Disassociated floating IP %v from instance %v", fip.IP, serverId)
+	}()
+	floatingIp, err := floatingip.Get(client, fip.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Floating IP %v is associated with Fixed IP %v", fip.IP, floatingIp.FixedIP)
+}
+
+func associateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, serverId string, fip *floatingip.FloatingIP) {
+	associateOpts := floatingip.AssociateOpts{
+		ServerID:   serverId,
+		FloatingIP: fip.IP,
+	}
+
+	err := floatingip.AssociateInstance(client, associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Associated floating IP %v from instance %v", fip.IP, serverId)
+	defer func() {
+		err = floatingip.DisassociateInstance(client, associateOpts).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Disassociated floating IP %v from instance %v", fip.IP, serverId)
+	}()
+	floatingIp, err := floatingip.Get(client, fip.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Floating IP %v is associated with Fixed IP %v", fip.IP, floatingIp.FixedIP)
+}
+
+func associateFloatingIPFixed(t *testing.T, client *gophercloud.ServiceClient, serverId string, fip *floatingip.FloatingIP) {
+
+	network := os.Getenv("OS_NETWORK_NAME")
+	server, err := servers.Get(client, serverId).Extract()
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	var fixedIP string
+	for _, networkAddresses := range server.Addresses[network].([]interface{}) {
+		address := networkAddresses.(map[string]interface{})
+		if address["OS-EXT-IPS:type"] == "fixed" {
+			if address["version"].(float64) == 4 {
+				fixedIP = address["addr"].(string)
+			}
+		}
+	}
+
+	associateOpts := floatingip.AssociateOpts{
+		ServerID:   serverId,
+		FloatingIP: fip.IP,
+		FixedIP:    fixedIP,
+	}
+
+	err = floatingip.AssociateInstance(client, associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Associated floating IP %v from instance %v with Fixed IP %v", fip.IP, serverId, fixedIP)
+	defer func() {
+		err = floatingip.DisassociateInstance(client, associateOpts).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Disassociated floating IP %v from instance %v with Fixed IP %v", fip.IP, serverId, fixedIP)
+	}()
+	floatingIp, err := floatingip.Get(client, fip.ID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, floatingIp.FixedIP, fixedIP)
+	t.Logf("Floating IP %v is associated with Fixed IP %v", fip.IP, floatingIp.FixedIP)
+}
+
+func TestFloatingIP(t *testing.T) {
+	pool := os.Getenv("OS_POOL_NAME")
+	if pool == "" {
+		t.Fatalf("OS_POOL_NAME must be set")
+	}
+
+	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 := createFIPServer(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)
+	}
+
+	fip, err := createFloatingIP(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create floating IP: %v", err)
+	}
+	defer func() {
+		err = floatingip.Delete(client, fip.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Floating IP deleted.")
+	}()
+
+	associateFloatingIPDeprecated(t, client, server.ID, fip)
+	associateFloatingIP(t, client, server.ID, fip)
+	associateFloatingIPFixed(t, client, server.ID, fip)
+
+}
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/keypairs_test.go b/acceptance/openstack/compute/v2/keypairs_test.go
new file mode 100644
index 0000000..a4fe8db
--- /dev/null
+++ b/acceptance/openstack/compute/v2/keypairs_test.go
@@ -0,0 +1,74 @@
+// +build acceptance
+
+package v2
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+
+	"golang.org/x/crypto/ssh"
+)
+
+const keyName = "gophercloud_test_key_pair"
+
+func TestCreateServerWithKeyPair(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	publicKey := privateKey.PublicKey
+	pub, err := ssh.NewPublicKey(&publicKey)
+	th.AssertNoErr(t, err)
+	pubBytes := ssh.MarshalAuthorizedKey(pub)
+	pk := string(pubBytes)
+
+	kp, err := keypairs.Create(client, keypairs.CreateOpts{
+		Name:      keyName,
+		PublicKey: pk,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created key pair: %s\n", kp)
+
+	choices, err := ComputeChoicesFromEnv()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("Gophercloud-", 8)
+	t.Logf("Creating server [%s] with key pair.", name)
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+	}
+
+	server, err := servers.Create(client, keypairs.CreateOptsExt{
+		serverCreateOpts,
+		keyName,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer servers.Delete(client, server.ID)
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+
+	server, err = servers.Get(client, server.ID).Extract()
+	t.Logf("Created server: %+v\n", server)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, server.KeyName, keyName)
+
+	t.Logf("Deleting key pair [%s]...", kp.Name)
+	err = keypairs.Delete(client, keyName).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleting server [%s]...", name)
+}
diff --git a/acceptance/openstack/compute/v2/network_test.go b/acceptance/openstack/compute/v2/network_test.go
new file mode 100644
index 0000000..7ebe7ec
--- /dev/null
+++ b/acceptance/openstack/compute/v2/network_test.go
@@ -0,0 +1,78 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/networks"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func getNetworkIDFromNetworkExtension(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) {
+	allPages, err := networks.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	networkList, err := networks.ExtractNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	networkID := ""
+	for _, network := range networkList {
+		t.Logf("Network: %v", network)
+		if network.Label == networkName {
+			networkID = network.ID
+		}
+	}
+
+	t.Logf("Found network ID for %s: %s\n", networkName, networkID)
+
+	return networkID, nil
+}
+
+func TestNetworks(t *testing.T) {
+	networkName := os.Getenv("OS_NETWORK_NAME")
+	if networkName == "" {
+		t.Fatalf("OS_NETWORK_NAME must be set")
+	}
+
+	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)
+	}
+
+	networkID, err := getNetworkIDFromNetworkExtension(t, client, networkName)
+	if err != nil {
+		t.Fatalf("Unable to get network ID: %v", err)
+	}
+
+	// createNetworkServer is defined in tenantnetworks_test.go
+	server, err := createNetworkServer(t, client, choices, networkID)
+	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)
+	}
+
+	allPages, err := networks.List(client).AllPages()
+	allNetworks, err := networks.ExtractNetworks(allPages)
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved all %d networks: %+v", len(allNetworks), allNetworks)
+}
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/secdefrules_test.go b/acceptance/openstack/compute/v2/secdefrules_test.go
new file mode 100644
index 0000000..78b0798
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secdefrules_test.go
@@ -0,0 +1,72 @@
+// +build acceptance compute defsecrules
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	dsr "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecDefRules(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	id := createDefRule(t, client)
+
+	listDefRules(t, client)
+
+	getDefRule(t, client, id)
+
+	deleteDefRule(t, client, id)
+}
+
+func createDefRule(t *testing.T, client *gophercloud.ServiceClient) string {
+	opts := dsr.CreateOpts{
+		FromPort:   tools.RandomInt(80, 89),
+		ToPort:     tools.RandomInt(90, 99),
+		IPProtocol: "TCP",
+		CIDR:       "0.0.0.0/0",
+	}
+
+	rule, err := dsr.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created default rule %s", rule.ID)
+
+	return rule.ID
+}
+
+func listDefRules(t *testing.T, client *gophercloud.ServiceClient) {
+	err := dsr.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		drList, err := dsr.ExtractDefaultRules(page)
+		th.AssertNoErr(t, err)
+
+		for _, dr := range drList {
+			t.Logf("Listing default rule %s: Name [%s] From Port [%s] To Port [%s] Protocol [%s]",
+				dr.ID, dr.FromPort, dr.ToPort, dr.IPProtocol)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	rule, err := dsr.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting rule %s: %#v", id, rule)
+}
+
+func deleteDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := dsr.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted rule %s", id)
+}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
new file mode 100644
index 0000000..4f50739
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -0,0 +1,177 @@
+// +build acceptance compute secgroups
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecGroups(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	serverID, needsDeletion := findServer(t, client)
+
+	groupID := createSecGroup(t, client)
+
+	listSecGroups(t, client)
+
+	newName := tools.RandomString("secgroup_", 5)
+	updateSecGroup(t, client, groupID, newName)
+
+	getSecGroup(t, client, groupID)
+
+	addRemoveRules(t, client, groupID)
+
+	addServerToSecGroup(t, client, serverID, newName)
+
+	removeServerFromSecGroup(t, client, serverID, newName)
+
+	if needsDeletion {
+		servers.Delete(client, serverID)
+	}
+
+	deleteSecGroup(t, client, groupID)
+}
+
+func createSecGroup(t *testing.T, client *gophercloud.ServiceClient) string {
+	opts := secgroups.CreateOpts{
+		Name:        tools.RandomString("secgroup_", 5),
+		Description: "something",
+	}
+
+	group, err := secgroups.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created secgroup %s %s", group.ID, group.Name)
+
+	return group.ID
+}
+
+func listSecGroups(t *testing.T, client *gophercloud.ServiceClient) {
+	err := secgroups.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		secGrpList, err := secgroups.ExtractSecurityGroups(page)
+		th.AssertNoErr(t, err)
+
+		for _, sg := range secGrpList {
+			t.Logf("Listing secgroup %s: Name [%s] Desc [%s] TenantID [%s]", sg.ID,
+				sg.Name, sg.Description, sg.TenantID)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updateSecGroup(t *testing.T, client *gophercloud.ServiceClient, id, newName string) {
+	opts := secgroups.UpdateOpts{
+		Name:        newName,
+		Description: tools.RandomString("dec_", 10),
+	}
+	group, err := secgroups.Update(client, id, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated %s's name to %s", group.ID, group.Name)
+}
+
+func getSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	group, err := secgroups.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting %s: %#v", id, group)
+}
+
+func addRemoveRules(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	opts := secgroups.CreateRuleOpts{
+		ParentGroupID: id,
+		FromPort:      22,
+		ToPort:        22,
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := secgroups.CreateRule(client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Adding rule %s to group %s", rule.ID, id)
+
+	err = secgroups.DeleteRule(client, rule.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted rule %s from group %s", rule.ID, id)
+}
+
+func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) {
+	var serverID string
+	var needsDeletion bool
+
+	err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		sList, err := servers.ExtractServers(page)
+		th.AssertNoErr(t, err)
+
+		for _, s := range sList {
+			serverID = s.ID
+			needsDeletion = false
+
+			t.Logf("Found an existing server: ID [%s]", serverID)
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if serverID == "" {
+		t.Log("No server found, creating one")
+
+		choices, err := ComputeChoicesFromEnv()
+		th.AssertNoErr(t, err)
+
+		opts := &servers.CreateOpts{
+			Name:      tools.RandomString("secgroup_test_", 5),
+			ImageRef:  choices.ImageID,
+			FlavorRef: choices.FlavorID,
+		}
+
+		s, err := servers.Create(client, opts).Extract()
+		th.AssertNoErr(t, err)
+		serverID = s.ID
+
+		t.Logf("Created server %s, waiting for it to build", s.ID)
+		err = servers.WaitForStatus(client, serverID, "ACTIVE", 300)
+		th.AssertNoErr(t, err)
+
+		needsDeletion = true
+	}
+
+	return serverID, needsDeletion
+}
+
+func addServerToSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) {
+	err := secgroups.AddServerToGroup(client, serverID, groupName).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Adding group %s to server %s", groupName, serverID)
+}
+
+func removeServerFromSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) {
+	err := secgroups.RemoveServerFromGroup(client, serverID, groupName).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Removing group %s from server %s", groupName, serverID)
+}
+
+func deleteSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := secgroups.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted group %s", id)
+}
diff --git a/acceptance/openstack/compute/v2/servergroup_test.go b/acceptance/openstack/compute/v2/servergroup_test.go
new file mode 100644
index 0000000..945854e
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servergroup_test.go
@@ -0,0 +1,143 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient) (*servergroups.ServerGroup, error) {
+	sg, err := servergroups.Create(computeClient, &servergroups.CreateOpts{
+		Name:     "test",
+		Policies: []string{"affinity"},
+	}).Extract()
+
+	if err != nil {
+		t.Fatalf("Unable to create server group: %v", err)
+	}
+
+	t.Logf("Created server group: %v", sg.ID)
+	t.Logf("It has policies: %v", sg.Policies)
+
+	return sg, nil
+}
+
+func getServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient, sgID string) error {
+	sg, err := servergroups.Get(computeClient, sgID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get server group: %v", err)
+	}
+
+	t.Logf("Got server group: %v", sg.Name)
+
+	return nil
+}
+
+func createServerInGroup(t *testing.T, computeClient *gophercloud.ServiceClient, choices *ComputeChoices, serverGroup *servergroups.ServerGroup) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		AdminPass: pwd,
+	}
+	server, err := servers.Create(computeClient, schedulerhints.CreateOptsExt{
+		serverCreateOpts,
+		schedulerhints.SchedulerHints{
+			Group: serverGroup.ID,
+		},
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	th.AssertEquals(t, pwd, server.AdminPass)
+
+	return server, err
+}
+
+func verifySchedulerWorked(t *testing.T, firstServer, secondServer *servers.Server) error {
+	t.Logf("First server hostID: %v", firstServer.HostID)
+	t.Logf("Second server hostID: %v", secondServer.HostID)
+	if firstServer.HostID == secondServer.HostID {
+		return nil
+	}
+
+	return fmt.Errorf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID)
+}
+
+func TestServerGroups(t *testing.T) {
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	computeClient, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	sg, err := createServerGroup(t, computeClient)
+	if err != nil {
+		t.Fatalf("Unable to create server group: %v", err)
+	}
+	defer func() {
+		servergroups.Delete(computeClient, sg.ID)
+		t.Logf("Server Group deleted.")
+	}()
+
+	err = getServerGroup(t, computeClient, sg.ID)
+	if err != nil {
+		t.Fatalf("Unable to get server group: %v", err)
+	}
+
+	firstServer, err := createServerInGroup(t, computeClient, choices, sg)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer func() {
+		servers.Delete(computeClient, firstServer.ID)
+		t.Logf("Server deleted.")
+	}()
+
+	if err = waitForStatus(computeClient, firstServer, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+
+	firstServer, err = servers.Get(computeClient, firstServer.ID).Extract()
+
+	secondServer, err := createServerInGroup(t, computeClient, choices, sg)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer func() {
+		servers.Delete(computeClient, secondServer.ID)
+		t.Logf("Server deleted.")
+	}()
+
+	if err = waitForStatus(computeClient, secondServer, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+
+	secondServer, err = servers.Get(computeClient, secondServer.ID).Extract()
+
+	if err = verifySchedulerWorked(t, firstServer, secondServer); err != nil {
+		t.Fatalf("Scheduling did not work: %v", err)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
new file mode 100644
index 0000000..f6c7c05
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -0,0 +1,484 @@
+// +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"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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{
+		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:  choices.NetworkName,
+		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)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err := servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		Networks: []servers.Network{
+			servers.Network{UUID: network.ID},
+		},
+		AdminPass: pwd,
+		Personality: servers.Personality{
+			&servers.File{
+				Path:     "/etc/test",
+				Contents: []byte("hello world"),
+			},
+		},
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	th.AssertEquals(t, pwd, server.AdminPass)
+
+	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)
+	}
+
+	pager := servers.ListAddresses(client, server.ID)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		networks, err := servers.ExtractAddresses(page)
+		if err != nil {
+			return false, err
+		}
+
+		for n, a := range networks {
+			t.Logf("%s: %+v\n", n, a)
+		}
+		return true, nil
+	})
+
+	pager = servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		addresses, err := servers.ExtractNetworkAddresses(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, a := range addresses {
+			t.Logf("%+v\n", a)
+		}
+		return true, nil
+	})
+}
+
+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)
+	}
+}
+
+func TestServerMetadata(t *testing.T) {
+	t.Parallel()
+
+	choices, err := ComputeChoicesFromEnv()
+	th.AssertNoErr(t, 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)
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{
+		"foo":  "bar",
+		"this": "that",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("UpdateMetadata result: %+v\n", metadata)
+
+	err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr()
+	th.AssertNoErr(t, err)
+
+	metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{
+		"foo": "baz",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("CreateMetadatum result: %+v\n", metadata)
+
+	metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadatum result: %+v\n", metadata)
+	th.AssertEquals(t, "baz", metadata["foo"])
+
+	metadata, err = servers.Metadata(client, server.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadata result: %+v\n", metadata)
+
+	metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("ResetMetadata result: %+v\n", metadata)
+	th.AssertDeepEquals(t, map[string]string{}, metadata)
+}
diff --git a/acceptance/openstack/compute/v2/tenantnetworks_test.go b/acceptance/openstack/compute/v2/tenantnetworks_test.go
new file mode 100644
index 0000000..a92e8bf
--- /dev/null
+++ b/acceptance/openstack/compute/v2/tenantnetworks_test.go
@@ -0,0 +1,109 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func getNetworkID(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) {
+	allPages, err := tenantnetworks.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	networkList, err := tenantnetworks.ExtractNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	networkID := ""
+	for _, network := range networkList {
+		t.Logf("Network: %v", network)
+		if network.Name == networkName {
+			networkID = network.ID
+		}
+	}
+
+	t.Logf("Found network ID for %s: %s\n", networkName, networkID)
+
+	return networkID, nil
+}
+
+func createNetworkServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices, networkID string) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	networks := make([]servers.Network, 1)
+	networks[0] = servers.Network{
+		UUID: networkID,
+	}
+
+	server, err := servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		AdminPass: pwd,
+		Networks:  networks,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	th.AssertEquals(t, pwd, server.AdminPass)
+
+	return server, err
+}
+
+func TestTenantNetworks(t *testing.T) {
+	networkName := os.Getenv("OS_NETWORK_NAME")
+	if networkName == "" {
+		t.Fatalf("OS_NETWORK_NAME must be set")
+	}
+
+	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)
+	}
+
+	networkID, err := getNetworkID(t, client, networkName)
+	if err != nil {
+		t.Fatalf("Unable to get network ID: %v", err)
+	}
+
+	server, err := createNetworkServer(t, client, choices, networkID)
+	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)
+	}
+
+	allPages, err := tenantnetworks.List(client).AllPages()
+	allNetworks, err := tenantnetworks.ExtractNetworks(allPages)
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved all %d networks: %+v", len(allNetworks), allNetworks)
+}
diff --git a/acceptance/openstack/compute/v2/volumeattach_test.go b/acceptance/openstack/compute/v2/volumeattach_test.go
new file mode 100644
index 0000000..34634c9
--- /dev/null
+++ b/acceptance/openstack/compute/v2/volumeattach_test.go
@@ -0,0 +1,125 @@
+// +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/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newBlockClient(t *testing.T) (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := openstack.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func createVAServer(t *testing.T, computeClient *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err := servers.Create(computeClient, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		AdminPass: pwd,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	th.AssertEquals(t, pwd, server.AdminPass)
+
+	return server, err
+}
+
+func createVAVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) {
+	volume, err := volumes.Create(blockClient, &volumes.CreateOpts{
+		Size: 1,
+		Name: "gophercloud-test-volume",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		err = volumes.WaitForStatus(blockClient, volume.ID, "available", 60)
+		th.AssertNoErr(t, err)
+	}()
+
+	return volume, err
+}
+
+func createVolumeAttachment(t *testing.T, computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, volumeId string) {
+	va, err := volumeattach.Create(computeClient, serverId, &volumeattach.CreateOpts{
+		VolumeID: volumeId,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		err = volumes.WaitForStatus(blockClient, volumeId, "in-use", 60)
+		th.AssertNoErr(t, err)
+		err = volumeattach.Delete(computeClient, serverId, va.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		err = volumes.WaitForStatus(blockClient, volumeId, "available", 60)
+		th.AssertNoErr(t, err)
+	}()
+}
+
+func TestAttachVolume(t *testing.T) {
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	computeClient, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	blockClient, err := newBlockClient(t)
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	server, err := createVAServer(t, computeClient, choices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer func() {
+		servers.Delete(computeClient, server.ID)
+		t.Logf("Server deleted.")
+	}()
+
+	if err = waitForStatus(computeClient, server, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+
+	volume, err := createVAVolume(t, blockClient)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer func() {
+		err = volumes.Delete(blockClient, volume.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Volume deleted.")
+	}()
+
+	createVolumeAttachment(t, computeClient, blockClient, server.ID, volume.ID)
+
+}
diff --git a/acceptance/openstack/db/v1/common.go b/acceptance/openstack/db/v1/common.go
new file mode 100644
index 0000000..f7ffc37
--- /dev/null
+++ b/acceptance/openstack/db/v1/common.go
@@ -0,0 +1,70 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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.NewDBV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+	th.AssertNoErr(t, err)
+
+	return c
+}
+
+type context struct {
+	test       *testing.T
+	client     *gophercloud.ServiceClient
+	instanceID string
+	DBIDs      []string
+	users      []string
+}
+
+func newContext(t *testing.T) context {
+	return context{
+		test:   t,
+		client: newClient(t),
+	}
+}
+
+func (c context) Logf(msg string, args ...interface{}) {
+	if len(args) > 0 {
+		c.test.Logf(msg, args...)
+	} else {
+		c.test.Log(msg)
+	}
+}
+
+func (c context) AssertNoErr(err error) {
+	th.AssertNoErr(c.test, err)
+}
+
+func (c context) WaitUntilActive(id string) {
+	err := gophercloud.WaitFor(60, func() (bool, error) {
+		inst, err := instances.Get(c.client, id).Extract()
+		if err != nil {
+			return false, err
+		}
+		if inst.Status == "ACTIVE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/openstack/db/v1/database_test.go b/acceptance/openstack/db/v1/database_test.go
new file mode 100644
index 0000000..2fd3175
--- /dev/null
+++ b/acceptance/openstack/db/v1/database_test.go
@@ -0,0 +1,45 @@
+// +build acceptance db
+
+package v1
+
+import (
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func (c context) createDBs() {
+	opts := db.BatchCreateOpts{
+		db.CreateOpts{Name: "db1"},
+		db.CreateOpts{Name: "db2"},
+		db.CreateOpts{Name: "db3"},
+	}
+
+	err := db.Create(c.client, c.instanceID, opts).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Created three databases on instance %s: db1, db2, db3", c.instanceID)
+}
+
+func (c context) listDBs() {
+	c.Logf("Listing databases on instance %s", c.instanceID)
+
+	err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		dbList, err := db.ExtractDBs(page)
+		c.AssertNoErr(err)
+
+		for _, db := range dbList {
+			c.Logf("DB: %#v", db)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c context) deleteDBs() {
+	for _, id := range []string{"db1", "db2", "db3"} {
+		err := db.Delete(c.client, c.instanceID, id).ExtractErr()
+		c.AssertNoErr(err)
+		c.Logf("Deleted DB %s", id)
+	}
+}
diff --git a/acceptance/openstack/db/v1/flavor_test.go b/acceptance/openstack/db/v1/flavor_test.go
new file mode 100644
index 0000000..46f986c
--- /dev/null
+++ b/acceptance/openstack/db/v1/flavor_test.go
@@ -0,0 +1,31 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func (c context) listFlavors() {
+	c.Logf("Listing flavors")
+
+	err := flavors.List(c.client).EachPage(func(page pagination.Page) (bool, error) {
+		flavorList, err := flavors.ExtractFlavors(page)
+		c.AssertNoErr(err)
+
+		for _, f := range flavorList {
+			c.Logf("Flavor: ID [%s] Name [%s] RAM [%d]", f.ID, f.Name, f.RAM)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c context) getFlavor() {
+	flavor, err := flavors.Get(c.client, "1").Extract()
+	c.Logf("Getting flavor %s", flavor.ID)
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/openstack/db/v1/instance_test.go b/acceptance/openstack/db/v1/instance_test.go
new file mode 100644
index 0000000..dfded21
--- /dev/null
+++ b/acceptance/openstack/db/v1/instance_test.go
@@ -0,0 +1,138 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const envDSType = "DATASTORE_TYPE_ID"
+
+func TestRunner(t *testing.T) {
+	c := newContext(t)
+
+	// FLAVOR tests
+	c.listFlavors()
+	c.getFlavor()
+
+	// INSTANCE tests
+	c.createInstance()
+	c.listInstances()
+	c.getInstance()
+	c.isRootEnabled()
+	c.enableRootUser()
+	c.isRootEnabled()
+	c.restartInstance()
+	//c.resizeInstance()
+	//c.resizeVol()
+
+	// DATABASE tests
+	c.createDBs()
+	c.listDBs()
+
+	// USER tests
+	c.createUsers()
+	c.listUsers()
+
+	// TEARDOWN
+	c.deleteUsers()
+	c.deleteDBs()
+	c.deleteInstance()
+}
+
+func (c context) createInstance() {
+	if os.Getenv(envDSType) == "" {
+		c.test.Fatalf("%s must be set as an environment var", envDSType)
+	}
+
+	opts := instances.CreateOpts{
+		FlavorRef: "2",
+		Size:      5,
+		Name:      tools.RandomString("gopher_db", 5),
+		Datastore: &instances.DatastoreOpts{Type: os.Getenv(envDSType)},
+	}
+
+	instance, err := instances.Create(c.client, opts).Extract()
+	th.AssertNoErr(c.test, err)
+
+	c.Logf("Restarting %s. Waiting...", instance.ID)
+	c.WaitUntilActive(instance.ID)
+	c.Logf("Created Instance %s", instance.ID)
+
+	c.instanceID = instance.ID
+}
+
+func (c context) listInstances() {
+	c.Logf("Listing instances")
+
+	err := instances.List(c.client).EachPage(func(page pagination.Page) (bool, error) {
+		instanceList, err := instances.ExtractInstances(page)
+		c.AssertNoErr(err)
+
+		for _, i := range instanceList {
+			c.Logf("Instance: ID [%s] Name [%s] Status [%s] VolSize [%d] Datastore Type [%s]",
+				i.ID, i.Name, i.Status, i.Volume.Size, i.Datastore.Type)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c context) getInstance() {
+	instance, err := instances.Get(c.client, c.instanceID).Extract()
+	c.AssertNoErr(err)
+	c.Logf("Getting instance: %s", instance.ID)
+}
+
+func (c context) deleteInstance() {
+	err := instances.Delete(c.client, c.instanceID).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Deleted instance %s", c.instanceID)
+}
+
+func (c context) enableRootUser() {
+	_, err := instances.EnableRootUser(c.client, c.instanceID).Extract()
+	c.AssertNoErr(err)
+	c.Logf("Enabled root user on %s", c.instanceID)
+}
+
+func (c context) isRootEnabled() {
+	enabled, err := instances.IsRootEnabled(c.client, c.instanceID)
+	c.AssertNoErr(err)
+	c.Logf("Is root enabled? %d", enabled)
+}
+
+func (c context) restartInstance() {
+	id := c.instanceID
+	err := instances.Restart(c.client, id).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Restarting %s. Waiting...", id)
+	c.WaitUntilActive(id)
+	c.Logf("Restarted %s", id)
+}
+
+func (c context) resizeInstance() {
+	id := c.instanceID
+	err := instances.Resize(c.client, id, "3").ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Resizing %s. Waiting...", id)
+	c.WaitUntilActive(id)
+	c.Logf("Resized %s with flavorRef %s", id, "2")
+}
+
+func (c context) resizeVol() {
+	id := c.instanceID
+	err := instances.ResizeVolume(c.client, id, 4).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Resizing volume of %s. Waiting...", id)
+	c.WaitUntilActive(id)
+	c.Logf("Resized the volume of %s to %d GB", id, 2)
+}
diff --git a/acceptance/openstack/db/v1/pkg.go b/acceptance/openstack/db/v1/pkg.go
new file mode 100644
index 0000000..b7b1f99
--- /dev/null
+++ b/acceptance/openstack/db/v1/pkg.go
@@ -0,0 +1 @@
+package v1
diff --git a/acceptance/openstack/db/v1/user_test.go b/acceptance/openstack/db/v1/user_test.go
new file mode 100644
index 0000000..25a4794
--- /dev/null
+++ b/acceptance/openstack/db/v1/user_test.go
@@ -0,0 +1,70 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	u "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func (c context) createUsers() {
+	users := []string{
+		tools.RandomString("user_", 5),
+		tools.RandomString("user_", 5),
+		tools.RandomString("user_", 5),
+	}
+
+	db1 := db.CreateOpts{Name: "db1"}
+	db2 := db.CreateOpts{Name: "db2"}
+	db3 := db.CreateOpts{Name: "db3"}
+
+	opts := u.BatchCreateOpts{
+		u.CreateOpts{
+			Name:      users[0],
+			Password:  tools.RandomString("", 5),
+			Databases: db.BatchCreateOpts{db1, db2, db3},
+		},
+		u.CreateOpts{
+			Name:      users[1],
+			Password:  tools.RandomString("", 5),
+			Databases: db.BatchCreateOpts{db1, db2},
+		},
+		u.CreateOpts{
+			Name:      users[2],
+			Password:  tools.RandomString("", 5),
+			Databases: db.BatchCreateOpts{db3},
+		},
+	}
+
+	err := u.Create(c.client, c.instanceID, opts).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Created three users on instance %s: %s, %s, %s", c.instanceID, users[0], users[1], users[2])
+	c.users = users
+}
+
+func (c context) listUsers() {
+	c.Logf("Listing databases on instance %s", c.instanceID)
+
+	err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		dbList, err := db.ExtractDBs(page)
+		c.AssertNoErr(err)
+
+		for _, db := range dbList {
+			c.Logf("DB: %#v", db)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c context) deleteUsers() {
+	for _, id := range c.DBIDs {
+		err := db.Delete(c.client, c.instanceID, id).ExtractErr()
+		c.AssertNoErr(err)
+		c.Logf("Deleted DB %s", id)
+	}
+}
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
new file mode 100644
index 0000000..d1fa1e3
--- /dev/null
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance identity
+
+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..96bf1fd
--- /dev/null
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -0,0 +1,47 @@
+// +build acceptance identity
+
+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/role_test.go b/acceptance/openstack/identity/v2/role_test.go
new file mode 100644
index 0000000..ba243fe
--- /dev/null
+++ b/acceptance/openstack/identity/v2/role_test.go
@@ -0,0 +1,58 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+	client := authenticatedClient(t)
+
+	tenantID := findTenant(t, client)
+	userID := createUser(t, client, tenantID)
+	roleID := listRoles(t, client)
+
+	addUserRole(t, client, tenantID, userID, roleID)
+
+	deleteUserRole(t, client, tenantID, userID, roleID)
+
+	deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+	var roleID string
+
+	err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		roleList, err := roles.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		for _, role := range roleList {
+			t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+			roleID = role.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+	err := roles.AddUserRole(client, tenantID, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+	err := roles.DeleteUserRole(client, tenantID, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
new file mode 100644
index 0000000..578fc48
--- /dev/null
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -0,0 +1,32 @@
+// +build acceptance identity
+
+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..e01b3b3
--- /dev/null
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -0,0 +1,54 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"testing"
+
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticateAndValidate(t *testing.T) {
+	// 1. TestAuthenticate
+	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)
+		}
+	}
+
+	// 2. TestValidate
+	client := authenticatedClient(t)
+
+	// Validate Token!
+	getResult := tokens2.Get(client, token.ID)
+
+	// Extract and print the user.
+	user, err := getResult.ExtractUser()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Acquired User: [%s]", user.Name)
+	t.Logf("The User id: [%s]", user.ID)
+	t.Logf("The User username: [%s]", user.UserName)
+	t.Logf("The User roles: [%#v]", user.Roles)
+}
diff --git a/acceptance/openstack/identity/v2/user_test.go b/acceptance/openstack/identity/v2/user_test.go
new file mode 100644
index 0000000..fe73d19
--- /dev/null
+++ b/acceptance/openstack/identity/v2/user_test.go
@@ -0,0 +1,127 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+	client := authenticatedClient(t)
+
+	tenantID := findTenant(t, client)
+
+	userID := createUser(t, client, tenantID)
+
+	listUsers(t, client)
+
+	getUser(t, client, userID)
+
+	updateUser(t, client, userID)
+
+	listUserRoles(t, client, tenantID, userID)
+
+	deleteUser(t, client, userID)
+}
+
+func findTenant(t *testing.T, client *gophercloud.ServiceClient) string {
+	var tenantID string
+	err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		tenantList, err := tenants.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		for _, t := range tenantList {
+			tenantID = t.ID
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	return tenantID
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient, tenantID string) string {
+	t.Log("Creating user")
+
+	opts := users.CreateOpts{
+		Name:     tools.RandomString("user_", 5),
+		Enabled:  users.Disabled,
+		TenantID: tenantID,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created user %s on tenant %s", user.ID, tenantID)
+
+	return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+	err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		userList, err := users.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		for _, user := range userList {
+			t.Logf("Listing user: ID [%s] Name [%s] Email [%s] Enabled? [%s]",
+				user.ID, user.Name, user.Email, strconv.FormatBool(user.Enabled))
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	_, err := users.Get(client, userID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	opts := users.UpdateOpts{Name: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+	user, err := users.Update(client, userID, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated user %s: Name [%s] Email [%s]", userID, user.Name, user.Email)
+}
+
+func listUserRoles(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID string) {
+	count := 0
+	err := users.ListRoles(client, tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+
+		roleList, err := users.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Listing roles for user %s", userID)
+
+		for _, r := range roleList {
+			t.Logf("- %s (%s)", r.Name, r.ID)
+		}
+
+		return true, nil
+	})
+
+	if count == 0 {
+		t.Logf("No roles for user %s", userID)
+	}
+
+	th.AssertNoErr(t, err)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	res := users.Delete(client, userID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted user %s", userID)
+}
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/fwaas/firewall_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
new file mode 100644
index 0000000..80246b6
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
@@ -0,0 +1,116 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func firewallSetup(t *testing.T) string {
+	base.Setup(t)
+	return createPolicy(t, &policies.CreateOpts{})
+}
+
+func firewallTeardown(t *testing.T, policyID string) {
+	defer base.Teardown()
+	deletePolicy(t, policyID)
+}
+
+func TestFirewall(t *testing.T) {
+	policyID := firewallSetup(t)
+	defer firewallTeardown(t, policyID)
+
+	firewallID := createFirewall(t, &firewalls.CreateOpts{
+		Name:        "gophercloud test",
+		Description: "acceptance test",
+		PolicyID:    policyID,
+	})
+
+	waitForFirewallToBeActive(t, firewallID)
+
+	listFirewalls(t)
+
+	updateFirewall(t, firewallID, &firewalls.UpdateOpts{
+		Description: "acceptance test updated",
+	})
+
+	waitForFirewallToBeActive(t, firewallID)
+
+	deleteFirewall(t, firewallID)
+
+	waitForFirewallToBeDeleted(t, firewallID)
+}
+
+func createFirewall(t *testing.T, opts *firewalls.CreateOpts) string {
+	f, err := firewalls.Create(base.Client, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created firewall: %#v", opts)
+	return f.ID
+}
+
+func listFirewalls(t *testing.T) {
+	err := firewalls.List(base.Client, firewalls.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		firewallList, err := firewalls.ExtractFirewalls(page)
+		if err != nil {
+			t.Errorf("Failed to extract firewalls: %v", err)
+			return false, err
+		}
+
+		for _, r := range firewallList {
+			t.Logf("Listing firewalls: ID [%s]", r.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func updateFirewall(t *testing.T, firewallID string, opts *firewalls.UpdateOpts) {
+	f, err := firewalls.Update(base.Client, firewallID, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated firewall ID [%s]", f.ID)
+}
+
+func getFirewall(t *testing.T, firewallID string) *firewalls.Firewall {
+	f, err := firewalls.Get(base.Client, firewallID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting firewall ID [%s]", f.ID)
+	return f
+}
+
+func deleteFirewall(t *testing.T, firewallID string) {
+	res := firewalls.Delete(base.Client, firewallID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted firewall %s", firewallID)
+}
+
+func waitForFirewallToBeActive(t *testing.T, firewallID string) {
+	for i := 0; i < 10; i++ {
+		fw := getFirewall(t, firewallID)
+		if fw.Status == "ACTIVE" {
+			break
+		}
+		time.Sleep(time.Second)
+	}
+}
+
+func waitForFirewallToBeDeleted(t *testing.T, firewallID string) {
+	for i := 0; i < 10; i++ {
+		err := firewalls.Get(base.Client, firewallID).Err
+		if err != nil {
+			httpStatus := err.(*gophercloud.UnexpectedResponseCodeError)
+			if httpStatus.Actual == 404 {
+				return
+			}
+		}
+		time.Sleep(time.Second)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go b/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go
new file mode 100644
index 0000000..206bf33
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go
@@ -0,0 +1 @@
+package fwaas
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go
new file mode 100644
index 0000000..fdca22e
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go
@@ -0,0 +1,107 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func firewallPolicySetup(t *testing.T) string {
+	base.Setup(t)
+	return createRule(t, &rules.CreateOpts{
+		Protocol: "tcp",
+		Action:   "allow",
+	})
+}
+
+func firewallPolicyTeardown(t *testing.T, ruleID string) {
+	defer base.Teardown()
+	deleteRule(t, ruleID)
+}
+
+func TestFirewallPolicy(t *testing.T) {
+	ruleID := firewallPolicySetup(t)
+	defer firewallPolicyTeardown(t, ruleID)
+
+	policyID := createPolicy(t, &policies.CreateOpts{
+		Name:        "gophercloud test",
+		Description: "acceptance test",
+		Rules: []string{
+			ruleID,
+		},
+	})
+
+	listPolicies(t)
+
+	updatePolicy(t, policyID, &policies.UpdateOpts{
+		Description: "acceptance test updated",
+	})
+
+	getPolicy(t, policyID)
+
+	removeRuleFromPolicy(t, policyID, ruleID)
+
+	addRuleToPolicy(t, policyID, ruleID)
+
+	deletePolicy(t, policyID)
+}
+
+func createPolicy(t *testing.T, opts *policies.CreateOpts) string {
+	p, err := policies.Create(base.Client, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created policy: %#v", opts)
+	return p.ID
+}
+
+func listPolicies(t *testing.T) {
+	err := policies.List(base.Client, policies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		policyList, err := policies.ExtractPolicies(page)
+		if err != nil {
+			t.Errorf("Failed to extract policies: %v", err)
+			return false, err
+		}
+
+		for _, p := range policyList {
+			t.Logf("Listing policies: ID [%s]", p.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func updatePolicy(t *testing.T, policyID string, opts *policies.UpdateOpts) {
+	p, err := policies.Update(base.Client, policyID, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated policy ID [%s]", p.ID)
+}
+
+func removeRuleFromPolicy(t *testing.T, policyID string, ruleID string) {
+	err := policies.RemoveRule(base.Client, policyID, ruleID)
+	th.AssertNoErr(t, err)
+	t.Logf("Removed rule [%s] from policy ID [%s]", ruleID, policyID)
+}
+
+func addRuleToPolicy(t *testing.T, policyID string, ruleID string) {
+	err := policies.InsertRule(base.Client, policyID, ruleID, "", "")
+	th.AssertNoErr(t, err)
+	t.Logf("Inserted rule [%s] into policy ID [%s]", ruleID, policyID)
+}
+
+func getPolicy(t *testing.T, policyID string) {
+	p, err := policies.Get(base.Client, policyID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting policy ID [%s]", p.ID)
+}
+
+func deletePolicy(t *testing.T, policyID string) {
+	res := policies.Delete(base.Client, policyID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted policy %s", policyID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go
new file mode 100644
index 0000000..144aa09
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go
@@ -0,0 +1,84 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestFirewallRules(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	ruleID := createRule(t, &rules.CreateOpts{
+		Name:                 "gophercloud_test",
+		Description:          "acceptance test",
+		Protocol:             "tcp",
+		Action:               "allow",
+		DestinationIPAddress: "192.168.0.0/24",
+		DestinationPort:      "22",
+	})
+
+	listRules(t)
+
+	destinationIPAddress := "192.168.1.0/24"
+	destinationPort := ""
+	sourcePort := "1234"
+
+	updateRule(t, ruleID, &rules.UpdateOpts{
+		DestinationIPAddress: &destinationIPAddress,
+		DestinationPort:      &destinationPort,
+		SourcePort:           &sourcePort,
+	})
+
+	getRule(t, ruleID)
+
+	deleteRule(t, ruleID)
+}
+
+func createRule(t *testing.T, opts *rules.CreateOpts) string {
+	r, err := rules.Create(base.Client, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created rule: %#v", opts)
+	return r.ID
+}
+
+func listRules(t *testing.T) {
+	err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		ruleList, err := rules.ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract rules: %v", err)
+			return false, err
+		}
+
+		for _, r := range ruleList {
+			t.Logf("Listing rules: ID [%s]", r.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func updateRule(t *testing.T, ruleID string, opts *rules.UpdateOpts) {
+	r, err := rules.Update(base.Client, ruleID, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated rule ID [%s]", r.ID)
+}
+
+func getRule(t *testing.T, ruleID string) {
+	r, err := rules.Get(base.Client, ruleID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting rule ID [%s]", r.ID)
+}
+
+func deleteRule(t *testing.T, ruleID string) {
+	res := rules.Delete(base.Client, ruleID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted rule %s", ruleID)
+}
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..03e8e27
--- /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 [%s] 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..24cc62b
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/accounts_test.go
@@ -0,0 +1,50 @@
+// +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})
+	t.Logf("Update Account Response: %+v\n", updateres)
+	updateHeaders, err := updateres.Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Update Account Response Headers: %+v\n", updateHeaders)
+
+	// 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)
+	}()
+
+	// Extract the custom metadata from the 'Get' response.
+	res := accounts.Get(client, nil)
+
+	h, err := res.Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Get Account Response Headers: %+v\n", h)
+
+	am, err := res.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..8328a4f
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/containers_test.go
@@ -0,0 +1,137 @@
+// +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)
+		}
+	}
+}
+
+func TestListAllContainers(t *testing.T) {
+	// Create a new client to execute the HTTP requests. See common.go for newClient body.
+	client := newClient(t)
+
+	numContainers := 20
+
+	// 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 all the numContainer names that were just created. To just list those,
+	// the 'prefix' parameter is used.
+	allPages, err := containers.List(client, &containers.ListOpts{Full: true, Limit: 5, Prefix: "gophercloud-test-container-"}).AllPages()
+	th.AssertNoErr(t, err)
+	containerInfoList, err := containers.ExtractInfo(allPages)
+	th.AssertNoErr(t, err)
+	for _, n := range containerInfoList {
+		t.Logf("Container: Name [%s] Count [%d] Bytes [%d]",
+			n.Name, n.Count, n.Bytes)
+	}
+	th.AssertEquals(t, numContainers, len(containerInfoList))
+
+	// List the info for all the numContainer containers that were created.
+	allPages, err = containers.List(client, &containers.ListOpts{Full: false, Limit: 2, Prefix: "gophercloud-test-container-"}).AllPages()
+	th.AssertNoErr(t, err)
+	containerNamesList, err := containers.ExtractNames(allPages)
+	th.AssertNoErr(t, err)
+	for _, n := range containerNamesList {
+		t.Logf("Container: Name [%s]", n)
+	}
+	th.AssertEquals(t, numContainers, len(containerNamesList))
+}
diff --git a/acceptance/openstack/objectstorage/v1/objects_test.go b/acceptance/openstack/objectstorage/v1/objects_test.go
new file mode 100644
index 0000000..a8de338
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/objects_test.go
@@ -0,0 +1,119 @@
+// +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)
+	header, err := containers.Create(client, cName, nil).ExtractHeader()
+	th.AssertNoErr(t, err)
+	t.Logf("Create object headers: %+v\n", header)
+
+	// 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/orchestration/v1/buildinfo_test.go b/acceptance/openstack/orchestration/v1/buildinfo_test.go
new file mode 100644
index 0000000..05a5e1d
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/buildinfo_test.go
@@ -0,0 +1,20 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBuildInfo(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	bi, err := buildinfo.Get(client).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("retrieved build info: %+v\n", bi)
+}
diff --git a/acceptance/openstack/orchestration/v1/common.go b/acceptance/openstack/orchestration/v1/common.go
new file mode 100644
index 0000000..2c28dcb
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/common.go
@@ -0,0 +1,44 @@
+// +build acceptance
+
+package v1
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var template = fmt.Sprintf(`
+{
+	"heat_template_version": "2013-05-23",
+	"description": "Simple template to test heat commands",
+	"parameters": {},
+	"resources": {
+		"hello_world": {
+			"type":"OS::Nova::Server",
+			"properties": {
+				"flavor": "%s",
+				"image": "%s",
+				"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+			}
+		}
+	}
+}`, os.Getenv("OS_FLAVOR_ID"), os.Getenv("OS_IMAGE_ID"))
+
+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.NewOrchestrationV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+	th.AssertNoErr(t, err)
+	return c
+}
diff --git a/acceptance/openstack/orchestration/v1/hello-compute.json b/acceptance/openstack/orchestration/v1/hello-compute.json
new file mode 100644
index 0000000..11cfc80
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/hello-compute.json
@@ -0,0 +1,13 @@
+{
+  "heat_template_version": "2013-05-23",
+  "resources": {
+    "compute_instance": {
+      "type": "OS::Nova::Server",
+      "properties": {
+        "flavor": "m1.small",
+        "image": "cirros-0.3.2-x86_64-disk",
+        "name": "Single Compute Instance"
+      }
+    }
+  }
+}
diff --git a/acceptance/openstack/orchestration/v1/stackevents_test.go b/acceptance/openstack/orchestration/v1/stackevents_test.go
new file mode 100644
index 0000000..e356c86
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stackevents_test.go
@@ -0,0 +1,68 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackEvents(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName := "postman_stack_2"
+	resourceName := "hello_world"
+	var eventID string
+
+	createOpts := stacks.CreateOpts{
+		Name:     stackName,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) {
+		events, err := stackevents.ExtractEvents(page)
+		th.AssertNoErr(t, err)
+		t.Logf("listed events: %+v\n", events)
+		eventID = events[0].ID
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+
+	err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) {
+		resourceEvents, err := stackevents.ExtractEvents(page)
+		th.AssertNoErr(t, err)
+		t.Logf("listed resource events: %+v\n", resourceEvents)
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+
+	event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("retrieved event: %+v\n", event)
+}
diff --git a/acceptance/openstack/orchestration/v1/stackresources_test.go b/acceptance/openstack/orchestration/v1/stackresources_test.go
new file mode 100644
index 0000000..b614f1c
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stackresources_test.go
@@ -0,0 +1,62 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackResources(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName := "postman_stack_2"
+
+	createOpts := stacks.CreateOpts{
+		Name:     stackName,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	resourceName := "hello_world"
+	resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack resource: %+v\n", resource)
+
+	metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack resource metadata: %+v\n", metadata)
+
+	err = stackresources.List(client, stackName, stack.ID, stackresources.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		resources, err := stackresources.ExtractResources(page)
+		th.AssertNoErr(t, err)
+		t.Logf("resources: %+v\n", resources)
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/orchestration/v1/stacks_test.go b/acceptance/openstack/orchestration/v1/stacks_test.go
new file mode 100644
index 0000000..db31cd4
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stacks_test.go
@@ -0,0 +1,153 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStacks(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName1 := "gophercloud-test-stack-2"
+	createOpts := stacks.CreateOpts{
+		Name:     stackName1,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName1, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName1)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	updateOpts := stacks.UpdateOpts{
+		Template: template,
+		Timeout:  20,
+	}
+	err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "UPDATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	t.Logf("Updated stack")
+
+	err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		stackList, err := stacks.ExtractStacks(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Got stack list: %+v\n", stackList)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack: %+v\n", getStack)
+
+	abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Abandonded stack %+v\n", abandonedStack)
+	th.AssertNoErr(t, err)
+}
+
+// Test using the updated interface
+func TestStacksNewTemplateFormat(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName1 := "gophercloud-test-stack-2"
+	templateOpts := new(osStacks.Template)
+	templateOpts.Bin = []byte(template)
+	createOpts := osStacks.CreateOpts{
+		Name:         stackName1,
+		TemplateOpts: templateOpts,
+		Timeout:      5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName1, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName1)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	updateOpts := osStacks.UpdateOpts{
+		TemplateOpts: templateOpts,
+		Timeout:      20,
+	}
+	err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "UPDATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	t.Logf("Updated stack")
+
+	err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		stackList, err := osStacks.ExtractStacks(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Got stack list: %+v\n", stackList)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack: %+v\n", getStack)
+
+	abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Abandonded stack %+v\n", abandonedStack)
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/orchestration/v1/stacktemplates_test.go b/acceptance/openstack/orchestration/v1/stacktemplates_test.go
new file mode 100644
index 0000000..22d5e88
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stacktemplates_test.go
@@ -0,0 +1,75 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackTemplates(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName := "postman_stack_2"
+
+	createOpts := stacks.CreateOpts{
+		Name:     stackName,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("retrieved template: %+v\n", tmpl)
+
+	validateOpts := osStacktemplates.ValidateOpts{
+		Template: `{"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type":    "string",
+				},
+			},
+			"resources": {
+				"hello_world": {
+					"type": "OS::Nova::Server",
+					"properties": {
+						"key_name": "heat_key",
+						"flavor": {
+							"get_param": "flavor",
+						},
+						"image":     "ad091b52-742f-469e-8f3c-fd81cadf0743",
+						"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n",
+					},
+				},
+			},
+		}`}
+	validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("validated template: %+v\n", validatedTemplate)
+}
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..25b2cfe
--- /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) {
+	res := snapshots.Delete(client, id)
+	th.AssertNoErr(t, res.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..f86f9ad
--- /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) {
+	res := volumes.Delete(client, id)
+	th.AssertNoErr(t, res.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/cdn/v1/base_test.go b/acceptance/rackspace/cdn/v1/base_test.go
new file mode 100644
index 0000000..135f5b3
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/base_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/base"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBaseOps(t *testing.T) {
+	client := newClient(t)
+	t.Log("Retrieving Home Document")
+	testHomeDocumentGet(t, client)
+
+	t.Log("Pinging root URL")
+	testPing(t, client)
+}
+
+func testHomeDocumentGet(t *testing.T, client *gophercloud.ServiceClient) {
+	hd, err := base.Get(client).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved home document: %+v", *hd)
+}
+
+func testPing(t *testing.T, client *gophercloud.ServiceClient) {
+	err := base.Ping(client).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully pinged root URL")
+}
diff --git a/acceptance/rackspace/cdn/v1/common.go b/acceptance/rackspace/cdn/v1/common.go
new file mode 100644
index 0000000..2333ca7
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/common.go
@@ -0,0 +1,23 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+	ao, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := rackspace.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	c, err := rackspace.NewCDNV1(client, gophercloud.EndpointOpts{})
+	th.AssertNoErr(t, err)
+	return c
+}
diff --git a/acceptance/rackspace/cdn/v1/flavor_test.go b/acceptance/rackspace/cdn/v1/flavor_test.go
new file mode 100644
index 0000000..f26cff0
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/flavor_test.go
@@ -0,0 +1,47 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestFlavor(t *testing.T) {
+	client := newClient(t)
+
+	t.Log("Listing Flavors")
+	id := testFlavorsList(t, client)
+
+	t.Log("Retrieving Flavor")
+	testFlavorGet(t, client, id)
+}
+
+func testFlavorsList(t *testing.T, client *gophercloud.ServiceClient) string {
+	var id string
+	err := flavors.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		flavorList, err := os.ExtractFlavors(page)
+		th.AssertNoErr(t, err)
+
+		for _, flavor := range flavorList {
+			t.Logf("Listing flavor: ID [%s] Providers [%+v]", flavor.ID, flavor.Providers)
+			id = flavor.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	return id
+}
+
+func testFlavorGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	flavor, err := flavors.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved Flavor: %+v", *flavor)
+}
diff --git a/acceptance/rackspace/cdn/v1/service_test.go b/acceptance/rackspace/cdn/v1/service_test.go
new file mode 100644
index 0000000..c19c241
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/service_test.go
@@ -0,0 +1,93 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/services"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestService(t *testing.T) {
+	client := newClient(t)
+
+	t.Log("Creating Service")
+	loc := testServiceCreate(t, client, "test-site-1")
+	t.Logf("Created service at location: %s", loc)
+
+	defer testServiceDelete(t, client, loc)
+
+	t.Log("Updating Service")
+	testServiceUpdate(t, client, loc)
+
+	t.Log("Retrieving Service")
+	testServiceGet(t, client, loc)
+
+	t.Log("Listing Services")
+	testServiceList(t, client)
+}
+
+func testServiceCreate(t *testing.T, client *gophercloud.ServiceClient, name string) string {
+	createOpts := os.CreateOpts{
+		Name: name,
+		Domains: []os.Domain{
+			os.Domain{
+				Domain: "www." + name + ".com",
+			},
+		},
+		Origins: []os.Origin{
+			os.Origin{
+				Origin: name + ".com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		FlavorID: "cdn",
+	}
+	l, err := services.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	return l
+}
+
+func testServiceGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	s, err := services.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved service: %+v", *s)
+}
+
+func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	opts := os.UpdateOpts{
+		os.Append{
+			Value: os.Domain{Domain: "newDomain.com", Protocol: "http"},
+		},
+	}
+
+	loc, err := services.Update(client, id, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully updated service at location: %s", loc)
+}
+
+func testServiceList(t *testing.T, client *gophercloud.ServiceClient) {
+	err := services.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		serviceList, err := os.ExtractServices(page)
+		th.AssertNoErr(t, err)
+
+		for _, service := range serviceList {
+			t.Logf("Listing service: %+v", service)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func testServiceDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := services.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully deleted service (%s)", id)
+}
diff --git a/acceptance/rackspace/cdn/v1/serviceasset_test.go b/acceptance/rackspace/cdn/v1/serviceasset_test.go
new file mode 100644
index 0000000..c32bf25
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/serviceasset_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	osServiceAssets "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceAsset(t *testing.T) {
+	client := newClient(t)
+
+	t.Log("Creating Service")
+	loc := testServiceCreate(t, client, "test-site-2")
+	t.Logf("Created service at location: %s", loc)
+
+	t.Log("Deleting Service Assets")
+	testServiceAssetDelete(t, client, loc)
+}
+
+func testServiceAssetDelete(t *testing.T, client *gophercloud.ServiceClient, url string) {
+	deleteOpts := osServiceAssets.DeleteOpts{
+		All: true,
+	}
+	err := serviceassets.Delete(client, url, deleteOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Log("Successfully deleted all Service Assets")
+}
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..d7e6aa7
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
@@ -0,0 +1,49 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	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"
+)
+
+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)
+
+	getServer(t, client, server)
+
+	listServers(t, client)
+}
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..9bd6eb4
--- /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).ExtractErr()
+	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..a8b5937
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -0,0 +1,217 @@
+// +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)
+
+	pwd := tools.MakeNewPassword("")
+
+	opts := &servers.CreateOpts{
+		Name:       name,
+		ImageRef:   options.imageID,
+		FlavorRef:  options.flavorID,
+		DiskConfig: diskconfig.Manual,
+		AdminPass:  pwd,
+	}
+
+	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.")
+
+	th.CheckEquals(t, pwd, s.AdminPass)
+
+	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 updateServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Get")
+
+	opts := os.UpdateOpts{
+		Name: "updated-server",
+	}
+	updatedServer, err := servers.Update(client, server.ID, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "updated-server", updatedServer.Name)
+	logServer(t, updatedServer, -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)).ExtractErr()
+	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).ExtractErr()
+	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")
+
+	res := servers.Delete(client, server.ID)
+	th.AssertNoErr(t, res.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).ExtractErr()
+	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)
+	updateServer(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/compute/v2/volumeattach_test.go b/acceptance/rackspace/compute/v2/volumeattach_test.go
new file mode 100644
index 0000000..9848e2e
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/volumeattach_test.go
@@ -0,0 +1,130 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack"
+	osVolumes "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	osVolumeAttach "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
+	osServers "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/rackspace"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newBlockClient(t *testing.T) (*gophercloud.ServiceClient, error) {
+	ao, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := rackspace.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("RS_REGION_NAME"),
+	})
+}
+
+func createVAServer(t *testing.T, computeClient *gophercloud.ServiceClient, choices *serverOpts) (*osServers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err := servers.Create(computeClient, osServers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.flavorID,
+		ImageRef:  choices.imageID,
+		AdminPass: pwd,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	th.AssertEquals(t, pwd, server.AdminPass)
+
+	return server, err
+}
+
+func createVAVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) {
+	volume, err := volumes.Create(blockClient, &osVolumes.CreateOpts{
+		Size: 80,
+		Name: "gophercloud-test-volume",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		err = osVolumes.WaitForStatus(blockClient, volume.ID, "available", 60)
+		th.AssertNoErr(t, err)
+	}()
+
+	return volume, err
+}
+
+func createVolumeAttachment(t *testing.T, computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverID string, volumeID string) {
+	va, err := volumeattach.Create(computeClient, serverID, &osVolumeAttach.CreateOpts{
+		VolumeID: volumeID,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		err = osVolumes.WaitForStatus(blockClient, volumeID, "in-use", 60)
+		th.AssertNoErr(t, err)
+		err = volumeattach.Delete(computeClient, serverID, va.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		err = osVolumes.WaitForStatus(blockClient, volumeID, "available", 60)
+		th.AssertNoErr(t, err)
+	}()
+	t.Logf("Attached volume to server: %+v", va)
+}
+
+func TestAttachVolume(t *testing.T) {
+	choices, err := optionsFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	computeClient, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	blockClient, err := newBlockClient(t)
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	server, err := createVAServer(t, computeClient, choices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer func() {
+		servers.Delete(computeClient, server.ID)
+		t.Logf("Server deleted.")
+	}()
+
+	if err = osServers.WaitForStatus(computeClient, server.ID, "ACTIVE", 300); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+
+	volume, err := createVAVolume(t, blockClient)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer func() {
+		err = volumes.Delete(blockClient, volume.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Volume deleted.")
+	}()
+
+	createVolumeAttachment(t, computeClient, blockClient, server.ID, volume.ID)
+
+}
diff --git a/acceptance/rackspace/db/v1/backup_test.go b/acceptance/rackspace/db/v1/backup_test.go
new file mode 100644
index 0000000..522aace
--- /dev/null
+++ b/acceptance/rackspace/db/v1/backup_test.go
@@ -0,0 +1,84 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/rackspace/gophercloud/rackspace/db/v1/backups"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+)
+
+func (c *context) createBackup() {
+	opts := backups.CreateOpts{
+		Name:       tools.RandomString("backup_", 5),
+		InstanceID: c.instanceID,
+	}
+
+	backup, err := backups.Create(c.client, opts).Extract()
+
+	c.Logf("Created backup %#v", backup)
+	c.AssertNoErr(err)
+
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		b, err := backups.Get(c.client, backup.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if b.Status == "COMPLETED" {
+			return true, nil
+		}
+		return false, nil
+	})
+	c.AssertNoErr(err)
+
+	c.backupID = backup.ID
+}
+
+func (c *context) getBackup() {
+	backup, err := backups.Get(c.client, c.backupID).Extract()
+	c.AssertNoErr(err)
+	c.Logf("Getting backup %s", backup.ID)
+}
+
+func (c *context) listAllBackups() {
+	c.Logf("Listing backups")
+
+	err := backups.List(c.client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		backupList, err := backups.ExtractBackups(page)
+		c.AssertNoErr(err)
+
+		for _, b := range backupList {
+			c.Logf("Backup: %#v", b)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) listInstanceBackups() {
+	c.Logf("Listing backups for instance %s", c.instanceID)
+
+	err := instances.ListBackups(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		backupList, err := backups.ExtractBackups(page)
+		c.AssertNoErr(err)
+
+		for _, b := range backupList {
+			c.Logf("Backup: %#v", b)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) deleteBackup() {
+	err := backups.Delete(c.client, c.backupID).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Deleted backup %s", c.backupID)
+}
diff --git a/acceptance/rackspace/db/v1/common.go b/acceptance/rackspace/db/v1/common.go
new file mode 100644
index 0000000..24512b9
--- /dev/null
+++ b/acceptance/rackspace/db/v1/common.go
@@ -0,0 +1,73 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+	opts, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+	opts = tools.OnlyRS(opts)
+
+	client, err := rackspace.AuthenticatedClient(opts)
+	th.AssertNoErr(t, err)
+
+	c, err := rackspace.NewDBV1(client, gophercloud.EndpointOpts{
+		Region: "IAD",
+	})
+	th.AssertNoErr(t, err)
+
+	return c
+}
+
+type context struct {
+	test          *testing.T
+	client        *gophercloud.ServiceClient
+	instanceID    string
+	DBIDs         []string
+	replicaID     string
+	backupID      string
+	configGroupID string
+	users         []string
+}
+
+func newContext(t *testing.T) context {
+	return context{
+		test:   t,
+		client: newClient(t),
+	}
+}
+
+func (c context) Logf(msg string, args ...interface{}) {
+	if len(args) > 0 {
+		c.test.Logf(msg, args...)
+	} else {
+		c.test.Log(msg)
+	}
+}
+
+func (c context) AssertNoErr(err error) {
+	th.AssertNoErr(c.test, err)
+}
+
+func (c context) WaitUntilActive(id string) {
+	err := gophercloud.WaitFor(60, func() (bool, error) {
+		inst, err := instances.Get(c.client, id).Extract()
+		if err != nil {
+			return false, err
+		}
+		if inst.Status == "ACTIVE" {
+			return true, nil
+		}
+		return false, nil
+	})
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/config_group_test.go b/acceptance/rackspace/db/v1/config_group_test.go
new file mode 100644
index 0000000..81bd40a
--- /dev/null
+++ b/acceptance/rackspace/db/v1/config_group_test.go
@@ -0,0 +1,93 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+	"github.com/rackspace/gophercloud/pagination"
+	config "github.com/rackspace/gophercloud/rackspace/db/v1/configurations"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+)
+
+func (c *context) createConfigGrp() {
+	opts := os.CreateOpts{
+		Name: tools.RandomString("config_", 5),
+		Values: map[string]interface{}{
+			"connect_timeout":  300,
+			"join_buffer_size": 900000,
+		},
+	}
+
+	cg, err := config.Create(c.client, opts).Extract()
+
+	c.AssertNoErr(err)
+	c.Logf("Created config group %#v", cg)
+
+	c.configGroupID = cg.ID
+}
+
+func (c *context) getConfigGrp() {
+	cg, err := config.Get(c.client, c.configGroupID).Extract()
+	c.Logf("Getting config group: %#v", cg)
+	c.AssertNoErr(err)
+}
+
+func (c *context) updateConfigGrp() {
+	opts := os.UpdateOpts{
+		Name: tools.RandomString("new_name_", 5),
+		Values: map[string]interface{}{
+			"connect_timeout": 250,
+		},
+	}
+	err := config.Update(c.client, c.configGroupID, opts).ExtractErr()
+	c.Logf("Updated config group %s", c.configGroupID)
+	c.AssertNoErr(err)
+}
+
+func (c *context) replaceConfigGrp() {
+	opts := os.UpdateOpts{
+		Values: map[string]interface{}{
+			"big_tables": 1,
+		},
+	}
+
+	err := config.Replace(c.client, c.configGroupID, opts).ExtractErr()
+	c.Logf("Replaced values for config group %s", c.configGroupID)
+	c.AssertNoErr(err)
+}
+
+func (c *context) associateInstanceWithConfigGrp() {
+	err := instances.AssociateWithConfigGroup(c.client, c.instanceID, c.configGroupID).ExtractErr()
+	c.Logf("Associated instance %s with config group %s", c.instanceID, c.configGroupID)
+	c.AssertNoErr(err)
+}
+
+func (c *context) listConfigGrpInstances() {
+	c.Logf("Listing all instances associated with config group %s", c.configGroupID)
+
+	err := config.ListInstances(c.client, c.configGroupID).EachPage(func(page pagination.Page) (bool, error) {
+		instanceList, err := instances.ExtractInstances(page)
+		c.AssertNoErr(err)
+
+		for _, instance := range instanceList {
+			c.Logf("Instance: %#v", instance)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) deleteConfigGrp() {
+	err := config.Delete(c.client, c.configGroupID).ExtractErr()
+	c.Logf("Deleted config group %s", c.configGroupID)
+	c.AssertNoErr(err)
+}
+
+func (c *context) detachInstanceFromGrp() {
+	err := instances.DetachFromConfigGroup(c.client, c.instanceID).ExtractErr()
+	c.Logf("Detached instance %s from config groups", c.instanceID)
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/database_test.go b/acceptance/rackspace/db/v1/database_test.go
new file mode 100644
index 0000000..d5c448f
--- /dev/null
+++ b/acceptance/rackspace/db/v1/database_test.go
@@ -0,0 +1,54 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func (c *context) createDBs() {
+	dbs := []string{
+		tools.RandomString("db_", 5),
+		tools.RandomString("db_", 5),
+		tools.RandomString("db_", 5),
+	}
+
+	opts := db.BatchCreateOpts{
+		db.CreateOpts{Name: dbs[0]},
+		db.CreateOpts{Name: dbs[1]},
+		db.CreateOpts{Name: dbs[2]},
+	}
+
+	err := db.Create(c.client, c.instanceID, opts).ExtractErr()
+	c.Logf("Created three databases on instance %s: %s, %s, %s", c.instanceID, dbs[0], dbs[1], dbs[2])
+	c.AssertNoErr(err)
+
+	c.DBIDs = dbs
+}
+
+func (c *context) listDBs() {
+	c.Logf("Listing databases on instance %s", c.instanceID)
+
+	err := db.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		dbList, err := db.ExtractDBs(page)
+		c.AssertNoErr(err)
+
+		for _, db := range dbList {
+			c.Logf("DB: %#v", db)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) deleteDBs() {
+	for _, id := range c.DBIDs {
+		err := db.Delete(c.client, c.instanceID, id).ExtractErr()
+		c.AssertNoErr(err)
+		c.Logf("Deleted DB %s", id)
+	}
+}
diff --git a/acceptance/rackspace/db/v1/flavor_test.go b/acceptance/rackspace/db/v1/flavor_test.go
new file mode 100644
index 0000000..0d6e6df
--- /dev/null
+++ b/acceptance/rackspace/db/v1/flavor_test.go
@@ -0,0 +1,32 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/flavors"
+)
+
+func (c context) listFlavors() {
+	c.Logf("Listing flavors")
+
+	err := flavors.List(c.client).EachPage(func(page pagination.Page) (bool, error) {
+		flavorList, err := os.ExtractFlavors(page)
+		c.AssertNoErr(err)
+
+		for _, f := range flavorList {
+			c.Logf("Flavor: ID [%s] Name [%s] RAM [%d]", f.ID, f.Name, f.RAM)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c context) getFlavor() {
+	flavor, err := flavors.Get(c.client, "1").Extract()
+	c.Logf("Getting flavor %s", flavor.ID)
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/instance_test.go b/acceptance/rackspace/db/v1/instance_test.go
new file mode 100644
index 0000000..b5540e3
--- /dev/null
+++ b/acceptance/rackspace/db/v1/instance_test.go
@@ -0,0 +1,169 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRunner(t *testing.T) {
+	c := newContext(t)
+
+	// FLAVOR tests
+	c.listFlavors()
+	c.getFlavor()
+
+	// INSTANCE tests
+	c.createInstance()
+	c.listInstances()
+	c.getInstance()
+	c.isRootEnabled()
+	c.enableRootUser()
+	c.isRootEnabled()
+	c.restartInstance()
+	c.resizeInstance()
+	c.resizeVol()
+	c.getDefaultConfig()
+
+	// REPLICA tests
+	c.createReplica()
+	c.detachReplica()
+
+	// BACKUP tests
+	c.createBackup()
+	c.getBackup()
+	c.listAllBackups()
+	c.listInstanceBackups()
+	c.deleteBackup()
+
+	// CONFIG GROUP tests
+	c.createConfigGrp()
+	c.getConfigGrp()
+	c.updateConfigGrp()
+	c.replaceConfigGrp()
+	c.associateInstanceWithConfigGrp()
+	c.listConfigGrpInstances()
+	c.detachInstanceFromGrp()
+	c.deleteConfigGrp()
+
+	// DATABASE tests
+	c.createDBs()
+	c.listDBs()
+
+	// USER tests
+	c.createUsers()
+	c.listUsers()
+	c.changeUserPwd()
+	c.getUser()
+	c.updateUser()
+	c.listUserAccess()
+	c.revokeUserAccess()
+	c.grantUserAccess()
+
+	// TEARDOWN
+	c.deleteUsers()
+	c.deleteDBs()
+
+	c.restartInstance()
+	c.WaitUntilActive(c.instanceID)
+
+	c.deleteInstance(c.replicaID)
+	c.deleteInstance(c.instanceID)
+}
+
+func (c *context) createInstance() {
+	opts := instances.CreateOpts{
+		FlavorRef: "1",
+		Size:      1,
+		Name:      tools.RandomString("gopher_db", 5),
+	}
+
+	instance, err := instances.Create(c.client, opts).Extract()
+	th.AssertNoErr(c.test, err)
+
+	c.Logf("Creating %s. Waiting...", instance.ID)
+	c.WaitUntilActive(instance.ID)
+	c.Logf("Created instance %s", instance.ID)
+
+	c.instanceID = instance.ID
+}
+
+func (c *context) listInstances() {
+	c.Logf("Listing instances")
+
+	err := instances.List(c.client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		instanceList, err := instances.ExtractInstances(page)
+		c.AssertNoErr(err)
+
+		for _, i := range instanceList {
+			c.Logf("Instance: ID [%s] Name [%s] Status [%s] VolSize [%d] Datastore Type [%s]",
+				i.ID, i.Name, i.Status, i.Volume.Size, i.Datastore.Type)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) getInstance() {
+	instance, err := instances.Get(c.client, c.instanceID).Extract()
+	c.AssertNoErr(err)
+	c.Logf("Getting instance: %#v", instance)
+}
+
+func (c *context) deleteInstance(id string) {
+	err := instances.Delete(c.client, id).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Deleted instance %s", id)
+}
+
+func (c *context) enableRootUser() {
+	_, err := instances.EnableRootUser(c.client, c.instanceID).Extract()
+	c.AssertNoErr(err)
+	c.Logf("Enabled root user on %s", c.instanceID)
+}
+
+func (c *context) isRootEnabled() {
+	enabled, err := instances.IsRootEnabled(c.client, c.instanceID)
+	c.AssertNoErr(err)
+	c.Logf("Is root enabled? %s", enabled)
+}
+
+func (c *context) restartInstance() {
+	id := c.instanceID
+	err := instances.Restart(c.client, id).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Restarting %s. Waiting...", id)
+	c.WaitUntilActive(id)
+	c.Logf("Restarted %s", id)
+}
+
+func (c *context) resizeInstance() {
+	id := c.instanceID
+	err := instances.Resize(c.client, id, "2").ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Resizing %s. Waiting...", id)
+	c.WaitUntilActive(id)
+	c.Logf("Resized %s with flavorRef %s", id, "2")
+}
+
+func (c *context) resizeVol() {
+	id := c.instanceID
+	err := instances.ResizeVolume(c.client, id, 2).ExtractErr()
+	c.AssertNoErr(err)
+	c.Logf("Resizing volume of %s. Waiting...", id)
+	c.WaitUntilActive(id)
+	c.Logf("Resized the volume of %s to %d GB", id, 2)
+}
+
+func (c *context) getDefaultConfig() {
+	config, err := instances.GetDefaultConfig(c.client, c.instanceID).Extract()
+	c.Logf("Default config group for instance %s: %#v", c.instanceID, config)
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/pkg.go b/acceptance/rackspace/db/v1/pkg.go
new file mode 100644
index 0000000..b7b1f99
--- /dev/null
+++ b/acceptance/rackspace/db/v1/pkg.go
@@ -0,0 +1 @@
+package v1
diff --git a/acceptance/rackspace/db/v1/replica_test.go b/acceptance/rackspace/db/v1/replica_test.go
new file mode 100644
index 0000000..89edf9d
--- /dev/null
+++ b/acceptance/rackspace/db/v1/replica_test.go
@@ -0,0 +1,33 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func (c *context) createReplica() {
+	opts := instances.CreateOpts{
+		FlavorRef: "2",
+		Size:      1,
+		Name:      tools.RandomString("gopher_db", 5),
+		ReplicaOf: c.instanceID,
+	}
+
+	repl, err := instances.Create(c.client, opts).Extract()
+	th.AssertNoErr(c.test, err)
+
+	c.Logf("Creating replica of %s. Waiting...", c.instanceID)
+	c.WaitUntilActive(repl.ID)
+	c.Logf("Created replica %#v", repl)
+
+	c.replicaID = repl.ID
+}
+
+func (c *context) detachReplica() {
+	err := instances.DetachReplica(c.client, c.replicaID).ExtractErr()
+	c.Logf("Detached replica %s", c.replicaID)
+	c.AssertNoErr(err)
+}
diff --git a/acceptance/rackspace/db/v1/user_test.go b/acceptance/rackspace/db/v1/user_test.go
new file mode 100644
index 0000000..0488f5d
--- /dev/null
+++ b/acceptance/rackspace/db/v1/user_test.go
@@ -0,0 +1,125 @@
+// +build acceptance db rackspace
+
+package v1
+
+import (
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/users"
+)
+
+func (c *context) createUsers() {
+	c.users = []string{
+		tools.RandomString("user_", 5),
+		tools.RandomString("user_", 5),
+		tools.RandomString("user_", 5),
+	}
+
+	db1 := db.CreateOpts{Name: c.DBIDs[0]}
+	db2 := db.CreateOpts{Name: c.DBIDs[1]}
+	db3 := db.CreateOpts{Name: c.DBIDs[2]}
+
+	opts := os.BatchCreateOpts{
+		os.CreateOpts{
+			Name:      c.users[0],
+			Password:  tools.RandomString("db_", 5),
+			Databases: db.BatchCreateOpts{db1, db2, db3},
+		},
+		os.CreateOpts{
+			Name:      c.users[1],
+			Password:  tools.RandomString("db_", 5),
+			Databases: db.BatchCreateOpts{db1, db2},
+		},
+		os.CreateOpts{
+			Name:      c.users[2],
+			Password:  tools.RandomString("db_", 5),
+			Databases: db.BatchCreateOpts{db3},
+		},
+	}
+
+	err := users.Create(c.client, c.instanceID, opts).ExtractErr()
+	c.Logf("Created three users on instance %s: %s, %s, %s", c.instanceID, c.users[0], c.users[1], c.users[2])
+	c.AssertNoErr(err)
+}
+
+func (c *context) listUsers() {
+	c.Logf("Listing users on instance %s", c.instanceID)
+
+	err := os.List(c.client, c.instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		uList, err := os.ExtractUsers(page)
+		c.AssertNoErr(err)
+
+		for _, u := range uList {
+			c.Logf("User: %#v", u)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) deleteUsers() {
+	for _, id := range c.users {
+		err := users.Delete(c.client, c.instanceID, id).ExtractErr()
+		c.AssertNoErr(err)
+		c.Logf("Deleted user %s", id)
+	}
+}
+
+func (c *context) changeUserPwd() {
+	opts := os.BatchCreateOpts{}
+
+	for _, name := range c.users[:1] {
+		opts = append(opts, os.CreateOpts{Name: name, Password: tools.RandomString("", 5)})
+	}
+
+	err := users.ChangePassword(c.client, c.instanceID, opts).ExtractErr()
+	c.Logf("Updated 2 users' passwords")
+	c.AssertNoErr(err)
+}
+
+func (c *context) getUser() {
+	user, err := users.Get(c.client, c.instanceID, c.users[0]).Extract()
+	c.Logf("Getting user %s", user)
+	c.AssertNoErr(err)
+}
+
+func (c *context) updateUser() {
+	opts := users.UpdateOpts{Name: tools.RandomString("new_name_", 5)}
+	err := users.Update(c.client, c.instanceID, c.users[0], opts).ExtractErr()
+	c.Logf("Updated user %s", c.users[0])
+	c.AssertNoErr(err)
+	c.users[0] = opts.Name
+}
+
+func (c *context) listUserAccess() {
+	err := users.ListAccess(c.client, c.instanceID, c.users[0]).EachPage(func(page pagination.Page) (bool, error) {
+		dbList, err := users.ExtractDBs(page)
+		c.AssertNoErr(err)
+
+		for _, db := range dbList {
+			c.Logf("User %s has access to DB: %#v", db)
+		}
+
+		return true, nil
+	})
+
+	c.AssertNoErr(err)
+}
+
+func (c *context) grantUserAccess() {
+	opts := db.BatchCreateOpts{db.CreateOpts{Name: c.DBIDs[0]}}
+	err := users.GrantAccess(c.client, c.instanceID, c.users[0], opts).ExtractErr()
+	c.Logf("Granted access for user %s to DB %s", c.users[0], c.DBIDs[0])
+	c.AssertNoErr(err)
+}
+
+func (c *context) revokeUserAccess() {
+	dbName, userName := c.DBIDs[0], c.users[0]
+	err := users.RevokeAccess(c.client, c.instanceID, userName, dbName).ExtractErr()
+	c.Logf("Revoked access for user %s to DB %s", userName, dbName)
+	c.AssertNoErr(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/pkg.go b/acceptance/rackspace/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/identity/v2/role_test.go b/acceptance/rackspace/identity/v2/role_test.go
new file mode 100644
index 0000000..efaeb75
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/role_test.go
@@ -0,0 +1,59 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/roles"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+	client := authenticatedClient(t)
+
+	userID := createUser(t, client)
+	roleID := listRoles(t, client)
+
+	addUserRole(t, client, userID, roleID)
+
+	deleteUserRole(t, client, userID, roleID)
+
+	deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+	var roleID string
+
+	err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		roleList, err := os.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		for _, role := range roleList {
+			t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+			roleID = role.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+	err := roles.AddUserRole(client, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+	err := roles.DeleteUserRole(client, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Removed role %s from user %s", roleID, userID)
+}
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/identity/v2/tokens_test.go b/acceptance/rackspace/identity/v2/tokens_test.go
new file mode 100644
index 0000000..95ee7e6
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/tokens_test.go
@@ -0,0 +1,61 @@
+// +build acceptance
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/tokens"
+	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 TestTokenAuth(t *testing.T) {
+	authedClient := createClient(t, true)
+	token := authedClient.TokenID
+
+	tenantID := os.Getenv("RS_TENANT_ID")
+	if tenantID == "" {
+		t.Skip("You must set RS_TENANT_ID environment variable to run this test")
+	}
+
+	authOpts := tokens.AuthOptions{}
+	authOpts.TenantID = tenantID
+	authOpts.Token = token
+
+	_, err := tokens.Create(authedClient, authOpts).ExtractToken()
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/rackspace/identity/v2/user_test.go b/acceptance/rackspace/identity/v2/user_test.go
new file mode 100644
index 0000000..28c0c83
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/user_test.go
@@ -0,0 +1,93 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/users"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+	client := authenticatedClient(t)
+
+	userID := createUser(t, client)
+
+	listUsers(t, client)
+
+	getUser(t, client, userID)
+
+	updateUser(t, client, userID)
+
+	resetApiKey(t, client, userID)
+
+	deleteUser(t, client, userID)
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient) string {
+	t.Log("Creating user")
+
+	opts := users.CreateOpts{
+		Username: tools.RandomString("user_", 5),
+		Enabled:  os.Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created user %s", user.ID)
+
+	return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+	err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		userList, err := os.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		for _, user := range userList {
+			t.Logf("Listing user: ID [%s] Username [%s] Email [%s] Enabled? [%s]",
+				user.ID, user.Username, user.Email, strconv.FormatBool(user.Enabled))
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	_, err := users.Get(client, userID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	opts := users.UpdateOpts{Username: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+	user, err := users.Update(client, userID, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated user %s: Username [%s] Email [%s]", userID, user.Username, user.Email)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	res := users.Delete(client, userID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted user %s", userID)
+}
+
+func resetApiKey(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	key, err := users.ResetAPIKey(client, userID).Extract()
+	th.AssertNoErr(t, err)
+
+	if key.APIKey == "" {
+		t.Fatal("failed to reset API key for user")
+	}
+
+	t.Logf("Reset API key for user %s to %s", key.Username, key.APIKey)
+}
diff --git a/acceptance/rackspace/lb/v1/acl_test.go b/acceptance/rackspace/lb/v1/acl_test.go
new file mode 100644
index 0000000..7a38027
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/acl_test.go
@@ -0,0 +1,94 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestACL(t *testing.T) {
+	client := setup(t)
+
+	ids := createLB(t, client, 1)
+	lbID := ids[0]
+
+	createACL(t, client, lbID)
+
+	waitForLB(client, lbID, lbs.ACTIVE)
+
+	networkIDs := showACL(t, client, lbID)
+
+	deleteNetworkItem(t, client, lbID, networkIDs[0])
+	waitForLB(client, lbID, lbs.ACTIVE)
+
+	bulkDeleteACL(t, client, lbID, networkIDs[1:2])
+	waitForLB(client, lbID, lbs.ACTIVE)
+
+	deleteACL(t, client, lbID)
+	waitForLB(client, lbID, lbs.ACTIVE)
+
+	deleteLB(t, client, lbID)
+}
+
+func createACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	opts := acl.CreateOpts{
+		acl.CreateOpt{Address: "206.160.163.21", Type: acl.DENY},
+		acl.CreateOpt{Address: "206.160.165.11", Type: acl.DENY},
+		acl.CreateOpt{Address: "206.160.165.12", Type: acl.DENY},
+		acl.CreateOpt{Address: "206.160.165.13", Type: acl.ALLOW},
+	}
+
+	err := acl.Create(client, lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created ACL items for LB %d", lbID)
+}
+
+func showACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) []int {
+	ids := []int{}
+
+	err := acl.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) {
+		accessList, err := acl.ExtractAccessList(page)
+		th.AssertNoErr(t, err)
+
+		for _, i := range accessList {
+			t.Logf("Listing network item: ID [%s] Address [%s] Type [%s]", i.ID, i.Address, i.Type)
+			ids = append(ids, i.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	return ids
+}
+
+func deleteNetworkItem(t *testing.T, client *gophercloud.ServiceClient, lbID, itemID int) {
+	err := acl.Delete(client, lbID, itemID).ExtractErr()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted network item %d", itemID)
+}
+
+func bulkDeleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int, items []int) {
+	err := acl.BulkDelete(client, lbID, items).ExtractErr()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted network items %s", intsToStr(items))
+}
+
+func deleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	err := acl.DeleteAll(client, lbID).ExtractErr()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted ACL from LB %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/common.go b/acceptance/rackspace/lb/v1/common.go
new file mode 100644
index 0000000..4ce05e6
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/common.go
@@ -0,0 +1,62 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"os"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newProvider() (*gophercloud.ProviderClient, error) {
+	opts, err := rackspace.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+	opts = tools.OnlyRS(opts)
+
+	return rackspace.AuthenticatedClient(opts)
+}
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	provider, err := newProvider()
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewLBV1(provider, gophercloud.EndpointOpts{
+		Region: os.Getenv("RS_REGION"),
+	})
+}
+
+func newComputeClient() (*gophercloud.ServiceClient, error) {
+	provider, err := newProvider()
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewComputeV2(provider, gophercloud.EndpointOpts{
+		Region: os.Getenv("RS_REGION"),
+	})
+}
+
+func setup(t *testing.T) *gophercloud.ServiceClient {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	return client
+}
+
+func intsToStr(ids []int) string {
+	strIDs := []string{}
+	for _, id := range ids {
+		strIDs = append(strIDs, strconv.Itoa(id))
+	}
+	return strings.Join(strIDs, ", ")
+}
diff --git a/acceptance/rackspace/lb/v1/lb_test.go b/acceptance/rackspace/lb/v1/lb_test.go
new file mode 100644
index 0000000..c67ddec
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/lb_test.go
@@ -0,0 +1,214 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestLBs(t *testing.T) {
+	client := setup(t)
+
+	ids := createLB(t, client, 3)
+	id := ids[0]
+
+	listLBProtocols(t, client)
+
+	listLBAlgorithms(t, client)
+
+	listLBs(t, client)
+
+	getLB(t, client, id)
+
+	checkLBLogging(t, client, id)
+
+	checkErrorPage(t, client, id)
+
+	getStats(t, client, id)
+
+	updateLB(t, client, id)
+
+	deleteLB(t, client, id)
+
+	batchDeleteLBs(t, client, ids[1:])
+}
+
+func createLB(t *testing.T, client *gophercloud.ServiceClient, count int) []int {
+	ids := []int{}
+
+	for i := 0; i < count; i++ {
+		opts := lbs.CreateOpts{
+			Name:     tools.RandomString("test_", 5),
+			Port:     80,
+			Protocol: "HTTP",
+			VIPs: []vips.VIP{
+				vips.VIP{Type: vips.PUBLIC},
+			},
+		}
+
+		lb, err := lbs.Create(client, opts).Extract()
+		th.AssertNoErr(t, err)
+
+		t.Logf("Created LB %d - waiting for it to build...", lb.ID)
+		waitForLB(client, lb.ID, lbs.ACTIVE)
+		t.Logf("LB %d has reached ACTIVE state", lb.ID)
+
+		ids = append(ids, lb.ID)
+	}
+
+	return ids
+}
+
+func waitForLB(client *gophercloud.ServiceClient, id int, state lbs.Status) {
+	gophercloud.WaitFor(60, func() (bool, error) {
+		lb, err := lbs.Get(client, id).Extract()
+		if err != nil {
+			return false, err
+		}
+		if lb.Status != state {
+			return false, nil
+		}
+		return true, nil
+	})
+}
+
+func listLBProtocols(t *testing.T, client *gophercloud.ServiceClient) {
+	err := lbs.ListProtocols(client).EachPage(func(page pagination.Page) (bool, error) {
+		pList, err := lbs.ExtractProtocols(page)
+		th.AssertNoErr(t, err)
+
+		for _, p := range pList {
+			t.Logf("Listing protocol: Name [%s]", p.Name)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func listLBAlgorithms(t *testing.T, client *gophercloud.ServiceClient) {
+	err := lbs.ListAlgorithms(client).EachPage(func(page pagination.Page) (bool, error) {
+		aList, err := lbs.ExtractAlgorithms(page)
+		th.AssertNoErr(t, err)
+
+		for _, a := range aList {
+			t.Logf("Listing algorithm: Name [%s]", a.Name)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func listLBs(t *testing.T, client *gophercloud.ServiceClient) {
+	err := lbs.List(client, lbs.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		lbList, err := lbs.ExtractLBs(page)
+		th.AssertNoErr(t, err)
+
+		for _, lb := range lbList {
+			t.Logf("Listing LB: ID [%d] Name [%s] Protocol [%s] Status [%s] Node count [%d] Port [%d]",
+				lb.ID, lb.Name, lb.Protocol, lb.Status, lb.NodeCount, lb.Port)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	lb, err := lbs.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting LB %d: Created [%s] VIPs [%#v] Logging [%#v] Persistence [%#v] SourceAddrs [%#v]",
+		lb.ID, lb.Created, lb.VIPs, lb.ConnectionLogging, lb.SessionPersistence, lb.SourceAddrs)
+}
+
+func updateLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	opts := lbs.UpdateOpts{
+		Name:          tools.RandomString("new_", 5),
+		Protocol:      "TCP",
+		HalfClosed:    gophercloud.Enabled,
+		Algorithm:     "RANDOM",
+		Port:          8080,
+		Timeout:       100,
+		HTTPSRedirect: gophercloud.Disabled,
+	}
+
+	err := lbs.Update(client, id, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updating LB %d - waiting for it to finish", id)
+	waitForLB(client, id, lbs.ACTIVE)
+	t.Logf("LB %d has reached ACTIVE state", id)
+}
+
+func deleteLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	err := lbs.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted LB %d", id)
+}
+
+func batchDeleteLBs(t *testing.T, client *gophercloud.ServiceClient, ids []int) {
+	err := lbs.BulkDelete(client, ids).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted LB %s", intsToStr(ids))
+}
+
+func checkLBLogging(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	err := lbs.EnableLogging(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Enabled logging for LB %d", id)
+
+	res, err := lbs.IsLoggingEnabled(client, id)
+	th.AssertNoErr(t, err)
+	t.Logf("LB %d log enabled? %s", id, strconv.FormatBool(res))
+
+	waitForLB(client, id, lbs.ACTIVE)
+
+	err = lbs.DisableLogging(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Disabled logging for LB %d", id)
+}
+
+func checkErrorPage(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	content, err := lbs.SetErrorPage(client, id, "<html>New content!</html>").Extract()
+	t.Logf("Set error page for LB %d", id)
+
+	content, err = lbs.GetErrorPage(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Error page for LB %d: %s", id, content)
+
+	err = lbs.DeleteErrorPage(client, id).ExtractErr()
+	t.Logf("Deleted error page for LB %d", id)
+}
+
+func getStats(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	waitForLB(client, id, lbs.ACTIVE)
+
+	stats, err := lbs.GetStats(client, id).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Stats for LB %d: %#v", id, stats)
+}
+
+func checkCaching(t *testing.T, client *gophercloud.ServiceClient, id int) {
+	err := lbs.EnableCaching(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Enabled caching for LB %d", id)
+
+	res, err := lbs.IsContentCached(client, id)
+	th.AssertNoErr(t, err)
+	t.Logf("Is caching enabled for LB? %s", strconv.FormatBool(res))
+
+	err = lbs.DisableCaching(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Disabled caching for LB %d", id)
+}
diff --git a/acceptance/rackspace/lb/v1/monitor_test.go b/acceptance/rackspace/lb/v1/monitor_test.go
new file mode 100644
index 0000000..c1a8e24
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/monitor_test.go
@@ -0,0 +1,60 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMonitors(t *testing.T) {
+	client := setup(t)
+
+	ids := createLB(t, client, 1)
+	lbID := ids[0]
+
+	getMonitor(t, client, lbID)
+
+	updateMonitor(t, client, lbID)
+
+	deleteMonitor(t, client, lbID)
+
+	deleteLB(t, client, lbID)
+}
+
+func getMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	hm, err := monitors.Get(client, lbID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Health monitor for LB %d: Type [%s] Delay [%d] Timeout [%d] AttemptLimit [%d]",
+		lbID, hm.Type, hm.Delay, hm.Timeout, hm.AttemptLimit)
+}
+
+func updateMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	opts := monitors.UpdateHTTPMonitorOpts{
+		AttemptLimit: 3,
+		Delay:        10,
+		Timeout:      10,
+		BodyRegex:    "hello is it me you're looking for",
+		Path:         "/foo",
+		StatusRegex:  "200",
+		Type:         monitors.HTTP,
+	}
+
+	err := monitors.Update(client, lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	waitForLB(client, lbID, lbs.ACTIVE)
+	t.Logf("Updated monitor for LB %d", lbID)
+}
+
+func deleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	err := monitors.Delete(client, lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	waitForLB(client, lbID, lbs.ACTIVE)
+	t.Logf("Deleted monitor for LB %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/node_test.go b/acceptance/rackspace/lb/v1/node_test.go
new file mode 100644
index 0000000..18b9fe7
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/node_test.go
@@ -0,0 +1,175 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNodes(t *testing.T) {
+	client := setup(t)
+
+	serverIP := findServer(t)
+	ids := createLB(t, client, 1)
+	lbID := ids[0]
+
+	nodeID := addNodes(t, client, lbID, serverIP)
+
+	listNodes(t, client, lbID)
+
+	getNode(t, client, lbID, nodeID)
+
+	updateNode(t, client, lbID, nodeID)
+
+	listEvents(t, client, lbID)
+
+	deleteNode(t, client, lbID, nodeID)
+
+	waitForLB(client, lbID, lbs.ACTIVE)
+	deleteLB(t, client, lbID)
+}
+
+func findServer(t *testing.T) string {
+	var serverIP string
+
+	client, err := newComputeClient()
+	th.AssertNoErr(t, err)
+
+	err = servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		sList, err := servers.ExtractServers(page)
+		th.AssertNoErr(t, err)
+
+		for _, s := range sList {
+			serverIP = s.AccessIPv4
+			t.Logf("Found an existing server: ID [%s] Public IP [%s]", s.ID, serverIP)
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if serverIP == "" {
+		t.Log("No server found, creating one")
+
+		imageRef := os.Getenv("RS_IMAGE_ID")
+		if imageRef == "" {
+			t.Fatalf("OS var RS_IMAGE_ID undefined")
+		}
+		flavorRef := os.Getenv("RS_FLAVOR_ID")
+		if flavorRef == "" {
+			t.Fatalf("OS var RS_FLAVOR_ID undefined")
+		}
+
+		opts := &servers.CreateOpts{
+			Name:       tools.RandomString("lb_test_", 5),
+			ImageRef:   imageRef,
+			FlavorRef:  flavorRef,
+			DiskConfig: diskconfig.Manual,
+		}
+
+		s, err := servers.Create(client, opts).Extract()
+		th.AssertNoErr(t, err)
+		serverIP = s.AccessIPv4
+
+		t.Logf("Created server %s, waiting for it to build", s.ID)
+		err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300)
+		th.AssertNoErr(t, err)
+		t.Logf("Server created successfully.")
+	}
+
+	return serverIP
+}
+
+func addNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int, serverIP string) int {
+	opts := nodes.CreateOpts{
+		nodes.CreateOpt{
+			Address:   serverIP,
+			Port:      80,
+			Condition: nodes.ENABLED,
+			Type:      nodes.PRIMARY,
+		},
+	}
+
+	page := nodes.Create(client, lbID, opts)
+
+	nodeList, err := page.ExtractNodes()
+	th.AssertNoErr(t, err)
+
+	var nodeID int
+	for _, n := range nodeList {
+		nodeID = n.ID
+	}
+	if nodeID == 0 {
+		t.Fatalf("nodeID could not be extracted from create response")
+	}
+
+	t.Logf("Added node %d to LB %d", nodeID, lbID)
+	waitForLB(client, lbID, lbs.ACTIVE)
+
+	return nodeID
+}
+
+func listNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	err := nodes.List(client, lbID, nil).EachPage(func(page pagination.Page) (bool, error) {
+		nodeList, err := nodes.ExtractNodes(page)
+		th.AssertNoErr(t, err)
+
+		for _, n := range nodeList {
+			t.Logf("Listing node: ID [%d] Address [%s:%d] Status [%s]", n.ID, n.Address, n.Port, n.Status)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func getNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+	node, err := nodes.Get(client, lbID, nodeID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting node %d: Type [%s] Weight [%d]", nodeID, node.Type, node.Weight)
+}
+
+func updateNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+	opts := nodes.UpdateOpts{
+		Weight:    gophercloud.IntToPointer(10),
+		Condition: nodes.DRAINING,
+		Type:      nodes.SECONDARY,
+	}
+	err := nodes.Update(client, lbID, nodeID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated node %d", nodeID)
+	waitForLB(client, lbID, lbs.ACTIVE)
+}
+
+func listEvents(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	pager := nodes.ListEvents(client, lbID, nodes.ListEventsOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		eventList, err := nodes.ExtractNodeEvents(page)
+		th.AssertNoErr(t, err)
+
+		for _, e := range eventList {
+			t.Logf("Listing events for node %d: Type [%s] Msg [%s] Severity [%s] Date [%s]",
+				e.NodeID, e.Type, e.DetailedMessage, e.Severity, e.Created)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func deleteNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+	err := nodes.Delete(client, lbID, nodeID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted node %d", nodeID)
+}
diff --git a/acceptance/rackspace/lb/v1/session_test.go b/acceptance/rackspace/lb/v1/session_test.go
new file mode 100644
index 0000000..8d85655
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/session_test.go
@@ -0,0 +1,47 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSession(t *testing.T) {
+	client := setup(t)
+
+	ids := createLB(t, client, 1)
+	lbID := ids[0]
+
+	getSession(t, client, lbID)
+
+	enableSession(t, client, lbID)
+	waitForLB(client, lbID, "ACTIVE")
+
+	disableSession(t, client, lbID)
+	waitForLB(client, lbID, "ACTIVE")
+
+	deleteLB(t, client, lbID)
+}
+
+func getSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	sp, err := sessions.Get(client, lbID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Session config: Type [%s]", sp.Type)
+}
+
+func enableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	opts := sessions.CreateOpts{Type: sessions.HTTPCOOKIE}
+	err := sessions.Enable(client, lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Enable %s sessions for %d", opts.Type, lbID)
+}
+
+func disableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	err := sessions.Disable(client, lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Disable sessions for %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/throttle_test.go b/acceptance/rackspace/lb/v1/throttle_test.go
new file mode 100644
index 0000000..1cc1235
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/throttle_test.go
@@ -0,0 +1,53 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestThrottle(t *testing.T) {
+	client := setup(t)
+
+	ids := createLB(t, client, 1)
+	lbID := ids[0]
+
+	getThrottleConfig(t, client, lbID)
+
+	createThrottleConfig(t, client, lbID)
+	waitForLB(client, lbID, "ACTIVE")
+
+	deleteThrottleConfig(t, client, lbID)
+	waitForLB(client, lbID, "ACTIVE")
+
+	deleteLB(t, client, lbID)
+}
+
+func getThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	sp, err := throttle.Get(client, lbID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Throttle config: MaxConns [%s]", sp.MaxConnections)
+}
+
+func createThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	opts := throttle.CreateOpts{
+		MaxConnections:    200,
+		MaxConnectionRate: 100,
+		MinConnections:    0,
+		RateInterval:      10,
+	}
+
+	err := throttle.Create(client, lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Enable throttling for %d", lbID)
+}
+
+func deleteThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	err := throttle.Delete(client, lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Disable throttling for %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/vip_test.go b/acceptance/rackspace/lb/v1/vip_test.go
new file mode 100644
index 0000000..bc0c2a8
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/vip_test.go
@@ -0,0 +1,83 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVIPs(t *testing.T) {
+	client := setup(t)
+
+	ids := createLB(t, client, 1)
+	lbID := ids[0]
+
+	listVIPs(t, client, lbID)
+
+	vipIDs := addVIPs(t, client, lbID, 3)
+
+	deleteVIP(t, client, lbID, vipIDs[0])
+
+	bulkDeleteVIPs(t, client, lbID, vipIDs[1:])
+
+	waitForLB(client, lbID, lbs.ACTIVE)
+	deleteLB(t, client, lbID)
+}
+
+func listVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+	err := vips.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) {
+		vipList, err := vips.ExtractVIPs(page)
+		th.AssertNoErr(t, err)
+
+		for _, vip := range vipList {
+			t.Logf("Listing VIP: ID [%s] Address [%s] Type [%s] Version [%s]",
+				vip.ID, vip.Address, vip.Type, vip.Version)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func addVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID, count int) []int {
+	ids := []int{}
+
+	for i := 0; i < count; i++ {
+		opts := vips.CreateOpts{
+			Type:    vips.PUBLIC,
+			Version: vips.IPV6,
+		}
+
+		vip, err := vips.Create(client, lbID, opts).Extract()
+		th.AssertNoErr(t, err)
+
+		t.Logf("Created VIP %d", vip.ID)
+
+		waitForLB(client, lbID, lbs.ACTIVE)
+
+		ids = append(ids, vip.ID)
+	}
+
+	return ids
+}
+
+func deleteVIP(t *testing.T, client *gophercloud.ServiceClient, lbID, vipID int) {
+	err := vips.Delete(client, lbID, vipID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted VIP %d", vipID)
+
+	waitForLB(client, lbID, lbs.ACTIVE)
+}
+
+func bulkDeleteVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int, ids []int) {
+	err := vips.BulkDelete(client, lbID, ids).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted VIPs %s", intsToStr(ids))
+}
diff --git a/acceptance/rackspace/networking/v2/common.go b/acceptance/rackspace/networking/v2/common.go
new file mode 100644
index 0000000..8170418
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/common.go
@@ -0,0 +1,39 @@
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var Client *gophercloud.ServiceClient
+
+func NewClient() (*gophercloud.ServiceClient, error) {
+	opts, err := rackspace.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	provider, err := rackspace.AuthenticatedClient(opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewNetworkV2(provider, gophercloud.EndpointOpts{
+		Name:   "cloudNetworks",
+		Region: os.Getenv("RS_REGION"),
+	})
+}
+
+func Setup(t *testing.T) {
+	client, err := NewClient()
+	th.AssertNoErr(t, err)
+	Client = client
+}
+
+func Teardown() {
+	Client = nil
+}
diff --git a/acceptance/rackspace/networking/v2/network_test.go b/acceptance/rackspace/networking/v2/network_test.go
new file mode 100644
index 0000000..3862123
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/network_test.go
@@ -0,0 +1,65 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Create a network
+	n, err := networks.Create(Client, os.CreateOpts{Name: "sample_network", AdminStateUp: os.Up}).Extract()
+	th.AssertNoErr(t, err)
+	defer networks.Delete(Client, n.ID)
+	th.AssertEquals(t, "sample_network", n.Name)
+	th.AssertEquals(t, true, n.AdminStateUp)
+	networkID := n.ID
+
+	// List networks
+	pager := networks.List(Client, os.ListOpts{Limit: 2})
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		networkList, err := os.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, "ACTIVE", n.Status)
+	th.AssertDeepEquals(t, []string{}, n.Subnets)
+	th.AssertEquals(t, "sample_network", n.Name)
+	th.AssertEquals(t, true, n.AdminStateUp)
+	th.AssertEquals(t, false, n.Shared)
+	th.AssertEquals(t, networkID, n.ID)
+
+	// Update network
+	n, err = networks.Update(Client, networkID, os.UpdateOpts{Name: "new_network_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "new_network_name", n.Name)
+
+	// Delete network
+	res := networks.Delete(Client, networkID)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/rackspace/networking/v2/port_test.go b/acceptance/rackspace/networking/v2/port_test.go
new file mode 100644
index 0000000..3c42bb2
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/port_test.go
@@ -0,0 +1,116 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/ports"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/subnets"
+	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, portID, p.ID)
+
+	// Update port
+	p, err = ports.Update(Client, portID, osPorts.UpdateOpts{Name: "new_port_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "new_port_name", p.Name)
+
+	// Delete port
+	res := ports.Delete(Client, portID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func createPort(t *testing.T, networkID, subnetID string) string {
+	enable := true
+	opts := osPorts.CreateOpts{
+		NetworkID:    networkID,
+		Name:         "my_port",
+		AdminStateUp: &enable,
+		FixedIPs:     []osPorts.IP{osPorts.IP{SubnetID: subnetID}},
+	}
+	p, err := ports.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, networkID, p.NetworkID)
+	th.AssertEquals(t, "my_port", p.Name)
+	th.AssertEquals(t, true, p.AdminStateUp)
+
+	return p.ID
+}
+
+func listPorts(t *testing.T) {
+	count := 0
+	pager := ports.List(Client, osPorts.ListOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- Page ---")
+
+		portList, err := osPorts.ExtractPorts(page)
+		th.AssertNoErr(t, err)
+
+		for _, p := range portList {
+			t.Logf("Port: ID [%s] Name [%s] Status [%s] 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, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract()
+	return res.ID, err
+}
+
+func createSubnet(networkID string) (string, error) {
+	s, err := subnets.Create(Client, osSubnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  osSubnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: osSubnets.Down,
+	}).Extract()
+	return s.ID, err
+}
diff --git a/acceptance/rackspace/networking/v2/security_test.go b/acceptance/rackspace/networking/v2/security_test.go
new file mode 100644
index 0000000..ec02991
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/security_test.go
@@ -0,0 +1,165 @@
+// +build acceptance networking security
+
+package v2
+
+import (
+	"testing"
+
+	osGroups "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups"
+	osRules "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/pagination"
+	rsNetworks "github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	rsPorts "github.com/rackspace/gophercloud/rackspace/networking/v2/ports"
+	rsGroups "github.com/rackspace/gophercloud/rackspace/networking/v2/security/groups"
+	rsRules "github.com/rackspace/gophercloud/rackspace/networking/v2/security/rules"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecurityGroups(t *testing.T) {
+	Setup(t)
+	defer 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)
+}
+
+func TestSecurityGroupRules(t *testing.T) {
+	Setup(t)
+	defer 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 := rsGroups.Create(Client, osGroups.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 := rsGroups.List(Client, osGroups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		list, err := osGroups.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 := rsGroups.Get(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 createSecGroupPort(t *testing.T, groupID string) (string, string) {
+	n, err := rsNetworks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network"}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created network %s", n.ID)
+
+	opts := osPorts.CreateOpts{
+		NetworkID:      n.ID,
+		Name:           "my_port",
+		SecurityGroups: []string{groupID},
+	}
+	p, err := rsPorts.Create(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 := rsGroups.Delete(Client, groupID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted security group %s", groupID)
+}
+
+func createSecRule(t *testing.T, groupID string) string {
+	r, err := rsRules.Create(Client, osRules.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 := rsRules.List(Client, osRules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		list, err := osRules.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 := rsRules.Get(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 := rsRules.Delete(Client, id)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted security rule %s", id)
+}
diff --git a/acceptance/rackspace/networking/v2/subnet_test.go b/acceptance/rackspace/networking/v2/subnet_test.go
new file mode 100644
index 0000000..c401432
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/subnet_test.go
@@ -0,0 +1,84 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/subnets"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListSubnets(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	pager := subnets.List(Client, osSubnets.ListOpts{Limit: 2})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		subnetList, err := osSubnets.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 TestSubnetCRUD(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Setup network
+	t.Log("Setting up network")
+	n, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract()
+	th.AssertNoErr(t, err)
+	networkID := n.ID
+	defer networks.Delete(Client, networkID)
+
+	// Create subnet
+	t.Log("Create subnet")
+	enable := false
+	opts := osSubnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  osSubnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: &enable,
+	}
+	s, err := subnets.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, networkID, s.NetworkID)
+	th.AssertEquals(t, "192.168.199.0/24", s.CIDR)
+	th.AssertEquals(t, 4, s.IPVersion)
+	th.AssertEquals(t, "my_subnet", s.Name)
+	th.AssertEquals(t, false, s.EnableDHCP)
+	subnetID := s.ID
+
+	// Get subnet
+	t.Log("Getting subnet")
+	s, err = subnets.Get(Client, subnetID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, subnetID, s.ID)
+
+	// Update subnet
+	t.Log("Update subnet")
+	s, err = subnets.Update(Client, subnetID, osSubnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "new_subnet_name", s.Name)
+
+	// Delete subnet
+	t.Log("Delete subnet")
+	res := subnets.Delete(Client, subnetID)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/rackspace/objectstorage/v1/accounts_test.go b/acceptance/rackspace/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..8b3cde4
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/accounts_test.go
@@ -0,0 +1,38 @@
+// +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)
+
+	updateHeaders, err := raxAccounts.Update(c, raxAccounts.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Update Account Response Headers: %+v\n", updateHeaders)
+	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"], "")
+	}()
+
+	getResp := raxAccounts.Get(c)
+	th.AssertNoErr(t, getResp.Err)
+
+	getHeaders, _ := getResp.Extract()
+	t.Logf("Get Account Response Headers: %+v\n", getHeaders)
+
+	metadata, _ := getResp.ExtractMetadata()
+	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..0f56f49
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/cdncontainers_test.go
@@ -0,0 +1,66 @@
+// +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)
+	enableRes := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900})
+	t.Logf("Header map from Enable CDN Container request: %+v\n", enableRes.Header)
+	enableHeader, err := enableRes.Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Enable CDN Container request: %+v\n", enableHeader)
+
+	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.")
+	}
+
+	updateOpts := raxCDNContainers.UpdateOpts{XCDNEnabled: raxCDNContainers.Disabled, XLogRetention: raxCDNContainers.Enabled}
+	updateHeader, err := raxCDNContainers.Update(raxCDNClient, "gophercloud-test", updateOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Update CDN Container request: %+v\n", updateHeader)
+
+	getRes := raxCDNContainers.Get(raxCDNClient, "gophercloud-test")
+	getHeader, err := getRes.Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Get CDN Container request (after update): %+v\n", getHeader)
+	metadata, err := getRes.ExtractMetadata()
+	t.Logf("Metadata 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..0c0ab8a
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/cdnobjects_test.go
@@ -0,0 +1,50 @@
+// +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)
+	}()
+
+	header, err := raxObjects.Create(raxClient, "gophercloud-test", "test-object", bytes.NewBufferString("gophercloud cdn test"), nil).ExtractHeader()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Create Object request: %+v\n", 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)
+
+	enableHeader, err := raxCDNContainers.Enable(raxCDNClient, "gophercloud-test", raxCDNContainers.EnableOpts{CDNEnabled: true, TTL: 900}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Enable CDN Container request: %+v\n", enableHeader)
+
+	objCDNURL, err := raxCDNObjects.CDNURL(raxCDNClient, "gophercloud-test", "test-object")
+	th.AssertNoErr(t, err)
+	t.Logf("%s CDN URL: %s\n", "test_object", objCDNURL)
+
+	deleteHeader, err := raxCDNObjects.Delete(raxCDNClient, "gophercloud-test", "test-object", nil).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Delete CDN Object request: %+v\n", deleteHeader)
+}
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..c895513
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/containers_test.go
@@ -0,0 +1,90 @@
+// +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.")
+	}
+
+	createHeader, err := raxContainers.Create(c, "gophercloud-test", nil).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Create Container request: %+v\n", createHeader)
+	defer func() {
+		deleteres := raxContainers.Delete(c, "gophercloud-test")
+		deleteHeader, err := deleteres.Extract()
+		th.AssertNoErr(t, err)
+		t.Logf("Headers from Delete Container request: %+v\n", deleteres.Header)
+		t.Logf("Headers from Delete Container request: %+v\n", deleteHeader)
+	}()
+
+	updateHeader, err := raxContainers.Update(c, "gophercloud-test", raxContainers.UpdateOpts{Metadata: map[string]string{"white": "mountains"}}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Update Container request: %+v\n", updateHeader)
+	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 Container request (after update reverted): %+v\n", metadata)
+		th.CheckEquals(t, metadata["White"], "")
+	}()
+
+	getres := raxContainers.Get(c, "gophercloud-test")
+	getHeader, err := getres.Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Headers from Get Container request (after update): %+v\n", getHeader)
+	metadata, err := getres.ExtractMetadata()
+	t.Logf("Metadata from Get Container 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..585dea7
--- /dev/null
+++ b/acceptance/rackspace/objectstorage/v1/objects_test.go
@@ -0,0 +1,124 @@
+// +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() {
+		t.Logf("Deleting container...")
+		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() {
+		t.Logf("Deleting object o1...")
+		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() {
+		t.Logf("Deleting object o2...")
+		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, "mountains", metadata["White"])
+
+	createTempURLOpts := osObjects.CreateTempURLOpts{
+		Method: osObjects.GET,
+		TTL:    600,
+	}
+	tempURL, err := raxObjects.CreateTempURL(c, "gophercloud-test", "o1", createTempURLOpts)
+	th.AssertNoErr(t, err)
+	t.Logf("TempURL for object (%s): %s", "o1", tempURL)
+}
diff --git a/acceptance/rackspace/orchestration/v1/buildinfo_test.go b/acceptance/rackspace/orchestration/v1/buildinfo_test.go
new file mode 100644
index 0000000..42cc048
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/buildinfo_test.go
@@ -0,0 +1,20 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBuildInfo(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	bi, err := buildinfo.Get(client).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("retrieved build info: %+v\n", bi)
+}
diff --git a/acceptance/rackspace/orchestration/v1/common.go b/acceptance/rackspace/orchestration/v1/common.go
new file mode 100644
index 0000000..b9d5197
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/common.go
@@ -0,0 +1,45 @@
+// +build acceptance
+
+package v1
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var template = fmt.Sprintf(`
+{
+		"heat_template_version": "2013-05-23",
+		"description": "Simple template to test heat commands",
+		"parameters": {},
+		"resources": {
+				"hello_world": {
+						"type":"OS::Nova::Server",
+						"properties": {
+								"flavor": "%s",
+								"image": "%s",
+								"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+						}
+				}
+		}
+}
+`, os.Getenv("RS_FLAVOR_ID"), os.Getenv("RS_IMAGE_ID"))
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+	ao, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := rackspace.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	c, err := rackspace.NewOrchestrationV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("RS_REGION_NAME"),
+	})
+	th.AssertNoErr(t, err)
+	return c
+}
diff --git a/acceptance/rackspace/orchestration/v1/stackevents_test.go b/acceptance/rackspace/orchestration/v1/stackevents_test.go
new file mode 100644
index 0000000..9e3fc08
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stackevents_test.go
@@ -0,0 +1,70 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	osStackEvents "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+	osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackEvents(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName := "postman_stack_2"
+	resourceName := "hello_world"
+	var eventID string
+
+	createOpts := osStacks.CreateOpts{
+		Name:     stackName,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) {
+		events, err := osStackEvents.ExtractEvents(page)
+		th.AssertNoErr(t, err)
+		t.Logf("listed events: %+v\n", events)
+		eventID = events[0].ID
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+
+	err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) {
+		resourceEvents, err := osStackEvents.ExtractResourceEvents(page)
+		th.AssertNoErr(t, err)
+		t.Logf("listed resource events: %+v\n", resourceEvents)
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+
+	event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("retrieved event: %+v\n", event)
+}
diff --git a/acceptance/rackspace/orchestration/v1/stackresources_test.go b/acceptance/rackspace/orchestration/v1/stackresources_test.go
new file mode 100644
index 0000000..65926e7
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stackresources_test.go
@@ -0,0 +1,64 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	osStackResources "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+	osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackResources(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName := "postman_stack_2"
+
+	createOpts := osStacks.CreateOpts{
+		Name:     stackName,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	resourceName := "hello_world"
+	resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack resource: %+v\n", resource)
+
+	metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack resource metadata: %+v\n", metadata)
+
+	err = stackresources.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) {
+		resources, err := osStackResources.ExtractResources(page)
+		th.AssertNoErr(t, err)
+		t.Logf("resources: %+v\n", resources)
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/rackspace/orchestration/v1/stacks_test.go b/acceptance/rackspace/orchestration/v1/stacks_test.go
new file mode 100644
index 0000000..61969b5
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stacks_test.go
@@ -0,0 +1,154 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStacks(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName1 := "gophercloud-test-stack-2"
+	createOpts := osStacks.CreateOpts{
+		Name:     stackName1,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName1, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName1)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	updateOpts := osStacks.UpdateOpts{
+		Template: template,
+		Timeout:  20,
+	}
+	err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "UPDATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	t.Logf("Updated stack")
+
+	err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		stackList, err := osStacks.ExtractStacks(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Got stack list: %+v\n", stackList)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack: %+v\n", getStack)
+
+	abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Abandonded stack %+v\n", abandonedStack)
+	th.AssertNoErr(t, err)
+}
+
+// Test using the updated interface
+func TestStacksNewTemplateFormat(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName1 := "gophercloud-test-stack-2"
+	templateOpts := new(osStacks.Template)
+	templateOpts.Bin = []byte(template)
+	createOpts := osStacks.CreateOpts{
+		Name:         stackName1,
+		TemplateOpts: templateOpts,
+		Timeout:      5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName1, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName1)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	updateOpts := osStacks.UpdateOpts{
+		TemplateOpts: templateOpts,
+		Timeout:      20,
+	}
+	err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "UPDATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	t.Logf("Updated stack")
+
+	err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		stackList, err := osStacks.ExtractStacks(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Got stack list: %+v\n", stackList)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got stack: %+v\n", getStack)
+
+	abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Abandonded stack %+v\n", abandonedStack)
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/rackspace/orchestration/v1/stacktemplates_test.go b/acceptance/rackspace/orchestration/v1/stacktemplates_test.go
new file mode 100644
index 0000000..e4ccd9e
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stacktemplates_test.go
@@ -0,0 +1,77 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	osStacktemplates "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackTemplates(t *testing.T) {
+	// Create a provider client for making the HTTP requests.
+	// See common.go in this directory for more information.
+	client := newClient(t)
+
+	stackName := "postman_stack_2"
+
+	createOpts := osStacks.CreateOpts{
+		Name:     stackName,
+		Template: template,
+		Timeout:  5,
+	}
+	stack, err := stacks.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created stack: %+v\n", stack)
+	defer func() {
+		err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+		t.Logf("Deleted stack (%s)", stackName)
+	}()
+	err = gophercloud.WaitFor(60, func() (bool, error) {
+		getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+		if getStack.Status == "CREATE_COMPLETE" {
+			return true, nil
+		}
+		return false, nil
+	})
+
+	tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("retrieved template: %+v\n", tmpl)
+
+	validateOpts := osStacktemplates.ValidateOpts{
+		Template: `{"heat_template_version": "2013-05-23",
+			"description":           "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type":    "string",
+				},
+			},
+			"resources": {
+				"hello_world": {
+					"type": "OS::Nova::Server",
+					"properties": {
+						"key_name": "heat_key",
+						"flavor": {
+							"get_param": "flavor",
+						},
+						"image":     "ad091b52-742f-469e-8f3c-fd81cadf0743",
+						"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n",
+					},
+				},
+			},
+		}`}
+	validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("validated template: %+v\n", validatedTemplate)
+}
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/rackspace/rackconnect/v3/cloudnetworks_test.go b/acceptance/rackspace/rackconnect/v3/cloudnetworks_test.go
new file mode 100644
index 0000000..2c6287e
--- /dev/null
+++ b/acceptance/rackspace/rackconnect/v3/cloudnetworks_test.go
@@ -0,0 +1,36 @@
+// +build acceptance
+
+package v3
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/rackconnect/v3/cloudnetworks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCloudNetworks(t *testing.T) {
+	c := newClient(t)
+	cnID := testListNetworks(t, c)
+	testGetNetworks(t, c, cnID)
+}
+
+func testListNetworks(t *testing.T, c *gophercloud.ServiceClient) string {
+	allPages, err := cloudnetworks.List(c).AllPages()
+	th.AssertNoErr(t, err)
+	allcn, err := cloudnetworks.ExtractCloudNetworks(allPages)
+	fmt.Printf("Listing all cloud networks: %+v\n\n", allcn)
+	var cnID string
+	if len(allcn) > 0 {
+		cnID = allcn[0].ID
+	}
+	return cnID
+}
+
+func testGetNetworks(t *testing.T, c *gophercloud.ServiceClient, id string) {
+	cn, err := cloudnetworks.Get(c, id).Extract()
+	th.AssertNoErr(t, err)
+	fmt.Printf("Retrieved cloud network: %+v\n\n", cn)
+}
diff --git a/acceptance/rackspace/rackconnect/v3/common.go b/acceptance/rackspace/rackconnect/v3/common.go
new file mode 100644
index 0000000..8c75314
--- /dev/null
+++ b/acceptance/rackspace/rackconnect/v3/common.go
@@ -0,0 +1,26 @@
+// +build acceptance
+
+package v3
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+	ao, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := rackspace.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	c, err := rackspace.NewRackConnectV3(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("RS_REGION_NAME"),
+	})
+	th.AssertNoErr(t, err)
+	return c
+}
diff --git a/acceptance/rackspace/rackconnect/v3/lbpools_test.go b/acceptance/rackspace/rackconnect/v3/lbpools_test.go
new file mode 100644
index 0000000..85ac931
--- /dev/null
+++ b/acceptance/rackspace/rackconnect/v3/lbpools_test.go
@@ -0,0 +1,71 @@
+// +build acceptance
+
+package v3
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/rackconnect/v3/lbpools"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestLBPools(t *testing.T) {
+	c := newClient(t)
+	pID := testListPools(t, c)
+	testGetPools(t, c, pID)
+	nID := testListNodes(t, c, pID)
+	testListNodeDetails(t, c, pID)
+	testGetNode(t, c, pID, nID)
+	testGetNodeDetails(t, c, pID, nID)
+}
+
+func testListPools(t *testing.T, c *gophercloud.ServiceClient) string {
+	allPages, err := lbpools.List(c).AllPages()
+	th.AssertNoErr(t, err)
+	allp, err := lbpools.ExtractPools(allPages)
+	fmt.Printf("Listing all LB pools: %+v\n\n", allp)
+	var pID string
+	if len(allp) > 0 {
+		pID = allp[0].ID
+	}
+	return pID
+}
+
+func testGetPools(t *testing.T, c *gophercloud.ServiceClient, pID string) {
+	p, err := lbpools.Get(c, pID).Extract()
+	th.AssertNoErr(t, err)
+	fmt.Printf("Retrieved LB pool: %+v\n\n", p)
+}
+
+func testListNodes(t *testing.T, c *gophercloud.ServiceClient, pID string) string {
+	allPages, err := lbpools.ListNodes(c, pID).AllPages()
+	th.AssertNoErr(t, err)
+	alln, err := lbpools.ExtractNodes(allPages)
+	fmt.Printf("Listing all LB pool nodes for pool (%s): %+v\n\n", pID, alln)
+	var nID string
+	if len(alln) > 0 {
+		nID = alln[0].ID
+	}
+	return nID
+}
+
+func testListNodeDetails(t *testing.T, c *gophercloud.ServiceClient, pID string) {
+	allPages, err := lbpools.ListNodesDetails(c, pID).AllPages()
+	th.AssertNoErr(t, err)
+	alln, err := lbpools.ExtractNodesDetails(allPages)
+	fmt.Printf("Listing all LB pool nodes details for pool (%s): %+v\n\n", pID, alln)
+}
+
+func testGetNode(t *testing.T, c *gophercloud.ServiceClient, pID, nID string) {
+	n, err := lbpools.GetNode(c, pID, nID).Extract()
+	th.AssertNoErr(t, err)
+	fmt.Printf("Retrieved LB node: %+v\n\n", n)
+}
+
+func testGetNodeDetails(t *testing.T, c *gophercloud.ServiceClient, pID, nID string) {
+	n, err := lbpools.GetNodeDetails(c, pID, nID).Extract()
+	th.AssertNoErr(t, err)
+	fmt.Printf("Retrieved LB node details: %+v\n\n", n)
+}
diff --git a/acceptance/rackspace/rackconnect/v3/publicips_test.go b/acceptance/rackspace/rackconnect/v3/publicips_test.go
new file mode 100644
index 0000000..8dc6270
--- /dev/null
+++ b/acceptance/rackspace/rackconnect/v3/publicips_test.go
@@ -0,0 +1,45 @@
+// +build acceptance
+
+package v3
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/rackconnect/v3/publicips"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPublicIPs(t *testing.T) {
+	c := newClient(t)
+	ipID := testListIPs(t, c)
+	sID := testGetIP(t, c, ipID)
+	testListIPsForServer(t, c, sID)
+}
+
+func testListIPs(t *testing.T, c *gophercloud.ServiceClient) string {
+	allPages, err := publicips.List(c).AllPages()
+	th.AssertNoErr(t, err)
+	allip, err := publicips.ExtractPublicIPs(allPages)
+	fmt.Printf("Listing all public IPs: %+v\n\n", allip)
+	var ipID string
+	if len(allip) > 0 {
+		ipID = allip[0].ID
+	}
+	return ipID
+}
+
+func testGetIP(t *testing.T, c *gophercloud.ServiceClient, ipID string) string {
+	ip, err := publicips.Get(c, ipID).Extract()
+	th.AssertNoErr(t, err)
+	fmt.Printf("Retrieved public IP (%s): %+v\n\n", ipID, ip)
+	return ip.CloudServer.ID
+}
+
+func testListIPsForServer(t *testing.T, c *gophercloud.ServiceClient, sID string) {
+	allPages, err := publicips.ListForServer(c, sID).AllPages()
+	th.AssertNoErr(t, err)
+	allip, err := publicips.ExtractPublicIPs(allPages)
+	fmt.Printf("Listing all public IPs for server (%s): %+v\n\n", sID, allip)
+}
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..35679b7
--- /dev/null
+++ b/acceptance/tools/tools.go
@@ -0,0 +1,89 @@
+// +build acceptance common
+
+package tools
+
+import (
+	"crypto/rand"
+	"errors"
+	mrand "math/rand"
+	"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)
+}
+
+// RandomInt will return a random integer between a specified range.
+func RandomInt(min, max int) int {
+	mrand.Seed(time.Now().Unix())
+	return mrand.Intn(max-min) + min
+}
+
+// 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/auth_options.go b/auth_options.go
new file mode 100644
index 0000000..d26e16a
--- /dev/null
+++ b/auth_options.go
@@ -0,0 +1,50 @@
+package gophercloud
+
+/*
+AuthOptions stores information needed to authenticate to an OpenStack cluster.
+You can populate one manually, or use a provider's AuthOptionsFromEnv() function
+to read relevant information from the standard environment variables. Pass one
+to a provider's AuthenticatedClient function to authenticate and obtain a
+ProviderClient representing an active session on that provider.
+
+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. While it's ultimately needed by
+	// all of the identity services, it will often be populated by a provider-level
+	// function.
+	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 are needed.
+	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
+
+	// TokenID allows users to authenticate (possibly as another user) with an
+	// authentication token ID.
+	TokenID string
+}
diff --git a/auth_results.go b/auth_results.go
new file mode 100644
index 0000000..856a233
--- /dev/null
+++ b/auth_results.go
@@ -0,0 +1,14 @@
+package gophercloud
+
+import "time"
+
+// AuthResults [deprecated] is a leftover type from the v0.x days. It was
+// intended to describe common functionality among identity service results, but
+// is not actually used anywhere.
+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/doc.go b/doc.go
new file mode 100644
index 0000000..fb81a9d
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,67 @@
+/*
+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()
+
+Intermediate Result structs are returned for API operations, which allow
+generic access to the HTTP headers, response body, and any errors associated
+with the network transaction. To turn a result into a usable resource struct,
+you must call the Extract method which is chained to the response, or an
+Extract function from an applicable extension:
+
+  result := servers.Get(client, "{serverId}")
+
+  // Attempt to extract the disk configuration from the OS-DCF disk config
+  // extension:
+  config, err := diskconfig.ExtractGet(result)
+
+All requests that enumerate a collection return a Pager struct that is used to
+iterate through the results one page at a time. Use the EachPage method on that
+Pager to handle each successive Page in a closure, then use the appropriate
+extraction method from that request's package to interpret that Page as a slice
+of results:
+
+  err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) {
+    s, err := servers.ExtractServers(page)
+    if err != nil {
+      return false, err
+    }
+
+    // Handle the []servers.Server slice.
+
+    // Return "false" or an error to prematurely stop fetching new pages.
+    return true, nil
+  })
+
+This top-level package contains utility functions and data types that are used
+throughout the provider and service packages. Of particular note for end users
+are the AuthOptions and EndpointOpts structs.
+*/
+package gophercloud
diff --git a/endpoint_search.go b/endpoint_search.go
new file mode 100644
index 0000000..5189431
--- /dev/null
+++ b/endpoint_search.go
@@ -0,0 +1,92 @@
+package gophercloud
+
+import "errors"
+
+var (
+	// ErrServiceNotFound is returned when no service in a service catalog matches
+	// the provided EndpointOpts. This is generally returned by provider service
+	// factory methods like "NewComputeV2()" and can mean that a service is not
+	// enabled for your account.
+	ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
+
+	// ErrEndpointNotFound is returned when no available endpoints match the
+	// provided EndpointOpts. This is also generally returned by provider service
+	// factory methods, and usually indicates that a region was specified
+	// incorrectly.
+	ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
+)
+
+// Availability indicates to whom a specific service endpoint is accessible:
+// the internet at large, internal networks only, or only to administrators.
+// Different identity services use different terminology for these. Identity v2
+// lists them as different kinds of URLs within the service catalog ("adminURL",
+// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an
+// endpoint's response.
+type Availability string
+
+const (
+	// AvailabilityAdmin indicates that an endpoint is only available to
+	// administrators.
+	AvailabilityAdmin Availability = "admin"
+
+	// AvailabilityPublic indicates that an endpoint is available to everyone on
+	// the internet.
+	AvailabilityPublic Availability = "public"
+
+	// AvailabilityInternal indicates that an endpoint is only available within
+	// the cluster's internal network.
+	AvailabilityInternal Availability = "internal"
+)
+
+// EndpointOpts specifies search criteria used by queries against an
+// OpenStack service catalog. The options must contain enough information to
+// unambiguously identify one, and only one, endpoint within the catalog.
+//
+// Usually, these are passed to service client factory functions in a provider
+// package, like "rackspace.NewComputeV2()".
+type EndpointOpts struct {
+	// Type [required] is the service type for the client (e.g., "compute",
+	// "object-store"). Generally, this will be supplied by the service client
+	// function, but a user-given value will be honored if provided.
+	Type string
+
+	// Name [optional] 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.
+	Name string
+
+	// Region [required] is the geographic region in which the endpoint resides,
+	// generally specifying which datacenter should house your resources.
+	// Required only for services that span multiple regions.
+	Region string
+
+	// Availability [optional] is the visibility of the endpoint to be returned.
+	// Valid types include the constants AvailabilityPublic, AvailabilityInternal,
+	// or AvailabilityAdmin from this package.
+	//
+	// Availability is not required, and defaults to AvailabilityPublic. Not all
+	// providers or services offer all Availability options.
+	Availability Availability
+}
+
+/*
+EndpointLocator is an internal function to be used by provider implementations.
+
+It provides an implementation that locates a single endpoint from a service
+catalog for a specific ProviderClient based on user-provided EndpointOpts. The
+provider then uses it to discover related ServiceClients.
+*/
+type EndpointLocator func(EndpointOpts) (string, error)
+
+// ApplyDefaults is an internal method to be used by provider implementations.
+//
+// It sets EndpointOpts fields if not already set, including a default type.
+// 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/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..bb2c259
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,21 @@
+package apiversions
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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
+	_, res.Err = client.Get(getURL(client, v), &res.Body, nil)
+	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..71936e5
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,206 @@
+package snapshots
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, id), nil)
+	return res
+}
+
+// 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 = client.Get(getURL(client, id), &res.Body, nil)
+	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 = client.Put(updateMetadataURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// IDFromName is a convienience function that returns a snapshot's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	snapshotCount := 0
+	snapshotID := ""
+	if name == "" {
+		return "", fmt.Errorf("A snapshot name must be provided.")
+	}
+	pager := List(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		snapshotList, err := ExtractSnapshots(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range snapshotList {
+			if s.Name == name {
+				snapshotCount++
+				snapshotID = s.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch snapshotCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find snapshot: %s", name)
+	case 1:
+		return snapshotID, nil
+	default:
+		return "", fmt.Errorf("Found %d snapshots matching %s", snapshotCount, name)
+	}
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..d0f9e88
--- /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)
+
+	res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..e595798
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,123 @@
+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
+}
+
+// DeleteResult contains the response body and error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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/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/requests.go b/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..3e9243a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,236 @@
+package volumes
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, id), nil)
+	return res
+}
+
+// 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 = client.Get(getURL(client, id), &res.Body, nil)
+	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 = client.Put(updateURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// IDFromName is a convienience function that returns a server's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	volumeCount := 0
+	volumeID := ""
+	if name == "" {
+		return "", fmt.Errorf("A volume name must be provided.")
+	}
+	pager := List(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		volumeList, err := ExtractVolumes(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range volumeList {
+			if s.Name == name {
+				volumeCount++
+				volumeID = s.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch volumeCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find volume: %s", name)
+	case 1:
+		return volumeID, nil
+	default:
+		return "", fmt.Errorf("Found %d volumes matching %s", volumeCount, name)
+	}
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..75c2bbc
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,123 @@
+package volumes
+
+import (
+	"testing"
+
+	fixtures "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/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()
+
+	fixtures.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 TestListAll(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	fixtures.MockListResponse(t)
+
+	allPages, err := List(client.ServiceClient(), &ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := ExtractVolumes(allPages)
+	th.AssertNoErr(t, 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)
+
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	fixtures.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")
+	th.AssertEquals(t, v.Attachments[0]["device"], "/dev/vde")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	fixtures.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()
+
+	fixtures.MockDeleteResponse(t)
+
+	res := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	fixtures.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..2fd4ef1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,113 @@
+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 []map[string]interface{} `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_description"`
+
+	// 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
+}
+
+// DeleteResult contains the response body and error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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/testing/doc.go b/openstack/blockstorage/v1/volumes/testing/doc.go
new file mode 100644
index 0000000..2f66ba5
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/testing/doc.go
@@ -0,0 +1,7 @@
+/*
+This is package created is to hold fixtures (which imports testing),
+so that importing volumes package does not inadvertently import testing into production code
+More information here:
+https://github.com/rackspace/gophercloud/issues/473
+*/
+package testing
diff --git a/openstack/blockstorage/v1/volumes/testing/fixtures.go b/openstack/blockstorage/v1/volumes/testing/fixtures.go
new file mode 100644
index 0000000..3df7653
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/testing/fixtures.go
@@ -0,0 +1,113 @@
+package testing
+
+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",
+ 	"attachments": [
+	  {
+            "device": "/dev/vde",
+            "server_id": "a740d24b-dc5b-4d59-ac75-53971c2920ba",
+            "id": "d6da11e5-2ed3-413e-88d8-b772ba62193d",
+            "volume_id": "d6da11e5-2ed3-413e-88d8-b772ba62193d"
+          }
+        ]
+   }
+}
+      `)
+	})
+}
+
+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/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/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..1673d13
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,76 @@
+package volumetypes
+
+import (
+	"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 = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, id), nil)
+	return res
+}
+
+// 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 := client.Get(getURL(client, id), &res.Body, nil)
+	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..8d40bfe
--- /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").ExtractErr()
+	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..c049a04
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,72 @@
+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
+}
+
+// DeleteResult contains the response error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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/cdn/v1/base/doc.go b/openstack/cdn/v1/base/doc.go
new file mode 100644
index 0000000..f78d4f7
--- /dev/null
+++ b/openstack/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the OpenStack CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/openstack/cdn/v1/base/fixtures.go b/openstack/cdn/v1/base/fixtures.go
new file mode 100644
index 0000000..19b5ece
--- /dev/null
+++ b/openstack/cdn/v1/base/fixtures.go
@@ -0,0 +1,53 @@
+package base
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetSuccessfully(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.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+    {
+        "resources": {
+            "rel/cdn": {
+                "href-template": "services{?marker,limit}",
+                "href-vars": {
+                    "marker": "param/marker",
+                    "limit": "param/limit"
+                },
+                "hints": {
+                    "allow": [
+                        "GET"
+                    ],
+                    "formats": {
+                        "application/json": {}
+                    }
+                }
+            }
+        }
+    }
+    `)
+
+	})
+}
+
+// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler
+// mux that responds with a `Ping` response.
+func HandlePingSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/cdn/v1/base/requests.go b/openstack/cdn/v1/base/requests.go
new file mode 100644
index 0000000..dd221bc
--- /dev/null
+++ b/openstack/cdn/v1/base/requests.go
@@ -0,0 +1,21 @@
+package base
+
+import "github.com/rackspace/gophercloud"
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c), &res.Body, nil)
+	return res
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) PingResult {
+	var res PingResult
+	_, res.Err = c.Get(pingURL(c), nil, &gophercloud.RequestOpts{
+		OkCodes:     []int{204},
+		MoreHeaders: map[string]string{"Accept": ""},
+	})
+	return res
+}
diff --git a/openstack/cdn/v1/base/requests_test.go b/openstack/cdn/v1/base/requests_test.go
new file mode 100644
index 0000000..2c20a71
--- /dev/null
+++ b/openstack/cdn/v1/base/requests_test.go
@@ -0,0 +1,43 @@
+package base
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(fake.ServiceClient()).Extract()
+	th.CheckNoErr(t, err)
+
+	expected := HomeDocument{
+		"rel/cdn": map[string]interface{}{
+			"href-template": "services{?marker,limit}",
+			"href-vars": map[string]interface{}{
+				"marker": "param/marker",
+				"limit":  "param/limit",
+			},
+			"hints": map[string]interface{}{
+				"allow": []string{"GET"},
+				"formats": map[string]interface{}{
+					"application/json": map[string]interface{}{},
+				},
+			},
+		},
+	}
+	th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePingSuccessfully(t)
+
+	err := Ping(fake.ServiceClient()).ExtractErr()
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/base/results.go b/openstack/cdn/v1/base/results.go
new file mode 100644
index 0000000..bef1da8
--- /dev/null
+++ b/openstack/cdn/v1/base/results.go
@@ -0,0 +1,35 @@
+package base
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// HomeDocument is a resource that contains all the resources for the CDN API.
+type HomeDocument map[string]interface{}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a home document resource.
+func (r GetResult) Extract() (*HomeDocument, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	submap, ok := r.Body.(map[string]interface{})["resources"]
+	if !ok {
+		return nil, errors.New("Unexpected HomeDocument structure")
+	}
+	casted := HomeDocument(submap.(map[string]interface{}))
+
+	return &casted, nil
+}
+
+// PingResult represents the result of a Ping operation.
+type PingResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/base/urls.go b/openstack/cdn/v1/base/urls.go
new file mode 100644
index 0000000..a95e18b
--- /dev/null
+++ b/openstack/cdn/v1/base/urls.go
@@ -0,0 +1,11 @@
+package base
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL()
+}
+
+func pingURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("ping")
+}
diff --git a/openstack/cdn/v1/flavors/doc.go b/openstack/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..d406698
--- /dev/null
+++ b/openstack/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/openstack/cdn/v1/flavors/fixtures.go b/openstack/cdn/v1/flavors/fixtures.go
new file mode 100644
index 0000000..d7ec1a0
--- /dev/null
+++ b/openstack/cdn/v1/flavors/fixtures.go
@@ -0,0 +1,82 @@
+package flavors
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNFlavorsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+      {
+        "flavors": [
+            {
+                "id": "europe",
+                "providers": [
+                    {
+                        "provider": "Fastly",
+                        "links": [
+                            {
+                                "href": "http://www.fastly.com",
+                                "rel": "provider_url"
+                            }
+                        ]
+                    }
+                ],
+                "links": [
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+                        "rel": "self"
+                    }
+                ]
+            }
+        ]
+    }
+    `)
+	})
+}
+
+// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNFlavorSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+      {
+          "id" : "asia",
+          "providers" : [
+              {
+                  "provider" : "ChinaCache",
+                  "links": [
+                      {
+                          "href": "http://www.chinacache.com",
+                          "rel": "provider_url"
+                      }
+                  ]
+              }
+          ],
+          "links": [
+              {
+                  "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+                  "rel": "self"
+              }
+          ]
+      }
+    `)
+	})
+}
diff --git a/openstack/cdn/v1/flavors/requests.go b/openstack/cdn/v1/flavors/requests.go
new file mode 100644
index 0000000..8755a95
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests.go
@@ -0,0 +1,22 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(c)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
+	return res
+}
diff --git a/openstack/cdn/v1/flavors/requests_test.go b/openstack/cdn/v1/flavors/requests_test.go
new file mode 100644
index 0000000..f731738
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests_test.go
@@ -0,0 +1,89 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"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()
+
+	HandleListCDNFlavorsSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractFlavors(page)
+		if err != nil {
+			t.Errorf("Failed to extract flavors: %v", err)
+			return false, err
+		}
+
+		expected := []Flavor{
+			Flavor{
+				ID: "europe",
+				Providers: []Provider{
+					Provider{
+						Provider: "Fastly",
+						Links: []gophercloud.Link{
+							gophercloud.Link{
+								Href: "http://www.fastly.com",
+								Rel:  "provider_url",
+							},
+						},
+					},
+				},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "self",
+					},
+				},
+			},
+		}
+
+		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()
+
+	HandleGetCDNFlavorSuccessfully(t)
+
+	expected := &Flavor{
+		ID: "asia",
+		Providers: []Provider{
+			Provider{
+				Provider: "ChinaCache",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "http://www.chinacache.com",
+						Rel:  "provider_url",
+					},
+				},
+			},
+		},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+				Rel:  "self",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "asia").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/cdn/v1/flavors/results.go b/openstack/cdn/v1/flavors/results.go
new file mode 100644
index 0000000..8cab48b
--- /dev/null
+++ b/openstack/cdn/v1/flavors/results.go
@@ -0,0 +1,71 @@
+package flavors
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Provider represents a provider for a particular flavor.
+type Provider struct {
+	// Specifies the name of the provider. The name must not exceed 64 bytes in
+	// length and is limited to unicode, digits, underscores, and hyphens.
+	Provider string `mapstructure:"provider"`
+	// Specifies a list with an href where rel is provider_url.
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// Flavor represents a mapping configuration to a CDN provider.
+type Flavor struct {
+	// Specifies the name of the flavor. The name must not exceed 64 bytes in
+	// length and is limited to unicode, digits, underscores, and hyphens.
+	ID string `mapstructure:"id"`
+	// Specifies the list of providers mapped to this flavor.
+	Providers []Provider `mapstructure:"providers"`
+	// Specifies the self-navigating JSON document paths.
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// FlavorPage is the page returned by a pager when traversing over a
+// collection of CDN flavors.
+type FlavorPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a FlavorPage contains no Flavors.
+func (r FlavorPage) IsEmpty() (bool, error) {
+	flavors, err := ExtractFlavors(r)
+	if err != nil {
+		return true, err
+	}
+	return len(flavors) == 0, nil
+}
+
+// ExtractFlavors extracts and returns Flavors. It is used while iterating over
+// a flavors.List call.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+	var response struct {
+		Flavors []Flavor `json:"flavors"`
+	}
+
+	err := mapstructure.Decode(page.(FlavorPage).Body, &response)
+	return response.Flavors, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a flavor from a GetResult.
+func (r GetResult) Extract() (*Flavor, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res Flavor
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res, err
+}
diff --git a/openstack/cdn/v1/flavors/urls.go b/openstack/cdn/v1/flavors/urls.go
new file mode 100644
index 0000000..6eb38d2
--- /dev/null
+++ b/openstack/cdn/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("flavors")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("flavors", id)
+}
diff --git a/openstack/cdn/v1/serviceassets/doc.go b/openstack/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..ceecaa5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the OpenStack CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/openstack/cdn/v1/serviceassets/fixtures.go b/openstack/cdn/v1/serviceassets/fixtures.go
new file mode 100644
index 0000000..5c6b5d0
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/fixtures.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNAssetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/cdn/v1/serviceassets/requests.go b/openstack/cdn/v1/serviceassets/requests.go
new file mode 100644
index 0000000..1ddc65f
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests.go
@@ -0,0 +1,48 @@
+package serviceassets
+
+import (
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the Delete
+// request.
+type DeleteOptsBuilder interface {
+	ToCDNAssetDeleteParams() (string, error)
+}
+
+// DeleteOpts is a structure that holds options for deleting CDN service assets.
+type DeleteOpts struct {
+	// If all is set to true, specifies that the delete occurs against all of the
+	// assets for the service.
+	All bool `q:"all"`
+	// Specifies the relative URL of the asset to be deleted.
+	URL string `q:"url"`
+}
+
+// ToCDNAssetDeleteParams formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with
+// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) DeleteResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = deleteURL(c, idOrURL)
+	}
+
+	var res DeleteResult
+	_, res.Err = c.Delete(url, nil)
+	return res
+}
diff --git a/openstack/cdn/v1/serviceassets/requests_test.go b/openstack/cdn/v1/serviceassets/requests_test.go
new file mode 100644
index 0000000..dde7bc1
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests_test.go
@@ -0,0 +1,18 @@
+package serviceassets
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleDeleteCDNAssetSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/serviceassets/results.go b/openstack/cdn/v1/serviceassets/results.go
new file mode 100644
index 0000000..1d8734b
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/results.go
@@ -0,0 +1,8 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/serviceassets/urls.go b/openstack/cdn/v1/serviceassets/urls.go
new file mode 100644
index 0000000..cb0aea8
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/urls.go
@@ -0,0 +1,7 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("services", id, "assets")
+}
diff --git a/openstack/cdn/v1/services/doc.go b/openstack/cdn/v1/services/doc.go
new file mode 100644
index 0000000..41f7c60
--- /dev/null
+++ b/openstack/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/openstack/cdn/v1/services/errors.go b/openstack/cdn/v1/services/errors.go
new file mode 100644
index 0000000..359584c
--- /dev/null
+++ b/openstack/cdn/v1/services/errors.go
@@ -0,0 +1,7 @@
+package services
+
+import "fmt"
+
+func no(str string) error {
+	return fmt.Errorf("Required parameter %s not provided", str)
+}
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
new file mode 100644
index 0000000..d9bc9f2
--- /dev/null
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -0,0 +1,372 @@
+package services
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNServiceSuccessfully(t *testing.T) {
+	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().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+    {
+        "links": [
+            {
+                "rel": "next",
+                "href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20"
+            }
+        ],
+        "services": [
+            {
+                "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+                "name": "mywebsite.com",
+                "domains": [
+                    {
+                        "domain": "www.mywebsite.com"
+                    }
+                ],
+                "origins": [
+                    {
+                        "origin": "mywebsite.com",
+                        "port": 80,
+                        "ssl": false
+                    }
+                ],
+                "caching": [
+                    {
+                        "name": "default",
+                        "ttl": 3600
+                    },
+                    {
+                        "name": "home",
+                        "ttl": 17200,
+                        "rules": [
+                            {
+                                "name": "index",
+                                "request_url": "/index.htm"
+                            }
+                        ]
+                    },
+                    {
+                        "name": "images",
+                        "ttl": 12800,
+                        "rules": [
+                            {
+                                "name": "images",
+                                "request_url": "*.png"
+                            }
+                        ]
+                    }
+                ],
+                "restrictions": [
+                    {
+                        "name": "website only",
+                        "rules": [
+                            {
+                                "name": "mywebsite.com",
+                                "referrer": "www.mywebsite.com"
+                            }
+                        ]
+                    }
+                ],
+                "flavor_id": "asia",
+                "status": "deployed",
+                "errors" : [],
+                "links": [
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "mywebsite.com.cdn123.poppycdn.net",
+                        "rel": "access_url"
+                    },
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+                        "rel": "flavor"
+                    }
+                ]
+            },
+            {
+                "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+                "name": "myothersite.com",
+                "domains": [
+                    {
+                        "domain": "www.myothersite.com"
+                    }
+                ],
+                "origins": [
+                    {
+                        "origin": "44.33.22.11",
+                        "port": 80,
+                        "ssl": false
+                    },
+                    {
+                        "origin": "77.66.55.44",
+                        "port": 80,
+                        "ssl": false,
+                        "rules": [
+                            {
+                                "name": "videos",
+                                "request_url": "^/videos/*.m3u"
+                            }
+                        ]
+                    }
+                ],
+                "caching": [
+                    {
+                        "name": "default",
+                        "ttl": 3600
+                    }
+                ],
+                "restrictions": [
+                    {}
+                ],
+                "flavor_id": "europe",
+                "status": "deployed",
+                "links": [
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "myothersite.com.poppycdn.net",
+                        "rel": "access_url"
+                    },
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+                        "rel": "flavor"
+                    }
+                ]
+            }
+        ]
+    }
+    `)
+		case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1":
+			fmt.Fprintf(w, `{
+        "services": []
+    }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `Create` response.
+func HandleCreateCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `
+      {
+        "name": "mywebsite.com",
+        "domains": [
+            {
+                "domain": "www.mywebsite.com"
+            },
+            {
+                "domain": "blog.mywebsite.com"
+            }
+        ],
+        "origins": [
+            {
+                "origin": "mywebsite.com",
+                "port": 80,
+                "ssl": false
+            }
+        ],
+        "restrictions": [
+                         {
+                         "name": "website only",
+                         "rules": [
+                                   {
+                                   "name": "mywebsite.com",
+                                   "referrer": "www.mywebsite.com"
+                    }
+                ]
+            }
+        ],
+        "caching": [
+            {
+                "name": "default",
+                "ttl": 3600
+            }
+        ],
+
+        "flavor_id": "cdn"
+      }
+   `)
+		w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+    {
+        "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+        "name": "mywebsite.com",
+        "domains": [
+            {
+                "domain": "www.mywebsite.com",
+                "protocol": "http"
+            }
+        ],
+        "origins": [
+            {
+                "origin": "mywebsite.com",
+                "port": 80,
+                "ssl": false
+            }
+        ],
+        "caching": [
+            {
+                "name": "default",
+                "ttl": 3600
+            },
+            {
+                "name": "home",
+                "ttl": 17200,
+                "rules": [
+                    {
+                        "name": "index",
+                        "request_url": "/index.htm"
+                    }
+                ]
+            },
+            {
+                "name": "images",
+                "ttl": 12800,
+                "rules": [
+                    {
+                        "name": "images",
+                        "request_url": "*.png"
+                    }
+                ]
+            }
+        ],
+        "restrictions": [
+            {
+                "name": "website only",
+                "rules": [
+                    {
+                        "name": "mywebsite.com",
+                        "referrer": "www.mywebsite.com"
+                    }
+                ]
+            }
+        ],
+        "flavor_id": "cdn",
+        "status": "deployed",
+        "errors" : [],
+        "links": [
+            {
+                "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+                "rel": "self"
+            },
+            {
+                "href": "blog.mywebsite.com.cdn1.raxcdn.com",
+                "rel": "access_url"
+            },
+            {
+                "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+                "rel": "flavor"
+            }
+        ]
+    }
+    `)
+	})
+}
+
+// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Update` response.
+func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `
+      [
+				{
+					"op": "add",
+					"path": "/domains/-",
+					"value": {"domain": "appended.mocksite4.com"}
+				},
+				{
+					"op": "add",
+					"path": "/domains/4",
+					"value": {"domain": "inserted.mocksite4.com"}
+				},
+				{
+					"op": "add",
+					"path": "/domains",
+					"value": [
+						{"domain": "bulkadded1.mocksite4.com"},
+						{"domain": "bulkadded2.mocksite4.com"}
+					]
+				},
+				{
+					"op": "replace",
+					"path": "/origins/2",
+					"value": {"origin": "44.33.22.11", "port": 80, "ssl": false}
+				},
+				{
+					"op": "replace",
+					"path": "/origins",
+					"value": [
+						{"origin": "44.33.22.11", "port": 80, "ssl": false},
+						{"origin": "55.44.33.22", "port": 443, "ssl": true}
+					]
+				},
+				{
+					"op": "remove",
+					"path": "/caching/8"
+				},
+				{
+					"op": "remove",
+					"path": "/caching"
+				},
+				{
+					"op": "replace",
+					"path": "/name",
+					"value": "differentServiceName"
+				}
+    ]
+   `)
+		w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/cdn/v1/services/requests.go b/openstack/cdn/v1/services/requests.go
new file mode 100644
index 0000000..8b37928
--- /dev/null
+++ b/openstack/cdn/v1/services/requests.go
@@ -0,0 +1,378 @@
+package services
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToCDNServiceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Marker string `q:"marker"`
+	Limit  int    `q:"limit"`
+}
+
+// ToCDNServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToCDNServiceListQuery() (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
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToCDNServiceListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		p := ServicePage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	pager := pagination.NewPager(c, url, createPage)
+	return pager
+}
+
+// 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 {
+	ToCDNServiceCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// REQUIRED. Specifies the name of the service. The minimum length for name is
+	// 3. The maximum length is 256.
+	Name string
+	// REQUIRED. Specifies a list of domains used by users to access their website.
+	Domains []Domain
+	// REQUIRED. Specifies a list of origin domains or IP addresses where the
+	// original assets are stored.
+	Origins []Origin
+	// REQUIRED. Specifies the CDN provider flavor ID to use. For a list of
+	// flavors, see the operation to list the available flavors. The minimum
+	// length for flavor_id is 1. The maximum length is 256.
+	FlavorID string
+	// OPTIONAL. Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control.
+	Caching []CacheRule
+	// OPTIONAL. Specifies the restrictions that define who can access assets (content from the CDN cache).
+	Restrictions []Restriction
+}
+
+// ToCDNServiceCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return nil, no("Name")
+	}
+	s["name"] = opts.Name
+
+	if opts.Domains == nil {
+		return nil, no("Domains")
+	}
+	for _, domain := range opts.Domains {
+		if domain.Domain == "" {
+			return nil, no("Domains[].Domain")
+		}
+	}
+	s["domains"] = opts.Domains
+
+	if opts.Origins == nil {
+		return nil, no("Origins")
+	}
+	for _, origin := range opts.Origins {
+		if origin.Origin == "" {
+			return nil, no("Origins[].Origin")
+		}
+		if origin.Rules == nil && len(opts.Origins) > 1 {
+			return nil, no("Origins[].Rules")
+		}
+		for _, rule := range origin.Rules {
+			if rule.Name == "" {
+				return nil, no("Origins[].Rules[].Name")
+			}
+			if rule.RequestURL == "" {
+				return nil, no("Origins[].Rules[].RequestURL")
+			}
+		}
+	}
+	s["origins"] = opts.Origins
+
+	if opts.FlavorID == "" {
+		return nil, no("FlavorID")
+	}
+	s["flavor_id"] = opts.FlavorID
+
+	if opts.Caching != nil {
+		for _, cache := range opts.Caching {
+			if cache.Name == "" {
+				return nil, no("Caching[].Name")
+			}
+			if cache.Rules != nil {
+				for _, rule := range cache.Rules {
+					if rule.Name == "" {
+						return nil, no("Caching[].Rules[].Name")
+					}
+					if rule.RequestURL == "" {
+						return nil, no("Caching[].Rules[].RequestURL")
+					}
+				}
+			}
+		}
+		s["caching"] = opts.Caching
+	}
+
+	if opts.Restrictions != nil {
+		for _, restriction := range opts.Restrictions {
+			if restriction.Name == "" {
+				return nil, no("Restrictions[].Name")
+			}
+			if restriction.Rules != nil {
+				for _, rule := range restriction.Rules {
+					if rule.Name == "" {
+						return nil, no("Restrictions[].Rules[].Name")
+					}
+				}
+			}
+		}
+		s["restrictions"] = opts.Restrictions
+	}
+
+	return s, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToCDNServiceCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	resp, err := c.Post(createURL(c), &reqBody, nil, nil)
+	res.Header = resp.Header
+	res.Err = err
+	return res
+}
+
+// Get retrieves a specific service based on its URL or its unique ID. For
+// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Get(c *gophercloud.ServiceClient, idOrURL string) GetResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = getURL(c, idOrURL)
+	}
+
+	var res GetResult
+	_, res.Err = c.Get(url, &res.Body, nil)
+	return res
+}
+
+// Path is a JSON pointer location that indicates which service parameter is being added, replaced,
+// or removed.
+type Path struct {
+	baseElement string
+}
+
+func (p Path) renderRoot() string {
+	return "/" + p.baseElement
+}
+
+func (p Path) renderDash() string {
+	return fmt.Sprintf("/%s/-", p.baseElement)
+}
+
+func (p Path) renderIndex(index int64) string {
+	return fmt.Sprintf("/%s/%d", p.baseElement, index)
+}
+
+var (
+	// PathDomains indicates that an update operation is to be performed on a Domain.
+	PathDomains = Path{baseElement: "domains"}
+
+	// PathOrigins indicates that an update operation is to be performed on an Origin.
+	PathOrigins = Path{baseElement: "origins"}
+
+	// PathCaching indicates that an update operation is to be performed on a CacheRule.
+	PathCaching = Path{baseElement: "caching"}
+)
+
+type value interface {
+	toPatchValue() interface{}
+	appropriatePath() Path
+	renderRootOr(func(p Path) string) string
+}
+
+// Patch represents a single update to an existing Service. Multiple updates to a service can be
+// submitted at the same time.
+type Patch interface {
+	ToCDNServiceUpdateMap() map[string]interface{}
+}
+
+// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to
+// a Service at a fixed index. Use an Append instead to append the new value to the end of its
+// collection. Pass it to the Update function as part of the Patch slice.
+type Insertion struct {
+	Index int64
+	Value value
+}
+
+// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the
+// Update call.
+func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }),
+		"value": i.Value.toPatchValue(),
+	}
+}
+
+// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a
+// Service at the end of its respective collection. Use an Insertion instead to insert the value
+// at a fixed index within the collection. Pass this to the Update function as part of its
+// Patch slice.
+type Append struct {
+	Value value
+}
+
+// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the
+// Update call.
+func (a Append) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  a.Value.renderRootOr(func(p Path) string { return p.renderDash() }),
+		"value": a.Value.toPatchValue(),
+	}
+}
+
+// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule)
+// in-place by index. Pass it to the Update function as part of the Patch slice.
+type Replacement struct {
+	Value value
+	Index int64
+}
+
+// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the
+// Update call.
+func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }),
+		"value": r.Value.toPatchValue(),
+	}
+}
+
+// NameReplacement specifically updates the Service name. Pass it to the Update function as part
+// of the Patch slice.
+type NameReplacement struct {
+	NewName string
+}
+
+// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the
+// Update call.
+func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/name",
+		"value": r.NewName,
+	}
+}
+
+// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or
+// CacheRule) by index. Pass it to the Update function as part of the Patch slice.
+type Removal struct {
+	Path  Path
+	Index int64
+	All   bool
+}
+
+// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the
+// Update call.
+func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} {
+	result := map[string]interface{}{"op": "remove"}
+	if r.All {
+		result["path"] = r.Path.renderRoot()
+	} else {
+		result["path"] = r.Path.renderIndex(r.Index)
+	}
+	return result
+}
+
+type UpdateOpts []Patch
+
+// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and
+// updates an existing CDN service using the values provided. idOrURL can be either the service's
+// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOpts) UpdateResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = updateURL(c, idOrURL)
+	}
+
+	reqBody := make([]map[string]interface{}, len(opts))
+	for i, patch := range opts {
+		reqBody[i] = patch.ToCDNServiceUpdateMap()
+	}
+
+	resp, err := c.Request("PATCH", url, gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{202},
+	})
+	var result UpdateResult
+	result.Header = resp.Header
+	result.Err = err
+	return result
+}
+
+// Delete accepts a service's ID or its URL and deletes the CDN service
+// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string) DeleteResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = deleteURL(c, idOrURL)
+	}
+
+	var res DeleteResult
+	_, res.Err = c.Delete(url, nil)
+	return res
+}
diff --git a/openstack/cdn/v1/services/requests_test.go b/openstack/cdn/v1/services/requests_test.go
new file mode 100644
index 0000000..59e826f
--- /dev/null
+++ b/openstack/cdn/v1/services/requests_test.go
@@ -0,0 +1,358 @@
+package services
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"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()
+
+	HandleListCDNServiceSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractServices(page)
+		if err != nil {
+			t.Errorf("Failed to extract services: %v", err)
+			return false, err
+		}
+
+		expected := []Service{
+			Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Name: "mywebsite.com",
+				Domains: []Domain{
+					Domain{
+						Domain: "www.mywebsite.com",
+					},
+				},
+				Origins: []Origin{
+					Origin{
+						Origin: "mywebsite.com",
+						Port:   80,
+						SSL:    false,
+					},
+				},
+				Caching: []CacheRule{
+					CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+					CacheRule{
+						Name: "home",
+						TTL:  17200,
+						Rules: []TTLRule{
+							TTLRule{
+								Name:       "index",
+								RequestURL: "/index.htm",
+							},
+						},
+					},
+					CacheRule{
+						Name: "images",
+						TTL:  12800,
+						Rules: []TTLRule{
+							TTLRule{
+								Name:       "images",
+								RequestURL: "*.png",
+							},
+						},
+					},
+				},
+				Restrictions: []Restriction{
+					Restriction{
+						Name: "website only",
+						Rules: []RestrictionRule{
+							RestrictionRule{
+								Name:     "mywebsite.com",
+								Referrer: "www.mywebsite.com",
+							},
+						},
+					},
+				},
+				FlavorID: "asia",
+				Status:   "deployed",
+				Errors:   []Error{},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "mywebsite.com.cdn123.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+						Rel:  "flavor",
+					},
+				},
+			},
+			Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+				Name: "myothersite.com",
+				Domains: []Domain{
+					Domain{
+						Domain: "www.myothersite.com",
+					},
+				},
+				Origins: []Origin{
+					Origin{
+						Origin: "44.33.22.11",
+						Port:   80,
+						SSL:    false,
+					},
+					Origin{
+						Origin: "77.66.55.44",
+						Port:   80,
+						SSL:    false,
+						Rules: []OriginRule{
+							OriginRule{
+								Name:       "videos",
+								RequestURL: "^/videos/*.m3u",
+							},
+						},
+					},
+				},
+				Caching: []CacheRule{
+					CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+				},
+				Restrictions: []Restriction{},
+				FlavorID:     "europe",
+				Status:       "deployed",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "myothersite.com.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "flavor",
+					},
+				},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleCreateCDNServiceSuccessfully(t)
+
+	createOpts := CreateOpts{
+		Name: "mywebsite.com",
+		Domains: []Domain{
+			Domain{
+				Domain: "www.mywebsite.com",
+			},
+			Domain{
+				Domain: "blog.mywebsite.com",
+			},
+		},
+		Origins: []Origin{
+			Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Restrictions: []Restriction{
+			Restriction{
+				Name: "website only",
+				Rules: []RestrictionRule{
+					RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		Caching: []CacheRule{
+			CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+		},
+		FlavorID: "cdn",
+	}
+
+	expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleGetCDNServiceSuccessfully(t)
+
+	expected := &Service{
+		ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+		Name: "mywebsite.com",
+		Domains: []Domain{
+			Domain{
+				Domain:   "www.mywebsite.com",
+				Protocol: "http",
+			},
+		},
+		Origins: []Origin{
+			Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Caching: []CacheRule{
+			CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+			CacheRule{
+				Name: "home",
+				TTL:  17200,
+				Rules: []TTLRule{
+					TTLRule{
+						Name:       "index",
+						RequestURL: "/index.htm",
+					},
+				},
+			},
+			CacheRule{
+				Name: "images",
+				TTL:  12800,
+				Rules: []TTLRule{
+					TTLRule{
+						Name:       "images",
+						RequestURL: "*.png",
+					},
+				},
+			},
+		},
+		Restrictions: []Restriction{
+			Restriction{
+				Name: "website only",
+				Rules: []RestrictionRule{
+					RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		FlavorID: "cdn",
+		Status:   "deployed",
+		Errors:   []Error{},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+				Rel:  "access_url",
+			},
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+				Rel:  "flavor",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestSuccessfulUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleUpdateCDNServiceSuccessfully(t)
+
+	expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	ops := UpdateOpts{
+		// Append a single Domain
+		Append{Value: Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		Insertion{
+			Index: 4,
+			Value: Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		Append{
+			Value: DomainList{
+				Domain{Domain: "bulkadded1.mocksite4.com"},
+				Domain{Domain: "bulkadded2.mocksite4.com"},
+			},
+		},
+		// Replace a single Origin
+		Replacement{
+			Index: 2,
+			Value: Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		Replacement{
+			Index: 0, // Ignored
+			Value: OriginList{
+				Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+				Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
+			},
+		},
+		// Remove a single CacheRule
+		Removal{
+			Index: 8,
+			Path:  PathCaching,
+		},
+		// Bulk removal
+		Removal{
+			All:  true,
+			Path: PathCaching,
+		},
+		// Service name replacement
+		NameReplacement{
+			NewName: "differentServiceName",
+		},
+	}
+
+	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleDeleteCDNServiceSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
new file mode 100644
index 0000000..33406c4
--- /dev/null
+++ b/openstack/cdn/v1/services/results.go
@@ -0,0 +1,316 @@
+package services
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Domain represents a domain used by users to access their website.
+type Domain struct {
+	// Specifies the domain used to access the assets on their website, for which
+	// a CNAME is given to the CDN provider.
+	Domain string `mapstructure:"domain" json:"domain"`
+	// Specifies the protocol used to access the assets on this domain. Only "http"
+	// or "https" are currently allowed. The default is "http".
+	Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
+}
+
+func (d Domain) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["domain"] = d.Domain
+	if d.Protocol != "" {
+		r["protocol"] = d.Protocol
+	}
+	return r
+}
+
+func (d Domain) appropriatePath() Path {
+	return PathDomains
+}
+
+func (d Domain) renderRootOr(render func(p Path) string) string {
+	return render(d.appropriatePath())
+}
+
+// DomainList provides a useful way to perform bulk operations in a single Patch.
+type DomainList []Domain
+
+func (list DomainList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, domain := range list {
+		r[i] = domain.toPatchValue()
+	}
+	return r
+}
+
+func (list DomainList) appropriatePath() Path {
+	return PathDomains
+}
+
+func (list DomainList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
+// OriginRule represents a rule that defines when an origin should be accessed.
+type OriginRule struct {
+	// Specifies the name of this rule.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the request URL this rule should match for this origin to be used. Regex is supported.
+	RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// Origin specifies a list of origin domains or IP addresses where the original assets are stored.
+type Origin struct {
+	// Specifies the URL or IP address to pull origin content from.
+	Origin string `mapstructure:"origin" json:"origin"`
+	// Specifies the port used to access the origin. The default is port 80.
+	Port int `mapstructure:"port" json:"port,omitempty"`
+	// Specifies whether or not to use HTTPS to access the origin. The default
+	// is false.
+	SSL bool `mapstructure:"ssl" json:"ssl"`
+	// Specifies a collection of rules that define the conditions when this origin
+	// should be accessed. If there is more than one origin, the rules parameter is required.
+	Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+func (o Origin) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["origin"] = o.Origin
+	r["port"] = o.Port
+	r["ssl"] = o.SSL
+	if len(o.Rules) > 0 {
+		r["rules"] = make([]map[string]interface{}, len(o.Rules))
+		for index, rule := range o.Rules {
+			submap := r["rules"].([]map[string]interface{})[index]
+			submap["name"] = rule.Name
+			submap["request_url"] = rule.RequestURL
+		}
+	}
+	return r
+}
+
+func (o Origin) appropriatePath() Path {
+	return PathOrigins
+}
+
+func (o Origin) renderRootOr(render func(p Path) string) string {
+	return render(o.appropriatePath())
+}
+
+// OriginList provides a useful way to perform bulk operations in a single Patch.
+type OriginList []Origin
+
+func (list OriginList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, origin := range list {
+		r[i] = origin.toPatchValue()
+	}
+	return r
+}
+
+func (list OriginList) appropriatePath() Path {
+	return PathOrigins
+}
+
+func (list OriginList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
+// TTLRule specifies a rule that determines if a TTL should be applied to an asset.
+type TTLRule struct {
+	// Specifies the name of this rule.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the request URL this rule should match for this TTL to be used. Regex is supported.
+	RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// CacheRule specifies the TTL rules for the assets under this service.
+type CacheRule struct {
+	// Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the TTL to apply.
+	TTL int `mapstructure:"ttl" json:"ttl"`
+	// Specifies a collection of rules that determine if this TTL should be applied to an asset.
+	Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+func (c CacheRule) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["name"] = c.Name
+	r["ttl"] = c.TTL
+	r["rules"] = make([]map[string]interface{}, len(c.Rules))
+	for index, rule := range c.Rules {
+		submap := r["rules"].([]map[string]interface{})[index]
+		submap["name"] = rule.Name
+		submap["request_url"] = rule.RequestURL
+	}
+	return r
+}
+
+func (c CacheRule) appropriatePath() Path {
+	return PathCaching
+}
+
+func (c CacheRule) renderRootOr(render func(p Path) string) string {
+	return render(c.appropriatePath())
+}
+
+// CacheRuleList provides a useful way to perform bulk operations in a single Patch.
+type CacheRuleList []CacheRule
+
+func (list CacheRuleList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, rule := range list {
+		r[i] = rule.toPatchValue()
+	}
+	return r
+}
+
+func (list CacheRuleList) appropriatePath() Path {
+	return PathCaching
+}
+
+func (list CacheRuleList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
+// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
+type RestrictionRule struct {
+	// Specifies the name of this rule.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the http host that requests must come from.
+	Referrer string `mapstructure:"referrer" json:"referrer,omitempty"`
+}
+
+// Restriction specifies a restriction that defines who can access assets (content from the CDN cache).
+type Restriction struct {
+	// Specifies the name of this restriction.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies a collection of rules that determine if this TTL should be applied to an asset.
+	Rules []RestrictionRule `mapstructure:"rules" json:"rules"`
+}
+
+// Error specifies an error that occurred during the previous service action.
+type Error struct {
+	// Specifies an error message detailing why there is an error.
+	Message string `mapstructure:"message"`
+}
+
+// Service represents a CDN service resource.
+type Service struct {
+	// Specifies the service ID that represents distributed content. The value is
+	// a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server.
+	ID string `mapstructure:"id"`
+	// Specifies the name of the service.
+	Name string `mapstructure:"name"`
+	// Specifies a list of domains used by users to access their website.
+	Domains []Domain `mapstructure:"domains"`
+	// Specifies a list of origin domains or IP addresses where the original assets are stored.
+	Origins []Origin `mapstructure:"origins"`
+	// Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control.
+	Caching []CacheRule `mapstructure:"caching"`
+	// Specifies the restrictions that define who can access assets (content from the CDN cache).
+	Restrictions []Restriction `mapstructure:"restrictions" json:"restrictions,omitempty"`
+	// Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors.
+	FlavorID string `mapstructure:"flavor_id"`
+	// Specifies the current status of the service.
+	Status string `mapstructure:"status"`
+	// Specifies the list of errors that occurred during the previous service action.
+	Errors []Error `mapstructure:"errors"`
+	// Specifies the self-navigating JSON document paths.
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// ServicePage is the page returned by a pager when traversing over a
+// collection of CDN services.
+type ServicePage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no services.
+func (r ServicePage) IsEmpty() (bool, error) {
+	services, err := ExtractServices(r)
+	if err != nil {
+		return true, err
+	}
+	return len(services) == 0, nil
+}
+
+// LastMarker returns the last service in a ListResult.
+func (r ServicePage) LastMarker() (string, error) {
+	services, err := ExtractServices(r)
+	if err != nil {
+		return "", err
+	}
+	if len(services) == 0 {
+		return "", nil
+	}
+	return (services[len(services)-1]).ID, nil
+}
+
+// ExtractServices is a function that takes a ListResult and returns the services' information.
+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
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that extracts the location of a newly created service.
+func (r CreateResult) Extract() (string, error) {
+	if r.Err != nil {
+		return "", r.Err
+	}
+	if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+		return l[0], nil
+	}
+	return "", nil
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a service from a GetResult.
+func (r GetResult) Extract() (*Service, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res Service
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res, err
+}
+
+// UpdateResult represents the result of a Update operation.
+type UpdateResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that extracts the location of an updated service.
+func (r UpdateResult) Extract() (string, error) {
+	if r.Err != nil {
+		return "", r.Err
+	}
+	if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+		return l[0], nil
+	}
+	return "", nil
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/services/urls.go b/openstack/cdn/v1/services/urls.go
new file mode 100644
index 0000000..d953d4c
--- /dev/null
+++ b/openstack/cdn/v1/services/urls.go
@@ -0,0 +1,23 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("services")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("services", id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
diff --git a/openstack/client.go b/openstack/client.go
new file mode 100644
index 0000000..33602a6
--- /dev/null
+++ b/openstack/client.go
@@ -0,0 +1,274 @@
+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, 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
+	}
+
+	if options.AllowReauth {
+		client.ReauthFunc = func() error {
+			client.TokenID = ""
+			return AuthenticateV2(client, options)
+		}
+	}
+	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
+	}
+
+	var scope *tokens3.Scope
+	if options.TenantID != "" {
+		scope = &tokens3.Scope{
+			ProjectID: options.TenantID,
+		}
+		options.TenantID = ""
+		options.TenantName = ""
+	} else {
+		if options.TenantName != "" {
+			scope = &tokens3.Scope{
+				ProjectName: options.TenantName,
+				DomainID:    options.DomainID,
+				DomainName:  options.DomainName,
+			}
+			options.TenantName = ""
+		}
+	}
+
+	result := tokens3.Create(v3Client, options, scope)
+
+	token, err := result.ExtractToken()
+	if err != nil {
+		return err
+	}
+
+	catalog, err := result.ExtractServiceCatalog()
+	if err != nil {
+		return err
+	}
+
+	client.TokenID = token.ID
+
+	if options.AllowReauth {
+		client.ReauthFunc = func() error {
+			client.TokenID = ""
+			return AuthenticateV3(client, options)
+		}
+	}
+	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
+		return V3EndpointURL(catalog, 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
+}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("cdn")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
+func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("orchestration")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
+func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("database")
+	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..0b71085
--- /dev/null
+++ b/openstack/common/extensions/requests.go
@@ -0,0 +1,21 @@
+package extensions
+
+import (
+	"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 = c.Get(ExtensionURL(c, alias), &res.Body, nil)
+	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..c8edee0
--- /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"
+)
+
+// 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
+	}
+
+	// Delete imageName and flavorName that come from ToServerCreateMap().
+	// As of Liberty, Boot From Volume is failing if they are passed.
+	delete(reqBody["server"].(map[string]interface{}), "imageName")
+	delete(reqBody["server"].(map[string]interface{}), "flavorName")
+
+	_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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..8a7fa74
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests_test.go
@@ -0,0 +1,53 @@
+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",
+	"flavorName": "",
+	"imageName": "",
+        "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/defsecrules/doc.go b/openstack/compute/v2/extensions/defsecrules/doc.go
new file mode 100644
index 0000000..2571a1a
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/doc.go
@@ -0,0 +1 @@
+package defsecrules
diff --git a/openstack/compute/v2/extensions/defsecrules/fixtures.go b/openstack/compute/v2/extensions/defsecrules/fixtures.go
new file mode 100644
index 0000000..c28e492
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/fixtures.go
@@ -0,0 +1,108 @@
+package defsecrules
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const rootPath = "/os-security-group-default-rules"
+
+func mockListRulesResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, 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_default_rules": [
+    {
+      "from_port": 80,
+      "id": "{ruleID}",
+      "ip_protocol": "TCP",
+      "ip_range": {
+        "cidr": "10.10.10.0/24"
+      },
+      "to_port": 80
+    }
+  ]
+}
+      `)
+	})
+}
+
+func mockCreateRuleResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group_default_rule": {
+    "ip_protocol": "TCP",
+    "from_port": 80,
+    "to_port": 80,
+    "cidr": "10.10.12.0/24"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_default_rule": {
+    "from_port": 80,
+    "id": "{ruleID}",
+    "ip_protocol": "TCP",
+    "ip_range": {
+      "cidr": "10.10.12.0/24"
+    },
+    "to_port": 80
+  }
+}
+`)
+	})
+}
+
+func mockGetRuleResponse(t *testing.T, ruleID string) {
+	url := rootPath + "/" + ruleID
+	th.Mux.HandleFunc(url, 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_default_rule": {
+    "id": "{ruleID}",
+    "from_port": 80,
+    "to_port": 80,
+    "ip_protocol": "TCP",
+    "ip_range": {
+      "cidr": "10.10.12.0/24"
+    }
+  }
+}
+			`)
+	})
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+	url := rootPath + "/" + ruleID
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests.go b/openstack/compute/v2/extensions/defsecrules/requests.go
new file mode 100644
index 0000000..9f27ef1
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests.go
@@ -0,0 +1,95 @@
+package defsecrules
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will return a collection of default rules.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return DefaultRulePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// CreateOpts represents the configuration for adding a new default rule.
+type CreateOpts struct {
+	// Required - the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+
+	// Required - the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+
+	// Required - the protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol"`
+
+	// ONLY required if FromGroupID is blank. This represents the IP range that
+	// will be the source of network traffic to your security group. Use
+	// 0.0.0.0/0 to allow all IP addresses.
+	CIDR string `json:"cidr,omitempty"`
+}
+
+// CreateOptsBuilder builds the create rule options into a serializable format.
+type CreateOptsBuilder interface {
+	ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// ToRuleCreateMap builds the create rule options into a serializable format.
+func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	rule := make(map[string]interface{})
+
+	if opts.FromPort == 0 {
+		return rule, errors.New("A FromPort must be set")
+	}
+	if opts.ToPort == 0 {
+		return rule, errors.New("A ToPort must be set")
+	}
+	if opts.IPProtocol == "" {
+		return rule, errors.New("A IPProtocol must be set")
+	}
+	if opts.CIDR == "" {
+		return rule, errors.New("A CIDR must be set")
+	}
+
+	rule["from_port"] = opts.FromPort
+	rule["to_port"] = opts.ToPort
+	rule["ip_protocol"] = opts.IPProtocol
+	rule["cidr"] = opts.CIDR
+
+	return map[string]interface{}{"security_group_default_rule": rule}, nil
+}
+
+// Create is the operation responsible for creating a new default rule.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var result CreateResult
+
+	reqBody, err := opts.ToRuleCreateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = client.Post(rootURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return result
+}
+
+// Get will return details for a particular default rule.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = client.Get(resourceURL(client, id), &result.Body, nil)
+	return result
+}
+
+// Delete will permanently delete a default rule from the project.
+func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+	_, result.Err = client.Delete(resourceURL(client, id), nil)
+	return result
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests_test.go b/openstack/compute/v2/extensions/defsecrules/requests_test.go
new file mode 100644
index 0000000..d4ebe87
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests_test.go
@@ -0,0 +1,100 @@
+package defsecrules
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const ruleID = "{ruleID}"
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListRulesResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractDefaultRules(page)
+		th.AssertNoErr(t, err)
+
+		expected := []DefaultRule{
+			DefaultRule{
+				FromPort:   80,
+				ID:         ruleID,
+				IPProtocol: "TCP",
+				IPRange:    secgroups.IPRange{CIDR: "10.10.10.0/24"},
+				ToPort:     80,
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateRuleResponse(t)
+
+	opts := CreateOpts{
+		IPProtocol: "TCP",
+		FromPort:   80,
+		ToPort:     80,
+		CIDR:       "10.10.12.0/24",
+	}
+
+	group, err := Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &DefaultRule{
+		ID:         ruleID,
+		FromPort:   80,
+		ToPort:     80,
+		IPProtocol: "TCP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetRuleResponse(t, ruleID)
+
+	group, err := Get(client.ServiceClient(), ruleID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &DefaultRule{
+		ID:         ruleID,
+		FromPort:   80,
+		ToPort:     80,
+		IPProtocol: "TCP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteRuleResponse(t, ruleID)
+
+	err := Delete(client.ServiceClient(), ruleID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/results.go b/openstack/compute/v2/extensions/defsecrules/results.go
new file mode 100644
index 0000000..e588d3e
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/results.go
@@ -0,0 +1,69 @@
+package defsecrules
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// DefaultRule represents a default rule - which is identical to a
+// normal security rule.
+type DefaultRule secgroups.Rule
+
+// DefaultRulePage is a single page of a DefaultRule collection.
+type DefaultRulePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of default rules contains any results.
+func (page DefaultRulePage) IsEmpty() (bool, error) {
+	users, err := ExtractDefaultRules(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractDefaultRules returns a slice of DefaultRules contained in a single
+// page of results.
+func ExtractDefaultRules(page pagination.Page) ([]DefaultRule, error) {
+	casted := page.(DefaultRulePage).Body
+	var response struct {
+		Rules []DefaultRule `mapstructure:"security_group_default_rules"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.Rules, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// Extract will extract a DefaultRule struct from most responses.
+func (r commonResult) Extract() (*DefaultRule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Rule DefaultRule `mapstructure:"security_group_default_rule"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &response)
+
+	return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/urls.go b/openstack/compute/v2/extensions/defsecrules/urls.go
new file mode 100644
index 0000000..cc928ab
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/urls.go
@@ -0,0 +1,13 @@
+package defsecrules
+
+import "github.com/rackspace/gophercloud"
+
+const rulepath = "os-security-group-default-rules"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rulepath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rulepath)
+}
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..80785fa
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/doc.go
@@ -0,0 +1,3 @@
+// Package diskconfig provides information and interaction with 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..17418a3
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/requests_test.go
@@ -0,0 +1,89 @@
+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",
+				"flavorName": "",
+				"imageName": "",
+				"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/floatingip/doc.go b/openstack/compute/v2/extensions/floatingip/doc.go
new file mode 100644
index 0000000..f74f58c
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/doc.go
@@ -0,0 +1,3 @@
+// Package floatingip provides the ability to manage floating ips through
+// nova-network
+package floatingip
diff --git a/openstack/compute/v2/extensions/floatingip/fixtures.go b/openstack/compute/v2/extensions/floatingip/fixtures.go
new file mode 100644
index 0000000..e47fa4c
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/fixtures.go
@@ -0,0 +1,193 @@
+// +build fixtures
+
+package floatingip
+
+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 = `
+{
+    "floating_ips": [
+        {
+            "fixed_ip": null,
+            "id": 1,
+            "instance_id": null,
+            "ip": "10.10.10.1",
+            "pool": "nova"
+        },
+        {
+            "fixed_ip": "166.78.185.201",
+            "id": 2,
+            "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+            "ip": "10.10.10.2",
+            "pool": "nova"
+        }
+    ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "floating_ip": {
+        "fixed_ip": "166.78.185.201",
+        "id": 2,
+        "instance_id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+        "ip": "10.10.10.2",
+        "pool": "nova"
+    }
+}
+`
+
+// CreateOutput is a sample response to a Post call
+const CreateOutput = `
+{
+    "floating_ip": {
+        "fixed_ip": null,
+        "id": 1,
+        "instance_id": null,
+        "ip": "10.10.10.1",
+        "pool": "nova"
+    }
+}
+`
+
+// FirstFloatingIP is the first result in ListOutput.
+var FirstFloatingIP = FloatingIP{
+	ID:   "1",
+	IP:   "10.10.10.1",
+	Pool: "nova",
+}
+
+// SecondFloatingIP is the first result in ListOutput.
+var SecondFloatingIP = FloatingIP{
+	FixedIP:    "166.78.185.201",
+	ID:         "2",
+	InstanceID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	IP:         "10.10.10.2",
+	Pool:       "nova",
+}
+
+// ExpectedFloatingIPsSlice is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedFloatingIPsSlice = []FloatingIP{FirstFloatingIP, SecondFloatingIP}
+
+// CreatedFloatingIP is the parsed result from CreateOutput.
+var CreatedFloatingIP = FloatingIP{
+	ID:   "1",
+	IP:   "10.10.10.1",
+	Pool: "nova",
+}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-floating-ips", 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 an existing floating ip
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-floating-ips/2", 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 floating ip
+func HandleCreateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-floating-ips", 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, `
+{
+	"pool": "nova"
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, CreateOutput)
+	})
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
+// an existing floating ip
+func HandleDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-floating-ips/1", 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)
+	})
+}
+
+// HandleAssociateSuccessfully configures the test server to respond to a Post request
+// to associate an allocated floating IP
+func HandleAssociateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/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, `
+{
+	"addFloatingIp": {
+		"address": "10.10.10.2"
+	}
+}
+`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleFixedAssociateSucessfully configures the test server to respond to a Post request
+// to associate an allocated floating IP with a specific fixed IP address
+func HandleAssociateFixedSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/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, `
+{
+	"addFloatingIp": {
+		"address": "10.10.10.2",
+		"fixed_address": "166.78.185.201"
+	}
+}
+`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleDisassociateSuccessfully configures the test server to respond to a Post request
+// to disassociate an allocated floating IP
+func HandleDisassociateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/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, `
+{
+	"removeFloatingIp": {
+		"address": "10.10.10.2"
+	}
+}
+`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/compute/v2/extensions/floatingip/requests.go b/openstack/compute/v2/extensions/floatingip/requests.go
new file mode 100644
index 0000000..8206462
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/requests.go
@@ -0,0 +1,171 @@
+package floatingip
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of FloatingIPs.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return FloatingIPsPage{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 {
+	ToFloatingIPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies a Floating IP allocation request
+type CreateOpts struct {
+	// Pool is the pool of floating IPs to allocate one from
+	Pool string
+}
+
+// AssociateOpts specifies the required information to associate or disassociate a floating IP to an instance
+type AssociateOpts struct {
+	// ServerID is the UUID of the server
+	ServerID string
+
+	// FixedIP is an optional fixed IP address of the server
+	FixedIP string
+
+	// FloatingIP is the floating IP to associate with an instance
+	FloatingIP string
+}
+
+// ToFloatingIPCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
+	if opts.Pool == "" {
+		return nil, errors.New("Missing field required for floating IP creation: Pool")
+	}
+
+	return map[string]interface{}{"pool": opts.Pool}, nil
+}
+
+// ToAssociateMap constructs a request body from AssociateOpts.
+func (opts AssociateOpts) ToAssociateMap() (map[string]interface{}, error) {
+	if opts.ServerID == "" {
+		return nil, errors.New("Required field missing for floating IP association: ServerID")
+	}
+
+	if opts.FloatingIP == "" {
+		return nil, errors.New("Required field missing for floating IP association: FloatingIP")
+	}
+
+	associateInfo := map[string]interface{}{
+		"serverId":   opts.ServerID,
+		"floatingIp": opts.FloatingIP,
+		"fixedIp":    opts.FixedIP,
+	}
+
+	return associateInfo, nil
+
+}
+
+// Create requests the creation of a new floating IP
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToFloatingIPCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Get returns data about a previously created FloatingIP.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
+
+// Delete requests the deletion of a previous allocated FloatingIP.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, id), nil)
+	return res
+}
+
+// association / disassociation
+
+// Associate pairs an allocated floating IP with an instance
+// Deprecated. Use AssociateInstance.
+func Associate(client *gophercloud.ServiceClient, serverId, fip string) AssociateResult {
+	var res AssociateResult
+
+	addFloatingIp := make(map[string]interface{})
+	addFloatingIp["address"] = fip
+	reqBody := map[string]interface{}{"addFloatingIp": addFloatingIp}
+
+	_, res.Err = client.Post(associateURL(client, serverId), reqBody, nil, nil)
+	return res
+}
+
+// AssociateInstance pairs an allocated floating IP with an instance.
+func AssociateInstance(client *gophercloud.ServiceClient, opts AssociateOpts) AssociateResult {
+	var res AssociateResult
+
+	associateInfo, err := opts.ToAssociateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	addFloatingIp := make(map[string]interface{})
+	addFloatingIp["address"] = associateInfo["floatingIp"].(string)
+
+	// fixedIp is not required
+	if associateInfo["fixedIp"] != "" {
+		addFloatingIp["fixed_address"] = associateInfo["fixedIp"].(string)
+	}
+
+	serverId := associateInfo["serverId"].(string)
+
+	reqBody := map[string]interface{}{"addFloatingIp": addFloatingIp}
+	_, res.Err = client.Post(associateURL(client, serverId), reqBody, nil, nil)
+	return res
+}
+
+// Disassociate decouples an allocated floating IP from an instance
+// Deprecated. Use DisassociateInstance.
+func Disassociate(client *gophercloud.ServiceClient, serverId, fip string) DisassociateResult {
+	var res DisassociateResult
+
+	removeFloatingIp := make(map[string]interface{})
+	removeFloatingIp["address"] = fip
+	reqBody := map[string]interface{}{"removeFloatingIp": removeFloatingIp}
+
+	_, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil)
+	return res
+}
+
+// DisassociateInstance decouples an allocated floating IP from an instance
+func DisassociateInstance(client *gophercloud.ServiceClient, opts AssociateOpts) DisassociateResult {
+	var res DisassociateResult
+
+	associateInfo, err := opts.ToAssociateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	removeFloatingIp := make(map[string]interface{})
+	removeFloatingIp["address"] = associateInfo["floatingIp"].(string)
+	reqBody := map[string]interface{}{"removeFloatingIp": removeFloatingIp}
+
+	serverId := associateInfo["serverId"].(string)
+
+	_, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/floatingip/requests_test.go b/openstack/compute/v2/extensions/floatingip/requests_test.go
new file mode 100644
index 0000000..4d86fe2
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/requests_test.go
@@ -0,0 +1,123 @@
+package floatingip
+
+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 := ExtractFloatingIPs(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedFloatingIPsSlice, 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{
+		Pool: "nova",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &CreatedFloatingIP, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "2").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &SecondFloatingIP, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "1").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAssociateDeprecated(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateSuccessfully(t)
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+	fip := "10.10.10.2"
+
+	err := Associate(client.ServiceClient(), serverId, fip).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAssociate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateSuccessfully(t)
+
+	associateOpts := AssociateOpts{
+		ServerID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+		FloatingIP: "10.10.10.2",
+	}
+
+	err := AssociateInstance(client.ServiceClient(), associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAssociateFixed(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateFixedSuccessfully(t)
+
+	associateOpts := AssociateOpts{
+		ServerID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+		FloatingIP: "10.10.10.2",
+		FixedIP:    "166.78.185.201",
+	}
+
+	err := AssociateInstance(client.ServiceClient(), associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisassociateDeprecated(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDisassociateSuccessfully(t)
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+	fip := "10.10.10.2"
+
+	err := Disassociate(client.ServiceClient(), serverId, fip).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisassociateInstance(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDisassociateSuccessfully(t)
+
+	associateOpts := AssociateOpts{
+		ServerID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+		FloatingIP: "10.10.10.2",
+	}
+
+	err := DisassociateInstance(client.ServiceClient(), associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/floatingip/results.go b/openstack/compute/v2/extensions/floatingip/results.go
new file mode 100644
index 0000000..be77fa1
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/results.go
@@ -0,0 +1,99 @@
+package floatingip
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// A FloatingIP is an IP that can be associated with an instance
+type FloatingIP struct {
+	// ID is a unique ID of the Floating IP
+	ID string `mapstructure:"id"`
+
+	// FixedIP is the IP of the instance related to the Floating IP
+	FixedIP string `mapstructure:"fixed_ip,omitempty"`
+
+	// InstanceID is the ID of the instance that is using the Floating IP
+	InstanceID string `mapstructure:"instance_id"`
+
+	// IP is the actual Floating IP
+	IP string `mapstructure:"ip"`
+
+	// Pool is the pool of floating IPs that this floating IP belongs to
+	Pool string `mapstructure:"pool"`
+}
+
+// FloatingIPsPage stores a single, only page of FloatingIPs
+// results from a List call.
+type FloatingIPsPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a FloatingIPsPage is empty.
+func (page FloatingIPsPage) IsEmpty() (bool, error) {
+	va, err := ExtractFloatingIPs(page)
+	return len(va) == 0, err
+}
+
+// ExtractFloatingIPs interprets a page of results as a slice of
+// FloatingIPs.
+func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) {
+	casted := page.(FloatingIPsPage).Body
+	var response struct {
+		FloatingIPs []FloatingIP `mapstructure:"floating_ips"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.FloatingIPs, err
+}
+
+type FloatingIPResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any FloatingIP resource
+// response as a FloatingIP struct.
+func (r FloatingIPResult) Extract() (*FloatingIP, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		FloatingIP *FloatingIP `json:"floating_ip" mapstructure:"floating_ip"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &res)
+	return res.FloatingIP, err
+}
+
+// CreateResult is the response from a Create operation. Call its Extract method to interpret it
+// as a FloatingIP.
+type CreateResult struct {
+	FloatingIPResult
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a FloatingIP.
+type GetResult struct {
+	FloatingIPResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
+// the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// AssociateResult is the response from a Delete operation. Call its Extract method to determine if
+// the call succeeded or failed.
+type AssociateResult struct {
+	gophercloud.ErrResult
+}
+
+// DisassociateResult is the response from a Delete operation. Call its Extract method to determine if
+// the call succeeded or failed.
+type DisassociateResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/compute/v2/extensions/floatingip/urls.go b/openstack/compute/v2/extensions/floatingip/urls.go
new file mode 100644
index 0000000..54198f8
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/urls.go
@@ -0,0 +1,37 @@
+package floatingip
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-floating-ips"
+
+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, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
+
+func serverURL(c *gophercloud.ServiceClient, serverId string) string {
+	return c.ServiceURL("servers/" + serverId + "/action")
+}
+
+func associateURL(c *gophercloud.ServiceClient, serverId string) string {
+	return serverURL(c, serverId)
+}
+
+func disassociateURL(c *gophercloud.ServiceClient, serverId string) string {
+	return serverURL(c, serverId)
+}
diff --git a/openstack/compute/v2/extensions/floatingip/urls_test.go b/openstack/compute/v2/extensions/floatingip/urls_test.go
new file mode 100644
index 0000000..f73d6fb
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingip/urls_test.go
@@ -0,0 +1,60 @@
+package floatingip
+
+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-floating-ips", listURL(c))
+}
+
+func TestCreateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-floating-ips", createURL(c))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-floating-ips/"+id, getURL(c, id))
+}
+
+func TestDeleteURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-floating-ips/"+id, deleteURL(c, id))
+}
+
+func TestAssociateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/action", associateURL(c, serverId))
+}
+
+func TestDisassociateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/action", disassociateURL(c, serverId))
+}
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..c56ee67
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/requests.go
@@ -0,0 +1,102 @@
+package keypairs
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsExt adds a KeyPair option to the base CreateOpts.
+type CreateOptsExt struct {
+	servers.CreateOptsBuilder
+	KeyName string `json:"key_name,omitempty"`
+}
+
+// ToServerCreateMap adds the key_name and, optionally, key_data options 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 opts.KeyName == "" {
+		return base, nil
+	}
+
+	serverMap := base["server"].(map[string]interface{})
+	serverMap["key_name"] = opts.KeyName
+
+	return base, nil
+}
+
+// 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 specifies 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 = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = client.Get(getURL(client, name), &res.Body, nil)
+	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 = client.Delete(deleteURL(client, name), nil)
+	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..67d1833
--- /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").ExtractErr()
+	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..f1a0d8e
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/results.go
@@ -0,0 +1,94 @@
+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.ErrResult
+}
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/extensions/networks/doc.go b/openstack/compute/v2/extensions/networks/doc.go
new file mode 100644
index 0000000..fafe4a0
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/doc.go
@@ -0,0 +1,2 @@
+// Package network provides the ability to manage nova-networks
+package networks
diff --git a/openstack/compute/v2/extensions/networks/fixtures.go b/openstack/compute/v2/extensions/networks/fixtures.go
new file mode 100644
index 0000000..12b9485
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/fixtures.go
@@ -0,0 +1,209 @@
+// +build fixtures
+
+package networks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput is a sample response to a List call.
+const ListOutput = `
+{
+    "networks": [
+        {
+            "bridge": "br100",
+            "bridge_interface": "eth0",
+            "broadcast": "10.0.0.7",
+            "cidr": "10.0.0.0/29",
+            "cidr_v6": null,
+            "created_at": "2011-08-15 06:19:19.387525",
+            "deleted": false,
+            "deleted_at": null,
+            "dhcp_start": "10.0.0.3",
+            "dns1": null,
+            "dns2": null,
+            "gateway": "10.0.0.1",
+            "gateway_v6": null,
+            "host": "nsokolov-desktop",
+            "id": "20c8acc0-f747-4d71-a389-46d078ebf047",
+            "injected": false,
+            "label": "mynet_0",
+            "multi_host": false,
+            "netmask": "255.255.255.248",
+            "netmask_v6": null,
+            "priority": null,
+            "project_id": "1234",
+            "rxtx_base": null,
+            "updated_at": "2011-08-16 09:26:13.048257",
+            "vlan": 100,
+            "vpn_private_address": "10.0.0.2",
+            "vpn_public_address": "127.0.0.1",
+            "vpn_public_port": 1000
+        },
+        {
+            "bridge": "br101",
+            "bridge_interface": "eth0",
+            "broadcast": "10.0.0.15",
+            "cidr": "10.0.0.10/29",
+            "cidr_v6": null,
+            "created_at": "2011-08-15 06:19:19.885495",
+            "deleted": false,
+            "deleted_at": null,
+            "dhcp_start": "10.0.0.11",
+            "dns1": null,
+            "dns2": null,
+            "gateway": "10.0.0.9",
+            "gateway_v6": null,
+            "host": null,
+            "id": "20c8acc0-f747-4d71-a389-46d078ebf000",
+            "injected": false,
+            "label": "mynet_1",
+            "multi_host": false,
+            "netmask": "255.255.255.248",
+            "netmask_v6": null,
+            "priority": null,
+            "project_id": null,
+            "rxtx_base": null,
+            "updated_at": null,
+            "vlan": 101,
+            "vpn_private_address": "10.0.0.10",
+            "vpn_public_address": null,
+            "vpn_public_port": 1001
+        }
+    ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "network": {
+			"bridge": "br101",
+			"bridge_interface": "eth0",
+			"broadcast": "10.0.0.15",
+			"cidr": "10.0.0.10/29",
+			"cidr_v6": null,
+			"created_at": "2011-08-15 06:19:19.885495",
+			"deleted": false,
+			"deleted_at": null,
+			"dhcp_start": "10.0.0.11",
+			"dns1": null,
+			"dns2": null,
+			"gateway": "10.0.0.9",
+			"gateway_v6": null,
+			"host": null,
+			"id": "20c8acc0-f747-4d71-a389-46d078ebf000",
+			"injected": false,
+			"label": "mynet_1",
+			"multi_host": false,
+			"netmask": "255.255.255.248",
+			"netmask_v6": null,
+			"priority": null,
+			"project_id": null,
+			"rxtx_base": null,
+			"updated_at": null,
+			"vlan": 101,
+			"vpn_private_address": "10.0.0.10",
+			"vpn_public_address": null,
+			"vpn_public_port": 1001
+		}
+}
+`
+
+// FirstNetwork is the first result in ListOutput.
+var nilTime time.Time
+var FirstNetwork = Network{
+	Bridge:            "br100",
+	BridgeInterface:   "eth0",
+	Broadcast:         "10.0.0.7",
+	CIDR:              "10.0.0.0/29",
+	CIDRv6:            "",
+	CreatedAt:         time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC),
+	Deleted:           false,
+	DeletedAt:         nilTime,
+	DHCPStart:         "10.0.0.3",
+	DNS1:              "",
+	DNS2:              "",
+	Gateway:           "10.0.0.1",
+	Gatewayv6:         "",
+	Host:              "nsokolov-desktop",
+	ID:                "20c8acc0-f747-4d71-a389-46d078ebf047",
+	Injected:          false,
+	Label:             "mynet_0",
+	MultiHost:         false,
+	Netmask:           "255.255.255.248",
+	Netmaskv6:         "",
+	Priority:          0,
+	ProjectID:         "1234",
+	RXTXBase:          0,
+	UpdatedAt:         time.Date(2011, 8, 16, 9, 26, 13, 48257000, time.UTC),
+	VLAN:              100,
+	VPNPrivateAddress: "10.0.0.2",
+	VPNPublicAddress:  "127.0.0.1",
+	VPNPublicPort:     1000,
+}
+
+// SecondNetwork is the second result in ListOutput.
+var SecondNetwork = Network{
+	Bridge:            "br101",
+	BridgeInterface:   "eth0",
+	Broadcast:         "10.0.0.15",
+	CIDR:              "10.0.0.10/29",
+	CIDRv6:            "",
+	CreatedAt:         time.Date(2011, 8, 15, 6, 19, 19, 885495000, time.UTC),
+	Deleted:           false,
+	DeletedAt:         nilTime,
+	DHCPStart:         "10.0.0.11",
+	DNS1:              "",
+	DNS2:              "",
+	Gateway:           "10.0.0.9",
+	Gatewayv6:         "",
+	Host:              "",
+	ID:                "20c8acc0-f747-4d71-a389-46d078ebf000",
+	Injected:          false,
+	Label:             "mynet_1",
+	MultiHost:         false,
+	Netmask:           "255.255.255.248",
+	Netmaskv6:         "",
+	Priority:          0,
+	ProjectID:         "",
+	RXTXBase:          0,
+	UpdatedAt:         nilTime,
+	VLAN:              101,
+	VPNPrivateAddress: "10.0.0.10",
+	VPNPublicAddress:  "",
+	VPNPublicPort:     1001,
+}
+
+// ExpectedNetworkSlice is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedNetworkSlice = []Network{FirstNetwork, SecondNetwork}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-networks", 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 an existing network.
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+}
diff --git a/openstack/compute/v2/extensions/networks/requests.go b/openstack/compute/v2/extensions/networks/requests.go
new file mode 100644
index 0000000..eb20387
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/requests.go
@@ -0,0 +1,22 @@
+package networks
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of Network.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(client)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, url, createPage)
+}
+
+// Get returns data about a previously created Network.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/networks/requests_test.go b/openstack/compute/v2/extensions/networks/requests_test.go
new file mode 100644
index 0000000..722b3f0
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/requests_test.go
@@ -0,0 +1,37 @@
+package networks
+
+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 := ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedNetworkSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &SecondNetwork, actual)
+}
diff --git a/openstack/compute/v2/extensions/networks/results.go b/openstack/compute/v2/extensions/networks/results.go
new file mode 100644
index 0000000..55b361d
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/results.go
@@ -0,0 +1,222 @@
+package networks
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// A Network represents a nova-network that an instance communicates on
+type Network struct {
+	// The Bridge that VIFs on this network are connected to
+	Bridge string `mapstructure:"bridge"`
+
+	// BridgeInterface is what interface is connected to the Bridge
+	BridgeInterface string `mapstructure:"bridge_interface"`
+
+	// The Broadcast address of the network.
+	Broadcast string `mapstructure:"broadcast"`
+
+	// CIDR is the IPv4 subnet.
+	CIDR string `mapstructure:"cidr"`
+
+	// CIDRv6 is the IPv6 subnet.
+	CIDRv6 string `mapstructure:"cidr_v6"`
+
+	// CreatedAt is when the network was created..
+	CreatedAt time.Time `mapstructure:"-"`
+
+	// Deleted shows if the network has been deleted.
+	Deleted bool `mapstructure:"deleted"`
+
+	// DeletedAt is the time when the network was deleted.
+	DeletedAt time.Time `mapstructure:"-"`
+
+	// DHCPStart is the start of the DHCP address range.
+	DHCPStart string `mapstructure:"dhcp_start"`
+
+	// DNS1 is the first DNS server to use through DHCP.
+	DNS1 string `mapstructure:"dns_1"`
+
+	// DNS2 is the first DNS server to use through DHCP.
+	DNS2 string `mapstructure:"dns_2"`
+
+	// Gateway is the network gateway.
+	Gateway string `mapstructure:"gateway"`
+
+	// Gatewayv6 is the IPv6 network gateway.
+	Gatewayv6 string `mapstructure:"gateway_v6"`
+
+	// Host is the host that the network service is running on.
+	Host string `mapstructure:"host"`
+
+	// ID is the UUID of the network.
+	ID string `mapstructure:"id"`
+
+	// Injected determines if network information is injected into the host.
+	Injected bool `mapstructure:"injected"`
+
+	// Label is the common name that the network has..
+	Label string `mapstructure:"label"`
+
+	// MultiHost is if multi-host networking is enablec..
+	MultiHost bool `mapstructure:"multi_host"`
+
+	// Netmask is the network netmask.
+	Netmask string `mapstructure:"netmask"`
+
+	// Netmaskv6 is the IPv6 netmask.
+	Netmaskv6 string `mapstructure:"netmask_v6"`
+
+	// Priority is the network interface priority.
+	Priority int `mapstructure:"priority"`
+
+	// ProjectID is the project associated with this network.
+	ProjectID string `mapstructure:"project_id"`
+
+	// RXTXBase configures bandwidth entitlement.
+	RXTXBase int `mapstructure:"rxtx_base"`
+
+	// UpdatedAt is the time when the network was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+
+	// VLAN is the vlan this network runs on.
+	VLAN int `mapstructure:"vlan"`
+
+	// VPNPrivateAddress is the private address of the CloudPipe VPN.
+	VPNPrivateAddress string `mapstructure:"vpn_private_address"`
+
+	// VPNPublicAddress is the public address of the CloudPipe VPN.
+	VPNPublicAddress string `mapstructure:"vpn_public_address"`
+
+	// VPNPublicPort is the port of the CloudPipe VPN.
+	VPNPublicPort int `mapstructure:"vpn_public_port"`
+}
+
+// NetworkPage stores a single, only page of Networks
+// results from a List call.
+type NetworkPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a NetworkPage is empty.
+func (page NetworkPage) IsEmpty() (bool, error) {
+	va, err := ExtractNetworks(page)
+	return len(va) == 0, err
+}
+
+// ExtractNetworks interprets a page of results as a slice of Networks
+func ExtractNetworks(page pagination.Page) ([]Network, error) {
+	var res struct {
+		Networks []Network `mapstructure:"networks"`
+	}
+
+	err := mapstructure.Decode(page.(NetworkPage).Body, &res)
+
+	var rawNetworks []interface{}
+	body := page.(NetworkPage).Body
+	switch body.(type) {
+	case map[string]interface{}:
+		rawNetworks = body.(map[string]interface{})["networks"].([]interface{})
+	case map[string][]interface{}:
+		rawNetworks = body.(map[string][]interface{})["networks"]
+	default:
+		return res.Networks, fmt.Errorf("Unknown type")
+	}
+
+	for i := range rawNetworks {
+		thisNetwork := rawNetworks[i].(map[string]interface{})
+		if t, ok := thisNetwork["created_at"].(string); ok && t != "" {
+			createdAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+			if err != nil {
+				return res.Networks, err
+			}
+			res.Networks[i].CreatedAt = createdAt
+		}
+
+		if t, ok := thisNetwork["updated_at"].(string); ok && t != "" {
+			updatedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+			if err != nil {
+				return res.Networks, err
+			}
+			res.Networks[i].UpdatedAt = updatedAt
+		}
+
+		if t, ok := thisNetwork["deleted_at"].(string); ok && t != "" {
+			deletedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+			if err != nil {
+				return res.Networks, err
+			}
+			res.Networks[i].DeletedAt = deletedAt
+		}
+	}
+
+	return res.Networks, err
+}
+
+type NetworkResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any Network resource
+// response as a Network struct.
+func (r NetworkResult) Extract() (*Network, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *Network `json:"network" mapstructure:"network"`
+	}
+
+	config := &mapstructure.DecoderConfig{
+		Result:           &res,
+		WeaklyTypedInput: true,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := decoder.Decode(r.Body); err != nil {
+		return nil, err
+	}
+
+	b := r.Body.(map[string]interface{})["network"].(map[string]interface{})
+
+	if t, ok := b["created_at"].(string); ok && t != "" {
+		createdAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+		if err != nil {
+			return res.Network, err
+		}
+		res.Network.CreatedAt = createdAt
+	}
+
+	if t, ok := b["updated_at"].(string); ok && t != "" {
+		updatedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+		if err != nil {
+			return res.Network, err
+		}
+		res.Network.UpdatedAt = updatedAt
+	}
+
+	if t, ok := b["deleted_at"].(string); ok && t != "" {
+		deletedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+		if err != nil {
+			return res.Network, err
+		}
+		res.Network.DeletedAt = deletedAt
+	}
+
+	return res.Network, err
+
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a Network.
+type GetResult struct {
+	NetworkResult
+}
diff --git a/openstack/compute/v2/extensions/networks/urls.go b/openstack/compute/v2/extensions/networks/urls.go
new file mode 100644
index 0000000..6966462
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/urls.go
@@ -0,0 +1,17 @@
+package networks
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-networks"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return resourceURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
diff --git a/openstack/compute/v2/extensions/networks/urls_test.go b/openstack/compute/v2/extensions/networks/urls_test.go
new file mode 100644
index 0000000..be54c90
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/urls_test.go
@@ -0,0 +1,25 @@
+package networks
+
+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-networks", listURL(c))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-networks/"+id, getURL(c, id))
+}
diff --git a/openstack/compute/v2/extensions/schedulerhints/doc.go b/openstack/compute/v2/extensions/schedulerhints/doc.go
new file mode 100644
index 0000000..0bd4566
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/doc.go
@@ -0,0 +1,3 @@
+// Package schedulerhints enables instances to provide the OpenStack scheduler
+// hints about where they should be placed in the cloud.
+package schedulerhints
diff --git a/openstack/compute/v2/extensions/schedulerhints/requests.go b/openstack/compute/v2/extensions/schedulerhints/requests.go
new file mode 100644
index 0000000..567eef4
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/requests.go
@@ -0,0 +1,134 @@
+package schedulerhints
+
+import (
+	"fmt"
+	"net"
+	"regexp"
+	"strings"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// SchedulerHints represents a set of scheduling hints that are passed to the
+// OpenStack scheduler
+type SchedulerHints struct {
+	// Group specifies a Server Group to place the instance in.
+	Group string
+
+	// DifferentHost will place the instance on a compute node that does not
+	// host the given instances.
+	DifferentHost []string
+
+	// SameHost will place the instance on a compute node that hosts the given
+	// instances.
+	SameHost []string
+
+	// Query is a conditional statement that results in compute nodes able to
+	// host the instance.
+	Query []interface{}
+
+	// TargetCell specifies a cell name where the instance will be placed.
+	TargetCell string
+
+	// BuildNearHostIP specifies a subnet of compute nodes to host the instance.
+	BuildNearHostIP string
+}
+
+// SchedulerHintsBuilder builds the scheduler hints into a serializable format.
+type SchedulerHintsBuilder interface {
+	ToServerSchedulerHintsMap() (map[string]interface{}, error)
+}
+
+// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format.
+func (opts SchedulerHints) ToServerSchedulerHintsMap() (map[string]interface{}, error) {
+	sh := make(map[string]interface{})
+
+	uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$")
+
+	if opts.Group != "" {
+		if !uuidRegex.MatchString(opts.Group) {
+			return nil, fmt.Errorf("Group must be a UUID")
+		}
+		sh["group"] = opts.Group
+	}
+
+	if len(opts.DifferentHost) > 0 {
+		for _, diffHost := range opts.DifferentHost {
+			if !uuidRegex.MatchString(diffHost) {
+				return nil, fmt.Errorf("The hosts in DifferentHost must be in UUID format.")
+			}
+		}
+		sh["different_host"] = opts.DifferentHost
+	}
+
+	if len(opts.SameHost) > 0 {
+		for _, sameHost := range opts.SameHost {
+			if !uuidRegex.MatchString(sameHost) {
+				return nil, fmt.Errorf("The hosts in SameHost must be in UUID format.")
+			}
+		}
+		sh["same_host"] = opts.SameHost
+	}
+
+	/* Query can be something simple like:
+	     [">=", "$free_ram_mb", 1024]
+
+			Or more complex like:
+				['and',
+					['>=', '$free_ram_mb', 1024],
+					['>=', '$free_disk_mb', 200 * 1024]
+				]
+
+		Because of the possible complexity, just make sure the length is a minimum of 3.
+	*/
+	if len(opts.Query) > 0 {
+		if len(opts.Query) < 3 {
+			return nil, fmt.Errorf("Query must be a conditional statement in the format of [op,variable,value]")
+		}
+		sh["query"] = opts.Query
+	}
+
+	if opts.TargetCell != "" {
+		sh["target_cell"] = opts.TargetCell
+	}
+
+	if opts.BuildNearHostIP != "" {
+		if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil {
+			return nil, fmt.Errorf("BuildNearHostIP must be a valid subnet in the form 192.168.1.1/24")
+		}
+		ipParts := strings.Split(opts.BuildNearHostIP, "/")
+		sh["build_near_host_ip"] = ipParts[0]
+		sh["cidr"] = "/" + ipParts[1]
+	}
+
+	return sh, nil
+}
+
+// CreateOptsExt adds a SchedulerHints option to the base CreateOpts.
+type CreateOptsExt struct {
+	servers.CreateOptsBuilder
+
+	// SchedulerHints provides a set of hints to the scheduler.
+	SchedulerHints SchedulerHintsBuilder
+}
+
+// ToServerCreateMap adds the SchedulerHints 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
+	}
+
+	schedulerHints, err := opts.SchedulerHints.ToServerSchedulerHintsMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(schedulerHints) == 0 {
+		return base, nil
+	}
+
+	base["os:scheduler_hints"] = schedulerHints
+
+	return base, nil
+}
diff --git a/openstack/compute/v2/extensions/schedulerhints/requests_test.go b/openstack/compute/v2/extensions/schedulerhints/requests_test.go
new file mode 100644
index 0000000..9b38b35
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/requests_test.go
@@ -0,0 +1,130 @@
+package schedulerhints
+
+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",
+	}
+
+	schedulerHints := SchedulerHints{
+		Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+		DifferentHost: []string{
+			"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+			"8c19174f-4220-44f0-824a-cd1eeef10287",
+		},
+		SameHost: []string{
+			"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+			"8c19174f-4220-44f0-824a-cd1eeef10287",
+		},
+		Query:           []interface{}{">=", "$free_ram_mb", "1024"},
+		TargetCell:      "foobar",
+		BuildNearHostIP: "192.168.1.1/24",
+	}
+
+	ext := CreateOptsExt{
+		CreateOptsBuilder: base,
+		SchedulerHints:    schedulerHints,
+	}
+
+	expected := `
+		{
+			"server": {
+				"name": "createdserver",
+				"imageRef": "asdfasdfasdf",
+				"flavorRef": "performance1-1",
+				"flavorName": "",
+				"imageName": ""
+			},
+			"os:scheduler_hints": {
+				"group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+				"different_host": [
+					"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+					"8c19174f-4220-44f0-824a-cd1eeef10287"
+				],
+				"same_host": [
+					"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+					"8c19174f-4220-44f0-824a-cd1eeef10287"
+				],
+				"query": [
+					">=", "$free_ram_mb", "1024"
+				],
+				"target_cell": "foobar",
+				"build_near_host_ip": "192.168.1.1",
+				"cidr": "/24"
+			}
+		}
+	`
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestCreateOptsWithComplexQuery(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	schedulerHints := SchedulerHints{
+		Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+		DifferentHost: []string{
+			"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+			"8c19174f-4220-44f0-824a-cd1eeef10287",
+		},
+		SameHost: []string{
+			"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+			"8c19174f-4220-44f0-824a-cd1eeef10287",
+		},
+		Query:           []interface{}{"and", []string{">=", "$free_ram_mb", "1024"}, []string{">=", "$free_disk_mb", "204800"}},
+		TargetCell:      "foobar",
+		BuildNearHostIP: "192.168.1.1/24",
+	}
+
+	ext := CreateOptsExt{
+		CreateOptsBuilder: base,
+		SchedulerHints:    schedulerHints,
+	}
+
+	expected := `
+		{
+			"server": {
+				"name": "createdserver",
+				"imageRef": "asdfasdfasdf",
+				"flavorRef": "performance1-1",
+				"flavorName": "",
+				"imageName": ""
+			},
+			"os:scheduler_hints": {
+				"group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+				"different_host": [
+					"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+					"8c19174f-4220-44f0-824a-cd1eeef10287"
+				],
+				"same_host": [
+					"a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+					"8c19174f-4220-44f0-824a-cd1eeef10287"
+				],
+				"query": [
+					"and",
+					[">=", "$free_ram_mb", "1024"],
+					[">=", "$free_disk_mb", "204800"]
+				],
+				"target_cell": "foobar",
+				"build_near_host_ip": "192.168.1.1",
+				"cidr": "/24"
+			}
+		}
+	`
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/secgroups/doc.go b/openstack/compute/v2/extensions/secgroups/doc.go
new file mode 100644
index 0000000..702f32c
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/doc.go
@@ -0,0 +1 @@
+package secgroups
diff --git a/openstack/compute/v2/extensions/secgroups/fixtures.go b/openstack/compute/v2/extensions/secgroups/fixtures.go
new file mode 100644
index 0000000..8c42e48
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/fixtures.go
@@ -0,0 +1,267 @@
+package secgroups
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const rootPath = "/os-security-groups"
+
+const listGroupsJSON = `
+{
+  "security_groups": [
+    {
+      "description": "default",
+      "id": "{groupID}",
+      "name": "default",
+      "rules": [],
+      "tenant_id": "openstack"
+    }
+  ]
+}
+`
+
+func mockListGroupsResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, 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, listGroupsJSON)
+	})
+}
+
+func mockListGroupsByServerResponse(t *testing.T, serverID string) {
+	url := fmt.Sprintf("/servers/%s%s", serverID, rootPath)
+	th.Mux.HandleFunc(url, 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, listGroupsJSON)
+	})
+}
+
+func mockCreateGroupResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group": {
+    "name": "test",
+    "description": "something"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group": {
+    "description": "something",
+    "id": "{groupID}",
+    "name": "test",
+    "rules": [],
+    "tenant_id": "openstack"
+  }
+}
+`)
+	})
+}
+
+func mockUpdateGroupResponse(t *testing.T, groupID string) {
+	url := fmt.Sprintf("%s/%s", rootPath, groupID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group": {
+    "name": "new_name",
+		"description": "new_desc"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group": {
+    "description": "something",
+    "id": "{groupID}",
+    "name": "new_name",
+    "rules": [],
+    "tenant_id": "openstack"
+  }
+}
+`)
+	})
+}
+
+func mockGetGroupsResponse(t *testing.T, groupID string) {
+	url := fmt.Sprintf("%s/%s", rootPath, groupID)
+	th.Mux.HandleFunc(url, 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": "{groupID}",
+    "name": "default",
+    "rules": [
+      {
+        "from_port": 80,
+        "group": {
+          "tenant_id": "openstack",
+          "name": "default"
+        },
+        "ip_protocol": "TCP",
+        "to_port": 85,
+        "parent_group_id": "{groupID}",
+        "ip_range": {
+						"cidr": "0.0.0.0"
+				},
+        "id": "{ruleID}"
+      }
+    ],
+    "tenant_id": "openstack"
+  }
+}
+			`)
+	})
+}
+
+func mockGetNumericIDGroupResponse(t *testing.T, groupID int) {
+	url := fmt.Sprintf("%s/%d", rootPath, groupID)
+	th.Mux.HandleFunc(url, 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": {
+		"id": 12345
+	}
+}
+			`)
+	})
+}
+
+func mockDeleteGroupResponse(t *testing.T, groupID string) {
+	url := fmt.Sprintf("%s/%s", rootPath, groupID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockAddRuleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group_rule": {
+    "from_port": 22,
+    "ip_protocol": "TCP",
+    "to_port": 22,
+    "parent_group_id": "{groupID}",
+    "cidr": "0.0.0.0/0"
+  }
+}	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_rule": {
+    "from_port": 22,
+    "group": {},
+    "ip_protocol": "TCP",
+    "to_port": 22,
+    "parent_group_id": "{groupID}",
+    "ip_range": {
+      "cidr": "0.0.0.0/0"
+    },
+    "id": "{ruleID}"
+  }
+}`)
+	})
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+	url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockAddServerToGroupResponse(t *testing.T, serverID string) {
+	url := fmt.Sprintf("/servers/%s/action", serverID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "addSecurityGroup": {
+    "name": "test"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+		fmt.Fprintf(w, `{}`)
+	})
+}
+
+func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) {
+	url := fmt.Sprintf("/servers/%s/action", serverID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "removeSecurityGroup": {
+    "name": "test"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+		fmt.Fprintf(w, `{}`)
+	})
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
new file mode 100644
index 0000000..4cef480
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -0,0 +1,257 @@
+package secgroups
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return SecurityGroupPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, createPage)
+}
+
+// List will return a collection of all the security groups for a particular
+// tenant.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return commonList(client, rootURL(client))
+}
+
+// ListByServer will return a collection of all the security groups which are
+// associated with a particular server.
+func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
+	return commonList(client, listByServerURL(client, serverID))
+}
+
+// GroupOpts is the underlying struct responsible for creating or updating
+// security groups. It therefore represents the mutable attributes of a
+// security group.
+type GroupOpts struct {
+	// Required - the name of your security group.
+	Name string `json:"name"`
+
+	// Required - the description of your security group.
+	Description string `json:"description"`
+}
+
+// CreateOpts is the struct responsible for creating a security group.
+type CreateOpts GroupOpts
+
+// CreateOptsBuilder builds the create options into a serializable format.
+type CreateOptsBuilder interface {
+	ToSecGroupCreateMap() (map[string]interface{}, error)
+}
+
+var (
+	errName = errors.New("Name is a required field")
+	errDesc = errors.New("Description is a required field")
+)
+
+// ToSecGroupCreateMap builds the create options into a serializable format.
+func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) {
+	sg := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return sg, errName
+	}
+	if opts.Description == "" {
+		return sg, errDesc
+	}
+
+	sg["name"] = opts.Name
+	sg["description"] = opts.Description
+
+	return map[string]interface{}{"security_group": sg}, nil
+}
+
+// Create will create a new security group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var result CreateResult
+
+	reqBody, err := opts.ToSecGroupCreateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = client.Post(rootURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return result
+}
+
+// UpdateOpts is the struct responsible for updating an existing security group.
+type UpdateOpts GroupOpts
+
+// UpdateOptsBuilder builds the update options into a serializable format.
+type UpdateOptsBuilder interface {
+	ToSecGroupUpdateMap() (map[string]interface{}, error)
+}
+
+// ToSecGroupUpdateMap builds the update options into a serializable format.
+func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) {
+	sg := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return sg, errName
+	}
+	if opts.Description == "" {
+		return sg, errDesc
+	}
+
+	sg["name"] = opts.Name
+	sg["description"] = opts.Description
+
+	return map[string]interface{}{"security_group": sg}, nil
+}
+
+// Update will modify the mutable properties of a security group, notably its
+// name and description.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	reqBody, err := opts.ToSecGroupUpdateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = client.Put(resourceURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return result
+}
+
+// Get will return details for a particular security group.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = client.Get(resourceURL(client, id), &result.Body, nil)
+	return result
+}
+
+// Delete will permanently delete a security group from the project.
+func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+	_, result.Err = client.Delete(resourceURL(client, id), nil)
+	return result
+}
+
+// CreateRuleOpts represents the configuration for adding a new rule to an
+// existing security group.
+type CreateRuleOpts struct {
+	// Required - the ID of the group that this rule will be added to.
+	ParentGroupID string `json:"parent_group_id"`
+
+	// Required - the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+
+	// Required - the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+
+	// Required - the protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol"`
+
+	// ONLY required if FromGroupID is blank. This represents the IP range that
+	// will be the source of network traffic to your security group. Use
+	// 0.0.0.0/0 to allow all IP addresses.
+	CIDR string `json:"cidr,omitempty"`
+
+	// ONLY required if CIDR is blank. This value represents the ID of a group
+	// that forwards traffic to the parent group. So, instead of accepting
+	// network traffic from an entire IP range, you can instead refine the
+	// inbound source by an existing security group.
+	FromGroupID string `json:"group_id,omitempty"`
+}
+
+// CreateRuleOptsBuilder builds the create rule options into a serializable format.
+type CreateRuleOptsBuilder interface {
+	ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// ToRuleCreateMap builds the create rule options into a serializable format.
+func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	rule := make(map[string]interface{})
+
+	if opts.ParentGroupID == "" {
+		return rule, errors.New("A ParentGroupID must be set")
+	}
+	if opts.FromPort == 0 {
+		return rule, errors.New("A FromPort must be set")
+	}
+	if opts.ToPort == 0 {
+		return rule, errors.New("A ToPort must be set")
+	}
+	if opts.IPProtocol == "" {
+		return rule, errors.New("A IPProtocol must be set")
+	}
+	if opts.CIDR == "" && opts.FromGroupID == "" {
+		return rule, errors.New("A CIDR or FromGroupID must be set")
+	}
+
+	rule["parent_group_id"] = opts.ParentGroupID
+	rule["from_port"] = opts.FromPort
+	rule["to_port"] = opts.ToPort
+	rule["ip_protocol"] = opts.IPProtocol
+
+	if opts.CIDR != "" {
+		rule["cidr"] = opts.CIDR
+	}
+	if opts.FromGroupID != "" {
+		rule["group_id"] = opts.FromGroupID
+	}
+
+	return map[string]interface{}{"security_group_rule": rule}, nil
+}
+
+// CreateRule will add a new rule to an existing security group (whose ID is
+// specified in CreateRuleOpts). You have the option of controlling inbound
+// traffic from either an IP range (CIDR) or from another security group.
+func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) CreateRuleResult {
+	var result CreateRuleResult
+
+	reqBody, err := opts.ToRuleCreateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = client.Post(rootRuleURL(client), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return result
+}
+
+// DeleteRule will permanently delete a rule from a security group.
+func DeleteRule(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+	_, result.Err = client.Delete(resourceRuleURL(client, id), nil)
+	return result
+}
+
+func actionMap(prefix, groupName string) map[string]map[string]string {
+	return map[string]map[string]string{
+		prefix + "SecurityGroup": map[string]string{"name": groupName},
+	}
+}
+
+// AddServerToGroup will associate a server and a security group, enforcing the
+// rules of the group on the server.
+func AddServerToGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+	_, result.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), &result.Body, nil)
+	return result
+}
+
+// RemoveServerFromGroup will disassociate a server from a security group.
+func RemoveServerFromGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+	_, result.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), &result.Body, nil)
+	return result
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests_test.go b/openstack/compute/v2/extensions/secgroups/requests_test.go
new file mode 100644
index 0000000..4e21d5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests_test.go
@@ -0,0 +1,248 @@
+package secgroups
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	serverID = "{serverID}"
+	groupID  = "{groupID}"
+	ruleID   = "{ruleID}"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListGroupsResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSecurityGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []SecurityGroup{
+			SecurityGroup{
+				ID:          groupID,
+				Description: "default",
+				Name:        "default",
+				Rules:       []Rule{},
+				TenantID:    "openstack",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestListByServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListGroupsByServerResponse(t, serverID)
+
+	count := 0
+
+	err := ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSecurityGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []SecurityGroup{
+			SecurityGroup{
+				ID:          groupID,
+				Description: "default",
+				Name:        "default",
+				Rules:       []Rule{},
+				TenantID:    "openstack",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateGroupResponse(t)
+
+	opts := CreateOpts{
+		Name:        "test",
+		Description: "something",
+	}
+
+	group, err := Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{
+		ID:          groupID,
+		Name:        "test",
+		Description: "something",
+		TenantID:    "openstack",
+		Rules:       []Rule{},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateGroupResponse(t, groupID)
+
+	opts := UpdateOpts{
+		Name:        "new_name",
+		Description: "new_desc",
+	}
+
+	group, err := Update(client.ServiceClient(), groupID, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{
+		ID:          groupID,
+		Name:        "new_name",
+		Description: "something",
+		TenantID:    "openstack",
+		Rules:       []Rule{},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetGroupsResponse(t, groupID)
+
+	group, err := Get(client.ServiceClient(), groupID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{
+		ID:          groupID,
+		Description: "default",
+		Name:        "default",
+		TenantID:    "openstack",
+		Rules: []Rule{
+			Rule{
+				FromPort:      80,
+				ToPort:        85,
+				IPProtocol:    "TCP",
+				IPRange:       IPRange{CIDR: "0.0.0.0"},
+				Group:         Group{TenantID: "openstack", Name: "default"},
+				ParentGroupID: groupID,
+				ID:            ruleID,
+			},
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGetNumericID(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	numericGroupID := 12345
+
+	mockGetNumericIDGroupResponse(t, numericGroupID)
+
+	group, err := Get(client.ServiceClient(), "12345").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{ID: "12345"}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteGroupResponse(t, groupID)
+
+	err := Delete(client.ServiceClient(), groupID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAddRule(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddRuleResponse(t)
+
+	opts := CreateRuleOpts{
+		ParentGroupID: groupID,
+		FromPort:      22,
+		ToPort:        22,
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := CreateRule(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Rule{
+		FromPort:      22,
+		ToPort:        22,
+		Group:         Group{},
+		IPProtocol:    "TCP",
+		ParentGroupID: groupID,
+		IPRange:       IPRange{CIDR: "0.0.0.0/0"},
+		ID:            ruleID,
+	}
+
+	th.AssertDeepEquals(t, expected, rule)
+}
+
+func TestDeleteRule(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteRuleResponse(t, ruleID)
+
+	err := DeleteRule(client.ServiceClient(), ruleID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAddServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddServerToGroupResponse(t, serverID)
+
+	err := AddServerToGroup(client.ServiceClient(), serverID, "test").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestRemoveServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockRemoveServerFromGroupResponse(t, serverID)
+
+	err := RemoveServerFromGroup(client.ServiceClient(), serverID, "test").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go
new file mode 100644
index 0000000..478c5dc
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/results.go
@@ -0,0 +1,147 @@
+package secgroups
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SecurityGroup represents a security group.
+type SecurityGroup struct {
+	// The unique ID of the group. If Neutron is installed, this ID will be
+	// represented as a string UUID; if Neutron is not installed, it will be a
+	// numeric ID. For the sake of consistency, we always cast it to a string.
+	ID string
+
+	// The human-readable name of the group, which needs to be unique.
+	Name string
+
+	// The human-readable description of the group.
+	Description string
+
+	// The rules which determine how this security group operates.
+	Rules []Rule
+
+	// The ID of the tenant to which this security group belongs.
+	TenantID string `mapstructure:"tenant_id"`
+}
+
+// Rule represents a security group rule, a policy which determines how a
+// security group operates and what inbound traffic it allows in.
+type Rule struct {
+	// The unique ID. If Neutron is installed, this ID will be
+	// represented as a string UUID; if Neutron is not installed, it will be a
+	// numeric ID. For the sake of consistency, we always cast it to a string.
+	ID string
+
+	// The lower bound of the port range which this security group should open up
+	FromPort int `mapstructure:"from_port"`
+
+	// The upper bound of the port range which this security group should open up
+	ToPort int `mapstructure:"to_port"`
+
+	// The IP protocol (e.g. TCP) which the security group accepts
+	IPProtocol string `mapstructure:"ip_protocol"`
+
+	// The CIDR IP range whose traffic can be received
+	IPRange IPRange `mapstructure:"ip_range"`
+
+	// The security group ID to which this rule belongs
+	ParentGroupID string `mapstructure:"parent_group_id"`
+
+	// Not documented.
+	Group Group
+}
+
+// IPRange represents the IP range whose traffic will be accepted by the
+// security group.
+type IPRange struct {
+	CIDR string
+}
+
+// Group represents a group.
+type Group struct {
+	TenantID string `mapstructure:"tenant_id"`
+	Name     string
+}
+
+// SecurityGroupPage is a single page of a SecurityGroup collection.
+type SecurityGroupPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Security Groups contains any results.
+func (page SecurityGroupPage) IsEmpty() (bool, error) {
+	users, err := ExtractSecurityGroups(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results.
+func ExtractSecurityGroups(page pagination.Page) ([]SecurityGroup, error) {
+	casted := page.(SecurityGroupPage).Body
+	var response struct {
+		SecurityGroups []SecurityGroup `mapstructure:"security_groups"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.SecurityGroups, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// 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
+}
+
+// Extract will extract a SecurityGroup struct from most responses.
+func (r commonResult) Extract() (*SecurityGroup, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		SecurityGroup SecurityGroup `mapstructure:"security_group"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &response)
+
+	return &response.SecurityGroup, err
+}
+
+// CreateRuleResult represents the result when adding rules to a security group.
+type CreateRuleResult struct {
+	gophercloud.Result
+}
+
+// Extract will extract a Rule struct from a CreateRuleResult.
+func (r CreateRuleResult) Extract() (*Rule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Rule Rule `mapstructure:"security_group_rule"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &response)
+
+	return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/secgroups/urls.go b/openstack/compute/v2/extensions/secgroups/urls.go
new file mode 100644
index 0000000..dc53fbf
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/urls.go
@@ -0,0 +1,32 @@
+package secgroups
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	secgrouppath = "os-security-groups"
+	rulepath     = "os-security-group-rules"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(secgrouppath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(secgrouppath)
+}
+
+func listByServerURL(c *gophercloud.ServiceClient, serverID string) string {
+	return c.ServiceURL("servers", serverID, secgrouppath)
+}
+
+func rootRuleURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rulepath)
+}
+
+func resourceRuleURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rulepath, id)
+}
+
+func serverActionURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("servers", id, "action")
+}
diff --git a/openstack/compute/v2/extensions/servergroups/doc.go b/openstack/compute/v2/extensions/servergroups/doc.go
new file mode 100644
index 0000000..1e5ed56
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/doc.go
@@ -0,0 +1,2 @@
+// Package servergroups provides the ability to manage server groups
+package servergroups
diff --git a/openstack/compute/v2/extensions/servergroups/fixtures.go b/openstack/compute/v2/extensions/servergroups/fixtures.go
new file mode 100644
index 0000000..133fd85
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/fixtures.go
@@ -0,0 +1,161 @@
+// +build fixtures
+
+package servergroups
+
+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 = `
+{
+    "server_groups": [
+        {
+            "id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
+            "name": "test",
+            "policies": [
+                "anti-affinity"
+            ],
+            "members": [],
+            "metadata": {}
+        },
+        {
+            "id": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+            "name": "test2",
+            "policies": [
+                "affinity"
+            ],
+            "members": [],
+            "metadata": {}
+        }
+    ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "server_group": {
+        "id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
+        "name": "test",
+        "policies": [
+            "anti-affinity"
+        ],
+        "members": [],
+        "metadata": {}
+    }
+}
+`
+
+// CreateOutput is a sample response to a Post call
+const CreateOutput = `
+{
+    "server_group": {
+        "id": "616fb98f-46ca-475e-917e-2563e5a8cd19",
+        "name": "test",
+        "policies": [
+            "anti-affinity"
+        ],
+        "members": [],
+        "metadata": {}
+    }
+}
+`
+
+// FirstServerGroup is the first result in ListOutput.
+var FirstServerGroup = ServerGroup{
+	ID:   "616fb98f-46ca-475e-917e-2563e5a8cd19",
+	Name: "test",
+	Policies: []string{
+		"anti-affinity",
+	},
+	Members:  []string{},
+	Metadata: map[string]interface{}{},
+}
+
+// SecondServerGroup is the second result in ListOutput.
+var SecondServerGroup = ServerGroup{
+	ID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	Name: "test2",
+	Policies: []string{
+		"affinity",
+	},
+	Members:  []string{},
+	Metadata: map[string]interface{}{},
+}
+
+// ExpectedServerGroupSlice is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedServerGroupSlice = []ServerGroup{FirstServerGroup, SecondServerGroup}
+
+// CreatedServerGroup is the parsed result from CreateOutput.
+var CreatedServerGroup = ServerGroup{
+	ID:   "616fb98f-46ca-475e-917e-2563e5a8cd19",
+	Name: "test",
+	Policies: []string{
+		"anti-affinity",
+	},
+	Members:  []string{},
+	Metadata: map[string]interface{}{},
+}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-server-groups", 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 an existing server group
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-server-groups/4d8c3732-a248-40ed-bebc-539a6ffd25c0", 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 server group
+func HandleCreateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-server-groups", 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_group": {
+        "name": "test",
+        "policies": [
+            "anti-affinity"
+        ]
+    }
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, CreateOutput)
+	})
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
+// an existing server group
+func HandleDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-server-groups/616fb98f-46ca-475e-917e-2563e5a8cd19", 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/servergroups/requests.go b/openstack/compute/v2/extensions/servergroups/requests.go
new file mode 100644
index 0000000..1597b43
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/requests.go
@@ -0,0 +1,77 @@
+package servergroups
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of ServerGroups.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return ServerGroupsPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notably, the
+// CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+	ToServerGroupCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies a Server Group allocation request
+type CreateOpts struct {
+	// Name is the name of the server group
+	Name string
+
+	// Policies are the server group policies
+	Policies []string
+}
+
+// ToServerGroupCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToServerGroupCreateMap() (map[string]interface{}, error) {
+	if opts.Name == "" {
+		return nil, errors.New("Missing field required for server group creation: Name")
+	}
+
+	if len(opts.Policies) < 1 {
+		return nil, errors.New("Missing field required for server group creation: Policies")
+	}
+
+	serverGroup := make(map[string]interface{})
+	serverGroup["name"] = opts.Name
+	serverGroup["policies"] = opts.Policies
+
+	return map[string]interface{}{"server_group": serverGroup}, nil
+}
+
+// Create requests the creation of a new Server Group
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToServerGroupCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Post(createURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Get returns data about a previously created ServerGroup.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
+
+// Delete requests the deletion of a previously allocated ServerGroup.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, id), nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/servergroups/requests_test.go b/openstack/compute/v2/extensions/servergroups/requests_test.go
new file mode 100644
index 0000000..07fec51
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/requests_test.go
@@ -0,0 +1,59 @@
+package servergroups
+
+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 := ExtractServerGroups(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedServerGroupSlice, 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:     "test",
+		Policies: []string{"anti-affinity"},
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &CreatedServerGroup, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstServerGroup, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "616fb98f-46ca-475e-917e-2563e5a8cd19").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/servergroups/results.go b/openstack/compute/v2/extensions/servergroups/results.go
new file mode 100644
index 0000000..d74ee5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/results.go
@@ -0,0 +1,87 @@
+package servergroups
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// A ServerGroup creates a policy for instance placement in the cloud
+type ServerGroup struct {
+	// ID is the unique ID of the Server Group.
+	ID string `mapstructure:"id"`
+
+	// Name is the common name of the server group.
+	Name string `mapstructure:"name"`
+
+	// Polices are the group policies.
+	Policies []string `mapstructure:"policies"`
+
+	// Members are the members of the server group.
+	Members []string `mapstructure:"members"`
+
+	// Metadata includes a list of all user-specified key-value pairs attached to the Server Group.
+	Metadata map[string]interface{}
+}
+
+// ServerGroupsPage stores a single, only page of ServerGroups
+// results from a List call.
+type ServerGroupsPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a ServerGroupsPage is empty.
+func (page ServerGroupsPage) IsEmpty() (bool, error) {
+	va, err := ExtractServerGroups(page)
+	return len(va) == 0, err
+}
+
+// ExtractServerGroups interprets a page of results as a slice of
+// ServerGroups.
+func ExtractServerGroups(page pagination.Page) ([]ServerGroup, error) {
+	casted := page.(ServerGroupsPage).Body
+	var response struct {
+		ServerGroups []ServerGroup `mapstructure:"server_groups"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.ServerGroups, err
+}
+
+type ServerGroupResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any Server Group resource
+// response as a ServerGroup struct.
+func (r ServerGroupResult) Extract() (*ServerGroup, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		ServerGroup *ServerGroup `json:"server_group" mapstructure:"server_group"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &res)
+	return res.ServerGroup, err
+}
+
+// CreateResult is the response from a Create operation. Call its Extract method to interpret it
+// as a ServerGroup.
+type CreateResult struct {
+	ServerGroupResult
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a ServerGroup.
+type GetResult struct {
+	ServerGroupResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
+// the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/compute/v2/extensions/servergroups/urls.go b/openstack/compute/v2/extensions/servergroups/urls.go
new file mode 100644
index 0000000..074a16c
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/urls.go
@@ -0,0 +1,25 @@
+package servergroups
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-server-groups"
+
+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, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
diff --git a/openstack/compute/v2/extensions/servergroups/urls_test.go b/openstack/compute/v2/extensions/servergroups/urls_test.go
new file mode 100644
index 0000000..bff4dfc
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/urls_test.go
@@ -0,0 +1,42 @@
+package servergroups
+
+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-server-groups", listURL(c))
+}
+
+func TestCreateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-server-groups", createURL(c))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-server-groups/"+id, getURL(c, id))
+}
+
+func TestDeleteURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-server-groups/"+id, deleteURL(c, id))
+}
diff --git a/openstack/compute/v2/extensions/startstop/doc.go b/openstack/compute/v2/extensions/startstop/doc.go
new file mode 100644
index 0000000..d2729f8
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/doc.go
@@ -0,0 +1,5 @@
+/*
+Package startstop provides functionality to start and stop servers that have
+been provisioned by the OpenStack Compute service.
+*/
+package startstop
diff --git a/openstack/compute/v2/extensions/startstop/fixtures.go b/openstack/compute/v2/extensions/startstop/fixtures.go
new file mode 100644
index 0000000..670828a
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/fixtures.go
@@ -0,0 +1,27 @@
+package startstop
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockStartServerResponse(t *testing.T, id string) {
+	th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-start": null}`)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockStopServerResponse(t *testing.T, id string) {
+	th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-stop": null}`)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/compute/v2/extensions/startstop/requests.go b/openstack/compute/v2/extensions/startstop/requests.go
new file mode 100644
index 0000000..0e090e6
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests.go
@@ -0,0 +1,23 @@
+package startstop
+
+import "github.com/rackspace/gophercloud"
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "action")
+}
+
+// Start is the operation responsible for starting a Compute server.
+func Start(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	reqBody := map[string]interface{}{"os-start": nil}
+	_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
+	return res
+}
+
+// Stop is the operation responsible for stopping a Compute server.
+func Stop(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	reqBody := map[string]interface{}{"os-stop": nil}
+	_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/startstop/requests_test.go b/openstack/compute/v2/extensions/startstop/requests_test.go
new file mode 100644
index 0000000..97a121b
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests_test.go
@@ -0,0 +1,30 @@
+package startstop
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const serverID = "{serverId}"
+
+func TestStart(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockStartServerResponse(t, serverID)
+
+	err := Start(client.ServiceClient(), serverID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestStop(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockStopServerResponse(t, serverID)
+
+	err := Stop(client.ServiceClient(), serverID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/doc.go b/openstack/compute/v2/extensions/tenantnetworks/doc.go
new file mode 100644
index 0000000..65c46ff
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/doc.go
@@ -0,0 +1,2 @@
+// Package tenantnetworks provides the ability for tenants to see information about the networks they have access to
+package tenantnetworks
diff --git a/openstack/compute/v2/extensions/tenantnetworks/fixtures.go b/openstack/compute/v2/extensions/tenantnetworks/fixtures.go
new file mode 100644
index 0000000..0cfa72a
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/fixtures.go
@@ -0,0 +1,84 @@
+// +build fixtures
+
+package tenantnetworks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput is a sample response to a List call.
+const ListOutput = `
+{
+    "networks": [
+        {
+            "cidr": "10.0.0.0/29",
+            "id": "20c8acc0-f747-4d71-a389-46d078ebf047",
+            "label": "mynet_0"
+        },
+        {
+            "cidr": "10.0.0.10/29",
+            "id": "20c8acc0-f747-4d71-a389-46d078ebf000",
+            "label": "mynet_1"
+        }
+    ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "network": {
+			"cidr": "10.0.0.10/29",
+			"id": "20c8acc0-f747-4d71-a389-46d078ebf000",
+			"label": "mynet_1"
+		}
+}
+`
+
+// FirstNetwork is the first result in ListOutput.
+var nilTime time.Time
+var FirstNetwork = Network{
+	CIDR: "10.0.0.0/29",
+	ID:   "20c8acc0-f747-4d71-a389-46d078ebf047",
+	Name: "mynet_0",
+}
+
+// SecondNetwork is the second result in ListOutput.
+var SecondNetwork = Network{
+	CIDR: "10.0.0.10/29",
+	ID:   "20c8acc0-f747-4d71-a389-46d078ebf000",
+	Name: "mynet_1",
+}
+
+// ExpectedNetworkSlice is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedNetworkSlice = []Network{FirstNetwork, SecondNetwork}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-tenant-networks", 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 an existing network.
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-tenant-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/requests.go b/openstack/compute/v2/extensions/tenantnetworks/requests.go
new file mode 100644
index 0000000..3ec13d3
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/requests.go
@@ -0,0 +1,22 @@
+package tenantnetworks
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of Network.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(client)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, url, createPage)
+}
+
+// Get returns data about a previously created Network.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/requests_test.go b/openstack/compute/v2/extensions/tenantnetworks/requests_test.go
new file mode 100644
index 0000000..fc4ee4f
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/requests_test.go
@@ -0,0 +1,37 @@
+package tenantnetworks
+
+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 := ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedNetworkSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &SecondNetwork, actual)
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/results.go b/openstack/compute/v2/extensions/tenantnetworks/results.go
new file mode 100644
index 0000000..8050092
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/results.go
@@ -0,0 +1,68 @@
+package tenantnetworks
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// A Network represents a nova-network that an instance communicates on
+type Network struct {
+	// CIDR is the IPv4 subnet.
+	CIDR string `mapstructure:"cidr"`
+
+	// ID is the UUID of the network.
+	ID string `mapstructure:"id"`
+
+	// Name is the common name that the network has.
+	Name string `mapstructure:"label"`
+}
+
+// NetworkPage stores a single, only page of Networks
+// results from a List call.
+type NetworkPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a NetworkPage is empty.
+func (page NetworkPage) IsEmpty() (bool, error) {
+	va, err := ExtractNetworks(page)
+	return len(va) == 0, err
+}
+
+// ExtractNetworks interprets a page of results as a slice of Networks
+func ExtractNetworks(page pagination.Page) ([]Network, error) {
+	networks := page.(NetworkPage).Body
+	var res struct {
+		Networks []Network `mapstructure:"networks"`
+	}
+
+	err := mapstructure.WeakDecode(networks, &res)
+
+	return res.Networks, err
+}
+
+type NetworkResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any Network resource
+// response as a Network struct.
+func (r NetworkResult) Extract() (*Network, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *Network `json:"network" mapstructure:"network"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+	return res.Network, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a Network.
+type GetResult struct {
+	NetworkResult
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/urls.go b/openstack/compute/v2/extensions/tenantnetworks/urls.go
new file mode 100644
index 0000000..2401a5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/urls.go
@@ -0,0 +1,17 @@
+package tenantnetworks
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-tenant-networks"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return resourceURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/urls_test.go b/openstack/compute/v2/extensions/tenantnetworks/urls_test.go
new file mode 100644
index 0000000..39c464e
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/urls_test.go
@@ -0,0 +1,25 @@
+package tenantnetworks
+
+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-tenant-networks", listURL(c))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-tenant-networks/"+id, getURL(c, id))
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/doc.go b/openstack/compute/v2/extensions/volumeattach/doc.go
new file mode 100644
index 0000000..22f68d8
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/doc.go
@@ -0,0 +1,3 @@
+// Package volumeattach provides the ability to attach and detach volumes
+// to instances
+package volumeattach
diff --git a/openstack/compute/v2/extensions/volumeattach/requests.go b/openstack/compute/v2/extensions/volumeattach/requests.go
new file mode 100644
index 0000000..b4ebede
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/requests.go
@@ -0,0 +1,75 @@
+package volumeattach
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of VolumeAttachments.
+func List(client *gophercloud.ServiceClient, serverId string) pagination.Pager {
+	return pagination.NewPager(client, listURL(client, serverId), func(r pagination.PageResult) pagination.Page {
+		return VolumeAttachmentsPage{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 {
+	ToVolumeAttachmentCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies volume attachment creation or import parameters.
+type CreateOpts struct {
+	// Device is the device that the volume will attach to the instance as. Omit for "auto"
+	Device string
+
+	// VolumeID is the ID of the volume to attach to the instance
+	VolumeID string
+}
+
+// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) {
+	if opts.VolumeID == "" {
+		return nil, errors.New("Missing field required for volume attachment creation: VolumeID")
+	}
+
+	volumeAttachment := make(map[string]interface{})
+	volumeAttachment["volumeId"] = opts.VolumeID
+	if opts.Device != "" {
+		volumeAttachment["device"] = opts.Device
+	}
+
+	return map[string]interface{}{"volumeAttachment": volumeAttachment}, nil
+}
+
+// Create requests the creation of a new volume attachment on the server
+func Create(client *gophercloud.ServiceClient, serverId string, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToVolumeAttachmentCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Post(createURL(client, serverId), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Get returns public data about a previously created VolumeAttachment.
+func Get(client *gophercloud.ServiceClient, serverId, aId string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, serverId, aId), &res.Body, nil)
+	return res
+}
+
+// Delete requests the deletion of a previous stored VolumeAttachment from the server.
+func Delete(client *gophercloud.ServiceClient, serverId, aId string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, serverId, aId), nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/requests_test.go b/openstack/compute/v2/extensions/volumeattach/requests_test.go
new file mode 100644
index 0000000..b0a765b
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/requests_test.go
@@ -0,0 +1,94 @@
+package volumeattach
+
+import (
+	"testing"
+
+	fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// FirstVolumeAttachment is the first result in ListOutput.
+var FirstVolumeAttachment = VolumeAttachment{
+	Device:   "/dev/vdd",
+	ID:       "a26887c6-c47b-4654-abb5-dfadf7d3f803",
+	ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
+}
+
+// SecondVolumeAttachment is the first result in ListOutput.
+var SecondVolumeAttachment = VolumeAttachment{
+	Device:   "/dev/vdc",
+	ID:       "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+	ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+}
+
+// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedVolumeAttachmentSlice = []VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment}
+
+//CreatedVolumeAttachment is the parsed result from CreatedOutput.
+var CreatedVolumeAttachment = VolumeAttachment{
+	Device:   "/dev/vdc",
+	ID:       "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+	ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleListSuccessfully(t)
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	count := 0
+	err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumeAttachments(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleCreateSuccessfully(t)
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	actual, err := Create(client.ServiceClient(), serverId, CreateOpts{
+		Device:   "/dev/vdc",
+		VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleGetSuccessfully(t)
+	aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	actual, err := Get(client.ServiceClient(), serverId, aId).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &SecondVolumeAttachment, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleDeleteSuccessfully(t)
+	aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	err := Delete(client.ServiceClient(), serverId, aId).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/results.go b/openstack/compute/v2/extensions/volumeattach/results.go
new file mode 100644
index 0000000..26be39e
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/results.go
@@ -0,0 +1,84 @@
+package volumeattach
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// VolumeAttach controls the attachment of a volume to an instance.
+type VolumeAttachment struct {
+	// ID is a unique id of the attachment
+	ID string `mapstructure:"id"`
+
+	// Device is what device the volume is attached as
+	Device string `mapstructure:"device"`
+
+	// VolumeID is the ID of the attached volume
+	VolumeID string `mapstructure:"volumeId"`
+
+	// ServerID is the ID of the instance that has the volume attached
+	ServerID string `mapstructure:"serverId"`
+}
+
+// VolumeAttachmentsPage stores a single, only page of VolumeAttachments
+// results from a List call.
+type VolumeAttachmentsPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a VolumeAttachmentsPage is empty.
+func (page VolumeAttachmentsPage) IsEmpty() (bool, error) {
+	va, err := ExtractVolumeAttachments(page)
+	return len(va) == 0, err
+}
+
+// ExtractVolumeAttachments interprets a page of results as a slice of
+// VolumeAttachments.
+func ExtractVolumeAttachments(page pagination.Page) ([]VolumeAttachment, error) {
+	casted := page.(VolumeAttachmentsPage).Body
+	var response struct {
+		VolumeAttachments []VolumeAttachment `mapstructure:"volumeAttachments"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.VolumeAttachments, err
+}
+
+type VolumeAttachmentResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any VolumeAttachment resource
+// response as a VolumeAttachment struct.
+func (r VolumeAttachmentResult) Extract() (*VolumeAttachment, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		VolumeAttachment *VolumeAttachment `json:"volumeAttachment" mapstructure:"volumeAttachment"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+	return res.VolumeAttachment, err
+}
+
+// CreateResult is the response from a Create operation. Call its Extract method to interpret it
+// as a VolumeAttachment.
+type CreateResult struct {
+	VolumeAttachmentResult
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a VolumeAttachment.
+type GetResult struct {
+	VolumeAttachmentResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its Extract method to determine if
+// the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/testing/doc.go b/openstack/compute/v2/extensions/volumeattach/testing/doc.go
new file mode 100644
index 0000000..183391a
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/testing/doc.go
@@ -0,0 +1,7 @@
+/*
+This is package created is to hold fixtures (which imports testing),
+so that importing volumeattach package does not inadvertently import testing into production code
+More information here:
+https://github.com/rackspace/gophercloud/issues/473
+*/
+package testing
diff --git a/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go b/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go
new file mode 100644
index 0000000..c469bfb
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go
@@ -0,0 +1,110 @@
+// +build fixtures
+
+package testing
+
+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 = `
+{
+  "volumeAttachments": [
+    {
+      "device": "/dev/vdd",
+      "id": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
+      "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+      "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803"
+    },
+    {
+      "device": "/dev/vdc",
+      "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+      "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+      "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+    }
+  ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+  "volumeAttachment": {
+    "device": "/dev/vdc",
+    "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+    "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+    "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+  }
+}
+`
+
+// CreateOutput is a sample response to a Create call.
+const CreateOutput = `
+{
+  "volumeAttachment": {
+    "device": "/dev/vdc",
+    "id": "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+    "serverId": "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+    "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+  }
+}
+`
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", 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 an existing attachment
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", 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 attachment
+func HandleCreateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments", 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, `
+{
+  "volumeAttachment": {
+    "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+    "device": "/dev/vdc"
+  }
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, CreateOutput)
+	})
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request for a
+// an existing attachment
+func HandleDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/os-volume_attachments/a26887c6-c47b-4654-abb5-dfadf7d3f804", 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/volumeattach/urls.go b/openstack/compute/v2/extensions/volumeattach/urls.go
new file mode 100644
index 0000000..9d9d178
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/urls.go
@@ -0,0 +1,25 @@
+package volumeattach
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-volume_attachments"
+
+func resourceURL(c *gophercloud.ServiceClient, serverId string) string {
+	return c.ServiceURL("servers", serverId, resourcePath)
+}
+
+func listURL(c *gophercloud.ServiceClient, serverId string) string {
+	return resourceURL(c, serverId)
+}
+
+func createURL(c *gophercloud.ServiceClient, serverId string) string {
+	return resourceURL(c, serverId)
+}
+
+func getURL(c *gophercloud.ServiceClient, serverId, aId string) string {
+	return c.ServiceURL("servers", serverId, resourcePath, aId)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, serverId, aId string) string {
+	return getURL(c, serverId, aId)
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/urls_test.go b/openstack/compute/v2/extensions/volumeattach/urls_test.go
new file mode 100644
index 0000000..8ee0e42
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/urls_test.go
@@ -0,0 +1,46 @@
+package volumeattach
+
+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()
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments", listURL(c, serverId))
+}
+
+func TestCreateURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments", createURL(c, serverId))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+	aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+
+	th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments/"+aId, getURL(c, serverId, aId))
+}
+
+func TestDeleteURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+	aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+
+	th.CheckEquals(t, c.Endpoint+"servers/"+serverId+"/os-volume_attachments/"+aId, deleteURL(c, serverId, aId))
+}
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..59123aa
--- /dev/null
+++ b/openstack/compute/v2/flavors/requests.go
@@ -0,0 +1,103 @@
+package flavors
+
+import (
+	"fmt"
+
+	"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 res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
+
+// IDFromName is a convienience function that returns a flavor's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	flavorCount := 0
+	flavorID := ""
+	if name == "" {
+		return "", fmt.Errorf("A flavor name must be provided.")
+	}
+	pager := ListDetail(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		flavorList, err := ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, f := range flavorList {
+			if f.Name == name {
+				flavorCount++
+				flavorID = f.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch flavorCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find flavor: %s", name)
+	case 1:
+		return flavorID, nil
+	default:
+		return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, name)
+	}
+}
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..8dddd70
--- /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 response 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..1e021ad
--- /dev/null
+++ b/openstack/compute/v2/images/requests.go
@@ -0,0 +1,109 @@
+package images
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 interpret the result as an openstack Image.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = client.Get(getURL(client, id), &result.Body, nil)
+	return result
+}
+
+// Delete deletes the specified image ID.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var result DeleteResult
+	_, result.Err = client.Delete(deleteURL(client, id), nil)
+	return result
+}
+
+// IDFromName is a convienience function that returns an image's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	imageCount := 0
+	imageID := ""
+	if name == "" {
+		return "", fmt.Errorf("An image name must be provided.")
+	}
+	pager := ListDetail(client, &ListOpts{
+		Name: name,
+	})
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		imageList, err := ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, i := range imageList {
+			if i.Name == name {
+				imageCount++
+				imageID = i.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch imageCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find image: %s", name)
+	case 1:
+		return imageID, nil
+	default:
+		return "", fmt.Errorf("Found %d images matching %s", imageCount, name)
+	}
+}
diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go
new file mode 100644
index 0000000..93a97bd
--- /dev/null
+++ b/openstack/compute/v2/images/requests_test.go
@@ -0,0 +1,191 @@
+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)
+}
+
+// Test Image delete
+func TestDeleteImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/12345678", 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(), "12345678")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..40e814d
--- /dev/null
+++ b/openstack/compute/v2/images/results.go
@@ -0,0 +1,95 @@
+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
+}
+
+// DeleteResult represents the result of an image.Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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..b1bf103
--- /dev/null
+++ b/openstack/compute/v2/images/urls.go
@@ -0,0 +1,15 @@
+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)
+}
+
+func deleteURL(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..4339a16
--- /dev/null
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -0,0 +1,664 @@
+// +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{}{},
+		SecurityGroups: []map[string]interface{}{
+			map[string]interface{}{
+				"name": "default",
+			},
+		},
+	}
+
+	// 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{}{},
+		SecurityGroups: []map[string]interface{}{
+			map[string]interface{}{
+				"name": "default",
+			},
+		},
+	}
+)
+
+// 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)
+	})
+}
+
+// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request.
+func HandleServerRescueSuccessfully(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, `{ "rescue": { "adminPass": "1234567890" } }`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(`{ "adminPass": "1234567890" }`))
+	})
+}
+
+// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request.
+func HandleMetadatumGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", 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")
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+	})
+}
+
+// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request.
+func HandleMetadatumCreateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"meta": {
+				"foo": "bar"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+	})
+}
+
+// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request.
+func HandleMetadatumDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", 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)
+	})
+}
+
+// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
+func HandleMetadataGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata", 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")
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+	})
+}
+
+// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request.
+func HandleMetadataResetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+				"metadata": {
+					"foo": "bar",
+					"this": "that"
+				}
+			}`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+	})
+}
+
+// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request.
+func HandleMetadataUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata", 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, `{
+				"metadata": {
+					"foo": "baz",
+					"this": "those"
+				}
+			}`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`))
+	})
+}
+
+// ListAddressesExpected represents an expected repsonse from a ListAddresses request.
+var ListAddressesExpected = map[string][]Address{
+	"public": []Address{
+		Address{
+			Version: 4,
+			Address: "80.56.136.39",
+		},
+		Address{
+			Version: 6,
+			Address: "2001:4800:790e:510:be76:4eff:fe04:82a8",
+		},
+	},
+	"private": []Address{
+		Address{
+			Version: 4,
+			Address: "10.880.3.154",
+		},
+	},
+}
+
+// HandleAddressListSuccessfully sets up the test server to respond to a ListAddresses request.
+func HandleAddressListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf/ips", 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, `{
+			"addresses": {
+				"public": [
+				{
+					"version": 4,
+					"addr": "50.56.176.35"
+				},
+				{
+					"version": 6,
+					"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8"
+				}
+				],
+				"private": [
+				{
+					"version": 4,
+					"addr": "10.180.3.155"
+				}
+				]
+			}
+		}`)
+	})
+}
+
+// ListNetworkAddressesExpected represents an expected repsonse from a ListAddressesByNetwork request.
+var ListNetworkAddressesExpected = []Address{
+	Address{
+		Version: 4,
+		Address: "50.56.176.35",
+	},
+	Address{
+		Version: 6,
+		Address: "2001:4800:780e:510:be76:4eff:fe04:84a8",
+	},
+}
+
+// HandleNetworkAddressListSuccessfully sets up the test server to respond to a ListAddressesByNetwork request.
+func HandleNetworkAddressListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf/ips/public", 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, `{
+			"public": [
+			{
+				"version": 4,
+				"addr": "50.56.176.35"
+				},
+				{
+					"version": 6,
+					"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8"
+				}
+			]
+			}`)
+	})
+}
+
+// HandleCreateServerImageSuccessfully sets up the test server to respond to a TestCreateServerImage request.
+func HandleCreateServerImageSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/serverimage/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Location", "https://0.0.0.0/images/xxxx-xxxxx-xxxxx-xxxx")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
new file mode 100644
index 0000000..f9839d9
--- /dev/null
+++ b/openstack/compute/v2/servers/requests.go
@@ -0,0 +1,852 @@
+package servers
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/images"
+	"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"`
+
+	// Bool to show all tenants
+	AllTenants bool `q:"all_tenants"`
+}
+
+// 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
+}
+
+// Personality is an array of files that are injected into the server at launch.
+type Personality []*File
+
+// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch.
+// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested,
+// json.Marshal will call File's MarshalJSON method.
+type File struct {
+	// Path of the file
+	Path string
+	// Contents of the file. Maximum content size is 255 bytes.
+	Contents []byte
+}
+
+// MarshalJSON marshals the escaped file, base64 encoding the contents.
+func (f *File) MarshalJSON() ([]byte, error) {
+	file := struct {
+		Path     string `json:"path"`
+		Contents string `json:"contents"`
+	}{
+		Path:     f.Path,
+		Contents: base64.StdEncoding.EncodeToString(f.Contents),
+	}
+	return json.Marshal(file)
+}
+
+// CreateOpts specifies server creation parameters.
+type CreateOpts struct {
+	// Name [required] is the name to assign to the newly launched server.
+	Name string
+
+	// ImageRef [optional; required if ImageName is not provided] is the ID or full
+	// URL to the image that contains the server's OS and initial state.
+	// Also optional if using the boot-from-volume extension.
+	ImageRef string
+
+	// ImageName [optional; required if ImageRef is not provided] is the name of the
+	// image that contains the server's OS and initial state.
+	// Also optional if using the boot-from-volume extension.
+	ImageName string
+
+	// FlavorRef [optional; required if FlavorName is not provided] is the ID or
+	// full URL to the flavor that describes the server's specs.
+	FlavorRef string
+
+	// FlavorName [optional; required if FlavorRef is not provided] is the name of
+	// the flavor that describes the server's specs.
+	FlavorName 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 files to inject into the server at launch.
+	// Create will base64-encode file contents for you.
+	Personality Personality
+
+	// ConfigDrive [optional] enables metadata injection through a configuration drive.
+	ConfigDrive bool
+
+	// AdminPass [optional] sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the response.
+	AdminPass string
+
+	// AccessIPv4 [optional] specifies an IPv4 address for the instance.
+	AccessIPv4 string
+
+	// AccessIPv6 [optional] specifies an IPv6 address for the instance.
+	AccessIPv6 string
+}
+
+// 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["imageName"] = opts.ImageName
+	server["flavorRef"] = opts.FlavorRef
+	server["flavorName"] = opts.FlavorName
+
+	if opts.UserData != nil {
+		encoded := base64.StdEncoding.EncodeToString(opts.UserData)
+		server["user_data"] = &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 opts.AdminPass != "" {
+		server["adminPass"] = opts.AdminPass
+	}
+	if opts.AccessIPv4 != "" {
+		server["accessIPv4"] = opts.AccessIPv4
+	}
+	if opts.AccessIPv6 != "" {
+		server["accessIPv6"] = opts.AccessIPv6
+	}
+
+	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}
+		}
+		server["security_groups"] = securityGroups
+	}
+
+	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
+	}
+
+	if len(opts.Personality) > 0 {
+		server["personality"] = opts.Personality
+	}
+
+	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
+	}
+
+	// If ImageRef isn't provided, use ImageName to ascertain the image ID.
+	if reqBody["server"].(map[string]interface{})["imageRef"].(string) == "" {
+		imageName := reqBody["server"].(map[string]interface{})["imageName"].(string)
+		if imageName == "" {
+			res.Err = errors.New("One and only one of ImageRef and ImageName must be provided.")
+			return res
+		}
+		imageID, err := images.IDFromName(client, imageName)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		reqBody["server"].(map[string]interface{})["imageRef"] = imageID
+	}
+	delete(reqBody["server"].(map[string]interface{}), "imageName")
+
+	// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
+	if reqBody["server"].(map[string]interface{})["flavorRef"].(string) == "" {
+		flavorName := reqBody["server"].(map[string]interface{})["flavorName"].(string)
+		if flavorName == "" {
+			res.Err = errors.New("One and only one of FlavorRef and FlavorName must be provided.")
+			return res
+		}
+		flavorID, err := flavors.IDFromName(client, flavorName)
+		if err != nil {
+			res.Err = err
+			return res
+		}
+		reqBody["server"].(map[string]interface{})["flavorRef"] = flavorID
+	}
+	delete(reqBody["server"].(map[string]interface{}), "flavorName")
+
+	_, res.Err = client.Post(listURL(client), reqBody, &res.Body, nil)
+	return res
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(deleteURL(client, id), nil)
+	return res
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = client.Get(getURL(client, id), &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 203},
+	})
+	return result
+}
+
+// UpdateOptsBuilder allows extensions 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
+	reqBody := opts.ToServerUpdateMap()
+	_, result.Err = client.Put(updateURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	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 = client.Post(actionURL(client, id), req, nil, nil)
+	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
+	}
+
+	reqBody := struct {
+		C map[string]string `json:"reboot"`
+	}{
+		map[string]string{"type": string(how)},
+	}
+
+	_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
+	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 files to inject into the server at launch.
+	// Rebuild will base64-encode file contents for you.
+	Personality Personality
+}
+
+// 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 len(opts.Personality) > 0 {
+		server["personality"] = opts.Personality
+	}
+
+	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 = client.Post(actionURL(client, id), reqBody, &result.Body, nil)
+	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 for 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 = client.Post(actionURL(client, id), reqBody, nil, nil)
+	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
+
+	reqBody := map[string]interface{}{"confirmResize": nil}
+	_, res.Err = client.Post(actionURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{201, 202, 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
+	reqBody := map[string]interface{}{"revertResize": nil}
+	_, res.Err = client.Post(actionURL(client, id), reqBody, nil, nil)
+	return res
+}
+
+// RescueOptsBuilder is an interface that allows extensions to override the
+// default structure of a Rescue request.
+type RescueOptsBuilder interface {
+	ToServerRescueMap() (map[string]interface{}, error)
+}
+
+// RescueOpts represents the configuration options used to control a Rescue
+// option.
+type RescueOpts struct {
+	// AdminPass is the desired administrative password for the instance in
+	// RESCUE mode. If it's left blank, the server will generate a password.
+	AdminPass string
+}
+
+// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON
+// request body for the Rescue request.
+func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) {
+	server := make(map[string]interface{})
+	if opts.AdminPass != "" {
+		server["adminPass"] = opts.AdminPass
+	}
+	return map[string]interface{}{"rescue": server}, nil
+}
+
+// Rescue instructs the provider to place the server into RESCUE mode.
+func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) RescueResult {
+	var result RescueResult
+
+	if id == "" {
+		result.Err = fmt.Errorf("ID is required")
+		return result
+	}
+	reqBody, err := opts.ToServerRescueMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = client.Post(actionURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return result
+}
+
+// ResetMetadataOptsBuilder allows extensions to add additional parameters to the
+// Reset request.
+type ResetMetadataOptsBuilder interface {
+	ToMetadataResetMap() (map[string]interface{}, error)
+}
+
+// MetadataOpts is a map that contains key-value pairs.
+type MetadataOpts map[string]string
+
+// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
+	return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
+	return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ResetMetadata will create multiple new key-value pairs for the given server ID.
+// Note: Using this operation will erase any already-existing metadata and create
+// the new metadata provided. To keep any already-existing metadata, use the
+// UpdateMetadatas or UpdateMetadata function.
+func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult {
+	var res ResetMetadataResult
+	metadata, err := opts.ToMetadataResetMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	_, res.Err = client.Put(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Metadata requests all the metadata for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult {
+	var res GetMetadataResult
+	_, res.Err = client.Get(metadataURL(client, id), &res.Body, nil)
+	return res
+}
+
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type UpdateMetadataOptsBuilder interface {
+	ToMetadataUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID.
+// This operation does not affect already-existing metadata that is not specified
+// by opts.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
+	var res UpdateMetadataResult
+	metadata, err := opts.ToMetadataUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	_, res.Err = client.Post(metadataURL(client, id), metadata, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// MetadatumOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type MetadatumOptsBuilder interface {
+	ToMetadatumCreateMap() (map[string]interface{}, string, error)
+}
+
+// MetadatumOpts is a map of length one that contains a key-value pair.
+type MetadatumOpts map[string]string
+
+// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts.
+func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
+	if len(opts) != 1 {
+		return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.")
+	}
+	metadatum := map[string]interface{}{"meta": opts}
+	var key string
+	for k := range metadatum["meta"].(MetadatumOpts) {
+		key = k
+	}
+	return metadatum, key, nil
+}
+
+// CreateMetadatum will create or update the key-value pair with the given key for the given server ID.
+func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult {
+	var res CreateMetadatumResult
+	metadatum, key, err := opts.ToMetadatumCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Put(metadatumURL(client, id, key), metadatum, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Metadatum requests the key-value pair with the given key for the given server ID.
+func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult {
+	var res GetMetadatumResult
+	_, res.Err = client.Request("GET", metadatumURL(client, id, key), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+	})
+	return res
+}
+
+// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
+func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
+	var res DeleteMetadatumResult
+	_, res.Err = client.Delete(metadatumURL(client, id, key), nil)
+	return res
+}
+
+// ListAddresses makes a request against the API to list the servers IP addresses.
+func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return AddressPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listAddressesURL(client, id), createPageFn)
+}
+
+// ListAddressesByNetwork makes a request against the API to list the servers IP addresses
+// for the given network.
+func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return NetworkAddressPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), createPageFn)
+}
+
+type CreateImageOpts struct {
+	// Name [required] of the image/snapshot
+	Name string
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the created image.
+	Metadata map[string]string
+}
+
+type CreateImageOptsBuilder interface {
+	ToServerCreateImageMap() (map[string]interface{}, error)
+}
+
+// ToServerCreateImageMap formats a CreateImageOpts structure into a request body.
+func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) {
+	var err error
+	img := make(map[string]interface{})
+	if opts.Name == "" {
+		return nil, fmt.Errorf("Cannot create a server image without a name")
+	}
+	img["name"] = opts.Name
+	if opts.Metadata != nil {
+		img["metadata"] = opts.Metadata
+	}
+	createImage := make(map[string]interface{})
+	createImage["createImage"] = img
+	return createImage, err
+}
+
+// CreateImage makes a request against the nova API to schedule an image to be created of the server
+func CreateImage(client *gophercloud.ServiceClient, serverId string, opts CreateImageOptsBuilder) CreateImageResult {
+	var res CreateImageResult
+	reqBody, err := opts.ToServerCreateImageMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	response, err := client.Post(actionURL(client, serverId), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	res.Err = err
+	res.Header = response.Header
+	return res
+}
+
+// IDFromName is a convienience function that returns a server's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	serverCount := 0
+	serverID := ""
+	if name == "" {
+		return "", fmt.Errorf("A server name must be provided.")
+	}
+	pager := List(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		serverList, err := ExtractServers(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range serverList {
+			if s.Name == name {
+				serverCount++
+				serverID = s.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch serverCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find server: %s", name)
+	case 1:
+		return serverID, nil
+	default:
+		return "", fmt.Errorf("Found %d servers matching %s", serverCount, name)
+	}
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..88cb54d
--- /dev/null
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -0,0 +1,373 @@
+package servers
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"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 TestListAllServers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerListSuccessfully(t)
+
+	allPages, err := List(client.ServiceClient(), ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := ExtractServers(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ServerHerp, actual[0])
+	th.CheckDeepEquals(t, ServerDerp, actual[1])
+}
+
+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)
+
+	res := Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, res.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)
+}
+
+func TestRescue(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleServerRescueSuccessfully(t)
+
+	res := Rescue(client.ServiceClient(), "1234asdf", RescueOpts{
+		AdminPass: "1234567890",
+	})
+	th.AssertNoErr(t, res.Err)
+	adminPass, _ := res.Extract()
+	th.AssertEquals(t, "1234567890", adminPass)
+}
+
+func TestGetMetadatum(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadatumGetSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar"}
+	actual, err := Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateMetadatum(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadatumCreateSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar"}
+	actual, err := CreateMetadatum(client.ServiceClient(), "1234asdf", MetadatumOpts{"foo": "bar"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteMetadatum(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadatumDeleteSuccessfully(t)
+
+	err := DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadataGetSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar", "this": "that"}
+	actual, err := Metadata(client.ServiceClient(), "1234asdf").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResetMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadataResetSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar", "this": "that"}
+	actual, err := ResetMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+		"foo":  "bar",
+		"this": "that",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadataUpdateSuccessfully(t)
+
+	expected := map[string]string{"foo": "baz", "this": "those"}
+	actual, err := UpdateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+		"foo":  "baz",
+		"this": "those",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListAddresses(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAddressListSuccessfully(t)
+
+	expected := ListAddressesExpected
+	pages := 0
+	err := ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 networks, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
+
+func TestListAddressesByNetwork(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNetworkAddressListSuccessfully(t)
+
+	expected := ListNetworkAddressesExpected
+	pages := 0
+	err := ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractNetworkAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 addresses, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
+
+func TestCreateServerImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateServerImageSuccessfully(t)
+
+	_, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID()
+	th.AssertNoErr(t, err)
+}
+
+func TestMarshalPersonality(t *testing.T) {
+	name := "/etc/test"
+	contents := []byte("asdfasdf")
+
+	personality := Personality{
+		&File{
+			Path:     name,
+			Contents: contents,
+		},
+	}
+
+	data, err := json.Marshal(personality)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var actual []map[string]string
+	err = json.Unmarshal(data, &actual)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if len(actual) != 1 {
+		t.Fatal("expected personality length 1")
+	}
+
+	if actual[0]["path"] != name {
+		t.Fatal("file path incorrect")
+	}
+
+	if actual[0]["contents"] != base64.StdEncoding.EncodeToString(contents) {
+		t.Fatal("file contents incorrect")
+	}
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..f278709
--- /dev/null
+++ b/openstack/compute/v2/servers/results.go
@@ -0,0 +1,372 @@
+package servers
+
+import (
+	"reflect"
+	"fmt"
+	"path"
+	"net/url"
+
+	"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"`
+	}
+
+	config := &mapstructure.DecoderConfig{
+		DecodeHook: toMapFromString,
+		Result:     &response,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	err = decoder.Decode(r.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	return &response.Server, nil
+}
+
+// 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
+}
+
+// DeleteResult temporarily contains the response from a Delete call.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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.ErrResult
+}
+
+// RescueResult represents the result of a server rescue operation
+type RescueResult struct {
+	ActionResult
+}
+
+// CreateImageResult represents the result of an image creation operation
+type CreateImageResult struct {
+	gophercloud.Result
+}
+
+// ExtractImageID gets the ID of the newly created server image from the header
+func (res CreateImageResult) ExtractImageID() (string, error) {
+	if res.Err != nil {
+		return "", res.Err
+	}
+	// Get the image id from the header
+	u, err := url.ParseRequestURI(res.Header.Get("Location"))
+	if err != nil {
+		return "", fmt.Errorf("Failed to parse the image id: %s", err.Error())
+	}
+	imageId := path.Base(u.Path)
+	if imageId == "." || imageId == "/" {
+		return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u)
+	}
+	return imageId, nil
+}
+
+// Extract interprets any RescueResult as an AdminPass, if possible.
+func (r RescueResult) Extract() (string, error) {
+	if r.Err != nil {
+		return "", r.Err
+	}
+
+	var response struct {
+		AdminPass string `mapstructure:"adminPass"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return response.AdminPass, 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, 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"`
+
+	// SecurityGroups includes the security groups that this instance has applied to it
+	SecurityGroups []map[string]interface{} `json:"security_groups" mapstructure:"security_groups"`
+}
+
+// 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"`
+	}
+
+	config := &mapstructure.DecoderConfig{
+		DecodeHook: toMapFromString,
+		Result:     &response,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	err = decoder.Decode(casted)
+
+	return response.Servers, err
+}
+
+// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
+type MetadataResult struct {
+	gophercloud.Result
+}
+
+// GetMetadataResult temporarily contains the response from a metadata Get call.
+type GetMetadataResult struct {
+	MetadataResult
+}
+
+// ResetMetadataResult temporarily contains the response from a metadata Reset call.
+type ResetMetadataResult struct {
+	MetadataResult
+}
+
+// UpdateMetadataResult temporarily contains the response from a metadata Update call.
+type UpdateMetadataResult struct {
+	MetadataResult
+}
+
+// MetadatumResult contains the result of a call for individual a single key-value pair.
+type MetadatumResult struct {
+	gophercloud.Result
+}
+
+// GetMetadatumResult temporarily contains the response from a metadatum Get call.
+type GetMetadatumResult struct {
+	MetadatumResult
+}
+
+// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
+type CreateMetadatumResult struct {
+	MetadatumResult
+}
+
+// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
+type DeleteMetadatumResult struct {
+	gophercloud.ErrResult
+}
+
+// Extract interprets any MetadataResult as a Metadata, if possible.
+func (r MetadataResult) Extract() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Metadata map[string]string `mapstructure:"metadata"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return response.Metadata, err
+}
+
+// Extract interprets any MetadatumResult as a Metadatum, if possible.
+func (r MetadatumResult) Extract() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Metadatum map[string]string `mapstructure:"meta"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return response.Metadatum, err
+}
+
+func toMapFromString(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) {
+	if (from == reflect.String) && (to == reflect.Map) {
+		return map[string]interface{}{}, nil
+	}
+	return data, nil
+}
+
+// Address represents an IP address.
+type Address struct {
+	Version int    `mapstructure:"version"`
+	Address string `mapstructure:"addr"`
+}
+
+// AddressPage abstracts the raw results of making a ListAddresses() 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 ExtractAddresses call.
+type AddressPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if an AddressPage contains no networks.
+func (r AddressPage) IsEmpty() (bool, error) {
+	addresses, err := ExtractAddresses(r)
+	if err != nil {
+		return true, err
+	}
+	return len(addresses) == 0, nil
+}
+
+// ExtractAddresses interprets the results of a single page from a ListAddresses() call,
+// producing a map of addresses.
+func ExtractAddresses(page pagination.Page) (map[string][]Address, error) {
+	casted := page.(AddressPage).Body
+
+	var response struct {
+		Addresses map[string][]Address `mapstructure:"addresses"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response.Addresses, err
+}
+
+// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() 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 ExtractAddresses call.
+type NetworkAddressPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a NetworkAddressPage contains no addresses.
+func (r NetworkAddressPage) IsEmpty() (bool, error) {
+	addresses, err := ExtractNetworkAddresses(r)
+	if err != nil {
+		return true, err
+	}
+	return len(addresses) == 0, nil
+}
+
+// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call,
+// producing a slice of addresses.
+func ExtractNetworkAddresses(page pagination.Page) ([]Address, error) {
+	casted := page.(NetworkAddressPage).Body
+
+	var response map[string][]Address
+	err := mapstructure.Decode(casted, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	var key string
+	for k := range response {
+		key = k
+	}
+
+	return response[key], err
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..8998354
--- /dev/null
+++ b/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,47 @@
+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")
+}
+
+func metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
+	return client.ServiceURL("servers", id, "metadata", key)
+}
+
+func metadataURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "metadata")
+}
+
+func listAddressesURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "ips")
+}
+
+func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
+	return client.ServiceURL("servers", id, "ips", network)
+}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
new file mode 100644
index 0000000..17a1d28
--- /dev/null
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -0,0 +1,68 @@
+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)
+}
+
+func TestMetadatumURL(t *testing.T) {
+	actual := metadatumURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "servers/foo/metadata/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+	actual := metadataURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo/metadata"
+	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/db/v1/configurations/doc.go b/openstack/db/v1/configurations/doc.go
new file mode 100644
index 0000000..45b9cfb
--- /dev/null
+++ b/openstack/db/v1/configurations/doc.go
@@ -0,0 +1,11 @@
+// Package configurations provides information and interaction with the
+// configuration API resource in the Rackspace Database service.
+//
+// A configuration group is a collection of key/value pairs which define how a
+// particular database operates. These key/value pairs are specific to each
+// datastore type and serve like settings. Some directives are capable of being
+// applied dynamically, while other directives require a server restart to take
+// effect. The configuration group can be applied to an instance at creation or
+// applied to an existing instance to modify the behavior of the running
+// datastore on the instance.
+package configurations
diff --git a/openstack/db/v1/configurations/fixtures.go b/openstack/db/v1/configurations/fixtures.go
new file mode 100644
index 0000000..ae65416
--- /dev/null
+++ b/openstack/db/v1/configurations/fixtures.go
@@ -0,0 +1,157 @@
+package configurations
+
+import (
+	"fmt"
+	"time"
+)
+
+var (
+	timestamp  = "2015-11-12T14:22:42Z"
+	timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var singleConfigJSON = `
+{
+  "created": "` + timestamp + `",
+  "datastore_name": "mysql",
+  "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+  "datastore_version_name": "5.6",
+  "description": "example_description",
+  "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+  "name": "example-configuration-name",
+  "updated": "` + timestamp + `"
+}
+`
+
+var singleConfigWithValuesJSON = `
+{
+  "created": "` + timestamp + `",
+  "datastore_name": "mysql",
+  "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+  "datastore_version_name": "5.6",
+  "description": "example description",
+  "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+  "instance_count": 0,
+  "name": "example-configuration-name",
+  "updated": "` + timestamp + `",
+  "values": {
+    "collation_server": "latin1_swedish_ci",
+    "connect_timeout": 120
+  }
+}
+`
+
+var (
+	ListConfigsJSON  = fmt.Sprintf(`{"configurations": [%s]}`, singleConfigJSON)
+	GetConfigJSON    = fmt.Sprintf(`{"configuration": %s}`, singleConfigJSON)
+	CreateConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigWithValuesJSON)
+)
+
+var CreateReq = `
+{
+  "configuration": {
+    "datastore": {
+      "type": "a00000a0-00a0-0a00-00a0-000a000000aa",
+      "version": "b00000b0-00b0-0b00-00b0-000b000000bb"
+    },
+    "description": "example description",
+    "name": "example-configuration-name",
+    "values": {
+      "collation_server": "latin1_swedish_ci",
+      "connect_timeout": 120
+    }
+  }
+}
+`
+
+var UpdateReq = `
+{
+  "configuration": {
+    "values": {
+      "connect_timeout": 300
+    }
+  }
+}
+`
+
+var ListInstancesJSON = `
+{
+  "instances": [
+    {
+      "id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+      "name": "json_rack_instance"
+    }
+  ]
+}
+`
+
+var ListParamsJSON = `
+{
+  "configuration-parameters": [
+    {
+      "max": 1,
+      "min": 0,
+      "name": "innodb_file_per_table",
+      "restart_required": true,
+      "type": "integer"
+    },
+    {
+      "max": 4294967296,
+      "min": 0,
+      "name": "key_buffer_size",
+      "restart_required": false,
+      "type": "integer"
+    },
+    {
+      "max": 65535,
+      "min": 2,
+      "name": "connect_timeout",
+      "restart_required": false,
+      "type": "integer"
+    },
+    {
+      "max": 4294967296,
+      "min": 0,
+      "name": "join_buffer_size",
+      "restart_required": false,
+      "type": "integer"
+    }
+  ]
+}
+`
+
+var GetParamJSON = `
+{
+  "max": 1,
+  "min": 0,
+  "name": "innodb_file_per_table",
+  "restart_required": true,
+  "type": "integer"
+}
+`
+
+var ExampleConfig = Config{
+	Created:              timeVal,
+	DatastoreName:        "mysql",
+	DatastoreVersionID:   "b00000b0-00b0-0b00-00b0-000b000000bb",
+	DatastoreVersionName: "5.6",
+	Description:          "example_description",
+	ID:                   "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+	Name:                 "example-configuration-name",
+	Updated:              timeVal,
+}
+
+var ExampleConfigWithValues = Config{
+	Created:              timeVal,
+	DatastoreName:        "mysql",
+	DatastoreVersionID:   "b00000b0-00b0-0b00-00b0-000b000000bb",
+	DatastoreVersionName: "5.6",
+	Description:          "example description",
+	ID:                   "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+	Name:                 "example-configuration-name",
+	Updated:              timeVal,
+	Values: map[string]interface{}{
+		"collation_server": "latin1_swedish_ci",
+		"connect_timeout":  120,
+	},
+}
diff --git a/openstack/db/v1/configurations/requests.go b/openstack/db/v1/configurations/requests.go
new file mode 100644
index 0000000..83c7102
--- /dev/null
+++ b/openstack/db/v1/configurations/requests.go
@@ -0,0 +1,287 @@
+package configurations
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all of the available configurations.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return ConfigPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, baseURL(client), pageFn)
+}
+
+// CreateOptsBuilder is a top-level interface which renders a JSON map.
+type CreateOptsBuilder interface {
+	ToConfigCreateMap() (map[string]interface{}, error)
+}
+
+// DatastoreOpts is the primary options struct for creating and modifying
+// how configuration resources are associated with datastores.
+type DatastoreOpts struct {
+	// [OPTIONAL] The type of datastore. Defaults to "MySQL".
+	Type string
+
+	// [OPTIONAL] The specific version of a datastore. Defaults to "5.6".
+	Version string
+}
+
+// ToMap renders a JSON map for a datastore setting.
+func (opts DatastoreOpts) ToMap() (map[string]string, error) {
+	datastore := map[string]string{}
+
+	if opts.Type != "" {
+		datastore["type"] = opts.Type
+	}
+
+	if opts.Version != "" {
+		datastore["version"] = opts.Version
+	}
+
+	return datastore, nil
+}
+
+// CreateOpts is the struct responsible for configuring new configurations.
+type CreateOpts struct {
+	// [REQUIRED] The configuration group name
+	Name string
+
+	// [REQUIRED] A map of user-defined configuration settings that will define
+	// how each associated datastore works. Each key/value pair is specific to a
+	// datastore type.
+	Values map[string]interface{}
+
+	// [OPTIONAL] Associates the configuration group with a particular datastore.
+	Datastore *DatastoreOpts
+
+	// [OPTIONAL] A human-readable explanation for the group.
+	Description string
+}
+
+// ToConfigCreateMap casts a CreateOpts struct into a JSON map.
+func (opts CreateOpts) ToConfigCreateMap() (map[string]interface{}, error) {
+	if opts.Name == "" {
+		return nil, errors.New("Name is a required field")
+	}
+	if len(opts.Values) == 0 {
+		return nil, errors.New("Values must be a populated map")
+	}
+
+	config := map[string]interface{}{
+		"name":   opts.Name,
+		"values": opts.Values,
+	}
+
+	if opts.Datastore != nil {
+		ds, err := opts.Datastore.ToMap()
+		if err != nil {
+			return config, err
+		}
+		config["datastore"] = ds
+	}
+
+	if opts.Description != "" {
+		config["description"] = opts.Description
+	}
+
+	return map[string]interface{}{"configuration": config}, nil
+}
+
+// Create will create a new configuration group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToConfigCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+	})
+
+	return res
+}
+
+// Get will retrieve the details for a specified configuration group.
+func Get(client *gophercloud.ServiceClient, configID string) GetResult {
+	var res GetResult
+
+	_, res.Err = client.Request("GET", resourceURL(client, configID), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
+	})
+
+	return res
+}
+
+// UpdateOptsBuilder is the top-level interface for casting update options into
+// JSON maps.
+type UpdateOptsBuilder interface {
+	ToConfigUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the struct responsible for modifying existing configurations.
+type UpdateOpts struct {
+	// [OPTIONAL] The configuration group name
+	Name string
+
+	// [OPTIONAL] A map of user-defined configuration settings that will define
+	// how each associated datastore works. Each key/value pair is specific to a
+	// datastore type.
+	Values map[string]interface{}
+
+	// [OPTIONAL] Associates the configuration group with a particular datastore.
+	Datastore *DatastoreOpts
+
+	// [OPTIONAL] A human-readable explanation for the group.
+	Description string
+}
+
+// ToConfigUpdateMap will cast an UpdateOpts struct into a JSON map.
+func (opts UpdateOpts) ToConfigUpdateMap() (map[string]interface{}, error) {
+	config := map[string]interface{}{}
+
+	if opts.Name != "" {
+		config["name"] = opts.Name
+	}
+
+	if opts.Description != "" {
+		config["description"] = opts.Description
+	}
+
+	if opts.Datastore != nil {
+		ds, err := opts.Datastore.ToMap()
+		if err != nil {
+			return config, err
+		}
+		config["datastore"] = ds
+	}
+
+	if len(opts.Values) > 0 {
+		config["values"] = opts.Values
+	}
+
+	return map[string]interface{}{"configuration": config}, nil
+}
+
+// Update will modify an existing configuration group by performing a merge
+// between new and existing values. If the key already exists, the new value
+// will overwrite. All other keys will remain unaffected.
+func Update(client *gophercloud.ServiceClient, configID string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToConfigUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("PATCH", resourceURL(client, configID), gophercloud.RequestOpts{
+		OkCodes:  []int{200},
+		JSONBody: &reqBody,
+	})
+
+	return res
+}
+
+// Replace will modify an existing configuration group by overwriting the
+// entire parameter group with the new values provided. Any existing keys not
+// included in UpdateOptsBuilder will be deleted.
+func Replace(client *gophercloud.ServiceClient, configID string, opts UpdateOptsBuilder) ReplaceResult {
+	var res ReplaceResult
+
+	reqBody, err := opts.ToConfigUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("PUT", resourceURL(client, configID), gophercloud.RequestOpts{
+		OkCodes:  []int{202},
+		JSONBody: &reqBody,
+	})
+
+	return res
+}
+
+// Delete will permanently delete a configuration group. Please note that
+// config groups cannot be deleted whilst still attached to running instances -
+// you must detach and then delete them.
+func Delete(client *gophercloud.ServiceClient, configID string) DeleteResult {
+	var res DeleteResult
+
+	_, res.Err = client.Request("DELETE", resourceURL(client, configID), gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return res
+}
+
+// ListInstances will list all the instances associated with a particular
+// configuration group.
+func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return instances.InstancePage{pagination.LinkedPageBase{PageResult: r}}
+	}
+	return pagination.NewPager(client, instancesURL(client, configID), pageFn)
+}
+
+// ListDatastoreParams will list all the available and supported parameters
+// that can be used for a particular datastore ID and a particular version.
+// For example, if you are wondering how you can configure a MySQL 5.6 instance,
+// you can use this operation (you will need to retrieve the MySQL datastore ID
+// by using the datastores API).
+func ListDatastoreParams(client *gophercloud.ServiceClient, datastoreID, versionID string) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return ParamPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listDSParamsURL(client, datastoreID, versionID), pageFn)
+}
+
+// GetDatastoreParam will retrieve information about a specific configuration
+// parameter. For example, you can use this operation to understand more about
+// "innodb_file_per_table" configuration param for MySQL datastores. You will
+// need the param's ID first, which can be attained by using the ListDatastoreParams
+// operation.
+func GetDatastoreParam(client *gophercloud.ServiceClient, datastoreID, versionID, paramID string) ParamResult {
+	var res ParamResult
+
+	_, res.Err = client.Request("GET", getDSParamURL(client, datastoreID, versionID, paramID), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
+	})
+
+	return res
+}
+
+// ListGlobalParams is similar to ListDatastoreParams but does not require a
+// DatastoreID.
+func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return ParamPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listGlobalParamsURL(client, versionID), pageFn)
+}
+
+// GetGlobalParam is similar to GetDatastoreParam but does not require a
+// DatastoreID.
+func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) ParamResult {
+	var res ParamResult
+
+	_, res.Err = client.Request("GET", getGlobalParamURL(client, versionID, paramID), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
+	})
+
+	return res
+}
diff --git a/openstack/db/v1/configurations/requests_test.go b/openstack/db/v1/configurations/requests_test.go
new file mode 100644
index 0000000..db66f29
--- /dev/null
+++ b/openstack/db/v1/configurations/requests_test.go
@@ -0,0 +1,236 @@
+package configurations
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	configID = "{configID}"
+	_baseURL = "/configurations"
+	resURL   = _baseURL + "/" + configID
+
+	dsID               = "{datastoreID}"
+	versionID          = "{versionID}"
+	paramID            = "{paramID}"
+	dsParamListURL     = "/datastores/" + dsID + "/versions/" + versionID + "/parameters"
+	dsParamGetURL      = "/datastores/" + dsID + "/versions/" + versionID + "/parameters/" + paramID
+	globalParamListURL = "/datastores/versions/" + versionID + "/parameters"
+	globalParamGetURL  = "/datastores/versions/" + versionID + "/parameters/" + paramID
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _baseURL, "GET", "", ListConfigsJSON, 200)
+
+	count := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractConfigs(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Config{ExampleConfig}
+		th.AssertDeepEquals(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()
+	fixture.SetupHandler(t, resURL, "GET", "", GetConfigJSON, 200)
+
+	config, err := Get(fake.ServiceClient(), configID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &ExampleConfig, config)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _baseURL, "POST", CreateReq, CreateConfigJSON, 200)
+
+	opts := CreateOpts{
+		Datastore: &DatastoreOpts{
+			Type:    "a00000a0-00a0-0a00-00a0-000a000000aa",
+			Version: "b00000b0-00b0-0b00-00b0-000b000000bb",
+		},
+		Description: "example description",
+		Name:        "example-configuration-name",
+		Values: map[string]interface{}{
+			"collation_server": "latin1_swedish_ci",
+			"connect_timeout":  120,
+		},
+	}
+
+	config, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &ExampleConfigWithValues, config)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "PATCH", UpdateReq, "", 200)
+
+	opts := UpdateOpts{
+		Values: map[string]interface{}{
+			"connect_timeout": 300,
+		},
+	}
+
+	err := Update(fake.ServiceClient(), configID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestReplace(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "PUT", UpdateReq, "", 202)
+
+	opts := UpdateOpts{
+		Values: map[string]interface{}{
+			"connect_timeout": 300,
+		},
+	}
+
+	err := Replace(fake.ServiceClient(), configID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+
+	err := Delete(fake.ServiceClient(), configID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListInstances(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL+"/instances", "GET", "", ListInstancesJSON, 200)
+
+	expectedInstance := instances.Instance{
+		ID:   "d4603f69-ec7e-4e9b-803f-600b9205576f",
+		Name: "json_rack_instance",
+	}
+
+	pages := 0
+	err := ListInstances(fake.ServiceClient(), configID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := instances.ExtractInstances(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.AssertDeepEquals(t, actual, []instances.Instance{expectedInstance})
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestListDSParams(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, dsParamListURL, "GET", "", ListParamsJSON, 200)
+
+	pages := 0
+	err := ListDatastoreParams(fake.ServiceClient(), dsID, versionID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractParams(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Param{
+			Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+			Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+			Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+			Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+		}
+
+		th.AssertDeepEquals(t, actual, expected)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetDSParam(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, dsParamGetURL, "GET", "", GetParamJSON, 200)
+
+	param, err := GetDatastoreParam(fake.ServiceClient(), dsID, versionID, paramID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Param{
+		Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+	}
+
+	th.AssertDeepEquals(t, expected, param)
+}
+
+func TestListGlobalParams(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, globalParamListURL, "GET", "", ListParamsJSON, 200)
+
+	pages := 0
+	err := ListGlobalParams(fake.ServiceClient(), versionID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractParams(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Param{
+			Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+			Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+			Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+			Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+		}
+
+		th.AssertDeepEquals(t, actual, expected)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetGlobalParam(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, globalParamGetURL, "GET", "", GetParamJSON, 200)
+
+	param, err := GetGlobalParam(fake.ServiceClient(), versionID, paramID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Param{
+		Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+	}
+
+	th.AssertDeepEquals(t, expected, param)
+}
diff --git a/openstack/db/v1/configurations/results.go b/openstack/db/v1/configurations/results.go
new file mode 100644
index 0000000..d0d1d6e
--- /dev/null
+++ b/openstack/db/v1/configurations/results.go
@@ -0,0 +1,197 @@
+package configurations
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Config represents a configuration group API resource.
+type Config struct {
+	Created              time.Time `mapstructure:"-"`
+	Updated              time.Time `mapstructure:"-"`
+	DatastoreName        string    `mapstructure:"datastore_name"`
+	DatastoreVersionID   string    `mapstructure:"datastore_version_id"`
+	DatastoreVersionName string    `mapstructure:"datastore_version_name"`
+	Description          string
+	ID                   string
+	Name                 string
+	Values               map[string]interface{}
+}
+
+// ConfigPage contains a page of Config resources in a paginated collection.
+type ConfigPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a ConfigPage is empty.
+func (r ConfigPage) IsEmpty() (bool, error) {
+	is, err := ExtractConfigs(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractConfigs will retrieve a slice of Config structs from a page.
+func ExtractConfigs(page pagination.Page) ([]Config, error) {
+	casted := page.(ConfigPage).Body
+
+	var resp struct {
+		Configs []Config `mapstructure:"configurations" json:"configurations"`
+	}
+
+	if err := mapstructure.Decode(casted, &resp); err != nil {
+		return nil, err
+	}
+
+	var vals []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		vals = casted.(map[string]interface{})["configurations"].([]interface{})
+	case map[string][]interface{}:
+		vals = casted.(map[string][]interface{})["configurations"]
+	default:
+		return resp.Configs, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i, v := range vals {
+		val := v.(map[string]interface{})
+
+		if t, ok := val["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Configs, err
+			}
+			resp.Configs[i].Created = creationTime
+		}
+
+		if t, ok := val["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Configs, err
+			}
+			resp.Configs[i].Updated = updatedTime
+		}
+	}
+
+	return resp.Configs, nil
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will retrieve a Config resource from an operation result.
+func (r commonResult) Extract() (*Config, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Config Config `mapstructure:"configuration"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	val := r.Body.(map[string]interface{})["configuration"].(map[string]interface{})
+
+	if t, ok := val["created"].(string); ok && t != "" {
+		creationTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Config, err
+		}
+		response.Config.Created = creationTime
+	}
+
+	if t, ok := val["updated"].(string); ok && t != "" {
+		updatedTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Config, err
+		}
+		response.Config.Updated = updatedTime
+	}
+
+	return &response.Config, err
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	commonResult
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// ReplaceResult represents the result of a Replace operation.
+type ReplaceResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// Param represents a configuration parameter API resource.
+type Param struct {
+	Max             int
+	Min             int
+	Name            string
+	RestartRequired bool `mapstructure:"restart_required" json:"restart_required"`
+	Type            string
+}
+
+// ParamPage contains a page of Param resources in a paginated collection.
+type ParamPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a ParamPage is empty.
+func (r ParamPage) IsEmpty() (bool, error) {
+	is, err := ExtractParams(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractParams will retrieve a slice of Param structs from a page.
+func ExtractParams(page pagination.Page) ([]Param, error) {
+	casted := page.(ParamPage).Body
+
+	var resp struct {
+		Params []Param `mapstructure:"configuration-parameters" json:"configuration-parameters"`
+	}
+
+	err := mapstructure.Decode(casted, &resp)
+	return resp.Params, err
+}
+
+// ParamResult represents the result of an operation which retrieves details
+// about a particular configuration param.
+type ParamResult struct {
+	gophercloud.Result
+}
+
+// Extract will retrieve a param from an operation result.
+func (r ParamResult) Extract() (*Param, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var param Param
+
+	err := mapstructure.Decode(r.Body, &param)
+	return &param, err
+}
diff --git a/openstack/db/v1/configurations/urls.go b/openstack/db/v1/configurations/urls.go
new file mode 100644
index 0000000..abea961
--- /dev/null
+++ b/openstack/db/v1/configurations/urls.go
@@ -0,0 +1,31 @@
+package configurations
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("configurations")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, configID string) string {
+	return c.ServiceURL("configurations", configID)
+}
+
+func instancesURL(c *gophercloud.ServiceClient, configID string) string {
+	return c.ServiceURL("configurations", configID, "instances")
+}
+
+func listDSParamsURL(c *gophercloud.ServiceClient, datastoreID, versionID string) string {
+	return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters")
+}
+
+func getDSParamURL(c *gophercloud.ServiceClient, datastoreID, versionID, paramID string) string {
+	return c.ServiceURL("datastores", datastoreID, "versions", versionID, "parameters", paramID)
+}
+
+func listGlobalParamsURL(c *gophercloud.ServiceClient, versionID string) string {
+	return c.ServiceURL("datastores", "versions", versionID, "parameters")
+}
+
+func getGlobalParamURL(c *gophercloud.ServiceClient, versionID, paramID string) string {
+	return c.ServiceURL("datastores", "versions", versionID, "parameters", paramID)
+}
diff --git a/openstack/db/v1/databases/doc.go b/openstack/db/v1/databases/doc.go
new file mode 100644
index 0000000..15275fe
--- /dev/null
+++ b/openstack/db/v1/databases/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the database API
+// resource in the OpenStack Database service.
+//
+// A database, when referred to here, refers to the database engine running on
+// an instance.
+package databases
diff --git a/openstack/db/v1/databases/fixtures.go b/openstack/db/v1/databases/fixtures.go
new file mode 100644
index 0000000..3e67721
--- /dev/null
+++ b/openstack/db/v1/databases/fixtures.go
@@ -0,0 +1,61 @@
+package databases
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	instanceID = "{instanceID}"
+	resURL     = "/instances/" + instanceID + "/databases"
+)
+
+var createDBsReq = `
+{
+	"databases": [
+		{
+			"character_set": "utf8",
+			"collate": "utf8_general_ci",
+			"name": "testingdb"
+		},
+		{
+			"name": "sampledb"
+		}
+	]
+}
+`
+
+var listDBsResp = `
+{
+	"databases": [
+		{
+			"name": "anotherexampledb"
+		},
+		{
+			"name": "exampledb"
+		},
+		{
+			"name": "nextround"
+		},
+		{
+			"name": "sampledb"
+		},
+		{
+			"name": "testingdb"
+		}
+	]
+}
+`
+
+func HandleCreate(t *testing.T) {
+	fixture.SetupHandler(t, resURL, "POST", createDBsReq, "", 202)
+}
+
+func HandleList(t *testing.T) {
+	fixture.SetupHandler(t, resURL, "GET", "", listDBsResp, 200)
+}
+
+func HandleDelete(t *testing.T) {
+	fixture.SetupHandler(t, resURL+"/{dbName}", "DELETE", "", "", 202)
+}
diff --git a/openstack/db/v1/databases/requests.go b/openstack/db/v1/databases/requests.go
new file mode 100644
index 0000000..f1eb5d9
--- /dev/null
+++ b/openstack/db/v1/databases/requests.go
@@ -0,0 +1,115 @@
+package databases
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder builds create options
+type CreateOptsBuilder interface {
+	ToDBCreateMap() (map[string]interface{}, error)
+}
+
+// DatabaseOpts is the struct responsible for configuring a database; often in
+// the context of an instance.
+type CreateOpts struct {
+	// [REQUIRED] Specifies the name of the database. Valid names can be composed
+	// of the following characters: letters (either case); numbers; these
+	// characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
+	// permitted anywhere. Prohibited characters that are forbidden include:
+	// single quotes, double quotes, back quotes, semicolons, commas, backslashes,
+	// and forward slashes.
+	Name string
+
+	// [OPTIONAL] Set of symbols and encodings. The default character set is
+	// "utf8". See http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for
+	// supported character sets.
+	CharSet string
+
+	// [OPTIONAL] Set of rules for comparing characters in a character set. The
+	// default value for collate is "utf8_general_ci". See
+	// http://dev.mysql.com/doc/refman/5.1/en/charset-mysql.html for supported
+	// collations.
+	Collate string
+}
+
+// ToMap is a helper function to convert individual DB create opt structures
+// into sub-maps.
+func (opts CreateOpts) ToMap() (map[string]string, error) {
+	if opts.Name == "" {
+		return nil, fmt.Errorf("Name is a required field")
+	}
+	if len(opts.Name) > 64 {
+		return nil, fmt.Errorf("Name must be less than 64 chars long")
+	}
+
+	db := map[string]string{"name": opts.Name}
+
+	if opts.CharSet != "" {
+		db["character_set"] = opts.CharSet
+	}
+	if opts.Collate != "" {
+		db["collate"] = opts.Collate
+	}
+	return db, nil
+}
+
+// BatchCreateOpts allows for multiple databases to created and modified.
+type BatchCreateOpts []CreateOpts
+
+// ToDBCreateMap renders a JSON map for creating DBs.
+func (opts BatchCreateOpts) ToDBCreateMap() (map[string]interface{}, error) {
+	dbs := make([]map[string]string, len(opts))
+	for i, db := range opts {
+		dbMap, err := db.ToMap()
+		if err != nil {
+			return nil, err
+		}
+		dbs[i] = dbMap
+	}
+	return map[string]interface{}{"databases": dbs}, nil
+}
+
+// Create will create a new database within the specified instance. If the
+// specified instance does not exist, a 404 error will be returned.
+func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToDBCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("POST", baseURL(client, instanceID), gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// List will list all of the databases for a specified instance. Note: this
+// operation will only return user-defined databases; it will exclude system
+// databases like "mysql", "information_schema", "lost+found" etc.
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return DBPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+// Delete will permanently delete the database within a specified instance.
+// All contained data inside the database will also be permanently deleted.
+func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) DeleteResult {
+	var res DeleteResult
+
+	_, res.Err = client.Request("DELETE", dbURL(client, instanceID, dbName), gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return res
+}
diff --git a/openstack/db/v1/databases/requests_test.go b/openstack/db/v1/databases/requests_test.go
new file mode 100644
index 0000000..8a1b297
--- /dev/null
+++ b/openstack/db/v1/databases/requests_test.go
@@ -0,0 +1,66 @@
+package databases
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreate(t)
+
+	opts := BatchCreateOpts{
+		CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"},
+		CreateOpts{Name: "sampledb"},
+	}
+
+	res := Create(fake.ServiceClient(), instanceID, opts)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	expectedDBs := []Database{
+		Database{Name: "anotherexampledb"},
+		Database{Name: "exampledb"},
+		Database{Name: "nextround"},
+		Database{Name: "sampledb"},
+		Database{Name: "testingdb"},
+	}
+
+	pages := 0
+	err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractDBs(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, expectedDBs, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDelete(t)
+
+	err := Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/db/v1/databases/results.go b/openstack/db/v1/databases/results.go
new file mode 100644
index 0000000..7d4b6ae
--- /dev/null
+++ b/openstack/db/v1/databases/results.go
@@ -0,0 +1,72 @@
+package databases
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Database represents a Database API resource.
+type Database struct {
+	// Specifies the name of the MySQL database.
+	Name string
+
+	// Set of symbols and encodings. The default character set is utf8.
+	CharSet string
+
+	// Set of rules for comparing characters in a character set. The default
+	// value for collate is utf8_general_ci.
+	Collate string
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// DBPage represents a single page of a paginated DB collection.
+type DBPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page DBPage) IsEmpty() (bool, error) {
+	dbs, err := ExtractDBs(page)
+	if err != nil {
+		return true, err
+	}
+	return len(dbs) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page DBPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"databases_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractDBs will convert a generic pagination struct into a more
+// relevant slice of DB structs.
+func ExtractDBs(page pagination.Page) ([]Database, error) {
+	casted := page.(DBPage).Body
+
+	var response struct {
+		Databases []Database `mapstructure:"databases"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Databases, err
+}
diff --git a/openstack/db/v1/databases/urls.go b/openstack/db/v1/databases/urls.go
new file mode 100644
index 0000000..027ca58
--- /dev/null
+++ b/openstack/db/v1/databases/urls.go
@@ -0,0 +1,11 @@
+package databases
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+	return c.ServiceURL("instances", instanceID, "databases")
+}
+
+func dbURL(c *gophercloud.ServiceClient, instanceID, dbName string) string {
+	return c.ServiceURL("instances", instanceID, "databases", dbName)
+}
diff --git a/openstack/db/v1/datastores/doc.go b/openstack/db/v1/datastores/doc.go
new file mode 100644
index 0000000..ae14026
--- /dev/null
+++ b/openstack/db/v1/datastores/doc.go
@@ -0,0 +1,3 @@
+// Package datastores provides information and interaction with the datastore
+// API resource in the Rackspace Database service.
+package datastores
diff --git a/openstack/db/v1/datastores/fixtures.go b/openstack/db/v1/datastores/fixtures.go
new file mode 100644
index 0000000..fd767cd
--- /dev/null
+++ b/openstack/db/v1/datastores/fixtures.go
@@ -0,0 +1,100 @@
+package datastores
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const version1JSON = `
+{
+	"id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+	"links": [
+		{
+			"href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb",
+			"rel": "self"
+		},
+		{
+			"href": "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb",
+			"rel": "bookmark"
+		}
+	],
+	"name": "5.1"
+}
+`
+
+const version2JSON = `
+{
+	"id": "c00000b0-00c0-0c00-00c0-000b000000cc",
+	"links": [
+		{
+			"href": "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc",
+			"rel": "self"
+		},
+		{
+			"href": "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc",
+			"rel": "bookmark"
+		}
+	],
+	"name": "5.2"
+}
+`
+
+var versionsJSON = fmt.Sprintf(`"versions": [%s, %s]`, version1JSON, version2JSON)
+
+var singleDSJSON = fmt.Sprintf(`
+{
+  "default_version": "c00000b0-00c0-0c00-00c0-000b000000cc",
+  "id": "10000000-0000-0000-0000-000000000001",
+  "links": [
+    {
+      "href": "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001",
+      "rel": "self"
+    },
+    {
+      "href": "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001",
+      "rel": "bookmark"
+    }
+  ],
+  "name": "mysql",
+  %s
+}
+`, versionsJSON)
+
+var (
+	ListDSResp       = fmt.Sprintf(`{"datastores":[%s]}`, singleDSJSON)
+	GetDSResp        = fmt.Sprintf(`{"datastore":%s}`, singleDSJSON)
+	ListVersionsResp = fmt.Sprintf(`{%s}`, versionsJSON)
+	GetVersionResp   = fmt.Sprintf(`{"version":%s}`, version1JSON)
+)
+
+var ExampleVersion1 = Version{
+	ID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+	Links: []gophercloud.Link{
+		gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
+		gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
+	},
+	Name: "5.1",
+}
+
+var exampleVersion2 = Version{
+	ID: "c00000b0-00c0-0c00-00c0-000b000000cc",
+	Links: []gophercloud.Link{
+		gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
+		gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
+	},
+	Name: "5.2",
+}
+
+var ExampleVersions = []Version{ExampleVersion1, exampleVersion2}
+
+var ExampleDatastore = Datastore{
+	DefaultVersion: "c00000b0-00c0-0c00-00c0-000b000000cc",
+	ID:             "10000000-0000-0000-0000-000000000001",
+	Links: []gophercloud.Link{
+		gophercloud.Link{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001"},
+		gophercloud.Link{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/10000000-0000-0000-0000-000000000001"},
+	},
+	Name:     "mysql",
+	Versions: ExampleVersions,
+}
diff --git a/openstack/db/v1/datastores/requests.go b/openstack/db/v1/datastores/requests.go
new file mode 100644
index 0000000..9e147ab
--- /dev/null
+++ b/openstack/db/v1/datastores/requests.go
@@ -0,0 +1,47 @@
+package datastores
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available datastore types that instances can use.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return DatastorePage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, baseURL(client), pageFn)
+}
+
+// Get will retrieve the details of a specified datastore type.
+func Get(client *gophercloud.ServiceClient, datastoreID string) GetResult {
+	var res GetResult
+
+	_, res.Err = client.Request("GET", resourceURL(client, datastoreID), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
+	})
+
+	return res
+}
+
+// ListVersions will list all of the available versions for a specified
+// datastore type.
+func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return VersionPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, versionsURL(client, datastoreID), pageFn)
+}
+
+// GetVersion will retrieve the details of a specified datastore version.
+func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) GetVersionResult {
+	var res GetVersionResult
+
+	_, res.Err = client.Request("GET", versionURL(client, datastoreID, versionID), gophercloud.RequestOpts{
+		OkCodes:      []int{200},
+		JSONResponse: &res.Body,
+	})
+
+	return res
+}
diff --git a/openstack/db/v1/datastores/requests_test.go b/openstack/db/v1/datastores/requests_test.go
new file mode 100644
index 0000000..b4ce871
--- /dev/null
+++ b/openstack/db/v1/datastores/requests_test.go
@@ -0,0 +1,78 @@
+package datastores
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores", "GET", "", ListDSResp, 200)
+
+	pages := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractDatastores(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, []Datastore{ExampleDatastore}, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores/{dsID}", "GET", "", GetDSResp, 200)
+
+	ds, err := Get(fake.ServiceClient(), "{dsID}").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &ExampleDatastore, ds)
+}
+
+func TestListVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores/{dsID}/versions", "GET", "", ListVersionsResp, 200)
+
+	pages := 0
+
+	err := ListVersions(fake.ServiceClient(), "{dsID}").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractVersions(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, ExampleVersions, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetVersion(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores/{dsID}/versions/{versionID}", "GET", "", GetVersionResp, 200)
+
+	ds, err := GetVersion(fake.ServiceClient(), "{dsID}", "{versionID}").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &ExampleVersion1, ds)
+}
diff --git a/openstack/db/v1/datastores/results.go b/openstack/db/v1/datastores/results.go
new file mode 100644
index 0000000..a86a3cc
--- /dev/null
+++ b/openstack/db/v1/datastores/results.go
@@ -0,0 +1,123 @@
+package datastores
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Version represents a version API resource. Multiple versions belong to a Datastore.
+type Version struct {
+	ID    string
+	Links []gophercloud.Link
+	Name  string
+}
+
+// Datastore represents a Datastore API resource.
+type Datastore struct {
+	DefaultVersion string `json:"default_version" mapstructure:"default_version"`
+	ID             string
+	Links          []gophercloud.Link
+	Name           string
+	Versions       []Version
+}
+
+// DatastorePartial is a meta structure which is used in various API responses.
+// It is a lightweight and truncated version of a full Datastore resource,
+// offering details of the Version, Type and VersionID only.
+type DatastorePartial struct {
+	Version   string
+	Type      string
+	VersionID string `json:"version_id" mapstructure:"version_id"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// GetVersionResult represents the result of getting a version.
+type GetVersionResult struct {
+	gophercloud.Result
+}
+
+// DatastorePage represents a page of datastore resources.
+type DatastorePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a Datastore collection is empty.
+func (r DatastorePage) IsEmpty() (bool, error) {
+	is, err := ExtractDatastores(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractDatastores retrieves a slice of datastore structs from a paginated
+// collection.
+func ExtractDatastores(page pagination.Page) ([]Datastore, error) {
+	casted := page.(DatastorePage).Body
+
+	var resp struct {
+		Datastores []Datastore `mapstructure:"datastores" json:"datastores"`
+	}
+
+	err := mapstructure.Decode(casted, &resp)
+	return resp.Datastores, err
+}
+
+// Extract retrieves a single Datastore struct from an operation result.
+func (r GetResult) Extract() (*Datastore, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Datastore Datastore `mapstructure:"datastore"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return &response.Datastore, err
+}
+
+// DatastorePage represents a page of version resources.
+type VersionPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty indicates whether a collection of version resources is empty.
+func (r VersionPage) IsEmpty() (bool, error) {
+	is, err := ExtractVersions(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractVersions retrieves a slice of versions from a paginated collection.
+func ExtractVersions(page pagination.Page) ([]Version, error) {
+	casted := page.(VersionPage).Body
+
+	var resp struct {
+		Versions []Version `mapstructure:"versions" json:"versions"`
+	}
+
+	err := mapstructure.Decode(casted, &resp)
+	return resp.Versions, err
+}
+
+// Extract retrieves a single Version struct from an operation result.
+func (r GetVersionResult) Extract() (*Version, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Version Version `mapstructure:"version"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return &response.Version, err
+}
diff --git a/openstack/db/v1/datastores/urls.go b/openstack/db/v1/datastores/urls.go
new file mode 100644
index 0000000..c4d5248
--- /dev/null
+++ b/openstack/db/v1/datastores/urls.go
@@ -0,0 +1,19 @@
+package datastores
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("datastores")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, dsID string) string {
+	return c.ServiceURL("datastores", dsID)
+}
+
+func versionsURL(c *gophercloud.ServiceClient, dsID string) string {
+	return c.ServiceURL("datastores", dsID, "versions")
+}
+
+func versionURL(c *gophercloud.ServiceClient, dsID, versionID string) string {
+	return c.ServiceURL("datastores", dsID, "versions", versionID)
+}
diff --git a/openstack/db/v1/flavors/doc.go b/openstack/db/v1/flavors/doc.go
new file mode 100644
index 0000000..4d281d5
--- /dev/null
+++ b/openstack/db/v1/flavors/doc.go
@@ -0,0 +1,7 @@
+// Package flavors provides information and interaction with the flavor API
+// resource in the OpenStack Database service.
+//
+// A flavor is an available hardware configuration for a database instance.
+// Each flavor has a unique combination of disk space, memory capacity and
+// priority for CPU time.
+package flavors
diff --git a/openstack/db/v1/flavors/fixtures.go b/openstack/db/v1/flavors/fixtures.go
new file mode 100644
index 0000000..f0016bc
--- /dev/null
+++ b/openstack/db/v1/flavors/fixtures.go
@@ -0,0 +1,50 @@
+package flavors
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+const flavor = `
+{
+	"id": %d,
+	"links": [
+		{
+			"href": "https://openstack.example.com/v1.0/1234/flavors/%d",
+			"rel": "self"
+		},
+		{
+			"href": "https://openstack.example.com/flavors/%d",
+			"rel": "bookmark"
+		}
+	],
+	"name": "%s",
+	"ram": %d
+}
+`
+
+var (
+	flavorID = "{flavorID}"
+	_baseURL = "/flavors"
+	resURL   = "/flavors/" + flavorID
+)
+
+var (
+	flavor1 = fmt.Sprintf(flavor, 1, 1, 1, "m1.tiny", 512)
+	flavor2 = fmt.Sprintf(flavor, 2, 2, 2, "m1.small", 1024)
+	flavor3 = fmt.Sprintf(flavor, 3, 3, 3, "m1.medium", 2048)
+	flavor4 = fmt.Sprintf(flavor, 4, 4, 4, "m1.large", 4096)
+
+	listFlavorsResp = fmt.Sprintf(`{"flavors":[%s, %s, %s, %s]}`, flavor1, flavor2, flavor3, flavor4)
+	getFlavorResp   = fmt.Sprintf(`{"flavor": %s}`, flavor1)
+)
+
+func HandleList(t *testing.T) {
+	fixture.SetupHandler(t, _baseURL, "GET", "", listFlavorsResp, 200)
+}
+
+func HandleGet(t *testing.T) {
+	fixture.SetupHandler(t, resURL, "GET", "", getFlavorResp, 200)
+}
diff --git a/openstack/db/v1/flavors/requests.go b/openstack/db/v1/flavors/requests.go
new file mode 100644
index 0000000..fa34446
--- /dev/null
+++ b/openstack/db/v1/flavors/requests.go
@@ -0,0 +1,29 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available hardware flavors that an instance can use. The
+// operation is identical to the one supported by the Nova API, but without the
+// "disk" property.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// Get will retrieve information for a specified hardware flavor.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var gr GetResult
+
+	_, gr.Err = client.Request("GET", getURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &gr.Body,
+		OkCodes:      []int{200},
+	})
+
+	return gr
+}
diff --git a/openstack/db/v1/flavors/requests_test.go b/openstack/db/v1/flavors/requests_test.go
new file mode 100644
index 0000000..88b5871
--- /dev/null
+++ b/openstack/db/v1/flavors/requests_test.go
@@ -0,0 +1,91 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	pages := 0
+	err := List(fake.ServiceClient()).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",
+				RAM:  512,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/1", Rel: "bookmark"},
+				},
+			},
+			Flavor{
+				ID:   "2",
+				Name: "m1.small",
+				RAM:  1024,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/2", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/2", Rel: "bookmark"},
+				},
+			},
+			Flavor{
+				ID:   "3",
+				Name: "m1.medium",
+				RAM:  2048,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/3", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/3", Rel: "bookmark"},
+				},
+			},
+			Flavor{
+				ID:   "4",
+				Name: "m1.large",
+				RAM:  4096,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/4", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/4", Rel: "bookmark"},
+				},
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGet(t)
+
+	actual, err := Get(fake.ServiceClient(), flavorID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Flavor{
+		ID:   "1",
+		Name: "m1.tiny",
+		RAM:  512,
+		Links: []gophercloud.Link{
+			gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/db/v1/flavors/results.go b/openstack/db/v1/flavors/results.go
new file mode 100644
index 0000000..2cee010
--- /dev/null
+++ b/openstack/db/v1/flavors/results.go
@@ -0,0 +1,92 @@
+package flavors
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily holds the response 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"`
+	}
+
+	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+		WeaklyTypedInput: true,
+		Result:           &result,
+	})
+
+	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 flavor's unique identifier.
+	ID string `mapstructure:"id"`
+
+	// The RAM capacity for the flavor.
+	RAM int `mapstructure:"ram"`
+
+	// The Name field provides a human-readable moniker for the flavor.
+	Name string `mapstructure:"name"`
+
+	// Links to access the flavor.
+	Links []gophercloud.Link
+}
+
+// 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)
+}
+
+// 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"`
+	}
+
+	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+		WeaklyTypedInput: true,
+		Result:           &container,
+	})
+
+	err = decoder.Decode(casted)
+
+	return container.Flavors, err
+}
diff --git a/openstack/db/v1/flavors/urls.go b/openstack/db/v1/flavors/urls.go
new file mode 100644
index 0000000..80da11f
--- /dev/null
+++ b/openstack/db/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+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")
+}
diff --git a/openstack/db/v1/instances/doc.go b/openstack/db/v1/instances/doc.go
new file mode 100644
index 0000000..dc5c90f
--- /dev/null
+++ b/openstack/db/v1/instances/doc.go
@@ -0,0 +1,7 @@
+// Package instances provides information and interaction with the instance API
+// resource in the OpenStack Database service.
+//
+// A database instance is an isolated database environment with compute and
+// storage resources in a single tenant environment on a shared physical host
+// machine.
+package instances
diff --git a/openstack/db/v1/instances/fixtures.go b/openstack/db/v1/instances/fixtures.go
new file mode 100644
index 0000000..af7b185
--- /dev/null
+++ b/openstack/db/v1/instances/fixtures.go
@@ -0,0 +1,169 @@
+package instances
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	timestamp  = "2015-11-12T14:22:42Z"
+	timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var instance = `
+{
+  "created": "` + timestamp + `",
+  "datastore": {
+    "type": "mysql",
+    "version": "5.6"
+  },
+  "flavor": {
+    "id": "1",
+    "links": [
+      {
+        "href": "https://my-openstack.com/v1.0/1234/flavors/1",
+        "rel": "self"
+      },
+      {
+        "href": "https://my-openstack.com/v1.0/1234/flavors/1",
+        "rel": "bookmark"
+      }
+    ]
+  },
+  "links": [
+    {
+      "href": "https://my-openstack.com/v1.0/1234/instances/1",
+      "rel": "self"
+    }
+  ],
+  "hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.my-openstack.com",
+  "id": "{instanceID}",
+  "name": "json_rack_instance",
+  "status": "BUILD",
+  "updated": "` + timestamp + `",
+  "volume": {
+    "size": 2
+  }
+}
+`
+
+var createReq = `
+{
+	"instance": {
+		"databases": [
+			{
+				"character_set": "utf8",
+				"collate": "utf8_general_ci",
+				"name": "sampledb"
+			},
+			{
+				"name": "nextround"
+			}
+		],
+		"flavorRef": "1",
+		"name": "json_rack_instance",
+		"users": [
+			{
+				"databases": [
+					{
+						"name": "sampledb"
+					}
+				],
+				"name": "demouser",
+				"password": "demopassword"
+			}
+		],
+		"volume": {
+			"size": 2
+		}
+	}
+}
+`
+
+var (
+	instanceID = "{instanceID}"
+	rootURL    = "/instances"
+	resURL     = rootURL + "/" + instanceID
+	uRootURL   = resURL + "/root"
+	aURL       = resURL + "/action"
+)
+
+var (
+	restartReq   = `{"restart": {}}`
+	resizeReq    = `{"resize": {"flavorRef": "2"}}`
+	resizeVolReq = `{"resize": {"volume": {"size": 4}}}`
+)
+
+var (
+	createResp        = fmt.Sprintf(`{"instance": %s}`, instance)
+	listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance)
+	getInstanceResp   = createResp
+	enableUserResp    = `{"user":{"name":"root","password":"secretsecret"}}`
+	isUserEnabledResp = `{"rootEnabled":true}`
+)
+
+var expectedInstance = Instance{
+	Created: timeVal,
+	Updated: timeVal,
+	Flavor: flavors.Flavor{
+		ID: "1",
+		Links: []gophercloud.Link{
+			gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/flavors/1", Rel: "self"},
+			gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/flavors/1", Rel: "bookmark"},
+		},
+	},
+	Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.my-openstack.com",
+	ID:       instanceID,
+	Links: []gophercloud.Link{
+		gophercloud.Link{Href: "https://my-openstack.com/v1.0/1234/instances/1", Rel: "self"},
+	},
+	Name:   "json_rack_instance",
+	Status: "BUILD",
+	Volume: Volume{Size: 2},
+	Datastore: datastores.DatastorePartial{
+		Type:    "mysql",
+		Version: "5.6",
+	},
+}
+
+func HandleCreate(t *testing.T) {
+	fixture.SetupHandler(t, rootURL, "POST", createReq, createResp, 200)
+}
+
+func HandleList(t *testing.T) {
+	fixture.SetupHandler(t, rootURL, "GET", "", listInstancesResp, 200)
+}
+
+func HandleGet(t *testing.T) {
+	fixture.SetupHandler(t, resURL, "GET", "", getInstanceResp, 200)
+}
+
+func HandleDelete(t *testing.T) {
+	fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+}
+
+func HandleEnableRoot(t *testing.T) {
+	fixture.SetupHandler(t, uRootURL, "POST", "", enableUserResp, 200)
+}
+
+func HandleIsRootEnabled(t *testing.T) {
+	fixture.SetupHandler(t, uRootURL, "GET", "", isUserEnabledResp, 200)
+}
+
+func HandleRestart(t *testing.T) {
+	fixture.SetupHandler(t, aURL, "POST", restartReq, "", 202)
+}
+
+func HandleResize(t *testing.T) {
+	fixture.SetupHandler(t, aURL, "POST", resizeReq, "", 202)
+}
+
+func HandleResizeVol(t *testing.T) {
+	fixture.SetupHandler(t, aURL, "POST", resizeVolReq, "", 202)
+}
diff --git a/openstack/db/v1/instances/requests.go b/openstack/db/v1/instances/requests.go
new file mode 100644
index 0000000..f4a63b8
--- /dev/null
+++ b/openstack/db/v1/instances/requests.go
@@ -0,0 +1,238 @@
+package instances
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder is the top-level interface for create options.
+type CreateOptsBuilder interface {
+	ToInstanceCreateMap() (map[string]interface{}, error)
+}
+
+// DatastoreOpts represents the configuration for how an instance stores data.
+type DatastoreOpts struct {
+	Version string
+	Type    string
+}
+
+func (opts DatastoreOpts) ToMap() (map[string]string, error) {
+	return map[string]string{
+		"version": opts.Version,
+		"type":    opts.Type,
+	}, nil
+}
+
+// CreateOpts is the struct responsible for configuring a new database instance.
+type CreateOpts struct {
+	// Either the integer UUID (in string form) of the flavor, or its URI
+	// reference as specified in the response from the List() call. Required.
+	FlavorRef string
+
+	// Specifies the volume size in gigabytes (GB). The value must be between 1
+	// and 300. Required.
+	Size int
+
+	// Name of the instance to create. The length of the name is limited to
+	// 255 characters and any characters are permitted. Optional.
+	Name string
+
+	// A slice of database information options.
+	Databases db.CreateOptsBuilder
+
+	// A slice of user information options.
+	Users users.CreateOptsBuilder
+
+	// Options to configure the type of datastore the instance will use. This is
+	// optional, and if excluded will default to MySQL.
+	Datastore *DatastoreOpts
+}
+
+// ToInstanceCreateMap will render a JSON map.
+func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
+	if opts.Size > 300 || opts.Size < 1 {
+		return nil, fmt.Errorf("Size (GB) must be between 1-300")
+	}
+	if opts.FlavorRef == "" {
+		return nil, fmt.Errorf("FlavorRef is a required field")
+	}
+
+	instance := map[string]interface{}{
+		"volume":    map[string]int{"size": opts.Size},
+		"flavorRef": opts.FlavorRef,
+	}
+
+	if opts.Name != "" {
+		instance["name"] = opts.Name
+	}
+	if opts.Databases != nil {
+		dbs, err := opts.Databases.ToDBCreateMap()
+		if err != nil {
+			return nil, err
+		}
+		instance["databases"] = dbs["databases"]
+	}
+	if opts.Users != nil {
+		users, err := opts.Users.ToUserCreateMap()
+		if err != nil {
+			return nil, err
+		}
+		instance["users"] = users["users"]
+	}
+
+	return map[string]interface{}{"instance": instance}, nil
+}
+
+// Create asynchronously provisions a new database instance. It requires the
+// user to specify a flavor and a volume size. The API service then provisions
+// the instance with the requested flavor and sets up a volume of the specified
+// size, which is the storage for the database instance.
+//
+// Although this call only allows the creation of 1 instance per request, you
+// can create an instance with multiple databases and users. The default
+// binding for a MySQL instance is port 3306.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToInstanceCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// List retrieves the status and information for all database instances.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return InstancePage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, baseURL(client), createPageFn)
+}
+
+// Get retrieves the status and information for a specified database instance.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+
+	_, res.Err = client.Request("GET", resourceURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// Delete permanently destroys the database instance.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+
+	_, res.Err = client.Request("DELETE", resourceURL(client, id), gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return res
+}
+
+// EnableRootUser enables the login from any host for the root user and
+// provides the user with a generated root password.
+func EnableRootUser(client *gophercloud.ServiceClient, id string) UserRootResult {
+	var res UserRootResult
+
+	_, res.Err = client.Request("POST", userRootURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// IsRootEnabled checks an instance to see if root access is enabled. It returns
+// True if root user is enabled for the specified database instance or False
+// otherwise.
+func IsRootEnabled(client *gophercloud.ServiceClient, id string) (bool, error) {
+	var res gophercloud.Result
+
+	_, err := client.Request("GET", userRootURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res.Body.(map[string]interface{})["rootEnabled"] == true, err
+}
+
+// Restart will restart only the MySQL Instance. Restarting MySQL will
+// erase any dynamic configuration settings that you have made within MySQL.
+// The MySQL service will be unavailable until the instance restarts.
+func Restart(client *gophercloud.ServiceClient, id string) ActionResult {
+	var res ActionResult
+
+	_, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
+		JSONBody: map[string]interface{}{"restart": struct{}{}},
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// Resize changes the memory size of the instance, assuming a valid
+// flavorRef is provided. It will also restart the MySQL service.
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) ActionResult {
+	var res ActionResult
+
+	type resize struct {
+		FlavorRef string `json:"flavorRef"`
+	}
+
+	type req struct {
+		Resize resize `json:"resize"`
+	}
+
+	reqBody := req{Resize: resize{FlavorRef: flavorRef}}
+
+	_, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
+		JSONBody: reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// ResizeVolume will resize the attached volume for an instance. It supports
+// only increasing the volume size and does not support decreasing the size.
+// The volume size is in gigabytes (GB) and must be an integer.
+func ResizeVolume(client *gophercloud.ServiceClient, id string, size int) ActionResult {
+	var res ActionResult
+
+	type volume struct {
+		Size int `json:"size"`
+	}
+
+	type resize struct {
+		Volume volume `json:"volume"`
+	}
+
+	type req struct {
+		Resize resize `json:"resize"`
+	}
+
+	reqBody := req{Resize: resize{Volume: volume{Size: size}}}
+
+	_, res.Err = client.Request("POST", actionURL(client, id), gophercloud.RequestOpts{
+		JSONBody: reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
diff --git a/openstack/db/v1/instances/requests_test.go b/openstack/db/v1/instances/requests_test.go
new file mode 100644
index 0000000..3cc2b70
--- /dev/null
+++ b/openstack/db/v1/instances/requests_test.go
@@ -0,0 +1,133 @@
+package instances
+
+import (
+	"testing"
+
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreate(t)
+
+	opts := CreateOpts{
+		Name:      "json_rack_instance",
+		FlavorRef: "1",
+		Databases: db.BatchCreateOpts{
+			db.CreateOpts{CharSet: "utf8", Collate: "utf8_general_ci", Name: "sampledb"},
+			db.CreateOpts{Name: "nextround"},
+		},
+		Users: users.BatchCreateOpts{
+			users.CreateOpts{
+				Name:     "demouser",
+				Password: "demopassword",
+				Databases: db.BatchCreateOpts{
+					db.CreateOpts{Name: "sampledb"},
+				},
+			},
+		},
+		Size: 2,
+	}
+
+	instance, err := Create(fake.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &expectedInstance, instance)
+}
+
+func TestInstanceList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	pages := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractInstances(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, []Instance{expectedInstance}, actual)
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetInstance(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGet(t)
+
+	instance, err := Get(fake.ServiceClient(), instanceID).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &expectedInstance, instance)
+}
+
+func TestDeleteInstance(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDelete(t)
+
+	res := Delete(fake.ServiceClient(), instanceID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestEnableRootUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleEnableRoot(t)
+
+	expected := &users.User{Name: "root", Password: "secretsecret"}
+	user, err := EnableRootUser(fake.ServiceClient(), instanceID).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestIsRootEnabled(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleIsRootEnabled(t)
+
+	isEnabled, err := IsRootEnabled(fake.ServiceClient(), instanceID)
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, isEnabled)
+}
+
+func TestRestart(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRestart(t)
+
+	res := Restart(fake.ServiceClient(), instanceID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleResize(t)
+
+	res := Resize(fake.ServiceClient(), instanceID, "2")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResizeVolume(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleResizeVol(t)
+
+	res := ResizeVolume(fake.ServiceClient(), instanceID, 4)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/instances/results.go b/openstack/db/v1/instances/results.go
new file mode 100644
index 0000000..95aed16
--- /dev/null
+++ b/openstack/db/v1/instances/results.go
@@ -0,0 +1,213 @@
+package instances
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	"github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Volume represents information about an attached volume for a database instance.
+type Volume struct {
+	// The size in GB of the volume
+	Size int
+
+	Used float64
+}
+
+// Instance represents a remote MySQL instance.
+type Instance struct {
+	// Indicates the datetime that the instance was created
+	Created time.Time `mapstructure:"-"`
+
+	// Indicates the most recent datetime that the instance was updated.
+	Updated time.Time `mapstructure:"-"`
+
+	// Indicates the hardware flavor the instance uses.
+	Flavor flavors.Flavor
+
+	// A DNS-resolvable hostname associated with the database instance (rather
+	// than an IPv4 address). Since the hostname always resolves to the correct
+	// IP address of the database instance, this relieves the user from the task
+	// of maintaining the mapping. Note that although the IP address may likely
+	// change on resizing, migrating, and so forth, the hostname always resolves
+	// to the correct database instance.
+	Hostname string
+
+	// Indicates the unique identifier for the instance resource.
+	ID string
+
+	// Exposes various links that reference the instance resource.
+	Links []gophercloud.Link
+
+	// The human-readable name of the instance.
+	Name string
+
+	// The build status of the instance.
+	Status string
+
+	// Information about the attached volume of the instance.
+	Volume Volume
+
+	// Indicates how the instance stores data.
+	Datastore datastores.DatastorePartial
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// 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 struct {
+	gophercloud.ErrResult
+}
+
+// Extract will extract an Instance from various result structs.
+func (r commonResult) Extract() (*Instance, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Instance Instance `mapstructure:"instance"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	val := r.Body.(map[string]interface{})["instance"].(map[string]interface{})
+
+	if t, ok := val["created"].(string); ok && t != "" {
+		creationTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Instance, err
+		}
+		response.Instance.Created = creationTime
+	}
+
+	if t, ok := val["updated"].(string); ok && t != "" {
+		updatedTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Instance, err
+		}
+		response.Instance.Updated = updatedTime
+	}
+
+	return &response.Instance, err
+}
+
+// InstancePage represents a single page of a paginated instance collection.
+type InstancePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page InstancePage) IsEmpty() (bool, error) {
+	instances, err := ExtractInstances(page)
+	if err != nil {
+		return true, err
+	}
+	return len(instances) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page InstancePage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"instances_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractInstances will convert a generic pagination struct into a more
+// relevant slice of Instance structs.
+func ExtractInstances(page pagination.Page) ([]Instance, error) {
+	casted := page.(InstancePage).Body
+
+	var resp struct {
+		Instances []Instance `mapstructure:"instances"`
+	}
+
+	if err := mapstructure.Decode(casted, &resp); err != nil {
+		return nil, err
+	}
+
+	var vals []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		vals = casted.(map[string]interface{})["instances"].([]interface{})
+	case map[string][]interface{}:
+		vals = casted.(map[string][]interface{})["instances"]
+	default:
+		return resp.Instances, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i, v := range vals {
+		val := v.(map[string]interface{})
+
+		if t, ok := val["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Instances, err
+			}
+			resp.Instances[i].Created = creationTime
+		}
+
+		if t, ok := val["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Instances, err
+			}
+			resp.Instances[i].Updated = updatedTime
+		}
+	}
+
+	return resp.Instances, nil
+}
+
+// UserRootResult represents the result of an operation to enable the root user.
+type UserRootResult struct {
+	gophercloud.Result
+}
+
+// Extract will extract root user information from a UserRootResult.
+func (r UserRootResult) Extract() (*users.User, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		User users.User `mapstructure:"user"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.User, err
+}
+
+// ActionResult represents the result of action requests, such as: restarting
+// an instance service, resizing its memory allocation, and resizing its
+// attached volume size.
+type ActionResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/db/v1/instances/urls.go b/openstack/db/v1/instances/urls.go
new file mode 100644
index 0000000..28c0bec
--- /dev/null
+++ b/openstack/db/v1/instances/urls.go
@@ -0,0 +1,19 @@
+package instances
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("instances")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("instances", id)
+}
+
+func userRootURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("instances", id, "root")
+}
+
+func actionURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("instances", id, "action")
+}
diff --git a/openstack/db/v1/users/doc.go b/openstack/db/v1/users/doc.go
new file mode 100644
index 0000000..cf07832
--- /dev/null
+++ b/openstack/db/v1/users/doc.go
@@ -0,0 +1,3 @@
+// Package users provides information and interaction with the user API
+// resource in the OpenStack Database service.
+package users
diff --git a/openstack/db/v1/users/fixtures.go b/openstack/db/v1/users/fixtures.go
new file mode 100644
index 0000000..516b335
--- /dev/null
+++ b/openstack/db/v1/users/fixtures.go
@@ -0,0 +1,37 @@
+package users
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+const user1 = `
+{"databases": [{"name": "databaseA"}],"name": "dbuser3"%s}
+`
+
+const user2 = `
+{"databases": [{"name": "databaseB"},{"name": "databaseC"}],"name": "dbuser4"%s}
+`
+
+var (
+	instanceID = "{instanceID}"
+	_rootURL   = "/instances/" + instanceID + "/users"
+	pUser1     = fmt.Sprintf(user1, `,"password":"secretsecret"`)
+	pUser2     = fmt.Sprintf(user2, `,"password":"secretsecret"`)
+	createReq  = fmt.Sprintf(`{"users":[%s, %s]}`, pUser1, pUser2)
+	listResp   = fmt.Sprintf(`{"users":[%s, %s]}`, fmt.Sprintf(user1, ""), fmt.Sprintf(user2, ""))
+)
+
+func HandleCreate(t *testing.T) {
+	fixture.SetupHandler(t, _rootURL, "POST", createReq, "", 202)
+}
+
+func HandleList(t *testing.T) {
+	fixture.SetupHandler(t, _rootURL, "GET", "", listResp, 200)
+}
+
+func HandleDelete(t *testing.T) {
+	fixture.SetupHandler(t, _rootURL+"/{userName}", "DELETE", "", "", 202)
+}
diff --git a/openstack/db/v1/users/requests.go b/openstack/db/v1/users/requests.go
new file mode 100644
index 0000000..7533fc4
--- /dev/null
+++ b/openstack/db/v1/users/requests.go
@@ -0,0 +1,132 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder is the top-level interface for creating JSON maps.
+type CreateOptsBuilder interface {
+	ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the struct responsible for configuring a new user; often in the
+// context of an instance.
+type CreateOpts struct {
+	// [REQUIRED] Specifies a name for the user. Valid names can be composed
+	// of the following characters: letters (either case); numbers; these
+	// characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
+	// permitted anywhere. Prohibited characters that are forbidden include:
+	// single quotes, double quotes, back quotes, semicolons, commas, backslashes,
+	// and forward slashes. Spaces at the front or end of a user name are also
+	// not permitted.
+	Name string
+
+	// [REQUIRED] Specifies a password for the user.
+	Password string
+
+	// [OPTIONAL] An array of databases that this user will connect to. The
+	// "name" field is the only requirement for each option.
+	Databases db.BatchCreateOpts
+
+	// [OPTIONAL] Specifies the host from which a user is allowed to connect to
+	// the database. Possible values are a string containing an IPv4 address or
+	// "%" to allow connecting from any host. Optional; the default is "%".
+	Host string
+}
+
+// ToMap is a convenience function for creating sub-maps for individual users.
+func (opts CreateOpts) ToMap() (map[string]interface{}, error) {
+
+	if opts.Name == "root" {
+		return nil, errors.New("root is a reserved user name and cannot be used")
+	}
+	if opts.Name == "" {
+		return nil, errors.New("Name is a required field")
+	}
+	if opts.Password == "" {
+		return nil, errors.New("Password is a required field")
+	}
+
+	user := map[string]interface{}{
+		"name":     opts.Name,
+		"password": opts.Password,
+	}
+
+	if opts.Host != "" {
+		user["host"] = opts.Host
+	}
+
+	dbs := make([]map[string]string, len(opts.Databases))
+	for i, db := range opts.Databases {
+		dbs[i] = map[string]string{"name": db.Name}
+	}
+
+	if len(dbs) > 0 {
+		user["databases"] = dbs
+	}
+
+	return user, nil
+}
+
+// BatchCreateOpts allows multiple users to be created at once.
+type BatchCreateOpts []CreateOpts
+
+// ToUserCreateMap will generate a JSON map.
+func (opts BatchCreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	users := make([]map[string]interface{}, len(opts))
+	for i, opt := range opts {
+		user, err := opt.ToMap()
+		if err != nil {
+			return nil, err
+		}
+		users[i] = user
+	}
+	return map[string]interface{}{"users": users}, nil
+}
+
+// Create asynchronously provisions a new user for the specified database
+// instance based on the configuration defined in CreateOpts. If databases are
+// assigned for a particular user, the user will be granted all privileges
+// for those specified databases. "root" is a reserved name and cannot be used.
+func Create(client *gophercloud.ServiceClient, instanceID string, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToUserCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("POST", baseURL(client, instanceID), gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// List will list all the users associated with a specified database instance,
+// along with their associated databases. This operation will not return any
+// system users or administrators for a database.
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+// Delete will permanently delete a user from a specified database instance.
+func Delete(client *gophercloud.ServiceClient, instanceID, userName string) DeleteResult {
+	var res DeleteResult
+
+	_, res.Err = client.Request("DELETE", userURL(client, instanceID, userName), gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return res
+}
diff --git a/openstack/db/v1/users/requests_test.go b/openstack/db/v1/users/requests_test.go
new file mode 100644
index 0000000..5711f63
--- /dev/null
+++ b/openstack/db/v1/users/requests_test.go
@@ -0,0 +1,84 @@
+package users
+
+import (
+	"testing"
+
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreate(t)
+
+	opts := BatchCreateOpts{
+		CreateOpts{
+			Databases: db.BatchCreateOpts{
+				db.CreateOpts{Name: "databaseA"},
+			},
+			Name:     "dbuser3",
+			Password: "secretsecret",
+		},
+		CreateOpts{
+			Databases: db.BatchCreateOpts{
+				db.CreateOpts{Name: "databaseB"},
+				db.CreateOpts{Name: "databaseC"},
+			},
+			Name:     "dbuser4",
+			Password: "secretsecret",
+		},
+	}
+
+	res := Create(fake.ServiceClient(), instanceID, opts)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUserList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	expectedUsers := []User{
+		User{
+			Databases: []db.Database{
+				db.Database{Name: "databaseA"},
+			},
+			Name: "dbuser3",
+		},
+		User{
+			Databases: []db.Database{
+				db.Database{Name: "databaseB"},
+				db.Database{Name: "databaseC"},
+			},
+			Name: "dbuser4",
+		},
+	}
+
+	pages := 0
+	err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractUsers(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, expectedUsers, actual)
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDelete(t)
+
+	res := Delete(fake.ServiceClient(), instanceID, "{userName}")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/users/results.go b/openstack/db/v1/users/results.go
new file mode 100644
index 0000000..217ddd8
--- /dev/null
+++ b/openstack/db/v1/users/results.go
@@ -0,0 +1,73 @@
+package users
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a database user
+type User struct {
+	// The user name
+	Name string
+
+	// The user password
+	Password string
+
+	// The databases associated with this user
+	Databases []db.Database
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// UserPage represents a single page of a paginated user collection.
+type UserPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page UserPage) IsEmpty() (bool, error) {
+	users, err := ExtractUsers(page)
+	if err != nil {
+		return true, err
+	}
+	return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page UserPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"users_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractUsers will convert a generic pagination struct into a more
+// relevant slice of User structs.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+	casted := page.(UserPage).Body
+
+	var response struct {
+		Users []User `mapstructure:"users"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+
+	return response.Users, err
+}
diff --git a/openstack/db/v1/users/urls.go b/openstack/db/v1/users/urls.go
new file mode 100644
index 0000000..2a3cacd
--- /dev/null
+++ b/openstack/db/v1/users/urls.go
@@ -0,0 +1,11 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+	return c.ServiceURL("instances", instanceID, "users")
+}
+
+func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+	return c.ServiceURL("instances", instanceID, "users", userName)
+}
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
new file mode 100644
index 0000000..29d02c4
--- /dev/null
+++ b/openstack/endpoint_location.go
@@ -0,0 +1,91 @@
+package openstack
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
+)
+
+// 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 from a Catalog acquired
+// during the v3 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 V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
+	// Extract Endpoints from the catalog entries that match the requested Type, Interface,
+	// Name if provided, and Region if provided.
+	var endpoints = make([]tokens3.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.Availability != gophercloud.AvailabilityAdmin &&
+					opts.Availability != gophercloud.AvailabilityPublic &&
+					opts.Availability != gophercloud.AvailabilityInternal {
+					return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
+				}
+				if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
+					(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 URL from the matching Endpoint.
+	for _, endpoint := range endpoints {
+		return gophercloud.NormalizeURL(endpoint.URL), nil
+	}
+
+	// Report an error if there were no matching endpoints.
+	return "", gophercloud.ErrEndpointNotFound
+}
diff --git a/openstack/endpoint_location_test.go b/openstack/endpoint_location_test.go
new file mode 100644
index 0000000..8e65918
--- /dev/null
+++ b/openstack/endpoint_location_test.go
@@ -0,0 +1,228 @@
+package openstack
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// 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, "Unexpected availability in endpoint query: wat", err.Error())
+}
+
+var catalog3 = tokens3.ServiceCatalog{
+	Entries: []tokens3.CatalogEntry{
+		tokens3.CatalogEntry{
+			Type: "same",
+			Name: "same",
+			Endpoints: []tokens3.Endpoint{
+				tokens3.Endpoint{
+					ID:        "1",
+					Region:    "same",
+					Interface: "public",
+					URL:       "https://public.correct.com/",
+				},
+				tokens3.Endpoint{
+					ID:        "2",
+					Region:    "same",
+					Interface: "admin",
+					URL:       "https://admin.correct.com/",
+				},
+				tokens3.Endpoint{
+					ID:        "3",
+					Region:    "same",
+					Interface: "internal",
+					URL:       "https://internal.correct.com/",
+				},
+				tokens3.Endpoint{
+					ID:        "4",
+					Region:    "different",
+					Interface: "public",
+					URL:       "https://badregion.com/",
+				},
+			},
+		},
+		tokens3.CatalogEntry{
+			Type: "same",
+			Name: "different",
+			Endpoints: []tokens3.Endpoint{
+				tokens3.Endpoint{
+					ID:        "5",
+					Region:    "same",
+					Interface: "public",
+					URL:       "https://badname.com/",
+				},
+				tokens3.Endpoint{
+					ID:        "6",
+					Region:    "different",
+					Interface: "public",
+					URL:       "https://badname.com/+badregion",
+				},
+			},
+		},
+		tokens3.CatalogEntry{
+			Type: "different",
+			Name: "different",
+			Endpoints: []tokens3.Endpoint{
+				tokens3.Endpoint{
+					ID:        "7",
+					Region:    "same",
+					Interface: "public",
+					URL:       "https://badtype.com/+badname",
+				},
+				tokens3.Endpoint{
+					ID:        "8",
+					Region:    "different",
+					Interface: "public",
+					URL:       "https://badtype.com/+badregion+badname",
+				},
+			},
+		},
+	},
+}
+
+func TestV3EndpointExact(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 := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+			Type:         "same",
+			Name:         "same",
+			Region:       "same",
+			Availability: availability,
+		})
+		th.AssertNoErr(t, err)
+		th.CheckEquals(t, expected, actual)
+	}
+}
+
+func TestV3EndpointNone(t *testing.T) {
+	_, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+		Type:         "nope",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	th.CheckEquals(t, gophercloud.ErrEndpointNotFound, err)
+}
+
+func TestV3EndpointMultiple(t *testing.T) {
+	_, err := V3EndpointURL(&catalog3, 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 TestV3EndpointBadAvailability(t *testing.T) {
+	_, err := V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+		Type:         "same",
+		Name:         "same",
+		Region:       "same",
+		Availability: "wat",
+	})
+	th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error())
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/docs.go b/openstack/identity/v2/extensions/admin/roles/docs.go
new file mode 100644
index 0000000..8954178
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/docs.go
@@ -0,0 +1,16 @@
+// Package roles provides functionality to interact with and control roles on
+// the API.
+//
+// A role represents a personality that a user can assume when performing a
+// specific set of operations. If a role includes a set of rights and
+// privileges, a user assuming that role inherits those rights and privileges.
+//
+// When a token is generated, the list of roles that user can assume is returned
+// back to them. Services that are being called by that user determine how they
+// interpret the set of roles a user has and to which operations or resources
+// each role grants access.
+//
+// It is up to individual services such as Compute or Image to assign meaning
+// to these roles. As far as the Identity service is concerned, a role is an
+// arbitrary name assigned by the user.
+package roles
diff --git a/openstack/identity/v2/extensions/admin/roles/fixtures.go b/openstack/identity/v2/extensions/admin/roles/fixtures.go
new file mode 100644
index 0000000..8256f0f
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/fixtures.go
@@ -0,0 +1,48 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/OS-KSADM/roles", 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, `
+{
+    "roles": [
+        {
+            "id": "123",
+            "name": "compute:admin",
+            "description": "Nova Administrator"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", 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/identity/v2/extensions/admin/roles/requests.go b/openstack/identity/v2/extensions/admin/roles/requests.go
new file mode 100644
index 0000000..9a33314
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests.go
@@ -0,0 +1,33 @@
+package roles
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+	_, result.Err = client.Put(userRoleURL(client, tenantID, userID, roleID), nil, nil, nil)
+	return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+	_, result.Err = client.Delete(userRoleURL(client, tenantID, userID, roleID), nil)
+	return result
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/requests_test.go b/openstack/identity/v2/extensions/admin/roles/requests_test.go
new file mode 100644
index 0000000..7bfeea4
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests_test.go
@@ -0,0 +1,64 @@
+package roles
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []Role{
+			Role{
+				ID:          "123",
+				Name:        "compute:admin",
+				Description: "Nova Administrator",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := AddUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := DeleteUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/results.go b/openstack/identity/v2/extensions/admin/roles/results.go
new file mode 100644
index 0000000..ebb3aa5
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/results.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Role represents an API role resource.
+type Role struct {
+	// The unique ID for the role.
+	ID string
+
+	// The human-readable name of the role.
+	Name string
+
+	// The description of the role.
+	Description string
+
+	// The associated service for this role.
+	ServiceID string
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+	casted := page.(RolePage).Body
+	var response struct {
+		Roles []Role `mapstructure:"roles"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Roles, err
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/urls.go b/openstack/identity/v2/extensions/admin/roles/urls.go
new file mode 100644
index 0000000..61b3155
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/urls.go
@@ -0,0 +1,21 @@
+package roles
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	ExtPath  = "OS-KSADM"
+	RolePath = "roles"
+	UserPath = "users"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(ExtPath, RolePath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(ExtPath, RolePath)
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string {
+	return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID)
+}
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..3dfdc08
--- /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 to 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..6245259
--- /dev/null
+++ b/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,195 @@
+// +build fixtures
+
+package tokens
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	th "github.com/rackspace/gophercloud/testhelper"
+	thclient "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// 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",
+				},
+			},
+		},
+	},
+}
+
+// ExpectedUser is the token that should be parsed from TokenGetResponse.
+var ExpectedUser = &User{
+	ID:       "a530fefc3d594c4ba2693a4ecd6be74e",
+	Name:     "apiserver",
+	Roles:    []Role{{"member"}, {"service"}},
+	UserName: "apiserver",
+}
+
+// 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"
+			}
+		]
+	}
+}
+`
+
+// TokenGetResponse is a JSON response that contains ExpectedToken and ExpectedUser.
+const TokenGetResponse = `
+{
+    "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": [], 
+		"user": {
+            "id": "a530fefc3d594c4ba2693a4ecd6be74e", 
+            "name": "apiserver", 
+            "roles": [
+                {
+                    "name": "member"
+                }, 
+                {
+                    "name": "service"
+                }
+            ], 
+            "roles_links": [], 
+            "username": "apiserver"
+        }
+    }
+}`
+
+// 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)
+	})
+}
+
+// HandleTokenGet expects a Get against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenGet(t *testing.T, token string) {
+	th.Mux.HandleFunc("/tokens/"+token, 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", thclient.TokenID)
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, TokenGetResponse)
+	})
+}
+
+// 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)
+}
+
+// GetIsSuccessful ensures that a GetResult was successful and contains the correct token and
+// User Info.
+func GetIsSuccessful(t *testing.T, result GetResult) {
+	token, err := result.ExtractToken()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedToken, token)
+
+	user, err := result.ExtractUser()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedUser, user)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..1f51438
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,99 @@
+package tokens
+
+import (
+	"fmt"
+
+	"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
+	}
+
+	// Populate the request map.
+	authMap := make(map[string]interface{})
+
+	if auth.Username != "" {
+		if auth.Password != "" {
+			authMap["passwordCredentials"] = map[string]interface{}{
+				"username": auth.Username,
+				"password": auth.Password,
+			}
+		} else {
+			return nil, ErrPasswordRequired
+		}
+	} else if auth.TokenID != "" {
+		authMap["token"] = map[string]interface{}{
+			"id": auth.TokenID,
+		}
+	} else {
+		return nil, fmt.Errorf("You must provide either username/password or tenantID/token values.")
+	}
+
+	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 = client.Post(CreateURL(client), request, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 203},
+	})
+	return result
+}
+
+// Validates and retrieves information for user's token.
+func Get(client *gophercloud.ServiceClient, token string) GetResult {
+	var result GetResult
+	_, result.Err = client.Get(GetURL(client, token), &result.Body, &gophercloud.RequestOpts{
+		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..f1ec339
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,152 @@
+package tokens
+
+import (
+	"fmt"
+	"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.CheckDeepEquals(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, fmt.Errorf("You must provide either username/password or tenantID/token values."))
+}
+
+func TestRequirePassword(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Username: "me",
+	}
+
+	tokenPostErr(t, options, ErrPasswordRequired)
+}
+
+func tokenGet(t *testing.T, tokenId string) GetResult {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleTokenGet(t, tokenId)
+	return Get(client.ServiceClient(), tokenId)
+}
+
+func TestGetWithToken(t *testing.T) {
+	GetIsSuccessful(t, tokenGet(t, "db22caf43c934e6c829087c41ff8d8d6"))
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..67c577b
--- /dev/null
+++ b/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,170 @@
+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
+}
+
+// Authorization need user info which can get from token authentication's response
+type Role struct {
+	Name string `mapstructure:"name"`
+}
+type User struct {
+	ID       string `mapstructure:"id"`
+	Name     string `mapstructure:"name"`
+	UserName string `mapstructure:"username"`
+	Roles    []Role `mapstructure:"roles"`
+}
+
+// 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
+}
+
+// GetResult is the deferred response from a Get call, which is the same with a Created token.
+// Use ExtractUser() to interpret it as a User.
+type GetResult struct {
+	CreateResult
+}
+
+// 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}}
+}
+
+// ExtractUser returns the User from a GetResult.
+func (result GetResult) ExtractUser() (*User, error) {
+	if result.Err != nil {
+		return nil, result.Err
+	}
+
+	var response struct {
+		Access struct {
+			User User `mapstructure:"user"`
+		} `mapstructure:"access"`
+	}
+
+	err := mapstructure.Decode(result.Body, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return &response.Access.User, nil
+}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..ee13932
--- /dev/null
+++ b/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,13 @@
+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")
+}
+
+// GetURL generates the URL used to Validate Tokens.
+func GetURL(client *gophercloud.ServiceClient, token string) string {
+	return client.ServiceURL("tokens", token)
+}
diff --git a/openstack/identity/v2/users/doc.go b/openstack/identity/v2/users/doc.go
new file mode 100644
index 0000000..82abcb9
--- /dev/null
+++ b/openstack/identity/v2/users/doc.go
@@ -0,0 +1 @@
+package users
diff --git a/openstack/identity/v2/users/fixtures.go b/openstack/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..8941868
--- /dev/null
+++ b/openstack/identity/v2/users/fixtures.go
@@ -0,0 +1,163 @@
+package users
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+    "users":[
+        {
+            "id": "u1000",
+						"name": "John Smith",
+            "username": "jqsmith",
+            "email": "john.smith@example.org",
+            "enabled": true,
+						"tenant_id": "12345"
+        },
+        {
+            "id": "u1001",
+						"name": "Jane Smith",
+            "username": "jqsmith",
+            "email": "jane.smith@example.org",
+            "enabled": true,
+						"tenant_id": "12345"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func mockCreateUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+    "user": {
+		    "name": "new_user",
+		    "tenant_id": "12345",
+				"enabled": false,
+				"email": "new_user@foo.com"
+    }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "user": {
+        "name": "new_user",
+        "tenant_id": "12345",
+        "enabled": false,
+        "email": "new_user@foo.com",
+        "id": "c39e3de9be2d4c779f1dfd6abacc176d"
+    }
+}
+`)
+	})
+}
+
+func mockGetUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/new_user", 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, `
+{
+		"user": {
+				"name": "new_user",
+				"tenant_id": "12345",
+				"enabled": false,
+				"email": "new_user@foo.com",
+				"id": "c39e3de9be2d4c779f1dfd6abacc176d"
+		}
+}
+`)
+	})
+}
+
+func mockUpdateUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+    "user": {
+		    "name": "new_name",
+		    "enabled": true,
+		    "email": "new_email@foo.com"
+    }
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"user": {
+				"name": "new_name",
+				"tenant_id": "12345",
+				"enabled": true,
+				"email": "new_email@foo.com",
+				"id": "c39e3de9be2d4c779f1dfd6abacc176d"
+		}
+}
+`)
+	})
+}
+
+func mockDeleteUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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 mockListRolesResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", 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, `
+{
+    "roles": [
+        {
+            "id": "9fe2ff9ee4384b1894a90878d3e92bab",
+            "name": "foo_role"
+        },
+        {
+            "id": "1ea3d56793574b668e85960fbf651e13",
+            "name": "admin"
+        }
+    ]
+}
+	`)
+	})
+}
diff --git a/openstack/identity/v2/users/requests.go b/openstack/identity/v2/users/requests.go
new file mode 100644
index 0000000..88be45e
--- /dev/null
+++ b/openstack/identity/v2/users/requests.go
@@ -0,0 +1,161 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// EnabledState represents whether the user is enabled or not.
+type EnabledState *bool
+
+// Useful variables to use when creating or updating users.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Enabled  EnabledState = &iTrue
+	Disabled EnabledState = &iFalse
+)
+
+// CommonOpts are the parameters that are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+	// Either a name or username is required. When provided, the value must be
+	// unique or a 409 conflict error will be returned. If you provide a name but
+	// omit a username, the latter will be set to the former; and vice versa.
+	Name, Username string
+
+	// The ID of the tenant to which you want to assign this user.
+	TenantID string
+
+	// Indicates whether this user is enabled or not.
+	Enabled EnabledState
+
+	// The email address of this user.
+	Email string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+type CreateOptsBuilder interface {
+	ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+
+	if opts.Name == "" && opts.Username == "" {
+		return m, errors.New("Either a Name or Username must be provided")
+	}
+
+	if opts.Name != "" {
+		m["name"] = opts.Name
+	}
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.TenantID != "" {
+		m["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToUserCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Post(rootURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+
+	return res
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = client.Get(ResourceURL(client, id), &result.Body, nil)
+	return result
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if opts.Name != "" {
+		m["name"] = opts.Name
+	}
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.TenantID != "" {
+		m["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+	reqBody := opts.ToUserUpdateMap()
+	_, result.Err = client.Put(ResourceURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var result DeleteResult
+	_, result.Err = client.Delete(ResourceURL(client, id), nil)
+	return result
+}
+
+func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage)
+}
diff --git a/openstack/identity/v2/users/requests_test.go b/openstack/identity/v2/users/requests_test.go
new file mode 100644
index 0000000..04f8371
--- /dev/null
+++ b/openstack/identity/v2/users/requests_test.go
@@ -0,0 +1,165 @@
+package users
+
+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()
+
+	MockListUserResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractUsers(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []User{
+			User{
+				ID:       "u1000",
+				Name:     "John Smith",
+				Username: "jqsmith",
+				Email:    "john.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+			User{
+				ID:       "u1001",
+				Name:     "Jane Smith",
+				Username: "jqsmith",
+				Email:    "jane.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateUserResponse(t)
+
+	opts := CreateOpts{
+		Name:     "new_user",
+		TenantID: "12345",
+		Enabled:  Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_user",
+		ID:       "c39e3de9be2d4c779f1dfd6abacc176d",
+		Email:    "new_user@foo.com",
+		Enabled:  false,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetUserResponse(t)
+
+	user, err := Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_user",
+		ID:       "c39e3de9be2d4c779f1dfd6abacc176d",
+		Email:    "new_user@foo.com",
+		Enabled:  false,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateUserResponse(t)
+
+	id := "c39e3de9be2d4c779f1dfd6abacc176d"
+	opts := UpdateOpts{
+		Name:    "new_name",
+		Enabled: Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_name",
+		ID:       id,
+		Email:    "new_email@foo.com",
+		Enabled:  true,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestDeleteUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteUserResponse(t)
+
+	res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestListingUserRoles(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListRolesResponse(t)
+
+	tenantID := "1d8b6120dcc640fda4fc9194ffc80273"
+	userID := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+	err := ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Role{
+			Role{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"},
+			Role{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/users/results.go b/openstack/identity/v2/users/results.go
new file mode 100644
index 0000000..f531d5d
--- /dev/null
+++ b/openstack/identity/v2/users/results.go
@@ -0,0 +1,128 @@
+package users
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+	// The UUID for this user.
+	ID string
+
+	// The human name for this user.
+	Name string
+
+	// The username for this user.
+	Username string
+
+	// Indicates whether the user is enabled (true) or disabled (false).
+	Enabled bool
+
+	// The email address for this user.
+	Email string
+
+	// The ID of the tenant to which this user belongs.
+	TenantID string `mapstructure:"tenant_id"`
+}
+
+// Role assigns specific responsibilities to users, allowing them to accomplish
+// certain API operations whilst scoped to a service.
+type Role struct {
+	// UUID of the role
+	ID string
+
+	// Name of the role
+	Name string
+}
+
+// UserPage is a single page of a User collection.
+type UserPage struct {
+	pagination.SinglePageBase
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page UserPage) IsEmpty() (bool, error) {
+	users, err := ExtractUsers(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractUsers returns a slice of Tenants contained in a single page of results.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+	casted := page.(UserPage).Body
+	var response struct {
+		Users []User `mapstructure:"users"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Users, err
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of Roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+	casted := page.(RolePage).Body
+	var response struct {
+		Roles []Role `mapstructure:"roles"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Roles, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as a User, if possible.
+func (r commonResult) Extract() (*User, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		User User `mapstructure:"user"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.User, 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 struct {
+	commonResult
+}
diff --git a/openstack/identity/v2/users/urls.go b/openstack/identity/v2/users/urls.go
new file mode 100644
index 0000000..7ec4385
--- /dev/null
+++ b/openstack/identity/v2/users/urls.go
@@ -0,0 +1,21 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	tenantPath = "tenants"
+	userPath   = "users"
+	rolePath   = "roles"
+)
+
+func ResourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(userPath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(userPath)
+}
+
+func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string {
+	return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath)
+}
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..99a495d
--- /dev/null
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -0,0 +1,123 @@
+package endpoints
+
+import (
+	"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 = client.Post(listURL(client), reqBody, &result.Body, nil)
+	return result
+}
+
+// ListOpts allows finer control over the endpoints returned by a List call.
+// All fields are optional.
+type ListOpts struct {
+	Availability gophercloud.Availability `q:"interface"`
+	ServiceID    string                   `q:"service_id"`
+	Page         int                      `q:"page"`
+	PerPage      int                      `q:"per_page"`
+}
+
+// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	u := listURL(client)
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u += q.String()
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return EndpointPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	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 = client.Request("PATCH", endpointURL(client, endpointID), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &result.Body,
+		OkCodes:      []int{200},
+	})
+	return result
+}
+
+// Delete removes an endpoint from the service catalog.
+func Delete(client *gophercloud.ServiceClient, endpointID string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(endpointURL(client, endpointID), nil)
+	return res
+}
diff --git a/openstack/identity/v3/endpoints/requests_test.go b/openstack/identity/v3/endpoints/requests_test.go
new file mode 100644
index 0000000..80687c4
--- /dev/null
+++ b/openstack/identity/v3/endpoints/requests_test.go
@@ -0,0 +1,226 @@
+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)
+	})
+
+	res := Delete(client.ServiceClient(), "34")
+	testhelper.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
new file mode 100644
index 0000000..1281122
--- /dev/null
+++ b/openstack/identity/v3/endpoints/results.go
@@ -0,0 +1,82 @@
+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
+}
+
+// DeleteResult is the deferred result of an Delete call.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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/roles/doc.go b/openstack/identity/v3/roles/doc.go
new file mode 100644
index 0000000..bdbc674
--- /dev/null
+++ b/openstack/identity/v3/roles/doc.go
@@ -0,0 +1,3 @@
+// Package roles provides information and interaction with the roles API
+// resource for the OpenStack Identity service.
+package roles
diff --git a/openstack/identity/v3/roles/requests.go b/openstack/identity/v3/roles/requests.go
new file mode 100644
index 0000000..d95c1e5
--- /dev/null
+++ b/openstack/identity/v3/roles/requests.go
@@ -0,0 +1,50 @@
+package roles
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListAssignmentsOptsBuilder allows extensions to add additional parameters to
+// the ListAssignments request.
+type ListAssignmentsOptsBuilder interface {
+	ToRolesListAssignmentsQuery() (string, error)
+}
+
+// ListAssignmentsOpts allows you to query the ListAssignments method.
+// Specify one of or a combination of GroupId, RoleId, ScopeDomainId, ScopeProjectId,
+// and/or UserId to search for roles assigned to corresponding entities.
+// Effective lists effective assignments at the user, project, and domain level,
+// allowing for the effects of group membership.
+type ListAssignmentsOpts struct {
+	GroupId        string `q:"group.id"`
+	RoleId         string `q:"role.id"`
+	ScopeDomainId  string `q:"scope.domain.id"`
+	ScopeProjectId string `q:"scope.project.id"`
+	UserId         string `q:"user.id"`
+	Effective      bool   `q:"effective"`
+}
+
+// ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string.
+func (opts ListAssignmentsOpts) ToRolesListAssignmentsQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListAssignments enumerates the roles assigned to a specified resource.
+func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager {
+	url := listAssignmentsURL(client)
+	query, err := opts.ToRolesListAssignmentsQuery()
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	url += query
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RoleAssignmentsPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, url, createPage)
+}
diff --git a/openstack/identity/v3/roles/requests_test.go b/openstack/identity/v3/roles/requests_test.go
new file mode 100644
index 0000000..d62dbff
--- /dev/null
+++ b/openstack/identity/v3/roles/requests_test.go
@@ -0,0 +1,104 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListSinglePage(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/role_assignments", 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, `
+			{
+                "role_assignments": [
+                    {
+                        "links": {
+                            "assignment": "http://identity:35357/v3/domains/161718/users/313233/roles/123456"
+                        },
+                        "role": {
+                            "id": "123456"
+                        },
+                        "scope": {
+                            "domain": {
+                                "id": "161718"
+                            }
+                        },
+                        "user": {
+                            "id": "313233"
+                        }
+                    },
+                    {
+                        "links": {
+                            "assignment": "http://identity:35357/v3/projects/456789/groups/101112/roles/123456",
+                            "membership": "http://identity:35357/v3/groups/101112/users/313233"
+                        },
+                        "role": {
+                            "id": "123456"
+                        },
+                        "scope": {
+                            "project": {
+                                "id": "456789"
+                            }
+                        },
+                        "user": {
+                            "id": "313233"
+                        }
+                    }
+                ],
+                "links": {
+                    "self": "http://identity:35357/v3/role_assignments?effective",
+                    "previous": null,
+                    "next": null
+                }
+            }
+		`)
+	})
+
+	count := 0
+	err := ListAssignments(client.ServiceClient(), ListAssignmentsOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRoleAssignments(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []RoleAssignment{
+			RoleAssignment{
+				Role:  Role{ID: "123456"},
+				Scope: Scope{Domain: Domain{ID: "161718"}},
+				User:  User{ID: "313233"},
+				Group: Group{},
+			},
+			RoleAssignment{
+				Role:  Role{ID: "123456"},
+				Scope: Scope{Project: Project{ID: "456789"}},
+				User:  User{ID: "313233"},
+				Group: Group{},
+			},
+		}
+
+		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)
+	}
+}
diff --git a/openstack/identity/v3/roles/results.go b/openstack/identity/v3/roles/results.go
new file mode 100644
index 0000000..d25abd2
--- /dev/null
+++ b/openstack/identity/v3/roles/results.go
@@ -0,0 +1,81 @@
+package roles
+
+import (
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// RoleAssignment is the result of a role assignments query.
+type RoleAssignment struct {
+	Role  Role  `json:"role,omitempty"`
+	Scope Scope `json:"scope,omitempty"`
+	User  User  `json:"user,omitempty"`
+	Group Group `json:"group,omitempty"`
+}
+
+type Role struct {
+	ID string `json:"id,omitempty"`
+}
+
+type Scope struct {
+	Domain  Domain  `json:"domain,omitempty"`
+	Project Project `json:"domain,omitempty"`
+}
+
+type Domain struct {
+	ID string `json:"id,omitempty"`
+}
+
+type Project struct {
+	ID string `json:"id,omitempty"`
+}
+
+type User struct {
+	ID string `json:"id,omitempty"`
+}
+
+type Group struct {
+	ID string `json:"id,omitempty"`
+}
+
+// RoleAssignmentsPage is a single page of RoleAssignments results.
+type RoleAssignmentsPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if the page contains no results.
+func (p RoleAssignmentsPage) IsEmpty() (bool, error) {
+	roleAssignments, err := ExtractRoleAssignments(p)
+	if err != nil {
+		return true, err
+	}
+	return len(roleAssignments) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page RoleAssignmentsPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links struct {
+			Next string `mapstructure:"next"`
+		} `mapstructure:"links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return r.Links.Next, nil
+}
+
+// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection acquired from List.
+func ExtractRoleAssignments(page pagination.Page) ([]RoleAssignment, error) {
+	var response struct {
+		RoleAssignments []RoleAssignment `mapstructure:"role_assignments"`
+	}
+
+	err := mapstructure.Decode(page.(RoleAssignmentsPage).Body, &response)
+	return response.RoleAssignments, err
+}
diff --git a/openstack/identity/v3/roles/urls.go b/openstack/identity/v3/roles/urls.go
new file mode 100644
index 0000000..b009340
--- /dev/null
+++ b/openstack/identity/v3/roles/urls.go
@@ -0,0 +1,7 @@
+package roles
+
+import "github.com/rackspace/gophercloud"
+
+func listAssignmentsURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("role_assignments")
+}
diff --git a/openstack/identity/v3/roles/urls_test.go b/openstack/identity/v3/roles/urls_test.go
new file mode 100644
index 0000000..04679da
--- /dev/null
+++ b/openstack/identity/v3/roles/urls_test.go
@@ -0,0 +1,15 @@
+package roles
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func TestListAssignmentsURL(t *testing.T) {
+	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+	url := listAssignmentsURL(&client)
+	if url != "http://localhost:5000/v3/role_assignments" {
+		t.Errorf("Unexpected list 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..3ee924f
--- /dev/null
+++ b/openstack/identity/v3/services/requests.go
@@ -0,0 +1,77 @@
+package services
+
+import (
+	"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 = client.Post(listURL(client), req, &result.Body, nil)
+	return result
+}
+
+// ListOpts allows you to query the List method.
+type ListOpts struct {
+	ServiceType string `q:"type"`
+	PerPage     int    `q:"perPage"`
+	Page        int    `q:"page"`
+}
+
+// List enumerates the services available to a specific user.
+func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	u := listURL(client)
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u += q.String()
+	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 = client.Get(serviceURL(client, serviceID), &result.Body, nil)
+	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 = client.Request("PATCH", serviceURL(client, serviceID), gophercloud.RequestOpts{
+		JSONBody:     &req,
+		JSONResponse: &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) DeleteResult {
+	var res DeleteResult
+	_, res.Err = client.Delete(serviceURL(client, serviceID), nil)
+	return res
+}
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
new file mode 100644
index 0000000..42f05d3
--- /dev/null
+++ b/openstack/identity/v3/services/requests_test.go
@@ -0,0 +1,209 @@
+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)
+	})
+
+	res := Delete(client.ServiceClient(), "12345")
+	testhelper.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
new file mode 100644
index 0000000..1d0d141
--- /dev/null
+++ b/openstack/identity/v3/services/results.go
@@ -0,0 +1,80 @@
+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
+}
+
+// DeleteResult is the deferred result of an Delete call.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// 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..d449ca3
--- /dev/null
+++ b/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,281 @@
+package tokens
+
+import (
+	"net/http"
+
+	"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 *http.Response
+	response, result.Err = c.Post(tokenURL(c), req, &result.Body, nil)
+	if result.Err != nil {
+		return result
+	}
+	result.Header = response.Header
+	return result
+}
+
+// Get validates and retrieves information about another token.
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+	var result GetResult
+	var response *http.Response
+	response, result.Err = c.Get(tokenURL(c), &result.Body, &gophercloud.RequestOpts{
+		MoreHeaders: subjectTokenHeaders(c, token),
+		OkCodes:     []int{200, 203},
+	})
+	if result.Err != nil {
+		return result
+	}
+	result.Header = response.Header
+	return result
+}
+
+// Validate determines if a specified token is valid or not.
+func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
+	response, err := c.Request("HEAD", tokenURL(c), gophercloud.RequestOpts{
+		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) RevokeResult {
+	var res RevokeResult
+	_, res.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{
+		MoreHeaders: subjectTokenHeaders(c, token),
+	})
+	return res
+}
diff --git a/openstack/identity/v3/tokens/requests_test.go b/openstack/identity/v3/tokens/requests_test.go
new file mode 100644
index 0000000..2b26e4a
--- /dev/null
+++ b/openstack/identity/v3/tokens/requests_test.go
@@ -0,0 +1,514 @@
+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)
+
+	res := Revoke(&client, "abcdef12345")
+	testhelper.AssertNoErr(t, res.Err)
+}
+
+func TestRevokeRequestError(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	client := prepareAuthTokenHandler(t, "DELETE", http.StatusNotFound)
+
+	res := Revoke(&client, "abcdef12345")
+	if res.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..d134f7d
--- /dev/null
+++ b/openstack/identity/v3/tokens/results.go
@@ -0,0 +1,139 @@
+package tokens
+
+import (
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+)
+
+// Endpoint represents a single API endpoint offered by a service.
+// It matches either a public, internal or admin URL.
+// If supported, it contains a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+type Endpoint struct {
+	ID        string `mapstructure:"id"`
+	Region    string `mapstructure:"region"`
+	Interface string `mapstructure:"interface"`
+	URL       string `mapstructure:"url"`
+}
+
+// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing.
+// Each class of service, such as cloud DNS or block storage services, could have multiple
+// CatalogEntry representing it (one by interface type, e.g public, admin or internal).
+//
+// 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 {
+
+	// Service ID
+	ID string `mapstructure:"id"`
+
+	// 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
+}
+
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a shortcut for ExtractToken.
+// This function is deprecated and still present for backward compatibility.
+func (r commonResult) Extract() (*Token, error) {
+	return r.ExtractToken()
+}
+
+// ExtractToken interprets a commonResult as a Token.
+func (r commonResult) ExtractToken() (*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
+}
+
+// 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 {
+		Token struct {
+			Entries []CatalogEntry `mapstructure:"catalog"`
+		} `mapstructure:"token"`
+	}
+
+	err := mapstructure.Decode(result.Body, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ServiceCatalog{Entries: response.Token.Entries}, nil
+}
+
+// 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 {
+	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
+}
+
+// RevokeResult is the deferred response from a Revoke call.
+type RevokeResult 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..097ae37
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/requests.go
@@ -0,0 +1,69 @@
+package external
+
+import (
+	"time"
+
+	"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) {
+
+	// DO NOT REMOVE. Though this line seemingly does nothing of value, it is a
+	// splint to prevent the unit test from failing on Go Tip. We suspect it is a
+	// compiler issue that will hopefully be worked out prior to our next release.
+	// Again, for all the unit tests to pass, this line is necessary and sufficient
+	// at the moment. We should reassess after the Go 1.5 release to determine
+	// if this line is still needed.
+	time.Sleep(0 * time.Millisecond)
+
+	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..54dbf4b
--- /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 NetworkExternal 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/fwaas/doc.go b/openstack/networking/v2/extensions/fwaas/doc.go
new file mode 100644
index 0000000..3ec450a
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/doc.go
@@ -0,0 +1,3 @@
+// Package fwaas provides information and interaction with the Firewall
+// as a Service extension for the OpenStack Networking service.
+package fwaas
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/errors.go b/openstack/networking/v2/extensions/fwaas/firewalls/errors.go
new file mode 100644
index 0000000..dd92bb2
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/errors.go
@@ -0,0 +1,11 @@
+package firewalls
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var (
+	errPolicyRequired = err("A policy ID is required")
+)
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/requests.go b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
new file mode 100644
index 0000000..12d587f
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
@@ -0,0 +1,216 @@
+package firewalls
+
+import (
+	"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
+
+// Shared gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Yes` and `No` enums.
+type Shared *bool
+
+// Convenience vars for AdminStateUp and Shared values.
+var (
+	iTrue             = true
+	iFalse            = false
+	Up     AdminState = &iTrue
+	Down   AdminState = &iFalse
+	Yes    Shared     = &iTrue
+	No     Shared     = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToFirewallListQuery() (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 firewall attributes you want to see returned. SortKey allows you to sort
+// by a particular firewall attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	TenantID     string `q:"tenant_id"`
+	Name         string `q:"name"`
+	Description  string `q:"description"`
+	AdminStateUp bool   `q:"admin_state_up"`
+	Shared       bool   `q:"shared"`
+	PolicyID     string `q:"firewall_policy_id"`
+	ID           string `q:"id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// ToFirewallListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFirewallListQuery() (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
+// firewalls. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those firewalls that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+
+	if opts != nil {
+		query, err := opts.ToFirewallListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return FirewallPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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 {
+	ToFirewallCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new firewall.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a firewall
+	// for another tenant.
+	TenantID     string
+	Name         string
+	Description  string
+	AdminStateUp *bool
+	Shared       *bool
+	PolicyID     string
+}
+
+// ToFirewallCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToFirewallCreateMap() (map[string]interface{}, error) {
+	if opts.PolicyID == "" {
+		return nil, errPolicyRequired
+	}
+
+	f := make(map[string]interface{})
+
+	if opts.TenantID != "" {
+		f["tenant_id"] = opts.TenantID
+	}
+	if opts.Name != "" {
+		f["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		f["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		f["shared"] = *opts.Shared
+	}
+	if opts.AdminStateUp != nil {
+		f["admin_state_up"] = *opts.AdminStateUp
+	}
+	if opts.PolicyID != "" {
+		f["firewall_policy_id"] = opts.PolicyID
+	}
+
+	return map[string]interface{}{"firewall": f}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToFirewallCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular firewall based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 {
+	ToFirewallUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall.
+type UpdateOpts struct {
+	// Name of the firewall.
+	Name         string
+	Description  string
+	AdminStateUp *bool
+	Shared       *bool
+	PolicyID     string
+}
+
+// ToFirewallUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToFirewallUpdateMap() (map[string]interface{}, error) {
+	f := make(map[string]interface{})
+
+	if opts.Name != "" {
+		f["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		f["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		f["shared"] = *opts.Shared
+	}
+	if opts.AdminStateUp != nil {
+		f["admin_state_up"] = *opts.AdminStateUp
+	}
+	if opts.PolicyID != "" {
+		f["firewall_policy_id"] = opts.PolicyID
+	}
+
+	return map[string]interface{}{"firewall": f}, nil
+}
+
+// Update allows firewalls to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToFirewallUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular firewall based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, id), nil)
+	return res
+}
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go b/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go
new file mode 100644
index 0000000..19d32c5
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go
@@ -0,0 +1,246 @@
+package firewalls
+
+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/fw/firewalls", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls", 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, `
+{
+   "firewalls":[
+        {
+           "status": "ACTIVE",
+           "name": "fw1",
+           "admin_state_up": false,
+           "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+           "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a",
+           "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61",
+           "description": "OpenStack firewall 1"
+        },
+        {
+           "status": "PENDING_UPDATE",
+           "name": "fw2",
+           "admin_state_up": true,
+           "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+           "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e299",
+           "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f99",
+           "description": "OpenStack firewall 2"
+        }
+   ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractFirewalls(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Firewall{
+			Firewall{
+				Status:       "ACTIVE",
+				Name:         "fw1",
+				AdminStateUp: false,
+				TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+				PolicyID:     "34be8c83-4d42-4dca-a74e-b77fffb8e28a",
+				ID:           "fb5b5315-64f6-4ea3-8e58-981cc37c6f61",
+				Description:  "OpenStack firewall 1",
+			},
+			Firewall{
+				Status:       "PENDING_UPDATE",
+				Name:         "fw2",
+				AdminStateUp: true,
+				TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+				PolicyID:     "34be8c83-4d42-4dca-a74e-b77fffb8e299",
+				ID:           "fb5b5315-64f6-4ea3-8e58-981cc37c6f99",
+				Description:  "OpenStack firewall 2",
+			},
+		}
+
+		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/fw/firewalls", 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, `
+{
+    "firewall":{
+        "name": "fw",
+        "description": "OpenStack firewall",
+        "admin_state_up": true,
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "firewall":{
+        "status": "PENDING_CREATE",
+        "name": "fw",
+        "description": "OpenStack firewall",
+        "admin_state_up": true,
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c"
+    }
+}
+    `)
+	})
+
+	options := CreateOpts{
+		TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+		Name:         "fw",
+		Description:  "OpenStack firewall",
+		AdminStateUp: Up,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	_, 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/fw/firewalls/fb5b5315-64f6-4ea3-8e58-981cc37c6f61", 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, `
+{
+    "firewall": {
+        "status": "ACTIVE",
+        "name": "fw",
+        "admin_state_up": true,
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a",
+        "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61",
+        "description": "OpenStack firewall"
+    }
+}
+        `)
+	})
+
+	fw, err := Get(fake.ServiceClient(), "fb5b5315-64f6-4ea3-8e58-981cc37c6f61").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "ACTIVE", fw.Status)
+	th.AssertEquals(t, "fw", fw.Name)
+	th.AssertEquals(t, "OpenStack firewall", fw.Description)
+	th.AssertEquals(t, true, fw.AdminStateUp)
+	th.AssertEquals(t, "34be8c83-4d42-4dca-a74e-b77fffb8e28a", fw.PolicyID)
+	th.AssertEquals(t, "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", fw.ID)
+	th.AssertEquals(t, "b4eedccc6fb74fa8a7ad6b08382b852b", fw.TenantID)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls/ea5b5315-64f6-4ea3-8e58-981cc37c6576", 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, `
+{
+    "firewall":{
+        "name": "fw",
+        "description": "updated fw",
+        "admin_state_up":false,
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall": {
+        "status": "ACTIVE",
+        "name": "fw",
+        "admin_state_up": false,
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+        "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576",
+        "description": "OpenStack firewall"
+    }
+}
+    `)
+	})
+
+	options := UpdateOpts{
+		Name:         "fw",
+		Description:  "updated fw",
+		AdminStateUp: Down,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+
+	_, err := Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls/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/fwaas/firewalls/results.go b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
new file mode 100644
index 0000000..a8c76ee
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
@@ -0,0 +1,101 @@
+package firewalls
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type Firewall struct {
+	ID           string `json:"id" mapstructure:"id"`
+	Name         string `json:"name" mapstructure:"name"`
+	Description  string `json:"description" mapstructure:"description"`
+	AdminStateUp bool   `json:"admin_state_up" mapstructure:"admin_state_up"`
+	Status       string `json:"status" mapstructure:"status"`
+	PolicyID     string `json:"firewall_policy_id" mapstructure:"firewall_policy_id"`
+	TenantID     string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall.
+func (r commonResult) Extract() (*Firewall, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Firewall *Firewall `json:"firewall"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Firewall, err
+}
+
+// FirewallPage is the page returned by a pager when traversing over a
+// collection of firewalls.
+type FirewallPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of firewalls 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 FirewallPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"firewalls_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a FirewallPage struct is empty.
+func (p FirewallPage) IsEmpty() (bool, error) {
+	is, err := ExtractFirewalls(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractFirewalls 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 ExtractFirewalls(page pagination.Page) ([]Firewall, error) {
+	var resp struct {
+		Firewalls []Firewall `mapstructure:"firewalls" json:"firewalls"`
+	}
+
+	err := mapstructure.Decode(page.(FirewallPage).Body, &resp)
+
+	return resp.Firewalls, err
+}
+
+// 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 struct {
+	gophercloud.ErrResult
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/urls.go b/openstack/networking/v2/extensions/fwaas/firewalls/urls.go
new file mode 100644
index 0000000..4dde530
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/urls.go
@@ -0,0 +1,16 @@
+package firewalls
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "fw"
+	resourcePath = "firewalls"
+)
+
+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/fwaas/policies/requests.go b/openstack/networking/v2/extensions/fwaas/policies/requests.go
new file mode 100644
index 0000000..fe07d9a
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/requests.go
@@ -0,0 +1,243 @@
+package policies
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Binary gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Yes` and `No` enums
+type Binary *bool
+
+// Convenience vars for Audited and Shared values.
+var (
+	iTrue         = true
+	iFalse        = false
+	Yes    Binary = &iTrue
+	No     Binary = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToPolicyListQuery() (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 firewall policy attributes you want to see returned. SortKey allows you
+// to sort by a particular firewall policy attribute. SortDir sets the direction,
+// and is either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	TenantID    string `q:"tenant_id"`
+	Name        string `q:"name"`
+	Description string `q:"description"`
+	Shared      bool   `q:"shared"`
+	Audited     bool   `q:"audited"`
+	ID          string `q:"id"`
+	Limit       int    `q:"limit"`
+	Marker      string `q:"marker"`
+	SortKey     string `q:"sort_key"`
+	SortDir     string `q:"sort_dir"`
+}
+
+// ToPolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPolicyListQuery() (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
+// firewall policies. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those firewall policies that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+
+	if opts != nil {
+		query, err := opts.ToPolicyListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return PolicyPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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 {
+	ToPolicyCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new firewall policy.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a firewall policy
+	// for another tenant.
+	TenantID    string
+	Name        string
+	Description string
+	Shared      *bool
+	Audited     *bool
+	Rules       []string
+}
+
+// ToPolicyCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.TenantID != "" {
+		p["tenant_id"] = opts.TenantID
+	}
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		p["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		p["shared"] = *opts.Shared
+	}
+	if opts.Audited != nil {
+		p["audited"] = *opts.Audited
+	}
+	if opts.Rules != nil {
+		p["firewall_rules"] = opts.Rules
+	}
+
+	return map[string]interface{}{"firewall_policy": p}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall policy
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToPolicyCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular firewall policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 {
+	ToPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall policy.
+type UpdateOpts struct {
+	// Name of the firewall policy.
+	Name        string
+	Description string
+	Shared      *bool
+	Audited     *bool
+	Rules       []string
+}
+
+// ToPolicyUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		p["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		p["shared"] = *opts.Shared
+	}
+	if opts.Audited != nil {
+		p["audited"] = *opts.Audited
+	}
+	if opts.Rules != nil {
+		p["firewall_rules"] = opts.Rules
+	}
+
+	return map[string]interface{}{"firewall_policy": p}, nil
+}
+
+// Update allows firewall policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToPolicyUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular firewall policy based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, id), nil)
+	return res
+}
+
+func InsertRule(c *gophercloud.ServiceClient, policyID, ruleID, beforeID, afterID string) error {
+	type request struct {
+		RuleId string `json:"firewall_rule_id"`
+		Before string `json:"insert_before,omitempty"`
+		After  string `json:"insert_after,omitempty"`
+	}
+
+	reqBody := request{
+		RuleId: ruleID,
+		Before: beforeID,
+		After:  afterID,
+	}
+
+	// Send request to API
+	var res commonResult
+	_, res.Err = c.Put(insertURL(c, policyID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res.Err
+}
+
+func RemoveRule(c *gophercloud.ServiceClient, policyID, ruleID string) error {
+	type request struct {
+		RuleId string `json:"firewall_rule_id"`
+	}
+
+	reqBody := request{
+		RuleId: ruleID,
+	}
+
+	// Send request to API
+	var res commonResult
+	_, res.Err = c.Put(removeURL(c, policyID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res.Err
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/requests_test.go b/openstack/networking/v2/extensions/fwaas/policies/requests_test.go
new file mode 100644
index 0000000..b9d7865
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/requests_test.go
@@ -0,0 +1,279 @@
+package policies
+
+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/fw/firewall_policies", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies", 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, `
+{
+    "firewall_policies": [
+        {
+            "name": "policy1",
+            "firewall_rules": [
+                "75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+                "c9e77ca0-1bc8-497d-904d-948107873dc6"
+            ],
+            "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+            "audited": true,
+			"shared": false,
+            "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+            "description": "Firewall policy 1"
+        },
+        {
+            "name": "policy2",
+            "firewall_rules": [
+                "03d2a6ad-633f-431a-8463-4370d06a22c8"
+            ],
+            "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+            "audited": false,
+			"shared": true,
+            "id": "c854fab5-bdaf-4a86-9359-78de93e5df01",
+            "description": "Firewall policy 2"
+        }
+    ]
+}
+        `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPolicies(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Policy{
+			Policy{
+				Name: "policy1",
+				Rules: []string{
+					"75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+					"c9e77ca0-1bc8-497d-904d-948107873dc6",
+				},
+				TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+				Audited:     true,
+				Shared:      false,
+				ID:          "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+				Description: "Firewall policy 1",
+			},
+			Policy{
+				Name: "policy2",
+				Rules: []string{
+					"03d2a6ad-633f-431a-8463-4370d06a22c8",
+				},
+				TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+				Audited:     false,
+				Shared:      true,
+				ID:          "c854fab5-bdaf-4a86-9359-78de93e5df01",
+				Description: "Firewall policy 2",
+			},
+		}
+
+		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/fw/firewall_policies", 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, `
+{
+    "firewall_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "98a58c87-76be-ae7c-a74e-b77fffb88d95",
+            "11a58c87-76be-ae7c-a74e-b77fffb88a32"
+        ],
+        "description": "Firewall policy",
+		"tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+		"audited": true,
+		"shared": false
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "98a58c87-76be-ae7c-a74e-b77fffb88d95",
+            "11a58c87-76be-ae7c-a74e-b77fffb88a32"
+        ],
+        "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+        "audited": false,
+        "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+        "description": "Firewall policy"
+    }
+}
+        `)
+	})
+
+	options := CreateOpts{
+		TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+		Name:        "policy",
+		Description: "Firewall policy",
+		Shared:      No,
+		Audited:     Yes,
+		Rules: []string{
+			"98a58c87-76be-ae7c-a74e-b77fffb88d95",
+			"11a58c87-76be-ae7c-a74e-b77fffb88a32",
+		},
+	}
+
+	_, 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/fw/firewall_policies/bcab5315-64f6-4ea3-8e58-981cc37c6f61", 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, `
+{
+    "firewall_policy":{
+        "name": "www",
+        "firewall_rules": [
+            "75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+            "c9e77ca0-1bc8-497d-904d-948107873dc6",
+            "03d2a6ad-633f-431a-8463-4370d06a22c8"
+        ],
+        "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+        "audited": false,
+        "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+        "description": "Firewall policy web"
+    }
+}
+        `)
+	})
+
+	policy, err := Get(fake.ServiceClient(), "bcab5315-64f6-4ea3-8e58-981cc37c6f61").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "www", policy.Name)
+	th.AssertEquals(t, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", policy.ID)
+	th.AssertEquals(t, "Firewall policy web", policy.Description)
+	th.AssertEquals(t, 3, len(policy.Rules))
+	th.AssertEquals(t, "75452b36-268e-4e75-aaf4-f0e7ed50bc97", policy.Rules[0])
+	th.AssertEquals(t, "c9e77ca0-1bc8-497d-904d-948107873dc6", policy.Rules[1])
+	th.AssertEquals(t, "03d2a6ad-633f-431a-8463-4370d06a22c8", policy.Rules[2])
+	th.AssertEquals(t, "9145d91459d248b1b02fdaca97c6a75d", policy.TenantID)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies/f2b08c1e-aa81-4668-8ae1-1401bcb0576c", 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, `
+{
+    "firewall_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "98a58c87-76be-ae7c-a74e-b77fffb88d95",
+            "11a58c87-76be-ae7c-a74e-b77fffb88a32"
+        ],
+        "description": "Firewall policy"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+            "c9e77ca0-1bc8-497d-904d-948107873dc6",
+            "03d2a6ad-633f-431a-8463-4370d06a22c8"
+        ],
+        "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+        "audited": false,
+        "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+        "description": "Firewall policy"
+    }
+}
+    `)
+	})
+
+	options := UpdateOpts{
+		Name:        "policy",
+		Description: "Firewall policy",
+		Rules: []string{
+			"98a58c87-76be-ae7c-a74e-b77fffb88d95",
+			"11a58c87-76be-ae7c-a74e-b77fffb88a32",
+		},
+	}
+
+	_, err := Update(fake.ServiceClient(), "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies/4ec89077-d057-4a2b-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(), "4ec89077-d057-4a2b-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/results.go b/openstack/networking/v2/extensions/fwaas/policies/results.go
new file mode 100644
index 0000000..a9a0c35
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/results.go
@@ -0,0 +1,101 @@
+package policies
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type Policy struct {
+	ID          string   `json:"id" mapstructure:"id"`
+	Name        string   `json:"name" mapstructure:"name"`
+	Description string   `json:"description" mapstructure:"description"`
+	TenantID    string   `json:"tenant_id" mapstructure:"tenant_id"`
+	Audited     bool     `json:"audited" mapstructure:"audited"`
+	Shared      bool     `json:"shared" mapstructure:"shared"`
+	Rules       []string `json:"firewall_rules,omitempty" mapstructure:"firewall_rules"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall policy.
+func (r commonResult) Extract() (*Policy, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Policy *Policy `json:"firewall_policy" mapstructure:"firewall_policy"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Policy, err
+}
+
+// PolicyPage is the page returned by a pager when traversing over a
+// collection of firewall policies.
+type PolicyPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of firewall policies 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 PolicyPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"firewall_policies_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PolicyPage struct is empty.
+func (p PolicyPage) IsEmpty() (bool, error) {
+	is, err := ExtractPolicies(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractPolicies 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 ExtractPolicies(page pagination.Page) ([]Policy, error) {
+	var resp struct {
+		Policies []Policy `mapstructure:"firewall_policies" json:"firewall_policies"`
+	}
+
+	err := mapstructure.Decode(page.(PolicyPage).Body, &resp)
+
+	return resp.Policies, err
+}
+
+// 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 struct {
+	gophercloud.ErrResult
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/urls.go b/openstack/networking/v2/extensions/fwaas/policies/urls.go
new file mode 100644
index 0000000..27ea9ae
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/urls.go
@@ -0,0 +1,26 @@
+package policies
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "fw"
+	resourcePath = "firewall_policies"
+	insertPath   = "insert_rule"
+	removePath   = "remove_rule"
+)
+
+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 insertURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id, insertPath)
+}
+
+func removeURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id, removePath)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/errors.go b/openstack/networking/v2/extensions/fwaas/rules/errors.go
new file mode 100644
index 0000000..0b29d39
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/errors.go
@@ -0,0 +1,12 @@
+package rules
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var (
+	errProtocolRequired = err("A protocol is required (tcp, udp, icmp or any)")
+	errActionRequired   = err("An action is required (allow or deny)")
+)
diff --git a/openstack/networking/v2/extensions/fwaas/rules/requests.go b/openstack/networking/v2/extensions/fwaas/rules/requests.go
new file mode 100644
index 0000000..57a0e8b
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests.go
@@ -0,0 +1,285 @@
+package rules
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Binary gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Yes` and `No` enums
+type Binary *bool
+
+// Convenience vars for Enabled and Shared values.
+var (
+	iTrue         = true
+	iFalse        = false
+	Yes    Binary = &iTrue
+	No     Binary = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToRuleListQuery() (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 Firewall rule attributes you want to see returned. SortKey allows you to
+// sort by a particular firewall rule attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	TenantID             string `q:"tenant_id"`
+	Name                 string `q:"name"`
+	Description          string `q:"description"`
+	Protocol             string `q:"protocol"`
+	Action               string `q:"action"`
+	IPVersion            int    `q:"ip_version"`
+	SourceIPAddress      string `q:"source_ip_address"`
+	DestinationIPAddress string `q:"destination_ip_address"`
+	SourcePort           string `q:"source_port"`
+	DestinationPort      string `q:"destination_port"`
+	Enabled              bool   `q:"enabled"`
+	ID                   string `q:"id"`
+	Limit                int    `q:"limit"`
+	Marker               string `q:"marker"`
+	SortKey              string `q:"sort_key"`
+	SortDir              string `q:"sort_dir"`
+}
+
+// ToRuleListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToRuleListQuery() (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
+// firewall rules. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those firewall rules that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+
+	if opts != nil {
+		query, err := opts.ToRuleListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return RulePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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 {
+	ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new firewall rule.
+type CreateOpts struct {
+	// Mandatory for create
+	Protocol string
+	Action   string
+	// Optional
+	TenantID             string
+	Name                 string
+	Description          string
+	IPVersion            int
+	SourceIPAddress      string
+	DestinationIPAddress string
+	SourcePort           string
+	DestinationPort      string
+	Shared               *bool
+	Enabled              *bool
+}
+
+// ToRuleCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	if opts.Protocol == "" {
+		return nil, errProtocolRequired
+	}
+
+	if opts.Action == "" {
+		return nil, errActionRequired
+	}
+
+	r := make(map[string]interface{})
+
+	r["protocol"] = opts.Protocol
+	r["action"] = opts.Action
+
+	if opts.TenantID != "" {
+		r["tenant_id"] = opts.TenantID
+	}
+	if opts.Name != "" {
+		r["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		r["description"] = opts.Description
+	}
+	if opts.IPVersion != 0 {
+		r["ip_version"] = opts.IPVersion
+	}
+	if opts.SourceIPAddress != "" {
+		r["source_ip_address"] = opts.SourceIPAddress
+	}
+	if opts.DestinationIPAddress != "" {
+		r["destination_ip_address"] = opts.DestinationIPAddress
+	}
+	if opts.SourcePort != "" {
+		r["source_port"] = opts.SourcePort
+	}
+	if opts.DestinationPort != "" {
+		r["destination_port"] = opts.DestinationPort
+	}
+	if opts.Shared != nil {
+		r["shared"] = *opts.Shared
+	}
+	if opts.Enabled != nil {
+		r["enabled"] = *opts.Enabled
+	}
+
+	return map[string]interface{}{"firewall_rule": r}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall rule
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToRuleCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular firewall rule based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 {
+	ToRuleUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall rule.
+// Optional
+type UpdateOpts struct {
+	Protocol             string
+	Action               string
+	Name                 string
+	Description          string
+	IPVersion            int
+	SourceIPAddress      *string
+	DestinationIPAddress *string
+	SourcePort           *string
+	DestinationPort      *string
+	Shared               *bool
+	Enabled              *bool
+}
+
+// ToRuleUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToRuleUpdateMap() (map[string]interface{}, error) {
+	r := make(map[string]interface{})
+
+	if opts.Protocol != "" {
+		r["protocol"] = opts.Protocol
+	}
+	if opts.Action != "" {
+		r["action"] = opts.Action
+	}
+	if opts.Name != "" {
+		r["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		r["description"] = opts.Description
+	}
+	if opts.IPVersion != 0 {
+		r["ip_version"] = opts.IPVersion
+	}
+	if opts.SourceIPAddress != nil {
+		s := *opts.SourceIPAddress
+		if s == "" {
+			r["source_ip_address"] = nil
+		} else {
+			r["source_ip_address"] = s
+		}
+	}
+	if opts.DestinationIPAddress != nil {
+		s := *opts.DestinationIPAddress
+		if s == "" {
+			r["destination_ip_address"] = nil
+		} else {
+			r["destination_ip_address"] = s
+		}
+	}
+	if opts.SourcePort != nil {
+		s := *opts.SourcePort
+		if s == "" {
+			r["source_port"] = nil
+		} else {
+			r["source_port"] = s
+		}
+	}
+	if opts.DestinationPort != nil {
+		s := *opts.DestinationPort
+		if s == "" {
+			r["destination_port"] = nil
+		} else {
+			r["destination_port"] = s
+		}
+	}
+	if opts.Shared != nil {
+		r["shared"] = *opts.Shared
+	}
+	if opts.Enabled != nil {
+		r["enabled"] = *opts.Enabled
+	}
+
+	return map[string]interface{}{"firewall_rule": r}, nil
+}
+
+// Update allows firewall policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToRuleUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a particular firewall rule based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, id), nil)
+	return res
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/requests_test.go b/openstack/networking/v2/extensions/fwaas/rules/requests_test.go
new file mode 100644
index 0000000..36f89fa
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests_test.go
@@ -0,0 +1,328 @@
+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/fw/firewall_rules", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_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, `
+{
+    "firewall_rules": [
+        {
+            "protocol": "tcp",
+            "description": "ssh rule",
+            "source_port": null,
+            "source_ip_address": null,
+            "destination_ip_address": "192.168.1.0/24",
+            "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+            "position": 2,
+            "destination_port": "22",
+            "id": "f03bd950-6c56-4f5e-a307-45967078f507",
+            "name": "ssh_form_any",
+            "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+            "enabled": true,
+            "action": "allow",
+            "ip_version": 4,
+            "shared": false
+        },
+        {
+            "protocol": "udp",
+            "description": "udp rule",
+            "source_port": null,
+            "source_ip_address": null,
+            "destination_ip_address": null,
+            "firewall_policy_id": "98d7fb51-698c-4123-87e8-f1eee6b5ab7e",
+            "position": 1,
+            "destination_port": null,
+            "id": "ab7bd950-6c56-4f5e-a307-45967078f890",
+            "name": "deny_all_udp",
+            "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+            "enabled": true,
+            "action": "deny",
+            "ip_version": 4,
+            "shared": false
+        }
+    ]
+}
+        `)
+	})
+
+	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 members: %v", err)
+			return false, err
+		}
+
+		expected := []Rule{
+			Rule{
+				Protocol:             "tcp",
+				Description:          "ssh rule",
+				SourcePort:           "",
+				SourceIPAddress:      "",
+				DestinationIPAddress: "192.168.1.0/24",
+				PolicyID:             "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+				Position:             2,
+				DestinationPort:      "22",
+				ID:                   "f03bd950-6c56-4f5e-a307-45967078f507",
+				Name:                 "ssh_form_any",
+				TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+				Enabled:              true,
+				Action:               "allow",
+				IPVersion:            4,
+				Shared:               false,
+			},
+			Rule{
+				Protocol:             "udp",
+				Description:          "udp rule",
+				SourcePort:           "",
+				SourceIPAddress:      "",
+				DestinationIPAddress: "",
+				PolicyID:             "98d7fb51-698c-4123-87e8-f1eee6b5ab7e",
+				Position:             1,
+				DestinationPort:      "",
+				ID:                   "ab7bd950-6c56-4f5e-a307-45967078f890",
+				Name:                 "deny_all_udp",
+				TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+				Enabled:              true,
+				Action:               "deny",
+				IPVersion:            4,
+				Shared:               false,
+			},
+		}
+
+		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/fw/firewall_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, `
+{
+	"firewall_rule": {
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"destination_ip_address": "192.168.1.0/24",
+		"destination_port": "22",
+		"name": "ssh_form_any",
+		"action": "allow",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61"
+	}
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": "22",
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "ssh_form_any",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": true,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+        `)
+	})
+
+	options := CreateOpts{
+		TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+		Protocol:             "tcp",
+		Description:          "ssh rule",
+		DestinationIPAddress: "192.168.1.0/24",
+		DestinationPort:      "22",
+		Name:                 "ssh_form_any",
+		Action:               "allow",
+	}
+
+	_, 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/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", 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, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": "22",
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "ssh_form_any",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": true,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+        `)
+	})
+
+	rule, err := Get(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "tcp", rule.Protocol)
+	th.AssertEquals(t, "ssh rule", rule.Description)
+	th.AssertEquals(t, "192.168.1.0/24", rule.DestinationIPAddress)
+	th.AssertEquals(t, "e2a5fb51-698c-4898-87e8-f1eee6b50919", rule.PolicyID)
+	th.AssertEquals(t, 2, rule.Position)
+	th.AssertEquals(t, "22", rule.DestinationPort)
+	th.AssertEquals(t, "f03bd950-6c56-4f5e-a307-45967078f507", rule.ID)
+	th.AssertEquals(t, "ssh_form_any", rule.Name)
+	th.AssertEquals(t, "80cf934d6ffb4ef5b244f1c512ad1e61", rule.TenantID)
+	th.AssertEquals(t, true, rule.Enabled)
+	th.AssertEquals(t, "allow", rule.Action)
+	th.AssertEquals(t, 4, rule.IPVersion)
+	th.AssertEquals(t, false, rule.Shared)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", 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, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"destination_ip_address": "192.168.1.0/24",
+		"destination_port": "22",
+		"source_ip_address": null,
+		"source_port": null,
+		"name": "ssh_form_any",
+		"action": "allow",
+		"enabled": false
+	}
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": "22",
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "ssh_form_any",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": false,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+		`)
+	})
+
+	destinationIPAddress := "192.168.1.0/24"
+	destinationPort := "22"
+	empty := ""
+
+	options := UpdateOpts{
+		Protocol:             "tcp",
+		Description:          "ssh rule",
+		DestinationIPAddress: &destinationIPAddress,
+		DestinationPort:      &destinationPort,
+		Name:                 "ssh_form_any",
+		SourceIPAddress:      &empty,
+		SourcePort:           &empty,
+		Action:               "allow",
+		Enabled:              No,
+	}
+
+	_, err := Update(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules/4ec89077-d057-4a2b-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(), "4ec89077-d057-4a2b-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/results.go b/openstack/networking/v2/extensions/fwaas/rules/results.go
new file mode 100644
index 0000000..d772024
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/results.go
@@ -0,0 +1,110 @@
+package rules
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Rule represents a firewall rule
+type Rule struct {
+	ID                   string `json:"id" mapstructure:"id"`
+	Name                 string `json:"name,omitempty" mapstructure:"name"`
+	Description          string `json:"description,omitempty" mapstructure:"description"`
+	Protocol             string `json:"protocol" mapstructure:"protocol"`
+	Action               string `json:"action" mapstructure:"action"`
+	IPVersion            int    `json:"ip_version,omitempty" mapstructure:"ip_version"`
+	SourceIPAddress      string `json:"source_ip_address,omitempty" mapstructure:"source_ip_address"`
+	DestinationIPAddress string `json:"destination_ip_address,omitempty" mapstructure:"destination_ip_address"`
+	SourcePort           string `json:"source_port,omitempty" mapstructure:"source_port"`
+	DestinationPort      string `json:"destination_port,omitempty" mapstructure:"destination_port"`
+	Shared               bool   `json:"shared,omitempty" mapstructure:"shared"`
+	Enabled              bool   `json:"enabled,omitempty" mapstructure:"enabled"`
+	PolicyID             string `json:"firewall_policy_id" mapstructure:"firewall_policy_id"`
+	Position             int    `json:"position" mapstructure:"position"`
+	TenantID             string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// RulePage is the page returned by a pager when traversing over a
+// collection of firewall rules.
+type RulePage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of firewall 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 RulePage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"firewall_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 RulePage struct is empty.
+func (p RulePage) 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 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 ExtractRules(page pagination.Page) ([]Rule, error) {
+	var resp struct {
+		Rules []Rule `mapstructure:"firewall_rules" json:"firewall_rules"`
+	}
+
+	err := mapstructure.Decode(page.(RulePage).Body, &resp)
+
+	return resp.Rules, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall rule.
+func (r commonResult) Extract() (*Rule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Rule *Rule `json:"firewall_rule" mapstructure:"firewall_rule"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Rule, err
+}
+
+// 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 struct {
+	gophercloud.ErrResult
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/urls.go b/openstack/networking/v2/extensions/fwaas/rules/urls.go
new file mode 100644
index 0000000..20b0879
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/urls.go
@@ -0,0 +1,16 @@
+package rules
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "fw"
+	resourcePath = "firewall_rules"
+)
+
+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/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..29f752a
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -0,0 +1,168 @@
+package floatingips
+
+import (
+	"fmt"
+
+	"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")
+)
+
+// 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
+	}
+
+	// Define structures
+	type floatingIP struct {
+		FloatingNetworkID string `json:"floating_network_id"`
+		FloatingIP        string `json:"floating_ip_address,omitempty"`
+		PortID            string `json:"port_id,omitempty"`
+		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,
+		FloatingIP:        opts.FloatingIP,
+		PortID:            opts.PortID,
+		FixedIP:           opts.FixedIP,
+		TenantID:          opts.TenantID,
+	}}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(resourceURL(c, id), nil)
+	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..d914a79
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
@@ -0,0 +1,355 @@
+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 TestCreateEmptyPort(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"
+				}
+			}
+			`)
+
+		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": "",
+						"id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+					}
+				}
+				`)
+	})
+
+	options := CreateOpts{
+		FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+	}
+
+	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, "", 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..a1c7afe
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -0,0 +1,127 @@
+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 this 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 struct {
+	gophercloud.ErrResult
+}
+
+// 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 100644
index 0000000..8b6e73d
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -0,0 +1,230 @@
+package routers
+
+import (
+	"errors"
+
+	"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 = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	return res
+}
+
+// UpdateOpts contains the values used when updating a router.
+type UpdateOpts struct {
+	Name         string
+	AdminStateUp *bool
+	GatewayInfo  *GatewayInfo
+	Routes       []Route
+}
+
+// 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"`
+		Routes       []Route      `json:"routes"`
+	}
+
+	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
+	}
+
+	if opts.Routes != nil {
+		reqBody.Router.Routes = opts.Routes
+	}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(resourceURL(c, id), nil)
+	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 = c.Put(addInterfaceURL(c, id), body, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Put(removeInterfaceURL(c, id), body, &res.Body, &gophercloud.RequestOpts{
+		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 100644
index 0000000..1981733
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -0,0 +1,405 @@
+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"
+        },
+        "routes": [
+            {
+                "nexthop": "10.1.0.10",
+                "destination": "40.0.1.0/24"
+            }
+        ],
+        "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")
+	th.AssertDeepEquals(t, n.Routes, []Route{Route{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}})
+}
+
+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"
+		},
+        "routes": [
+            {
+                "nexthop": "10.1.0.10",
+                "destination": "40.0.1.0/24"
+            }
+        ]
+    }
+}
+			`)
+
+		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",
+        "routes": [
+            {
+                "nexthop": "10.1.0.10",
+                "destination": "40.0.1.0/24"
+            }
+        ]
+    }
+}
+		`)
+	})
+
+	gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+	r := []Route{Route{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}}
+	options := UpdateOpts{Name: "new_name", GatewayInfo: &gwi, Routes: r}
+
+	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"})
+	th.AssertDeepEquals(t, n.Routes, []Route{Route{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}})
+}
+
+func TestAllRoutesRemoved(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": {
+        "routes": []
+    }
+}
+			`)
+
+		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": "name",
+        "admin_state_up": true,
+        "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+        "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e",
+        "routes": []
+    }
+}
+		`)
+	})
+
+	r := []Route{}
+	options := UpdateOpts{Routes: r}
+
+	n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, n.Routes, []Route{})
+}
+
+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 100644
index 0000000..5e297ab
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -0,0 +1,168 @@
+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"`
+}
+
+type Route struct {
+	NextHop         string `mapstructure:"nexthop" json:"nexthop"`
+	DestinationCIDR string `mapstructure:"destination" json:"destination"`
+}
+
+// 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"`
+
+	Routes []Route `json:"routes" mapstructure:"routes"`
+}
+
+// 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 struct {
+	gophercloud.ErrResult
+}
+
+// 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..848938f
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/requests.go
@@ -0,0 +1,123 @@
+package members
+
+import (
+	"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,omitempty"`
+		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 = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	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 = c.Delete(resourceURL(c, id), nil)
+	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..3cad339
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -0,0 +1,122 @@
+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 struct {
+	gophercloud.ErrResult
+}
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..71b21ef
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests.go
@@ -0,0 +1,265 @@
+package monitors
+
+import (
+	"fmt"
+
+	"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 = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(resourceURL(c, id), nil)
+	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..d595abd
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -0,0 +1,147 @@
+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 struct {
+	gophercloud.ErrResult
+}
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..2bb0acc
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests.go
@@ -0,0 +1,181 @@
+package pools
+
+import (
+	"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 = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(resourceURL(c, id), nil)
+	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 = c.Post(associateURL(c, poolID), reqBody, &res.Body, nil)
+	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 = c.Delete(disassociateURL(c, poolID, monitorID), nil)
+	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..3b5c7c7
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
@@ -0,0 +1,318 @@
+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)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	_, 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..07ec85e
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -0,0 +1,146 @@
+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 struct {
+	gophercloud.ErrResult
+}
+
+// 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..6216f87
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests.go
@@ -0,0 +1,256 @@
+package vips
+
+import (
+	"fmt"
+
+	"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 = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(resourceURL(c, id), nil)
+	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..e1092e7
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -0,0 +1,166 @@
+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 struct {
+	gophercloud.ErrResult
+}
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..f07d628
--- /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.WeakDecode(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.WeakDecode(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.WeakDecode(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.WeakDecode(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..8081692
--- /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": 1234567890,
+            "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:  "1234567890",
+			},
+		}
+
+		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..31f744c
--- /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 is 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
+//    is 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..2712ac1
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests.go
@@ -0,0 +1,131 @@
+package groups
+
+import (
+	"fmt"
+
+	"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
+
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID 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"`
+		TenantID    string `json:"tenant_id,omitempty"`
+		Description string `json:"description,omitempty"`
+	}
+
+	type request struct {
+		SecGroup secgroup `json:"security_group"`
+	}
+
+	reqBody := request{SecGroup: secgroup{
+		Name:        opts.Name,
+		TenantID:    opts.TenantID,
+		Description: opts.Description,
+	}}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	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 = c.Get(resourceURL(c, id), &res.Body, nil)
+	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 = c.Delete(resourceURL(c, id), nil)
+	return res
+}
+
+// IDFromName is a convenience function that returns a security group's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	securityGroupCount := 0
+	securityGroupID := ""
+	if name == "" {
+		return "", fmt.Errorf("A security group name must be provided.")
+	}
+	pager := List(client, ListOpts{})
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		securityGroupList, err := ExtractGroups(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range securityGroupList {
+			if s.Name == name {
+				securityGroupCount++
+				securityGroupID = s.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch securityGroupCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find security group: %s", name)
+	case 1:
+		return securityGroupID, nil
+	default:
+		return "", fmt.Errorf("Found %d security groups matching %s", securityGroupCount, name)
+	}
+}
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..49db261
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -0,0 +1,108 @@
+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 struct {
+	gophercloud.ErrResult
+}
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..e06934a
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -0,0 +1,174 @@
+package rules
+
+import (
+	"fmt"
+
+	"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
+
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID string
+}
+
+// Create is an operation which adds a new security group rule and associates it
+// with an existing security group (whose ID is specified in CreateOpts).
+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"`
+		TenantID       string `json:"tenant_id,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,
+		TenantID:       opts.TenantID,
+	}}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular security group rule based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+	return res
+}
+
+// Delete will permanently delete a particular security group rule based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, id), nil)
+	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..6e13857
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -0,0 +1,133 @@
+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 struct {
+	gophercloud.ErrResult
+}
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..25ab7a8
--- /dev/null
+++ b/openstack/networking/v2/networks/requests.go
@@ -0,0 +1,226 @@
+package networks
+
+import (
+	"fmt"
+
+	"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
+)
+
+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 = c.Get(getURL(c, id), &res.Body, nil)
+	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
+	}
+
+	_, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil)
+	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 = c.Put(updateURL(c, networkID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(deleteURL(c, networkID), nil)
+	return res
+}
+
+// IDFromName is a convenience function that returns a network's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	networkCount := 0
+	networkID := ""
+	if name == "" {
+		return "", fmt.Errorf("A network name must be provided.")
+	}
+	pager := List(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		networkList, err := ExtractNetworks(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, n := range networkList {
+			if n.Name == name {
+				networkCount++
+				networkID = n.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch networkCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find network: %s", name)
+	case 1:
+		return networkID, nil
+	default:
+		return "", fmt.Errorf("Found %d networks matching %s", networkCount, name)
+	}
+}
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
new file mode 100644
index 0000000..81eb79c
--- /dev/null
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -0,0 +1,276 @@
+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)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	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..3ecedde
--- /dev/null
+++ b/openstack/networking/v2/networks/results.go
@@ -0,0 +1,116 @@
+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 struct {
+	gophercloud.ErrResult
+}
+
+// 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..a9eecc5
--- /dev/null
+++ b/openstack/networking/v2/networks/urls.go
@@ -0,0 +1,31 @@
+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 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/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..2caf1ca
--- /dev/null
+++ b/openstack/networking/v2/ports/requests.go
@@ -0,0 +1,260 @@
+package ports
+
+import (
+	"fmt"
+
+	"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
+)
+
+// 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 a 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 = c.Get(getURL(c, id), &res.Body, nil)
+	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
+	}
+
+	_, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil)
+	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 = c.Put(updateURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(deleteURL(c, id), nil)
+	return res
+}
+
+// IDFromName is a convenience function that returns a port's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	portCount := 0
+	portID := ""
+	if name == "" {
+		return "", fmt.Errorf("A port name must be provided.")
+	}
+	pager := List(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		portList, err := ExtractPorts(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, p := range portList {
+			if p.Name == name {
+				portCount++
+				portID = p.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch portCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find port: %s", name)
+	case 1:
+		return portID, nil
+	default:
+		return "", fmt.Errorf("Found %d ports matching %s", portCount, name)
+	}
+}
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..2511ff5
--- /dev/null
+++ b/openstack/networking/v2/ports/results.go
@@ -0,0 +1,126 @@
+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 struct {
+	gophercloud.ErrResult
+}
+
+// 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..6cde048
--- /dev/null
+++ b/openstack/networking/v2/subnets/requests.go
@@ -0,0 +1,271 @@
+package subnets
+
+import (
+	"fmt"
+
+	"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
+)
+
+// 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 a 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 = c.Get(getURL(c, id), &res.Body, nil)
+	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 = c.Post(createURL(c), reqBody, &res.Body, nil)
+	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 opts.DNSNameservers != nil {
+		s["dns_nameservers"] = opts.DNSNameservers
+	}
+	if opts.HostRoutes != nil {
+		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 = c.Put(updateURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(deleteURL(c, id), nil)
+	return res
+}
+
+// IDFromName is a convenience function that returns a subnet's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	subnetCount := 0
+	subnetID := ""
+	if name == "" {
+		return "", fmt.Errorf("A subnet name must be provided.")
+	}
+	pager := List(client, nil)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		subnetList, err := ExtractSubnets(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range subnetList {
+			if s.Name == name {
+				subnetCount++
+				subnetID = s.ID
+			}
+		}
+		return true, nil
+	})
+
+	switch subnetCount {
+	case 0:
+		return "", fmt.Errorf("Unable to find subnet: %s", name)
+	case 1:
+		return subnetID, nil
+	default:
+		return "", fmt.Errorf("Found %d subnets matching %s", subnetCount, name)
+	}
+}
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..77b956a
--- /dev/null
+++ b/openstack/networking/v2/subnets/results.go
@@ -0,0 +1,132 @@
+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 struct {
+	gophercloud.ErrResult
+}
+
+// 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 `mapstructure:"destination" json:"destination"`
+	NextHop         string `mapstructure:"nexthop" 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/results_test.go b/openstack/networking/v2/subnets/results_test.go
new file mode 100644
index 0000000..d404838
--- /dev/null
+++ b/openstack/networking/v2/subnets/results_test.go
@@ -0,0 +1,54 @@
+package subnets
+
+import (
+	"encoding/json"
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"testing"
+)
+
+func TestHostRoute(t *testing.T) {
+	sejson := []byte(`
+    {"subnet": {
+      "name": "test-subnet",
+      "enable_dhcp": false,
+      "network_id": "3e66c41e-cbbd-4019-9aab-740b7e4150a0",
+      "tenant_id": "f86e123198cf42d19c8854c5f80c2f06",
+      "dns_nameservers": [],
+      "gateway_ip": "172.16.0.1",
+      "ipv6_ra_mode": null,
+      "allocation_pools": [
+        {
+          "start": "172.16.0.2",
+          "end": "172.16.255.254"
+        }
+      ],
+      "host_routes": [
+        {
+          "destination": "172.20.1.0/24",
+		  "nexthop": "172.16.0.2"
+        }
+      ],
+      "ip_version": 4,
+      "ipv6_address_mode": null,
+      "cidr": "172.16.0.0/16",
+      "id": "6dcaa873-7115-41af-9ef5-915f73636e43",
+      "subnetpool_id": null
+  }}
+`)
+
+	var dejson interface{}
+	err := json.Unmarshal(sejson, &dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	resp := commonResult{gophercloud.Result{Body: dejson}}
+	subnet, err := resp.Extract()
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	route := subnet.HostRoutes[0]
+	th.AssertEquals(t, route.NextHop, "172.16.0.2")
+	th.AssertEquals(t, route.DestinationCIDR, "172.20.1.0/24")
+}
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..f22b687
--- /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, "HEAD")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		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, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+
+		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..66c46a9
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -0,0 +1,107 @@
+package accounts
+
+import "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
+// ExtractHeader 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 := c.Request("HEAD", getURL(c), gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{204},
+	})
+	if resp != nil {
+		res.Header = resp.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 := make(map[string]string)
+
+	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 := c.Request("POST", updateURL(c), gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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..6454c0a
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests_test.go
@@ -0,0 +1,32 @@
+package accounts
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestUpdateAccount(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateAccountSuccessfully(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()
+	HandleGetAccountSuccessfully(t)
+
+	expectedMetadata := map[string]string{"Subject": "books"}
+	res := Get(fake.ServiceClient(), &GetOpts{})
+	th.AssertNoErr(t, res.Err)
+	actualMetadata, _ := res.ExtractMetadata()
+	th.CheckDeepEquals(t, expectedMetadata, actualMetadata)
+	//headers, err := res.Extract()
+	//th.AssertNoErr(t, err)
+}
diff --git a/openstack/objectstorage/v1/accounts/results.go b/openstack/objectstorage/v1/accounts/results.go
new file mode 100644
index 0000000..6ab1a23
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/results.go
@@ -0,0 +1,102 @@
+package accounts
+
+import (
+	"strings"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// UpdateResult is returned from a call to the Update function.
+type UpdateResult struct {
+	gophercloud.HeaderResult
+}
+
+// UpdateHeader represents the headers returned in the response from an Update request.
+type UpdateHeader struct {
+	ContentLength string    `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// Extract will return a struct of headers returned from a call to Get. To obtain
+// a map of headers, call the ExtractHeader method on the GetResult.
+func (ur UpdateResult) Extract() (UpdateHeader, error) {
+	var uh UpdateHeader
+	if ur.Err != nil {
+		return uh, ur.Err
+	}
+
+	if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil {
+		return uh, err
+	}
+
+	if date, ok := ur.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, ur.Header["Date"][0])
+		if err != nil {
+			return uh, err
+		}
+		uh.Date = t
+	}
+
+	return uh, nil
+}
+
+// GetHeader represents the headers returned in the response from a Get request.
+type GetHeader struct {
+	BytesUsed      int64     `mapstructure:"X-Account-Bytes-Used"`
+	ContainerCount int       `mapstructure:"X-Account-Container-Count"`
+	ContentLength  int64     `mapstructure:"Content-Length"`
+	ContentType    string    `mapstructure:"Content-Type"`
+	Date           time.Time `mapstructure:"-"`
+	ObjectCount    int64     `mapstructure:"X-Account-Object-Count"`
+	TransID        string    `mapstructure:"X-Trans-Id"`
+	TempURLKey     string    `mapstructure:"X-Account-Meta-Temp-URL-Key"`
+	TempURLKey2    string    `mapstructure:"X-Account-Meta-Temp-URL-Key-2"`
+}
+
+// GetResult is returned from a call to the Get function.
+type GetResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Get. To obtain
+// a map of headers, call the ExtractHeader method on the GetResult.
+func (gr GetResult) Extract() (GetHeader, error) {
+	var gh GetHeader
+	if gr.Err != nil {
+		return gh, gr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil {
+		return gh, err
+	}
+
+	if date, ok := gr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, gr.Header["Date"][0])
+		if err != nil {
+			return gh, err
+		}
+		gh.Date = t
+	}
+
+	return gh, nil
+}
+
+// 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
+}
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..e607352
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/fixtures.go
@@ -0,0 +1,143 @@
+// +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 "janeausten":
+			fmt.Fprintf(w, `[
+				{
+					"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 "janeausten":
+			fmt.Fprintf(w, "marktwain\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.Header().Add("X-Trans-Id", "1234567")
+		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..50ff9f4
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -0,0 +1,205 @@
+package containers
+
+import (
+	"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 := c.Request("PUT", createURL(c, containerName), gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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
+	_, res.Err = c.Delete(deleteURL(c, containerName), nil)
+	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 := c.Request("POST", updateURL(c, containerName), gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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 := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{
+		OkCodes: []int{200, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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..0ccd5a7
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests_test.go
@@ -0,0 +1,117 @@
+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 TestListAllContainerInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListContainerInfoSuccessfully(t)
+
+	allPages, err := List(fake.ServiceClient(), &ListOpts{Full: true}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := ExtractInfo(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedListInfo, actual)
+}
+
+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 TestListAllContainerNames(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListContainerNamesSuccessfully(t)
+
+	allPages, err := List(fake.ServiceClient(), &ListOpts{Full: false}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := ExtractNames(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedListNames, actual)
+}
+
+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)
+	c, err := res.Extract()
+	th.CheckNoErr(t, err)
+	th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0])
+	th.CheckEquals(t, "1234567", c.TransID)
+}
+
+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..e682b8d
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -0,0 +1,270 @@
+package containers
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"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)
+	}
+}
+
+// GetHeader represents the headers returned in the response from a Get request.
+type GetHeader struct {
+	AcceptRanges     string    `mapstructure:"Accept-Ranges"`
+	BytesUsed        int64     `mapstructure:"X-Account-Bytes-Used"`
+	ContentLength    int64     `mapstructure:"Content-Length"`
+	ContentType      string    `mapstructure:"Content-Type"`
+	Date             time.Time `mapstructure:"-"`
+	ObjectCount      int64     `mapstructure:"X-Container-Object-Count"`
+	Read             string    `mapstructure:"X-Container-Read"`
+	TransID          string    `mapstructure:"X-Trans-Id"`
+	VersionsLocation string    `mapstructure:"X-Versions-Location"`
+	Write            string    `mapstructure:"X-Container-Write"`
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Get. To obtain
+// a map of headers, call the ExtractHeader method on the GetResult.
+func (gr GetResult) Extract() (GetHeader, error) {
+	var gh GetHeader
+	if gr.Err != nil {
+		return gh, gr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil {
+		return gh, err
+	}
+
+	if date, ok := gr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, gr.Header["Date"][0])
+		if err != nil {
+			return gh, err
+		}
+		gh.Date = t
+	}
+
+	return gh, nil
+}
+
+// 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
+}
+
+// CreateHeader represents the headers returned in the response from a Create request.
+type CreateHeader struct {
+	ContentLength int64     `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// CreateResult represents the result of a create operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeader'
+// method on the result struct.
+type CreateResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Create. To obtain
+// a map of headers, call the ExtractHeader method on the CreateResult.
+func (cr CreateResult) Extract() (CreateHeader, error) {
+	var ch CreateHeader
+	if cr.Err != nil {
+		return ch, cr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(cr.Header, &ch); err != nil {
+		return ch, err
+	}
+
+	if date, ok := cr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, cr.Header["Date"][0])
+		if err != nil {
+			return ch, err
+		}
+		ch.Date = t
+	}
+
+	return ch, nil
+}
+
+// UpdateHeader represents the headers returned in the response from a Update request.
+type UpdateHeader struct {
+	ContentLength int64     `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// UpdateResult represents the result of an update operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeader'
+// method on the result struct.
+type UpdateResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Update. To obtain
+// a map of headers, call the ExtractHeader method on the UpdateResult.
+func (ur UpdateResult) Extract() (UpdateHeader, error) {
+	var uh UpdateHeader
+	if ur.Err != nil {
+		return uh, ur.Err
+	}
+
+	if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil {
+		return uh, err
+	}
+
+	if date, ok := ur.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, ur.Header["Date"][0])
+		if err != nil {
+			return uh, err
+		}
+		uh.Date = t
+	}
+
+	return uh, nil
+}
+
+// DeleteHeader represents the headers returned in the response from a Delete request.
+type DeleteHeader struct {
+	ContentLength int64     `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// DeleteResult represents the result of a delete operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeader'
+// method on the result struct.
+type DeleteResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Delete. To obtain
+// a map of headers, call the ExtractHeader method on the DeleteResult.
+func (dr DeleteResult) Extract() (DeleteHeader, error) {
+	var dh DeleteHeader
+	if dr.Err != nil {
+		return dh, dr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(dr.Header, &dh); err != nil {
+		return dh, err
+	}
+
+	if date, ok := dr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, dr.Header["Date"][0])
+		if err != nil {
+			return dh, err
+		}
+		dh.Date = t
+	}
+
+	return dh, nil
+}
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..7a6e6e1
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/fixtures.go
@@ -0,0 +1,195 @@
+// +build fixtures
+
+package objects
+
+import (
+	"crypto/md5"
+	"fmt"
+	"io"
+	"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)
+		}
+	})
+}
+
+// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux
+// that responds with a `Create` response. A Content-Type of "text/plain" is expected.
+func HandleCreateTextObjectSuccessfully(t *testing.T, content string) {
+	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, "Content-Type", "text/plain")
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		hash := md5.New()
+		io.WriteString(hash, content)
+		localChecksum := hash.Sum(nil)
+
+		w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum))
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
+// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server-
+// side content-type detection will be triggered properly.
+func HandleCreateTypelessObjectSuccessfully(t *testing.T, content string) {
+	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")
+
+		if contentType, present := r.Header["Content-Type"]; present {
+			t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType)
+		}
+
+		hash := md5.New()
+		io.WriteString(hash, content)
+		localChecksum := hash.Sum(nil)
+
+		w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum))
+		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..f85add0
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -0,0 +1,501 @@
+package objects
+
+import (
+	"bufio"
+	"crypto/hmac"
+	"crypto/md5"
+	"crypto/sha1"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strings"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts"
+	"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 is a true/false value that represents the amount of object information
+	// returned. If Full is set to true, then the content-type, number of bytes, hash
+	// date last modified, and name are returned. If set to false or not set, then
+	// only the object names are returned.
+	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 DownloadOpts) 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 := c.Request("GET", url, gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{200, 304},
+	})
+	if resp != nil {
+		res.Header = resp.Header
+		res.Body = resp.Body
+	}
+	res.Err = err
+
+	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      int64  `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:"multipart-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. If the returned response's ETag
+// header fails to match the local checksum, the failed request will automatically be retried up to a maximum of 3 times.
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	url := createURL(c, containerName, objectName)
+	h := make(map[string]string)
+
+	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
+	}
+
+	hash := md5.New()
+	bufioReader := bufio.NewReader(io.TeeReader(content, hash))
+	io.Copy(ioutil.Discard, bufioReader)
+	localChecksum := hash.Sum(nil)
+
+	h["ETag"] = fmt.Sprintf("%x", localChecksum)
+
+	_, err := content.Seek(0, 0)
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	ropts := gophercloud.RequestOpts{
+		RawBody:     content,
+		MoreHeaders: h,
+	}
+
+	resp, err := c.Request("PUT", url, ropts)
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	if resp != nil {
+		res.Header = resp.Header
+		if resp.Header.Get("ETag") == fmt.Sprintf("%x", localChecksum) {
+			res.Err = err
+			return res
+		}
+		res.Err = fmt.Errorf("Local checksum does not match API ETag header")
+	}
+
+	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 := c.Request("COPY", url, gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{201},
+	})
+	if resp != nil {
+		res.Header = resp.Header
+	}
+	res.Err = err
+	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 := c.Delete(url, nil)
+	if resp != nil {
+		res.Header = resp.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 := c.Request("HEAD", url, gophercloud.RequestOpts{
+		OkCodes: []int{200, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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 := c.Request("POST", url, gophercloud.RequestOpts{
+		MoreHeaders: h,
+	})
+	if resp != nil {
+		res.Header = resp.Header
+	}
+	res.Err = err
+	return res
+}
+
+// HTTPMethod represents an HTTP method string (e.g. "GET").
+type HTTPMethod string
+
+var (
+	// GET represents an HTTP "GET" method.
+	GET HTTPMethod = "GET"
+	// POST represents an HTTP "POST" method.
+	POST HTTPMethod = "POST"
+)
+
+// CreateTempURLOpts are options for creating a temporary URL for an object.
+type CreateTempURLOpts struct {
+	// (REQUIRED) Method is the HTTP method to allow for users of the temp URL. Valid values
+	// are "GET" and "POST".
+	Method HTTPMethod
+	// (REQUIRED) TTL is the number of seconds the temp URL should be active.
+	TTL int
+	// (Optional) Split is the string on which to split the object URL. Since only
+	// the object path is used in the hash, the object URL needs to be parsed. If
+	// empty, the default OpenStack URL split point will be used ("/v1/").
+	Split string
+}
+
+// CreateTempURL is a function for creating a temporary URL for an object. It
+// allows users to have "GET" or "POST" access to a particular tenant's object
+// for a limited amount of time.
+func CreateTempURL(c *gophercloud.ServiceClient, containerName, objectName string, opts CreateTempURLOpts) (string, error) {
+	if opts.Split == "" {
+		opts.Split = "/v1/"
+	}
+	duration := time.Duration(opts.TTL) * time.Second
+	expiry := time.Now().Add(duration).Unix()
+	getHeader, err := accounts.Get(c, nil).Extract()
+	if err != nil {
+		return "", err
+	}
+	secretKey := []byte(getHeader.TempURLKey)
+	url := getURL(c, containerName, objectName)
+	splitPath := strings.Split(url, opts.Split)
+	baseURL, objectPath := splitPath[0], splitPath[1]
+	objectPath = opts.Split + objectPath
+	body := fmt.Sprintf("%s\n%d\n%s", opts.Method, expiry, objectPath)
+	hash := hmac.New(sha1.New, secretKey)
+	hash.Write([]byte(body))
+	hexsum := fmt.Sprintf("%x", hash.Sum(nil))
+	return fmt.Sprintf("%s%s?temp_url_sig=%s&temp_url_expires=%d", baseURL, objectPath, hexsum, expiry), nil
+}
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
new file mode 100644
index 0000000..f7d6822
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -0,0 +1,165 @@
+package objects
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDownloadReader(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDownloadObjectSuccessfully(t)
+
+	response := Download(fake.ServiceClient(), "testContainer", "testObject", nil)
+	defer response.Body.Close()
+
+	// Check reader
+	buf := bytes.NewBuffer(make([]byte, 0))
+	io.CopyN(buf, response.Body, 10)
+	th.CheckEquals(t, "Successful", string(buf.Bytes()))
+}
+
+func TestDownloadExtraction(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDownloadObjectSuccessfully(t)
+
+	response := Download(fake.ServiceClient(), "testContainer", "testObject", nil)
+
+	// Check []byte extraction
+	bytes, err := response.ExtractContent()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "Successful download with Gophercloud", string(bytes))
+}
+
+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()
+
+	content := "Did gyre and gimble in the wabe"
+
+	HandleCreateTextObjectSuccessfully(t, content)
+
+	options := &CreateOpts{ContentType: "text/plain"}
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateObjectWithoutContentType(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	content := "The sky was the color of television, tuned to a dead channel."
+
+	HandleCreateTypelessObjectSuccessfully(t, content)
+
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &CreateOpts{})
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestErrorIsRaisedForChecksumMismatch(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("ETag", "acbd18db4cc2f85cedef654fccc4a4d8")
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	content := strings.NewReader("The sky was the color of television, tuned to a dead channel.")
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
+
+	err := fmt.Errorf("Local checksum does not match API ETag header")
+	th.AssertDeepEquals(t, err, 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..ecb2c54
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -0,0 +1,438 @@
+package objects
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strconv"
+	"strings"
+	"time"
+
+	"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 is the total number of bytes that comprise the object.
+	Bytes int64 `json:"bytes" mapstructure:"bytes"`
+
+	// ContentType is the content type of the object.
+	ContentType string `json:"content_type" mapstructure:"content_type"`
+
+	// Hash represents the MD5 checksum value of the object's content.
+	Hash string `json:"hash" mapstructure:"hash"`
+
+	// LastModified is the RFC3339Milli time the object was last modified, represented
+	// as a string. For any given object (obj), this value may be parsed to a time.Time:
+	// lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified)
+	LastModified string `json:"last_modified" mapstructure:"last_modified"`
+
+	// Name is the unique name for the object.
+	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)
+	}
+}
+
+// DownloadHeader represents the headers returned in the response from a Download request.
+type DownloadHeader struct {
+	AcceptRanges       string    `mapstructure:"Accept-Ranges"`
+	ContentDisposition string    `mapstructure:"Content-Disposition"`
+	ContentEncoding    string    `mapstructure:"Content-Encoding"`
+	ContentLength      int64     `mapstructure:"Content-Length"`
+	ContentType        string    `mapstructure:"Content-Type"`
+	Date               time.Time `mapstructure:"-"`
+	DeleteAt           time.Time `mapstructure:"-"`
+	ETag               string    `mapstructure:"Etag"`
+	LastModified       time.Time `mapstructure:"-"`
+	ObjectManifest     string    `mapstructure:"X-Object-Manifest"`
+	StaticLargeObject  bool      `mapstructure:"X-Static-Large-Object"`
+	TransID            string    `mapstructure:"X-Trans-Id"`
+}
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult struct {
+	gophercloud.HeaderResult
+	Body io.ReadCloser
+}
+
+// Extract will return a struct of headers returned from a call to Download. To obtain
+// a map of headers, call the ExtractHeader method on the DownloadResult.
+func (dr DownloadResult) Extract() (DownloadHeader, error) {
+	var dh DownloadHeader
+	if dr.Err != nil {
+		return dh, dr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(dr.Header, &dh); err != nil {
+		return dh, err
+	}
+
+	if date, ok := dr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, date[0])
+		if err != nil {
+			return dh, err
+		}
+		dh.Date = t
+	}
+
+	if date, ok := dr.Header["Last-Modified"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, date[0])
+		if err != nil {
+			return dh, err
+		}
+		dh.LastModified = t
+	}
+
+	if date, ok := dr.Header["X-Delete-At"]; ok && len(date) > 0 {
+		unix, err := strconv.ParseInt(date[0], 10, 64)
+		if err != nil {
+			return dh, err
+		}
+		dh.DeleteAt = time.Unix(unix, 0)
+	}
+
+	return dh, nil
+}
+
+// ExtractContent is a function that takes a DownloadResult's io.Reader body
+// and reads all available data into a slice of bytes. Please be aware that due
+// the nature of io.Reader is forward-only - meaning that it can only be read
+// once and not rewound. You can recreate a reader from the output of this
+// function by using bytes.NewReader(downloadBytes)
+func (dr DownloadResult) ExtractContent() ([]byte, error) {
+	if dr.Err != nil {
+		return nil, dr.Err
+	}
+	body, err := ioutil.ReadAll(dr.Body)
+	if err != nil {
+		return nil, err
+	}
+	dr.Body.Close()
+	return body, nil
+}
+
+// GetHeader represents the headers returned in the response from a Get request.
+type GetHeader struct {
+	ContentDisposition string    `mapstructure:"Content-Disposition"`
+	ContentEncoding    string    `mapstructure:"Content-Encoding"`
+	ContentLength      int64     `mapstructure:"Content-Length"`
+	ContentType        string    `mapstructure:"Content-Type"`
+	Date               time.Time `mapstructure:"-"`
+	DeleteAt           time.Time `mapstructure:"-"`
+	ETag               string    `mapstructure:"Etag"`
+	LastModified       time.Time `mapstructure:"-"`
+	ObjectManifest     string    `mapstructure:"X-Object-Manifest"`
+	StaticLargeObject  bool      `mapstructure:"X-Static-Large-Object"`
+	TransID            string    `mapstructure:"X-Trans-Id"`
+}
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Get. To obtain
+// a map of headers, call the ExtractHeader method on the GetResult.
+func (gr GetResult) Extract() (GetHeader, error) {
+	var gh GetHeader
+	if gr.Err != nil {
+		return gh, gr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil {
+		return gh, err
+	}
+
+	if date, ok := gr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, gr.Header["Date"][0])
+		if err != nil {
+			return gh, err
+		}
+		gh.Date = t
+	}
+
+	if date, ok := gr.Header["Last-Modified"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, gr.Header["Last-Modified"][0])
+		if err != nil {
+			return gh, err
+		}
+		gh.LastModified = t
+	}
+
+	if date, ok := gr.Header["X-Delete-At"]; ok && len(date) > 0 {
+		unix, err := strconv.ParseInt(date[0], 10, 64)
+		if err != nil {
+			return gh, err
+		}
+		gh.DeleteAt = time.Unix(unix, 0)
+	}
+
+	return gh, nil
+}
+
+// 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
+}
+
+// CreateHeader represents the headers returned in the response from a Create request.
+type CreateHeader struct {
+	ContentLength int64     `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	ETag          string    `mapstructure:"Etag"`
+	LastModified  time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Create. To obtain
+// a map of headers, call the ExtractHeader method on the CreateResult.
+func (cr CreateResult) Extract() (CreateHeader, error) {
+	var ch CreateHeader
+	if cr.Err != nil {
+		return ch, cr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(cr.Header, &ch); err != nil {
+		return ch, err
+	}
+
+	if date, ok := cr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, cr.Header["Date"][0])
+		if err != nil {
+			return ch, err
+		}
+		ch.Date = t
+	}
+
+	if date, ok := cr.Header["Last-Modified"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, cr.Header["Last-Modified"][0])
+		if err != nil {
+			return ch, err
+		}
+		ch.LastModified = t
+	}
+
+	return ch, nil
+}
+
+// UpdateHeader represents the headers returned in the response from a Update request.
+type UpdateHeader struct {
+	ContentLength int64     `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Update. To obtain
+// a map of headers, call the ExtractHeader method on the UpdateResult.
+func (ur UpdateResult) Extract() (UpdateHeader, error) {
+	var uh UpdateHeader
+	if ur.Err != nil {
+		return uh, ur.Err
+	}
+
+	if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil {
+		return uh, err
+	}
+
+	if date, ok := ur.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, ur.Header["Date"][0])
+		if err != nil {
+			return uh, err
+		}
+		uh.Date = t
+	}
+
+	return uh, nil
+}
+
+// DeleteHeader represents the headers returned in the response from a Delete request.
+type DeleteHeader struct {
+	ContentLength int64     `mapstructure:"Content-Length"`
+	ContentType   string    `mapstructure:"Content-Type"`
+	Date          time.Time `mapstructure:"-"`
+	TransID       string    `mapstructure:"X-Trans-Id"`
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Delete. To obtain
+// a map of headers, call the ExtractHeader method on the DeleteResult.
+func (dr DeleteResult) Extract() (DeleteHeader, error) {
+	var dh DeleteHeader
+	if dr.Err != nil {
+		return dh, dr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(dr.Header, &dh); err != nil {
+		return dh, err
+	}
+
+	if date, ok := dr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, dr.Header["Date"][0])
+		if err != nil {
+			return dh, err
+		}
+		dh.Date = t
+	}
+
+	return dh, nil
+}
+
+// CopyHeader represents the headers returned in the response from a Copy request.
+type CopyHeader struct {
+	ContentLength          int64     `mapstructure:"Content-Length"`
+	ContentType            string    `mapstructure:"Content-Type"`
+	CopiedFrom             string    `mapstructure:"X-Copied-From"`
+	CopiedFromLastModified time.Time `mapstructure:"-"`
+	Date                   time.Time `mapstructure:"-"`
+	ETag                   string    `mapstructure:"Etag"`
+	LastModified           time.Time `mapstructure:"-"`
+	TransID                string    `mapstructure:"X-Trans-Id"`
+}
+
+// CopyResult represents the result of a copy operation.
+type CopyResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Copy. To obtain
+// a map of headers, call the ExtractHeader method on the CopyResult.
+func (cr CopyResult) Extract() (CopyHeader, error) {
+	var ch CopyHeader
+	if cr.Err != nil {
+		return ch, cr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(cr.Header, &ch); err != nil {
+		return ch, err
+	}
+
+	if date, ok := cr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, cr.Header["Date"][0])
+		if err != nil {
+			return ch, err
+		}
+		ch.Date = t
+	}
+
+	if date, ok := cr.Header["Last-Modified"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, cr.Header["Last-Modified"][0])
+		if err != nil {
+			return ch, err
+		}
+		ch.LastModified = t
+	}
+
+	if date, ok := cr.Header["X-Copied-From-Last-Modified"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, cr.Header["X-Copied-From-Last-Modified"][0])
+		if err != nil {
+			return ch, err
+		}
+		ch.CopiedFromLastModified = t
+	}
+
+	return ch, nil
+}
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/orchestration/v1/apiversions/doc.go b/openstack/orchestration/v1/apiversions/doc.go
new file mode 100644
index 0000000..f2db622
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/doc.go
@@ -0,0 +1,4 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Heat service. This functionality is not
+// restricted to this particular version.
+package apiversions
diff --git a/openstack/orchestration/v1/apiversions/requests.go b/openstack/orchestration/v1/apiversions/requests.go
new file mode 100644
index 0000000..f6454c8
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/requests.go
@@ -0,0 +1,13 @@
+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)}
+	})
+}
diff --git a/openstack/orchestration/v1/apiversions/requests_test.go b/openstack/orchestration/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..a2fc980
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/requests_test.go
@@ -0,0 +1,89 @@
+package apiversions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"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": "v1.0",
+            "links": [
+                {
+                    "href": "http://23.253.228.211:8000/v1",
+                    "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:     "v1.0",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "http://23.253.228.211:8000/v1",
+						Rel:  "self",
+					},
+				},
+			},
+		}
+
+		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
+	})
+}
diff --git a/openstack/orchestration/v1/apiversions/results.go b/openstack/orchestration/v1/apiversions/results.go
new file mode 100644
index 0000000..0700ab0
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/results.go
@@ -0,0 +1,42 @@
+package apiversions
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"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"`
+	ID     string             `mapstructure:"id"`
+	Links  []gophercloud.Link `mapstructure:"links"`
+}
+
+// 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
+}
diff --git a/openstack/orchestration/v1/apiversions/urls.go b/openstack/orchestration/v1/apiversions/urls.go
new file mode 100644
index 0000000..55d6e0e
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/urls.go
@@ -0,0 +1,7 @@
+package apiversions
+
+import "github.com/rackspace/gophercloud"
+
+func apiVersionsURL(c *gophercloud.ServiceClient) string {
+	return c.Endpoint
+}
diff --git a/openstack/orchestration/v1/buildinfo/doc.go b/openstack/orchestration/v1/buildinfo/doc.go
new file mode 100644
index 0000000..183e8df
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/doc.go
@@ -0,0 +1,2 @@
+// Package buildinfo provides build information about heat deployments.
+package buildinfo
diff --git a/openstack/orchestration/v1/buildinfo/fixtures.go b/openstack/orchestration/v1/buildinfo/fixtures.go
new file mode 100644
index 0000000..20ea09b
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/fixtures.go
@@ -0,0 +1,45 @@
+package buildinfo
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &BuildInfo{
+	API: Revision{
+		Revision: "2.4.5",
+	},
+	Engine: Revision{
+		Revision: "1.2.1",
+	},
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+  "api": {
+    "revision": "2.4.5"
+  },
+  "engine": {
+    "revision": "1.2.1"
+  }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/build_info`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/build_info", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
diff --git a/openstack/orchestration/v1/buildinfo/requests.go b/openstack/orchestration/v1/buildinfo/requests.go
new file mode 100644
index 0000000..9e03e5c
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/requests.go
@@ -0,0 +1,10 @@
+package buildinfo
+
+import "github.com/rackspace/gophercloud"
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c), &res.Body, nil)
+	return res
+}
diff --git a/openstack/orchestration/v1/buildinfo/requests_test.go b/openstack/orchestration/v1/buildinfo/requests_test.go
new file mode 100644
index 0000000..1e0fe23
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/requests_test.go
@@ -0,0 +1,20 @@
+package buildinfo
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := Get(fake.ServiceClient()).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/buildinfo/results.go b/openstack/orchestration/v1/buildinfo/results.go
new file mode 100644
index 0000000..683a434
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/results.go
@@ -0,0 +1,37 @@
+package buildinfo
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+)
+
+// Revision represents the API/Engine revision of a Heat deployment.
+type Revision struct {
+	Revision string `mapstructure:"revision"`
+}
+
+// BuildInfo represents the build information for a Heat deployment.
+type BuildInfo struct {
+	API    Revision `mapstructure:"api"`
+	Engine Revision `mapstructure:"engine"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a BuildInfo object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*BuildInfo, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res BuildInfo
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	return &res, nil
+}
diff --git a/openstack/orchestration/v1/buildinfo/urls.go b/openstack/orchestration/v1/buildinfo/urls.go
new file mode 100644
index 0000000..2c873d0
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/urls.go
@@ -0,0 +1,7 @@
+package buildinfo
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("build_info")
+}
diff --git a/openstack/orchestration/v1/stackevents/doc.go b/openstack/orchestration/v1/stackevents/doc.go
new file mode 100644
index 0000000..51cdd97
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/doc.go
@@ -0,0 +1,4 @@
+// Package stackevents provides operations for finding, listing, and retrieving
+// stack events. Stack events are events that take place on stacks such as
+// updating and abandoning.
+package stackevents
diff --git a/openstack/orchestration/v1/stackevents/fixtures.go b/openstack/orchestration/v1/stackevents/fixtures.go
new file mode 100644
index 0000000..235787a
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/fixtures.go
@@ -0,0 +1,446 @@
+package stackevents
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// FindExpected represents the expected object from a Find request.
+var FindExpected = []Event{
+	Event{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "resource",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_IN_PROGRESS",
+		PhysicalResourceID:   "",
+		ID:                   "06feb26f-9298-4a9b-8749-9d770e5d577a",
+	},
+	Event{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "resource",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_COMPLETE",
+		PhysicalResourceID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		ID:                   "93940999-7d40-44ae-8de4-19624e7b8d18",
+	},
+}
+
+// FindOutput represents the response body from a Find request.
+const FindOutput = `
+{
+  "events": [
+  {
+    "resource_name": "hello_world",
+    "event_time": "2015-02-05T21:33:11",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "resource"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": null,
+    "id": "06feb26f-9298-4a9b-8749-9d770e5d577a"
+    },
+    {
+      "resource_name": "hello_world",
+      "event_time": "2015-02-05T21:33:27",
+      "links": [
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+        "rel": "self"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+        "rel": "resource"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+        "rel": "stack"
+      }
+      ],
+      "logical_resource_id": "hello_world",
+      "resource_status_reason": "state changed",
+      "resource_status": "CREATE_COMPLETE",
+      "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+      "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+    }
+  ]
+}`
+
+// HandleFindSuccessfully creates an HTTP handler at `/stacks/postman_stack/events`
+// on the test handler mux that responds with a `Find` response.
+func HandleFindSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/postman_stack/events", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// ListExpected represents the expected object from a List request.
+var ListExpected = []Event{
+	Event{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "resource",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_IN_PROGRESS",
+		PhysicalResourceID:   "",
+		ID:                   "06feb26f-9298-4a9b-8749-9d770e5d577a",
+	},
+	Event{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "resource",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_COMPLETE",
+		PhysicalResourceID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		ID:                   "93940999-7d40-44ae-8de4-19624e7b8d18",
+	},
+}
+
+// ListOutput represents the response body from a List request.
+const ListOutput = `
+{
+  "events": [
+  {
+    "resource_name": "hello_world",
+    "event_time": "2015-02-05T21:33:11",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "resource"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": null,
+    "id": "06feb26f-9298-4a9b-8749-9d770e5d577a"
+    },
+    {
+      "resource_name": "hello_world",
+      "event_time": "2015-02-05T21:33:27",
+      "links": [
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+        "rel": "self"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+        "rel": "resource"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+        "rel": "stack"
+      }
+      ],
+      "logical_resource_id": "hello_world",
+      "resource_status_reason": "state changed",
+      "resource_status": "CREATE_COMPLETE",
+      "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+      "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+    }
+  ]
+}`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events`
+// on the test handler mux that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events", 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, output)
+		case "93940999-7d40-44ae-8de4-19624e7b8d18":
+			fmt.Fprintf(w, `{"events":[]}`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// ListResourceEventsExpected represents the expected object from a ListResourceEvents request.
+var ListResourceEventsExpected = []Event{
+	Event{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "resource",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_IN_PROGRESS",
+		PhysicalResourceID:   "",
+		ID:                   "06feb26f-9298-4a9b-8749-9d770e5d577a",
+	},
+	Event{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "resource",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_COMPLETE",
+		PhysicalResourceID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		ID:                   "93940999-7d40-44ae-8de4-19624e7b8d18",
+	},
+}
+
+// ListResourceEventsOutput represents the response body from a ListResourceEvents request.
+const ListResourceEventsOutput = `
+{
+  "events": [
+  {
+    "resource_name": "hello_world",
+    "event_time": "2015-02-05T21:33:11",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "resource"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": null,
+    "id": "06feb26f-9298-4a9b-8749-9d770e5d577a"
+    },
+    {
+      "resource_name": "hello_world",
+      "event_time": "2015-02-05T21:33:27",
+      "links": [
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+        "rel": "self"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+        "rel": "resource"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+        "rel": "stack"
+      }
+      ],
+      "logical_resource_id": "hello_world",
+      "resource_status_reason": "state changed",
+      "resource_status": "CREATE_COMPLETE",
+      "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+      "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+    }
+  ]
+}`
+
+// HandleListResourceEventsSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events`
+// on the test handler mux that responds with a `ListResourceEvents` response.
+func HandleListResourceEventsSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events", 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, output)
+		case "93940999-7d40-44ae-8de4-19624e7b8d18":
+			fmt.Fprintf(w, `{"events":[]}`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &Event{
+	ResourceName: "hello_world",
+	Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+			Rel:  "self",
+		},
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+			Rel:  "resource",
+		},
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+			Rel:  "stack",
+		},
+	},
+	LogicalResourceID:    "hello_world",
+	ResourceStatusReason: "state changed",
+	ResourceStatus:       "CREATE_COMPLETE",
+	PhysicalResourceID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+	ID:                   "93940999-7d40-44ae-8de4-19624e7b8d18",
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+  "event":{
+    "resource_name": "hello_world",
+    "event_time": "2015-02-05T21:33:27",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "resource"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "resource_status": "CREATE_COMPLETE",
+    "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+    "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+  }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
diff --git a/openstack/orchestration/v1/stackevents/requests.go b/openstack/orchestration/v1/stackevents/requests.go
new file mode 100644
index 0000000..70c6b97
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/requests.go
@@ -0,0 +1,203 @@
+package stackevents
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retrieves stack events for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) FindResult {
+	var res FindResult
+
+	_, res.Err = c.Request("GET", findURL(c, stackName), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+	})
+	return res
+}
+
+// SortDir is a type for specifying in which direction to sort a list of events.
+type SortDir string
+
+// SortKey is a type for specifying by which key to sort a list of events.
+type SortKey string
+
+// ResourceStatus is a type for specifying by which resource status to filter a
+// list of events.
+type ResourceStatus string
+
+// ResourceAction is a type for specifying by which resource action to filter a
+// list of events.
+type ResourceAction string
+
+var (
+	// ResourceStatusInProgress is used to filter a List request by the 'IN_PROGRESS' status.
+	ResourceStatusInProgress ResourceStatus = "IN_PROGRESS"
+	// ResourceStatusComplete is used to filter a List request by the 'COMPLETE' status.
+	ResourceStatusComplete ResourceStatus = "COMPLETE"
+	// ResourceStatusFailed is used to filter a List request by the 'FAILED' status.
+	ResourceStatusFailed ResourceStatus = "FAILED"
+
+	// ResourceActionCreate is used to filter a List request by the 'CREATE' action.
+	ResourceActionCreate ResourceAction = "CREATE"
+	// ResourceActionDelete is used to filter a List request by the 'DELETE' action.
+	ResourceActionDelete ResourceAction = "DELETE"
+	// ResourceActionUpdate is used to filter a List request by the 'UPDATE' action.
+	ResourceActionUpdate ResourceAction = "UPDATE"
+	// ResourceActionRollback is used to filter a List request by the 'ROLLBACK' action.
+	ResourceActionRollback ResourceAction = "ROLLBACK"
+	// ResourceActionSuspend is used to filter a List request by the 'SUSPEND' action.
+	ResourceActionSuspend ResourceAction = "SUSPEND"
+	// ResourceActionResume is used to filter a List request by the 'RESUME' action.
+	ResourceActionResume ResourceAction = "RESUME"
+	// ResourceActionAbandon is used to filter a List request by the 'ABANDON' action.
+	ResourceActionAbandon ResourceAction = "ABANDON"
+
+	// SortAsc is used to sort a list of stacks in ascending order.
+	SortAsc SortDir = "asc"
+	// SortDesc is used to sort a list of stacks in descending order.
+	SortDesc SortDir = "desc"
+
+	// SortName is used to sort a list of stacks by name.
+	SortName SortKey = "name"
+	// SortResourceType is used to sort a list of stacks by resource type.
+	SortResourceType SortKey = "resource_type"
+	// SortCreatedAt is used to sort a list of stacks by date created.
+	SortCreatedAt SortKey = "created_at"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToStackEventListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+	// The stack resource ID with which to start the listing.
+	Marker string `q:"marker"`
+	// Integer value for the limit of values to return.
+	Limit int `q:"limit"`
+	// Filters the event list by the specified ResourceAction. You can use this
+	// filter multiple times to filter by multiple resource actions: CREATE, DELETE,
+	// UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT.
+	ResourceActions []ResourceAction `q:"resource_action"`
+	// Filters the event list by the specified resource_status. You can use this
+	// filter multiple times to filter by multiple resource statuses: IN_PROGRESS,
+	// COMPLETE or FAILED.
+	ResourceStatuses []ResourceStatus `q:"resource_status"`
+	// Filters the event list by the specified resource_name. You can use this
+	// filter multiple times to filter by multiple resource names.
+	ResourceNames []string `q:"resource_name"`
+	// Filters the event list by the specified resource_type. You can use this
+	// filter multiple times to filter by multiple resource types: OS::Nova::Server,
+	// OS::Cinder::Volume, and so on.
+	ResourceTypes []string `q:"resource_type"`
+	// Sorts the event list by: resource_type or created_at.
+	SortKey SortKey `q:"sort_keys"`
+	// The sort direction of the event list. Which is asc (ascending) or desc (descending).
+	SortDir SortDir `q:"sort_dir"`
+}
+
+// ToStackEventListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToStackEventListQuery() (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 resources for the given stack.
+func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client, stackName, stackID)
+
+	if opts != nil {
+		query, err := opts.ToStackEventListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		p := EventPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// ListResourceEventsOptsBuilder allows extensions to add additional parameters to the
+// ListResourceEvents request.
+type ListResourceEventsOptsBuilder interface {
+	ToResourceEventListQuery() (string, error)
+}
+
+// ListResourceEventsOpts allows the filtering and sorting of paginated resource events through
+// the API. Marker and Limit are used for pagination.
+type ListResourceEventsOpts struct {
+	// The stack resource ID with which to start the listing.
+	Marker string `q:"marker"`
+	// Integer value for the limit of values to return.
+	Limit int `q:"limit"`
+	// Filters the event list by the specified ResourceAction. You can use this
+	// filter multiple times to filter by multiple resource actions: CREATE, DELETE,
+	// UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT.
+	ResourceActions []string `q:"resource_action"`
+	// Filters the event list by the specified resource_status. You can use this
+	// filter multiple times to filter by multiple resource statuses: IN_PROGRESS,
+	// COMPLETE or FAILED.
+	ResourceStatuses []string `q:"resource_status"`
+	// Filters the event list by the specified resource_name. You can use this
+	// filter multiple times to filter by multiple resource names.
+	ResourceNames []string `q:"resource_name"`
+	// Filters the event list by the specified resource_type. You can use this
+	// filter multiple times to filter by multiple resource types: OS::Nova::Server,
+	// OS::Cinder::Volume, and so on.
+	ResourceTypes []string `q:"resource_type"`
+	// Sorts the event list by: resource_type or created_at.
+	SortKey SortKey `q:"sort_keys"`
+	// The sort direction of the event list. Which is asc (ascending) or desc (descending).
+	SortDir SortDir `q:"sort_dir"`
+}
+
+// ToResourceEventListQuery formats a ListResourceEventsOpts into a query string.
+func (opts ListResourceEventsOpts) ToResourceEventListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListResourceEvents makes a request against the API to list resources for the given stack.
+func ListResourceEvents(client *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts ListResourceEventsOptsBuilder) pagination.Pager {
+	url := listResourceEventsURL(client, stackName, stackID, resourceName)
+
+	if opts != nil {
+		query, err := opts.ToResourceEventListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		p := EventPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c, stackName, stackID, resourceName, eventID), &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
diff --git a/openstack/orchestration/v1/stackevents/requests_test.go b/openstack/orchestration/v1/stackevents/requests_test.go
new file mode 100644
index 0000000..a4da4d0
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/requests_test.go
@@ -0,0 +1,71 @@
+package stackevents
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindEvents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleFindSuccessfully(t, FindOutput)
+
+	actual, err := Find(fake.ServiceClient(), "postman_stack").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := FindExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t, ListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractEvents(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListResourceEvents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListResourceEventsSuccessfully(t, ListResourceEventsOutput)
+
+	count := 0
+	err := ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractEvents(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListResourceEventsExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetEvent(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stackevents/results.go b/openstack/orchestration/v1/stackevents/results.go
new file mode 100644
index 0000000..cf9e240
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/results.go
@@ -0,0 +1,172 @@
+package stackevents
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Event represents a stack event.
+type Event struct {
+	// The name of the resource for which the event occurred.
+	ResourceName string `mapstructure:"resource_name"`
+	// The time the event occurred.
+	Time time.Time `mapstructure:"-"`
+	// The URLs to the event.
+	Links []gophercloud.Link `mapstructure:"links"`
+	// The logical ID of the stack resource.
+	LogicalResourceID string `mapstructure:"logical_resource_id"`
+	// The reason of the status of the event.
+	ResourceStatusReason string `mapstructure:"resource_status_reason"`
+	// The status of the event.
+	ResourceStatus string `mapstructure:"resource_status"`
+	// The physical ID of the stack resource.
+	PhysicalResourceID string `mapstructure:"physical_resource_id"`
+	// The event ID.
+	ID string `mapstructure:"id"`
+	// Properties of the stack resource.
+	ResourceProperties map[string]interface{} `mapstructure:"resource_properties"`
+}
+
+// FindResult represents the result of a Find operation.
+type FindResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a slice of Event objects and is called after a
+// Find operation.
+func (r FindResult) Extract() ([]Event, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Res []Event `mapstructure:"events"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	events := r.Body.(map[string]interface{})["events"].([]interface{})
+
+	for i, eventRaw := range events {
+		event := eventRaw.(map[string]interface{})
+		if date, ok := event["event_time"]; ok && date != nil {
+			t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			res.Res[i].Time = t
+		}
+	}
+
+	return res.Res, nil
+}
+
+// EventPage 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 ExtractResources call.
+type EventPage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (r EventPage) IsEmpty() (bool, error) {
+	events, err := ExtractEvents(r)
+	if err != nil {
+		return true, err
+	}
+	return len(events) == 0, nil
+}
+
+// LastMarker returns the last stack ID in a ListResult.
+func (r EventPage) LastMarker() (string, error) {
+	events, err := ExtractEvents(r)
+	if err != nil {
+		return "", err
+	}
+	if len(events) == 0 {
+		return "", nil
+	}
+	return events[len(events)-1].ID, nil
+}
+
+// ExtractEvents interprets the results of a single page from a List() call, producing a slice of Event entities.
+func ExtractEvents(page pagination.Page) ([]Event, error) {
+	casted := page.(EventPage).Body
+
+	var res struct {
+		Res []Event `mapstructure:"events"`
+	}
+
+	if err := mapstructure.Decode(casted, &res); err != nil {
+		return nil, err
+	}
+
+	var events []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		events = casted.(map[string]interface{})["events"].([]interface{})
+	case map[string][]interface{}:
+		events = casted.(map[string][]interface{})["events"]
+	default:
+		return res.Res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i, eventRaw := range events {
+		event := eventRaw.(map[string]interface{})
+		if date, ok := event["event_time"]; ok && date != nil {
+			t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			res.Res[i].Time = t
+		}
+	}
+
+	return res.Res, nil
+}
+
+// ExtractResourceEvents interprets the results of a single page from a
+// ListResourceEvents() call, producing a slice of Event entities.
+func ExtractResourceEvents(page pagination.Page) ([]Event, error) {
+	return ExtractEvents(page)
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to an Event object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*Event, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Res *Event `mapstructure:"event"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	event := r.Body.(map[string]interface{})["event"].(map[string]interface{})
+
+	if date, ok := event["event_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Res.Time = t
+	}
+
+	return res.Res, nil
+}
diff --git a/openstack/orchestration/v1/stackevents/urls.go b/openstack/orchestration/v1/stackevents/urls.go
new file mode 100644
index 0000000..8b5eceb
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/urls.go
@@ -0,0 +1,19 @@
+package stackevents
+
+import "github.com/rackspace/gophercloud"
+
+func findURL(c *gophercloud.ServiceClient, stackName string) string {
+	return c.ServiceURL("stacks", stackName, "events")
+}
+
+func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "events")
+}
+
+func listResourceEventsURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events")
+}
+
+func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events", eventID)
+}
diff --git a/openstack/orchestration/v1/stackresources/doc.go b/openstack/orchestration/v1/stackresources/doc.go
new file mode 100644
index 0000000..e4f8b08
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/doc.go
@@ -0,0 +1,5 @@
+// Package stackresources provides operations for working with stack resources.
+// A resource is a template artifact that represents some component of your
+// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load
+// balancer, some configuration management system, and so forth).
+package stackresources
diff --git a/openstack/orchestration/v1/stackresources/fixtures.go b/openstack/orchestration/v1/stackresources/fixtures.go
new file mode 100644
index 0000000..952dc54
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/fixtures.go
@@ -0,0 +1,439 @@
+package stackresources
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// FindExpected represents the expected object from a Find request.
+var FindExpected = []Resource{
+	Resource{
+		Name: "hello_world",
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalID:    "hello_world",
+		StatusReason: "state changed",
+		UpdatedTime:  time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC),
+		RequiredBy:   []interface{}{},
+		Status:       "CREATE_IN_PROGRESS",
+		PhysicalID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		Type:         "OS::Nova::Server",
+		Attributes:   map[string]interface{}{"SXSW": "atx"},
+		Description:  "Some resource",
+	},
+}
+
+// FindOutput represents the response body from a Find request.
+const FindOutput = `
+{
+  "resources": [
+  {
+  	"description": "Some resource",
+  	"attributes": {"SXSW": "atx"},
+    "resource_name": "hello_world",
+    "links": [
+      {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "self"
+      },
+      {
+        "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+        "rel": "stack"
+      }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "updated_time": "2015-02-05T21:33:11",
+	"creation_time": "2015-02-05T21:33:10",
+    "required_by": [],
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+    "resource_type": "OS::Nova::Server"
+  }
+  ]
+}`
+
+// HandleFindSuccessfully creates an HTTP handler at `/stacks/hello_world/resources`
+// on the test handler mux that responds with a `Find` response.
+func HandleFindSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/resources", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// ListExpected represents the expected object from a List request.
+var ListExpected = []Resource{
+	Resource{
+		Name: "hello_world",
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+				Rel:  "stack",
+			},
+		},
+		LogicalID:    "hello_world",
+		StatusReason: "state changed",
+		UpdatedTime:  time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		CreationTime: time.Date(2015, 2, 5, 21, 33, 10, 0, time.UTC),
+		RequiredBy:   []interface{}{},
+		Status:       "CREATE_IN_PROGRESS",
+		PhysicalID:   "49181cd6-169a-4130-9455-31185bbfc5bf",
+		Type:         "OS::Nova::Server",
+		Attributes:   map[string]interface{}{"SXSW": "atx"},
+		Description:  "Some resource",
+	},
+}
+
+// ListOutput represents the response body from a List request.
+const ListOutput = `{
+  "resources": [
+  {
+    "resource_name": "hello_world",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "hello_world",
+    "resource_status_reason": "state changed",
+    "updated_time": "2015-02-05T21:33:11",
+    "required_by": [],
+    "resource_status": "CREATE_IN_PROGRESS",
+    "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+	"creation_time": "2015-02-05T21:33:10",
+    "resource_type": "OS::Nova::Server",
+	"attributes": {"SXSW": "atx"},
+	"description": "Some resource"
+  }
+]
+}`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources`
+// on the test handler mux that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources", 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, output)
+		case "49181cd6-169a-4130-9455-31185bbfc5bf":
+			fmt.Fprintf(w, `{"resources":[]}`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &Resource{
+	Name: "wordpress_instance",
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance",
+			Rel:  "self",
+		},
+		gophercloud.Link{
+			Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e",
+			Rel:  "stack",
+		},
+	},
+	LogicalID:    "wordpress_instance",
+	Attributes:   map[string]interface{}{"SXSW": "atx"},
+	StatusReason: "state changed",
+	UpdatedTime:  time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC),
+	RequiredBy:   []interface{}{},
+	Status:       "CREATE_COMPLETE",
+	PhysicalID:   "00e3a2fe-c65d-403c-9483-4db9930dd194",
+	Type:         "OS::Nova::Server",
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+  "resource": {
+    "description": "Some resource",
+    "attributes": {"SXSW": "atx"},
+    "resource_name": "wordpress_instance",
+    "description": "",
+    "links": [
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance",
+      "rel": "self"
+    },
+    {
+      "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e",
+      "rel": "stack"
+    }
+    ],
+    "logical_resource_id": "wordpress_instance",
+    "resource_status": "CREATE_COMPLETE",
+    "updated_time": "2014-12-10T18:34:35",
+    "required_by": [],
+    "resource_status_reason": "state changed",
+    "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194",
+    "resource_type": "OS::Nova::Server"
+  }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// MetadataExpected represents the expected object from a Metadata request.
+var MetadataExpected = map[string]string{
+	"number": "7",
+	"animal": "auk",
+}
+
+// MetadataOutput represents the response body from a Metadata request.
+const MetadataOutput = `
+{
+    "metadata": {
+      "number": "7",
+      "animal": "auk"
+    }
+}`
+
+// HandleMetadataSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata`
+// on the test handler mux that responds with a `Metadata` response.
+func HandleMetadataSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// ListTypesExpected represents the expected object from a ListTypes request.
+var ListTypesExpected = ResourceTypes{
+	"OS::Nova::Server",
+	"OS::Heat::RandomString",
+	"OS::Swift::Container",
+	"OS::Trove::Instance",
+	"OS::Nova::FloatingIPAssociation",
+	"OS::Cinder::VolumeAttachment",
+	"OS::Nova::FloatingIP",
+	"OS::Nova::KeyPair",
+}
+
+// same as above, but sorted
+var SortedListTypesExpected = ResourceTypes{
+	"OS::Cinder::VolumeAttachment",
+	"OS::Heat::RandomString",
+	"OS::Nova::FloatingIP",
+	"OS::Nova::FloatingIPAssociation",
+	"OS::Nova::KeyPair",
+	"OS::Nova::Server",
+	"OS::Swift::Container",
+	"OS::Trove::Instance",
+}
+
+// ListTypesOutput represents the response body from a ListTypes request.
+const ListTypesOutput = `
+{
+  "resource_types": [
+    "OS::Nova::Server",
+    "OS::Heat::RandomString",
+    "OS::Swift::Container",
+    "OS::Trove::Instance",
+    "OS::Nova::FloatingIPAssociation",
+    "OS::Cinder::VolumeAttachment",
+    "OS::Nova::FloatingIP",
+    "OS::Nova::KeyPair"
+  ]
+}`
+
+// HandleListTypesSuccessfully creates an HTTP handler at `/resource_types`
+// on the test handler mux that responds with a `ListTypes` response.
+func HandleListTypesSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/resource_types", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// GetSchemaExpected represents the expected object from a Schema request.
+var GetSchemaExpected = &TypeSchema{
+	Attributes: map[string]interface{}{
+		"an_attribute": map[string]interface{}{
+			"description": "An attribute description .",
+		},
+	},
+	Properties: map[string]interface{}{
+		"a_property": map[string]interface{}{
+			"update_allowed": false,
+			"required":       true,
+			"type":           "string",
+			"description":    "A resource description.",
+		},
+	},
+	ResourceType: "OS::Heat::AResourceName",
+	SupportStatus: map[string]interface{}{
+		"message": "A status message",
+		"status":  "SUPPORTED",
+		"version": "2014.1",
+	},
+}
+
+// GetSchemaOutput represents the response body from a Schema request.
+const GetSchemaOutput = `
+{
+  "attributes": {
+    "an_attribute": {
+      "description": "An attribute description ."
+    }
+  },
+  "properties": {
+    "a_property": {
+      "update_allowed": false,
+      "required": true,
+      "type": "string",
+      "description": "A resource description."
+    }
+  },
+  "resource_type": "OS::Heat::AResourceName",
+  "support_status": {
+	"message": "A status message",
+	"status": "SUPPORTED",
+	"version": "2014.1"
+  }
+}`
+
+// HandleGetSchemaSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName`
+// on the test handler mux that responds with a `Schema` response.
+func HandleGetSchemaSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// GetTemplateExpected represents the expected object from a Template request.
+var GetTemplateExpected = "{\n  \"HeatTemplateFormatVersion\": \"2012-12-12\",\n  \"Outputs\": {\n    \"private_key\": {\n      \"Description\": \"The private key if it has been saved.\",\n      \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"private_key\\\"]}\"\n    },\n    \"public_key\": {\n      \"Description\": \"The public key.\",\n      \"Value\": \"{\\\"Fn::GetAtt\\\": [\\\"KeyPair\\\", \\\"public_key\\\"]}\"\n    }\n  },\n  \"Parameters\": {\n    \"name\": {\n      \"Description\": \"The name of the key pair.\",\n      \"Type\": \"String\"\n    },\n    \"public_key\": {\n      \"Description\": \"The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.\",\n      \"Type\": \"String\"\n    },\n    \"save_private_key\": {\n      \"AllowedValues\": [\n        \"True\",\n        \"true\",\n        \"False\",\n        \"false\"\n      ],\n      \"Default\": false,\n      \"Description\": \"True if the system should remember a generated private key; False otherwise.\",\n      \"Type\": \"String\"\n    }\n  },\n  \"Resources\": {\n    \"KeyPair\": {\n      \"Properties\": {\n        \"name\": {\n          \"Ref\": \"name\"\n        },\n        \"public_key\": {\n          \"Ref\": \"public_key\"\n        },\n        \"save_private_key\": {\n          \"Ref\": \"save_private_key\"\n        }\n      },\n      \"Type\": \"OS::Nova::KeyPair\"\n    }\n  }\n}"
+
+// GetTemplateOutput represents the response body from a Template request.
+const GetTemplateOutput = `
+{
+  "HeatTemplateFormatVersion": "2012-12-12",
+  "Outputs": {
+    "private_key": {
+      "Description": "The private key if it has been saved.",
+      "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}"
+    },
+    "public_key": {
+      "Description": "The public key.",
+      "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}"
+    }
+  },
+  "Parameters": {
+    "name": {
+      "Description": "The name of the key pair.",
+      "Type": "String"
+    },
+    "public_key": {
+      "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.",
+      "Type": "String"
+    },
+    "save_private_key": {
+      "AllowedValues": [
+      "True",
+      "true",
+      "False",
+      "false"
+      ],
+      "Default": false,
+      "Description": "True if the system should remember a generated private key; False otherwise.",
+      "Type": "String"
+    }
+  },
+  "Resources": {
+    "KeyPair": {
+      "Properties": {
+        "name": {
+          "Ref": "name"
+        },
+        "public_key": {
+          "Ref": "public_key"
+        },
+        "save_private_key": {
+          "Ref": "save_private_key"
+        }
+      },
+      "Type": "OS::Nova::KeyPair"
+    }
+  }
+}`
+
+// HandleGetTemplateSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName/template`
+// on the test handler mux that responds with a `Template` response.
+func HandleGetTemplateSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName/template", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
diff --git a/openstack/orchestration/v1/stackresources/requests.go b/openstack/orchestration/v1/stackresources/requests.go
new file mode 100644
index 0000000..fcb8d8a
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/requests.go
@@ -0,0 +1,113 @@
+package stackresources
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retrieves stack resources for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) FindResult {
+	var res FindResult
+
+	// Send request to API
+	_, res.Err = c.Request("GET", findURL(c, stackName), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+	})
+	return res
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToStackResourceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+	// Include resources from nest stacks up to Depth levels of recursion.
+	Depth int `q:"nested_depth"`
+}
+
+// ToStackResourceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToStackResourceListQuery() (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 resources for the given stack.
+func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client, stackName, stackID)
+
+	if opts != nil {
+		query, err := opts.ToStackResourceListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return ResourcePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) GetResult {
+	var res GetResult
+
+	// Send request to API
+	_, res.Err = c.Get(getURL(c, stackName, stackID, resourceName), &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Metadata retreives the metadata for the given stack resource.
+func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) MetadataResult {
+	var res MetadataResult
+
+	// Send request to API
+	_, res.Err = c.Get(metadataURL(c, stackName, stackID, resourceName), &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(client *gophercloud.ServiceClient) pagination.Pager {
+	url := listTypesURL(client)
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return ResourceTypePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// Schema retreives the schema for the given resource type.
+func Schema(c *gophercloud.ServiceClient, resourceType string) SchemaResult {
+	var res SchemaResult
+
+	// Send request to API
+	_, res.Err = c.Get(schemaURL(c, resourceType), &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Template retreives the template representation for the given resource type.
+func Template(c *gophercloud.ServiceClient, resourceType string) TemplateResult {
+	var res TemplateResult
+
+	// Send request to API
+	_, res.Err = c.Get(templateURL(c, resourceType), &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
diff --git a/openstack/orchestration/v1/stackresources/requests_test.go b/openstack/orchestration/v1/stackresources/requests_test.go
new file mode 100644
index 0000000..e5045a7
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/requests_test.go
@@ -0,0 +1,111 @@
+package stackresources
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleFindSuccessfully(t, FindOutput)
+
+	actual, err := Find(fake.ServiceClient(), "hello_world").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := FindExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t, ListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractResources(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetResource(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResourceMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMetadataSuccessfully(t, MetadataOutput)
+
+	actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := MetadataExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResourceTypes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListTypesSuccessfully(t, ListTypesOutput)
+
+	count := 0
+	err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractResourceTypes(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListTypesExpected, actual)
+		// test if sorting works
+		sort.Sort(actual)
+		th.CheckDeepEquals(t, SortedListTypesExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGetResourceSchema(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSchemaSuccessfully(t, GetSchemaOutput)
+
+	actual, err := Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetSchemaExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestGetResourceTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetTemplateSuccessfully(t, GetTemplateOutput)
+
+	actual, err := Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetTemplateExpected
+	th.AssertDeepEquals(t, expected, string(actual))
+}
diff --git a/openstack/orchestration/v1/stackresources/results.go b/openstack/orchestration/v1/stackresources/results.go
new file mode 100644
index 0000000..6ddc766
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/results.go
@@ -0,0 +1,284 @@
+package stackresources
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Resource represents a stack resource.
+type Resource struct {
+	Attributes   map[string]interface{} `mapstructure:"attributes"`
+	CreationTime time.Time              `mapstructure:"-"`
+	Description  string                 `mapstructure:"description"`
+	Links        []gophercloud.Link     `mapstructure:"links"`
+	LogicalID    string                 `mapstructure:"logical_resource_id"`
+	Name         string                 `mapstructure:"resource_name"`
+	PhysicalID   string                 `mapstructure:"physical_resource_id"`
+	RequiredBy   []interface{}          `mapstructure:"required_by"`
+	Status       string                 `mapstructure:"resource_status"`
+	StatusReason string                 `mapstructure:"resource_status_reason"`
+	Type         string                 `mapstructure:"resource_type"`
+	UpdatedTime  time.Time              `mapstructure:"-"`
+}
+
+// FindResult represents the result of a Find operation.
+type FindResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a slice of Resource objects and is called after a
+// Find operation.
+func (r FindResult) Extract() ([]Resource, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Res []Resource `mapstructure:"resources"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	resources := r.Body.(map[string]interface{})["resources"].([]interface{})
+
+	for i, resourceRaw := range resources {
+		resource := resourceRaw.(map[string]interface{})
+		if date, ok := resource["updated_time"]; ok && date != nil {
+			t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			res.Res[i].UpdatedTime = t
+		}
+		if date, ok := resource["creation_time"]; ok && date != nil {
+			t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			res.Res[i].CreationTime = t
+		}
+	}
+
+	return res.Res, nil
+}
+
+// ResourcePage 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 ExtractResources call.
+type ResourcePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (r ResourcePage) IsEmpty() (bool, error) {
+	resources, err := ExtractResources(r)
+	if err != nil {
+		return true, err
+	}
+	return len(resources) == 0, nil
+}
+
+// ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities.
+func ExtractResources(page pagination.Page) ([]Resource, error) {
+	casted := page.(ResourcePage).Body
+
+	var response struct {
+		Resources []Resource `mapstructure:"resources"`
+	}
+	if err := mapstructure.Decode(casted, &response); err != nil {
+		return nil, err
+	}
+	var resources []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		resources = casted.(map[string]interface{})["resources"].([]interface{})
+	case map[string][]interface{}:
+		resources = casted.(map[string][]interface{})["resources"]
+	default:
+		return response.Resources, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i, resourceRaw := range resources {
+		resource := resourceRaw.(map[string]interface{})
+		if date, ok := resource["updated_time"]; ok && date != nil {
+			t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			response.Resources[i].UpdatedTime = t
+		}
+		if date, ok := resource["creation_time"]; ok && date != nil {
+			t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			response.Resources[i].CreationTime = t
+		}
+	}
+
+	return response.Resources, nil
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a Resource object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*Resource, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Res *Resource `mapstructure:"resource"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{})
+
+	if date, ok := resource["updated_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Res.UpdatedTime = t
+	}
+	if date, ok := resource["creation_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Res.CreationTime = t
+	}
+
+	return res.Res, nil
+}
+
+// MetadataResult represents the result of a Metadata operation.
+type MetadataResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a map object and is called after a
+// Metadata operation.
+func (r MetadataResult) Extract() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Meta map[string]string `mapstructure:"metadata"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	return res.Meta, nil
+}
+
+// ResourceTypePage abstracts the raw results of making a ListTypes() 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 ExtractResourceTypes call.
+type ResourceTypePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ResourceTypePage contains no resource types.
+func (r ResourceTypePage) IsEmpty() (bool, error) {
+	rts, err := ExtractResourceTypes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(rts) == 0, nil
+}
+
+// ResourceTypes represents the type that holds the result of ExtractResourceTypes.
+// We define methods on this type to sort it before output
+type ResourceTypes []string
+
+func (r ResourceTypes) Len() int {
+	return len(r)
+}
+
+func (r ResourceTypes) Swap(i, j int) {
+	r[i], r[j] = r[j], r[i]
+}
+
+func (r ResourceTypes) Less(i, j int) bool {
+	return r[i] < r[j]
+}
+
+// ExtractResourceTypes extracts and returns resource types.
+func ExtractResourceTypes(page pagination.Page) (ResourceTypes, error) {
+	casted := page.(ResourceTypePage).Body
+
+	var response struct {
+		ResourceTypes ResourceTypes `mapstructure:"resource_types"`
+	}
+
+	if err := mapstructure.Decode(casted, &response); err != nil {
+		return nil, err
+	}
+	return response.ResourceTypes, nil
+}
+
+// TypeSchema represents a stack resource schema.
+type TypeSchema struct {
+	Attributes    map[string]interface{} `mapstructure:"attributes"`
+	Properties    map[string]interface{} `mapstrucutre:"properties"`
+	ResourceType  string                 `mapstructure:"resource_type"`
+	SupportStatus map[string]interface{} `mapstructure:"support_status"`
+}
+
+// SchemaResult represents the result of a Schema operation.
+type SchemaResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a TypeSchema object and is called after a
+// Schema operation.
+func (r SchemaResult) Extract() (*TypeSchema, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res TypeSchema
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	return &res, nil
+}
+
+// TemplateResult represents the result of a Template operation.
+type TemplateResult struct {
+	gophercloud.Result
+}
+
+// Extract returns the template and is called after a
+// Template operation.
+func (r TemplateResult) Extract() ([]byte, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	template, err := json.MarshalIndent(r.Body, "", "  ")
+	if err != nil {
+		return nil, err
+	}
+	return template, nil
+}
diff --git a/openstack/orchestration/v1/stackresources/urls.go b/openstack/orchestration/v1/stackresources/urls.go
new file mode 100644
index 0000000..ef078d9
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/urls.go
@@ -0,0 +1,31 @@
+package stackresources
+
+import "github.com/rackspace/gophercloud"
+
+func findURL(c *gophercloud.ServiceClient, stackName string) string {
+	return c.ServiceURL("stacks", stackName, "resources")
+}
+
+func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "resources")
+}
+
+func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "metadata")
+}
+
+func listTypesURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("resource_types")
+}
+
+func schemaURL(c *gophercloud.ServiceClient, typeName string) string {
+	return c.ServiceURL("resource_types", typeName)
+}
+
+func templateURL(c *gophercloud.ServiceClient, typeName string) string {
+	return c.ServiceURL("resource_types", typeName, "template")
+}
diff --git a/openstack/orchestration/v1/stacks/doc.go b/openstack/orchestration/v1/stacks/doc.go
new file mode 100644
index 0000000..19231b5
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/doc.go
@@ -0,0 +1,8 @@
+// Package stacks provides operation for working with Heat stacks. A stack is a
+// group of resources (servers, load balancers, databases, and so forth)
+// combined to fulfill a useful purpose. Based on a template, Heat orchestration
+// engine creates an instantiated set of resources (a stack) to run the
+// application framework or component specified (in the template). A stack is a
+// running instance of a template. The result of creating a stack is a deployment
+// of the application framework or component.
+package stacks
diff --git a/openstack/orchestration/v1/stacks/environment.go b/openstack/orchestration/v1/stacks/environment.go
new file mode 100644
index 0000000..abaff20
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/environment.go
@@ -0,0 +1,137 @@
+package stacks
+
+import (
+	"fmt"
+	"strings"
+)
+
+// Environment is a structure that represents stack environments
+type Environment struct {
+	TE
+}
+
+// EnvironmentSections is a map containing allowed sections in a stack environment file
+var EnvironmentSections = map[string]bool{
+	"parameters":         true,
+	"parameter_defaults": true,
+	"resource_registry":  true,
+}
+
+// Validate validates the contents of the Environment
+func (e *Environment) Validate() error {
+	if e.Parsed == nil {
+		if err := e.Parse(); err != nil {
+			return err
+		}
+	}
+	for key := range e.Parsed {
+		if _, ok := EnvironmentSections[key]; !ok {
+			return fmt.Errorf("Environment has wrong section: %s", key)
+		}
+	}
+	return nil
+}
+
+// Parse environment file to resolve the URL's of the resources. This is done by
+// reading from the `Resource Registry` section, which is why the function is
+// named GetRRFileContents.
+func (e *Environment) getRRFileContents(ignoreIf igFunc) error {
+	// initialize environment if empty
+	if e.Files == nil {
+		e.Files = make(map[string]string)
+	}
+	if e.fileMaps == nil {
+		e.fileMaps = make(map[string]string)
+	}
+
+	// get the resource registry
+	rr := e.Parsed["resource_registry"]
+
+	// search the resource registry for URLs
+	switch rr.(type) {
+	// process further only if the resource registry is a map
+	case map[string]interface{}, map[interface{}]interface{}:
+		rrMap, err := toStringKeys(rr)
+		if err != nil {
+			return err
+		}
+		// the resource registry might contain a base URL for the resource. If
+		// such a field is present, use it. Otherwise, use the default base URL.
+		var baseURL string
+		if val, ok := rrMap["base_url"]; ok {
+			baseURL = val.(string)
+		} else {
+			baseURL = e.baseURL
+		}
+
+		// The contents of the resource may be located in a remote file, which
+		// will be a template. Instantiate a temporary template to manage the
+		// contents.
+		tempTemplate := new(Template)
+		tempTemplate.baseURL = baseURL
+		tempTemplate.client = e.client
+
+		// Fetch the contents of remote resource URL's
+		if err = tempTemplate.getFileContents(rr, ignoreIf, false); err != nil {
+			return err
+		}
+		// check the `resources` section (if it exists) for more URL's. Note that
+		// the previous call to GetFileContents was (deliberately) not recursive
+		// as we want more control over where to look for URL's
+		if val, ok := rrMap["resources"]; ok {
+			switch val.(type) {
+			// process further only if the contents are a map
+			case map[string]interface{}, map[interface{}]interface{}:
+				resourcesMap, err := toStringKeys(val)
+				if err != nil {
+					return err
+				}
+				for _, v := range resourcesMap {
+					switch v.(type) {
+					case map[string]interface{}, map[interface{}]interface{}:
+						resourceMap, err := toStringKeys(v)
+						if err != nil {
+							return err
+						}
+						var resourceBaseURL string
+						// if base_url for the resource type is defined, use it
+						if val, ok := resourceMap["base_url"]; ok {
+							resourceBaseURL = val.(string)
+						} else {
+							resourceBaseURL = baseURL
+						}
+						tempTemplate.baseURL = resourceBaseURL
+						if err := tempTemplate.getFileContents(v, ignoreIf, false); err != nil {
+							return err
+						}
+					}
+				}
+			}
+		}
+		// if the resource registry contained any URL's, store them. This can
+		// then be passed as parameter to api calls to Heat api.
+		e.Files = tempTemplate.Files
+		return nil
+	default:
+		return nil
+	}
+}
+
+// function to choose keys whose values are other environment files
+func ignoreIfEnvironment(key string, value interface{}) bool {
+	// base_url and hooks refer to components which cannot have urls
+	if key == "base_url" || key == "hooks" {
+		return true
+	}
+	// if value is not string, it cannot be a URL
+	valueString, ok := value.(string)
+	if !ok {
+		return true
+	}
+	// if value contains `::`, it must be a reference to another resource type
+	// e.g. OS::Nova::Server : Rackspace::Cloud::Server
+	if strings.Contains(valueString, "::") {
+		return true
+	}
+	return false
+}
diff --git a/openstack/orchestration/v1/stacks/environment_test.go b/openstack/orchestration/v1/stacks/environment_test.go
new file mode 100644
index 0000000..3a3c2b9
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/environment_test.go
@@ -0,0 +1,184 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnvironmentValidation(t *testing.T) {
+	environmentJSON := new(Environment)
+	environmentJSON.Bin = []byte(ValidJSONEnvironment)
+	err := environmentJSON.Validate()
+	th.AssertNoErr(t, err)
+
+	environmentYAML := new(Environment)
+	environmentYAML.Bin = []byte(ValidYAMLEnvironment)
+	err = environmentYAML.Validate()
+	th.AssertNoErr(t, err)
+
+	environmentInvalid := new(Environment)
+	environmentInvalid.Bin = []byte(InvalidEnvironment)
+	if err = environmentInvalid.Validate(); err == nil {
+		t.Error("environment validation did not catch invalid environment")
+	}
+}
+
+func TestEnvironmentParsing(t *testing.T) {
+	environmentJSON := new(Environment)
+	environmentJSON.Bin = []byte(ValidJSONEnvironment)
+	err := environmentJSON.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentJSON.Parsed)
+
+	environmentYAML := new(Environment)
+	environmentYAML.Bin = []byte(ValidJSONEnvironment)
+	err = environmentYAML.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentYAML.Parsed)
+
+	environmentInvalid := new(Environment)
+	environmentInvalid.Bin = []byte("Keep Austin Weird")
+	err = environmentInvalid.Parse()
+	if err == nil {
+		t.Error("environment parsing did not catch invalid environment")
+	}
+}
+
+func TestIgnoreIfEnvironment(t *testing.T) {
+	var keyValueTests = []struct {
+		key   string
+		value interface{}
+		out   bool
+	}{
+		{"base_url", "afksdf", true},
+		{"not_type", "hooks", false},
+		{"get_file", "::", true},
+		{"hooks", "dfsdfsd", true},
+		{"type", "sdfubsduf.yaml", false},
+		{"type", "sdfsdufs.environment", false},
+		{"type", "sdfsdf.file", false},
+		{"type", map[string]string{"key": "value"}, true},
+	}
+	var result bool
+	for _, kv := range keyValueTests {
+		result = ignoreIfEnvironment(kv.key, kv.value)
+		if result != kv.out {
+			t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, kv.out, result)
+		}
+	}
+}
+
+func TestGetRRFileContents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	environmentContent := `
+heat_template_version: 2013-05-23
+
+description:
+  Heat WordPress template to support F18, using only Heat OpenStack-native
+  resource types, and without the requirement for heat-cfntools in the image.
+  WordPress is web software you can use to create a beautiful website or blog.
+  This template installs a single-instance WordPress deployment using a local
+  MySQL database to store the data.
+
+parameters:
+
+  key_name:
+    type: string
+    description : Name of a KeyPair to enable SSH access to the instance
+
+resources:
+  wordpress_instance:
+    type: OS::Nova::Server
+    properties:
+      image: { get_param: image_id }
+      flavor: { get_param: instance_type }
+      key_name: { get_param: key_name }`
+
+	dbContent := `
+heat_template_version: 2014-10-16
+
+description:
+  Test template for Trove resource capabilities
+
+parameters:
+  db_pass:
+    type: string
+    hidden: true
+    description: Database access password
+    default: secrete
+
+resources:
+
+service_db:
+  type: OS::Trove::Instance
+  properties:
+    name: trove_test_db
+    datastore_type: mariadb
+    flavor: 1GB Instance
+    size: 10
+    databases:
+    - name: test_data
+    users:
+    - name: kitchen_sink
+      password: { get_param: db_pass }
+      databases: [ test_data ]`
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+
+	fakeEnvURL := strings.Join([]string{baseurl, "my_env.yaml"}, "/")
+	urlparsed, err := url.Parse(fakeEnvURL)
+	th.AssertNoErr(t, err)
+	// handler for my_env.yaml
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, environmentContent)
+	})
+
+	fakeDBURL := strings.Join([]string{baseurl, "my_db.yaml"}, "/")
+	urlparsed, err = url.Parse(fakeDBURL)
+	th.AssertNoErr(t, err)
+
+	// handler for my_db.yaml
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, dbContent)
+	})
+
+	client := fakeClient{BaseClient: getHTTPClient()}
+	env := new(Environment)
+	env.Bin = []byte(`{"resource_registry": {"My::WP::Server": "my_env.yaml", "resources": {"my_db_server": {"OS::DBInstance": "my_db.yaml"}}}}`)
+	env.client = client
+
+	err = env.Parse()
+	th.AssertNoErr(t, err)
+	err = env.getRRFileContents(ignoreIfEnvironment)
+	th.AssertNoErr(t, err)
+	expectedEnvFilesContent := "\nheat_template_version: 2013-05-23\n\ndescription:\n  Heat WordPress template to support F18, using only Heat OpenStack-native\n  resource types, and without the requirement for heat-cfntools in the image.\n  WordPress is web software you can use to create a beautiful website or blog.\n  This template installs a single-instance WordPress deployment using a local\n  MySQL database to store the data.\n\nparameters:\n\n  key_name:\n    type: string\n    description : Name of a KeyPair to enable SSH access to the instance\n\nresources:\n  wordpress_instance:\n    type: OS::Nova::Server\n    properties:\n      image: { get_param: image_id }\n      flavor: { get_param: instance_type }\n      key_name: { get_param: key_name }"
+	expectedDBFilesContent := "\nheat_template_version: 2014-10-16\n\ndescription:\n  Test template for Trove resource capabilities\n\nparameters:\n  db_pass:\n    type: string\n    hidden: true\n    description: Database access password\n    default: secrete\n\nresources:\n\nservice_db:\n  type: OS::Trove::Instance\n  properties:\n    name: trove_test_db\n    datastore_type: mariadb\n    flavor: 1GB Instance\n    size: 10\n    databases:\n    - name: test_data\n    users:\n    - name: kitchen_sink\n      password: { get_param: db_pass }\n      databases: [ test_data ]"
+
+	th.AssertEquals(t, expectedEnvFilesContent, env.Files[fakeEnvURL])
+	th.AssertEquals(t, expectedDBFilesContent, env.Files[fakeDBURL])
+
+	env.fixFileRefs()
+	expectedParsed := map[string]interface{}{
+		"resource_registry": "2015-04-30",
+		"My::WP::Server":    fakeEnvURL,
+		"resources": map[string]interface{}{
+			"my_db_server": map[string]interface{}{
+				"OS::DBInstance": fakeDBURL,
+			},
+		},
+	}
+	env.Parse()
+	th.AssertDeepEquals(t, expectedParsed, env.Parsed)
+}
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
new file mode 100644
index 0000000..83f5dec
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -0,0 +1,604 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// CreateExpected represents the expected object from a Create request.
+var CreateExpected = &CreatedStack{
+	ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+			Rel:  "self",
+		},
+	},
+}
+
+// CreateOutput represents the response body from a Create request.
+const CreateOutput = `
+{
+  "stack": {
+    "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+    "links": [
+    {
+      "href": "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+      "rel": "self"
+    }
+    ]
+  }
+}`
+
+// HandleCreateSuccessfully creates an HTTP handler at `/stacks` on the test handler mux
+// that responds with a `Create` response.
+func HandleCreateSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks", 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.StatusCreated)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// ListExpected represents the expected object from a List request.
+var ListExpected = []ListedStack{
+	ListedStack{
+		Description: "Simple template to test heat commands",
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+				Rel:  "self",
+			},
+		},
+		StatusReason: "Stack CREATE completed successfully",
+		Name:         "postman_stack",
+		CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
+		Status:       "CREATE_COMPLETE",
+		ID:           "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+		Tags:         []string{"rackspace", "atx"},
+	},
+	ListedStack{
+		Description: "Simple template to test heat commands",
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada",
+				Rel:  "self",
+			},
+		},
+		StatusReason: "Stack successfully updated",
+		Name:         "gophercloud-test-stack-2",
+		CreationTime: time.Date(2014, 12, 11, 17, 39, 16, 0, time.UTC),
+		UpdatedTime:  time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC),
+		Status:       "UPDATE_COMPLETE",
+		ID:           "db6977b2-27aa-4775-9ae7-6213212d4ada",
+		Tags:         []string{"sfo", "satx"},
+	},
+}
+
+// FullListOutput represents the response body from a List request without a marker.
+const FullListOutput = `
+{
+  "stacks": [
+  {
+    "description": "Simple template to test heat commands",
+    "links": [
+    {
+      "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+      "rel": "self"
+    }
+    ],
+    "stack_status_reason": "Stack CREATE completed successfully",
+    "stack_name": "postman_stack",
+    "creation_time": "2015-02-03T20:07:39",
+    "updated_time": null,
+    "stack_status": "CREATE_COMPLETE",
+    "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	"tags": ["rackspace", "atx"]
+  },
+  {
+    "description": "Simple template to test heat commands",
+    "links": [
+    {
+      "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada",
+      "rel": "self"
+    }
+    ],
+    "stack_status_reason": "Stack successfully updated",
+    "stack_name": "gophercloud-test-stack-2",
+    "creation_time": "2014-12-11T17:39:16",
+    "updated_time": "2014-12-11T17:40:37",
+    "stack_status": "UPDATE_COMPLETE",
+    "id": "db6977b2-27aa-4775-9ae7-6213212d4ada",
+	"tags": ["sfo", "satx"]
+  }
+  ]
+}
+`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks` on the test handler mux
+// that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks", 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, output)
+		case "db6977b2-27aa-4775-9ae7-6213212d4ada":
+			fmt.Fprintf(w, `[]`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &RetrievedStack{
+	DisableRollback: true,
+	Description:     "Simple template to test heat commands",
+	Parameters: map[string]string{
+		"flavor":         "m1.tiny",
+		"OS::stack_name": "postman_stack",
+		"OS::stack_id":   "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	},
+	StatusReason: "Stack CREATE completed successfully",
+	Name:         "postman_stack",
+	Outputs:      []map[string]interface{}{},
+	CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+			Rel:  "self",
+		},
+	},
+	Capabilities:        []interface{}{},
+	NotificationTopics:  []interface{}{},
+	Status:              "CREATE_COMPLETE",
+	ID:                  "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	TemplateDescription: "Simple template to test heat commands",
+	Tags:                []string{"rackspace", "atx"},
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+  "stack": {
+    "disable_rollback": true,
+    "description": "Simple template to test heat commands",
+    "parameters": {
+      "flavor": "m1.tiny",
+      "OS::stack_name": "postman_stack",
+      "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87"
+    },
+    "stack_status_reason": "Stack CREATE completed successfully",
+    "stack_name": "postman_stack",
+    "outputs": [],
+    "creation_time": "2015-02-03T20:07:39",
+    "links": [
+    {
+      "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+      "rel": "self"
+    }
+    ],
+    "capabilities": [],
+    "notification_topics": [],
+    "timeout_mins": null,
+    "stack_status": "CREATE_COMPLETE",
+    "updated_time": null,
+    "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+    "template_description": "Simple template to test heat commands",
+	"tags": ["rackspace", "atx"]
+  }
+}
+`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// HandleUpdateSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87`
+// on the test handler mux that responds with an `Update` response.
+func HandleUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", 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().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleDeleteSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87`
+// on the test handler mux that responds with a `Delete` response.
+func HandleDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", 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.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// GetExpected represents the expected object from a Get request.
+var PreviewExpected = &PreviewedStack{
+	DisableRollback: true,
+	Description:     "Simple template to test heat commands",
+	Parameters: map[string]string{
+		"flavor":         "m1.tiny",
+		"OS::stack_name": "postman_stack",
+		"OS::stack_id":   "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	},
+	Name:         "postman_stack",
+	CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+			Rel:  "self",
+		},
+	},
+	Capabilities:        []interface{}{},
+	NotificationTopics:  []interface{}{},
+	ID:                  "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	TemplateDescription: "Simple template to test heat commands",
+}
+
+// HandlePreviewSuccessfully creates an HTTP handler at `/stacks/preview`
+// on the test handler mux that responds with a `Preview` response.
+func HandlePreviewSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/preview", 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.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// AbandonExpected represents the expected object from an Abandon request.
+var AbandonExpected = &AbandonedStack{
+	Status: "COMPLETE",
+	Name:   "postman_stack",
+	Template: map[string]interface{}{
+		"heat_template_version": "2013-05-23",
+		"description":           "Simple template to test heat commands",
+		"parameters": map[string]interface{}{
+			"flavor": map[string]interface{}{
+				"default": "m1.tiny",
+				"type":    "string",
+			},
+		},
+		"resources": map[string]interface{}{
+			"hello_world": map[string]interface{}{
+				"type": "OS::Nova::Server",
+				"properties": map[string]interface{}{
+					"key_name": "heat_key",
+					"flavor": map[string]interface{}{
+						"get_param": "flavor",
+					},
+					"image":     "ad091b52-742f-469e-8f3c-fd81cadf0743",
+					"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n",
+				},
+			},
+		},
+	},
+	Action: "CREATE",
+	ID:     "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	Resources: map[string]interface{}{
+		"hello_world": map[string]interface{}{
+			"status":      "COMPLETE",
+			"name":        "hello_world",
+			"resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63",
+			"action":      "CREATE",
+			"type":        "OS::Nova::Server",
+		},
+	},
+	Files: map[string]string{
+		"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n  flavor:\n    type: string\n    description: Flavor for the server to be created\n    default: 4353\n    hidden: true\nresources:\n  test_server:\n    type: \"OS::Nova::Server\"\n    properties:\n      name: test-server\n      flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n",
+	},
+	StackUserProjectID: "897686",
+	ProjectID:          "897686",
+	Environment: map[string]interface{}{
+		"encrypted_param_names": make([]map[string]interface{}, 0),
+		"parameter_defaults":    make(map[string]interface{}),
+		"parameters":            make(map[string]interface{}),
+		"resource_registry": map[string]interface{}{
+			"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml",
+			"resources": make(map[string]interface{}),
+		},
+	},
+}
+
+// AbandonOutput represents the response body from an Abandon request.
+const AbandonOutput = `
+{
+  "status": "COMPLETE",
+  "name": "postman_stack",
+  "template": {
+    "heat_template_version": "2013-05-23",
+    "description": "Simple template to test heat commands",
+    "parameters": {
+      "flavor": {
+        "default": "m1.tiny",
+        "type": "string"
+      }
+    },
+    "resources": {
+      "hello_world": {
+        "type": "OS::Nova::Server",
+        "properties": {
+          "key_name": "heat_key",
+          "flavor": {
+            "get_param": "flavor"
+          },
+          "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+          "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+        }
+      }
+    }
+  },
+  "action": "CREATE",
+  "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+  "resources": {
+    "hello_world": {
+      "status": "COMPLETE",
+      "name": "hello_world",
+      "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63",
+      "action": "CREATE",
+      "type": "OS::Nova::Server"
+    }
+  },
+  "files": {
+    "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "heat_template_version: 2014-10-16\nparameters:\n  flavor:\n    type: string\n    description: Flavor for the server to be created\n    default: 4353\n    hidden: true\nresources:\n  test_server:\n    type: \"OS::Nova::Server\"\n    properties:\n      name: test-server\n      flavor: 2 GB General Purpose v1\n image: Debian 7 (Wheezy) (PVHVM)\n"
+},
+  "environment": {
+	"encrypted_param_names": [],
+	"parameter_defaults": {},
+	"parameters": {},
+	"resource_registry": {
+		"file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml": "file:///Users/prat8228/go/src/github.com/rackspace/rack/my_nova.yaml",
+		"resources": {}
+	}
+  },
+  "stack_user_project_id": "897686",
+  "project_id": "897686"
+}`
+
+// HandleAbandonSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon`
+// on the test handler mux that responds with an `Abandon` response.
+func HandleAbandonSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c8/abandon", 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.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// ValidJSONTemplate is a valid OpenStack Heat template in JSON format
+const ValidJSONTemplate = `
+{
+  "heat_template_version": "2014-10-16",
+  "parameters": {
+    "flavor": {
+      "default": 4353,
+      "description": "Flavor for the server to be created",
+      "hidden": true,
+      "type": "string"
+    }
+  },
+  "resources": {
+    "test_server": {
+      "properties": {
+        "flavor": "2 GB General Purpose v1",
+        "image": "Debian 7 (Wheezy) (PVHVM)",
+        "name": "test-server"
+      },
+      "type": "OS::Nova::Server"
+    }
+  }
+}
+`
+
+// ValidJSONTemplateParsed is the expected parsed version of ValidJSONTemplate
+var ValidJSONTemplateParsed = map[string]interface{}{
+	"heat_template_version": "2014-10-16",
+	"parameters": map[string]interface{}{
+		"flavor": map[string]interface{}{
+			"default":     4353,
+			"description": "Flavor for the server to be created",
+			"hidden":      true,
+			"type":        "string",
+		},
+	},
+	"resources": map[string]interface{}{
+		"test_server": map[string]interface{}{
+			"properties": map[string]interface{}{
+				"flavor": "2 GB General Purpose v1",
+				"image":  "Debian 7 (Wheezy) (PVHVM)",
+				"name":   "test-server",
+			},
+			"type": "OS::Nova::Server",
+		},
+	},
+}
+
+// ValidYAMLTemplate is a valid OpenStack Heat template in YAML format
+const ValidYAMLTemplate = `
+heat_template_version: 2014-10-16
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+`
+
+// InvalidTemplateNoVersion is an invalid template as it has no `version` section
+const InvalidTemplateNoVersion = `
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+`
+
+// ValidJSONEnvironment is a valid environment for a stack in JSON format
+const ValidJSONEnvironment = `
+{
+  "parameters": {
+    "user_key": "userkey"
+  },
+  "resource_registry": {
+    "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml",
+    "OS::Quantum*": "OS::Neutron*",
+    "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml",
+    "OS::Metering::Alarm": "OS::Ceilometer::Alarm",
+    "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml",
+    "resources": {
+      "my_db_server": {
+        "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml"
+      },
+      "my_server": {
+        "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
+        "hooks": "pre-create"
+      },
+      "nested_stack": {
+        "nested_resource": {
+          "hooks": "pre-update"
+        },
+        "another_resource": {
+          "hooks": [
+            "pre-create",
+            "pre-update"
+          ]
+        }
+      }
+    }
+  }
+}
+`
+
+// ValidJSONEnvironmentParsed is the expected parsed version of ValidJSONEnvironment
+var ValidJSONEnvironmentParsed = map[string]interface{}{
+	"parameters": map[string]interface{}{
+		"user_key": "userkey",
+	},
+	"resource_registry": map[string]interface{}{
+		"My::WP::Server":         "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml",
+		"OS::Quantum*":           "OS::Neutron*",
+		"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml",
+		"OS::Metering::Alarm":    "OS::Ceilometer::Alarm",
+		"AWS::RDS::DBInstance":   "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml",
+		"resources": map[string]interface{}{
+			"my_db_server": map[string]interface{}{
+				"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
+			},
+			"my_server": map[string]interface{}{
+				"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
+				"hooks":          "pre-create",
+			},
+			"nested_stack": map[string]interface{}{
+				"nested_resource": map[string]interface{}{
+					"hooks": "pre-update",
+				},
+				"another_resource": map[string]interface{}{
+					"hooks": []interface{}{
+						"pre-create",
+						"pre-update",
+					},
+				},
+			},
+		},
+	},
+}
+
+// ValidYAMLEnvironment is a valid environment for a stack in YAML format
+const ValidYAMLEnvironment = `
+parameters:
+  user_key: userkey
+resource_registry:
+  My::WP::Server: file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml
+  # allow older templates with Quantum in them.
+  "OS::Quantum*": "OS::Neutron*"
+  # Choose your implementation of AWS::CloudWatch::Alarm
+  "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml"
+  #"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm"
+  "OS::Metering::Alarm": "OS::Ceilometer::Alarm"
+  "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml"
+  resources:
+    my_db_server:
+      "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
+    my_server:
+      "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
+      hooks: pre-create
+    nested_stack:
+      nested_resource:
+        hooks: pre-update
+      another_resource:
+        hooks: [pre-create, pre-update]
+`
+
+// InvalidEnvironment is an invalid environment as it has an extra section called `resources`
+const InvalidEnvironment = `
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+parameter_defaults:
+  KeyName: heat_key
+`
diff --git a/openstack/orchestration/v1/stacks/requests.go b/openstack/orchestration/v1/stacks/requests.go
new file mode 100644
index 0000000..1fc484d
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/requests.go
@@ -0,0 +1,682 @@
+package stacks
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Rollback is used to specify whether or not a stack can be rolled back.
+type Rollback *bool
+
+var (
+	disable = true
+	// Disable is used to specify that a stack cannot be rolled back.
+	Disable Rollback = &disable
+	enable           = false
+	// Enable is used to specify that a stack can be rolled back.
+	Enable Rollback = &enable
+)
+
+// 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 {
+	ToStackCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// (REQUIRED) The name of the stack. It must start with an alphabetic character.
+	Name string
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
+	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+	// This value is ignored if Template is supplied inline.
+	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
+	// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+	// is a stringified version of the JSON/YAML template. Since the template will likely
+	// be located in a file, one way to set this variable is by using ioutil.ReadFile:
+	// import "io/ioutil"
+	// var opts stacks.CreateOpts
+	// b, err := ioutil.ReadFile("path/to/you/template/file.json")
+	// if err != nil {
+	//   // handle error...
+	// }
+	// opts.Template = string(b)
+	Template string
+	// (OPTIONAL) Enables or disables deletion of all stack resources when a stack
+	// creation fails. Default is true, meaning all resources are not deleted when
+	// stack creation fails.
+	DisableRollback Rollback
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
+	// (OPTIONAL) A stringified JSON environment for the stack.
+	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
+	// (OPTIONAL) A map that maps file names to file contents. It can also be used
+	// to pass provider template contents. Example:
+	// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+	Files map[string]interface{}
+	// (OPTIONAL) User-defined parameters to pass to the template.
+	Parameters map[string]string
+	// (OPTIONAL) The timeout for stack creation in minutes.
+	Timeout int
+	// (OPTIONAL) A list of tags to assosciate with the Stack
+	Tags []string
+}
+
+// ToStackCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return s, errors.New("Required field 'Name' not provided.")
+	}
+	s["stack_name"] = opts.Name
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		if opts.Template != "" {
+			s["template"] = opts.Template
+		} else if opts.TemplateURL != "" {
+			s["template_url"] = opts.TemplateURL
+		} else {
+			return s, errors.New("Either Template or TemplateURL must be provided.")
+		}
+	} else {
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
+
+		if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.fixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
+	if opts.DisableRollback != nil {
+		s["disable_rollback"] = &opts.DisableRollback
+	}
+
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.fixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
+		s["environment"] = opts.Environment
+	}
+
+	if opts.Files != nil {
+		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
+	}
+
+	if opts.DisableRollback != nil {
+		s["disable_rollback"] = &opts.DisableRollback
+	}
+
+	if opts.Parameters != nil {
+		s["parameters"] = opts.Parameters
+	}
+
+	if opts.Timeout != 0 {
+		s["timeout_mins"] = opts.Timeout
+	}
+
+	if opts.Tags != nil {
+		s["tags"] = strings.Join(opts.Tags, ",")
+	}
+	return s, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new stack using the values
+// provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToStackCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// AdoptOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the Adopt function 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 AdoptOptsBuilder interface {
+	ToStackAdoptMap() (map[string]interface{}, error)
+}
+
+// AdoptOpts is the common options struct used in this package's Adopt
+// operation.
+type AdoptOpts struct {
+	// (REQUIRED) Existing resources data represented as a string to add to the
+	// new stack. Data returned by Abandon could be provided as AdoptsStackData.
+	AdoptStackData string
+	// (REQUIRED) The name of the stack. It must start with an alphabetic character.
+	Name string
+	// (REQUIRED) The timeout for stack creation in minutes.
+	Timeout int
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
+	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+	// This value is ignored if Template is supplied inline.
+	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
+	// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+	// is a stringified version of the JSON/YAML template. Since the template will likely
+	// be located in a file, one way to set this variable is by using ioutil.ReadFile:
+	// import "io/ioutil"
+	// var opts stacks.CreateOpts
+	// b, err := ioutil.ReadFile("path/to/you/template/file.json")
+	// if err != nil {
+	//   // handle error...
+	// }
+	// opts.Template = string(b)
+	Template string
+	// (OPTIONAL) Enables or disables deletion of all stack resources when a stack
+	// creation fails. Default is true, meaning all resources are not deleted when
+	// stack creation fails.
+	DisableRollback Rollback
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
+	// (OPTIONAL) A stringified JSON environment for the stack.
+	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
+	// (OPTIONAL) A map that maps file names to file contents. It can also be used
+	// to pass provider template contents. Example:
+	// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+	Files map[string]interface{}
+	// (OPTIONAL) User-defined parameters to pass to the template.
+	Parameters map[string]string
+}
+
+// ToStackAdoptMap casts a CreateOpts struct to a map.
+func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return s, errors.New("Required field 'Name' not provided.")
+	}
+	s["stack_name"] = opts.Name
+	Files := make(map[string]string)
+	if opts.AdoptStackData != "" {
+		s["adopt_stack_data"] = opts.AdoptStackData
+	} else if opts.TemplateOpts == nil {
+		if opts.Template != "" {
+			s["template"] = opts.Template
+		} else if opts.TemplateURL != "" {
+			s["template_url"] = opts.TemplateURL
+		} else {
+			return s, errors.New("One of AdoptStackData, Template, TemplateURL or TemplateOpts must be provided.")
+		}
+	} else {
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
+
+		if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.fixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
+
+	if opts.DisableRollback != nil {
+		s["disable_rollback"] = &opts.DisableRollback
+	}
+
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.fixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
+		s["environment"] = opts.Environment
+	}
+
+	if opts.Files != nil {
+		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
+	}
+
+	if opts.Parameters != nil {
+		s["parameters"] = opts.Parameters
+	}
+
+	if opts.Timeout != 0 {
+		s["timeout"] = opts.Timeout
+	}
+	s["timeout_mins"] = opts.Timeout
+
+	return s, nil
+}
+
+// Adopt accepts an AdoptOpts struct and creates a new stack using the resources
+// from another stack.
+func Adopt(c *gophercloud.ServiceClient, opts AdoptOptsBuilder) AdoptResult {
+	var res AdoptResult
+
+	reqBody, err := opts.ToStackAdoptMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(adoptURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// SortDir is a type for specifying in which direction to sort a list of stacks.
+type SortDir string
+
+// SortKey is a type for specifying by which key to sort a list of stacks.
+type SortKey string
+
+var (
+	// SortAsc is used to sort a list of stacks in ascending order.
+	SortAsc SortDir = "asc"
+	// SortDesc is used to sort a list of stacks in descending order.
+	SortDesc SortDir = "desc"
+	// SortName is used to sort a list of stacks by name.
+	SortName SortKey = "name"
+	// SortStatus is used to sort a list of stacks by status.
+	SortStatus SortKey = "status"
+	// SortCreatedAt is used to sort a list of stacks by date created.
+	SortCreatedAt SortKey = "created_at"
+	// SortUpdatedAt is used to sort a list of stacks by date updated.
+	SortUpdatedAt SortKey = "updated_at"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToStackListQuery() (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"`
+	Marker  string  `q:"marker"`
+	Limit   int     `q:"limit"`
+	SortKey SortKey `q:"sort_keys"`
+	SortDir SortDir `q:"sort_dir"`
+}
+
+// ToStackListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToStackListQuery() (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
+// stacks. 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.ToStackListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return StackPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Get retreives a stack based on the stack name and stack ID.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c, stackName, stackID), &res.Body, nil)
+	return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the Update operation in this package.
+type UpdateOptsBuilder interface {
+	ToStackUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
+	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+	// This value is ignored if Template is supplied inline.
+	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
+	// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+	// is a stringified version of the JSON/YAML template. Since the template will likely
+	// be located in a file, one way to set this variable is by using ioutil.ReadFile:
+	// import "io/ioutil"
+	// var opts stacks.CreateOpts
+	// b, err := ioutil.ReadFile("path/to/you/template/file.json")
+	// if err != nil {
+	//   // handle error...
+	// }
+	// opts.Template = string(b)
+	Template string
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
+	// (OPTIONAL) A stringified JSON environment for the stack.
+	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
+	// (OPTIONAL) A map that maps file names to file contents. It can also be used
+	// to pass provider template contents. Example:
+	// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+	Files map[string]interface{}
+	// (OPTIONAL) User-defined parameters to pass to the template.
+	Parameters map[string]string
+	// (OPTIONAL) The timeout for stack creation in minutes.
+	Timeout int
+	// (OPTIONAL) A list of tags to assosciate with the Stack
+	Tags []string
+}
+
+// ToStackUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		if opts.Template != "" {
+			s["template"] = opts.Template
+		} else if opts.TemplateURL != "" {
+			s["template_url"] = opts.TemplateURL
+		} else {
+			return s, errors.New("Either Template or TemplateURL must be provided.")
+		}
+	} else {
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
+
+		if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.fixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
+
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.fixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
+		s["environment"] = opts.Environment
+	}
+
+	if opts.Files != nil {
+		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
+	}
+
+	if opts.Parameters != nil {
+		s["parameters"] = opts.Parameters
+	}
+
+	if opts.Timeout != 0 {
+		s["timeout_mins"] = opts.Timeout
+	}
+
+	if opts.Tags != nil {
+		s["tags"] = strings.Join(opts.Tags, ",")
+	}
+
+	return s, nil
+}
+
+// Update accepts an UpdateOpts struct and updates an existing stack using the values
+// provided.
+func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToStackUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(updateURL(c, stackName, stackID), reqBody, nil, nil)
+	return res
+}
+
+// Delete deletes a stack based on the stack name and stack ID.
+func Delete(c *gophercloud.ServiceClient, stackName, stackID string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(deleteURL(c, stackName, stackID), nil)
+	return res
+}
+
+// PreviewOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the Preview operation in this package.
+type PreviewOptsBuilder interface {
+	ToStackPreviewMap() (map[string]interface{}, error)
+}
+
+// PreviewOpts contains the common options struct used in this package's Preview
+// operation.
+type PreviewOpts struct {
+	// (REQUIRED) The name of the stack. It must start with an alphabetic character.
+	Name string
+	// (REQUIRED) The timeout for stack creation in minutes.
+	Timeout int
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
+	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+	// This value is ignored if Template is supplied inline.
+	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
+	// (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+	// is a stringified version of the JSON/YAML template. Since the template will likely
+	// be located in a file, one way to set this variable is by using ioutil.ReadFile:
+	// import "io/ioutil"
+	// var opts stacks.CreateOpts
+	// b, err := ioutil.ReadFile("path/to/you/template/file.json")
+	// if err != nil {
+	//   // handle error...
+	// }
+	// opts.Template = string(b)
+	Template string
+	// (OPTIONAL) Enables or disables deletion of all stack resources when a stack
+	// creation fails. Default is true, meaning all resources are not deleted when
+	// stack creation fails.
+	DisableRollback Rollback
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
+	// (OPTIONAL) A stringified JSON environment for the stack.
+	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
+	// (OPTIONAL) A map that maps file names to file contents. It can also be used
+	// to pass provider template contents. Example:
+	// Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+	Files map[string]interface{}
+	// (OPTIONAL) User-defined parameters to pass to the template.
+	Parameters map[string]string
+}
+
+// ToStackPreviewMap casts a PreviewOpts struct to a map.
+func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return s, errors.New("Required field 'Name' not provided.")
+	}
+	s["stack_name"] = opts.Name
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		if opts.Template != "" {
+			s["template"] = opts.Template
+		} else if opts.TemplateURL != "" {
+			s["template_url"] = opts.TemplateURL
+		} else {
+			return s, errors.New("Either Template or TemplateURL must be provided.")
+		}
+	} else {
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
+
+		if err := opts.TemplateOpts.getFileContents(opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.fixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
+	if opts.DisableRollback != nil {
+		s["disable_rollback"] = &opts.DisableRollback
+	}
+
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := opts.EnvironmentOpts.getRRFileContents(ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.fixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
+		s["environment"] = opts.Environment
+	}
+
+	if opts.Files != nil {
+		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
+	}
+
+	if opts.Parameters != nil {
+		s["parameters"] = opts.Parameters
+	}
+
+	if opts.Timeout != 0 {
+		s["timeout_mins"] = opts.Timeout
+	}
+
+	return s, nil
+}
+
+// Preview accepts a PreviewOptsBuilder interface and creates a preview of a stack using the values
+// provided.
+func Preview(c *gophercloud.ServiceClient, opts PreviewOptsBuilder) PreviewResult {
+	var res PreviewResult
+
+	reqBody, err := opts.ToStackPreviewMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Post(previewURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Abandon deletes the stack with the provided stackName and stackID, but leaves its
+// resources intact, and returns data describing the stack and its resources.
+func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) AbandonResult {
+	var res AbandonResult
+	_, res.Err = c.Delete(abandonURL(c, stackName, stackID), &gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res
+}
diff --git a/openstack/orchestration/v1/stacks/requests_test.go b/openstack/orchestration/v1/stacks/requests_test.go
new file mode 100644
index 0000000..0fde44b
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/requests_test.go
@@ -0,0 +1,358 @@
+package stacks
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+
+	createOpts := CreateOpts{
+		Name:    "stackcreated",
+		Timeout: 60,
+		Template: `
+    {
+      "stack_name": "postman_stack",
+      "template": {
+        "heat_template_version": "2013-05-23",
+        "description": "Simple template to test heat commands",
+        "parameters": {
+          "flavor": {
+            "default": "m1.tiny",
+            "type": "string"
+          }
+        },
+        "resources": {
+          "hello_world": {
+            "type":"OS::Nova::Server",
+            "properties": {
+              "key_name": "heat_key",
+              "flavor": {
+                "get_param": "flavor"
+              },
+              "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+              "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+            }
+          }
+        }
+      }
+    }`,
+		DisableRollback: Disable,
+	}
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+	template := new(Template)
+	template.Bin = []byte(`
+		{
+			"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type": "string"
+				}
+			}
+		}`)
+	createOpts := CreateOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: Disable,
+	}
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAdoptStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+
+	adoptOpts := AdoptOpts{
+		AdoptStackData: `{environment{parameters{}}}`,
+		Name:           "stackcreated",
+		Timeout:        60,
+		Template: `
+    {
+      "stack_name": "postman_stack",
+      "template": {
+        "heat_template_version": "2013-05-23",
+        "description": "Simple template to test heat commands",
+        "parameters": {
+          "flavor": {
+            "default": "m1.tiny",
+            "type": "string"
+          }
+        },
+        "resources": {
+          "hello_world": {
+            "type":"OS::Nova::Server",
+            "properties": {
+              "key_name": "heat_key",
+              "flavor": {
+                "get_param": "flavor"
+              },
+              "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+              "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+            }
+          }
+        }
+      }
+    }`,
+		DisableRollback: Disable,
+	}
+	actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAdoptStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+	template := new(Template)
+	template.Bin = []byte(`
+{
+  "stack_name": "postman_stack",
+  "template": {
+	"heat_template_version": "2013-05-23",
+	"description": "Simple template to test heat commands",
+	"parameters": {
+	  "flavor": {
+		"default": "m1.tiny",
+		"type": "string"
+	  }
+	},
+	"resources": {
+	  "hello_world": {
+		"type":"OS::Nova::Server",
+		"properties": {
+		  "key_name": "heat_key",
+		  "flavor": {
+			"get_param": "flavor"
+		  },
+		  "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+		  "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+		}
+	  }
+	}
+  }
+}`)
+	adoptOpts := AdoptOpts{
+		AdoptStackData:  `{environment{parameters{}}}`,
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: Disable,
+	}
+	actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t, FullListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractStacks(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateSuccessfully(t)
+
+	updateOpts := UpdateOpts{
+		Template: `
+    {
+      "heat_template_version": "2013-05-23",
+      "description": "Simple template to test heat commands",
+      "parameters": {
+        "flavor": {
+          "default": "m1.tiny",
+          "type": "string"
+        }
+      },
+      "resources": {
+        "hello_world": {
+          "type":"OS::Nova::Server",
+          "properties": {
+            "key_name": "heat_key",
+            "flavor": {
+              "get_param": "flavor"
+            },
+            "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+            "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+          }
+        }
+      }
+    }`,
+	}
+	err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdateStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateSuccessfully(t)
+
+	template := new(Template)
+	template.Bin = []byte(`
+		{
+			"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type": "string"
+				}
+			}
+		}`)
+	updateOpts := UpdateOpts{
+		TemplateOpts: template,
+	}
+	err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestPreviewStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePreviewSuccessfully(t, GetOutput)
+
+	previewOpts := PreviewOpts{
+		Name:    "stackcreated",
+		Timeout: 60,
+		Template: `
+    {
+      "stack_name": "postman_stack",
+      "template": {
+        "heat_template_version": "2013-05-23",
+        "description": "Simple template to test heat commands",
+        "parameters": {
+          "flavor": {
+            "default": "m1.tiny",
+            "type": "string"
+          }
+        },
+        "resources": {
+          "hello_world": {
+            "type":"OS::Nova::Server",
+            "properties": {
+              "key_name": "heat_key",
+              "flavor": {
+                "get_param": "flavor"
+              },
+              "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+              "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+            }
+          }
+        }
+      }
+    }`,
+		DisableRollback: Disable,
+	}
+	actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := PreviewExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestPreviewStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePreviewSuccessfully(t, GetOutput)
+
+	template := new(Template)
+	template.Bin = []byte(`
+		{
+			"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type": "string"
+				}
+			}
+		}`)
+	previewOpts := PreviewOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: Disable,
+	}
+	actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := PreviewExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAbandonStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAbandonSuccessfully(t, AbandonOutput)
+
+	actual, err := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c8").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := AbandonExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stacks/results.go b/openstack/orchestration/v1/stacks/results.go
new file mode 100644
index 0000000..432bc8e
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/results.go
@@ -0,0 +1,313 @@
+package stacks
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreatedStack represents the object extracted from a Create operation.
+type CreatedStack struct {
+	ID    string             `mapstructure:"id"`
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a CreatedStack object and is called after a
+// Create operation.
+func (r CreateResult) Extract() (*CreatedStack, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Stack *CreatedStack `mapstructure:"stack"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	return res.Stack, nil
+}
+
+// AdoptResult represents the result of an Adopt operation. AdoptResult has the
+// same form as CreateResult.
+type AdoptResult struct {
+	CreateResult
+}
+
+// StackPage is a pagination.Pager that is returned from a call to the List function.
+type StackPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Stacks.
+func (r StackPage) IsEmpty() (bool, error) {
+	stacks, err := ExtractStacks(r)
+	if err != nil {
+		return true, err
+	}
+	return len(stacks) == 0, nil
+}
+
+// ListedStack represents an element in the slice extracted from a List operation.
+type ListedStack struct {
+	CreationTime time.Time          `mapstructure:"-"`
+	Description  string             `mapstructure:"description"`
+	ID           string             `mapstructure:"id"`
+	Links        []gophercloud.Link `mapstructure:"links"`
+	Name         string             `mapstructure:"stack_name"`
+	Status       string             `mapstructure:"stack_status"`
+	StatusReason string             `mapstructure:"stack_status_reason"`
+	Tags         []string           `mapstructure:"tags"`
+	UpdatedTime  time.Time          `mapstructure:"-"`
+}
+
+// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating
+// over a stacks.List call.
+func ExtractStacks(page pagination.Page) ([]ListedStack, error) {
+	casted := page.(StackPage).Body
+
+	var res struct {
+		Stacks []ListedStack `mapstructure:"stacks"`
+	}
+
+	err := mapstructure.Decode(casted, &res)
+	if err != nil {
+		return nil, err
+	}
+
+	var rawStacks []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		rawStacks = casted.(map[string]interface{})["stacks"].([]interface{})
+	case map[string][]interface{}:
+		rawStacks = casted.(map[string][]interface{})["stacks"]
+	default:
+		return res.Stacks, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i := range rawStacks {
+		thisStack := (rawStacks[i]).(map[string]interface{})
+
+		if t, ok := thisStack["creation_time"].(string); ok && t != "" {
+			creationTime, err := time.Parse(gophercloud.STACK_TIME_FMT, t)
+			if err != nil {
+				return res.Stacks, err
+			}
+			res.Stacks[i].CreationTime = creationTime
+		}
+
+		if t, ok := thisStack["updated_time"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(gophercloud.STACK_TIME_FMT, t)
+			if err != nil {
+				return res.Stacks, err
+			}
+			res.Stacks[i].UpdatedTime = updatedTime
+		}
+	}
+
+	return res.Stacks, nil
+}
+
+// RetrievedStack represents the object extracted from a Get operation.
+type RetrievedStack struct {
+	Capabilities        []interface{}            `mapstructure:"capabilities"`
+	CreationTime        time.Time                `mapstructure:"-"`
+	Description         string                   `mapstructure:"description"`
+	DisableRollback     bool                     `mapstructure:"disable_rollback"`
+	ID                  string                   `mapstructure:"id"`
+	Links               []gophercloud.Link       `mapstructure:"links"`
+	NotificationTopics  []interface{}            `mapstructure:"notification_topics"`
+	Outputs             []map[string]interface{} `mapstructure:"outputs"`
+	Parameters          map[string]string        `mapstructure:"parameters"`
+	Name                string                   `mapstructure:"stack_name"`
+	Status              string                   `mapstructure:"stack_status"`
+	StatusReason        string                   `mapstructure:"stack_status_reason"`
+	Tags                []string                 `mapstructure:"tags"`
+	TemplateDescription string                   `mapstructure:"template_description"`
+	Timeout             int                      `mapstructure:"timeout_mins"`
+	UpdatedTime         time.Time                `mapstructure:"-"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a RetrievedStack object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*RetrievedStack, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Stack *RetrievedStack `mapstructure:"stack"`
+	}
+
+	config := &mapstructure.DecoderConfig{
+		Result:           &res,
+		WeaklyTypedInput: true,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := decoder.Decode(r.Body); err != nil {
+		return nil, err
+	}
+
+	b := r.Body.(map[string]interface{})["stack"].(map[string]interface{})
+
+	if date, ok := b["creation_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Stack.CreationTime = t
+	}
+
+	if date, ok := b["updated_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Stack.UpdatedTime = t
+	}
+
+	return res.Stack, err
+}
+
+// UpdateResult represents the result of a Update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// PreviewedStack represents the result of a Preview operation.
+type PreviewedStack struct {
+	Capabilities        []interface{}      `mapstructure:"capabilities"`
+	CreationTime        time.Time          `mapstructure:"-"`
+	Description         string             `mapstructure:"description"`
+	DisableRollback     bool               `mapstructure:"disable_rollback"`
+	ID                  string             `mapstructure:"id"`
+	Links               []gophercloud.Link `mapstructure:"links"`
+	Name                string             `mapstructure:"stack_name"`
+	NotificationTopics  []interface{}      `mapstructure:"notification_topics"`
+	Parameters          map[string]string  `mapstructure:"parameters"`
+	Resources           []interface{}      `mapstructure:"resources"`
+	TemplateDescription string             `mapstructure:"template_description"`
+	Timeout             int                `mapstructure:"timeout_mins"`
+	UpdatedTime         time.Time          `mapstructure:"-"`
+}
+
+// PreviewResult represents the result of a Preview operation.
+type PreviewResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a PreviewedStack object and is called after a
+// Preview operation.
+func (r PreviewResult) Extract() (*PreviewedStack, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Stack *PreviewedStack `mapstructure:"stack"`
+	}
+
+	config := &mapstructure.DecoderConfig{
+		Result:           &res,
+		WeaklyTypedInput: true,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := decoder.Decode(r.Body); err != nil {
+		return nil, err
+	}
+
+	b := r.Body.(map[string]interface{})["stack"].(map[string]interface{})
+
+	if date, ok := b["creation_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Stack.CreationTime = t
+	}
+
+	if date, ok := b["updated_time"]; ok && date != nil {
+		t, err := time.Parse(gophercloud.STACK_TIME_FMT, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.Stack.UpdatedTime = t
+	}
+
+	return res.Stack, err
+}
+
+// AbandonedStack represents the result of an Abandon operation.
+type AbandonedStack struct {
+	Status             string                 `mapstructure:"status"`
+	Name               string                 `mapstructure:"name"`
+	Template           map[string]interface{} `mapstructure:"template"`
+	Action             string                 `mapstructure:"action"`
+	ID                 string                 `mapstructure:"id"`
+	Resources          map[string]interface{} `mapstructure:"resources"`
+	Files              map[string]string      `mapstructure:"files"`
+	StackUserProjectID string                 `mapstructure:"stack_user_project_id"`
+	ProjectID          string                 `mapstructure:"project_id"`
+	Environment        map[string]interface{} `mapstructure:"environment"`
+}
+
+// AbandonResult represents the result of an Abandon operation.
+type AbandonResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to an AbandonedStack object and is called after an
+// Abandon operation.
+func (r AbandonResult) Extract() (*AbandonedStack, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res AbandonedStack
+
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	return &res, nil
+}
+
+// String converts an AbandonResult to a string. This is useful to when passing
+// the result of an Abandon operation to an AdoptOpts AdoptStackData field.
+func (r AbandonResult) String() (string, error) {
+	out, err := json.Marshal(r)
+	if err != nil {
+		return "", err
+	}
+	return string(out), nil
+}
diff --git a/openstack/orchestration/v1/stacks/template.go b/openstack/orchestration/v1/stacks/template.go
new file mode 100644
index 0000000..234ce49
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/template.go
@@ -0,0 +1,139 @@
+package stacks
+
+import (
+	"fmt"
+	"github.com/rackspace/gophercloud"
+	"reflect"
+	"strings"
+)
+
+// Template is a structure that represents OpenStack Heat templates
+type Template struct {
+	TE
+}
+
+// TemplateFormatVersions is a map containing allowed variations of the template format version
+// Note that this contains the permitted variations of the _keys_ not the values.
+var TemplateFormatVersions = map[string]bool{
+	"HeatTemplateFormatVersion": true,
+	"heat_template_version":     true,
+	"AWSTemplateFormatVersion":  true,
+}
+
+// Validate validates the contents of the Template
+func (t *Template) Validate() error {
+	if t.Parsed == nil {
+		if err := t.Parse(); err != nil {
+			return err
+		}
+	}
+	for key := range t.Parsed {
+		if _, ok := TemplateFormatVersions[key]; ok {
+			return nil
+		}
+	}
+	return fmt.Errorf("Template format version not found.")
+}
+
+// GetFileContents recursively parses a template to search for urls. These urls
+// are assumed to point to other templates (known in OpenStack Heat as child
+// templates). The contents of these urls are fetched and stored in the `Files`
+// parameter of the template structure. This is the only way that a user can
+// use child templates that are located in their filesystem; urls located on the
+// web (e.g. on github or swift) can be fetched directly by Heat engine.
+func (t *Template) getFileContents(te interface{}, ignoreIf igFunc, recurse bool) error {
+	// initialize template if empty
+	if t.Files == nil {
+		t.Files = make(map[string]string)
+	}
+	if t.fileMaps == nil {
+		t.fileMaps = make(map[string]string)
+	}
+	switch te.(type) {
+	// if te is a map
+	case map[string]interface{}, map[interface{}]interface{}:
+		teMap, err := toStringKeys(te)
+		if err != nil {
+			return err
+		}
+		for k, v := range teMap {
+			value, ok := v.(string)
+			if !ok {
+				// if the value is not a string, recursively parse that value
+				if err := t.getFileContents(v, ignoreIf, recurse); err != nil {
+					return err
+				}
+			} else if !ignoreIf(k, value) {
+				// at this point, the k, v pair has a reference to an external template.
+				// The assumption of heatclient is that value v is a reference
+				// to a file in the users environment
+
+				// create a new child template
+				childTemplate := new(Template)
+
+				// initialize child template
+
+				// get the base location of the child template
+				baseURL, err := gophercloud.NormalizePathURL(t.baseURL, value)
+				if err != nil {
+					return err
+				}
+				childTemplate.baseURL = baseURL
+				childTemplate.client = t.client
+
+				// fetch the contents of the child template
+				if err := childTemplate.Parse(); err != nil {
+					return err
+				}
+
+				// process child template recursively if required. This is
+				// required if the child template itself contains references to
+				// other templates
+				if recurse {
+					if err := childTemplate.getFileContents(childTemplate.Parsed, ignoreIf, recurse); err != nil {
+						return err
+					}
+				}
+				// update parent template with current child templates' content.
+				// At this point, the child template has been parsed recursively.
+				t.fileMaps[value] = childTemplate.URL
+				t.Files[childTemplate.URL] = string(childTemplate.Bin)
+
+			}
+		}
+		return nil
+	// if te is a slice, call the function on each element of the slice.
+	case []interface{}:
+		teSlice := te.([]interface{})
+		for i := range teSlice {
+			if err := t.getFileContents(teSlice[i], ignoreIf, recurse); err != nil {
+				return err
+			}
+		}
+	// if te is anything else, return
+	case string, bool, float64, nil, int:
+		return nil
+	default:
+		return fmt.Errorf("%v: Unrecognized type", reflect.TypeOf(te))
+
+	}
+	return nil
+}
+
+// function to choose keys whose values are other template files
+func ignoreIfTemplate(key string, value interface{}) bool {
+	// key must be either `get_file` or `type` for value to be a URL
+	if key != "get_file" && key != "type" {
+		return true
+	}
+	// value must be a string
+	valueString, ok := value.(string)
+	if !ok {
+		return true
+	}
+	// `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type`
+	if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) {
+		return true
+	}
+	return false
+}
diff --git a/openstack/orchestration/v1/stacks/template_test.go b/openstack/orchestration/v1/stacks/template_test.go
new file mode 100644
index 0000000..6884db8
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/template_test.go
@@ -0,0 +1,148 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTemplateValidation(t *testing.T) {
+	templateJSON := new(Template)
+	templateJSON.Bin = []byte(ValidJSONTemplate)
+	err := templateJSON.Validate()
+	th.AssertNoErr(t, err)
+
+	templateYAML := new(Template)
+	templateYAML.Bin = []byte(ValidYAMLTemplate)
+	err = templateYAML.Validate()
+	th.AssertNoErr(t, err)
+
+	templateInvalid := new(Template)
+	templateInvalid.Bin = []byte(InvalidTemplateNoVersion)
+	if err = templateInvalid.Validate(); err == nil {
+		t.Error("Template validation did not catch invalid template")
+	}
+}
+
+func TestTemplateParsing(t *testing.T) {
+	templateJSON := new(Template)
+	templateJSON.Bin = []byte(ValidJSONTemplate)
+	err := templateJSON.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateJSON.Parsed)
+
+	templateYAML := new(Template)
+	templateYAML.Bin = []byte(ValidJSONTemplate)
+	err = templateYAML.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateYAML.Parsed)
+
+	templateInvalid := new(Template)
+	templateInvalid.Bin = []byte("Keep Austin Weird")
+	err = templateInvalid.Parse()
+	if err == nil {
+		t.Error("Template parsing did not catch invalid template")
+	}
+}
+
+func TestIgnoreIfTemplate(t *testing.T) {
+	var keyValueTests = []struct {
+		key   string
+		value interface{}
+		out   bool
+	}{
+		{"not_get_file", "afksdf", true},
+		{"not_type", "sdfd", true},
+		{"get_file", "shdfuisd", false},
+		{"type", "dfsdfsd", true},
+		{"type", "sdfubsduf.yaml", false},
+		{"type", "sdfsdufs.template", false},
+		{"type", "sdfsdf.file", true},
+		{"type", map[string]string{"key": "value"}, true},
+	}
+	var result bool
+	for _, kv := range keyValueTests {
+		result = ignoreIfTemplate(kv.key, kv.value)
+		if result != kv.out {
+			t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, result, kv.out)
+		}
+	}
+}
+
+func TestGetFileContents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+	fakeURL := strings.Join([]string{baseurl, "my_nova.yaml"}, "/")
+	urlparsed, err := url.Parse(fakeURL)
+	th.AssertNoErr(t, err)
+	myNovaContent := `heat_template_version: 2014-10-16
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+      networks:
+      - {uuid: 11111111-1111-1111-1111-111111111111}`
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, myNovaContent)
+	})
+
+	client := fakeClient{BaseClient: getHTTPClient()}
+	te := new(Template)
+	te.Bin = []byte(`heat_template_version: 2015-04-30
+resources:
+  my_server:
+    type: my_nova.yaml`)
+	te.client = client
+
+	err = te.Parse()
+	th.AssertNoErr(t, err)
+	err = te.getFileContents(te.Parsed, ignoreIfTemplate, true)
+	th.AssertNoErr(t, err)
+	expectedFiles := map[string]string{
+		"my_nova.yaml": `heat_template_version: 2014-10-16
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+      networks:
+      - {uuid: 11111111-1111-1111-1111-111111111111}`}
+	th.AssertEquals(t, expectedFiles["my_nova.yaml"], te.Files[fakeURL])
+	te.fixFileRefs()
+	expectedParsed := map[string]interface{}{
+		"heat_template_version": "2015-04-30",
+		"resources": map[string]interface{}{
+			"my_server": map[string]interface{}{
+				"type": fakeURL,
+			},
+		},
+	}
+	te.Parse()
+	th.AssertDeepEquals(t, expectedParsed, te.Parsed)
+}
diff --git a/openstack/orchestration/v1/stacks/urls.go b/openstack/orchestration/v1/stacks/urls.go
new file mode 100644
index 0000000..3dd2bb3
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/urls.go
@@ -0,0 +1,35 @@
+package stacks
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("stacks")
+}
+
+func adoptURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, name, id string) string {
+	return c.ServiceURL("stacks", name, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, name, id string) string {
+	return getURL(c, name, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, name, id string) string {
+	return getURL(c, name, id)
+}
+
+func previewURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("stacks", "preview")
+}
+
+func abandonURL(c *gophercloud.ServiceClient, name, id string) string {
+	return c.ServiceURL("stacks", name, id, "abandon")
+}
diff --git a/openstack/orchestration/v1/stacks/utils.go b/openstack/orchestration/v1/stacks/utils.go
new file mode 100644
index 0000000..7b476a9
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/utils.go
@@ -0,0 +1,161 @@
+package stacks
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"path/filepath"
+	"reflect"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"gopkg.in/yaml.v2"
+)
+
+// Client is an interface that expects a Get method similar to http.Get. This
+// is needed for unit testing, since we can mock an http client. Thus, the
+// client will usually be an http.Client EXCEPT in unit tests.
+type Client interface {
+	Get(string) (*http.Response, error)
+}
+
+// TE is a base structure for both Template and Environment
+type TE struct {
+	// Bin stores the contents of the template or environment.
+	Bin []byte
+	// URL stores the URL of the template. This is allowed to be a 'file://'
+	// for local files.
+	URL string
+	// Parsed contains a parsed version of Bin. Since there are 2 different
+	// fields referring to the same value, you must be careful when accessing
+	// this filed.
+	Parsed map[string]interface{}
+	// Files contains a mapping between the urls in templates to their contents.
+	Files map[string]string
+	// fileMaps is a map used internally when determining Files.
+	fileMaps map[string]string
+	// baseURL represents the location of the template or environment file.
+	baseURL string
+	// client is an interface which allows TE to fetch contents from URLS
+	client Client
+}
+
+// Fetch fetches the contents of a TE from its URL. Once a TE structure has a
+// URL, call the fetch method to fetch the contents.
+func (t *TE) Fetch() error {
+	// if the baseURL is not provided, use the current directors as the base URL
+	if t.baseURL == "" {
+		u, err := getBasePath()
+		if err != nil {
+			return err
+		}
+		t.baseURL = u
+	}
+
+	// if the contents are already present, do nothing.
+	if t.Bin != nil {
+		return nil
+	}
+
+	// get a fqdn from the URL using the baseURL of the TE. For local files,
+	// the URL's will have the `file` scheme.
+	u, err := gophercloud.NormalizePathURL(t.baseURL, t.URL)
+	if err != nil {
+		return err
+	}
+	t.URL = u
+
+	// get an HTTP client if none present
+	if t.client == nil {
+		t.client = getHTTPClient()
+	}
+
+	// use the client to fetch the contents of the TE
+	resp, err := t.client.Get(t.URL)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	t.Bin = body
+	return nil
+}
+
+// get the basepath of the TE
+func getBasePath() (string, error) {
+	basePath, err := filepath.Abs(".")
+	if err != nil {
+		return "", err
+	}
+	u, err := gophercloud.NormalizePathURL("", basePath)
+	if err != nil {
+		return "", err
+	}
+	return u, nil
+}
+
+// get a an HTTP client to retrieve URL's. This client allows the use of `file`
+// scheme since we may need to fetch files from users filesystem
+func getHTTPClient() Client {
+	transport := &http.Transport{}
+	transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+	return &http.Client{Transport: transport}
+}
+
+// Parse will parse the contents and then validate. The contents MUST be either JSON or YAML.
+func (t *TE) Parse() error {
+	if err := t.Fetch(); err != nil {
+		return err
+	}
+	if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil {
+		if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil {
+			return fmt.Errorf("Data in neither json nor yaml format.")
+		}
+	}
+	return t.Validate()
+}
+
+// Validate validates the contents of TE
+func (t *TE) Validate() error {
+	return nil
+}
+
+// igfunc is a parameter used by GetFileContents and GetRRFileContents to check
+// for valid URL's.
+type igFunc func(string, interface{}) bool
+
+// convert map[interface{}]interface{} to map[string]interface{}
+func toStringKeys(m interface{}) (map[string]interface{}, error) {
+	switch m.(type) {
+	case map[string]interface{}, map[interface{}]interface{}:
+		typedMap := make(map[string]interface{})
+		if _, ok := m.(map[interface{}]interface{}); ok {
+			for k, v := range m.(map[interface{}]interface{}) {
+				typedMap[k.(string)] = v
+			}
+		} else {
+			typedMap = m.(map[string]interface{})
+		}
+		return typedMap, nil
+	default:
+		return nil, fmt.Errorf("Expected a map of type map[string]interface{} or map[interface{}]interface{}, actual type: %v", reflect.TypeOf(m))
+
+	}
+}
+
+// fix the reference to files by replacing relative URL's by absolute
+// URL's
+func (t *TE) fixFileRefs() {
+	tStr := string(t.Bin)
+	if t.fileMaps == nil {
+		return
+	}
+	for k, v := range t.fileMaps {
+		tStr = strings.Replace(tStr, k, v, -1)
+	}
+	t.Bin = []byte(tStr)
+}
diff --git a/openstack/orchestration/v1/stacks/utils_test.go b/openstack/orchestration/v1/stacks/utils_test.go
new file mode 100644
index 0000000..2536e03
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/utils_test.go
@@ -0,0 +1,94 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTEFixFileRefs(t *testing.T) {
+	te := TE{
+		Bin: []byte(`string_to_replace: my fair lady`),
+		fileMaps: map[string]string{
+			"string_to_replace": "london bridge is falling down",
+		},
+	}
+	te.fixFileRefs()
+	th.AssertEquals(t, string(te.Bin), `london bridge is falling down: my fair lady`)
+}
+
+func TesttoStringKeys(t *testing.T) {
+	var test1 interface{} = map[interface{}]interface{}{
+		"Adam":  "Smith",
+		"Isaac": "Newton",
+	}
+	result1, err := toStringKeys(test1)
+	th.AssertNoErr(t, err)
+
+	expected := map[string]interface{}{
+		"Adam":  "Smith",
+		"Isaac": "Newton",
+	}
+	th.AssertDeepEquals(t, result1, expected)
+}
+
+func TestGetBasePath(t *testing.T) {
+	_, err := getBasePath()
+	th.AssertNoErr(t, err)
+}
+
+// test if HTTP client can read file type URLS. Read the URL of this file
+// because if this test is running, it means this file _must_ exist
+func TestGetHTTPClient(t *testing.T) {
+	client := getHTTPClient()
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+	resp, err := client.Get(baseurl)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, resp.StatusCode, 200)
+}
+
+// Implement a fakeclient that can be used to mock out HTTP requests
+type fakeClient struct {
+	BaseClient Client
+}
+
+// this client's Get method first changes the URL given to point to
+// testhelper's (th) endpoints. This is done because the http Mux does not seem
+// to work for fqdns with the `file` scheme
+func (c fakeClient) Get(url string) (*http.Response, error) {
+	newurl := strings.Replace(url, "file://", th.Endpoint(), 1)
+	return c.BaseClient.Get(newurl)
+}
+
+// test the fetch function
+func TestFetch(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+	fakeURL := strings.Join([]string{baseurl, "file.yaml"}, "/")
+	urlparsed, err := url.Parse(fakeURL)
+	th.AssertNoErr(t, err)
+
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, "Fee-fi-fo-fum")
+	})
+
+	client := fakeClient{BaseClient: getHTTPClient()}
+	te := TE{
+		URL:    "file.yaml",
+		client: client,
+	}
+	err = te.Fetch()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, fakeURL, te.URL)
+	th.AssertEquals(t, "Fee-fi-fo-fum", string(te.Bin))
+}
diff --git a/openstack/orchestration/v1/stacktemplates/doc.go b/openstack/orchestration/v1/stacktemplates/doc.go
new file mode 100644
index 0000000..5af0bd6
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/doc.go
@@ -0,0 +1,8 @@
+// Package stacktemplates provides operations for working with Heat templates.
+// A Cloud Orchestration template is a portable file, written in a user-readable
+// language, that describes how a set of resources should be assembled and what
+// software should be installed in order to produce a working stack. The template
+// specifies what resources should be used, what attributes can be set, and other
+// parameters that are critical to the successful, repeatable automation of a
+// specific application stack.
+package stacktemplates
diff --git a/openstack/orchestration/v1/stacktemplates/fixtures.go b/openstack/orchestration/v1/stacktemplates/fixtures.go
new file mode 100644
index 0000000..fa9b301
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/fixtures.go
@@ -0,0 +1,95 @@
+package stacktemplates
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = "{\n  \"description\": \"Simple template to test heat commands\",\n  \"heat_template_version\": \"2013-05-23\",\n  \"parameters\": {\n    \"flavor\": {\n      \"default\": \"m1.tiny\",\n      \"type\": \"string\"\n    }\n  },\n  \"resources\": {\n    \"hello_world\": {\n      \"properties\": {\n        \"flavor\": {\n          \"get_param\": \"flavor\"\n        },\n        \"image\": \"ad091b52-742f-469e-8f3c-fd81cadf0743\",\n        \"key_name\": \"heat_key\"\n      },\n      \"type\": \"OS::Nova::Server\"\n    }\n  }\n}"
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+  "heat_template_version": "2013-05-23",
+  "description": "Simple template to test heat commands",
+  "parameters": {
+    "flavor": {
+      "default": "m1.tiny",
+      "type": "string"
+    }
+  },
+  "resources": {
+    "hello_world": {
+      "type": "OS::Nova::Server",
+      "properties": {
+        "key_name": "heat_key",
+        "flavor": {
+          "get_param": "flavor"
+        },
+        "image": "ad091b52-742f-469e-8f3c-fd81cadf0743"
+      }
+    }
+  }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
+
+// ValidateExpected represents the expected object from a Validate request.
+var ValidateExpected = &ValidatedTemplate{
+	Description: "Simple template to test heat commands",
+	Parameters: map[string]interface{}{
+		"flavor": map[string]interface{}{
+			"Default":     "m1.tiny",
+			"Type":        "String",
+			"NoEcho":      "false",
+			"Description": "",
+			"Label":       "flavor",
+		},
+	},
+}
+
+// ValidateOutput represents the response body from a Validate request.
+const ValidateOutput = `
+{
+	"Description": "Simple template to test heat commands",
+	"Parameters": {
+		"flavor": {
+			"Default": "m1.tiny",
+			"Type": "String",
+			"NoEcho": "false",
+			"Description": "",
+			"Label": "flavor"
+		}
+	}
+}`
+
+// HandleValidateSuccessfully creates an HTTP handler at `/validate`
+// on the test handler mux that responds with a `Validate` response.
+func HandleValidateSuccessfully(t *testing.T, output string) {
+	th.Mux.HandleFunc("/validate", 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.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, output)
+	})
+}
diff --git a/openstack/orchestration/v1/stacktemplates/requests.go b/openstack/orchestration/v1/stacktemplates/requests.go
new file mode 100644
index 0000000..c0cea35
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/requests.go
@@ -0,0 +1,58 @@
+package stacktemplates
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) GetResult {
+	var res GetResult
+	_, res.Err = c.Request("GET", getURL(c, stackName, stackID), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+	})
+	return res
+}
+
+// ValidateOptsBuilder describes struct types that can be accepted by the Validate call.
+// The ValidateOpts struct in this package does.
+type ValidateOptsBuilder interface {
+	ToStackTemplateValidateMap() (map[string]interface{}, error)
+}
+
+// ValidateOpts specifies the template validation parameters.
+type ValidateOpts struct {
+	Template    string
+	TemplateURL string
+}
+
+// ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts.
+func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) {
+	vo := make(map[string]interface{})
+	if opts.Template != "" {
+		vo["template"] = opts.Template
+		return vo, nil
+	}
+	if opts.TemplateURL != "" {
+		vo["template_url"] = opts.TemplateURL
+		return vo, nil
+	}
+	return vo, fmt.Errorf("One of Template or TemplateURL is required.")
+}
+
+// Validate validates the given stack template.
+func Validate(c *gophercloud.ServiceClient, opts ValidateOptsBuilder) ValidateResult {
+	var res ValidateResult
+
+	reqBody, err := opts.ToStackTemplateValidateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(validateURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
diff --git a/openstack/orchestration/v1/stacktemplates/requests_test.go b/openstack/orchestration/v1/stacktemplates/requests_test.go
new file mode 100644
index 0000000..42667c9
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/requests_test.go
@@ -0,0 +1,57 @@
+package stacktemplates
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, string(actual))
+}
+
+func TestValidateTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleValidateSuccessfully(t, ValidateOutput)
+
+	opts := ValidateOpts{
+		Template: `{
+		  "heat_template_version": "2013-05-23",
+		  "description": "Simple template to test heat commands",
+		  "parameters": {
+		    "flavor": {
+		      "default": "m1.tiny",
+		      "type": "string"
+		    }
+		  },
+		  "resources": {
+		    "hello_world": {
+		      "type": "OS::Nova::Server",
+		      "properties": {
+		        "key_name": "heat_key",
+		        "flavor": {
+		          "get_param": "flavor"
+		        },
+		        "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+		        "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+		      }
+		    }
+		  }
+		}`,
+	}
+	actual, err := Validate(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := ValidateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stacktemplates/results.go b/openstack/orchestration/v1/stacktemplates/results.go
new file mode 100644
index 0000000..4e9ba5a
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/results.go
@@ -0,0 +1,51 @@
+package stacktemplates
+
+import (
+	"encoding/json"
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+)
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract returns the JSON template and is called after a Get operation.
+func (r GetResult) Extract() ([]byte, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	template, err := json.MarshalIndent(r.Body, "", "  ")
+	if err != nil {
+		return nil, err
+	}
+	return template, nil
+}
+
+// ValidatedTemplate represents the parsed object returned from a Validate request.
+type ValidatedTemplate struct {
+	Description     string                 `mapstructure:"Description"`
+	Parameters      map[string]interface{} `mapstructure:"Parameters"`
+	ParameterGroups map[string]interface{} `mapstructure:"ParameterGroups"`
+}
+
+// ValidateResult represents the result of a Validate operation.
+type ValidateResult struct {
+	gophercloud.Result
+}
+
+// Extract returns a pointer to a ValidatedTemplate object and is called after a
+// Validate operation.
+func (r ValidateResult) Extract() (*ValidatedTemplate, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res ValidatedTemplate
+	if err := mapstructure.Decode(r.Body, &res); err != nil {
+		return nil, err
+	}
+
+	return &res, nil
+}
diff --git a/openstack/orchestration/v1/stacktemplates/urls.go b/openstack/orchestration/v1/stacktemplates/urls.go
new file mode 100644
index 0000000..c30b7ca
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/urls.go
@@ -0,0 +1,11 @@
+package stacktemplates
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient, stackName, stackID string) string {
+	return c.ServiceURL("stacks", stackName, stackID, "template")
+}
+
+func validateURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("validate")
+}
diff --git a/openstack/utils/choose_version.go b/openstack/utils/choose_version.go
new file mode 100644
index 0000000..b697ba8
--- /dev/null
+++ b/openstack/utils/choose_version.go
@@ -0,0 +1,114 @@
+package utils
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// 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 an 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(client *gophercloud.ProviderClient, 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(client.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 := client.Request("GET", client.IdentityBase, gophercloud.RequestOpts{
+		JSONResponse: &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, client.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", client.IdentityBase)
+	}
+	if endpoint == "" {
+		return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.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..388d689
--- /dev/null
+++ b/openstack/utils/choose_version_test.go
@@ -0,0 +1,118 @@
+package utils
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"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"}
+
+	c := &gophercloud.ProviderClient{
+		IdentityBase:     testhelper.Endpoint(),
+		IdentityEndpoint: "",
+	}
+	v, endpoint, err := ChooseVersion(c, []*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"}
+
+	c := &gophercloud.ProviderClient{
+		IdentityBase:     testhelper.Endpoint(),
+		IdentityEndpoint: testhelper.Endpoint() + "v2.0/",
+	}
+	v, endpoint, err := ChooseVersion(c, []*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/"}
+
+	c := &gophercloud.ProviderClient{
+		IdentityBase:     testhelper.Endpoint(),
+		IdentityEndpoint: testhelper.Endpoint() + "v2.0/",
+	}
+	v, endpoint, err := ChooseVersion(c, []*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/pagination/http.go b/pagination/http.go
new file mode 100644
index 0000000..1b3fe94
--- /dev/null
+++ b/pagination/http.go
@@ -0,0 +1,60 @@
+package pagination
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"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 PageResultFromParsed(resp, parsedBody), err
+}
+
+// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its
+// body parsed as JSON (and closed).
+func PageResultFromParsed(resp *http.Response, body interface{}) PageResult {
+	return PageResult{
+		Result: gophercloud.Result{
+			Body:   body,
+			Header: resp.Header,
+		},
+		URL: *resp.Request.URL,
+	}
+}
+
+// Request performs an HTTP request and extracts the http.Response from the result.
+func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) {
+	return client.Request("GET", url, gophercloud.RequestOpts{
+		MoreHeaders: headers,
+		OkCodes:     []int{200, 204},
+	})
+}
diff --git a/pagination/linked.go b/pagination/linked.go
new file mode 100644
index 0000000..e9bd8de
--- /dev/null
+++ b/pagination/linked.go
@@ -0,0 +1,67 @@
+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
+		}
+	}
+}
+
+// GetBody returns the linked page's body. This method is needed to satisfy the
+// Page interface.
+func (current LinkedPageBase) GetBody() interface{} {
+	return current.Body
+}
diff --git a/pagination/linked_test.go b/pagination/linked_test.go
new file mode 100644
index 0000000..1ac0f73
--- /dev/null
+++ b/pagination/linked_test.go
@@ -0,0 +1,120 @@
+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)
+	}
+}
+
+func TestAllPagesLinked(t *testing.T) {
+	pager := createLinked(t)
+	defer testhelper.TeardownHTTP()
+
+	page, err := pager.AllPages()
+	testhelper.AssertNoErr(t, err)
+
+	expected := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
+	actual, err := ExtractLinkedInts(page)
+	testhelper.AssertNoErr(t, err)
+	testhelper.CheckDeepEquals(t, expected, actual)
+}
diff --git a/pagination/marker.go b/pagination/marker.go
new file mode 100644
index 0000000..f355afc
--- /dev/null
+++ b/pagination/marker.go
@@ -0,0 +1,40 @@
+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
+}
+
+// GetBody returns the linked page's body. This method is needed to satisfy the
+// Page interface.
+func (current MarkerPageBase) GetBody() interface{} {
+	return current.Body
+}
diff --git a/pagination/marker_test.go b/pagination/marker_test.go
new file mode 100644
index 0000000..f4d55be
--- /dev/null
+++ b/pagination/marker_test.go
@@ -0,0 +1,126 @@
+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)
+}
+
+func TestAllPagesMarker(t *testing.T) {
+	pager := createMarkerPaged(t)
+	defer testhelper.TeardownHTTP()
+
+	page, err := pager.AllPages()
+	testhelper.AssertNoErr(t, err)
+
+	expected := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}
+	actual, err := ExtractMarkerStrings(page)
+	testhelper.AssertNoErr(t, err)
+	testhelper.CheckDeepEquals(t, expected, actual)
+}
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..a7593ac
--- /dev/null
+++ b/pagination/pager.go
@@ -0,0 +1,226 @@
+package pagination
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"strings"
+
+	"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)
+
+	// GetBody returns the Page Body. This is used in the `AllPages` method.
+	GetBody() interface{}
+}
+
+// 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
+		}
+	}
+}
+
+// AllPages returns all the pages from a `List` operation in a single page,
+// allowing the user to retrieve all the pages at once.
+func (p Pager) AllPages() (Page, error) {
+	// pagesSlice holds all the pages until they get converted into as Page Body.
+	var pagesSlice []interface{}
+	// body will contain the final concatenated Page body.
+	var body reflect.Value
+
+	// Grab a test page to ascertain the page body type.
+	testPage, err := p.fetchNextPage(p.initialURL)
+	if err != nil {
+		return nil, err
+	}
+	// Store the page type so we can use reflection to create a new mega-page of
+	// that type.
+	pageType := reflect.TypeOf(testPage)
+
+	// Switch on the page body type. Recognized types are `map[string]interface{}`,
+	// `[]byte`, and `[]interface{}`.
+	switch testPage.GetBody().(type) {
+	case map[string]interface{}:
+		// key is the map key for the page body if the body type is `map[string]interface{}`.
+		var key string
+		// Iterate over the pages to concatenate the bodies.
+		err := p.EachPage(func(page Page) (bool, error) {
+			b := page.GetBody().(map[string]interface{})
+			for k := range b {
+				// If it's a linked page, we don't want the `links`, we want the other one.
+				if !strings.HasSuffix(k, "links") {
+					key = k
+				}
+			}
+			pagesSlice = append(pagesSlice, b[key].([]interface{})...)
+			return true, nil
+		})
+		if err != nil {
+			return nil, err
+		}
+		// Set body to value of type `map[string]interface{}`
+		body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice)))
+		body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
+	case []byte:
+		// Iterate over the pages to concatenate the bodies.
+		err := p.EachPage(func(page Page) (bool, error) {
+			b := page.GetBody().([]byte)
+			pagesSlice = append(pagesSlice, b)
+			// seperate pages with a comma
+			pagesSlice = append(pagesSlice, []byte{10})
+			return true, nil
+		})
+		if err != nil {
+			return nil, err
+		}
+		if len(pagesSlice) > 0 {
+			// Remove the trailing comma.
+			pagesSlice = pagesSlice[:len(pagesSlice)-1]
+		}
+		var b []byte
+		// Combine the slice of slices in to a single slice.
+		for _, slice := range pagesSlice {
+			b = append(b, slice.([]byte)...)
+		}
+		// Set body to value of type `bytes`.
+		body = reflect.New(reflect.TypeOf(b)).Elem()
+		body.SetBytes(b)
+	case []interface{}:
+		// Iterate over the pages to concatenate the bodies.
+		err := p.EachPage(func(page Page) (bool, error) {
+			b := page.GetBody().([]interface{})
+			pagesSlice = append(pagesSlice, b...)
+			return true, nil
+		})
+		if err != nil {
+			return nil, err
+		}
+		// Set body to value of type `[]interface{}`
+		body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice))
+		for i, s := range pagesSlice {
+			body.Index(i).Set(reflect.ValueOf(s))
+		}
+	default:
+		return nil, fmt.Errorf("Page body has unrecognized type.")
+	}
+
+	// Each `Extract*` function is expecting a specific type of page coming back,
+	// otherwise the type assertion in those functions will fail. pageType is needed
+	// to create a type in this method that has the same type that the `Extract*`
+	// function is expecting and set the Body of that object to the concatenated
+	// pages.
+	page := reflect.New(pageType)
+	// Set the page body to be the concatenated pages.
+	page.Elem().FieldByName("Body").Set(body)
+	// Set any additional headers that were pass along. The `objectstorage` pacakge,
+	// for example, passes a Content-Type header.
+	h := make(http.Header)
+	for k, v := range p.Headers {
+		h.Add(k, v)
+	}
+	page.Elem().FieldByName("Header").Set(reflect.ValueOf(h))
+	// Type assert the page to a Page interface so that the type assertion in the
+	// `Extract*` methods will work.
+	return page.Elem().Interface().(Page), err
+}
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..f78d4ab
--- /dev/null
+++ b/pagination/single.go
@@ -0,0 +1,15 @@
+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
+}
+
+// GetBody returns the single page's body. This method is needed to satisfy the
+// Page interface.
+func (current SinglePageBase) GetBody() interface{} {
+	return current.Body
+}
diff --git a/pagination/single_test.go b/pagination/single_test.go
new file mode 100644
index 0000000..4af0fee
--- /dev/null
+++ b/pagination/single_test.go
@@ -0,0 +1,84 @@
+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)
+}
+
+func TestAllPagesSingle(t *testing.T) {
+	pager := setupSinglePaged()
+	defer testhelper.TeardownHTTP()
+
+	page, err := pager.AllPages()
+	testhelper.AssertNoErr(t, err)
+
+	expected := []int{1, 2, 3}
+	actual, err := ExtractSingleInts(page)
+	testhelper.AssertNoErr(t, err)
+	testhelper.CheckDeepEquals(t, expected, actual)
+}
diff --git a/params.go b/params.go
new file mode 100644
index 0000000..4d0f1e6
--- /dev/null
+++ b/params.go
@@ -0,0 +1,271 @@
+package gophercloud
+
+import (
+	"fmt"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// EnabledState is a convenience type, mostly used in Create and Update
+// operations. Because the zero value of a bool is FALSE, we need to use a
+// pointer instead to indicate zero-ness.
+type EnabledState *bool
+
+// Convenience vars for EnabledState values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Enabled  EnabledState = &iTrue
+	Disabled EnabledState = &iFalse
+)
+
+// IntToPointer is a function for converting integers into integer pointers.
+// This is useful when passing in options to operations.
+func IntToPointer(i int) *int {
+	return &i
+}
+
+/*
+MaybeString is an internal function to be used by request methods in individual
+resource packages.
+
+It takes a string that might be a zero value and returns either a pointer to its
+address or nil. This is useful for allowing users to conveniently omit values
+from an options struct by leaving them zeroed, but still pass nil to the JSON
+serializer so they'll be omitted from the request body.
+*/
+func MaybeString(original string) *string {
+	if original != "" {
+		return &original
+	}
+	return nil
+}
+
+/*
+MaybeInt is an internal function to be used by request methods in individual
+resource packages.
+
+Like MaybeString, it accepts an int that may or may not be a zero value, and
+returns either a pointer to its address or nil. It's intended to hint that the
+JSON serializer should omit its field.
+*/
+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 is an internal function to be used by request methods in
+individual resource packages.
+
+It accepts a tagged structure and expands it into a URL struct. Field names are
+converted into query parameters based on a "q" tag. For example:
+
+	type struct Something {
+	   Bar string `q:"x_bar"`
+	   Baz int    `q:"lorem_ipsum"`
+	}
+
+	instance := Something{
+	   Bar: "AAA",
+	   Baz: "BBB",
+	}
+
+will be converted into "?x_bar=AAA&lorem_ipsum=BBB".
+
+The struct's fields may be strings, integers, or boolean values. Fields left at
+their type's zero value will be omitted from the query.
+*/
+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()
+	}
+
+	params := url.Values{}
+
+	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:
+						params.Add(tags[0], v.String())
+					case reflect.Int:
+						params.Add(tags[0], strconv.FormatInt(v.Int(), 10))
+					case reflect.Bool:
+						params.Add(tags[0], strconv.FormatBool(v.Bool()))
+					case reflect.Slice:
+						switch v.Type().Elem() {
+						case reflect.TypeOf(0):
+							for i := 0; i < v.Len(); i++ {
+								params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10))
+							}
+						default:
+							for i := 0; i < v.Len(); i++ {
+								params.Add(tags[0], v.Index(i).String())
+							}
+						}
+					}
+				} 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)
+					}
+				}
+			}
+		}
+
+		return &url.URL{RawQuery: params.Encode()}, 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 is an internal function to be used by request methods in
+individual resource packages.
+
+It accepts an arbitrary tagged structure and produces a string map that's
+suitable for use as the HTTP headers of an outgoing request. Field names are
+mapped to header names based in "h" tags.
+
+  type struct Something {
+    Bar string `h:"x_bar"`
+    Baz int    `h:"lorem_ipsum"`
+  }
+
+  instance := Something{
+    Bar: "AAA",
+    Baz: "BBB",
+  }
+
+will be converted into:
+
+  map[string]string{
+    "x_bar": "AAA",
+    "lorem_ipsum": "BBB",
+  }
+
+Untagged fields and fields left at their zero values are skipped. Integers,
+booleans and string values are supported.
+*/
+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.")
+}
+
+// IDSliceToQueryString takes a slice of elements and converts them into a query
+// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the
+// result would be `?name=20&name=40&name=60'
+func IDSliceToQueryString(name string, ids []int) string {
+	str := ""
+	for k, v := range ids {
+		if k == 0 {
+			str += "?"
+		} else {
+			str += "&"
+		}
+		str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v))
+	}
+	return str
+}
+
+// IntWithinRange returns TRUE if an integer falls within a defined range, and
+// FALSE if not.
+func IntWithinRange(val, min, max int) bool {
+	return val > min && val < max
+}
diff --git a/params_test.go b/params_test.go
new file mode 100644
index 0000000..2f40eec
--- /dev/null
+++ b/params_test.go
@@ -0,0 +1,165 @@
+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) {
+	type testVar string
+	opts := struct {
+		J  int       `q:"j"`
+		R  string    `q:"r,required"`
+		C  bool      `q:"c"`
+		S  []string  `q:"s"`
+		TS []testVar `q:"ts"`
+		TI []int     `q:"ti"`
+	}{
+		J:  2,
+		R:  "red",
+		C:  true,
+		S:  []string{"one", "two", "three"},
+		TS: []testVar{"a", "b"},
+		TI: []int{1, 2},
+	}
+	expected := &url.URL{RawQuery: "c=true&j=2&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"}
+	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"`
+		S  []string  `q:"s"`
+		TS []testVar `q:"ts"`
+		TI []int     `q:"ti"`
+	}{
+		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)
+}
+
+func TestQueriesAreEscaped(t *testing.T) {
+	type foo struct {
+		Name  string `q:"something"`
+		Shape string `q:"else"`
+	}
+
+	expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"}
+
+	actual, err := BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"})
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/provider_client.go b/provider_client.go
new file mode 100644
index 0000000..9264355
--- /dev/null
+++ b/provider_client.go
@@ -0,0 +1,308 @@
+package gophercloud
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+// DefaultUserAgent is the default User-Agent string set in the request header.
+const DefaultUserAgent = "gophercloud/1.0.0"
+
+// UserAgent represents a User-Agent header.
+type UserAgent struct {
+	// prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
+	// All the strings to prepend are accumulated and prepended in the Join method.
+	prepend []string
+}
+
+// Prepend prepends a user-defined string to the default User-Agent string. Users
+// may pass in one or more strings to prepend.
+func (ua *UserAgent) Prepend(s ...string) {
+	ua.prepend = append(s, ua.prepend...)
+}
+
+// Join concatenates all the user-defined User-Agend strings with the default
+// Gophercloud User-Agent string.
+func (ua *UserAgent) Join() string {
+	uaSlice := append(ua.prepend, DefaultUserAgent)
+	return strings.Join(uaSlice, " ")
+}
+
+// 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
+
+	// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
+	HTTPClient http.Client
+
+	// UserAgent represents the User-Agent header in the HTTP request.
+	UserAgent UserAgent
+
+	// ReauthFunc is the function used to re-authenticate the user if the request
+	// fails with a 401 HTTP response code. This a needed because there may be multiple
+	// authentication functions for different Identity service versions.
+	ReauthFunc func() error
+}
+
+// AuthenticatedHeaders returns a map of HTTP headers that are common for all
+// authenticated service requests.
+func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
+	if client.TokenID == "" {
+		return map[string]string{}
+	}
+	return map[string]string{"X-Auth-Token": client.TokenID}
+}
+
+// RequestOpts customizes the behavior of the provider.Request() method.
+type RequestOpts struct {
+	// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
+	// content type of the request will default to "application/json" unless overridden by MoreHeaders.
+	// It's an error to specify both a JSONBody and a RawBody.
+	JSONBody interface{}
+	// RawBody contains an io.ReadSeeker that will be consumed by the request directly. No content-type
+	// will be set unless one is provided explicitly by MoreHeaders.
+	RawBody io.ReadSeeker
+
+	// JSONResponse, if provided, will be populated with the contents of the response body parsed as
+	// JSON.
+	JSONResponse interface{}
+	// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
+	// the response has a different code, an error will be returned.
+	OkCodes []int
+
+	// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
+	// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
+	// the default Accept header or an inferred Content-Type, for example.
+	MoreHeaders map[string]string
+}
+
+// UnexpectedResponseCodeError is returned by the Request method when a response code other than
+// those listed in OkCodes is encountered.
+type UnexpectedResponseCodeError struct {
+	URL      string
+	Method   string
+	Expected []int
+	Actual   int
+	Body     []byte
+}
+
+func (err *UnexpectedResponseCodeError) Error() string {
+	return fmt.Sprintf(
+		"Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
+		err.Expected, err.Method, err.URL, err.Actual, err.Body,
+	)
+}
+
+var applicationJSON = "application/json"
+
+// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
+// header will automatically be provided.
+func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
+	var body io.ReadSeeker
+	var contentType *string
+
+	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
+	// io.ReadSeeker as-is. Default the content-type to application/json.
+	if options.JSONBody != nil {
+		if options.RawBody != nil {
+			panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
+		}
+
+		rendered, err := json.Marshal(options.JSONBody)
+		if err != nil {
+			return nil, err
+		}
+
+		body = bytes.NewReader(rendered)
+		contentType = &applicationJSON
+	}
+
+	if options.RawBody != nil {
+		body = options.RawBody
+	}
+
+	// Construct the http.Request.
+	req, err := http.NewRequest(method, url, body)
+	if err != nil {
+		return nil, err
+	}
+
+	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
+	// modify or omit any header.
+	if contentType != nil {
+		req.Header.Set("Content-Type", *contentType)
+	}
+	req.Header.Set("Accept", applicationJSON)
+
+	for k, v := range client.AuthenticatedHeaders() {
+		req.Header.Add(k, v)
+	}
+
+	// Set the User-Agent header
+	req.Header.Set("User-Agent", client.UserAgent.Join())
+
+	if options.MoreHeaders != nil {
+		for k, v := range options.MoreHeaders {
+			if v != "" {
+				req.Header.Set(k, v)
+			} else {
+				req.Header.Del(k)
+			}
+		}
+	}
+
+	// Issue the request.
+	resp, err := client.HTTPClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode == http.StatusUnauthorized {
+		if client.ReauthFunc != nil {
+			err = client.ReauthFunc()
+			if err != nil {
+				return nil, fmt.Errorf("Error trying to re-authenticate: %s", err)
+			}
+			if options.RawBody != nil {
+				options.RawBody.Seek(0, 0)
+			}
+			resp.Body.Close()
+			resp, err = client.Request(method, url, options)
+			if err != nil {
+				return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
+			}
+
+			return resp, nil
+		}
+	}
+
+	// Allow default OkCodes if none explicitly set
+	if options.OkCodes == nil {
+		options.OkCodes = defaultOkCodes(method)
+	}
+
+	// Validate the HTTP response status.
+	var ok bool
+	for _, code := range options.OkCodes {
+		if resp.StatusCode == code {
+			ok = true
+			break
+		}
+	}
+	if !ok {
+		body, _ := ioutil.ReadAll(resp.Body)
+		resp.Body.Close()
+		return resp, &UnexpectedResponseCodeError{
+			URL:      url,
+			Method:   method,
+			Expected: options.OkCodes,
+			Actual:   resp.StatusCode,
+			Body:     body,
+		}
+	}
+
+	// Parse the response body as JSON, if requested to do so.
+	if options.JSONResponse != nil {
+		defer resp.Body.Close()
+		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
+			return nil, err
+		}
+	}
+
+	return resp, nil
+}
+
+func defaultOkCodes(method string) []int {
+	switch {
+	case method == "GET":
+		return []int{200}
+	case method == "POST":
+		return []int{201, 202}
+	case method == "PUT":
+		return []int{201, 202}
+	case method == "DELETE":
+		return []int{202, 204}
+	}
+
+	return []int{}
+}
+
+func (client *ProviderClient) Get(url string, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+	return client.Request("GET", url, *opts)
+}
+
+func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	if v, ok := (JSONBody).(io.ReadSeeker); ok {
+		opts.RawBody = v
+	} else if JSONBody != nil {
+		opts.JSONBody = JSONBody
+	}
+
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+
+	return client.Request("POST", url, *opts)
+}
+
+func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	if v, ok := (JSONBody).(io.ReadSeeker); ok {
+		opts.RawBody = v
+	} else if JSONBody != nil {
+		opts.JSONBody = JSONBody
+	}
+
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+
+	return client.Request("PUT", url, *opts)
+}
+
+func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	return client.Request("DELETE", url, *opts)
+}
diff --git a/provider_client_test.go b/provider_client_test.go
new file mode 100644
index 0000000..d79d862
--- /dev/null
+++ b/provider_client_test.go
@@ -0,0 +1,35 @@
+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)
+}
+
+func TestUserAgent(t *testing.T) {
+	p := &ProviderClient{}
+
+	p.UserAgent.Prepend("custom-user-agent/2.4.0")
+	expected := "custom-user-agent/2.4.0 gophercloud/1.0.0"
+	actual := p.UserAgent.Join()
+	th.CheckEquals(t, expected, actual)
+
+	p.UserAgent.Prepend("another-custom-user-agent/0.3.0", "a-third-ua/5.9.0")
+	expected = "another-custom-user-agent/0.3.0 a-third-ua/5.9.0 custom-user-agent/2.4.0 gophercloud/1.0.0"
+	actual = p.UserAgent.Join()
+	th.CheckEquals(t, expected, actual)
+
+	p.UserAgent = UserAgent{}
+	expected = "gophercloud/1.0.0"
+	actual = p.UserAgent.Join()
+	th.CheckEquals(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..1cd1b6e
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/delegate.go
@@ -0,0 +1,131 @@
+package snapshots
+
+import (
+	"errors"
+
+	"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) os.DeleteResult {
+	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 = c.Request("PUT", updateURL(c, snapshotID), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &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..1a02b46
--- /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)
+
+	res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.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..c81644c
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,147 @@
+package snapshots
+
+import (
+	"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.(*gophercloud.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..4383494
--- /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) os.DeleteResult {
+	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..b6831f2
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/delegate_test.go
@@ -0,0 +1,107 @@
+package volumes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/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()
+
+	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{volumes.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{volumes.CreateOpts{Size: 1}}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	_, err = Create(fake.ServiceClient(), CreateOpts{volumes.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)
+
+	res := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.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/cdn/v1/base/delegate.go b/rackspace/cdn/v1/base/delegate.go
new file mode 100644
index 0000000..5af7e07
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate.go
@@ -0,0 +1,18 @@
+package base
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+	return os.Get(c)
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) os.PingResult {
+	return os.Ping(c)
+}
diff --git a/rackspace/cdn/v1/base/delegate_test.go b/rackspace/cdn/v1/base/delegate_test.go
new file mode 100644
index 0000000..731fc6d
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate_test.go
@@ -0,0 +1,44 @@
+package base
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t)
+
+	actual, err := Get(fake.ServiceClient()).Extract()
+	th.CheckNoErr(t, err)
+
+	expected := os.HomeDocument{
+		"rel/cdn": map[string]interface{}{
+			"href-template": "services{?marker,limit}",
+			"href-vars": map[string]interface{}{
+				"marker": "param/marker",
+				"limit":  "param/limit",
+			},
+			"hints": map[string]interface{}{
+				"allow": []string{"GET"},
+				"formats": map[string]interface{}{
+					"application/json": map[string]interface{}{},
+				},
+			},
+		},
+	}
+	th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandlePingSuccessfully(t)
+
+	err := Ping(fake.ServiceClient()).ExtractErr()
+	th.CheckNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/base/doc.go b/rackspace/cdn/v1/base/doc.go
new file mode 100644
index 0000000..5582306
--- /dev/null
+++ b/rackspace/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the Rackspace CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/rackspace/cdn/v1/flavors/delegate.go b/rackspace/cdn/v1/flavors/delegate.go
new file mode 100644
index 0000000..7152fa2
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate.go
@@ -0,0 +1,18 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(c)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(c, id)
+}
diff --git a/rackspace/cdn/v1/flavors/delegate_test.go b/rackspace/cdn/v1/flavors/delegate_test.go
new file mode 100644
index 0000000..d6d299d
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate_test.go
@@ -0,0 +1,90 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+	"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.HandleListCDNFlavorsSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractFlavors(page)
+		if err != nil {
+			t.Errorf("Failed to extract flavors: %v", err)
+			return false, err
+		}
+
+		expected := []os.Flavor{
+			os.Flavor{
+				ID: "europe",
+				Providers: []os.Provider{
+					os.Provider{
+						Provider: "Fastly",
+						Links: []gophercloud.Link{
+							gophercloud.Link{
+								Href: "http://www.fastly.com",
+								Rel:  "provider_url",
+							},
+						},
+					},
+				},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "self",
+					},
+				},
+			},
+		}
+
+		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()
+
+	os.HandleGetCDNFlavorSuccessfully(t)
+
+	expected := &os.Flavor{
+		ID: "asia",
+		Providers: []os.Provider{
+			os.Provider{
+				Provider: "ChinaCache",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "http://www.chinacache.com",
+						Rel:  "provider_url",
+					},
+				},
+			},
+		},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+				Rel:  "self",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "asia").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/cdn/v1/flavors/doc.go b/rackspace/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..4ad966e
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/rackspace/cdn/v1/serviceassets/delegate.go b/rackspace/cdn/v1/serviceassets/delegate.go
new file mode 100644
index 0000000..07c93a8
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate.go
@@ -0,0 +1,13 @@
+package serviceassets
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+)
+
+// Delete accepts a unique ID and deletes the CDN service asset associated with
+// it.
+func Delete(c *gophercloud.ServiceClient, id string, opts os.DeleteOptsBuilder) os.DeleteResult {
+	return os.Delete(c, id, opts)
+}
diff --git a/rackspace/cdn/v1/serviceassets/delegate_test.go b/rackspace/cdn/v1/serviceassets/delegate_test.go
new file mode 100644
index 0000000..328e168
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate_test.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleDeleteCDNAssetSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/serviceassets/doc.go b/rackspace/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..46b3d50
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the Rackspace CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/rackspace/cdn/v1/services/delegate.go b/rackspace/cdn/v1/services/delegate.go
new file mode 100644
index 0000000..e3f1459
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate.go
@@ -0,0 +1,37 @@
+package services
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Get retrieves a specific service based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(c, id)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing CDN service using
+// the values provided.
+func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult {
+	return os.Update(c, id, patches)
+}
+
+// Delete accepts a unique ID and deletes the CDN service associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(c, id)
+}
diff --git a/rackspace/cdn/v1/services/delegate_test.go b/rackspace/cdn/v1/services/delegate_test.go
new file mode 100644
index 0000000..6c48365
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate_test.go
@@ -0,0 +1,359 @@
+package services
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+	"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.HandleListCDNServiceSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient(), &os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractServices(page)
+		if err != nil {
+			t.Errorf("Failed to extract services: %v", err)
+			return false, err
+		}
+
+		expected := []os.Service{
+			os.Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Name: "mywebsite.com",
+				Domains: []os.Domain{
+					os.Domain{
+						Domain: "www.mywebsite.com",
+					},
+				},
+				Origins: []os.Origin{
+					os.Origin{
+						Origin: "mywebsite.com",
+						Port:   80,
+						SSL:    false,
+					},
+				},
+				Caching: []os.CacheRule{
+					os.CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+					os.CacheRule{
+						Name: "home",
+						TTL:  17200,
+						Rules: []os.TTLRule{
+							os.TTLRule{
+								Name:       "index",
+								RequestURL: "/index.htm",
+							},
+						},
+					},
+					os.CacheRule{
+						Name: "images",
+						TTL:  12800,
+						Rules: []os.TTLRule{
+							os.TTLRule{
+								Name:       "images",
+								RequestURL: "*.png",
+							},
+						},
+					},
+				},
+				Restrictions: []os.Restriction{
+					os.Restriction{
+						Name: "website only",
+						Rules: []os.RestrictionRule{
+							os.RestrictionRule{
+								Name:     "mywebsite.com",
+								Referrer: "www.mywebsite.com",
+							},
+						},
+					},
+				},
+				FlavorID: "asia",
+				Status:   "deployed",
+				Errors:   []os.Error{},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "mywebsite.com.cdn123.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+						Rel:  "flavor",
+					},
+				},
+			},
+			os.Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+				Name: "myothersite.com",
+				Domains: []os.Domain{
+					os.Domain{
+						Domain: "www.myothersite.com",
+					},
+				},
+				Origins: []os.Origin{
+					os.Origin{
+						Origin: "44.33.22.11",
+						Port:   80,
+						SSL:    false,
+					},
+					os.Origin{
+						Origin: "77.66.55.44",
+						Port:   80,
+						SSL:    false,
+						Rules: []os.OriginRule{
+							os.OriginRule{
+								Name:       "videos",
+								RequestURL: "^/videos/*.m3u",
+							},
+						},
+					},
+				},
+				Caching: []os.CacheRule{
+					os.CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+				},
+				Restrictions: []os.Restriction{},
+				FlavorID:     "europe",
+				Status:       "deployed",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "myothersite.com.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "flavor",
+					},
+				},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleCreateCDNServiceSuccessfully(t)
+
+	createOpts := os.CreateOpts{
+		Name: "mywebsite.com",
+		Domains: []os.Domain{
+			os.Domain{
+				Domain: "www.mywebsite.com",
+			},
+			os.Domain{
+				Domain: "blog.mywebsite.com",
+			},
+		},
+		Origins: []os.Origin{
+			os.Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Restrictions: []os.Restriction{
+			os.Restriction{
+				Name: "website only",
+				Rules: []os.RestrictionRule{
+					os.RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		Caching: []os.CacheRule{
+			os.CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+		},
+		FlavorID: "cdn",
+	}
+
+	expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleGetCDNServiceSuccessfully(t)
+
+	expected := &os.Service{
+		ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+		Name: "mywebsite.com",
+		Domains: []os.Domain{
+			os.Domain{
+				Domain:   "www.mywebsite.com",
+				Protocol: "http",
+			},
+		},
+		Origins: []os.Origin{
+			os.Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Caching: []os.CacheRule{
+			os.CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+			os.CacheRule{
+				Name: "home",
+				TTL:  17200,
+				Rules: []os.TTLRule{
+					os.TTLRule{
+						Name:       "index",
+						RequestURL: "/index.htm",
+					},
+				},
+			},
+			os.CacheRule{
+				Name: "images",
+				TTL:  12800,
+				Rules: []os.TTLRule{
+					os.TTLRule{
+						Name:       "images",
+						RequestURL: "*.png",
+					},
+				},
+			},
+		},
+		Restrictions: []os.Restriction{
+			os.Restriction{
+				Name: "website only",
+				Rules: []os.RestrictionRule{
+					os.RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		FlavorID: "cdn",
+		Status:   "deployed",
+		Errors:   []os.Error{},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+				Rel:  "access_url",
+			},
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+				Rel:  "flavor",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestSuccessfulUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleUpdateCDNServiceSuccessfully(t)
+
+	expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	ops := []os.Patch{
+		// Append a single Domain
+		os.Append{Value: os.Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		os.Insertion{
+			Index: 4,
+			Value: os.Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		os.Append{
+			Value: os.DomainList{
+				os.Domain{Domain: "bulkadded1.mocksite4.com"},
+				os.Domain{Domain: "bulkadded2.mocksite4.com"},
+			},
+		},
+		// Replace a single Origin
+		os.Replacement{
+			Index: 2,
+			Value: os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		os.Replacement{
+			Index: 0, // Ignored
+			Value: os.OriginList{
+				os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+				os.Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
+			},
+		},
+		// Remove a single CacheRule
+		os.Removal{
+			Index: 8,
+			Path:  os.PathCaching,
+		},
+		// Bulk removal
+		os.Removal{
+			All:  true,
+			Path: os.PathCaching,
+		},
+		// Service name replacement
+		os.NameReplacement{
+			NewName: "differentServiceName",
+		},
+	}
+
+	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleDeleteCDNServiceSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/services/doc.go b/rackspace/cdn/v1/services/doc.go
new file mode 100644
index 0000000..ee6e2a5
--- /dev/null
+++ b/rackspace/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/rackspace/client.go b/rackspace/client.go
new file mode 100644
index 0000000..a8f413e
--- /dev/null
+++ b/rackspace/client.go
@@ -0,0 +1,224 @@
+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, 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
+	}
+
+	if options.AllowReauth {
+		client.ReauthFunc = func() error {
+			return AuthenticateV2(client, options)
+		}
+	}
+	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
+}
+
+// NewLBV1 creates a ServiceClient that can be used to access the Rackspace
+// Cloud Load Balancer v1 API.
+func NewLBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:load-balancer")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewNetworkV2 creates a ServiceClient that can be used to access the Rackspace
+// Networking v2 API.
+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}, nil
+}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the Rackspace v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:cdn")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
+func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("orchestration")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewRackConnectV3 creates a ServiceClient that may be used to access the v3 RackConnect service.
+func NewRackConnectV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:rackconnect")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
+func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:database")
+	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..571a1be
--- /dev/null
+++ b/rackspace/compute/v2/bootfromvolume/delegate_test.go
@@ -0,0 +1,54 @@
+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",
+	"flavorName": "",
+	"imageName": "",
+        "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..081ea47
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate.go
@@ -0,0 +1,43 @@
+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) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
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..957dccf
--- /dev/null
+++ b/rackspace/compute/v2/flavors/fixtures.go
@@ -0,0 +1,137 @@
+// +build fixtures
+
+package 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 = Flavor{
+	ID:         "performance1-1",
+	Disk:       20,
+	RAM:        1024,
+	Name:       "1 GB Performance",
+	RxTxFactor: 200.0,
+	Swap:       0,
+	VCPUs:      1,
+	ExtraSpecs: ExtraSpecs{
+		NumDataDisks: 0,
+		Class:        "performance1",
+		DiskIOIndex:  0,
+		PolicyClass:  "performance_flavor",
+	},
+}
+
+// Performance2Flavor is the second result expected from parsing ListOutput.
+var Performance2Flavor = Flavor{
+	ID:         "performance1-2",
+	Disk:       40,
+	RAM:        2048,
+	Name:       "2 GB Performance",
+	RxTxFactor: 400.0,
+	Swap:       0,
+	VCPUs:      2,
+	ExtraSpecs: ExtraSpecs{
+		NumDataDisks: 0,
+		Class:        "performance1",
+		DiskIOIndex:  0,
+		PolicyClass:  "performance_flavor",
+	},
+}
+
+// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from
+// ListOutput.
+var ExpectedFlavorSlice = []Flavor{Performance1Flavor, Performance2Flavor}
diff --git a/rackspace/compute/v2/flavors/results.go b/rackspace/compute/v2/flavors/results.go
new file mode 100644
index 0000000..af444a7
--- /dev/null
+++ b/rackspace/compute/v2/flavors/results.go
@@ -0,0 +1,104 @@
+package flavors
+
+import (
+	"reflect"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/mitchellh/mapstructure"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtraSpecs provide additional information about the flavor.
+type ExtraSpecs struct {
+	// The number of data disks
+	NumDataDisks int `mapstructure:"number_of_data_disks"`
+	// The flavor class
+	Class string `mapstructure:"class"`
+	// Relative measure of disk I/O performance from 0-99, where higher is faster
+	DiskIOIndex int    `mapstructure:"disk_io_index"`
+	PolicyClass string `mapstructure:"policy_class"`
+}
+
+// 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"`
+
+	// ExtraSpecs provides extra information about the flavor
+	ExtraSpecs ExtraSpecs `mapstructure:"OS-FLV-WITH-EXT-SPECS:extra_specs"`
+}
+
+// GetResult temporarily holds the response 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
+}
+
+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.(os.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/rackspace/compute/v2/flavors/urls.go b/rackspace/compute/v2/flavors/urls.go
new file mode 100644
index 0000000..f4e2c3d
--- /dev/null
+++ b/rackspace/compute/v2/flavors/urls.go
@@ -0,0 +1,9 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("flavors", id)
+}
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..ccfbdc6
--- /dev/null
+++ b/rackspace/compute/v2/images/fixtures.go
@@ -0,0 +1,200 @@
+// +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..62e5df9
--- /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").ExtractErr()
+	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..cebbffd
--- /dev/null
+++ b/rackspace/compute/v2/networks/requests.go
@@ -0,0 +1,89 @@
+package networks
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 = c.Get(getURL(c, id), &res.Body, nil)
+	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 = c.Post(createURL(c), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(deleteURL(c, networkID), nil)
+	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..eb6a76c
--- /dev/null
+++ b/rackspace/compute/v2/networks/results.go
@@ -0,0 +1,81 @@
+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 struct {
+	gophercloud.ErrResult
+}
+
+// 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..7810d15
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate.go
@@ -0,0 +1,116 @@
+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)
+}
+
+// Update requests an existing server to be updated with the supplied options.
+func Update(client *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(client, id, opts)
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+	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)
+}
+
+// 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 os.ResizeOptsBuilder) os.ActionResult {
+	return os.Resize(client, id, opts)
+}
+
+// ConfirmResize confirms a previous resize operation on a server.
+// See Resize() for more details.
+func ConfirmResize(client *gophercloud.ServiceClient, id string) os.ActionResult {
+	return os.ConfirmResize(client, id)
+}
+
+// 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)
+}
+
+// ListAddresses makes a request against the API to list the servers IP addresses.
+func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
+	return os.ListAddresses(client, id)
+}
+
+// ExtractAddresses interprets the results of a single page from a ListAddresses() call, producing a map of Address slices.
+func ExtractAddresses(page pagination.Page) (map[string][]os.Address, error) {
+	return os.ExtractAddresses(page)
+}
+
+// ListAddressesByNetwork makes a request against the API to list the servers IP addresses
+// for the given network.
+func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
+	return os.ListAddressesByNetwork(client, id, network)
+}
+
+// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, producing a map of Address slices.
+func ExtractNetworkAddresses(page pagination.Page) ([]os.Address, error) {
+	return os.ExtractNetworkAddresses(page)
+}
+
+// Metadata requests all the metadata for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id string) os.GetMetadataResult {
+	return os.Metadata(client, id)
+}
+
+// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID.
+// This operation does not affect already-existing metadata that is not specified
+// by opts.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts os.UpdateMetadataOptsBuilder) os.UpdateMetadataResult {
+	return os.UpdateMetadata(client, id, opts)
+}
diff --git a/rackspace/compute/v2/servers/delegate_test.go b/rackspace/compute/v2/servers/delegate_test.go
new file mode 100644
index 0000000..03e7ace
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate_test.go
@@ -0,0 +1,182 @@
+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)
+
+	res := Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, res.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 TestUpdateServer(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, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "server": { "name": "test-server-updated" } }`)
+
+		w.Header().Add("Content-Type", "application/json")
+
+		fmt.Fprintf(w, UpdateOutput)
+	})
+
+	opts := os.UpdateOpts{
+		Name: "test-server-updated",
+	}
+	actual, err := Update(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee", opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &GophercloudUpdatedServer, 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)
+}
+
+func TestListAddresses(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleAddressListSuccessfully(t)
+
+	expected := os.ListAddressesExpected
+	pages := 0
+	err := ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 networks, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
+
+func TestListAddressesByNetwork(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleNetworkAddressListSuccessfully(t)
+
+	expected := os.ListNetworkAddressesExpected
+	pages := 0
+	err := ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractNetworkAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 addresses, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
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..75cccd0
--- /dev/null
+++ b/rackspace/compute/v2/servers/fixtures.go
@@ -0,0 +1,574 @@
+// +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"
+	}
+}
+`
+
+// UpdateOutput is the recorded output of a Rackspace servers.Update request.
+const UpdateOutput = `
+{
+	"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": "test-server-updated",
+		"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: "",
+}
+
+// GophercloudUpdatedServer is the expected result from parsing UpdateOutput.
+var GophercloudUpdatedServer = os.Server{
+	ID:         "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+	Name:       "test-server-updated",
+	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..d4472a0
--- /dev/null
+++ b/rackspace/compute/v2/servers/requests.go
@@ -0,0 +1,178 @@
+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 [optional; required if ImageName is not provided] is the ID or full
+	// URL to the image that contains the server's OS and initial state.
+	// Also optional if using the boot-from-volume extension.
+	ImageRef string
+
+	// ImageName [optional; required if ImageRef is not provided] is the name of the
+	// image that contains the server's OS and initial state.
+	// Also optional if using the boot-from-volume extension.
+	ImageName string
+
+	// FlavorRef [optional; required if FlavorName is not provided] is the ID or
+	// full URL to the flavor that describes the server's specs.
+	FlavorRef string
+
+	// FlavorName [optional; required if FlavorRef is not provided] is the name of
+	// the flavor that describes the server's specs.
+	FlavorName 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 files to inject into the server at launch.
+	// Create will base64-encode file contents for you.
+	Personality os.Personality
+
+	// ConfigDrive [optional] enables metadata injection through a configuration drive.
+	ConfigDrive bool
+
+	// AdminPass [optional] sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the response.
+	AdminPass string
+
+	// 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 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,
+		ImageName:        opts.ImageName,
+		FlavorRef:        opts.FlavorRef,
+		FlavorName:       opts.FlavorName,
+		SecurityGroups:   opts.SecurityGroups,
+		UserData:         opts.UserData,
+		AvailabilityZone: opts.AvailabilityZone,
+		Networks:         opts.Networks,
+		Metadata:         opts.Metadata,
+		Personality:      opts.Personality,
+		ConfigDrive:      opts.ConfigDrive,
+		AdminPass:        opts.AdminPass,
+	}
+
+	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{})
+	if opts.KeyPair != "" {
+		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 files to inject into the server at launch.
+	// Rebuild will base64-encode file contents for you.
+	Personality os.Personality
+
+	// 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..828b5dc
--- /dev/null
+++ b/rackspace/compute/v2/servers/requests_test.go
@@ -0,0 +1,59 @@
+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",
+			"flavorName": "",
+			"imageName": "",
+			"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..1ff7c5a
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/requests.go
@@ -0,0 +1,45 @@
+package virtualinterfaces
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 = c.Post(createURL(c, instanceID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		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 = c.Delete(deleteURL(c, instanceID, interfaceID), &gophercloud.RequestOpts{
+		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..26fa7f3
--- /dev/null
+++ b/rackspace/compute/v2/virtualinterfaces/results.go
@@ -0,0 +1,81 @@
+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 struct {
+	gophercloud.ErrResult
+}
+
+// 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/compute/v2/volumeattach/delegate.go b/rackspace/compute/v2/volumeattach/delegate.go
new file mode 100644
index 0000000..c6003e0
--- /dev/null
+++ b/rackspace/compute/v2/volumeattach/delegate.go
@@ -0,0 +1,27 @@
+package volumeattach
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of VolumeAttachments.
+func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
+	return os.List(client, serverID)
+}
+
+// Create requests the creation of a new volume attachment on the server
+func Create(client *gophercloud.ServiceClient, serverID string, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, serverID, opts)
+}
+
+// Get returns public data about a previously created VolumeAttachment.
+func Get(client *gophercloud.ServiceClient, serverID, aID string) os.GetResult {
+	return os.Get(client, serverID, aID)
+}
+
+// Delete requests the deletion of a previous stored VolumeAttachment from the server.
+func Delete(client *gophercloud.ServiceClient, serverID, aID string) os.DeleteResult {
+	return os.Delete(client, serverID, aID)
+}
diff --git a/rackspace/compute/v2/volumeattach/delegate_test.go b/rackspace/compute/v2/volumeattach/delegate_test.go
new file mode 100644
index 0000000..f7ef45e
--- /dev/null
+++ b/rackspace/compute/v2/volumeattach/delegate_test.go
@@ -0,0 +1,95 @@
+package volumeattach
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
+	fixtures "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// FirstVolumeAttachment is the first result in ListOutput.
+var FirstVolumeAttachment = volumeattach.VolumeAttachment{
+	Device:   "/dev/vdd",
+	ID:       "a26887c6-c47b-4654-abb5-dfadf7d3f803",
+	ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f803",
+}
+
+// SecondVolumeAttachment is the first result in ListOutput.
+var SecondVolumeAttachment = volumeattach.VolumeAttachment{
+	Device:   "/dev/vdc",
+	ID:       "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+	ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+}
+
+// ExpectedVolumeAttachmentSlide is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedVolumeAttachmentSlice = []volumeattach.VolumeAttachment{FirstVolumeAttachment, SecondVolumeAttachment}
+
+//CreatedVolumeAttachment is the parsed result from CreatedOutput.
+var CreatedVolumeAttachment = volumeattach.VolumeAttachment{
+	Device:   "/dev/vdc",
+	ID:       "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+	ServerID: "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+	VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleListSuccessfully(t)
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	count := 0
+	err := List(client.ServiceClient(), serverId).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := volumeattach.ExtractVolumeAttachments(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedVolumeAttachmentSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleCreateSuccessfully(t)
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	actual, err := Create(client.ServiceClient(), serverId, volumeattach.CreateOpts{
+		Device:   "/dev/vdc",
+		VolumeID: "a26887c6-c47b-4654-abb5-dfadf7d3f804",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &CreatedVolumeAttachment, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleGetSuccessfully(t)
+	aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	actual, err := Get(client.ServiceClient(), serverId, aId).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &SecondVolumeAttachment, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixtures.HandleDeleteSuccessfully(t)
+	aId := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+	serverId := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	err := Delete(client.ServiceClient(), serverId, aId).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/compute/v2/volumeattach/doc.go b/rackspace/compute/v2/volumeattach/doc.go
new file mode 100644
index 0000000..2164908
--- /dev/null
+++ b/rackspace/compute/v2/volumeattach/doc.go
@@ -0,0 +1,3 @@
+// Package volumeattach provides the ability to attach and detach volume
+// to instances to Rackspace servers
+package volumeattach
diff --git a/rackspace/db/v1/backups/doc.go b/rackspace/db/v1/backups/doc.go
new file mode 100644
index 0000000..664eead
--- /dev/null
+++ b/rackspace/db/v1/backups/doc.go
@@ -0,0 +1,6 @@
+// Package backups provides information and interaction with the backup API
+// resource in the Rackspace Database service.
+//
+// A backup is a copy of a database instance that can be used to restore it to
+// some defined point in history.
+package backups
diff --git a/rackspace/db/v1/backups/fixtures.go b/rackspace/db/v1/backups/fixtures.go
new file mode 100644
index 0000000..45c2376
--- /dev/null
+++ b/rackspace/db/v1/backups/fixtures.go
@@ -0,0 +1,66 @@
+package backups
+
+import "time"
+
+var (
+	timestamp  = "2015-11-12T14:22:42Z"
+	timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var getResp = `
+{
+  "backup": {
+    "created": "` + timestamp + `",
+    "description": "My Backup",
+    "id": "61f12fef-edb1-4561-8122-e7c00ef26a82",
+    "instance_id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+    "locationRef": null,
+    "name": "snapshot",
+    "parent_id": null,
+    "size": 100,
+    "status": "NEW",
+		"datastore": {
+			"version": "5.1",
+			"type": "MySQL",
+			"version_id": "20000000-0000-0000-0000-000000000002"
+		},
+    "updated": "` + timestamp + `"
+  }
+}
+`
+
+var createReq = `
+{
+  "backup": {
+    "description": "My Backup",
+    "instance": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+    "name": "snapshot"
+  }
+}
+`
+
+var createResp = getResp
+
+var listResp = `
+{
+  "backups": [
+    {
+      "status": "COMPLETED",
+      "updated": "` + timestamp + `",
+      "description": "Backup from Restored Instance",
+      "datastore": {
+        "version": "5.1",
+        "type": "MySQL",
+        "version_id": "20000000-0000-0000-0000-000000000002"
+      },
+      "id": "87972694-4be2-40f5-83f8-501656e0032a",
+      "size": 0.141026,
+      "name": "restored_backup",
+      "created": "` + timestamp + `",
+      "instance_id": "29af2cd9-0674-48ab-b87a-b160f00208e6",
+      "parent_id": null,
+      "locationRef": "http://localhost/path/to/backup"
+    }
+  ]
+}
+`
diff --git a/rackspace/db/v1/backups/requests.go b/rackspace/db/v1/backups/requests.go
new file mode 100644
index 0000000..9170d78
--- /dev/null
+++ b/rackspace/db/v1/backups/requests.go
@@ -0,0 +1,138 @@
+package backups
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOptsBuilder is the top-level interface for creating JSON maps.
+type CreateOptsBuilder interface {
+	ToBackupCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is responsible for configuring newly provisioned backups.
+type CreateOpts struct {
+	// [REQUIRED] The name of the backup. The only restriction is the name must
+	// be less than 64 characters long.
+	Name string
+
+	// [REQUIRED] The ID of the instance being backed up.
+	InstanceID string
+
+	// [OPTIONAL] A human-readable explanation of the backup.
+	Description string
+}
+
+// ToBackupCreateMap will create a JSON map for the Create operation.
+func (opts CreateOpts) ToBackupCreateMap() (map[string]interface{}, error) {
+	if opts.Name == "" {
+		return nil, errors.New("Name is a required field")
+	}
+	if opts.InstanceID == "" {
+		return nil, errors.New("InstanceID is a required field")
+	}
+
+	backup := map[string]interface{}{
+		"name":     opts.Name,
+		"instance": opts.InstanceID,
+	}
+
+	if opts.Description != "" {
+		backup["description"] = opts.Description
+	}
+
+	return map[string]interface{}{"backup": backup}, nil
+}
+
+// Create asynchronously creates a new backup for a specified database instance.
+// During the backup process, write access on MyISAM databases will be
+// temporarily disabled; innoDB databases will be unaffected. During this time,
+// you will not be able to add or delete databases or users; nor delete, stop
+// or reboot the instance itself. Only one backup is permitted at once.
+//
+// Backups are not deleted when database instances are deleted; you must
+// manually delete any backups created using Delete(). Backups are saved to your
+// Cloud Files account in a new container called z_CLOUDDB_BACKUPS. It is
+// strongly recommended you do not alter this container or its contents; usual
+// storage costs apply.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToBackupCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("POST", baseURL(client), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{202},
+	})
+
+	return res
+}
+
+// ListOptsBuilder is the top-level interface for creating query strings.
+type ListOptsBuilder interface {
+	ToBackupListQuery() (string, error)
+}
+
+// ListOpts allows you to refine a list search by certain parameters.
+type ListOpts struct {
+	// The type of datastore by which to filter.
+	Datastore string `q:"datastore"`
+}
+
+// ToBackupListQuery converts a ListOpts struct into a query string.
+func (opts ListOpts) ToBackupListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List will list all the saved backups for all database instances.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := baseURL(client)
+
+	if opts != nil {
+		query, err := opts.ToBackupListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return BackupPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, pageFn)
+}
+
+// Get will retrieve details for a particular backup based on its unique ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+
+	_, res.Err = client.Request("GET", resourceURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a backup.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+
+	_, res.Err = client.Request("DELETE", resourceURL(client, id), gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return res
+}
diff --git a/rackspace/db/v1/backups/requests_test.go b/rackspace/db/v1/backups/requests_test.go
new file mode 100644
index 0000000..d706733
--- /dev/null
+++ b/rackspace/db/v1/backups/requests_test.go
@@ -0,0 +1,131 @@
+package backups
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	backupID = "{backupID}"
+	_rootURL = "/backups"
+	resURL   = _rootURL + "/" + backupID
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _rootURL, "POST", createReq, createResp, 202)
+
+	opts := CreateOpts{
+		Name:        "snapshot",
+		Description: "My Backup",
+		InstanceID:  "d4603f69-ec7e-4e9b-803f-600b9205576f",
+	}
+
+	instance, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Backup{
+		Created:     timeVal,
+		Description: "My Backup",
+		ID:          "61f12fef-edb1-4561-8122-e7c00ef26a82",
+		InstanceID:  "d4603f69-ec7e-4e9b-803f-600b9205576f",
+		LocationRef: "",
+		Name:        "snapshot",
+		ParentID:    "",
+		Size:        100,
+		Status:      "NEW",
+		Updated:     timeVal,
+		Datastore: datastores.DatastorePartial{
+			Version:   "5.1",
+			Type:      "MySQL",
+			VersionID: "20000000-0000-0000-0000-000000000002",
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, instance)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _rootURL, "GET", "", listResp, 200)
+
+	pages := 0
+
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		actual, err := ExtractBackups(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Backup{
+			Backup{
+				Created:     timeVal,
+				Description: "Backup from Restored Instance",
+				ID:          "87972694-4be2-40f5-83f8-501656e0032a",
+				InstanceID:  "29af2cd9-0674-48ab-b87a-b160f00208e6",
+				LocationRef: "http://localhost/path/to/backup",
+				Name:        "restored_backup",
+				ParentID:    "",
+				Size:        0.141026,
+				Status:      "COMPLETED",
+				Updated:     timeVal,
+				Datastore: datastores.DatastorePartial{
+					Version:   "5.1",
+					Type:      "MySQL",
+					VersionID: "20000000-0000-0000-0000-000000000002",
+				},
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "GET", "", getResp, 200)
+
+	instance, err := Get(fake.ServiceClient(), backupID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Backup{
+		Created:     timeVal,
+		Description: "My Backup",
+		ID:          "61f12fef-edb1-4561-8122-e7c00ef26a82",
+		InstanceID:  "d4603f69-ec7e-4e9b-803f-600b9205576f",
+		LocationRef: "",
+		Name:        "snapshot",
+		ParentID:    "",
+		Size:        100,
+		Status:      "NEW",
+		Updated:     timeVal,
+		Datastore: datastores.DatastorePartial{
+			Version:   "5.1",
+			Type:      "MySQL",
+			VersionID: "20000000-0000-0000-0000-000000000002",
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, instance)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+
+	err := Delete(fake.ServiceClient(), backupID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/backups/results.go b/rackspace/db/v1/backups/results.go
new file mode 100644
index 0000000..04faf32
--- /dev/null
+++ b/rackspace/db/v1/backups/results.go
@@ -0,0 +1,149 @@
+package backups
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Status represents the various states a Backup can be in.
+type Status string
+
+// Enum types for the status.
+const (
+	StatusNew          Status = "NEW"
+	StatusBuilding     Status = "BUILDING"
+	StatusCompleted    Status = "COMPLETED"
+	StatusFailed       Status = "FAILED"
+	StatusDeleteFailed Status = "DELETE_FAILED"
+)
+
+// Backup represents a Backup API resource.
+type Backup struct {
+	Description string
+	ID          string
+	InstanceID  string `json:"instance_id" mapstructure:"instance_id"`
+	LocationRef string
+	Name        string
+	ParentID    string `json:"parent_id" mapstructure:"parent_id"`
+	Size        float64
+	Status      Status
+	Created     time.Time `mapstructure:"-"`
+	Updated     time.Time `mapstructure:"-"`
+	Datastore   datastores.DatastorePartial
+}
+
+// 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 struct {
+	gophercloud.ErrResult
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will retrieve a Backup struct from an operation's result.
+func (r commonResult) Extract() (*Backup, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Backup Backup `mapstructure:"backup"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	val := r.Body.(map[string]interface{})["backup"].(map[string]interface{})
+
+	if t, ok := val["created"].(string); ok && t != "" {
+		creationTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Backup, err
+		}
+		response.Backup.Created = creationTime
+	}
+
+	if t, ok := val["updated"].(string); ok && t != "" {
+		updatedTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Backup, err
+		}
+		response.Backup.Updated = updatedTime
+	}
+
+	return &response.Backup, err
+}
+
+// BackupPage represents a page of backups.
+type BackupPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an BackupPage struct is empty.
+func (r BackupPage) IsEmpty() (bool, error) {
+	is, err := ExtractBackups(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractBackups will retrieve a slice of Backup structs from a paginated collection.
+func ExtractBackups(page pagination.Page) ([]Backup, error) {
+	casted := page.(BackupPage).Body
+
+	var resp struct {
+		Backups []Backup `mapstructure:"backups" json:"backups"`
+	}
+
+	if err := mapstructure.Decode(casted, &resp); err != nil {
+		return nil, err
+	}
+
+	var vals []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		vals = casted.(map[string]interface{})["backups"].([]interface{})
+	case map[string][]interface{}:
+		vals = casted.(map[string][]interface{})["backups"]
+	default:
+		return resp.Backups, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i, v := range vals {
+		val := v.(map[string]interface{})
+
+		if t, ok := val["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Backups, err
+			}
+			resp.Backups[i].Created = creationTime
+		}
+
+		if t, ok := val["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Backups, err
+			}
+			resp.Backups[i].Updated = updatedTime
+		}
+	}
+
+	return resp.Backups, nil
+}
diff --git a/rackspace/db/v1/backups/urls.go b/rackspace/db/v1/backups/urls.go
new file mode 100644
index 0000000..553444e
--- /dev/null
+++ b/rackspace/db/v1/backups/urls.go
@@ -0,0 +1,11 @@
+package backups
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("backups")
+}
+
+func resourceURL(c *gophercloud.ServiceClient, backupID string) string {
+	return c.ServiceURL("backups", backupID)
+}
diff --git a/rackspace/db/v1/configurations/delegate.go b/rackspace/db/v1/configurations/delegate.go
new file mode 100644
index 0000000..d8cb48a
--- /dev/null
+++ b/rackspace/db/v1/configurations/delegate.go
@@ -0,0 +1,79 @@
+package configurations
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all of the available configurations.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// Create will create a new configuration group.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, opts)
+}
+
+// Get will retrieve the details for a specified configuration group.
+func Get(client *gophercloud.ServiceClient, configID string) os.GetResult {
+	return os.Get(client, configID)
+}
+
+// Update will modify an existing configuration group by performing a merge
+// between new and existing values. If the key already exists, the new value
+// will overwrite. All other keys will remain unaffected.
+func Update(client *gophercloud.ServiceClient, configID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(client, configID, opts)
+}
+
+// Replace will modify an existing configuration group by overwriting the
+// entire parameter group with the new values provided. Any existing keys not
+// included in UpdateOptsBuilder will be deleted.
+func Replace(client *gophercloud.ServiceClient, configID string, opts os.UpdateOptsBuilder) os.ReplaceResult {
+	return os.Replace(client, configID, opts)
+}
+
+// Delete will permanently delete a configuration group. Please note that
+// config groups cannot be deleted whilst still attached to running instances -
+// you must detach and then delete them.
+func Delete(client *gophercloud.ServiceClient, configID string) os.DeleteResult {
+	return os.Delete(client, configID)
+}
+
+// ListInstances will list all the instances associated with a particular
+// configuration group.
+func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
+	return os.ListInstances(client, configID)
+}
+
+// ListDatastoreParams will list all the available and supported parameters
+// that can be used for a particular datastore ID and a particular version.
+// For example, if you are wondering how you can configure a MySQL 5.6 instance,
+// you can use this operation (you will need to retrieve the MySQL datastore ID
+// by using the datastores API).
+func ListDatastoreParams(client *gophercloud.ServiceClient, datastoreID, versionID string) pagination.Pager {
+	return os.ListDatastoreParams(client, datastoreID, versionID)
+}
+
+// GetDatastoreParam will retrieve information about a specific configuration
+// parameter. For example, you can use this operation to understand more about
+// "innodb_file_per_table" configuration param for MySQL datastores. You will
+// need the param's ID first, which can be attained by using the ListDatastoreParams
+// operation.
+func GetDatastoreParam(client *gophercloud.ServiceClient, datastoreID, versionID, paramID string) os.ParamResult {
+	return os.GetDatastoreParam(client, datastoreID, versionID, paramID)
+}
+
+// ListGlobalParams is similar to ListDatastoreParams but does not require a
+// DatastoreID.
+func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
+	return os.ListGlobalParams(client, versionID)
+}
+
+// GetGlobalParam is similar to GetDatastoreParam but does not require a
+// DatastoreID.
+func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) os.ParamResult {
+	return os.GetGlobalParam(client, versionID, paramID)
+}
diff --git a/rackspace/db/v1/configurations/delegate_test.go b/rackspace/db/v1/configurations/delegate_test.go
new file mode 100644
index 0000000..580f02a
--- /dev/null
+++ b/rackspace/db/v1/configurations/delegate_test.go
@@ -0,0 +1,237 @@
+package configurations
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/instances"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	configID = "{configID}"
+	_baseURL = "/configurations"
+	resURL   = _baseURL + "/" + configID
+
+	dsID               = "{datastoreID}"
+	versionID          = "{versionID}"
+	paramID            = "{paramID}"
+	dsParamListURL     = "/datastores/" + dsID + "/versions/" + versionID + "/parameters"
+	dsParamGetURL      = "/datastores/" + dsID + "/versions/" + versionID + "/parameters/" + paramID
+	globalParamListURL = "/datastores/versions/" + versionID + "/parameters"
+	globalParamGetURL  = "/datastores/versions/" + versionID + "/parameters/" + paramID
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _baseURL, "GET", "", listConfigsJSON, 200)
+
+	count := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractConfigs(page)
+		th.AssertNoErr(t, err)
+
+		expected := []os.Config{exampleConfig}
+		th.AssertDeepEquals(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()
+	fixture.SetupHandler(t, resURL, "GET", "", getConfigJSON, 200)
+
+	config, err := Get(fake.ServiceClient(), configID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &exampleConfig, config)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _baseURL, "POST", createReq, createConfigJSON, 200)
+
+	opts := os.CreateOpts{
+		Datastore: &os.DatastoreOpts{
+			Type:    "a00000a0-00a0-0a00-00a0-000a000000aa",
+			Version: "b00000b0-00b0-0b00-00b0-000b000000bb",
+		},
+		Description: "example description",
+		Name:        "example-configuration-name",
+		Values: map[string]interface{}{
+			"collation_server": "latin1_swedish_ci",
+			"connect_timeout":  120,
+		},
+	}
+
+	config, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &exampleConfigWithValues, config)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "PATCH", updateReq, "", 200)
+
+	opts := os.UpdateOpts{
+		Values: map[string]interface{}{
+			"connect_timeout": 300,
+		},
+	}
+
+	err := Update(fake.ServiceClient(), configID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestReplace(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "PUT", updateReq, "", 202)
+
+	opts := os.UpdateOpts{
+		Values: map[string]interface{}{
+			"connect_timeout": 300,
+		},
+	}
+
+	err := Replace(fake.ServiceClient(), configID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "DELETE", "", "", 202)
+
+	err := Delete(fake.ServiceClient(), configID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListInstances(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL+"/instances", "GET", "", listInstancesJSON, 200)
+
+	expectedInstance := instances.Instance{
+		ID:   "d4603f69-ec7e-4e9b-803f-600b9205576f",
+		Name: "json_rack_instance",
+	}
+
+	pages := 0
+	err := ListInstances(fake.ServiceClient(), configID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := instances.ExtractInstances(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.AssertDeepEquals(t, actual, []instances.Instance{expectedInstance})
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestListDSParams(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, dsParamListURL, "GET", "", listParamsJSON, 200)
+
+	pages := 0
+	err := ListDatastoreParams(fake.ServiceClient(), dsID, versionID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := os.ExtractParams(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []os.Param{
+			os.Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+			os.Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+			os.Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+			os.Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+		}
+
+		th.AssertDeepEquals(t, actual, expected)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetDSParam(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, dsParamGetURL, "GET", "", getParamJSON, 200)
+
+	param, err := GetDatastoreParam(fake.ServiceClient(), dsID, versionID, paramID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &os.Param{
+		Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+	}
+
+	th.AssertDeepEquals(t, expected, param)
+}
+
+func TestListGlobalParams(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, globalParamListURL, "GET", "", listParamsJSON, 200)
+
+	pages := 0
+	err := ListGlobalParams(fake.ServiceClient(), versionID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := os.ExtractParams(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []os.Param{
+			os.Param{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+			os.Param{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+			os.Param{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+			os.Param{Max: 4294967296, Min: 0, Name: "join_buffer_size", RestartRequired: false, Type: "integer"},
+		}
+
+		th.AssertDeepEquals(t, actual, expected)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetGlobalParam(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, globalParamGetURL, "GET", "", getParamJSON, 200)
+
+	param, err := GetGlobalParam(fake.ServiceClient(), versionID, paramID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &os.Param{
+		Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer",
+	}
+
+	th.AssertDeepEquals(t, expected, param)
+}
diff --git a/rackspace/db/v1/configurations/doc.go b/rackspace/db/v1/configurations/doc.go
new file mode 100644
index 0000000..48c51d6
--- /dev/null
+++ b/rackspace/db/v1/configurations/doc.go
@@ -0,0 +1 @@
+package configurations
diff --git a/rackspace/db/v1/configurations/fixtures.go b/rackspace/db/v1/configurations/fixtures.go
new file mode 100644
index 0000000..d8a2233
--- /dev/null
+++ b/rackspace/db/v1/configurations/fixtures.go
@@ -0,0 +1,159 @@
+package configurations
+
+import (
+	"fmt"
+	"time"
+
+	os "github.com/rackspace/gophercloud/openstack/db/v1/configurations"
+)
+
+var (
+	timestamp  = "2015-11-12T14:22:42Z"
+	timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var singleConfigJSON = `
+{
+  "created": "` + timestamp + `",
+  "datastore_name": "mysql",
+  "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+  "datastore_version_name": "5.6",
+  "description": "example_description",
+  "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+  "name": "example-configuration-name",
+  "updated": "` + timestamp + `"
+}
+`
+
+var singleConfigWithValuesJSON = `
+{
+  "created": "` + timestamp + `",
+  "datastore_name": "mysql",
+  "datastore_version_id": "b00000b0-00b0-0b00-00b0-000b000000bb",
+  "datastore_version_name": "5.6",
+  "description": "example description",
+  "id": "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+  "instance_count": 0,
+  "name": "example-configuration-name",
+  "updated": "` + timestamp + `",
+  "values": {
+    "collation_server": "latin1_swedish_ci",
+    "connect_timeout": 120
+  }
+}
+`
+
+var (
+	listConfigsJSON  = fmt.Sprintf(`{"configurations": [%s]}`, singleConfigJSON)
+	getConfigJSON    = fmt.Sprintf(`{"configuration": %s}`, singleConfigJSON)
+	createConfigJSON = fmt.Sprintf(`{"configuration": %s}`, singleConfigWithValuesJSON)
+)
+
+var createReq = `
+{
+  "configuration": {
+    "datastore": {
+      "type": "a00000a0-00a0-0a00-00a0-000a000000aa",
+      "version": "b00000b0-00b0-0b00-00b0-000b000000bb"
+    },
+    "description": "example description",
+    "name": "example-configuration-name",
+    "values": {
+      "collation_server": "latin1_swedish_ci",
+      "connect_timeout": 120
+    }
+  }
+}
+`
+
+var updateReq = `
+{
+  "configuration": {
+    "values": {
+      "connect_timeout": 300
+    }
+  }
+}
+`
+
+var listInstancesJSON = `
+{
+  "instances": [
+    {
+      "id": "d4603f69-ec7e-4e9b-803f-600b9205576f",
+      "name": "json_rack_instance"
+    }
+  ]
+}
+`
+
+var listParamsJSON = `
+{
+  "configuration-parameters": [
+    {
+      "max": 1,
+      "min": 0,
+      "name": "innodb_file_per_table",
+      "restart_required": true,
+      "type": "integer"
+    },
+    {
+      "max": 4294967296,
+      "min": 0,
+      "name": "key_buffer_size",
+      "restart_required": false,
+      "type": "integer"
+    },
+    {
+      "max": 65535,
+      "min": 2,
+      "name": "connect_timeout",
+      "restart_required": false,
+      "type": "integer"
+    },
+    {
+      "max": 4294967296,
+      "min": 0,
+      "name": "join_buffer_size",
+      "restart_required": false,
+      "type": "integer"
+    }
+  ]
+}
+`
+
+var getParamJSON = `
+{
+  "max": 1,
+  "min": 0,
+  "name": "innodb_file_per_table",
+  "restart_required": true,
+  "type": "integer"
+}
+`
+
+var exampleConfig = os.Config{
+	Created:              timeVal,
+	DatastoreName:        "mysql",
+	DatastoreVersionID:   "b00000b0-00b0-0b00-00b0-000b000000bb",
+	DatastoreVersionName: "5.6",
+	Description:          "example_description",
+	ID:                   "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+	Name:                 "example-configuration-name",
+	Updated:              timeVal,
+}
+
+var exampleConfigWithValues = os.Config{
+	Created:              timeVal,
+	DatastoreName:        "mysql",
+	DatastoreVersionID:   "b00000b0-00b0-0b00-00b0-000b000000bb",
+	DatastoreVersionName: "5.6",
+	Description:          "example description",
+	ID:                   "005a8bb7-a8df-40ee-b0b7-fc144641abc2",
+	Name:                 "example-configuration-name",
+	Updated:              timeVal,
+	Values: map[string]interface{}{
+		"collation_server": "latin1_swedish_ci",
+		"connect_timeout":  120,
+	},
+}
diff --git a/rackspace/db/v1/databases/delegate.go b/rackspace/db/v1/databases/delegate.go
new file mode 100644
index 0000000..56552d1
--- /dev/null
+++ b/rackspace/db/v1/databases/delegate.go
@@ -0,0 +1,19 @@
+package databases
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func Create(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, instanceID, opts)
+}
+
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+	return os.List(client, instanceID)
+}
+
+func Delete(client *gophercloud.ServiceClient, instanceID, dbName string) os.DeleteResult {
+	return os.Delete(client, instanceID, dbName)
+}
diff --git a/rackspace/db/v1/databases/delegate_test.go b/rackspace/db/v1/databases/delegate_test.go
new file mode 100644
index 0000000..b9e50a5
--- /dev/null
+++ b/rackspace/db/v1/databases/delegate_test.go
@@ -0,0 +1,71 @@
+package databases
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+var (
+	instanceID = "{instanceID}"
+	rootURL    = "/instances"
+	resURL     = rootURL + "/" + instanceID
+	uRootURL   = resURL + "/root"
+	aURL       = resURL + "/action"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreate(t)
+
+	opts := os.BatchCreateOpts{
+		os.CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"},
+		os.CreateOpts{Name: "sampledb"},
+	}
+
+	res := Create(fake.ServiceClient(), instanceID, opts)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleList(t)
+
+	expectedDBs := []os.Database{
+		os.Database{Name: "anotherexampledb"},
+		os.Database{Name: "exampledb"},
+		os.Database{Name: "nextround"},
+		os.Database{Name: "sampledb"},
+		os.Database{Name: "testingdb"},
+	}
+
+	pages := 0
+	err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := os.ExtractDBs(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, expectedDBs, actual)
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDelete(t)
+
+	err := os.Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/databases/doc.go b/rackspace/db/v1/databases/doc.go
new file mode 100644
index 0000000..1a178b6
--- /dev/null
+++ b/rackspace/db/v1/databases/doc.go
@@ -0,0 +1,3 @@
+// Package databases provides information and interaction with the database API
+// resource in the Rackspace Database service.
+package databases
diff --git a/rackspace/db/v1/databases/urls.go b/rackspace/db/v1/databases/urls.go
new file mode 100644
index 0000000..18cbec7
--- /dev/null
+++ b/rackspace/db/v1/databases/urls.go
@@ -0,0 +1 @@
+package databases
diff --git a/rackspace/db/v1/datastores/delegate.go b/rackspace/db/v1/datastores/delegate.go
new file mode 100644
index 0000000..573496d
--- /dev/null
+++ b/rackspace/db/v1/datastores/delegate.go
@@ -0,0 +1,28 @@
+package datastores
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available flavors.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// Get retrieves the details for a particular flavor.
+func Get(client *gophercloud.ServiceClient, flavorID string) os.GetResult {
+	return os.Get(client, flavorID)
+}
+
+// ListVersions will list all of the available versions for a specified
+// datastore type.
+func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
+	return os.ListVersions(client, datastoreID)
+}
+
+// GetVersion will retrieve the details of a specified datastore version.
+func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) os.GetVersionResult {
+	return os.GetVersion(client, datastoreID, versionID)
+}
diff --git a/rackspace/db/v1/datastores/delegate_test.go b/rackspace/db/v1/datastores/delegate_test.go
new file mode 100644
index 0000000..71111b9
--- /dev/null
+++ b/rackspace/db/v1/datastores/delegate_test.go
@@ -0,0 +1,79 @@
+package datastores
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores", "GET", "", os.ListDSResp, 200)
+
+	pages := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := os.ExtractDatastores(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, []os.Datastore{os.ExampleDatastore}, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores/{dsID}", "GET", "", os.GetDSResp, 200)
+
+	ds, err := Get(fake.ServiceClient(), "{dsID}").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &os.ExampleDatastore, ds)
+}
+
+func TestListVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores/{dsID}/versions", "GET", "", os.ListVersionsResp, 200)
+
+	pages := 0
+
+	err := ListVersions(fake.ServiceClient(), "{dsID}").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := os.ExtractVersions(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, os.ExampleVersions, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetVersion(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores/{dsID}/versions/{versionID}", "GET", "", os.GetVersionResp, 200)
+
+	ds, err := GetVersion(fake.ServiceClient(), "{dsID}", "{versionID}").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &os.ExampleVersion1, ds)
+}
diff --git a/rackspace/db/v1/datastores/doc.go b/rackspace/db/v1/datastores/doc.go
new file mode 100644
index 0000000..f36997a
--- /dev/null
+++ b/rackspace/db/v1/datastores/doc.go
@@ -0,0 +1 @@
+package datastores
diff --git a/rackspace/db/v1/flavors/delegate.go b/rackspace/db/v1/flavors/delegate.go
new file mode 100644
index 0000000..689b81e
--- /dev/null
+++ b/rackspace/db/v1/flavors/delegate.go
@@ -0,0 +1,17 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available flavors.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// Get retrieves the details for a particular flavor.
+func Get(client *gophercloud.ServiceClient, flavorID string) os.GetResult {
+	return os.Get(client, flavorID)
+}
diff --git a/rackspace/db/v1/flavors/delegate_test.go b/rackspace/db/v1/flavors/delegate_test.go
new file mode 100644
index 0000000..f5f6442
--- /dev/null
+++ b/rackspace/db/v1/flavors/delegate_test.go
@@ -0,0 +1,95 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleList(t)
+
+	pages := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := os.ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []os.Flavor{
+			os.Flavor{
+				ID:   "1",
+				Name: "m1.tiny",
+				RAM:  512,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/1", Rel: "bookmark"},
+				},
+			},
+			os.Flavor{
+				ID:   "2",
+				Name: "m1.small",
+				RAM:  1024,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/2", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/2", Rel: "bookmark"},
+				},
+			},
+			os.Flavor{
+				ID:   "3",
+				Name: "m1.medium",
+				RAM:  2048,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/3", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/3", Rel: "bookmark"},
+				},
+			},
+			os.Flavor{
+				ID:   "4",
+				Name: "m1.large",
+				RAM:  4096,
+				Links: []gophercloud.Link{
+					gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/4", Rel: "self"},
+					gophercloud.Link{Href: "https://openstack.example.com/flavors/4", Rel: "bookmark"},
+				},
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	if pages != 1 {
+		t.Errorf("Expected one page, got %d", pages)
+	}
+}
+
+func TestGetFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGet(t)
+
+	actual, err := Get(fake.ServiceClient(), "{flavorID}").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &os.Flavor{
+		ID:   "1",
+		Name: "m1.tiny",
+		RAM:  512,
+		Links: []gophercloud.Link{
+			gophercloud.Link{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/db/v1/flavors/doc.go b/rackspace/db/v1/flavors/doc.go
new file mode 100644
index 0000000..922a4e6
--- /dev/null
+++ b/rackspace/db/v1/flavors/doc.go
@@ -0,0 +1,3 @@
+// Package flavors provides information and interaction with the flavor API
+// resource in the Rackspace Database service.
+package flavors
diff --git a/rackspace/db/v1/instances/delegate.go b/rackspace/db/v1/instances/delegate.go
new file mode 100644
index 0000000..f2656fe
--- /dev/null
+++ b/rackspace/db/v1/instances/delegate.go
@@ -0,0 +1,49 @@
+package instances
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+)
+
+// Get retrieves the status and information for a specified database instance.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
+
+// Delete permanently destroys the database instance.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(client, id)
+}
+
+// EnableRootUser enables the login from any host for the root user and
+// provides the user with a generated root password.
+func EnableRootUser(client *gophercloud.ServiceClient, id string) os.UserRootResult {
+	return os.EnableRootUser(client, id)
+}
+
+// IsRootEnabled checks an instance to see if root access is enabled. It returns
+// True if root user is enabled for the specified database instance or False
+// otherwise.
+func IsRootEnabled(client *gophercloud.ServiceClient, id string) (bool, error) {
+	return os.IsRootEnabled(client, id)
+}
+
+// Restart will restart only the MySQL Instance. Restarting MySQL will
+// erase any dynamic configuration settings that you have made within MySQL.
+// The MySQL service will be unavailable until the instance restarts.
+func Restart(client *gophercloud.ServiceClient, id string) os.ActionResult {
+	return os.Restart(client, id)
+}
+
+// Resize changes the memory size of the instance, assuming a valid
+// flavorRef is provided. It will also restart the MySQL service.
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) os.ActionResult {
+	return os.Resize(client, id, flavorRef)
+}
+
+// ResizeVolume will resize the attached volume for an instance. It supports
+// only increasing the volume size and does not support decreasing the size.
+// The volume size is in gigabytes (GB) and must be an integer.
+func ResizeVolume(client *gophercloud.ServiceClient, id string, size int) os.ActionResult {
+	return os.ResizeVolume(client, id, size)
+}
diff --git a/rackspace/db/v1/instances/delegate_test.go b/rackspace/db/v1/instances/delegate_test.go
new file mode 100644
index 0000000..716e0a4
--- /dev/null
+++ b/rackspace/db/v1/instances/delegate_test.go
@@ -0,0 +1,107 @@
+package instances
+
+import (
+	"testing"
+
+	osDBs "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	osUsers "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	_rootURL = "/instances"
+	resURL   = "/instances/" + instanceID
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _rootURL, "POST", createReq, createResp, 200)
+
+	opts := CreateOpts{
+		Name:      "json_rack_instance",
+		FlavorRef: "1",
+		Databases: osDBs.BatchCreateOpts{
+			osDBs.CreateOpts{CharSet: "utf8", Collate: "utf8_general_ci", Name: "sampledb"},
+			osDBs.CreateOpts{Name: "nextround"},
+		},
+		Users: osUsers.BatchCreateOpts{
+			osUsers.CreateOpts{
+				Name:     "demouser",
+				Password: "demopassword",
+				Databases: osDBs.BatchCreateOpts{
+					osDBs.CreateOpts{Name: "sampledb"},
+				},
+			},
+		},
+		Size:         2,
+		RestorePoint: "1234567890",
+	}
+
+	instance, err := Create(fake.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expectedInstance, instance)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "GET", "", getResp, 200)
+
+	instance, err := Get(fake.ServiceClient(), instanceID).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expectedInstance, instance)
+}
+
+func TestDeleteInstance(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDelete(t)
+
+	res := Delete(fake.ServiceClient(), instanceID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestEnableRootUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleEnableRoot(t)
+
+	expected := &osUsers.User{Name: "root", Password: "secretsecret"}
+
+	user, err := EnableRootUser(fake.ServiceClient(), instanceID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestRestart(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleRestart(t)
+
+	res := Restart(fake.ServiceClient(), instanceID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleResize(t)
+
+	res := Resize(fake.ServiceClient(), instanceID, "2")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResizeVolume(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleResizeVol(t)
+
+	res := ResizeVolume(fake.ServiceClient(), instanceID, 4)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/db/v1/instances/doc.go b/rackspace/db/v1/instances/doc.go
new file mode 100644
index 0000000..0c8ad63
--- /dev/null
+++ b/rackspace/db/v1/instances/doc.go
@@ -0,0 +1,3 @@
+// Package instances provides information and interaction with the instance API
+// resource in the Rackspace Database service.
+package instances
diff --git a/rackspace/db/v1/instances/fixtures.go b/rackspace/db/v1/instances/fixtures.go
new file mode 100644
index 0000000..c5ff37a
--- /dev/null
+++ b/rackspace/db/v1/instances/fixtures.go
@@ -0,0 +1,340 @@
+package instances
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+)
+
+var (
+	timestamp  = "2015-11-12T14:22:42Z"
+	timeVal, _ = time.Parse(time.RFC3339, timestamp)
+)
+
+var instance = `
+{
+  "created": "` + timestamp + `",
+  "datastore": {
+    "type": "mysql",
+    "version": "5.6"
+  },
+  "flavor": {
+    "id": "1",
+    "links": [
+      {
+        "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
+        "rel": "self"
+      },
+      {
+        "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
+        "rel": "bookmark"
+      }
+    ]
+  },
+  "links": [
+    {
+      "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1",
+      "rel": "self"
+    }
+  ],
+  "hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.rackspaceclouddb.com",
+  "id": "{instanceID}",
+  "name": "json_rack_instance",
+  "status": "BUILD",
+  "updated": "` + timestamp + `",
+  "volume": {
+    "size": 2
+  }
+}
+`
+
+var createReq = `
+{
+  "instance": {
+    "databases": [
+      {
+        "character_set": "utf8",
+        "collate": "utf8_general_ci",
+        "name": "sampledb"
+      },
+      {
+        "name": "nextround"
+      }
+    ],
+    "flavorRef": "1",
+    "name": "json_rack_instance",
+    "users": [
+      {
+        "databases": [
+          {
+            "name": "sampledb"
+          }
+        ],
+        "name": "demouser",
+        "password": "demopassword"
+      }
+    ],
+    "volume": {
+      "size": 2
+    },
+    "restorePoint": {
+      "backupRef": "1234567890"
+    }
+  }
+}
+`
+
+var createReplicaReq = `
+{
+  "instance": {
+    "volume": {
+      "size": 1
+    },
+    "flavorRef": "9",
+    "name": "t2s1_ALT_GUEST",
+    "replica_of": "6bdca2fc-418e-40bd-a595-62abda61862d"
+  }
+}
+`
+
+var createReplicaResp = `
+{
+  "instance": {
+    "status": "BUILD",
+    "updated": "` + timestamp + `",
+    "name": "t2s1_ALT_GUEST",
+    "links": [
+      {
+        "href": "https://ord.databases.api.rackspacecloud.com/v1.0/5919009/instances/8367c312-7c40-4a66-aab1-5767478914fc",
+        "rel": "self"
+      },
+      {
+        "href": "https://ord.databases.api.rackspacecloud.com/instances/8367c312-7c40-4a66-aab1-5767478914fc",
+        "rel": "bookmark"
+      }
+    ],
+    "created": "` + timestamp + `",
+    "id": "8367c312-7c40-4a66-aab1-5767478914fc",
+    "volume": {
+      "size": 1
+    },
+    "flavor": {
+      "id": "9"
+    },
+    "datastore": {
+      "version": "5.6",
+      "type": "mysql"
+    },
+    "replica_of": {
+      "id": "6bdca2fc-418e-40bd-a595-62abda61862d"
+    }
+  }
+}
+`
+
+var listReplicasResp = `
+{
+  "instances": [
+    {
+      "status": "ACTIVE",
+      "name": "t1s1_ALT_GUEST",
+      "links": [
+        {
+          "href": "https://ord.databases.api.rackspacecloud.com/v1.0/1234/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+          "rel": "self"
+        },
+        {
+          "href": "https://ord.databases.api.rackspacecloud.com/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+          "rel": "bookmark"
+        }
+      ],
+      "ip": [
+        "10.0.0.3"
+      ],
+      "id": "3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+      "volume": {
+        "size": 1
+      },
+      "flavor": {
+        "id": "9"
+      },
+      "datastore": {
+        "version": "5.6",
+        "type": "mysql"
+      },
+      "replica_of": {
+        "id": "8b499b45-52d6-402d-b398-f9d8f279c69a"
+      }
+    }
+  ]
+}
+`
+
+var getReplicaResp = `
+{
+  "instance": {
+    "status": "ACTIVE",
+    "updated": "` + timestamp + `",
+    "name": "t1_ALT_GUEST",
+    "created": "` + timestamp + `",
+    "ip": [
+      "10.0.0.2"
+    ],
+    "replicas": [
+      {
+        "id": "3c691f06-bf9a-4618-b7ec-2817ce0cf254"
+      }
+    ],
+    "id": "8b499b45-52d6-402d-b398-f9d8f279c69a",
+    "volume": {
+      "used": 0.54,
+      "size": 1
+    },
+    "flavor": {
+      "id": "9"
+    },
+    "datastore": {
+      "version": "5.6",
+      "type": "mysql"
+    }
+  }
+}
+`
+
+var detachReq = `
+{
+  "instance": {
+    "replica_of": "",
+    "slave_of": ""
+  }
+}
+`
+
+var getConfigResp = `
+{
+  "instance": {
+    "configuration": {
+      "basedir": "/usr",
+      "connect_timeout": "15",
+      "datadir": "/var/lib/mysql",
+      "default_storage_engine": "innodb",
+      "innodb_buffer_pool_instances": "1",
+      "innodb_buffer_pool_size": "175M",
+      "innodb_checksum_algorithm": "crc32",
+      "innodb_data_file_path": "ibdata1:10M:autoextend",
+      "innodb_file_per_table": "1",
+      "innodb_io_capacity": "200",
+      "innodb_log_file_size": "256M",
+      "innodb_log_files_in_group": "2",
+      "innodb_open_files": "8192",
+      "innodb_thread_concurrency": "0",
+      "join_buffer_size": "1M",
+      "key_buffer_size": "50M",
+      "local-infile": "0",
+      "log-error": "/var/log/mysql/mysqld.log",
+      "max_allowed_packet": "16M",
+      "max_connect_errors": "10000",
+      "max_connections": "40",
+      "max_heap_table_size": "16M",
+      "myisam-recover": "BACKUP",
+      "open_files_limit": "8192",
+      "performance_schema": "off",
+      "pid_file": "/var/run/mysqld/mysqld.pid",
+      "port": "3306",
+      "query_cache_limit": "1M",
+      "query_cache_size": "8M",
+      "query_cache_type": "1",
+      "read_buffer_size": "256K",
+      "read_rnd_buffer_size": "1M",
+      "server_id": "1",
+      "skip-external-locking": "1",
+      "skip_name_resolve": "1",
+      "sort_buffer_size": "256K",
+      "table_open_cache": "4096",
+      "thread_stack": "192K",
+      "tmp_table_size": "16M",
+      "tmpdir": "/var/tmp",
+      "user": "mysql",
+      "wait_timeout": "3600"
+    }
+  }
+}
+`
+
+var associateReq = `{"instance": {"configuration": "{configGroupID}"}}`
+
+var listBackupsResp = `
+{
+  "backups": [
+    {
+      "status": "COMPLETED",
+      "updated": "` + timestamp + `",
+      "description": "Backup from Restored Instance",
+      "datastore": {
+        "version": "5.1",
+        "type": "MySQL",
+        "version_id": "20000000-0000-0000-0000-000000000002"
+      },
+      "id": "87972694-4be2-40f5-83f8-501656e0032a",
+      "size": 0.141026,
+      "name": "restored_backup",
+      "created": "` + timestamp + `",
+      "instance_id": "29af2cd9-0674-48ab-b87a-b160f00208e6",
+      "parent_id": null,
+      "locationRef": "http://localhost/path/to/backup"
+    }
+  ]
+}
+`
+
+var (
+	createResp        = fmt.Sprintf(`{"instance":%s}`, instance)
+	getResp           = fmt.Sprintf(`{"instance":%s}`, instance)
+	associateResp     = fmt.Sprintf(`{"instance":%s}`, instance)
+	listInstancesResp = fmt.Sprintf(`{"instances":[%s]}`, instance)
+)
+
+var instanceID = "{instanceID}"
+
+var expectedInstance = &Instance{
+	Created:   timeVal,
+	Updated:   timeVal,
+	Datastore: datastores.DatastorePartial{Type: "mysql", Version: "5.6"},
+	Flavor: flavors.Flavor{
+		ID: "1",
+		Links: []gophercloud.Link{
+			gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "self"},
+			gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "bookmark"},
+		},
+	},
+	Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.rackspaceclouddb.com",
+	ID:       instanceID,
+	Links: []gophercloud.Link{
+		gophercloud.Link{Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/flavors/1", Rel: "self"},
+	},
+	Name:   "json_rack_instance",
+	Status: "BUILD",
+	Volume: os.Volume{Size: 2},
+}
+
+var expectedReplica = &Instance{
+	Status:  "BUILD",
+	Updated: timeVal,
+	Name:    "t2s1_ALT_GUEST",
+	Links: []gophercloud.Link{
+		gophercloud.Link{Rel: "self", Href: "https://ord.databases.api.rackspacecloud.com/v1.0/5919009/instances/8367c312-7c40-4a66-aab1-5767478914fc"},
+		gophercloud.Link{Rel: "bookmark", Href: "https://ord.databases.api.rackspacecloud.com/instances/8367c312-7c40-4a66-aab1-5767478914fc"},
+	},
+	Created:   timeVal,
+	ID:        "8367c312-7c40-4a66-aab1-5767478914fc",
+	Volume:    os.Volume{Size: 1},
+	Flavor:    flavors.Flavor{ID: "9"},
+	Datastore: datastores.DatastorePartial{Version: "5.6", Type: "mysql"},
+	ReplicaOf: &Instance{
+		ID: "6bdca2fc-418e-40bd-a595-62abda61862d",
+	},
+}
diff --git a/rackspace/db/v1/instances/requests.go b/rackspace/db/v1/instances/requests.go
new file mode 100644
index 0000000..f4df692
--- /dev/null
+++ b/rackspace/db/v1/instances/requests.go
@@ -0,0 +1,199 @@
+package instances
+
+import (
+	"github.com/rackspace/gophercloud"
+	osDBs "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	osUsers "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/backups"
+)
+
+// CreateOpts is the struct responsible for configuring a new database instance.
+type CreateOpts struct {
+	// Either the integer UUID (in string form) of the flavor, or its URI
+	// reference as specified in the response from the List() call. Required.
+	FlavorRef string
+
+	// Specifies the volume size in gigabytes (GB). The value must be between 1
+	// and 300. Required.
+	Size int
+
+	// Name of the instance to create. The length of the name is limited to
+	// 255 characters and any characters are permitted. Optional.
+	Name string
+
+	// A slice of database information options.
+	Databases osDBs.CreateOptsBuilder
+
+	// A slice of user information options.
+	Users osUsers.CreateOptsBuilder
+
+	// ID of the configuration group to associate with the instance. Optional.
+	ConfigID string
+
+	// Options to configure the type of datastore the instance will use. This is
+	// optional, and if excluded will default to MySQL.
+	Datastore *os.DatastoreOpts
+
+	// Specifies the backup ID from which to restore the database instance. There
+	// are some things to be aware of before using this field.  When you execute
+	// the Restore Backup operation, a new database instance is created to store
+	// the backup whose ID is specified by the restorePoint attribute. This will
+	// mean that:
+	// - All users, passwords and access that were on the instance at the time of
+	// the backup will be restored along with the databases.
+	// - You can create new users or databases if you want, but they cannot be
+	// the same as the ones from the instance that was backed up.
+	RestorePoint string
+
+	ReplicaOf string
+}
+
+func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
+	instance, err := os.CreateOpts{
+		FlavorRef: opts.FlavorRef,
+		Size:      opts.Size,
+		Name:      opts.Name,
+		Databases: opts.Databases,
+		Users:     opts.Users,
+	}.ToInstanceCreateMap()
+
+	if err != nil {
+		return nil, err
+	}
+
+	instance = instance["instance"].(map[string]interface{})
+
+	if opts.ConfigID != "" {
+		instance["configuration"] = opts.ConfigID
+	}
+
+	if opts.Datastore != nil {
+		ds, err := opts.Datastore.ToMap()
+		if err != nil {
+			return nil, err
+		}
+		instance["datastore"] = ds
+	}
+
+	if opts.RestorePoint != "" {
+		instance["restorePoint"] = map[string]string{"backupRef": opts.RestorePoint}
+	}
+
+	if opts.ReplicaOf != "" {
+		instance["replica_of"] = opts.ReplicaOf
+	}
+
+	return map[string]interface{}{"instance": instance}, nil
+}
+
+// Create asynchronously provisions a new database instance. It requires the
+// user to specify a flavor and a volume size. The API service then provisions
+// the instance with the requested flavor and sets up a volume of the specified
+// size, which is the storage for the database instance.
+//
+// Although this call only allows the creation of 1 instance per request, you
+// can create an instance with multiple databases and users. The default
+// binding for a MySQL instance is port 3306.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// ListOpts specifies all of the query options to be used when returning a list
+// of database instances.
+type ListOpts struct {
+	// IncludeHA includes or excludes High Availability instances from the result set
+	IncludeHA bool `q:"include_ha"`
+
+	// IncludeReplicas includes or excludes Replica instances from the result set
+	IncludeReplicas bool `q:"include_replicas"`
+}
+
+// ToInstanceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToInstanceListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List retrieves the status and information for all database instances.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+	url := baseURL(client)
+
+	if opts != nil {
+		query, err := opts.ToInstanceListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return os.InstancePage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// GetDefaultConfig lists the default configuration settings from the template
+// that was applied to the specified instance. In a sense, this is the vanilla
+// configuration setting applied to an instance. Further configuration can be
+// applied by associating an instance with a configuration group.
+func GetDefaultConfig(client *gophercloud.ServiceClient, id string) ConfigResult {
+	var res ConfigResult
+
+	_, res.Err = client.Request("GET", configURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// AssociateWithConfigGroup associates a specified instance to a specified
+// configuration group. If any of the parameters within a configuration group
+// require a restart, then the instance will transition into a restart.
+func AssociateWithConfigGroup(client *gophercloud.ServiceClient, instanceID, configGroupID string) UpdateResult {
+	reqBody := map[string]string{
+		"configuration": configGroupID,
+	}
+
+	var res UpdateResult
+
+	_, res.Err = client.Request("PUT", resourceURL(client, instanceID), gophercloud.RequestOpts{
+		JSONBody: map[string]map[string]string{"instance": reqBody},
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// DetachFromConfigGroup will detach an instance from all config groups.
+func DetachFromConfigGroup(client *gophercloud.ServiceClient, instanceID string) UpdateResult {
+	return AssociateWithConfigGroup(client, instanceID, "")
+}
+
+// ListBackups will list all the backups for a specified database instance.
+func ListBackups(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return backups.BackupPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, backupsURL(client, instanceID), pageFn)
+}
+
+// DetachReplica will detach a specified replica instance from its source
+// instance, effectively allowing it to operate independently. Detaching a
+// replica will restart the MySQL service on the instance.
+func DetachReplica(client *gophercloud.ServiceClient, replicaID string) DetachResult {
+	var res DetachResult
+
+	_, res.Err = client.Request("PATCH", resourceURL(client, replicaID), gophercloud.RequestOpts{
+		JSONBody: map[string]interface{}{"instance": map[string]string{"replica_of": "", "slave_of": ""}},
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
diff --git a/rackspace/db/v1/instances/requests_test.go b/rackspace/db/v1/instances/requests_test.go
new file mode 100644
index 0000000..7fa4601
--- /dev/null
+++ b/rackspace/db/v1/instances/requests_test.go
@@ -0,0 +1,246 @@
+package instances
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/db/v1/backups"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+func TestInstanceList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	fixture.SetupHandler(t, "/instances", "GET", "", listInstancesResp, 200)
+
+	opts := &ListOpts{
+		IncludeHA:       false,
+		IncludeReplicas: false,
+	}
+
+	pages := 0
+	err := List(fake.ServiceClient(), opts).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractInstances(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, []Instance{*expectedInstance}, actual)
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetConfig(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL+"/configuration", "GET", "", getConfigResp, 200)
+
+	config, err := GetDefaultConfig(fake.ServiceClient(), instanceID).Extract()
+
+	expected := map[string]string{
+		"basedir":                      "/usr",
+		"connect_timeout":              "15",
+		"datadir":                      "/var/lib/mysql",
+		"default_storage_engine":       "innodb",
+		"innodb_buffer_pool_instances": "1",
+		"innodb_buffer_pool_size":      "175M",
+		"innodb_checksum_algorithm":    "crc32",
+		"innodb_data_file_path":        "ibdata1:10M:autoextend",
+		"innodb_file_per_table":        "1",
+		"innodb_io_capacity":           "200",
+		"innodb_log_file_size":         "256M",
+		"innodb_log_files_in_group":    "2",
+		"innodb_open_files":            "8192",
+		"innodb_thread_concurrency":    "0",
+		"join_buffer_size":             "1M",
+		"key_buffer_size":              "50M",
+		"local-infile":                 "0",
+		"log-error":                    "/var/log/mysql/mysqld.log",
+		"max_allowed_packet":           "16M",
+		"max_connect_errors":           "10000",
+		"max_connections":              "40",
+		"max_heap_table_size":          "16M",
+		"myisam-recover":               "BACKUP",
+		"open_files_limit":             "8192",
+		"performance_schema":           "off",
+		"pid_file":                     "/var/run/mysqld/mysqld.pid",
+		"port":                         "3306",
+		"query_cache_limit":            "1M",
+		"query_cache_size":             "8M",
+		"query_cache_type":             "1",
+		"read_buffer_size":             "256K",
+		"read_rnd_buffer_size":         "1M",
+		"server_id":                    "1",
+		"skip-external-locking":        "1",
+		"skip_name_resolve":            "1",
+		"sort_buffer_size":             "256K",
+		"table_open_cache":             "4096",
+		"thread_stack":                 "192K",
+		"tmp_table_size":               "16M",
+		"tmpdir":                       "/var/tmp",
+		"user":                         "mysql",
+		"wait_timeout":                 "3600",
+	}
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, config)
+}
+
+func TestAssociateWithConfigGroup(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "PUT", associateReq, associateResp, 202)
+
+	res := AssociateWithConfigGroup(fake.ServiceClient(), instanceID, "{configGroupID}")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestListBackups(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL+"/backups", "GET", "", listBackupsResp, 200)
+
+	pages := 0
+
+	err := ListBackups(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		actual, err := backups.ExtractBackups(page)
+		th.AssertNoErr(t, err)
+
+		expected := []backups.Backup{
+			backups.Backup{
+				Created:     timeVal,
+				Description: "Backup from Restored Instance",
+				ID:          "87972694-4be2-40f5-83f8-501656e0032a",
+				InstanceID:  "29af2cd9-0674-48ab-b87a-b160f00208e6",
+				LocationRef: "http://localhost/path/to/backup",
+				Name:        "restored_backup",
+				ParentID:    "",
+				Size:        0.141026,
+				Status:      "COMPLETED",
+				Updated:     timeVal,
+				Datastore:   datastores.DatastorePartial{Version: "5.1", Type: "MySQL", VersionID: "20000000-0000-0000-0000-000000000002"},
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestCreateReplica(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _rootURL, "POST", createReplicaReq, createReplicaResp, 200)
+
+	opts := CreateOpts{
+		Name:      "t2s1_ALT_GUEST",
+		FlavorRef: "9",
+		Size:      1,
+		ReplicaOf: "6bdca2fc-418e-40bd-a595-62abda61862d",
+	}
+
+	replica, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expectedReplica, replica)
+}
+
+func TestListReplicas(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _rootURL, "GET", "", listReplicasResp, 200)
+
+	pages := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractInstances(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Instance{
+			Instance{
+				Status: "ACTIVE",
+				Name:   "t1s1_ALT_GUEST",
+				Links: []gophercloud.Link{
+					gophercloud.Link{Rel: "self", Href: "https://ord.databases.api.rackspacecloud.com/v1.0/1234/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254"},
+					gophercloud.Link{Rel: "bookmark", Href: "https://ord.databases.api.rackspacecloud.com/instances/3c691f06-bf9a-4618-b7ec-2817ce0cf254"},
+				},
+				ID:        "3c691f06-bf9a-4618-b7ec-2817ce0cf254",
+				IP:        []string{"10.0.0.3"},
+				Volume:    os.Volume{Size: 1},
+				Flavor:    flavors.Flavor{ID: "9"},
+				Datastore: datastores.DatastorePartial{Version: "5.6", Type: "mysql"},
+				ReplicaOf: &Instance{
+					ID: "8b499b45-52d6-402d-b398-f9d8f279c69a",
+				},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGetReplica(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "GET", "", getReplicaResp, 200)
+
+	replica, err := Get(fake.ServiceClient(), instanceID).Extract()
+	th.AssertNoErr(t, err)
+
+	expectedReplica := &Instance{
+		Status:  "ACTIVE",
+		Updated: timeVal,
+		Name:    "t1_ALT_GUEST",
+		Created: timeVal,
+		IP: []string{
+			"10.0.0.2",
+		},
+		Replicas: []Instance{
+			Instance{ID: "3c691f06-bf9a-4618-b7ec-2817ce0cf254"},
+		},
+		ID: "8b499b45-52d6-402d-b398-f9d8f279c69a",
+		Volume: os.Volume{
+			Used: 0.54,
+			Size: 1,
+		},
+		Flavor: flavors.Flavor{ID: "9"},
+		Datastore: datastores.DatastorePartial{
+			Version: "5.6",
+			Type:    "mysql",
+		},
+	}
+
+	th.AssertDeepEquals(t, replica, expectedReplica)
+}
+
+func TestDetachReplica(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, resURL, "PATCH", detachReq, "", 202)
+
+	err := DetachReplica(fake.ServiceClient(), instanceID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/instances/results.go b/rackspace/db/v1/instances/results.go
new file mode 100644
index 0000000..cdcc9c7
--- /dev/null
+++ b/rackspace/db/v1/instances/results.go
@@ -0,0 +1,191 @@
+package instances
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/db/v1/datastores"
+	"github.com/rackspace/gophercloud/openstack/db/v1/flavors"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/instances"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Instance represents a remote MySQL instance.
+type Instance struct {
+	// Indicates the datetime that the instance was created
+	Created time.Time `mapstructure:"-"`
+
+	// Indicates the most recent datetime that the instance was updated.
+	Updated time.Time `mapstructure:"-"`
+
+	// Indicates how the instance stores data.
+	Datastore datastores.DatastorePartial
+
+	// Indicates the hardware flavor the instance uses.
+	Flavor flavors.Flavor
+
+	// A DNS-resolvable hostname associated with the database instance (rather
+	// than an IPv4 address). Since the hostname always resolves to the correct
+	// IP address of the database instance, this relieves the user from the task
+	// of maintaining the mapping. Note that although the IP address may likely
+	// change on resizing, migrating, and so forth, the hostname always resolves
+	// to the correct database instance.
+	Hostname string
+
+	// Indicates the unique identifier for the instance resource.
+	ID string
+
+	// Exposes various links that reference the instance resource.
+	Links []gophercloud.Link
+
+	// The human-readable name of the instance.
+	Name string
+
+	// The build status of the instance.
+	Status string
+
+	// Information about the attached volume of the instance.
+	Volume os.Volume
+
+	// IP indicates the various IP addresses which allow access.
+	IP []string
+
+	// Indicates whether this instance is a replica of another source instance.
+	ReplicaOf *Instance `mapstructure:"replica_of" json:"replica_of"`
+
+	// Indicates whether this instance is the source of other replica instances.
+	Replicas []Instance
+}
+
+func commonExtract(err error, body interface{}) (*Instance, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var response struct {
+		Instance Instance `mapstructure:"instance"`
+	}
+
+	err = mapstructure.Decode(body, &response)
+
+	val := body.(map[string]interface{})["instance"].(map[string]interface{})
+
+	if t, ok := val["created"].(string); ok && t != "" {
+		creationTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Instance, err
+		}
+		response.Instance.Created = creationTime
+	}
+
+	if t, ok := val["updated"].(string); ok && t != "" {
+		updatedTime, err := time.Parse(time.RFC3339, t)
+		if err != nil {
+			return &response.Instance, err
+		}
+		response.Instance.Updated = updatedTime
+	}
+
+	return &response.Instance, err
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	os.CreateResult
+}
+
+// Extract will retrieve an instance from a create result.
+func (r CreateResult) Extract() (*Instance, error) {
+	return commonExtract(r.Err, r.Body)
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	os.GetResult
+}
+
+// Extract will extract an Instance from a GetResult.
+func (r GetResult) Extract() (*Instance, error) {
+	return commonExtract(r.Err, r.Body)
+}
+
+// ConfigResult represents the result of getting default configuration for an
+// instance.
+type ConfigResult struct {
+	gophercloud.Result
+}
+
+// DetachResult represents the result of detaching a replica from its source.
+type DetachResult struct {
+	gophercloud.ErrResult
+}
+
+// Extract will extract the configuration information (in the form of a map)
+// about a particular instance.
+func (r ConfigResult) Extract() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Instance struct {
+			Config map[string]string `mapstructure:"configuration"`
+		} `mapstructure:"instance"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return response.Instance.Config, err
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// ExtractInstances retrieves a slice of instances from a paginated collection.
+func ExtractInstances(page pagination.Page) ([]Instance, error) {
+	casted := page.(os.InstancePage).Body
+
+	var resp struct {
+		Instances []Instance `mapstructure:"instances"`
+	}
+
+	if err := mapstructure.Decode(casted, &resp); err != nil {
+		return nil, err
+	}
+
+	var vals []interface{}
+	switch casted.(type) {
+	case map[string]interface{}:
+		vals = casted.(map[string]interface{})["instances"].([]interface{})
+	case map[string][]interface{}:
+		vals = casted.(map[string][]interface{})["instances"]
+	default:
+		return resp.Instances, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i, v := range vals {
+		val := v.(map[string]interface{})
+
+		if t, ok := val["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Instances, err
+			}
+			resp.Instances[i].Created = creationTime
+		}
+
+		if t, ok := val["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return resp.Instances, err
+			}
+			resp.Instances[i].Updated = updatedTime
+		}
+	}
+
+	return resp.Instances, nil
+}
diff --git a/rackspace/db/v1/instances/urls.go b/rackspace/db/v1/instances/urls.go
new file mode 100644
index 0000000..5955f4c
--- /dev/null
+++ b/rackspace/db/v1/instances/urls.go
@@ -0,0 +1,23 @@
+package instances
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("instances")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return baseURL(c)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("instances", id)
+}
+
+func configURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("instances", id, "configuration")
+}
+
+func backupsURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("instances", id, "backups")
+}
diff --git a/rackspace/db/v1/users/delegate.go b/rackspace/db/v1/users/delegate.go
new file mode 100644
index 0000000..8298c46
--- /dev/null
+++ b/rackspace/db/v1/users/delegate.go
@@ -0,0 +1,16 @@
+package users
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+)
+
+// Create will create a new database user for the specified database instance.
+func Create(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, instanceID, opts)
+}
+
+// Delete will permanently remove a user from a specified database instance.
+func Delete(client *gophercloud.ServiceClient, instanceID, userName string) os.DeleteResult {
+	return os.Delete(client, instanceID, userName)
+}
diff --git a/rackspace/db/v1/users/delegate_test.go b/rackspace/db/v1/users/delegate_test.go
new file mode 100644
index 0000000..7a1b773
--- /dev/null
+++ b/rackspace/db/v1/users/delegate_test.go
@@ -0,0 +1,48 @@
+package users
+
+import (
+	"testing"
+
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const instanceID = "{instanceID}"
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreate(t)
+
+	opts := os.BatchCreateOpts{
+		os.CreateOpts{
+			Databases: db.BatchCreateOpts{
+				db.CreateOpts{Name: "databaseA"},
+			},
+			Name:     "dbuser3",
+			Password: "secretsecret",
+		},
+		os.CreateOpts{
+			Databases: db.BatchCreateOpts{
+				db.CreateOpts{Name: "databaseB"},
+				db.CreateOpts{Name: "databaseC"},
+			},
+			Name:     "dbuser4",
+			Password: "secretsecret",
+		},
+	}
+
+	res := Create(fake.ServiceClient(), instanceID, opts)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDelete(t)
+
+	res := Delete(fake.ServiceClient(), instanceID, "{userName}")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/db/v1/users/doc.go b/rackspace/db/v1/users/doc.go
new file mode 100644
index 0000000..84f2eb3
--- /dev/null
+++ b/rackspace/db/v1/users/doc.go
@@ -0,0 +1,3 @@
+// Package users provides information and interaction with the user API
+// resource in the Rackspace Database service.
+package users
diff --git a/rackspace/db/v1/users/fixtures.go b/rackspace/db/v1/users/fixtures.go
new file mode 100644
index 0000000..5314e85
--- /dev/null
+++ b/rackspace/db/v1/users/fixtures.go
@@ -0,0 +1,77 @@
+package users
+
+const singleDB = `{"databases": [{"name": "databaseE"}]}`
+
+var changePwdReq = `
+{
+  "users": [
+    {
+      "name": "dbuser1",
+      "password": "newpassword"
+    },
+    {
+      "name": "dbuser2",
+      "password": "anotherpassword"
+    }
+  ]
+}
+`
+
+var updateReq = `
+{
+	"user": {
+		"name": "new_username",
+		"password": "new_password"
+	}
+}
+`
+
+var getResp = `
+{
+	"user": {
+		"name": "exampleuser",
+		"host": "foo",
+		"databases": [
+			{
+				"name": "databaseA"
+			},
+			{
+				"name": "databaseB"
+			}
+		]
+	}
+}
+`
+
+var listResp = `
+{
+"users": [
+  {
+    "name": "dbuser1",
+    "host": "localhost",
+    "databases": [
+      {
+        "name": "databaseA"
+      }
+    ]
+  },
+  {
+    "name": "dbuser2",
+    "host": "localhost",
+    "databases": [
+      {
+        "name": "databaseB"
+      },
+      {
+        "name": "databaseC"
+      }
+    ]
+  }
+]
+}
+`
+
+var (
+	listUserAccessResp = singleDB
+	grantUserAccessReq = singleDB
+)
diff --git a/rackspace/db/v1/users/requests.go b/rackspace/db/v1/users/requests.go
new file mode 100644
index 0000000..74e47ab
--- /dev/null
+++ b/rackspace/db/v1/users/requests.go
@@ -0,0 +1,176 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will list all available users for a specified database instance.
+func List(client *gophercloud.ServiceClient, instanceID string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, baseURL(client, instanceID), createPageFn)
+}
+
+/*
+ChangePassword changes the password for one or more users. For example, to
+change the respective passwords for two users:
+
+	opts := os.BatchCreateOpts{
+		os.CreateOpts{Name: "db_user_1", Password: "new_password_1"},
+		os.CreateOpts{Name: "db_user_2", Password: "new_password_2"},
+	}
+
+	ChangePassword(client, "instance_id", opts)
+*/
+func ChangePassword(client *gophercloud.ServiceClient, instanceID string, opts os.CreateOptsBuilder) UpdatePasswordsResult {
+	var res UpdatePasswordsResult
+
+	reqBody, err := opts.ToUserCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("PUT", baseURL(client, instanceID), gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// UpdateOpts is the struct responsible for updating an existing user.
+type UpdateOpts struct {
+	// [OPTIONAL] Specifies a name for the user. Valid names can be composed
+	// of the following characters: letters (either case); numbers; these
+	// characters '@', '?', '#', ' ' but NEVER beginning a name string; '_' is
+	// permitted anywhere. Prohibited characters that are forbidden include:
+	// single quotes, double quotes, back quotes, semicolons, commas, backslashes,
+	// and forward slashes. Spaces at the front or end of a user name are also
+	// not permitted.
+	Name string
+
+	// [OPTIONAL] Specifies a password for the user.
+	Password string
+
+	// [OPTIONAL] Specifies the host from which a user is allowed to connect to
+	// the database. Possible values are a string containing an IPv4 address or
+	// "%" to allow connecting from any host. Optional; the default is "%".
+	Host string
+}
+
+// ToMap is a convenience function for creating sub-maps for individual users.
+func (opts UpdateOpts) ToMap() (map[string]interface{}, error) {
+	if opts.Name == "root" {
+		return nil, errors.New("root is a reserved user name and cannot be used")
+	}
+
+	user := map[string]interface{}{}
+
+	if opts.Name != "" {
+		user["name"] = opts.Name
+	}
+
+	if opts.Password != "" {
+		user["password"] = opts.Password
+	}
+
+	if opts.Host != "" {
+		user["host"] = opts.Host
+	}
+
+	return user, nil
+}
+
+// Update will modify the attributes of a specified user. Attributes that can
+// be updated are: user name, password, and host.
+func Update(client *gophercloud.ServiceClient, instanceID, userName string, opts UpdateOpts) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	reqBody = map[string]interface{}{"user": reqBody}
+
+	_, res.Err = client.Request("PUT", userURL(client, instanceID, userName), gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+// Get will retrieve the details for a particular user.
+func Get(client *gophercloud.ServiceClient, instanceID, userName string) GetResult {
+	var res GetResult
+
+	_, res.Err = client.Request("GET", userURL(client, instanceID, userName), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// ListAccess will list all of the databases a user has access to.
+func ListAccess(client *gophercloud.ServiceClient, instanceID, userName string) pagination.Pager {
+	pageFn := func(r pagination.PageResult) pagination.Page {
+		return AccessPage{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.NewPager(client, dbsURL(client, instanceID, userName), pageFn)
+}
+
+/*
+GrantAccess for the specified user to one or more databases on a specified
+instance. For example, to add a user to multiple databases:
+
+	opts := db.BatchCreateOpts{
+		db.CreateOpts{Name: "database_1"},
+		db.CreateOpts{Name: "database_3"},
+		db.CreateOpts{Name: "database_19"},
+	}
+
+	GrantAccess(client, "instance_id", "user_name", opts)
+*/
+func GrantAccess(client *gophercloud.ServiceClient, instanceID, userName string, opts db.CreateOptsBuilder) GrantAccessResult {
+	var res GrantAccessResult
+
+	reqBody, err := opts.ToDBCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Request("PUT", dbsURL(client, instanceID, userName), gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{202},
+	})
+
+	return res
+}
+
+/*
+RevokeAccess will revoke access for the specified user to one or more databases
+on a specified instance. For example:
+
+	RevokeAccess(client, "instance_id", "user_name", "db_name")
+*/
+func RevokeAccess(client *gophercloud.ServiceClient, instanceID, userName, dbName string) RevokeAccessResult {
+	var res RevokeAccessResult
+
+	_, res.Err = client.Request("DELETE", dbURL(client, instanceID, userName, dbName), gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return res
+}
diff --git a/rackspace/db/v1/users/requests_test.go b/rackspace/db/v1/users/requests_test.go
new file mode 100644
index 0000000..2f2dca7
--- /dev/null
+++ b/rackspace/db/v1/users/requests_test.go
@@ -0,0 +1,156 @@
+package users
+
+import (
+	"testing"
+
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	os "github.com/rackspace/gophercloud/openstack/db/v1/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+	"github.com/rackspace/gophercloud/testhelper/fixture"
+)
+
+var (
+	userName = "{userName}"
+	_rootURL = "/instances/" + instanceID + "/users"
+	_userURL = _rootURL + "/" + userName
+	_dbURL   = _userURL + "/databases"
+)
+
+func TestChangeUserPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _rootURL, "PUT", changePwdReq, "", 202)
+
+	opts := os.BatchCreateOpts{
+		os.CreateOpts{Name: "dbuser1", Password: "newpassword"},
+		os.CreateOpts{Name: "dbuser2", Password: "anotherpassword"},
+	}
+
+	err := ChangePassword(fake.ServiceClient(), instanceID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _userURL, "PUT", updateReq, "", 202)
+
+	opts := UpdateOpts{
+		Name:     "new_username",
+		Password: "new_password",
+	}
+
+	err := Update(fake.ServiceClient(), instanceID, userName, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _userURL, "GET", "", getResp, 200)
+
+	user, err := Get(fake.ServiceClient(), instanceID, userName).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name: "exampleuser",
+		Host: "foo",
+		Databases: []db.Database{
+			db.Database{Name: "databaseA"},
+			db.Database{Name: "databaseB"},
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestUserAccessList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _userURL+"/databases", "GET", "", listUserAccessResp, 200)
+
+	expectedDBs := []db.Database{
+		db.Database{Name: "databaseE"},
+	}
+
+	pages := 0
+	err := ListAccess(fake.ServiceClient(), instanceID, userName).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractDBs(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, expectedDBs, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestUserList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	fixture.SetupHandler(t, "/instances/"+instanceID+"/users", "GET", "", listResp, 200)
+
+	expectedUsers := []User{
+		User{
+			Databases: []db.Database{
+				db.Database{Name: "databaseA"},
+			},
+			Name: "dbuser1",
+			Host: "localhost",
+		},
+		User{
+			Databases: []db.Database{
+				db.Database{Name: "databaseB"},
+				db.Database{Name: "databaseC"},
+			},
+			Name: "dbuser2",
+			Host: "localhost",
+		},
+	}
+
+	pages := 0
+	err := List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractUsers(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, expectedUsers, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+}
+
+func TestGrantAccess(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _dbURL, "PUT", grantUserAccessReq, "", 202)
+
+	opts := db.BatchCreateOpts{db.CreateOpts{Name: "databaseE"}}
+	err := GrantAccess(fake.ServiceClient(), instanceID, userName, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestRevokeAccess(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, _dbURL+"/{dbName}", "DELETE", "", "", 202)
+
+	err := RevokeAccess(fake.ServiceClient(), instanceID, userName, "{dbName}").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/db/v1/users/results.go b/rackspace/db/v1/users/results.go
new file mode 100644
index 0000000..85b3a7a
--- /dev/null
+++ b/rackspace/db/v1/users/results.go
@@ -0,0 +1,149 @@
+package users
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	db "github.com/rackspace/gophercloud/openstack/db/v1/databases"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a database user
+type User struct {
+	// The user name
+	Name string
+
+	// The user password
+	Password string
+
+	// Specifies the host from which a user is allowed to connect to the database.
+	// Possible values are a string containing an IPv4 address or "%" to allow
+	// connecting from any host.
+	Host string
+
+	// The databases associated with this user
+	Databases []db.Database
+}
+
+// UpdatePasswordsResult represents the result of changing a user password.
+type UpdatePasswordsResult struct {
+	gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of updating a user.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult represents the result of getting a user.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract will retrieve a User struct from a getresult.
+func (r GetResult) Extract() (*User, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		User User `mapstructure:"user"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return &response.User, err
+}
+
+// AccessPage represents a single page of a paginated user collection.
+type AccessPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page AccessPage) IsEmpty() (bool, error) {
+	users, err := ExtractDBs(page)
+	if err != nil {
+		return true, err
+	}
+	return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page AccessPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"databases_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractDBs will convert a generic pagination struct into a more
+// relevant slice of DB structs.
+func ExtractDBs(page pagination.Page) ([]db.Database, error) {
+	casted := page.(AccessPage).Body
+
+	var response struct {
+		DBs []db.Database `mapstructure:"databases"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.DBs, err
+}
+
+// UserPage represents a single page of a paginated user collection.
+type UserPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks to see whether the collection is empty.
+func (page UserPage) IsEmpty() (bool, error) {
+	users, err := ExtractUsers(page)
+	if err != nil {
+		return true, err
+	}
+	return len(users) == 0, nil
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page UserPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"users_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// ExtractUsers will convert a generic pagination struct into a more
+// relevant slice of User structs.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+	casted := page.(UserPage).Body
+
+	var response struct {
+		Users []User `mapstructure:"users"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+
+	return response.Users, err
+}
+
+// GrantAccessResult represents the result of granting access to a user.
+type GrantAccessResult struct {
+	gophercloud.ErrResult
+}
+
+// RevokeAccessResult represents the result of revoking access to a user.
+type RevokeAccessResult struct {
+	gophercloud.ErrResult
+}
diff --git a/rackspace/db/v1/users/urls.go b/rackspace/db/v1/users/urls.go
new file mode 100644
index 0000000..bac8788
--- /dev/null
+++ b/rackspace/db/v1/users/urls.go
@@ -0,0 +1,19 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func baseURL(c *gophercloud.ServiceClient, instanceID string) string {
+	return c.ServiceURL("instances", instanceID, "users")
+}
+
+func userURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+	return c.ServiceURL("instances", instanceID, "users", userName)
+}
+
+func dbsURL(c *gophercloud.ServiceClient, instanceID, userName string) string {
+	return c.ServiceURL("instances", instanceID, "users", userName, "databases")
+}
+
+func dbURL(c *gophercloud.ServiceClient, instanceID, userName, dbName string) string {
+	return c.ServiceURL("instances", instanceID, "users", userName, "databases", dbName)
+}
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/roles/delegate.go b/rackspace/identity/v2/roles/delegate.go
new file mode 100644
index 0000000..a6ee851
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate.go
@@ -0,0 +1,50 @@
+package roles
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = client.Request("PUT", userRoleURL(client, userID, roleID), gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+
+	return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = client.Request("DELETE", userRoleURL(client, userID, roleID), gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+
+	return result
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+	gophercloud.ErrResult
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string {
+	return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID)
+}
diff --git a/rackspace/identity/v2/roles/delegate_test.go b/rackspace/identity/v2/roles/delegate_test.go
new file mode 100644
index 0000000..fcee97d
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate_test.go
@@ -0,0 +1,66 @@
+package roles
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []os.Role{
+			os.Role{
+				ID:          "123",
+				Name:        "compute:admin",
+				Description: "Nova Administrator",
+				ServiceID:   "cke5372ebabeeabb70a0e702a4626977x4406e5",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := AddUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := DeleteUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/identity/v2/roles/fixtures.go b/rackspace/identity/v2/roles/fixtures.go
new file mode 100644
index 0000000..5f22d0f
--- /dev/null
+++ b/rackspace/identity/v2/roles/fixtures.go
@@ -0,0 +1,49 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/OS-KSADM/roles", 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, `
+{
+    "roles": [
+        {
+            "id": "123",
+            "name": "compute:admin",
+            "description": "Nova Administrator",
+            "serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", 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/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/identity/v2/users/delegate.go b/rackspace/identity/v2/users/delegate.go
new file mode 100644
index 0000000..6135bec
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate.go
@@ -0,0 +1,142 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a pager that allows traversal over a collection of users.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// CommonOpts are the options which are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+	// Required. The username to assign to the user. When provided, the username
+	// must:
+	// - start with an alphabetical (A-Za-z) character
+	// - have a minimum length of 1 character
+	//
+	// The username may contain upper and lowercase characters, as well as any of
+	// the following special character: . - @ _
+	Username string
+
+	// Required. Email address for the user account.
+	Email string
+
+	// Required. Indicates whether the user can authenticate after the user
+	// account is created. If no value is specified, the default value is true.
+	Enabled os.EnabledState
+
+	// Optional. The password to assign to the user. If provided, the password
+	// must:
+	// - start with an alphabetical (A-Za-z) character
+	// - have a minimum length of 8 characters
+	// - contain at least one uppercase character, one lowercase character, and
+	//   one numeric character.
+	//
+	// The password may contain any of the following special characters: . - @ _
+	Password string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+
+	if opts.Username == "" {
+		return m, errors.New("Username is a required field")
+	}
+	if opts.Enabled == nil {
+		return m, errors.New("Enabled is a required field")
+	}
+	if opts.Email == "" {
+		return m, errors.New("Email is a required field")
+	}
+
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = opts.Enabled
+	}
+	if opts.Password != "" {
+		m["OS-KSADM:password"] = opts.Password
+	}
+
+	return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+
+	return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	_, result.Err = client.Request("POST", os.ResourceURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &result.Body,
+		JSONBody:     opts.ToUserUpdateMap(),
+		OkCodes:      []int{200},
+	})
+
+	return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(client, id)
+}
+
+// ResetAPIKey resets the User's API key.
+func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult {
+	var result ResetAPIKeyResult
+
+	_, result.Err = client.Request("POST", resetAPIKeyURL(client, id), gophercloud.RequestOpts{
+		JSONResponse: &result.Body,
+		OkCodes:      []int{200},
+	})
+
+	return result
+}
diff --git a/rackspace/identity/v2/users/delegate_test.go b/rackspace/identity/v2/users/delegate_test.go
new file mode 100644
index 0000000..62faf0c
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate_test.go
@@ -0,0 +1,111 @@
+package users
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"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
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		users, err := os.ExtractUsers(page)
+
+		th.AssertNoErr(t, err)
+		th.AssertEquals(t, "u1000", users[0].ID)
+		th.AssertEquals(t, "u1001", users[1].ID)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateUser(t)
+
+	opts := CreateOpts{
+		Username: "new_user",
+		Enabled:  os.Disabled,
+		Email:    "new_user@foo.com",
+		Password: "foo",
+	}
+
+	user, err := Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "123456", user.ID)
+	th.AssertEquals(t, "5830280", user.DomainID)
+	th.AssertEquals(t, "DFW", user.DefaultRegion)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetUser(t)
+
+	user, err := Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, true, user.Enabled)
+	th.AssertEquals(t, true, user.MultiFactorEnabled)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateUser(t)
+
+	id := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+	opts := UpdateOpts{
+		Enabled: os.Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, true, user.Enabled)
+	th.AssertEquals(t, "new_email@foo.com", user.Email)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteUser(t)
+
+	res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResetAPIKey(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockResetAPIKey(t)
+
+	apiKey, err := ResetAPIKey(client.ServiceClient(), "99").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "joesmith", apiKey.Username)
+	th.AssertEquals(t, "mooH1eiLahd5ahYood7r", apiKey.APIKey)
+}
diff --git a/rackspace/identity/v2/users/fixtures.go b/rackspace/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..973f39e
--- /dev/null
+++ b/rackspace/identity/v2/users/fixtures.go
@@ -0,0 +1,154 @@
+package users
+
+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("/users", 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, `
+{
+		"users":[
+				{
+						"id": "u1000",
+						"username": "jqsmith",
+						"email": "john.smith@example.org",
+						"enabled": true
+				},
+				{
+						"id": "u1001",
+						"username": "jqsmith",
+						"email": "jane.smith@example.org",
+						"enabled": true
+				}
+		]
+}
+	`)
+	})
+}
+
+func mockCreateUser(t *testing.T) {
+	th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+    "user": {
+        "username": "new_user",
+        "enabled": false,
+        "email": "new_user@foo.com",
+				"OS-KSADM:password": "foo"
+    }
+}
+  `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "user": {
+    "RAX-AUTH:defaultRegion": "DFW",
+    "RAX-AUTH:domainId": "5830280",
+    "id": "123456",
+    "username": "new_user",
+    "email": "new_user@foo.com",
+    "enabled": false
+  }
+}
+`)
+	})
+}
+
+func mockGetUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/new_user", 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, `
+{
+	"user": {
+		"RAX-AUTH:defaultRegion": "DFW",
+		"RAX-AUTH:domainId": "5830280",
+		"RAX-AUTH:multiFactorEnabled": "true",
+		"id": "c39e3de9be2d4c779f1dfd6abacc176d",
+		"username": "jqsmith",
+		"email": "john.smith@example.org",
+		"enabled": true
+	}
+}
+`)
+	})
+}
+
+func mockUpdateUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+	"user": {
+		"email": "new_email@foo.com",
+		"enabled": true
+	}
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"user": {
+		"RAX-AUTH:defaultRegion": "DFW",
+		"RAX-AUTH:domainId": "5830280",
+		"RAX-AUTH:multiFactorEnabled": "true",
+		"id": "123456",
+		"username": "jqsmith",
+		"email": "new_email@foo.com",
+		"enabled": true
+	}
+}
+`)
+	})
+}
+
+func mockDeleteUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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 mockResetAPIKey(t *testing.T) {
+	th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "RAX-KSKEY:apiKeyCredentials": {
+        "username": "joesmith",
+        "apiKey": "mooH1eiLahd5ahYood7r"
+    }
+}`)
+	})
+}
diff --git a/rackspace/identity/v2/users/results.go b/rackspace/identity/v2/users/results.go
new file mode 100644
index 0000000..6936ecb
--- /dev/null
+++ b/rackspace/identity/v2/users/results.go
@@ -0,0 +1,129 @@
+package users
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+	// The UUID for this user.
+	ID string
+
+	// The human name for this user.
+	Name string
+
+	// The username for this user.
+	Username string
+
+	// Indicates whether the user is enabled (true) or disabled (false).
+	Enabled bool
+
+	// The email address for this user.
+	Email string
+
+	// The ID of the tenant to which this user belongs.
+	TenantID string `mapstructure:"tenant_id"`
+
+	// Specifies the default region for the user account. This value is inherited
+	// from the user administrator when the account is created.
+	DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"`
+
+	// Identifies the domain that contains the user account. This value is
+	// inherited from the user administrator when the account is created.
+	DomainID string `mapstructure:"RAX-AUTH:domainId"`
+
+	// The password value that the user needs for authentication. If the Add user
+	// request included a password value, this attribute is not included in the
+	// response.
+	Password string `mapstructure:"OS-KSADM:password"`
+
+	// Indicates whether the user has enabled multi-factor authentication.
+	MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"`
+}
+
+// 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) (*User, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		User *User `json:"user"`
+	}
+
+	// Since the API returns a string instead of a bool, we need to hack the JSON
+	json := resp.(map[string]interface{})
+	user := json["user"].(map[string]interface{})
+	if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" {
+		if b, err := strconv.ParseBool(s); err == nil {
+			user["RAX-AUTH:multiFactorEnabled"] = b
+		}
+	}
+
+	err = mapstructure.Decode(json, &respStruct)
+
+	return respStruct.User, err
+}
+
+// Extract will get the Snapshot object out of the GetResult object.
+func (r GetResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the CreateResult object.
+func (r CreateResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// ResetAPIKeyResult represents the server response to the ResetAPIKey method.
+type ResetAPIKeyResult struct {
+	gophercloud.Result
+}
+
+// ResetAPIKeyValue represents an API Key that has been reset.
+type ResetAPIKeyValue struct {
+	// The Username for this API Key reset.
+	Username string `mapstructure:"username"`
+
+	// The new API Key for this user.
+	APIKey string `mapstructure:"apiKey"`
+}
+
+// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object.
+func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.ResetAPIKeyValue, err
+}
diff --git a/rackspace/identity/v2/users/urls.go b/rackspace/identity/v2/users/urls.go
new file mode 100644
index 0000000..bc1aaef
--- /dev/null
+++ b/rackspace/identity/v2/users/urls.go
@@ -0,0 +1,7 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset")
+}
diff --git a/rackspace/lb/v1/acl/doc.go b/rackspace/lb/v1/acl/doc.go
new file mode 100644
index 0000000..42325fe
--- /dev/null
+++ b/rackspace/lb/v1/acl/doc.go
@@ -0,0 +1,12 @@
+/*
+Package acl provides information and interaction with the access lists feature
+of the Rackspace Cloud Load Balancer service.
+
+The access list management feature allows fine-grained network access controls
+to be applied to the load balancer's virtual IP address. A single IP address,
+multiple IP addresses, or entire network subnets can be added. Items that are
+configured with the ALLOW type always takes precedence over items with the DENY
+type. To reject traffic from all items except for those with the ALLOW type,
+add a networkItem with an address of "0.0.0.0/0" and a DENY type.
+*/
+package acl
diff --git a/rackspace/lb/v1/acl/fixtures.go b/rackspace/lb/v1/acl/fixtures.go
new file mode 100644
index 0000000..e3c941c
--- /dev/null
+++ b/rackspace/lb/v1/acl/fixtures.go
@@ -0,0 +1,109 @@
+package acl
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+	return "/loadbalancers/" + strconv.Itoa(lbID) + "/accesslist"
+}
+
+func mockListResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc(_rootURL(id), 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, `
+{
+  "accessList": [
+    {
+      "address": "206.160.163.21",
+      "id": 21,
+      "type": "DENY"
+    },
+    {
+      "address": "206.160.163.22",
+      "id": 22,
+      "type": "DENY"
+    },
+    {
+      "address": "206.160.163.23",
+      "id": 23,
+      "type": "DENY"
+    },
+    {
+      "address": "206.160.163.24",
+      "id": 24,
+      "type": "DENY"
+    }
+  ]
+}
+  `)
+	})
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "accessList": [
+    {
+      "address": "206.160.163.21",
+      "type": "DENY"
+    },
+    {
+      "address": "206.160.165.11",
+      "type": "DENY"
+    }
+  ]
+}
+    `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDeleteAllResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		r.ParseForm()
+
+		for k, v := range ids {
+			fids := r.Form["id"]
+			th.AssertEquals(t, strconv.Itoa(v), fids[k])
+		}
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDeleteResponse(t *testing.T, lbID, networkID int) {
+	th.Mux.HandleFunc(_rootURL(lbID)+"/"+strconv.Itoa(networkID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/rackspace/lb/v1/acl/requests.go b/rackspace/lb/v1/acl/requests.go
new file mode 100644
index 0000000..d4ce7c0
--- /dev/null
+++ b/rackspace/lb/v1/acl/requests.go
@@ -0,0 +1,111 @@
+package acl
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// network items that define a load balancer's access list.
+func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager {
+	url := rootURL(client, lbID)
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return AccessListPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+	ToAccessListCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+	// Required - the IP address or CIDR for item to add to access list.
+	Address string
+
+	// Required - the type of the node. Either ALLOW or DENY.
+	Type Type
+}
+
+// ToAccessListCreateMap converts a slice of options into a map that can be
+// used for the JSON.
+func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) {
+	type itemMap map[string]interface{}
+	items := []itemMap{}
+
+	for k, v := range opts {
+		if v.Address == "" {
+			return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k)
+		}
+		if v.Type != ALLOW && v.Type != DENY {
+			return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY")
+		}
+
+		item := make(itemMap)
+		item["address"] = v.Address
+		item["type"] = v.Type
+
+		items = append(items, item)
+	}
+
+	return itemMap{"accessList": items}, nil
+}
+
+// Create is the operation responsible for adding network items to the access
+// rules for a particular load balancer. If network items already exist, the
+// new item will be appended. A single IP address or subnet range is considered
+// unique and cannot be duplicated.
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToAccessListCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Post(rootURL(client, loadBalancerID), reqBody, nil, nil)
+	return res
+}
+
+// BulkDelete will delete multiple network items from a load balancer's access
+// list in a single operation.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult {
+	var res DeleteResult
+
+	if len(itemIDs) > 10 || len(itemIDs) == 0 {
+		res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs")
+		return res
+	}
+
+	url := rootURL(c, loadBalancerID)
+	url += gophercloud.IDSliceToQueryString("id", itemIDs)
+
+	_, res.Err = c.Delete(url, nil)
+	return res
+}
+
+// Delete will remove a single network item from a load balancer's access list.
+func Delete(c *gophercloud.ServiceClient, lbID, itemID int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, lbID, itemID), nil)
+	return res
+}
+
+// DeleteAll will delete the entire contents of a load balancer's access list,
+// effectively resetting it and allowing all traffic.
+func DeleteAll(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(rootURL(c, lbID), nil)
+	return res
+}
diff --git a/rackspace/lb/v1/acl/requests_test.go b/rackspace/lb/v1/acl/requests_test.go
new file mode 100644
index 0000000..c4961a3
--- /dev/null
+++ b/rackspace/lb/v1/acl/requests_test.go
@@ -0,0 +1,91 @@
+package acl
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	lbID    = 12345
+	itemID1 = 67890
+	itemID2 = 67891
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListResponse(t, lbID)
+
+	count := 0
+
+	err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractAccessList(page)
+		th.AssertNoErr(t, err)
+
+		expected := AccessList{
+			NetworkItem{Address: "206.160.163.21", ID: 21, Type: DENY},
+			NetworkItem{Address: "206.160.163.22", ID: 22, Type: DENY},
+			NetworkItem{Address: "206.160.163.23", ID: 23, Type: DENY},
+			NetworkItem{Address: "206.160.163.24", ID: 24, Type: DENY},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateResponse(t, lbID)
+
+	opts := CreateOpts{
+		CreateOpt{Address: "206.160.163.21", Type: DENY},
+		CreateOpt{Address: "206.160.165.11", Type: DENY},
+	}
+
+	err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestBulkDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	ids := []int{itemID1, itemID2}
+
+	mockBatchDeleteResponse(t, lbID, ids)
+
+	err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteResponse(t, lbID, itemID1)
+
+	err := Delete(client.ServiceClient(), lbID, itemID1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteAll(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteAllResponse(t, lbID)
+
+	err := DeleteAll(client.ServiceClient(), lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/acl/results.go b/rackspace/lb/v1/acl/results.go
new file mode 100644
index 0000000..9ea5ea2
--- /dev/null
+++ b/rackspace/lb/v1/acl/results.go
@@ -0,0 +1,72 @@
+package acl
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// AccessList represents the rules of network access to a particular load
+// balancer.
+type AccessList []NetworkItem
+
+// NetworkItem describes how an IP address or entire subnet may interact with a
+// load balancer.
+type NetworkItem struct {
+	// The IP address or subnet (CIDR) that defines the network item.
+	Address string
+
+	// The numeric unique ID for this item.
+	ID int
+
+	// Either ALLOW or DENY.
+	Type Type
+}
+
+// Type defines how an item may connect to the load balancer.
+type Type string
+
+// Convenience consts.
+const (
+	ALLOW Type = "ALLOW"
+	DENY  Type = "DENY"
+)
+
+// AccessListPage is the page returned by a pager for traversing over a
+// collection of network items in an access list.
+type AccessListPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AccessListPage struct is empty.
+func (p AccessListPage) IsEmpty() (bool, error) {
+	is, err := ExtractAccessList(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractAccessList accepts a Page struct, specifically an AccessListPage
+// struct, and extracts the elements into a slice of NetworkItem structs. In
+// other words, a generic collection is mapped into a relevant slice.
+func ExtractAccessList(page pagination.Page) (AccessList, error) {
+	var resp struct {
+		List AccessList `mapstructure:"accessList" json:"accessList"`
+	}
+
+	err := mapstructure.Decode(page.(AccessListPage).Body, &resp)
+
+	return resp.List, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/acl/urls.go b/rackspace/lb/v1/acl/urls.go
new file mode 100644
index 0000000..e373fa1
--- /dev/null
+++ b/rackspace/lb/v1/acl/urls.go
@@ -0,0 +1,20 @@
+package acl
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	path    = "loadbalancers"
+	aclPath = "accesslist"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string {
+	return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+	return c.ServiceURL(path, strconv.Itoa(lbID), aclPath)
+}
diff --git a/rackspace/lb/v1/lbs/doc.go b/rackspace/lb/v1/lbs/doc.go
new file mode 100644
index 0000000..05f0032
--- /dev/null
+++ b/rackspace/lb/v1/lbs/doc.go
@@ -0,0 +1,44 @@
+/*
+Package lbs provides information and interaction with the Load Balancer API
+resource for the Rackspace Cloud Load Balancer service.
+
+A load balancer is a logical device which belongs to a cloud account. It is
+used to distribute workloads between multiple back-end systems or services,
+based on the criteria defined as part of its configuration. This configuration
+is defined using the Create operation, and can be updated with Update.
+
+To conserve IPv4 address space, it is highly recommended that you share Virtual
+IPs between load balancers. If you have at least one load balancer, you may
+create subsequent ones that share a single virtual IPv4 and/or a single IPv6 by
+passing in a virtual IP ID to the Update operation (instead of a type). This
+feature is also highly desirable if you wish to load balance both an insecure
+and secure protocol using one IP or DNS name. In order to share a virtual IP,
+each Load Balancer must utilize a unique port.
+
+All load balancers have a Status attribute that shows the current configuration
+status of the device. This status is immutable by the caller and is updated
+automatically based on state changes within the service. When a load balancer
+is first created, it is placed into a BUILD state while the configuration is
+being generated and applied based on the request. Once the configuration is
+applied and finalized, it is in an ACTIVE status. In the event of a
+configuration change or update, the status of the load balancer changes to
+PENDING_UPDATE to signify configuration changes are in progress but have not yet
+been finalized. Load balancers in a SUSPENDED status are configured to reject
+traffic and do not forward requests to back-end nodes.
+
+An HTTP load balancer has the X-Forwarded-For (XFF) HTTP header set by default.
+This header contains the originating IP address of a client connecting to a web
+server through an HTTP proxy or load balancer, which many web applications are
+already designed to use when determining the source address for a request.
+
+It also includes the X-Forwarded-Proto (XFP) HTTP header, which has been added
+for identifying the originating protocol of an HTTP request as "http" or
+"https" depending on which protocol the client requested. This is useful when
+using SSL termination.
+
+Finally, it also includes the X-Forwarded-Port HTTP header, which has been
+added for being able to generate secure URLs containing the specified port.
+This header, along with the X-Forwarded-For header, provides the needed
+information to the underlying application servers.
+*/
+package lbs
diff --git a/rackspace/lb/v1/lbs/fixtures.go b/rackspace/lb/v1/lbs/fixtures.go
new file mode 100644
index 0000000..6325310
--- /dev/null
+++ b/rackspace/lb/v1/lbs/fixtures.go
@@ -0,0 +1,584 @@
+package lbs
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockListLBResponse(t *testing.T) {
+	th.Mux.HandleFunc("/loadbalancers", 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, `
+{
+   "loadBalancers":[
+      {
+         "name":"lb-site1",
+         "id":71,
+         "protocol":"HTTP",
+         "port":80,
+         "algorithm":"RANDOM",
+         "status":"ACTIVE",
+         "nodeCount":3,
+         "virtualIps":[
+            {
+               "id":403,
+               "address":"206.55.130.1",
+               "type":"PUBLIC",
+               "ipVersion":"IPV4"
+            }
+         ],
+         "created":{
+            "time":"2010-11-30T03:23:42Z"
+         },
+         "updated":{
+            "time":"2010-11-30T03:23:44Z"
+         }
+      },
+      {
+         "name":"lb-site2",
+         "id":72,
+         "created":{
+            "time":"2011-11-30T03:23:42Z"
+         },
+         "updated":{
+            "time":"2011-11-30T03:23:44Z"
+         }
+      },
+      {
+         "name":"lb-site3",
+         "id":73,
+         "created":{
+            "time":"2012-11-30T03:23:42Z"
+         },
+         "updated":{
+            "time":"2012-11-30T03:23:44Z"
+         }
+      }
+   ]
+}
+			`)
+	})
+}
+
+func mockCreateLBResponse(t *testing.T) {
+	th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "loadBalancer": {
+    "name": "a-new-loadbalancer",
+    "port": 80,
+    "protocol": "HTTP",
+    "virtualIps": [
+      {
+        "id": 2341
+      },
+      {
+        "id": 900001
+      }
+    ],
+    "nodes": [
+      {
+        "address": "10.1.1.1",
+        "port": 80,
+        "condition": "ENABLED"
+      }
+    ]
+  }
+}
+		`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+  "loadBalancer": {
+    "name": "a-new-loadbalancer",
+    "id": 144,
+    "protocol": "HTTP",
+    "halfClosed": false,
+    "port": 83,
+    "algorithm": "RANDOM",
+    "status": "BUILD",
+    "timeout": 30,
+    "cluster": {
+      "name": "ztm-n01.staging1.lbaas.rackspace.net"
+    },
+    "nodes": [
+      {
+        "address": "10.1.1.1",
+        "id": 653,
+        "port": 80,
+        "status": "ONLINE",
+        "condition": "ENABLED",
+        "weight": 1
+      }
+    ],
+    "virtualIps": [
+      {
+        "address": "206.10.10.210",
+        "id": 39,
+        "type": "PUBLIC",
+        "ipVersion": "IPV4"
+      },
+      {
+        "address": "2001:4801:79f1:0002:711b:be4c:0000:0021",
+        "id": 900001,
+        "type": "PUBLIC",
+        "ipVersion": "IPV6"
+      }
+    ],
+    "created": {
+      "time": "2010-11-30T03:23:42Z"
+    },
+    "updated": {
+      "time": "2010-11-30T03:23:44Z"
+    },
+    "connectionLogging": {
+      "enabled": false
+    }
+  }
+}
+	`)
+	})
+}
+
+func mockBatchDeleteLBResponse(t *testing.T, ids []int) {
+	th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		r.ParseForm()
+
+		for k, v := range ids {
+			fids := r.Form["id"]
+			th.AssertEquals(t, strconv.Itoa(v), fids[k])
+		}
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDeleteLBResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockGetLBResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), 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, `
+{
+  "loadBalancer": {
+    "id": 2000,
+    "name": "sample-loadbalancer",
+    "protocol": "HTTP",
+    "port": 80,
+    "algorithm": "RANDOM",
+    "status": "ACTIVE",
+    "timeout": 30,
+    "connectionLogging": {
+      "enabled": true
+    },
+    "virtualIps": [
+      {
+        "id": 1000,
+        "address": "206.10.10.210",
+        "type": "PUBLIC",
+        "ipVersion": "IPV4"
+      }
+    ],
+    "nodes": [
+      {
+        "id": 1041,
+        "address": "10.1.1.1",
+        "port": 80,
+        "condition": "ENABLED",
+        "status": "ONLINE"
+      },
+      {
+        "id": 1411,
+        "address": "10.1.1.2",
+        "port": 80,
+        "condition": "ENABLED",
+        "status": "ONLINE"
+      }
+    ],
+    "sessionPersistence": {
+      "persistenceType": "HTTP_COOKIE"
+    },
+    "connectionThrottle": {
+      "maxConnections": 100
+    },
+    "cluster": {
+      "name": "c1.dfw1"
+    },
+    "created": {
+      "time": "2010-11-30T03:23:42Z"
+    },
+    "updated": {
+      "time": "2010-11-30T03:23:44Z"
+    },
+    "sourceAddresses": {
+      "ipv6Public": "2001:4801:79f1:1::1/64",
+      "ipv4Servicenet": "10.0.0.0",
+      "ipv4Public": "10.12.99.28"
+    }
+  }
+}
+	`)
+	})
+}
+
+func mockUpdateLBResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+	"loadBalancer": {
+		"name": "a-new-loadbalancer",
+		"protocol": "TCP",
+		"halfClosed": true,
+		"algorithm": "RANDOM",
+		"port": 8080,
+		"timeout": 100,
+		"httpsRedirect": false
+	}
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockListProtocolsResponse(t *testing.T) {
+	th.Mux.HandleFunc("/loadbalancers/protocols", 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, `
+{
+  "protocols": [
+    {
+      "name": "DNS_TCP",
+      "port": 53
+    },
+    {
+      "name": "DNS_UDP",
+      "port": 53
+    },
+    {
+      "name": "FTP",
+      "port": 21
+    },
+    {
+      "name": "HTTP",
+      "port": 80
+    },
+    {
+      "name": "HTTPS",
+      "port": 443
+    },
+    {
+      "name": "IMAPS",
+      "port": 993
+    },
+    {
+      "name": "IMAPv4",
+      "port": 143
+    },
+    {
+      "name": "LDAP",
+      "port": 389
+    },
+    {
+      "name": "LDAPS",
+      "port": 636
+    },
+    {
+      "name": "MYSQL",
+      "port": 3306
+    },
+    {
+      "name": "POP3",
+      "port": 110
+    },
+    {
+      "name": "POP3S",
+      "port": 995
+    },
+    {
+      "name": "SMTP",
+      "port": 25
+    },
+    {
+      "name": "TCP",
+      "port": 0
+    },
+    {
+      "name": "TCP_CLIENT_FIRST",
+      "port": 0
+    },
+    {
+      "name": "UDP",
+      "port": 0
+    },
+    {
+      "name": "UDP_STREAM",
+      "port": 0
+    },
+    {
+      "name": "SFTP",
+      "port": 22
+    },
+    {
+      "name": "TCP_STREAM",
+      "port": 0
+    }
+  ]
+}
+	`)
+	})
+}
+
+func mockListAlgorithmsResponse(t *testing.T) {
+	th.Mux.HandleFunc("/loadbalancers/algorithms", 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, `
+{
+  "algorithms": [
+    {
+      "name": "LEAST_CONNECTIONS"
+    },
+    {
+      "name": "RANDOM"
+    },
+    {
+      "name": "ROUND_ROBIN"
+    },
+    {
+      "name": "WEIGHTED_LEAST_CONNECTIONS"
+    },
+    {
+      "name": "WEIGHTED_ROUND_ROBIN"
+    }
+  ]
+}
+			`)
+	})
+}
+
+func mockGetLoggingResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, `
+{
+  "connectionLogging": {
+    "enabled": true
+  }
+}
+			`)
+	})
+}
+
+func mockEnableLoggingResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+   "connectionLogging":{
+      "enabled":true
+   }
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDisableLoggingResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+	"connectionLogging":{
+			"enabled":false
+	}
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockGetErrorPageResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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, `
+{
+  "errorpage": {
+    "content": "<html>DEFAULT ERROR PAGE</html>"
+  }
+}
+			`)
+	})
+}
+
+func mockSetErrorPageResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+	"errorpage": {
+		"content": "<html>New error page</html>"
+	}
+}
+		`)
+
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"errorpage": {
+		"content": "<html>New error page</html>"
+	}
+}
+			`)
+	})
+}
+
+func mockDeleteErrorPageResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusOK)
+	})
+}
+
+func mockGetStatsResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/stats", 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, `
+{
+  "connectTimeOut": 10,
+  "connectError": 20,
+  "connectFailure": 30,
+  "dataTimedOut": 40,
+  "keepAliveTimedOut": 50,
+  "maxConn": 60,
+  "currentConn": 40,
+  "connectTimeOutSsl": 10,
+  "connectErrorSsl": 20,
+  "connectFailureSsl": 30,
+  "dataTimedOutSsl": 40,
+  "keepAliveTimedOutSsl": 50,
+  "maxConnSsl": 60,
+  "currentConnSsl": 40
+}
+			`)
+	})
+}
+
+func mockGetCachingResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, `
+{
+   "contentCaching": {
+      "enabled": true
+   }
+}
+			`)
+	})
+}
+
+func mockEnableCachingResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+   "contentCaching":{
+      "enabled":true
+   }
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDisableCachingResponse(t *testing.T, id int) {
+	th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+	"contentCaching":{
+			"enabled":false
+	}
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/rackspace/lb/v1/lbs/requests.go b/rackspace/lb/v1/lbs/requests.go
new file mode 100644
index 0000000..46f5f02
--- /dev/null
+++ b/rackspace/lb/v1/lbs/requests.go
@@ -0,0 +1,497 @@
+package lbs
+
+import (
+	"errors"
+
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+)
+
+var (
+	errNameRequired    = errors.New("Name is a required attribute")
+	errTimeoutExceeded = errors.New("Timeout must be less than 120")
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToLBListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListOpts struct {
+	ChangesSince string `q:"changes-since"`
+	Status       Status `q:"status"`
+	NodeAddr     string `q:"nodeaddress"`
+	Marker       string `q:"marker"`
+	Limit        int    `q:"limit"`
+}
+
+// ToLBListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToLBListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List is the operation responsible for returning a paginated collection of
+// load balancers. You may pass in a ListOpts struct to filter results.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(client)
+	if opts != nil {
+		query, err := opts.ToLBListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return LBPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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 {
+	ToLBCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// Required - name of the load balancer to create. The name must be 128
+	// characters or fewer in length, and all UTF-8 characters are valid.
+	Name string
+
+	// Optional - nodes to be added.
+	Nodes []nodes.Node
+
+	// Required - protocol of the service that is being load balanced.
+	// See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+	// for a full list of supported protocols.
+	Protocol string
+
+	// Optional - enables or disables Half-Closed support for the load balancer.
+	// Half-Closed support provides the ability for one end of the connection to
+	// terminate its output, while still receiving data from the other end. Only
+	// available for TCP/TCP_CLIENT_FIRST protocols.
+	HalfClosed gophercloud.EnabledState
+
+	// Optional - the type of virtual IPs you want associated with the load
+	// balancer.
+	VIPs []vips.VIP
+
+	// Optional - the access list management feature allows fine-grained network
+	// access controls to be applied to the load balancer virtual IP address.
+	AccessList *acl.AccessList
+
+	// Optional - algorithm that defines how traffic should be directed between
+	// back-end nodes.
+	Algorithm string
+
+	// Optional - current connection logging configuration.
+	ConnectionLogging *ConnectionLogging
+
+	// Optional - specifies a limit on the number of connections per IP address
+	// to help mitigate malicious or abusive traffic to your applications.
+	ConnThrottle *throttle.ConnectionThrottle
+
+	// Optional - the type of health monitor check to perform to ensure that the
+	// service is performing properly.
+	HealthMonitor *monitors.Monitor
+
+	// Optional - arbitrary information that can be associated with each LB.
+	Metadata map[string]interface{}
+
+	// Optional - port number for the service you are load balancing.
+	Port int
+
+	// Optional - the timeout value for the load balancer and communications with
+	// its nodes. Defaults to 30 seconds with a maximum of 120 seconds.
+	Timeout int
+
+	// Optional - specifies whether multiple requests from clients are directed
+	// to the same node.
+	SessionPersistence *sessions.SessionPersistence
+
+	// Optional - enables or disables HTTP to HTTPS redirection for the load
+	// balancer. When enabled, any HTTP request returns status code 301 (Moved
+	// Permanently), and the requester is redirected to the requested URL via the
+	// HTTPS protocol on port 443. For example, http://example.com/page.html
+	// would be redirected to https://example.com/page.html. Only available for
+	// HTTPS protocol (port=443), or HTTP protocol with a properly configured SSL
+	// termination (secureTrafficOnly=true, securePort=443).
+	HTTPSRedirect gophercloud.EnabledState
+}
+
+// ToLBCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToLBCreateMap() (map[string]interface{}, error) {
+	lb := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return lb, errNameRequired
+	}
+	if opts.Timeout > 120 {
+		return lb, errTimeoutExceeded
+	}
+
+	lb["name"] = opts.Name
+
+	if len(opts.Nodes) > 0 {
+		nodes := []map[string]interface{}{}
+		for _, n := range opts.Nodes {
+			nodes = append(nodes, map[string]interface{}{
+				"address":   n.Address,
+				"port":      n.Port,
+				"condition": n.Condition,
+			})
+		}
+		lb["nodes"] = nodes
+	}
+
+	if opts.Protocol != "" {
+		lb["protocol"] = opts.Protocol
+	}
+	if opts.HalfClosed != nil {
+		lb["halfClosed"] = opts.HalfClosed
+	}
+	if len(opts.VIPs) > 0 {
+		lb["virtualIps"] = opts.VIPs
+	}
+	if opts.AccessList != nil {
+		lb["accessList"] = &opts.AccessList
+	}
+	if opts.Algorithm != "" {
+		lb["algorithm"] = opts.Algorithm
+	}
+	if opts.ConnectionLogging != nil {
+		lb["connectionLogging"] = &opts.ConnectionLogging
+	}
+	if opts.ConnThrottle != nil {
+		lb["connectionThrottle"] = &opts.ConnThrottle
+	}
+	if opts.HealthMonitor != nil {
+		lb["healthMonitor"] = &opts.HealthMonitor
+	}
+	if len(opts.Metadata) != 0 {
+		lb["metadata"] = opts.Metadata
+	}
+	if opts.Port > 0 {
+		lb["port"] = opts.Port
+	}
+	if opts.Timeout > 0 {
+		lb["timeout"] = opts.Timeout
+	}
+	if opts.SessionPersistence != nil {
+		lb["sessionPersistence"] = &opts.SessionPersistence
+	}
+	if opts.HTTPSRedirect != nil {
+		lb["httpsRedirect"] = &opts.HTTPSRedirect
+	}
+
+	return map[string]interface{}{"loadBalancer": lb}, nil
+}
+
+// Create is the operation responsible for asynchronously provisioning a new
+// load balancer based on the configuration defined in CreateOpts. Once the
+// request is validated and progress has started on the provisioning process, a
+// response struct is returned. When extracted (with Extract()), you have
+// to the load balancer's unique ID and status.
+//
+// Once an ID is attained, you can check on the progress of the operation by
+// calling Get and passing in the ID. If the corresponding request cannot be
+// fulfilled due to insufficient or invalid data, an HTTP 400 (Bad Request)
+// error response is returned with information regarding the nature of the
+// failure in the body of the response. Failures in the validation process are
+// non-recoverable and require the caller to correct the cause of the failure.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToLBCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get is the operation responsible for providing detailed information
+// regarding a specific load balancer which is configured and associated with
+// your account. This operation is not capable of returning details for a load
+// balancer which has been deleted.
+func Get(c *gophercloud.ServiceClient, id int) GetResult {
+	var res GetResult
+
+	_, res.Err = c.Get(resourceURL(c, id), &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return res
+}
+
+// BulkDelete removes all the load balancers referenced in the slice of IDs.
+// Any and all configuration data associated with these load balancers is
+// immediately purged and is not recoverable.
+//
+// If one of the items in the list cannot be removed due to its current status,
+// a 400 Bad Request error is returned along with the IDs of the ones the
+// system identified as potential failures for this request.
+func BulkDelete(c *gophercloud.ServiceClient, ids []int) DeleteResult {
+	var res DeleteResult
+
+	if len(ids) > 10 || len(ids) == 0 {
+		res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 LB IDs")
+		return res
+	}
+
+	url := rootURL(c)
+	url += gophercloud.IDSliceToQueryString("id", ids)
+
+	_, res.Err = c.Delete(url, nil)
+	return res
+}
+
+// Delete removes a single load balancer.
+func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, id), nil)
+	return res
+}
+
+// UpdateOptsBuilder represents a type that can be converted into a JSON-like
+// map structure.
+type UpdateOptsBuilder interface {
+	ToLBUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the options for updating an existing load balancer.
+type UpdateOpts struct {
+	// Optional - new name of the load balancer.
+	Name string
+
+	// Optional - the new protocol you want your load balancer to have.
+	// See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+	// for a full list of supported protocols.
+	Protocol string
+
+	// Optional - see the HalfClosed field in CreateOpts for more information.
+	HalfClosed gophercloud.EnabledState
+
+	// Optional - see the Algorithm field in CreateOpts for more information.
+	Algorithm string
+
+	// Optional - see the Port field in CreateOpts for more information.
+	Port int
+
+	// Optional - see the Timeout field in CreateOpts for more information.
+	Timeout int
+
+	// Optional - see the HTTPSRedirect field in CreateOpts for more information.
+	HTTPSRedirect gophercloud.EnabledState
+}
+
+// ToLBUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToLBUpdateMap() (map[string]interface{}, error) {
+	lb := make(map[string]interface{})
+
+	if opts.Name != "" {
+		lb["name"] = opts.Name
+	}
+	if opts.Protocol != "" {
+		lb["protocol"] = opts.Protocol
+	}
+	if opts.HalfClosed != nil {
+		lb["halfClosed"] = opts.HalfClosed
+	}
+	if opts.Algorithm != "" {
+		lb["algorithm"] = opts.Algorithm
+	}
+	if opts.Port > 0 {
+		lb["port"] = opts.Port
+	}
+	if opts.Timeout > 0 {
+		lb["timeout"] = opts.Timeout
+	}
+	if opts.HTTPSRedirect != nil {
+		lb["httpsRedirect"] = &opts.HTTPSRedirect
+	}
+
+	return map[string]interface{}{"loadBalancer": lb}, nil
+}
+
+// Update is the operation responsible for asynchronously updating the
+// attributes of a specific load balancer. Upon successful validation of the
+// request, the service returns a 202 Accepted response, and the load balancer
+// enters a PENDING_UPDATE state. A user can poll the load balancer with Get to
+// wait for the changes to be applied. When this happens, the load balancer will
+// return to an ACTIVE state.
+func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToLBUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(resourceURL(c, id), reqBody, nil, nil)
+	return res
+}
+
+// ListProtocols is the operation responsible for returning a paginated
+// collection of load balancer protocols.
+func ListProtocols(client *gophercloud.ServiceClient) pagination.Pager {
+	url := protocolsURL(client)
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ProtocolPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// ListAlgorithms is the operation responsible for returning a paginated
+// collection of load balancer algorithms.
+func ListAlgorithms(client *gophercloud.ServiceClient) pagination.Pager {
+	url := algorithmsURL(client)
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return AlgorithmPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// IsLoggingEnabled returns true if the load balancer has connection logging
+// enabled and false if not.
+func IsLoggingEnabled(client *gophercloud.ServiceClient, id int) (bool, error) {
+	var body interface{}
+
+	_, err := client.Get(loggingURL(client, id), &body, nil)
+	if err != nil {
+		return false, err
+	}
+
+	var resp struct {
+		CL struct {
+			Enabled bool `mapstructure:"enabled"`
+		} `mapstructure:"connectionLogging"`
+	}
+
+	err = mapstructure.Decode(body, &resp)
+	return resp.CL.Enabled, err
+}
+
+func toConnLoggingMap(state bool) map[string]map[string]bool {
+	return map[string]map[string]bool{
+		"connectionLogging": map[string]bool{"enabled": state},
+	}
+}
+
+// EnableLogging will enable connection logging for a specified load balancer.
+func EnableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	_, res.Err = client.Put(loggingURL(client, id), toConnLoggingMap(true), nil, nil)
+	return res
+}
+
+// DisableLogging will disable connection logging for a specified load balancer.
+func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	_, res.Err = client.Put(loggingURL(client, id), toConnLoggingMap(false), nil, nil)
+	return res
+}
+
+// GetErrorPage will retrieve the current error page for the load balancer.
+func GetErrorPage(client *gophercloud.ServiceClient, id int) ErrorPageResult {
+	var res ErrorPageResult
+	_, res.Err = client.Get(errorPageURL(client, id), &res.Body, nil)
+	return res
+}
+
+// SetErrorPage will set the HTML of the load balancer's error page to a
+// specific value.
+func SetErrorPage(client *gophercloud.ServiceClient, id int, html string) ErrorPageResult {
+	var res ErrorPageResult
+
+	type stringMap map[string]string
+	reqBody := map[string]stringMap{"errorpage": stringMap{"content": html}}
+
+	_, res.Err = client.Put(errorPageURL(client, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return res
+}
+
+// DeleteErrorPage will delete the current error page for the load balancer.
+func DeleteErrorPage(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	_, res.Err = client.Delete(errorPageURL(client, id), &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// GetStats will retrieve detailed stats related to the load balancer's usage.
+func GetStats(client *gophercloud.ServiceClient, id int) StatsResult {
+	var res StatsResult
+	_, res.Err = client.Get(statsURL(client, id), &res.Body, nil)
+	return res
+}
+
+// IsContentCached will check to see whether the specified load balancer caches
+// content. When content caching is enabled, recently-accessed files are stored
+// on the load balancer for easy retrieval by web clients. Content caching
+// improves the performance of high traffic web sites by temporarily storing
+// data that was recently accessed. While it's cached, requests for that data
+// are served by the load balancer, which in turn reduces load off the back-end
+// nodes. The result is improved response times for those requests and less
+// load on the web server.
+func IsContentCached(client *gophercloud.ServiceClient, id int) (bool, error) {
+	var body interface{}
+
+	_, err := client.Get(cacheURL(client, id), &body, nil)
+	if err != nil {
+		return false, err
+	}
+
+	var resp struct {
+		CC struct {
+			Enabled bool `mapstructure:"enabled"`
+		} `mapstructure:"contentCaching"`
+	}
+
+	err = mapstructure.Decode(body, &resp)
+	return resp.CC.Enabled, err
+}
+
+func toCachingMap(state bool) map[string]map[string]bool {
+	return map[string]map[string]bool{
+		"contentCaching": map[string]bool{"enabled": state},
+	}
+}
+
+// EnableCaching will enable content-caching for the specified load balancer.
+func EnableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	_, res.Err = client.Put(cacheURL(client, id), toCachingMap(true), nil, nil)
+	return res
+}
+
+// DisableCaching will disable content-caching for the specified load balancer.
+func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+	_, res.Err = client.Put(cacheURL(client, id), toCachingMap(false), nil, nil)
+	return res
+}
diff --git a/rackspace/lb/v1/lbs/requests_test.go b/rackspace/lb/v1/lbs/requests_test.go
new file mode 100644
index 0000000..a8ec19e
--- /dev/null
+++ b/rackspace/lb/v1/lbs/requests_test.go
@@ -0,0 +1,438 @@
+package lbs
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	id1 = 12345
+	id2 = 67890
+	ts1 = "2010-11-30T03:23:42Z"
+	ts2 = "2010-11-30T03:23:44Z"
+)
+
+func toTime(t *testing.T, str string) time.Time {
+	ts, err := time.Parse(time.RFC3339, str)
+	if err != nil {
+		t.Fatalf("Could not parse time: %s", err.Error())
+	}
+	return ts
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListLBResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractLBs(page)
+		th.AssertNoErr(t, err)
+
+		expected := []LoadBalancer{
+			LoadBalancer{
+				Name:      "lb-site1",
+				ID:        71,
+				Protocol:  "HTTP",
+				Port:      80,
+				Algorithm: "RANDOM",
+				Status:    ACTIVE,
+				NodeCount: 3,
+				VIPs: []vips.VIP{
+					vips.VIP{
+						ID:      403,
+						Address: "206.55.130.1",
+						Type:    "PUBLIC",
+						Version: "IPV4",
+					},
+				},
+				Created: Datetime{Time: toTime(t, ts1)},
+				Updated: Datetime{Time: toTime(t, ts2)},
+			},
+			LoadBalancer{
+				ID:      72,
+				Name:    "lb-site2",
+				Created: Datetime{Time: toTime(t, "2011-11-30T03:23:42Z")},
+				Updated: Datetime{Time: toTime(t, "2011-11-30T03:23:44Z")},
+			},
+			LoadBalancer{
+				ID:      73,
+				Name:    "lb-site3",
+				Created: Datetime{Time: toTime(t, "2012-11-30T03:23:42Z")},
+				Updated: Datetime{Time: toTime(t, "2012-11-30T03:23:44Z")},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateLBResponse(t)
+
+	opts := CreateOpts{
+		Name:     "a-new-loadbalancer",
+		Port:     80,
+		Protocol: "HTTP",
+		VIPs: []vips.VIP{
+			vips.VIP{ID: 2341},
+			vips.VIP{ID: 900001},
+		},
+		Nodes: []nodes.Node{
+			nodes.Node{Address: "10.1.1.1", Port: 80, Condition: "ENABLED"},
+		},
+	}
+
+	lb, err := Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &LoadBalancer{
+		Name:       "a-new-loadbalancer",
+		ID:         144,
+		Protocol:   "HTTP",
+		HalfClosed: false,
+		Port:       83,
+		Algorithm:  "RANDOM",
+		Status:     BUILD,
+		Timeout:    30,
+		Cluster:    Cluster{Name: "ztm-n01.staging1.lbaas.rackspace.net"},
+		Nodes: []nodes.Node{
+			nodes.Node{
+				Address:   "10.1.1.1",
+				ID:        653,
+				Port:      80,
+				Status:    "ONLINE",
+				Condition: "ENABLED",
+				Weight:    1,
+			},
+		},
+		VIPs: []vips.VIP{
+			vips.VIP{
+				ID:      39,
+				Address: "206.10.10.210",
+				Type:    vips.PUBLIC,
+				Version: vips.IPV4,
+			},
+			vips.VIP{
+				ID:      900001,
+				Address: "2001:4801:79f1:0002:711b:be4c:0000:0021",
+				Type:    vips.PUBLIC,
+				Version: vips.IPV6,
+			},
+		},
+		Created:           Datetime{Time: toTime(t, ts1)},
+		Updated:           Datetime{Time: toTime(t, ts2)},
+		ConnectionLogging: ConnectionLogging{Enabled: false},
+	}
+
+	th.AssertDeepEquals(t, expected, lb)
+}
+
+func TestBulkDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	ids := []int{id1, id2}
+
+	mockBatchDeleteLBResponse(t, ids)
+
+	err := BulkDelete(client.ServiceClient(), ids).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteLBResponse(t, id1)
+
+	err := Delete(client.ServiceClient(), id1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetLBResponse(t, id1)
+
+	lb, err := Get(client.ServiceClient(), id1).Extract()
+
+	expected := &LoadBalancer{
+		Name:              "sample-loadbalancer",
+		ID:                2000,
+		Protocol:          "HTTP",
+		Port:              80,
+		Algorithm:         "RANDOM",
+		Status:            ACTIVE,
+		Timeout:           30,
+		ConnectionLogging: ConnectionLogging{Enabled: true},
+		VIPs: []vips.VIP{
+			vips.VIP{
+				ID:      1000,
+				Address: "206.10.10.210",
+				Type:    "PUBLIC",
+				Version: "IPV4",
+			},
+		},
+		Nodes: []nodes.Node{
+			nodes.Node{
+				Address:   "10.1.1.1",
+				ID:        1041,
+				Port:      80,
+				Status:    "ONLINE",
+				Condition: "ENABLED",
+			},
+			nodes.Node{
+				Address:   "10.1.1.2",
+				ID:        1411,
+				Port:      80,
+				Status:    "ONLINE",
+				Condition: "ENABLED",
+			},
+		},
+		SessionPersistence: sessions.SessionPersistence{Type: "HTTP_COOKIE"},
+		ConnectionThrottle: throttle.ConnectionThrottle{MaxConnections: 100},
+		Cluster:            Cluster{Name: "c1.dfw1"},
+		Created:            Datetime{Time: toTime(t, ts1)},
+		Updated:            Datetime{Time: toTime(t, ts2)},
+		SourceAddrs: SourceAddrs{
+			IPv4Public:  "10.12.99.28",
+			IPv4Private: "10.0.0.0",
+			IPv6Public:  "2001:4801:79f1:1::1/64",
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, lb)
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateLBResponse(t, id1)
+
+	opts := UpdateOpts{
+		Name:          "a-new-loadbalancer",
+		Protocol:      "TCP",
+		HalfClosed:    gophercloud.Enabled,
+		Algorithm:     "RANDOM",
+		Port:          8080,
+		Timeout:       100,
+		HTTPSRedirect: gophercloud.Disabled,
+	}
+
+	err := Update(client.ServiceClient(), id1, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListProtocols(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListProtocolsResponse(t)
+
+	count := 0
+
+	err := ListProtocols(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractProtocols(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Protocol{
+			Protocol{Name: "DNS_TCP", Port: 53},
+			Protocol{Name: "DNS_UDP", Port: 53},
+			Protocol{Name: "FTP", Port: 21},
+			Protocol{Name: "HTTP", Port: 80},
+			Protocol{Name: "HTTPS", Port: 443},
+			Protocol{Name: "IMAPS", Port: 993},
+			Protocol{Name: "IMAPv4", Port: 143},
+		}
+
+		th.CheckDeepEquals(t, expected[0:7], actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestListAlgorithms(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListAlgorithmsResponse(t)
+
+	count := 0
+
+	err := ListAlgorithms(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractAlgorithms(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Algorithm{
+			Algorithm{Name: "LEAST_CONNECTIONS"},
+			Algorithm{Name: "RANDOM"},
+			Algorithm{Name: "ROUND_ROBIN"},
+			Algorithm{Name: "WEIGHTED_LEAST_CONNECTIONS"},
+			Algorithm{Name: "WEIGHTED_ROUND_ROBIN"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestIsLoggingEnabled(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetLoggingResponse(t, id1)
+
+	res, err := IsLoggingEnabled(client.ServiceClient(), id1)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, res)
+}
+
+func TestEnablingLogging(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockEnableLoggingResponse(t, id1)
+
+	err := EnableLogging(client.ServiceClient(), id1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisablingLogging(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDisableLoggingResponse(t, id1)
+
+	err := DisableLogging(client.ServiceClient(), id1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetErrorPage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetErrorPageResponse(t, id1)
+
+	content, err := GetErrorPage(client.ServiceClient(), id1).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &ErrorPage{Content: "<html>DEFAULT ERROR PAGE</html>"}
+	th.AssertDeepEquals(t, expected, content)
+}
+
+func TestSetErrorPage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockSetErrorPageResponse(t, id1)
+
+	html := "<html>New error page</html>"
+	content, err := SetErrorPage(client.ServiceClient(), id1, html).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &ErrorPage{Content: html}
+	th.AssertDeepEquals(t, expected, content)
+}
+
+func TestDeleteErrorPage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteErrorPageResponse(t, id1)
+
+	err := DeleteErrorPage(client.ServiceClient(), id1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetStats(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetStatsResponse(t, id1)
+
+	content, err := GetStats(client.ServiceClient(), id1).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Stats{
+		ConnectTimeout:        10,
+		ConnectError:          20,
+		ConnectFailure:        30,
+		DataTimedOut:          40,
+		KeepAliveTimedOut:     50,
+		MaxConnections:        60,
+		CurrentConnections:    40,
+		SSLConnectTimeout:     10,
+		SSLConnectError:       20,
+		SSLConnectFailure:     30,
+		SSLDataTimedOut:       40,
+		SSLKeepAliveTimedOut:  50,
+		SSLMaxConnections:     60,
+		SSLCurrentConnections: 40,
+	}
+	th.AssertDeepEquals(t, expected, content)
+}
+
+func TestIsCached(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetCachingResponse(t, id1)
+
+	res, err := IsContentCached(client.ServiceClient(), id1)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, res)
+}
+
+func TestEnablingCaching(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockEnableCachingResponse(t, id1)
+
+	err := EnableCaching(client.ServiceClient(), id1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisablingCaching(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDisableCachingResponse(t, id1)
+
+	err := DisableCaching(client.ServiceClient(), id1).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/lbs/results.go b/rackspace/lb/v1/lbs/results.go
new file mode 100644
index 0000000..98f3962
--- /dev/null
+++ b/rackspace/lb/v1/lbs/results.go
@@ -0,0 +1,420 @@
+package lbs
+
+import (
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+	"github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+)
+
+// Protocol represents the network protocol which the load balancer accepts.
+type Protocol struct {
+	// The name of the protocol, e.g. HTTP, LDAP, FTP, etc.
+	Name string
+
+	// The port number for the protocol.
+	Port int
+}
+
+// Algorithm defines how traffic should be directed between back-end nodes.
+type Algorithm struct {
+	// The name of the algorithm, e.g RANDOM, ROUND_ROBIN, etc.
+	Name string
+}
+
+// Status represents the potential state of a load balancer resource.
+type Status string
+
+const (
+	// ACTIVE indicates that the LB is configured properly and ready to serve
+	// traffic to incoming requests via the configured virtual IPs.
+	ACTIVE Status = "ACTIVE"
+
+	// BUILD indicates that the LB is being provisioned for the first time and
+	// configuration is being applied to bring the service online. The service
+	// cannot yet serve incoming requests.
+	BUILD Status = "BUILD"
+
+	// PENDINGUPDATE indicates that the LB is online but configuration changes
+	// are being applied to update the service based on a previous request.
+	PENDINGUPDATE Status = "PENDING_UPDATE"
+
+	// PENDINGDELETE indicates that the LB is online but configuration changes
+	// are being applied to begin deletion of the service based on a previous
+	// request.
+	PENDINGDELETE Status = "PENDING_DELETE"
+
+	// SUSPENDED indicates that the LB has been taken offline and disabled.
+	SUSPENDED Status = "SUSPENDED"
+
+	// ERROR indicates that the system encountered an error when attempting to
+	// configure the load balancer.
+	ERROR Status = "ERROR"
+
+	// DELETED indicates that the LB has been deleted.
+	DELETED Status = "DELETED"
+)
+
+// Datetime represents the structure of a Created or Updated field.
+type Datetime struct {
+	Time time.Time `mapstructure:"-"`
+}
+
+// LoadBalancer represents a load balancer API resource.
+type LoadBalancer struct {
+	// Human-readable name for the load balancer.
+	Name string
+
+	// The unique ID for the load balancer.
+	ID int
+
+	// Represents the service protocol being load balanced. See Protocol type for
+	// a list of accepted values.
+	// See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+	// for a full list of supported protocols.
+	Protocol string
+
+	// Defines how traffic should be directed between back-end nodes. The default
+	// algorithm is RANDOM. See Algorithm type for a list of accepted values.
+	Algorithm string
+
+	// The current status of the load balancer.
+	Status Status
+
+	// The number of load balancer nodes.
+	NodeCount int `mapstructure:"nodeCount"`
+
+	// Slice of virtual IPs associated with this load balancer.
+	VIPs []vips.VIP `mapstructure:"virtualIps"`
+
+	// Datetime when the LB was created.
+	Created Datetime
+
+	// Datetime when the LB was created.
+	Updated Datetime
+
+	// Port number for the service you are load balancing.
+	Port int
+
+	// HalfClosed provides the ability for one end of the connection to
+	// terminate its output while still receiving data from the other end. This
+	// is only available on TCP/TCP_CLIENT_FIRST protocols.
+	HalfClosed bool
+
+	// Timeout represents the timeout value between a load balancer and its
+	// nodes. Defaults to 30 seconds with a maximum of 120 seconds.
+	Timeout int
+
+	// The cluster name.
+	Cluster Cluster
+
+	// Nodes shows all the back-end nodes which are associated with the load
+	// balancer. These are the devices which are delivered traffic.
+	Nodes []nodes.Node
+
+	// Current connection logging configuration.
+	ConnectionLogging ConnectionLogging
+
+	// SessionPersistence specifies whether multiple requests from clients are
+	// directed to the same node.
+	SessionPersistence sessions.SessionPersistence
+
+	// ConnectionThrottle specifies a limit on the number of connections per IP
+	// address to help mitigate malicious or abusive traffic to your applications.
+	ConnectionThrottle throttle.ConnectionThrottle
+
+	// The source public and private IP addresses.
+	SourceAddrs SourceAddrs `mapstructure:"sourceAddresses"`
+
+	// Represents the access rules for this particular load balancer. IP addresses
+	// or subnet ranges, depending on their type (ALLOW or DENY), can be permitted
+	// or blocked.
+	AccessList acl.AccessList
+}
+
+// SourceAddrs represents the source public and private IP addresses.
+type SourceAddrs struct {
+	IPv4Public  string `json:"ipv4Public" mapstructure:"ipv4Public"`
+	IPv4Private string `json:"ipv4Servicenet" mapstructure:"ipv4Servicenet"`
+	IPv6Public  string `json:"ipv6Public" mapstructure:"ipv6Public"`
+	IPv6Private string `json:"ipv6Servicenet" mapstructure:"ipv6Servicenet"`
+}
+
+// ConnectionLogging - temp
+type ConnectionLogging struct {
+	Enabled bool
+}
+
+// Cluster - temp
+type Cluster struct {
+	Name string
+}
+
+// LBPage is the page returned by a pager when traversing over a collection of
+// LBs.
+type LBPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p LBPage) IsEmpty() (bool, error) {
+	is, err := ExtractLBs(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractLBs accepts a Page struct, specifically a LBPage struct, and extracts
+// the elements into a slice of LoadBalancer structs. In other words, a generic
+// collection is mapped into a relevant slice.
+func ExtractLBs(page pagination.Page) ([]LoadBalancer, error) {
+	var resp struct {
+		LBs []LoadBalancer `mapstructure:"loadBalancers" json:"loadBalancers"`
+	}
+
+	coll := page.(LBPage).Body
+	err := mapstructure.Decode(coll, &resp)
+
+	s := reflect.ValueOf(coll.(map[string]interface{})["loadBalancers"])
+
+	for i := 0; i < s.Len(); i++ {
+		val := (s.Index(i).Interface()).(map[string]interface{})
+
+		ts, err := extractTS(val, "created")
+		if err != nil {
+			return resp.LBs, err
+		}
+		resp.LBs[i].Created.Time = ts
+
+		ts, err = extractTS(val, "updated")
+		if err != nil {
+			return resp.LBs, err
+		}
+		resp.LBs[i].Updated.Time = ts
+	}
+
+	return resp.LBs, err
+}
+
+func extractTS(body map[string]interface{}, key string) (time.Time, error) {
+	val := body[key].(map[string]interface{})
+	return time.Parse(time.RFC3339, val["time"].(string))
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as a LB, if possible.
+func (r commonResult) Extract() (*LoadBalancer, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		LB LoadBalancer `mapstructure:"loadBalancer"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	json := r.Body.(map[string]interface{})
+	lb := json["loadBalancer"].(map[string]interface{})
+
+	ts, err := extractTS(lb, "created")
+	if err != nil {
+		return nil, err
+	}
+	response.LB.Created.Time = ts
+
+	ts, err = extractTS(lb, "updated")
+	if err != nil {
+		return nil, err
+	}
+	response.LB.Updated.Time = ts
+
+	return &response.LB, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// ProtocolPage is the page returned by a pager when traversing over a
+// collection of LB protocols.
+type ProtocolPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a ProtocolPage struct is empty.
+func (p ProtocolPage) IsEmpty() (bool, error) {
+	is, err := ExtractProtocols(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractProtocols accepts a Page struct, specifically a ProtocolPage struct,
+// and extracts the elements into a slice of Protocol structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractProtocols(page pagination.Page) ([]Protocol, error) {
+	var resp struct {
+		Protocols []Protocol `mapstructure:"protocols" json:"protocols"`
+	}
+	err := mapstructure.Decode(page.(ProtocolPage).Body, &resp)
+	return resp.Protocols, err
+}
+
+// AlgorithmPage is the page returned by a pager when traversing over a
+// collection of LB algorithms.
+type AlgorithmPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AlgorithmPage struct is empty.
+func (p AlgorithmPage) IsEmpty() (bool, error) {
+	is, err := ExtractAlgorithms(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractAlgorithms accepts a Page struct, specifically an AlgorithmPage struct,
+// and extracts the elements into a slice of Algorithm structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractAlgorithms(page pagination.Page) ([]Algorithm, error) {
+	var resp struct {
+		Algorithms []Algorithm `mapstructure:"algorithms" json:"algorithms"`
+	}
+	err := mapstructure.Decode(page.(AlgorithmPage).Body, &resp)
+	return resp.Algorithms, err
+}
+
+// ErrorPage represents the HTML file that is shown to an end user who is
+// attempting to access a load balancer node that is offline/unavailable.
+//
+// During provisioning, every load balancer is configured with a default error
+// page that gets displayed when traffic is requested for an offline node.
+//
+// You can add a single custom error page with an HTTP-based protocol to a load
+// balancer. Page updates override existing content. If a custom error page is
+// deleted, or the load balancer is changed to a non-HTTP protocol, the default
+// error page is restored.
+type ErrorPage struct {
+	Content string
+}
+
+// ErrorPageResult represents the result of an error page operation -
+// specifically getting or creating one.
+type ErrorPageResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as an ErrorPage, if possible.
+func (r ErrorPageResult) Extract() (*ErrorPage, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		ErrorPage ErrorPage `mapstructure:"errorpage"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.ErrorPage, err
+}
+
+// Stats represents all the key information about a load balancer's usage.
+type Stats struct {
+	// The number of connections closed by this load balancer because its
+	// ConnectTimeout interval was exceeded.
+	ConnectTimeout int `mapstructure:"connectTimeOut"`
+
+	// The number of transaction or protocol errors for this load balancer.
+	ConnectError int
+
+	// Number of connection failures for this load balancer.
+	ConnectFailure int
+
+	// Number of connections closed by this load balancer because its Timeout
+	// interval was exceeded.
+	DataTimedOut int
+
+	// Number of connections closed by this load balancer because the
+	// 'keepalive_timeout' interval was exceeded.
+	KeepAliveTimedOut int
+
+	// The maximum number of simultaneous TCP connections this load balancer has
+	// processed at any one time.
+	MaxConnections int `mapstructure:"maxConn"`
+
+	// Number of simultaneous connections active at the time of the request.
+	CurrentConnections int `mapstructure:"currentConn"`
+
+	// Number of SSL connections closed by this load balancer because the
+	// ConnectTimeout interval was exceeded.
+	SSLConnectTimeout int `mapstructure:"connectTimeOutSsl"`
+
+	// Number of SSL transaction or protocol erros in this load balancer.
+	SSLConnectError int `mapstructure:"connectErrorSsl"`
+
+	// Number of SSL connection failures in this load balancer.
+	SSLConnectFailure int `mapstructure:"connectFailureSsl"`
+
+	// Number of SSL connections closed by this load balancer because the
+	// Timeout interval was exceeded.
+	SSLDataTimedOut int `mapstructure:"dataTimedOutSsl"`
+
+	// Number of SSL connections closed by this load balancer because the
+	// 'keepalive_timeout' interval was exceeded.
+	SSLKeepAliveTimedOut int `mapstructure:"keepAliveTimedOutSsl"`
+
+	// Maximum number of simultaneous SSL connections this load balancer has
+	// processed at any one time.
+	SSLMaxConnections int `mapstructure:"maxConnSsl"`
+
+	// Number of simultaneous SSL connections active at the time of the request.
+	SSLCurrentConnections int `mapstructure:"currentConnSsl"`
+}
+
+// StatsResult represents the result of a Stats operation.
+type StatsResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as a Stats struct, if possible.
+func (r StatsResult) Extract() (*Stats, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	res := &Stats{}
+	err := mapstructure.Decode(r.Body, res)
+	return res, err
+}
diff --git a/rackspace/lb/v1/lbs/urls.go b/rackspace/lb/v1/lbs/urls.go
new file mode 100644
index 0000000..471a86b
--- /dev/null
+++ b/rackspace/lb/v1/lbs/urls.go
@@ -0,0 +1,49 @@
+package lbs
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	path           = "loadbalancers"
+	protocolsPath  = "protocols"
+	algorithmsPath = "algorithms"
+	logPath        = "connectionlogging"
+	epPath         = "errorpage"
+	stPath         = "stats"
+	cachePath      = "contentcaching"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id))
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(path)
+}
+
+func protocolsURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(path, protocolsPath)
+}
+
+func algorithmsURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(path, algorithmsPath)
+}
+
+func loggingURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), logPath)
+}
+
+func errorPageURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), epPath)
+}
+
+func statsURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), stPath)
+}
+
+func cacheURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), cachePath)
+}
diff --git a/rackspace/lb/v1/monitors/doc.go b/rackspace/lb/v1/monitors/doc.go
new file mode 100644
index 0000000..2c5be75
--- /dev/null
+++ b/rackspace/lb/v1/monitors/doc.go
@@ -0,0 +1,21 @@
+/*
+Package monitors provides information and interaction with the Health Monitor
+API resource for the Rackspace Cloud Load Balancer service.
+
+The load balancing service includes a health monitoring resource that
+periodically checks your back-end nodes to ensure they are responding correctly.
+If a node does not respond, it is removed from rotation until the health monitor
+determines that the node is functional. In addition to being performed
+periodically, a health check also executes against every new node that is
+added, to ensure that the node is operating properly before allowing it to
+service traffic. Only one health monitor is allowed to be enabled on a load
+balancer at a time.
+
+As part of a good strategy for monitoring connections, secondary nodes should
+also be created which provide failover for effectively routing traffic in case
+the primary node fails. This is an additional feature that ensures that you
+remain up in case your primary node fails.
+
+There are three types of health monitor: CONNECT, HTTP and HTTPS.
+*/
+package monitors
diff --git a/rackspace/lb/v1/monitors/fixtures.go b/rackspace/lb/v1/monitors/fixtures.go
new file mode 100644
index 0000000..a565abc
--- /dev/null
+++ b/rackspace/lb/v1/monitors/fixtures.go
@@ -0,0 +1,87 @@
+package monitors
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+	return "/loadbalancers/" + strconv.Itoa(lbID) + "/healthmonitor"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+  "healthMonitor": {
+    "type": "CONNECT",
+    "delay": 10,
+    "timeout": 10,
+    "attemptsBeforeDeactivation": 3
+  }
+}
+  `)
+	})
+}
+
+func mockUpdateConnectResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "healthMonitor": {
+    "type": "CONNECT",
+    "delay": 10,
+    "timeout": 10,
+    "attemptsBeforeDeactivation": 3
+  }
+}
+    `)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockUpdateHTTPResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "healthMonitor": {
+    "attemptsBeforeDeactivation": 3,
+    "bodyRegex": "{regex}",
+    "delay": 10,
+    "path": "/foo",
+    "statusRegex": "200",
+    "timeout": 10,
+    "type": "HTTPS"
+  }
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/rackspace/lb/v1/monitors/requests.go b/rackspace/lb/v1/monitors/requests.go
new file mode 100644
index 0000000..d4ba276
--- /dev/null
+++ b/rackspace/lb/v1/monitors/requests.go
@@ -0,0 +1,160 @@
+package monitors
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+)
+
+var (
+	errAttemptLimit = errors.New("AttemptLimit field must be an int greater than 1 and less than 10")
+	errDelay        = errors.New("Delay field must be an int greater than 1 and less than 10")
+	errTimeout      = errors.New("Timeout field must be an int greater than 1 and less than 10")
+)
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package.
+type UpdateOptsBuilder interface {
+	ToMonitorUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateConnectMonitorOpts represents the options needed to update a CONNECT
+// monitor.
+type UpdateConnectMonitorOpts struct {
+	// Required - number of permissible monitor failures before removing a node
+	// from rotation. Must be a number between 1 and 10.
+	AttemptLimit int
+
+	// Required - the minimum number of seconds to wait before executing the
+	// health monitor. Must be a number between 1 and 3600.
+	Delay int
+
+	// Required - maximum number of seconds to wait for a connection to be
+	// established before timing out. Must be a number between 1 and 300.
+	Timeout int
+}
+
+// ToMonitorUpdateMap produces a map for updating CONNECT monitors.
+func (opts UpdateConnectMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+	type m map[string]interface{}
+
+	if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
+		return m{}, errAttemptLimit
+	}
+	if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
+		return m{}, errDelay
+	}
+	if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
+		return m{}, errTimeout
+	}
+
+	return m{"healthMonitor": m{
+		"attemptsBeforeDeactivation": opts.AttemptLimit,
+		"delay":   opts.Delay,
+		"timeout": opts.Timeout,
+		"type":    CONNECT,
+	}}, nil
+}
+
+// UpdateHTTPMonitorOpts represents the options needed to update a HTTP monitor.
+type UpdateHTTPMonitorOpts struct {
+	// Required - number of permissible monitor failures before removing a node
+	// from rotation. Must be a number between 1 and 10.
+	AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
+
+	// Required - the minimum number of seconds to wait before executing the
+	// health monitor. Must be a number between 1 and 3600.
+	Delay int
+
+	// Required - maximum number of seconds to wait for a connection to be
+	// established before timing out. Must be a number between 1 and 300.
+	Timeout int
+
+	// Required - a regular expression that will be used to evaluate the contents
+	// of the body of the response.
+	BodyRegex string
+
+	// Required - the HTTP path that will be used in the sample request.
+	Path string
+
+	// Required - a regular expression that will be used to evaluate the HTTP
+	// status code returned in the response.
+	StatusRegex string
+
+	// Optional - the name of a host for which the health monitors will check.
+	HostHeader string
+
+	// Required - either HTTP or HTTPS
+	Type Type
+}
+
+// ToMonitorUpdateMap produces a map for updating HTTP(S) monitors.
+func (opts UpdateHTTPMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+	type m map[string]interface{}
+
+	if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
+		return m{}, errAttemptLimit
+	}
+	if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
+		return m{}, errDelay
+	}
+	if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
+		return m{}, errTimeout
+	}
+	if opts.Type != HTTP && opts.Type != HTTPS {
+		return m{}, errors.New("Type must either by HTTP or HTTPS")
+	}
+	if opts.BodyRegex == "" {
+		return m{}, errors.New("BodyRegex is a required field")
+	}
+	if opts.Path == "" {
+		return m{}, errors.New("Path is a required field")
+	}
+	if opts.StatusRegex == "" {
+		return m{}, errors.New("StatusRegex is a required field")
+	}
+
+	json := m{
+		"attemptsBeforeDeactivation": opts.AttemptLimit,
+		"delay":       opts.Delay,
+		"timeout":     opts.Timeout,
+		"type":        opts.Type,
+		"bodyRegex":   opts.BodyRegex,
+		"path":        opts.Path,
+		"statusRegex": opts.StatusRegex,
+	}
+
+	if opts.HostHeader != "" {
+		json["hostHeader"] = opts.HostHeader
+	}
+
+	return m{"healthMonitor": json}, nil
+}
+
+// Update is the operation responsible for updating a health monitor.
+func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToMonitorUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(rootURL(c, id), reqBody, nil, nil)
+	return res
+}
+
+// Get is the operation responsible for showing details of a health monitor.
+func Get(c *gophercloud.ServiceClient, id int) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(rootURL(c, id), &res.Body, nil)
+	return res
+}
+
+// Delete is the operation responsible for deleting a health monitor.
+func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(rootURL(c, id), nil)
+	return res
+}
diff --git a/rackspace/lb/v1/monitors/requests_test.go b/rackspace/lb/v1/monitors/requests_test.go
new file mode 100644
index 0000000..76a60db
--- /dev/null
+++ b/rackspace/lb/v1/monitors/requests_test.go
@@ -0,0 +1,75 @@
+package monitors
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestUpdateCONNECT(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateConnectResponse(t, lbID)
+
+	opts := UpdateConnectMonitorOpts{
+		AttemptLimit: 3,
+		Delay:        10,
+		Timeout:      10,
+	}
+
+	err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdateHTTP(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateHTTPResponse(t, lbID)
+
+	opts := UpdateHTTPMonitorOpts{
+		AttemptLimit: 3,
+		Delay:        10,
+		Timeout:      10,
+		BodyRegex:    "{regex}",
+		Path:         "/foo",
+		StatusRegex:  "200",
+		Type:         HTTPS,
+	}
+
+	err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetResponse(t, lbID)
+
+	m, err := Get(client.ServiceClient(), lbID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Monitor{
+		Type:         CONNECT,
+		Delay:        10,
+		Timeout:      10,
+		AttemptLimit: 3,
+	}
+
+	th.AssertDeepEquals(t, expected, m)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteResponse(t, lbID)
+
+	err := Delete(client.ServiceClient(), lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/monitors/results.go b/rackspace/lb/v1/monitors/results.go
new file mode 100644
index 0000000..eec556f
--- /dev/null
+++ b/rackspace/lb/v1/monitors/results.go
@@ -0,0 +1,90 @@
+package monitors
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// Type represents the type of Monitor.
+type Type string
+
+// Useful constants.
+const (
+	CONNECT Type = "CONNECT"
+	HTTP    Type = "HTTP"
+	HTTPS   Type = "HTTPS"
+)
+
+// Monitor represents a health monitor API resource. A monitor comes in three
+// forms: CONNECT, HTTP or HTTPS.
+//
+// A CONNECT monitor establishes a basic connection to each node on its defined
+// port to ensure that the service is listening properly. The connect monitor
+// is the most basic type of health check and does no post-processing or
+// protocol-specific health checks.
+//
+// HTTP and HTTPS health monitors are generally considered more intelligent and
+// powerful than CONNECT. It is capable of processing an HTTP or HTTPS response
+// to determine the condition of a node. It supports the same basic properties
+// as CONNECT and includes additional attributes that are used to evaluate the
+// HTTP response.
+type Monitor struct {
+	// Number of permissible monitor failures before removing a node from
+	// rotation.
+	AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
+
+	// The minimum number of seconds to wait before executing the health monitor.
+	Delay int
+
+	// Maximum number of seconds to wait for a connection to be established
+	// before timing out.
+	Timeout int
+
+	// Type of the health monitor.
+	Type Type
+
+	// A regular expression that will be used to evaluate the contents of the
+	// body of the response.
+	BodyRegex string
+
+	// The name of a host for which the health monitors will check.
+	HostHeader string
+
+	// The HTTP path that will be used in the sample request.
+	Path string
+
+	// A regular expression that will be used to evaluate the HTTP status code
+	// returned in the response.
+	StatusRegex string
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// DeleteResult represents the result of an Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// Extract interprets any GetResult as a Monitor.
+func (r GetResult) Extract() (*Monitor, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		M Monitor `mapstructure:"healthMonitor"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.M, err
+}
diff --git a/rackspace/lb/v1/monitors/urls.go b/rackspace/lb/v1/monitors/urls.go
new file mode 100644
index 0000000..0a1e6df
--- /dev/null
+++ b/rackspace/lb/v1/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	path        = "loadbalancers"
+	monitorPath = "healthmonitor"
+)
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+	return c.ServiceURL(path, strconv.Itoa(lbID), monitorPath)
+}
diff --git a/rackspace/lb/v1/nodes/doc.go b/rackspace/lb/v1/nodes/doc.go
new file mode 100644
index 0000000..49c4318
--- /dev/null
+++ b/rackspace/lb/v1/nodes/doc.go
@@ -0,0 +1,35 @@
+/*
+Package nodes provides information and interaction with the Node API resource
+for the Rackspace Cloud Load Balancer service.
+
+Nodes are responsible for servicing the requests received through the load
+balancer's virtual IP. A node is usually a virtual machine. By default, the
+load balancer employs a basic health check that ensures the node is listening
+on its defined port. The node is checked at the time of addition and at regular
+intervals as defined by the load balancer's health check configuration. If a
+back-end node is not listening on its port, or does not meet the conditions of
+the defined check, then connections will not be forwarded to the node, and its
+status is changed to OFFLINE. Only nodes that are in an ONLINE status receive
+and can service traffic from the load balancer.
+
+All nodes have an associated status that indicates whether the node is
+ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status can receive
+and service traffic from the load balancer. The OFFLINE status represents a
+node that cannot accept or service traffic. A node in DRAINING status
+represents a node that stops the traffic manager from sending any additional
+new connections to the node, but honors established sessions. If the traffic
+manager receives a request and session persistence requires that the node is
+used, the traffic manager uses it. The status is determined by the passive or
+active health monitors.
+
+If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then the
+caller should assign the relevant weights to the node as part of the weight
+attribute of the node element. When the algorithm of the load balancer is
+changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an assigned
+weight, the service automatically sets the weight to 1 for all nodes.
+
+One or more secondary nodes can be added to a specified load balancer so that
+if all the primary nodes fail, traffic can be redirected to secondary nodes.
+The type attribute allows configuring the node as either PRIMARY or SECONDARY.
+*/
+package nodes
diff --git a/rackspace/lb/v1/nodes/fixtures.go b/rackspace/lb/v1/nodes/fixtures.go
new file mode 100644
index 0000000..8899fc5
--- /dev/null
+++ b/rackspace/lb/v1/nodes/fixtures.go
@@ -0,0 +1,243 @@
+package nodes
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+	return "/loadbalancers/" + strconv.Itoa(lbID) + "/nodes"
+}
+
+func _nodeURL(lbID, nodeID int) string {
+	return _rootURL(lbID) + "/" + strconv.Itoa(nodeID)
+}
+
+func mockListResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+  "nodes": [
+    {
+      "id": 410,
+      "address": "10.1.1.1",
+      "port": 80,
+      "condition": "ENABLED",
+      "status": "ONLINE",
+      "weight": 3,
+      "type": "PRIMARY"
+    },
+    {
+      "id": 411,
+      "address": "10.1.1.2",
+      "port": 80,
+      "condition": "ENABLED",
+      "status": "ONLINE",
+      "weight": 8,
+      "type": "SECONDARY"
+    }
+  ]
+}
+  `)
+	})
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "nodes": [
+    {
+      "address": "10.2.2.3",
+      "port": 80,
+      "condition": "ENABLED",
+      "type": "PRIMARY"
+    },
+    {
+      "address": "10.2.2.4",
+      "port": 81,
+      "condition": "ENABLED",
+      "type": "SECONDARY"
+    }
+  ]
+}
+    `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+  "nodes": [
+    {
+      "address": "10.2.2.3",
+      "id": 185,
+      "port": 80,
+      "status": "ONLINE",
+      "condition": "ENABLED",
+      "weight": 1,
+      "type": "PRIMARY"
+    },
+    {
+      "address": "10.2.2.4",
+      "id": 186,
+      "port": 81,
+      "status": "ONLINE",
+      "condition": "ENABLED",
+      "weight": 1,
+      "type": "SECONDARY"
+    }
+  ]
+}
+  `)
+	})
+}
+
+func mockCreateErrResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "nodes": [
+    {
+      "address": "10.2.2.3",
+      "port": 80,
+      "condition": "ENABLED",
+      "type": "PRIMARY"
+    },
+    {
+      "address": "10.2.2.4",
+      "port": 81,
+      "condition": "ENABLED",
+      "type": "SECONDARY"
+    }
+  ]
+}
+    `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(422) // Unprocessable Entity
+
+		fmt.Fprintf(w, `
+{
+  "code": 422,
+  "message": "Load Balancer '%d' has a status of 'PENDING_UPDATE' and is considered immutable."
+}
+  `, lbID)
+	})
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		r.ParseForm()
+
+		for k, v := range ids {
+			fids := r.Form["id"]
+			th.AssertEquals(t, strconv.Itoa(v), fids[k])
+		}
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDeleteResponse(t *testing.T, lbID, nodeID int) {
+	th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockGetResponse(t *testing.T, lbID, nodeID int) {
+	th.Mux.HandleFunc(_nodeURL(lbID, nodeID), 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, `
+{
+  "node": {
+    "id": 410,
+    "address": "10.1.1.1",
+    "port": 80,
+    "condition": "ENABLED",
+    "status": "ONLINE",
+    "weight": 12,
+    "type": "PRIMARY"
+  }
+}
+  `)
+	})
+}
+
+func mockUpdateResponse(t *testing.T, lbID, nodeID int) {
+	th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "node": {
+    "condition": "DRAINING",
+    "weight": 10,
+		"type": "SECONDARY"
+  }
+}
+    `)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockListEventsResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID)+"/events", 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, `
+{
+  "nodeServiceEvents": [
+    {
+      "detailedMessage": "Node is ok",
+      "nodeId": 373,
+      "id": 7,
+      "type": "UPDATE_NODE",
+      "description": "Node '373' status changed to 'ONLINE' for load balancer '323'",
+      "category": "UPDATE",
+      "severity": "INFO",
+      "relativeUri": "/406271/loadbalancers/323/nodes/373/events",
+      "accountId": 406271,
+      "loadbalancerId": 323,
+      "title": "Node Status Updated",
+      "author": "Rackspace Cloud",
+      "created": "10-30-2012 10:18:23"
+    }
+  ]
+}
+`)
+	})
+}
diff --git a/rackspace/lb/v1/nodes/requests.go b/rackspace/lb/v1/nodes/requests.go
new file mode 100644
index 0000000..dc2d46c
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -0,0 +1,251 @@
+package nodes
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// load balancer nodes. It requires the node ID, its parent load balancer ID,
+// and optional limit integer (passed in either as a pointer or a nil poitner).
+func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager {
+	url := rootURL(client, loadBalancerID)
+	if limit != nil {
+		url += fmt.Sprintf("?limit=%d", limit)
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return NodePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+	ToNodeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+	// Required - the IP address or CIDR for this back-end node. It can either be
+	// a private IP (ServiceNet) or a public IP.
+	Address string
+
+	// Optional - the port on which traffic is sent and received.
+	Port int
+
+	// Optional - the condition of the node. See the consts in Results.go.
+	Condition Condition
+
+	// Optional - the type of the node. See the consts in Results.go.
+	Type Type
+
+	// Optional - a pointer to an integer between 0 and 100.
+	Weight *int
+}
+
+func validateWeight(weight *int) error {
+	if weight != nil && (*weight > 100 || *weight < 0) {
+		return errors.New("Weight must be a valid int between 0 and 100")
+	}
+	return nil
+}
+
+// ToNodeCreateMap converts a slice of options into a map that can be used for
+// the JSON.
+func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) {
+	type nodeMap map[string]interface{}
+	nodes := []nodeMap{}
+
+	for k, v := range opts {
+		if v.Address == "" {
+			return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k)
+		}
+		if weightErr := validateWeight(v.Weight); weightErr != nil {
+			return nodeMap{}, weightErr
+		}
+
+		node := make(map[string]interface{})
+		node["address"] = v.Address
+
+		if v.Port > 0 {
+			node["port"] = v.Port
+		}
+		if v.Condition != "" {
+			node["condition"] = v.Condition
+		}
+		if v.Type != "" {
+			node["type"] = v.Type
+		}
+		if v.Weight != nil {
+			node["weight"] = &v.Weight
+		}
+
+		nodes = append(nodes, node)
+	}
+
+	return nodeMap{"nodes": nodes}, nil
+}
+
+// Create is the operation responsible for creating a new node on a load
+// balancer. Since every load balancer exists in both ServiceNet and the public
+// Internet, both private and public IP addresses can be used for nodes.
+//
+// If nodes need time to boot up services before they become operational, you
+// can temporarily prevent traffic from being sent to that node by setting the
+// Condition field to DRAINING. Health checks will still be performed; but once
+// your node is ready, you can update its condition to ENABLED and have it
+// handle traffic.
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToNodeCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	resp, err := client.Post(rootURL(client, loadBalancerID), reqBody, &res.Body, nil)
+
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	pr := pagination.PageResultFromParsed(resp, res.Body)
+	return CreateResult{pagination.SinglePageBase(pr)}
+}
+
+// BulkDelete is the operation responsible for batch deleting multiple nodes in
+// a single operation. It accepts a slice of integer IDs and will remove them
+// from the load balancer. The maximum limit is 10 node removals at once.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult {
+	var res DeleteResult
+
+	if len(nodeIDs) > 10 || len(nodeIDs) == 0 {
+		res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs")
+		return res
+	}
+
+	url := rootURL(c, loadBalancerID)
+	url += gophercloud.IDSliceToQueryString("id", nodeIDs)
+
+	_, res.Err = c.Delete(url, nil)
+	return res
+}
+
+// Get is the operation responsible for showing details for a single node.
+func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(resourceURL(c, lbID, nodeID), &res.Body, nil)
+	return res
+}
+
+// UpdateOptsBuilder represents a type that can be converted into a JSON-like
+// map structure.
+type UpdateOptsBuilder interface {
+	ToNodeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represent the options for updating an existing node.
+type UpdateOpts struct {
+	// Optional - the condition of the node. See the consts in Results.go.
+	Condition Condition
+
+	// Optional - the type of the node. See the consts in Results.go.
+	Type Type
+
+	// Optional - a pointer to an integer between 0 and 100.
+	Weight *int
+}
+
+// ToNodeUpdateMap converts an options struct into a JSON-like map.
+func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) {
+	node := make(map[string]interface{})
+
+	if opts.Condition != "" {
+		node["condition"] = opts.Condition
+	}
+	if opts.Weight != nil {
+		if weightErr := validateWeight(opts.Weight); weightErr != nil {
+			return node, weightErr
+		}
+		node["weight"] = &opts.Weight
+	}
+	if opts.Type != "" {
+		node["type"] = opts.Type
+	}
+
+	return map[string]interface{}{"node": node}, nil
+}
+
+// Update is the operation responsible for updating an existing node. A node's
+// IP, port, and status are immutable attributes and cannot be modified.
+func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToNodeUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(resourceURL(c, lbID, nodeID), reqBody, nil, nil)
+	return res
+}
+
+// Delete is the operation responsible for permanently deleting a node.
+func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, lbID, nodeID), nil)
+	return res
+}
+
+// ListEventsOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListEventsOptsBuilder interface {
+	ToEventsListQuery() (string, error)
+}
+
+// ListEventsOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListEventsOpts struct {
+	Marker string `q:"marker"`
+	Limit  int    `q:"limit"`
+}
+
+// ToEventsListQuery formats a ListOpts into a query string.
+func (opts ListEventsOpts) ToEventsListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// ListEvents is the operation responsible for listing all the events
+// associated with the activity between the node and the load balancer. The
+// events report errors found with the node. The detailedMessage provides the
+// detailed reason for the error.
+func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager {
+	url := eventsURL(client, loadBalancerID)
+
+	if opts != nil {
+		query, err := opts.ToEventsListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return NodeEventPage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/rackspace/lb/v1/nodes/requests_test.go b/rackspace/lb/v1/nodes/requests_test.go
new file mode 100644
index 0000000..a964af8
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests_test.go
@@ -0,0 +1,243 @@
+package nodes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	lbID    = 12345
+	nodeID  = 67890
+	nodeID2 = 67891
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListResponse(t, lbID)
+
+	count := 0
+
+	err := List(client.ServiceClient(), lbID, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNodes(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Node{
+			Node{
+				ID:        410,
+				Address:   "10.1.1.1",
+				Port:      80,
+				Condition: ENABLED,
+				Status:    ONLINE,
+				Weight:    3,
+				Type:      PRIMARY,
+			},
+			Node{
+				ID:        411,
+				Address:   "10.1.1.2",
+				Port:      80,
+				Condition: ENABLED,
+				Status:    ONLINE,
+				Weight:    8,
+				Type:      SECONDARY,
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateResponse(t, lbID)
+
+	opts := CreateOpts{
+		CreateOpt{
+			Address:   "10.2.2.3",
+			Port:      80,
+			Condition: ENABLED,
+			Type:      PRIMARY,
+		},
+		CreateOpt{
+			Address:   "10.2.2.4",
+			Port:      81,
+			Condition: ENABLED,
+			Type:      SECONDARY,
+		},
+	}
+
+	page := Create(client.ServiceClient(), lbID, opts)
+
+	actual, err := page.ExtractNodes()
+	th.AssertNoErr(t, err)
+
+	expected := []Node{
+		Node{
+			ID:        185,
+			Address:   "10.2.2.3",
+			Port:      80,
+			Condition: ENABLED,
+			Status:    ONLINE,
+			Weight:    1,
+			Type:      PRIMARY,
+		},
+		Node{
+			ID:        186,
+			Address:   "10.2.2.4",
+			Port:      81,
+			Condition: ENABLED,
+			Status:    ONLINE,
+			Weight:    1,
+			Type:      SECONDARY,
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestCreateErr(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateErrResponse(t, lbID)
+
+	opts := CreateOpts{
+		CreateOpt{
+			Address:   "10.2.2.3",
+			Port:      80,
+			Condition: ENABLED,
+			Type:      PRIMARY,
+		},
+		CreateOpt{
+			Address:   "10.2.2.4",
+			Port:      81,
+			Condition: ENABLED,
+			Type:      SECONDARY,
+		},
+	}
+
+	page := Create(client.ServiceClient(), lbID, opts)
+
+	actual, err := page.ExtractNodes()
+	if err == nil {
+		t.Fatal("Did not receive expected error from ExtractNodes")
+	}
+	if actual != nil {
+		t.Fatalf("Received non-nil result from failed ExtractNodes: %#v", actual)
+	}
+}
+
+func TestBulkDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	ids := []int{nodeID, nodeID2}
+
+	mockBatchDeleteResponse(t, lbID, ids)
+
+	err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetResponse(t, lbID, nodeID)
+
+	node, err := Get(client.ServiceClient(), lbID, nodeID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Node{
+		ID:        410,
+		Address:   "10.1.1.1",
+		Port:      80,
+		Condition: ENABLED,
+		Status:    ONLINE,
+		Weight:    12,
+		Type:      PRIMARY,
+	}
+
+	th.AssertDeepEquals(t, expected, node)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateResponse(t, lbID, nodeID)
+
+	opts := UpdateOpts{
+		Weight:    gophercloud.IntToPointer(10),
+		Condition: DRAINING,
+		Type:      SECONDARY,
+	}
+
+	err := Update(client.ServiceClient(), lbID, nodeID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteResponse(t, lbID, nodeID)
+
+	err := Delete(client.ServiceClient(), lbID, nodeID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListEvents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListEventsResponse(t, lbID)
+
+	count := 0
+
+	pager := ListEvents(client.ServiceClient(), lbID, ListEventsOpts{})
+
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNodeEvents(page)
+		th.AssertNoErr(t, err)
+
+		expected := []NodeEvent{
+			NodeEvent{
+				DetailedMessage: "Node is ok",
+				NodeID:          373,
+				ID:              7,
+				Type:            "UPDATE_NODE",
+				Description:     "Node '373' status changed to 'ONLINE' for load balancer '323'",
+				Category:        "UPDATE",
+				Severity:        "INFO",
+				RelativeURI:     "/406271/loadbalancers/323/nodes/373/events",
+				AccountID:       406271,
+				LoadBalancerID:  323,
+				Title:           "Node Status Updated",
+				Author:          "Rackspace Cloud",
+				Created:         "10-30-2012 10:18:23",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
diff --git a/rackspace/lb/v1/nodes/results.go b/rackspace/lb/v1/nodes/results.go
new file mode 100644
index 0000000..57835dc
--- /dev/null
+++ b/rackspace/lb/v1/nodes/results.go
@@ -0,0 +1,213 @@
+package nodes
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Node represents a back-end device, usually a virtual machine, that can
+// handle traffic. It is assigned traffic based on its parent load balancer.
+type Node struct {
+	// The IP address or CIDR for this back-end node.
+	Address string
+
+	// The unique ID for this node.
+	ID int
+
+	// The port on which traffic is sent and received.
+	Port int
+
+	// The node's status.
+	Status Status
+
+	// The node's condition.
+	Condition Condition
+
+	// The priority at which this node will receive traffic if a weighted
+	// algorithm is used by its parent load balancer. Ranges from 1 to 100.
+	Weight int
+
+	// Type of node.
+	Type Type
+}
+
+// Type indicates whether the node is of a PRIMARY or SECONDARY nature.
+type Type string
+
+const (
+	// PRIMARY nodes are in the normal rotation to receive traffic from the load
+	// balancer.
+	PRIMARY Type = "PRIMARY"
+
+	// SECONDARY nodes are only in the rotation to receive traffic from the load
+	// balancer when all the primary nodes fail. This provides a failover feature
+	// that automatically routes traffic to the secondary node in the event that
+	// the primary node is disabled or in a failing state. Note that active
+	// health monitoring must be enabled on the load balancer to enable the
+	// failover feature to the secondary node.
+	SECONDARY Type = "SECONDARY"
+)
+
+// Condition represents the condition of a node.
+type Condition string
+
+const (
+	// ENABLED indicates that the node is permitted to accept new connections.
+	ENABLED Condition = "ENABLED"
+
+	// DISABLED indicates that the node is not permitted to accept any new
+	// connections regardless of session persistence configuration. Existing
+	// connections are forcibly terminated.
+	DISABLED Condition = "DISABLED"
+
+	// DRAINING indicates that the node is allowed to service existing
+	// established connections and connections that are being directed to it as a
+	// result of the session persistence configuration.
+	DRAINING Condition = "DRAINING"
+)
+
+// Status indicates whether the node can accept service traffic. If a node is
+// not listening on its port or does not meet the conditions of the defined
+// active health check for the load balancer, then the load balancer does not
+// forward connections, and its status is listed as OFFLINE.
+type Status string
+
+const (
+	// ONLINE indicates that the node is healthy and capable of receiving traffic
+	// from the load balancer.
+	ONLINE Status = "ONLINE"
+
+	// OFFLINE indicates that the node is not in a position to receive service
+	// traffic. It is usually switched into this state when a health check is not
+	// satisfied with the node's response time.
+	OFFLINE Status = "OFFLINE"
+)
+
+// NodePage is the page returned by a pager when traversing over a collection
+// of nodes.
+type NodePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a NodePage struct is empty.
+func (p NodePage) IsEmpty() (bool, error) {
+	is, err := ExtractNodes(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+func commonExtractNodes(body interface{}) ([]Node, error) {
+	var resp struct {
+		Nodes []Node `mapstructure:"nodes" json:"nodes"`
+	}
+
+	err := mapstructure.Decode(body, &resp)
+
+	return resp.Nodes, err
+}
+
+// ExtractNodes accepts a Page struct, specifically a NodePage struct, and
+// extracts the elements into a slice of Node structs. In other words, a
+// generic collection is mapped into a relevant slice.
+func ExtractNodes(page pagination.Page) ([]Node, error) {
+	return commonExtractNodes(page.(NodePage).Body)
+}
+
+// CreateResult represents the result of a create operation. Since multiple
+// nodes can be added in one operation, this result represents multiple nodes
+// and should be treated as a typical pagination Page. Use its ExtractNodes
+// method to get out a slice of Node structs.
+type CreateResult struct {
+	pagination.SinglePageBase
+}
+
+// ExtractNodes extracts a slice of Node structs from a CreateResult.
+func (res CreateResult) ExtractNodes() ([]Node, error) {
+	if res.Err != nil {
+		return nil, res.Err
+	}
+	return commonExtractNodes(res.Body)
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+func (r commonResult) Extract() (*Node, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Node Node `mapstructure:"node"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.Node, err
+}
+
+// NodeEvent represents a service event that occurred between a node and a
+// load balancer.
+type NodeEvent struct {
+	ID              int
+	DetailedMessage string
+	NodeID          int
+	Type            string
+	Description     string
+	Category        string
+	Severity        string
+	RelativeURI     string
+	AccountID       int
+	LoadBalancerID  int
+	Title           string
+	Author          string
+	Created         string
+}
+
+// NodeEventPage is a concrete type which embeds the common SinglePageBase
+// struct, and is used when traversing node event collections.
+type NodeEventPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty is a concrete function which indicates whether an NodeEventPage is
+// empty or not.
+func (r NodeEventPage) IsEmpty() (bool, error) {
+	is, err := ExtractNodeEvents(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractNodeEvents accepts a Page struct, specifically a NodeEventPage
+// struct, and extracts the elements into a slice of NodeEvent structs. In
+// other words, the collection is mapped into a relevant slice.
+func ExtractNodeEvents(page pagination.Page) ([]NodeEvent, error) {
+	var resp struct {
+		Events []NodeEvent `mapstructure:"nodeServiceEvents" json:"nodeServiceEvents"`
+	}
+
+	err := mapstructure.Decode(page.(NodeEventPage).Body, &resp)
+
+	return resp.Events, err
+}
diff --git a/rackspace/lb/v1/nodes/urls.go b/rackspace/lb/v1/nodes/urls.go
new file mode 100644
index 0000000..2cefee2
--- /dev/null
+++ b/rackspace/lb/v1/nodes/urls.go
@@ -0,0 +1,25 @@
+package nodes
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	lbPath    = "loadbalancers"
+	nodePath  = "nodes"
+	eventPath = "events"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string {
+	return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, strconv.Itoa(nodeID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+	return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath)
+}
+
+func eventsURL(c *gophercloud.ServiceClient, lbID int) string {
+	return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, eventPath)
+}
diff --git a/rackspace/lb/v1/sessions/doc.go b/rackspace/lb/v1/sessions/doc.go
new file mode 100644
index 0000000..dcec0a8
--- /dev/null
+++ b/rackspace/lb/v1/sessions/doc.go
@@ -0,0 +1,30 @@
+/*
+Package sessions provides information and interaction with the Session
+Persistence feature of the Rackspace Cloud Load Balancer service.
+
+Session persistence is a feature of the load balancing service that forces
+multiple requests from clients (of the same protocol) to be directed to the
+same node. This is common with many web applications that do not inherently
+share application state between back-end servers.
+
+There are two modes to choose from: HTTP_COOKIE and SOURCE_IP. You can only set
+one of the session persistence modes on a load balancer, and it can only
+support one protocol. If you set HTTP_COOKIE mode for an HTTP load balancer, it
+supports session persistence for HTTP requests only. Likewise, if you set
+SOURCE_IP mode for an HTTPS load balancer, it supports session persistence for
+only HTTPS requests.
+
+To support session persistence for both HTTP and HTTPS requests concurrently,
+choose one of the following options:
+
+- Use two load balancers, one configured for session persistence for HTTP
+requests and the other configured for session persistence for HTTPS requests.
+That way, the load balancers support session persistence for both HTTP and
+HTTPS requests concurrently, with each load balancer supporting one of the
+protocols.
+
+- Use one load balancer, configure it for session persistence for HTTP requests,
+and then enable SSL termination for that load balancer. The load balancer
+supports session persistence for both HTTP and HTTPS requests concurrently.
+*/
+package sessions
diff --git a/rackspace/lb/v1/sessions/fixtures.go b/rackspace/lb/v1/sessions/fixtures.go
new file mode 100644
index 0000000..077ef04
--- /dev/null
+++ b/rackspace/lb/v1/sessions/fixtures.go
@@ -0,0 +1,59 @@
+package sessions
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+	return "/loadbalancers/" + strconv.Itoa(id) + "/sessionpersistence"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+  "sessionPersistence": {
+    "persistenceType": "HTTP_COOKIE"
+  }
+}
+`)
+	})
+}
+
+func mockEnableResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "sessionPersistence": {
+    "persistenceType": "HTTP_COOKIE"
+  }
+}
+    `)
+
+		w.WriteHeader(http.StatusAccepted)
+		fmt.Fprintf(w, `{}`)
+	})
+}
+
+func mockDisableResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/rackspace/lb/v1/sessions/requests.go b/rackspace/lb/v1/sessions/requests.go
new file mode 100644
index 0000000..a93d766
--- /dev/null
+++ b/rackspace/lb/v1/sessions/requests.go
@@ -0,0 +1,63 @@
+package sessions
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package.
+type CreateOptsBuilder interface {
+	ToSPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// Required - can either be HTTPCOOKIE or SOURCEIP
+	Type Type
+}
+
+// ToSPCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSPCreateMap() (map[string]interface{}, error) {
+	sp := make(map[string]interface{})
+
+	if opts.Type == "" {
+		return sp, errors.New("Type is a required field")
+	}
+
+	sp["persistenceType"] = opts.Type
+	return map[string]interface{}{"sessionPersistence": sp}, nil
+}
+
+// Enable is the operation responsible for enabling session persistence for a
+// particular load balancer.
+func Enable(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) EnableResult {
+	var res EnableResult
+
+	reqBody, err := opts.ToSPCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(rootURL(c, lbID), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get is the operation responsible for showing details of the session
+// persistence configuration for a particular load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(rootURL(c, lbID), &res.Body, nil)
+	return res
+}
+
+// Disable is the operation responsible for disabling session persistence for a
+// particular load balancer.
+func Disable(c *gophercloud.ServiceClient, lbID int) DisableResult {
+	var res DisableResult
+	_, res.Err = c.Delete(rootURL(c, lbID), nil)
+	return res
+}
diff --git a/rackspace/lb/v1/sessions/requests_test.go b/rackspace/lb/v1/sessions/requests_test.go
new file mode 100644
index 0000000..f319e54
--- /dev/null
+++ b/rackspace/lb/v1/sessions/requests_test.go
@@ -0,0 +1,44 @@
+package sessions
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestEnable(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockEnableResponse(t, lbID)
+
+	opts := CreateOpts{Type: HTTPCOOKIE}
+	err := Enable(client.ServiceClient(), lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetResponse(t, lbID)
+
+	sp, err := Get(client.ServiceClient(), lbID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SessionPersistence{Type: HTTPCOOKIE}
+	th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestDisable(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDisableResponse(t, lbID)
+
+	err := Disable(client.ServiceClient(), lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/sessions/results.go b/rackspace/lb/v1/sessions/results.go
new file mode 100644
index 0000000..fe90e72
--- /dev/null
+++ b/rackspace/lb/v1/sessions/results.go
@@ -0,0 +1,58 @@
+package sessions
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// Type represents the type of session persistence being used.
+type Type string
+
+const (
+	// HTTPCOOKIE is a session persistence mechanism that inserts an HTTP cookie
+	// and is used to determine the destination back-end node. This is supported
+	// for HTTP load balancing only.
+	HTTPCOOKIE Type = "HTTP_COOKIE"
+
+	// SOURCEIP is a session persistence mechanism that keeps track of the source
+	// IP address that is mapped and is able to determine the destination
+	// back-end node. This is supported for HTTPS pass-through and non-HTTP load
+	// balancing only.
+	SOURCEIP Type = "SOURCE_IP"
+)
+
+// SessionPersistence indicates how a load balancer is using session persistence
+type SessionPersistence struct {
+	Type Type `mapstructure:"persistenceType"`
+}
+
+// EnableResult represents the result of an enable operation.
+type EnableResult struct {
+	gophercloud.ErrResult
+}
+
+// DisableResult represents the result of a disable operation.
+type DisableResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult as an SP, if possible.
+func (r GetResult) Extract() (*SessionPersistence, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		SP SessionPersistence `mapstructure:"sessionPersistence"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.SP, err
+}
diff --git a/rackspace/lb/v1/sessions/urls.go b/rackspace/lb/v1/sessions/urls.go
new file mode 100644
index 0000000..c4a896d
--- /dev/null
+++ b/rackspace/lb/v1/sessions/urls.go
@@ -0,0 +1,16 @@
+package sessions
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	path   = "loadbalancers"
+	spPath = "sessionpersistence"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), spPath)
+}
diff --git a/rackspace/lb/v1/ssl/doc.go b/rackspace/lb/v1/ssl/doc.go
new file mode 100644
index 0000000..6a2c174
--- /dev/null
+++ b/rackspace/lb/v1/ssl/doc.go
@@ -0,0 +1,22 @@
+/*
+Package ssl provides information and interaction with the SSL Termination
+feature of the Rackspace Cloud Load Balancer service.
+
+You may only enable and configure SSL termination on load balancers with
+non-secure protocols, such as HTTP, but not HTTPS.
+
+SSL-terminated load balancers decrypt the traffic at the traffic manager and
+pass unencrypted traffic to the back-end node. Because of this, the customer's
+back-end nodes don't know what protocol the client requested. For this reason,
+the X-Forwarded-Proto (XFP) header has been added for identifying the
+originating protocol of an HTTP request as "http" or "https" depending on what
+protocol the client requested.
+
+Not every service returns certificates in the proper order. Please verify that
+your chain of certificates matches that of walking up the chain from the domain
+to the CA root.
+
+If used for HTTP to HTTPS redirection, the LoadBalancer's securePort attribute
+must be set to 443, and its secureTrafficOnly attribute must be true.
+*/
+package ssl
diff --git a/rackspace/lb/v1/ssl/fixtures.go b/rackspace/lb/v1/ssl/fixtures.go
new file mode 100644
index 0000000..5a52962
--- /dev/null
+++ b/rackspace/lb/v1/ssl/fixtures.go
@@ -0,0 +1,196 @@
+package ssl
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+	return "/loadbalancers/" + strconv.Itoa(id) + "/ssltermination"
+}
+
+func _certURL(id, certID int) string {
+	url := _rootURL(id) + "/certificatemappings"
+	if certID > 0 {
+		url += "/" + strconv.Itoa(certID)
+	}
+	return url
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+  "sslTermination": {
+    "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+    "enabled": true,
+    "secureTrafficOnly": false,
+    "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+    "securePort": 443
+  }
+}
+`)
+	})
+}
+
+func mockUpdateResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "sslTermination": {
+    "enabled": true,
+    "securePort": 443,
+    "secureTrafficOnly": false,
+		"privateKey": "foo",
+		"certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+  	"intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+	}
+}
+    `)
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{}`)
+	})
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusOK)
+	})
+}
+
+func mockListCertsResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_certURL(lbID, 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, `
+{
+  "certificateMappings": [
+    {
+      "certificateMapping": {
+        "id": 123,
+        "hostName": "rackspace.com"
+      }
+    },
+    {
+      "certificateMapping": {
+        "id": 124,
+        "hostName": "*.rackspace.com"
+      }
+    }
+  ]
+}
+`)
+	})
+}
+
+func mockAddCertResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_certURL(lbID, 0), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "certificateMapping": {
+    "hostName": "rackspace.com",
+    "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+    "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+    "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+  }
+}
+		`)
+
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"certificateMapping": {
+		"id": 123,
+		"hostName": "rackspace.com",
+		"certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		"intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+	}
+}
+			`)
+	})
+}
+
+func mockGetCertResponse(t *testing.T, lbID, certID int) {
+	th.Mux.HandleFunc(_certURL(lbID, certID), 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, `
+{
+  "certificateMapping": {
+    "id": 123,
+    "hostName": "rackspace.com",
+    "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+    "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+  }
+}
+`)
+	})
+}
+
+func mockUpdateCertResponse(t *testing.T, lbID, certID int) {
+	th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "certificateMapping": {
+    "hostName": "rackspace.com",
+    "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+    "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+    "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+  }
+}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+	"certificateMapping": {
+		"id": 123,
+		"hostName": "rackspace.com",
+		"certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		"intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+	}
+}
+			`)
+	})
+}
+
+func mockDeleteCertResponse(t *testing.T, lbID, certID int) {
+	th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusOK)
+	})
+}
diff --git a/rackspace/lb/v1/ssl/requests.go b/rackspace/lb/v1/ssl/requests.go
new file mode 100644
index 0000000..bb53ef8
--- /dev/null
+++ b/rackspace/lb/v1/ssl/requests.go
@@ -0,0 +1,247 @@
+package ssl
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+var (
+	errPrivateKey     = errors.New("PrivateKey is a required field")
+	errCertificate    = errors.New("Certificate is a required field")
+	errIntCertificate = errors.New("IntCertificate is a required field")
+)
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package.
+type UpdateOptsBuilder interface {
+	ToSSLUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	// Required - consult the SSLTermConfig struct for more info.
+	SecurePort int
+
+	// Required - consult the SSLTermConfig struct for more info.
+	PrivateKey string
+
+	// Required - consult the SSLTermConfig struct for more info.
+	Certificate string
+
+	// Required - consult the SSLTermConfig struct for more info.
+	IntCertificate string
+
+	// Optional - consult the SSLTermConfig struct for more info.
+	Enabled *bool
+
+	// Optional - consult the SSLTermConfig struct for more info.
+	SecureTrafficOnly *bool
+}
+
+// ToSSLUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToSSLUpdateMap() (map[string]interface{}, error) {
+	ssl := make(map[string]interface{})
+
+	if opts.SecurePort == 0 {
+		return ssl, errors.New("SecurePort needs to be an integer greater than 0")
+	}
+	if opts.PrivateKey == "" {
+		return ssl, errPrivateKey
+	}
+	if opts.Certificate == "" {
+		return ssl, errCertificate
+	}
+	if opts.IntCertificate == "" {
+		return ssl, errIntCertificate
+	}
+
+	ssl["securePort"] = opts.SecurePort
+	ssl["privateKey"] = opts.PrivateKey
+	ssl["certificate"] = opts.Certificate
+	ssl["intermediateCertificate"] = opts.IntCertificate
+
+	if opts.Enabled != nil {
+		ssl["enabled"] = &opts.Enabled
+	}
+
+	if opts.SecureTrafficOnly != nil {
+		ssl["secureTrafficOnly"] = &opts.SecureTrafficOnly
+	}
+
+	return map[string]interface{}{"sslTermination": ssl}, nil
+}
+
+// Update is the operation responsible for updating the SSL Termination
+// configuration for a load balancer.
+func Update(c *gophercloud.ServiceClient, lbID int, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToSSLUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(rootURL(c, lbID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Get is the operation responsible for showing the details of the SSL
+// Termination configuration for a load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(rootURL(c, lbID), &res.Body, nil)
+	return res
+}
+
+// Delete is the operation responsible for deleting the SSL Termination
+// configuration for a load balancer.
+func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(rootURL(c, lbID), &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// ListCerts will list all of the certificate mappings associated with a
+// SSL-terminated HTTP load balancer.
+func ListCerts(c *gophercloud.ServiceClient, lbID int) pagination.Pager {
+	url := certURL(c, lbID)
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return CertPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateCertOptsBuilder is the interface options structs have to satisfy in
+// order to be used in the AddCert operation in this package.
+type CreateCertOptsBuilder interface {
+	ToCertCreateMap() (map[string]interface{}, error)
+}
+
+// CreateCertOpts represents the options used when adding a new certificate mapping.
+type CreateCertOpts struct {
+	HostName       string
+	PrivateKey     string
+	Certificate    string
+	IntCertificate string
+}
+
+// ToCertCreateMap will cast an CreateCertOpts struct to a map for JSON serialization.
+func (opts CreateCertOpts) ToCertCreateMap() (map[string]interface{}, error) {
+	cm := make(map[string]interface{})
+
+	if opts.HostName == "" {
+		return cm, errors.New("HostName is a required option")
+	}
+	if opts.PrivateKey == "" {
+		return cm, errPrivateKey
+	}
+	if opts.Certificate == "" {
+		return cm, errCertificate
+	}
+
+	cm["hostName"] = opts.HostName
+	cm["privateKey"] = opts.PrivateKey
+	cm["certificate"] = opts.Certificate
+
+	if opts.IntCertificate != "" {
+		cm["intermediateCertificate"] = opts.IntCertificate
+	}
+
+	return map[string]interface{}{"certificateMapping": cm}, nil
+}
+
+// CreateCert will add a new SSL certificate and allow an SSL-terminated HTTP
+// load balancer to use it. This feature is useful because it allows multiple
+// certificates to be used. The maximum number of certificates that can be
+// stored per LB is 20.
+func CreateCert(c *gophercloud.ServiceClient, lbID int, opts CreateCertOptsBuilder) CreateCertResult {
+	var res CreateCertResult
+
+	reqBody, err := opts.ToCertCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(certURL(c, lbID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return res
+}
+
+// GetCert will show the details of an existing SSL certificate.
+func GetCert(c *gophercloud.ServiceClient, lbID, certID int) GetCertResult {
+	var res GetCertResult
+	_, res.Err = c.Get(certResourceURL(c, lbID, certID), &res.Body, nil)
+	return res
+}
+
+// UpdateCertOptsBuilder is the interface options structs have to satisfy in
+// order to be used in the UpdateCert operation in this package.
+type UpdateCertOptsBuilder interface {
+	ToCertUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateCertOpts represents the options needed to update a SSL certificate.
+type UpdateCertOpts struct {
+	HostName       string
+	PrivateKey     string
+	Certificate    string
+	IntCertificate string
+}
+
+// ToCertUpdateMap will cast an UpdateCertOpts struct into a map for JSON
+// seralization.
+func (opts UpdateCertOpts) ToCertUpdateMap() (map[string]interface{}, error) {
+	cm := make(map[string]interface{})
+
+	if opts.HostName != "" {
+		cm["hostName"] = opts.HostName
+	}
+	if opts.PrivateKey != "" {
+		cm["privateKey"] = opts.PrivateKey
+	}
+	if opts.Certificate != "" {
+		cm["certificate"] = opts.Certificate
+	}
+	if opts.IntCertificate != "" {
+		cm["intermediateCertificate"] = opts.IntCertificate
+	}
+
+	return map[string]interface{}{"certificateMapping": cm}, nil
+}
+
+// UpdateCert is the operation responsible for updating the details of an
+// existing SSL certificate.
+func UpdateCert(c *gophercloud.ServiceClient, lbID, certID int, opts UpdateCertOptsBuilder) UpdateCertResult {
+	var res UpdateCertResult
+
+	reqBody, err := opts.ToCertUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(certResourceURL(c, lbID, certID), reqBody, &res.Body, nil)
+	return res
+}
+
+// DeleteCert is the operation responsible for permanently removing a SSL
+// certificate.
+func DeleteCert(c *gophercloud.ServiceClient, lbID, certID int) DeleteResult {
+	var res DeleteResult
+
+	_, res.Err = c.Delete(certResourceURL(c, lbID, certID), &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+
+	return res
+}
diff --git a/rackspace/lb/v1/ssl/requests_test.go b/rackspace/lb/v1/ssl/requests_test.go
new file mode 100644
index 0000000..fb14c4a
--- /dev/null
+++ b/rackspace/lb/v1/ssl/requests_test.go
@@ -0,0 +1,167 @@
+package ssl
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	lbID   = 12345
+	certID = 67890
+)
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetResponse(t, lbID)
+
+	sp, err := Get(client.ServiceClient(), lbID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SSLTermConfig{
+		Certificate:       "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		Enabled:           true,
+		SecureTrafficOnly: false,
+		IntCertificate:    "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+		SecurePort:        443,
+	}
+	th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateResponse(t, lbID)
+
+	opts := UpdateOpts{
+		Enabled:           gophercloud.Enabled,
+		SecurePort:        443,
+		SecureTrafficOnly: gophercloud.Disabled,
+		PrivateKey:        "foo",
+		Certificate:       "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		IntCertificate:    "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+	}
+	err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteResponse(t, lbID)
+
+	err := Delete(client.ServiceClient(), lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListCerts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListCertsResponse(t, lbID)
+
+	count := 0
+
+	err := ListCerts(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractCerts(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Certificate{
+			Certificate{ID: 123, HostName: "rackspace.com"},
+			Certificate{ID: 124, HostName: "*.rackspace.com"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateCert(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddCertResponse(t, lbID)
+
+	opts := CreateCertOpts{
+		HostName:       "rackspace.com",
+		PrivateKey:     "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+		Certificate:    "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+	}
+
+	cm, err := CreateCert(client.ServiceClient(), lbID, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Certificate{
+		ID:             123,
+		HostName:       "rackspace.com",
+		Certificate:    "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+	}
+	th.AssertDeepEquals(t, expected, cm)
+}
+
+func TestGetCertMapping(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetCertResponse(t, lbID, certID)
+
+	sp, err := GetCert(client.ServiceClient(), lbID, certID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Certificate{
+		ID:             123,
+		HostName:       "rackspace.com",
+		Certificate:    "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+	}
+	th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestUpdateCert(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateCertResponse(t, lbID, certID)
+
+	opts := UpdateCertOpts{
+		HostName:       "rackspace.com",
+		PrivateKey:     "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+		Certificate:    "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+	}
+
+	cm, err := UpdateCert(client.ServiceClient(), lbID, certID, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Certificate{
+		ID:             123,
+		HostName:       "rackspace.com",
+		Certificate:    "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+		IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+	}
+	th.AssertDeepEquals(t, expected, cm)
+}
+
+func TestDeleteCert(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteCertResponse(t, lbID, certID)
+
+	err := DeleteCert(client.ServiceClient(), lbID, certID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/ssl/results.go b/rackspace/lb/v1/ssl/results.go
new file mode 100644
index 0000000..ead9fcd
--- /dev/null
+++ b/rackspace/lb/v1/ssl/results.go
@@ -0,0 +1,148 @@
+package ssl
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SSLTermConfig represents the SSL configuration for a particular load balancer.
+type SSLTermConfig struct {
+	// The port on which the SSL termination load balancer listens for secure
+	// traffic. The value must be unique to the existing LB protocol/port
+	// combination
+	SecurePort int `mapstructure:"securePort"`
+
+	// The private key for the SSL certificate which is validated and verified
+	// against the provided certificates.
+	PrivateKey string `mapstructure:"privatekey"`
+
+	// The certificate used for SSL termination, which is validated and verified
+	// against the key and intermediate certificate if provided.
+	Certificate string
+
+	// The intermediate certificate (for the user). The intermediate certificate
+	// is validated and verified against the key and certificate credentials
+	// provided. A user may only provide this value when accompanied by a
+	// Certificate, PrivateKey, and SecurePort. It may not be added or updated as
+	// a single attribute in a future operation.
+	IntCertificate string `mapstructure:"intermediatecertificate"`
+
+	// Determines if the load balancer is enabled to terminate SSL traffic or not.
+	// If this is set to false, the load balancer retains its specified SSL
+	// attributes but does not terminate SSL traffic.
+	Enabled bool
+
+	// Determines if the load balancer can only accept secure traffic. If set to
+	// true, the load balancer will not accept non-secure traffic.
+	SecureTrafficOnly bool
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult as a SSLTermConfig struct, if possible.
+func (r GetResult) Extract() (*SSLTermConfig, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		SSL SSLTermConfig `mapstructure:"sslTermination"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.SSL, err
+}
+
+// Certificate represents an SSL certificate associated with an SSL-terminated
+// HTTP load balancer.
+type Certificate struct {
+	ID             int
+	HostName       string
+	Certificate    string
+	IntCertificate string `mapstructure:"intermediateCertificate"`
+}
+
+// CertPage represents a page of certificates.
+type CertPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a CertMappingPage struct is empty.
+func (p CertPage) IsEmpty() (bool, error) {
+	is, err := ExtractCerts(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractCerts accepts a Page struct, specifically a CertPage struct, and
+// extracts the elements into a slice of Cert structs. In other words, a generic
+// collection is mapped into a relevant slice.
+func ExtractCerts(page pagination.Page) ([]Certificate, error) {
+	type NestedMap struct {
+		Cert Certificate `mapstructure:"certificateMapping" json:"certificateMapping"`
+	}
+	var resp struct {
+		Certs []NestedMap `mapstructure:"certificateMappings" json:"certificateMappings"`
+	}
+
+	err := mapstructure.Decode(page.(CertPage).Body, &resp)
+
+	slice := []Certificate{}
+	for _, cert := range resp.Certs {
+		slice = append(slice, cert.Cert)
+	}
+
+	return slice, err
+}
+
+type certResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a result as a CertMapping struct, if possible.
+func (r certResult) Extract() (*Certificate, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Cert Certificate `mapstructure:"certificateMapping"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.Cert, err
+}
+
+// CreateCertResult represents the result of an CreateCert operation.
+type CreateCertResult struct {
+	certResult
+}
+
+// GetCertResult represents the result of a GetCert operation.
+type GetCertResult struct {
+	certResult
+}
+
+// UpdateCertResult represents the result of an UpdateCert operation.
+type UpdateCertResult struct {
+	certResult
+}
diff --git a/rackspace/lb/v1/ssl/urls.go b/rackspace/lb/v1/ssl/urls.go
new file mode 100644
index 0000000..aa814b3
--- /dev/null
+++ b/rackspace/lb/v1/ssl/urls.go
@@ -0,0 +1,25 @@
+package ssl
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	path     = "loadbalancers"
+	sslPath  = "ssltermination"
+	certPath = "certificatemappings"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), sslPath)
+}
+
+func certURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath)
+}
+
+func certResourceURL(c *gophercloud.ServiceClient, id, certID int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath, strconv.Itoa(certID))
+}
diff --git a/rackspace/lb/v1/throttle/doc.go b/rackspace/lb/v1/throttle/doc.go
new file mode 100644
index 0000000..1ed605d
--- /dev/null
+++ b/rackspace/lb/v1/throttle/doc.go
@@ -0,0 +1,5 @@
+/*
+Package throttle provides information and interaction with the Connection
+Throttling feature of the Rackspace Cloud Load Balancer service.
+*/
+package throttle
diff --git a/rackspace/lb/v1/throttle/fixtures.go b/rackspace/lb/v1/throttle/fixtures.go
new file mode 100644
index 0000000..f3e49fa
--- /dev/null
+++ b/rackspace/lb/v1/throttle/fixtures.go
@@ -0,0 +1,62 @@
+package throttle
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+	return "/loadbalancers/" + strconv.Itoa(id) + "/connectionthrottle"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+  "connectionThrottle": {
+    "maxConnections": 100
+  }
+}
+`)
+	})
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "connectionThrottle": {
+    "maxConnectionRate": 0,
+    "maxConnections": 200,
+    "minConnections": 0,
+    "rateInterval": 0
+  }
+}
+    `)
+
+		w.WriteHeader(http.StatusAccepted)
+		fmt.Fprintf(w, `{}`)
+	})
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/rackspace/lb/v1/throttle/requests.go b/rackspace/lb/v1/throttle/requests.go
new file mode 100644
index 0000000..0446b97
--- /dev/null
+++ b/rackspace/lb/v1/throttle/requests.go
@@ -0,0 +1,76 @@
+package throttle
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package.
+type CreateOptsBuilder interface {
+	ToCTCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// Required - the maximum amount of connections per IP address to allow per LB.
+	MaxConnections int
+
+	// Deprecated as of v1.22.
+	MaxConnectionRate int
+
+	// Deprecated as of v1.22.
+	MinConnections int
+
+	// Deprecated as of v1.22.
+	RateInterval int
+}
+
+// ToCTCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCTCreateMap() (map[string]interface{}, error) {
+	ct := make(map[string]interface{})
+
+	if opts.MaxConnections < 0 || opts.MaxConnections > 100000 {
+		return ct, errors.New("MaxConnections must be an int between 0 and 100000")
+	}
+
+	ct["maxConnections"] = opts.MaxConnections
+	ct["maxConnectionRate"] = opts.MaxConnectionRate
+	ct["minConnections"] = opts.MinConnections
+	ct["rateInterval"] = opts.RateInterval
+
+	return map[string]interface{}{"connectionThrottle": ct}, nil
+}
+
+// Create is the operation responsible for creating or updating the connection
+// throttling configuration for a load balancer.
+func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToCTCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Put(rootURL(c, lbID), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get is the operation responsible for showing the details of the connection
+// throttling configuration for a load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(rootURL(c, lbID), &res.Body, nil)
+	return res
+}
+
+// Delete is the operation responsible for deleting the connection throttling
+// configuration for a load balancer.
+func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(rootURL(c, lbID), nil)
+	return res
+}
diff --git a/rackspace/lb/v1/throttle/requests_test.go b/rackspace/lb/v1/throttle/requests_test.go
new file mode 100644
index 0000000..6e9703f
--- /dev/null
+++ b/rackspace/lb/v1/throttle/requests_test.go
@@ -0,0 +1,44 @@
+package throttle
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateResponse(t, lbID)
+
+	opts := CreateOpts{MaxConnections: 200}
+	err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetResponse(t, lbID)
+
+	sp, err := Get(client.ServiceClient(), lbID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &ConnectionThrottle{MaxConnections: 100}
+	th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteResponse(t, lbID)
+
+	err := Delete(client.ServiceClient(), lbID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/throttle/results.go b/rackspace/lb/v1/throttle/results.go
new file mode 100644
index 0000000..db93c6f
--- /dev/null
+++ b/rackspace/lb/v1/throttle/results.go
@@ -0,0 +1,43 @@
+package throttle
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// ConnectionThrottle represents the connection throttle configuration for a
+// particular load balancer.
+type ConnectionThrottle struct {
+	MaxConnections int
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult as a SP, if possible.
+func (r GetResult) Extract() (*ConnectionThrottle, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		CT ConnectionThrottle `mapstructure:"connectionThrottle"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.CT, err
+}
diff --git a/rackspace/lb/v1/throttle/urls.go b/rackspace/lb/v1/throttle/urls.go
new file mode 100644
index 0000000..b77f0ac
--- /dev/null
+++ b/rackspace/lb/v1/throttle/urls.go
@@ -0,0 +1,16 @@
+package throttle
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	path   = "loadbalancers"
+	ctPath = "connectionthrottle"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+	return c.ServiceURL(path, strconv.Itoa(id), ctPath)
+}
diff --git a/rackspace/lb/v1/vips/doc.go b/rackspace/lb/v1/vips/doc.go
new file mode 100644
index 0000000..5c3846d
--- /dev/null
+++ b/rackspace/lb/v1/vips/doc.go
@@ -0,0 +1,13 @@
+/*
+Package vips provides information and interaction with the Virtual IP API
+resource for the Rackspace Cloud Load Balancer service.
+
+A virtual IP (VIP) makes a load balancer accessible by clients. The load
+balancing service supports either a public VIP, routable on the public Internet,
+or a ServiceNet address, routable only within the region in which the load
+balancer resides.
+
+All load balancers must have at least one virtual IP associated with them at
+all times.
+*/
+package vips
diff --git a/rackspace/lb/v1/vips/fixtures.go b/rackspace/lb/v1/vips/fixtures.go
new file mode 100644
index 0000000..158759f
--- /dev/null
+++ b/rackspace/lb/v1/vips/fixtures.go
@@ -0,0 +1,88 @@
+package vips
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+	return "/loadbalancers/" + strconv.Itoa(lbID) + "/virtualips"
+}
+
+func mockListResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+  "virtualIps": [
+    {
+      "id": 1000,
+      "address": "206.10.10.210",
+      "type": "PUBLIC"
+    }
+  ]
+}
+  `)
+	})
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+    "type":"PUBLIC",
+    "ipVersion":"IPV6"
+}
+    `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+    "address":"fd24:f480:ce44:91bc:1af2:15ff:0000:0002",
+    "id":9000134,
+    "type":"PUBLIC",
+    "ipVersion":"IPV6"
+}
+  `)
+	})
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		r.ParseForm()
+
+		for k, v := range ids {
+			fids := r.Form["id"]
+			th.AssertEquals(t, strconv.Itoa(v), fids[k])
+		}
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockDeleteResponse(t *testing.T, lbID, vipID int) {
+	url := _rootURL(lbID) + "/" + strconv.Itoa(vipID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/rackspace/lb/v1/vips/requests.go b/rackspace/lb/v1/vips/requests.go
new file mode 100644
index 0000000..2bc924f
--- /dev/null
+++ b/rackspace/lb/v1/vips/requests.go
@@ -0,0 +1,97 @@
+package vips
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// load balancer virtual IP addresses.
+func List(client *gophercloud.ServiceClient, loadBalancerID int) pagination.Pager {
+	url := rootURL(client, loadBalancerID)
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return VIPPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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 {
+	ToVIPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// Optional - the ID of an existing virtual IP. By doing this, you are
+	// allowing load balancers to share IPV6 addresses.
+	ID string
+
+	// Optional - the type of address.
+	Type Type
+
+	// Optional - the version of address.
+	Version Version
+}
+
+// ToVIPCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) {
+	lb := make(map[string]interface{})
+
+	if opts.ID != "" {
+		lb["id"] = opts.ID
+	}
+	if opts.Type != "" {
+		lb["type"] = opts.Type
+	}
+	if opts.Version != "" {
+		lb["ipVersion"] = opts.Version
+	}
+
+	return lb, nil
+}
+
+// Create is the operation responsible for assigning a new Virtual IP to an
+// existing load balancer resource. Currently, only version 6 IP addresses may
+// be added.
+func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToVIPCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(rootURL(c, lbID), reqBody, &res.Body, nil)
+	return res
+}
+
+// BulkDelete is the operation responsible for batch deleting multiple VIPs in
+// a single operation. It accepts a slice of integer IDs and will remove them
+// from the load balancer. The maximum limit is 10 VIP removals at once.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, vipIDs []int) DeleteResult {
+	var res DeleteResult
+
+	if len(vipIDs) > 10 || len(vipIDs) == 0 {
+		res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 VIP IDs")
+		return res
+	}
+
+	url := rootURL(c, loadBalancerID)
+	url += gophercloud.IDSliceToQueryString("id", vipIDs)
+
+	_, res.Err = c.Delete(url, nil)
+	return res
+}
+
+// Delete is the operation responsible for permanently deleting a VIP.
+func Delete(c *gophercloud.ServiceClient, lbID, vipID int) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, lbID, vipID), nil)
+	return res
+}
diff --git a/rackspace/lb/v1/vips/requests_test.go b/rackspace/lb/v1/vips/requests_test.go
new file mode 100644
index 0000000..74ac461
--- /dev/null
+++ b/rackspace/lb/v1/vips/requests_test.go
@@ -0,0 +1,87 @@
+package vips
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	lbID   = 12345
+	vipID  = 67890
+	vipID2 = 67891
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListResponse(t, lbID)
+
+	count := 0
+
+	err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVIPs(page)
+		th.AssertNoErr(t, err)
+
+		expected := []VIP{
+			VIP{ID: 1000, Address: "206.10.10.210", Type: "PUBLIC"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateResponse(t, lbID)
+
+	opts := CreateOpts{
+		Type:    "PUBLIC",
+		Version: "IPV6",
+	}
+
+	vip, err := Create(client.ServiceClient(), lbID, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &VIP{
+		Address: "fd24:f480:ce44:91bc:1af2:15ff:0000:0002",
+		ID:      9000134,
+		Type:    "PUBLIC",
+		Version: "IPV6",
+	}
+
+	th.CheckDeepEquals(t, expected, vip)
+}
+
+func TestBulkDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	ids := []int{vipID, vipID2}
+
+	mockBatchDeleteResponse(t, lbID, ids)
+
+	err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteResponse(t, lbID, vipID)
+
+	err := Delete(client.ServiceClient(), lbID, vipID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/vips/results.go b/rackspace/lb/v1/vips/results.go
new file mode 100644
index 0000000..678b2af
--- /dev/null
+++ b/rackspace/lb/v1/vips/results.go
@@ -0,0 +1,89 @@
+package vips
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// VIP represents a Virtual IP API resource.
+type VIP struct {
+	Address string  `json:"address,omitempty"`
+	ID      int     `json:"id,omitempty"`
+	Type    Type    `json:"type,omitempty"`
+	Version Version `json:"ipVersion,omitempty" mapstructure:"ipVersion"`
+}
+
+// Version represents the version of a VIP.
+type Version string
+
+// Convenient constants to use for type
+const (
+	IPV4 Version = "IPV4"
+	IPV6 Version = "IPV6"
+)
+
+// Type represents the type of a VIP.
+type Type string
+
+const (
+	// PUBLIC indicates a VIP type that is routable on the public Internet.
+	PUBLIC Type = "PUBLIC"
+
+	// PRIVATE indicates a VIP type that is routable only on ServiceNet.
+	PRIVATE Type = "SERVICENET"
+)
+
+// VIPPage is the page returned by a pager when traversing over a collection
+// of VIPs.
+type VIPPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a VIPPage 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 VIP structs. In other words, a
+// generic collection is mapped into a relevant slice.
+func ExtractVIPs(page pagination.Page) ([]VIP, error) {
+	var resp struct {
+		VIPs []VIP `mapstructure:"virtualIps" json:"virtualIps"`
+	}
+
+	err := mapstructure.Decode(page.(VIPPage).Body, &resp)
+
+	return resp.VIPs, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+func (r commonResult) Extract() (*VIP, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	resp := &VIP{}
+	err := mapstructure.Decode(r.Body, resp)
+
+	return resp, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/vips/urls.go b/rackspace/lb/v1/vips/urls.go
new file mode 100644
index 0000000..28f063a
--- /dev/null
+++ b/rackspace/lb/v1/vips/urls.go
@@ -0,0 +1,20 @@
+package vips
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+const (
+	lbPath  = "loadbalancers"
+	vipPath = "virtualips"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string {
+	return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath, strconv.Itoa(nodeID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+	return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath)
+}
diff --git a/rackspace/networking/v2/common/common_tests.go b/rackspace/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..129cd63
--- /dev/null
+++ b/rackspace/networking/v2/common/common_tests.go
@@ -0,0 +1,12 @@
+package common
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return client.ServiceClient()
+}
diff --git a/rackspace/networking/v2/networks/delegate.go b/rackspace/networking/v2/networks/delegate.go
new file mode 100644
index 0000000..dcb0855
--- /dev/null
+++ b/rackspace/networking/v2/networks/delegate.go
@@ -0,0 +1,41 @@
+package networks
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retrieves a specific network based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+	return os.Get(c, networkID)
+}
+
+// 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 os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// 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 os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+	return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/networks/delegate_test.go b/rackspace/networking/v2/networks/delegate_test.go
new file mode 100644
index 0000000..0b3a6b1
--- /dev/null
+++ b/rackspace/networking/v2/networks/delegate_test.go
@@ -0,0 +1,285 @@
+package networks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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": [
+        {
+            "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, os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []os.Network{
+			os.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",
+			},
+			os.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("/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("/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 := os.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("/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)
+		fmt.Fprintf(w, `
+{
+	"network": {
+			"name": "sample_network",
+			"admin_state_up": true,
+			"shared": true,
+			"tenant_id": "12345"
+	}
+}
+		`)
+	})
+
+	iTrue := true
+	options := os.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("/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 := true
+	options := os.UpdateOpts{Name: "new_network_name", AdminStateUp: os.Down, 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("/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/rackspace/networking/v2/ports/delegate.go b/rackspace/networking/v2/ports/delegate.go
new file mode 100644
index 0000000..95728d1
--- /dev/null
+++ b/rackspace/networking/v2/ports/delegate.go
@@ -0,0 +1,43 @@
+package ports
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 a user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+	return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. You must remember to provide a NetworkID value.
+//
+// NOTE: Currently the SecurityGroup option is not implemented to work with
+// Rackspace.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the port associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+	return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/ports/delegate_test.go b/rackspace/networking/v2/ports/delegate_test.go
new file mode 100644
index 0000000..f53ff59
--- /dev/null
+++ b/rackspace/networking/v2/ports/delegate_test.go
@@ -0,0 +1,322 @@
+package ports
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/pagination"
+	fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		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(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractPorts(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []os.Port{
+			os.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: []os.IP{
+					os.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("/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, []os.IP{
+		os.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("/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 := os.CreateOpts{
+		Name:         "private-port",
+		AdminStateUp: &asu,
+		NetworkID:    "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+		FixedIPs: []os.IP{
+			os.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, []os.IP{
+		os.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(), os.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+		th.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 := os.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []os.IP{
+			os.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, []os.IP{
+		os.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("/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/rackspace/networking/v2/security/doc.go b/rackspace/networking/v2/security/doc.go
new file mode 100644
index 0000000..31f744c
--- /dev/null
+++ b/rackspace/networking/v2/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 is 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
+//    is 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/rackspace/networking/v2/security/groups/delegate.go b/rackspace/networking/v2/security/groups/delegate.go
new file mode 100644
index 0000000..1e9a23a
--- /dev/null
+++ b/rackspace/networking/v2/security/groups/delegate.go
@@ -0,0 +1,30 @@
+package groups
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 os.ListOpts) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// 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 os.CreateOpts) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(c, id)
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(c, id)
+}
diff --git a/rackspace/networking/v2/security/groups/delegate_test.go b/rackspace/networking/v2/security/groups/delegate_test.go
new file mode 100644
index 0000000..45cd3ba
--- /dev/null
+++ b/rackspace/networking/v2/security/groups/delegate_test.go
@@ -0,0 +1,206 @@
+package groups
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	osGroups "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups"
+	osRules "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"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/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(), osGroups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := osGroups.ExtractGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract secgroups: %v", err)
+			return false, err
+		}
+
+		expected := []osGroups.SecGroup{
+			osGroups.SecGroup{
+				Description: "default",
+				ID:          "85cc3048-abc3-43cc-89b3-377341426ac5",
+				Name:        "default",
+				Rules:       []osRules.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 := osGroups.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/rackspace/networking/v2/security/rules/delegate.go b/rackspace/networking/v2/security/rules/delegate.go
new file mode 100644
index 0000000..23b4b31
--- /dev/null
+++ b/rackspace/networking/v2/security/rules/delegate.go
@@ -0,0 +1,30 @@
+package rules
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 os.ListOpts) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// 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 os.CreateOpts) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(c, id)
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(c, id)
+}
diff --git a/rackspace/networking/v2/security/rules/delegate_test.go b/rackspace/networking/v2/security/rules/delegate_test.go
new file mode 100644
index 0000000..3563fbe
--- /dev/null
+++ b/rackspace/networking/v2/security/rules/delegate_test.go
@@ -0,0 +1,236 @@
+package rules
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	osRules "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"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/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(), osRules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := osRules.ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract secrules: %v", err)
+			return false, err
+		}
+
+		expected := []osRules.SecGroupRule{
+			osRules.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",
+			},
+			osRules.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 := osRules.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(), osRules.CreateOpts{Direction: "something"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), osRules.CreateOpts{Direction: osRules.DirIngress, EtherType: "something"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), osRules.CreateOpts{Direction: osRules.DirIngress, EtherType: osRules.Ether4})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = Create(fake.ServiceClient(), osRules.CreateOpts{Direction: osRules.DirIngress, EtherType: osRules.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/rackspace/networking/v2/subnets/delegate.go b/rackspace/networking/v2/subnets/delegate.go
new file mode 100644
index 0000000..a7fb7bb
--- /dev/null
+++ b/rackspace/networking/v2/subnets/delegate.go
@@ -0,0 +1,40 @@
+package subnets
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 a user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retrieves a specific subnet based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+	return os.Get(c, networkID)
+}
+
+// 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 os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnet using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the subnet associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+	return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/subnets/delegate_test.go b/rackspace/networking/v2/subnets/delegate_test.go
new file mode 100644
index 0000000..fafc6fb
--- /dev/null
+++ b/rackspace/networking/v2/subnets/delegate_test.go
@@ -0,0 +1,363 @@
+package subnets
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		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(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractSubnets(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []os.Subnet{
+			os.Subnet{
+				Name:           "private-subnet",
+				EnableDHCP:     true,
+				NetworkID:      "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+				TenantID:       "26a7980765d0414dbc1fc1f88cdb7e6e",
+				DNSNameservers: []string{},
+				AllocationPools: []os.AllocationPool{
+					os.AllocationPool{
+						Start: "10.0.0.2",
+						End:   "10.0.0.254",
+					},
+				},
+				HostRoutes: []os.HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "10.0.0.1",
+				CIDR:       "10.0.0.0/24",
+				ID:         "08eae331-0402-425a-923c-34f7cfe39c1b",
+			},
+			os.Subnet{
+				Name:           "my_subnet",
+				EnableDHCP:     true,
+				NetworkID:      "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+				TenantID:       "4fd44f30292945e481c7b8a0c8908869",
+				DNSNameservers: []string{},
+				AllocationPools: []os.AllocationPool{
+					os.AllocationPool{
+						Start: "192.0.0.2",
+						End:   "192.255.255.254",
+					},
+				},
+				HostRoutes: []os.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("/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, []os.AllocationPool{
+		os.AllocationPool{
+			Start: "192.0.0.2",
+			End:   "192.255.255.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []os.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("/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 := os.CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+		IPVersion: 4,
+		CIDR:      "192.168.199.0/24",
+		AllocationPools: []os.AllocationPool{
+			os.AllocationPool{
+				Start: "192.168.199.2",
+				End:   "192.168.199.254",
+			},
+		},
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []os.HostRoute{
+			os.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, []os.AllocationPool{
+		os.AllocationPool{
+			Start: "192.168.199.2",
+			End:   "192.168.199.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []os.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(), os.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+		th.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 := os.UpdateOpts{
+		Name:           "my_new_subnet",
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []os.HostRoute{
+			os.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("/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/rackspace/objectstorage/v1/accounts/delegate.go b/rackspace/objectstorage/v1/accounts/delegate.go
new file mode 100644
index 0000000..9473930
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/delegate.go
@@ -0,0 +1,39 @@
+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
+// ExtractHeader 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
+}
+
+// Update will update an account's metadata with the Metadata in the UpdateOptsBuilder.
+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..a1ea98b
--- /dev/null
+++ b/rackspace/objectstorage/v1/accounts/delegate_test.go
@@ -0,0 +1,32 @@
+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 TestUpdateAccounts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleUpdateAccountSuccessfully(t)
+
+	options := &UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+	res := Update(fake.ServiceClient(), options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestGetAccounts(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleGetAccountSuccessfully(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..0aeec15
--- /dev/null
+++ b/rackspace/objectstorage/v1/bulk/requests.go
@@ -0,0 +1,51 @@
+package bulk
+
+import (
+	"net/url"
+	"strings"
+
+	"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 := c.Request("DELETE", deleteURL(c), gophercloud.RequestOpts{
+		MoreHeaders:  map[string]string{"Content-Type": "text/plain"},
+		OkCodes:      []int{200},
+		JSONBody:     reqBody,
+		JSONResponse: &res.Body,
+	})
+	if resp != nil {
+		res.Header = resp.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..89adb83
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/delegate.go
@@ -0,0 +1,38 @@
+package cdncontainers
+
+import (
+	"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)
+}
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..6acebb0
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/requests.go
@@ -0,0 +1,161 @@
+package cdncontainers
+
+import (
+	"strconv"
+
+	"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 := c.Request("PUT", enableURL(c, containerName), gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{201, 202, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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 := c.Request("HEAD", getURL(c, containerName), gophercloud.RequestOpts{
+		OkCodes: []int{200, 204},
+	})
+	if resp != nil {
+		res.Header = resp.Header
+	}
+	res.Err = err
+	return res
+}
+
+// State is the state of an option. It is a pointer to a boolean to enable checking for
+// a zero-value of nil instead of false, which is a valid option.
+type State *bool
+
+var (
+	iTrue  = true
+	iFalse = false
+
+	// Enabled is used for a true value for options in request bodies.
+	Enabled State = &iTrue
+	// Disabled is used for a false value for options in request bodies.
+	Disabled State = &iFalse
+)
+
+// 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 {
+	// Whether or not to CDN-enable a container. Prefer using XCDNEnabled, which
+	// is of type *bool underneath.
+	// TODO v2.0: change type to Enabled/Disabled (*bool)
+	CDNEnabled bool `h:"X-Cdn-Enabled"`
+	// Whether or not to enable log retention. Prefer using XLogRetention, which
+	// is of type *bool underneath.
+	// TODO v2.0: change type to Enabled/Disabled (*bool)
+	LogRetention  bool `h:"X-Log-Retention"`
+	XCDNEnabled   *bool
+	XLogRetention *bool
+	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)
+	h["X-Log-Retention"] = strconv.FormatBool(opts.LogRetention)
+
+	if opts.XCDNEnabled != nil {
+		h["X-Cdn-Enabled"] = strconv.FormatBool(*opts.XCDNEnabled)
+	}
+
+	if opts.XLogRetention != nil {
+		h["X-Log-Retention"] = strconv.FormatBool(*opts.XLogRetention)
+	}
+
+	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 := c.Request("POST", updateURL(c, containerName), gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{202, 204},
+	})
+	if resp != nil {
+		res.Header = resp.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..cb0ad30
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/results.go
@@ -0,0 +1,149 @@
+package cdncontainers
+
+import (
+	"strings"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// EnableHeader represents the headers returned in the response from an Enable request.
+type EnableHeader struct {
+	CDNIosURI       string    `mapstructure:"X-Cdn-Ios-Uri"`
+	CDNSslURI       string    `mapstructure:"X-Cdn-Ssl-Uri"`
+	CDNStreamingURI string    `mapstructure:"X-Cdn-Streaming-Uri"`
+	CDNUri          string    `mapstructure:"X-Cdn-Uri"`
+	ContentLength   int       `mapstructure:"Content-Length"`
+	ContentType     string    `mapstructure:"Content-Type"`
+	Date            time.Time `mapstructure:"-"`
+	TransID         string    `mapstructure:"X-Trans-Id"`
+}
+
+// EnableResult represents the result of an Enable operation.
+type EnableResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return extract an EnableHeader from the response to an Enable
+// request. To obtain a map of headers, call the ExtractHeader method on the EnableResult.
+func (er EnableResult) Extract() (EnableHeader, error) {
+	var eh EnableHeader
+	if er.Err != nil {
+		return eh, er.Err
+	}
+
+	if err := gophercloud.DecodeHeader(er.Header, &eh); err != nil {
+		return eh, err
+	}
+
+	if date, ok := er.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, er.Header["Date"][0])
+		if err != nil {
+			return eh, err
+		}
+		eh.Date = t
+	}
+
+	return eh, nil
+}
+
+// GetHeader represents the headers returned in the response from a Get request.
+type GetHeader struct {
+	CDNEnabled      bool      `mapstructure:"X-Cdn-Enabled"`
+	CDNIosURI       string    `mapstructure:"X-Cdn-Ios-Uri"`
+	CDNSslURI       string    `mapstructure:"X-Cdn-Ssl-Uri"`
+	CDNStreamingURI string    `mapstructure:"X-Cdn-Streaming-Uri"`
+	CDNUri          string    `mapstructure:"X-Cdn-Uri"`
+	ContentLength   int       `mapstructure:"Content-Length"`
+	ContentType     string    `mapstructure:"Content-Type"`
+	Date            time.Time `mapstructure:"-"`
+	LogRetention    bool      `mapstructure:"X-Log-Retention"`
+	TransID         string    `mapstructure:"X-Trans-Id"`
+	TTL             int       `mapstructure:"X-Ttl"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Get. To obtain
+// a map of headers, call the ExtractHeader method on the GetResult.
+func (gr GetResult) Extract() (GetHeader, error) {
+	var gh GetHeader
+	if gr.Err != nil {
+		return gh, gr.Err
+	}
+
+	if err := gophercloud.DecodeHeader(gr.Header, &gh); err != nil {
+		return gh, err
+	}
+
+	if date, ok := gr.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, gr.Header["Date"][0])
+		if err != nil {
+			return gh, err
+		}
+		gh.Date = t
+	}
+
+	return gh, nil
+}
+
+// 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
+}
+
+// UpdateHeader represents the headers returned in the response from a Update request.
+type UpdateHeader struct {
+	CDNIosURI       string    `mapstructure:"X-Cdn-Ios-Uri"`
+	CDNSslURI       string    `mapstructure:"X-Cdn-Ssl-Uri"`
+	CDNStreamingURI string    `mapstructure:"X-Cdn-Streaming-Uri"`
+	CDNUri          string    `mapstructure:"X-Cdn-Uri"`
+	ContentLength   int       `mapstructure:"Content-Length"`
+	ContentType     string    `mapstructure:"Content-Type"`
+	Date            time.Time `mapstructure:"-"`
+	TransID         string    `mapstructure:"X-Trans-Id"`
+}
+
+// UpdateResult represents the result of an update operation. To extract the
+// the headers from the HTTP response, you can invoke the 'ExtractHeader'
+// method on the result struct.
+type UpdateResult struct {
+	gophercloud.HeaderResult
+}
+
+// Extract will return a struct of headers returned from a call to Update. To obtain
+// a map of headers, call the ExtractHeader method on the UpdateResult.
+func (ur UpdateResult) Extract() (UpdateHeader, error) {
+	var uh UpdateHeader
+	if ur.Err != nil {
+		return uh, ur.Err
+	}
+
+	if err := gophercloud.DecodeHeader(ur.Header, &uh); err != nil {
+		return uh, err
+	}
+
+	if date, ok := ur.Header["Date"]; ok && len(date) > 0 {
+		t, err := time.Parse(time.RFC1123, ur.Header["Date"][0])
+		if err != nil {
+			return uh, err
+		}
+		uh.Date = t
+	}
+
+	return uh, nil
+}
diff --git a/rackspace/objectstorage/v1/cdncontainers/urls.go b/rackspace/objectstorage/v1/cdncontainers/urls.go
new file mode 100644
index 0000000..541249a
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdncontainers/urls.go
@@ -0,0 +1,15 @@
+package cdncontainers
+
+import "github.com/rackspace/gophercloud"
+
+func enableURL(c *gophercloud.ServiceClient, containerName string) string {
+	return c.ServiceURL(containerName)
+}
+
+func getURL(c *gophercloud.ServiceClient, container string) string {
+	return c.ServiceURL(container)
+}
+
+func updateURL(c *gophercloud.ServiceClient, container string) string {
+	return getURL(c, container)
+}
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/cdnobjects/request.go b/rackspace/objectstorage/v1/cdnobjects/request.go
new file mode 100644
index 0000000..540e0cd
--- /dev/null
+++ b/rackspace/objectstorage/v1/cdnobjects/request.go
@@ -0,0 +1,15 @@
+package cdnobjects
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/objectstorage/v1/cdncontainers"
+)
+
+// CDNURL returns the unique CDN URI for the given container and object.
+func CDNURL(c *gophercloud.ServiceClient, containerName, objectName string) (string, error) {
+	h, err := cdncontainers.Get(c, containerName).Extract()
+	if err != nil {
+		return "", err
+	}
+	return h.CDNUri + "/" + objectName, nil
+}
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..94c820b
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/delegate.go
@@ -0,0 +1,94 @@
+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.ReadSeeker, 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)
+}
+
+func CreateTempURL(c *gophercloud.ServiceClient, containerName, objectName string, opts os.CreateTempURLOpts) (string, error) {
+	return os.CreateTempURL(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..21cd417
--- /dev/null
+++ b/rackspace/objectstorage/v1/objects/delegate_test.go
@@ -0,0 +1,127 @@
+package objects
+
+import (
+	"strings"
+	"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()
+	
+        content := "Did gyre and gimble in the wabe"
+        os.HandleCreateTextObjectSuccessfully(t, content)
+
+	options := &os.CreateOpts{ContentType: "text/plain"}
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateObjectWithoutContentType(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+        content := "The sky was the color of television, tuned to a dead channel."
+	os.HandleCreateTypelessObjectSuccessfully(t, content)
+
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", strings.NewReader(content), &os.CreateOpts{})
+	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/rackspace/orchestration/v1/buildinfo/delegate.go b/rackspace/orchestration/v1/buildinfo/delegate.go
new file mode 100644
index 0000000..c834e5c
--- /dev/null
+++ b/rackspace/orchestration/v1/buildinfo/delegate.go
@@ -0,0 +1,11 @@
+package buildinfo
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo"
+)
+
+// Get retreives build info data for the Heat deployment.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+	return os.Get(c)
+}
diff --git a/rackspace/orchestration/v1/buildinfo/delegate_test.go b/rackspace/orchestration/v1/buildinfo/delegate_test.go
new file mode 100644
index 0000000..b25a690
--- /dev/null
+++ b/rackspace/orchestration/v1/buildinfo/delegate_test.go
@@ -0,0 +1,21 @@
+package buildinfo
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t, os.GetOutput)
+
+	actual, err := Get(fake.ServiceClient()).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/buildinfo/doc.go b/rackspace/orchestration/v1/buildinfo/doc.go
new file mode 100644
index 0000000..183e8df
--- /dev/null
+++ b/rackspace/orchestration/v1/buildinfo/doc.go
@@ -0,0 +1,2 @@
+// Package buildinfo provides build information about heat deployments.
+package buildinfo
diff --git a/rackspace/orchestration/v1/stackevents/delegate.go b/rackspace/orchestration/v1/stackevents/delegate.go
new file mode 100644
index 0000000..08675de
--- /dev/null
+++ b/rackspace/orchestration/v1/stackevents/delegate.go
@@ -0,0 +1,27 @@
+package stackevents
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retreives stack events for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult {
+	return os.Find(c, stackName)
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, stackName, stackID, opts)
+}
+
+// ListResourceEvents makes a request against the API to list resources for the given stack.
+func ListResourceEvents(c *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts os.ListResourceEventsOptsBuilder) pagination.Pager {
+	return os.ListResourceEvents(c, stackName, stackID, resourceName, opts)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) os.GetResult {
+	return os.Get(c, stackName, stackID, resourceName, eventID)
+}
diff --git a/rackspace/orchestration/v1/stackevents/delegate_test.go b/rackspace/orchestration/v1/stackevents/delegate_test.go
new file mode 100644
index 0000000..e1c0bc8
--- /dev/null
+++ b/rackspace/orchestration/v1/stackevents/delegate_test.go
@@ -0,0 +1,72 @@
+package stackevents
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindEvents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleFindSuccessfully(t, os.FindOutput)
+
+	actual, err := Find(fake.ServiceClient(), "postman_stack").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.FindExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListSuccessfully(t, os.ListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractEvents(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestListResourceEvents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListResourceEventsSuccessfully(t, os.ListResourceEventsOutput)
+
+	count := 0
+	err := ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractEvents(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListResourceEventsExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetEvent(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t, os.GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/stackevents/doc.go b/rackspace/orchestration/v1/stackevents/doc.go
new file mode 100644
index 0000000..dfd6ef6
--- /dev/null
+++ b/rackspace/orchestration/v1/stackevents/doc.go
@@ -0,0 +1,3 @@
+// Package stackevents provides operations for finding, listing, and retrieving
+// stack events.
+package stackevents
diff --git a/rackspace/orchestration/v1/stackresources/delegate.go b/rackspace/orchestration/v1/stackresources/delegate.go
new file mode 100644
index 0000000..cb7be28
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/delegate.go
@@ -0,0 +1,42 @@
+package stackresources
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retreives stack resources for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult {
+	return os.Find(c, stackName)
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, stackName, stackID, opts)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.GetResult {
+	return os.Get(c, stackName, stackID, resourceName)
+}
+
+// Metadata retreives the metadata for the given stack resource.
+func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.MetadataResult {
+	return os.Metadata(c, stackName, stackID, resourceName)
+}
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(c *gophercloud.ServiceClient) pagination.Pager {
+	return os.ListTypes(c)
+}
+
+// Schema retreives the schema for the given resource type.
+func Schema(c *gophercloud.ServiceClient, resourceType string) os.SchemaResult {
+	return os.Schema(c, resourceType)
+}
+
+// Template retreives the template representation for the given resource type.
+func Template(c *gophercloud.ServiceClient, resourceType string) os.TemplateResult {
+	return os.Template(c, resourceType)
+}
diff --git a/rackspace/orchestration/v1/stackresources/delegate_test.go b/rackspace/orchestration/v1/stackresources/delegate_test.go
new file mode 100644
index 0000000..116e44c
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/delegate_test.go
@@ -0,0 +1,108 @@
+package stackresources
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleFindSuccessfully(t, os.FindOutput)
+
+	actual, err := Find(fake.ServiceClient(), "hello_world").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.FindExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListSuccessfully(t, os.ListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractResources(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetResource(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t, os.GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResourceMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleMetadataSuccessfully(t, os.MetadataOutput)
+
+	actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.MetadataExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResourceTypes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListTypesSuccessfully(t, os.ListTypesOutput)
+
+	count := 0
+	err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractResourceTypes(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListTypesExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGetResourceSchema(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSchemaSuccessfully(t, os.GetSchemaOutput)
+
+	actual, err := Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetSchemaExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestGetResourceTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetTemplateSuccessfully(t, os.GetTemplateOutput)
+
+	actual, err := Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetTemplateExpected
+	th.AssertDeepEquals(t, expected, string(actual))
+}
diff --git a/rackspace/orchestration/v1/stackresources/doc.go b/rackspace/orchestration/v1/stackresources/doc.go
new file mode 100644
index 0000000..e4f8b08
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/doc.go
@@ -0,0 +1,5 @@
+// Package stackresources provides operations for working with stack resources.
+// A resource is a template artifact that represents some component of your
+// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load
+// balancer, some configuration management system, and so forth).
+package stackresources
diff --git a/rackspace/orchestration/v1/stacks/delegate.go b/rackspace/orchestration/v1/stacks/delegate.go
new file mode 100644
index 0000000..f7e387f
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/delegate.go
@@ -0,0 +1,49 @@
+package stacks
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Create accepts an os.CreateOpts struct and creates a new stack using the values
+// provided.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Adopt accepts an os.AdoptOpts struct and creates a new stack from existing stack
+// resources using the values provided.
+func Adopt(c *gophercloud.ServiceClient, opts os.AdoptOptsBuilder) os.AdoptResult {
+	return os.Adopt(c, opts)
+}
+
+// List accepts an os.ListOpts struct and lists stacks based on the options provided.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retreives a stack based on the stack name and stack ID.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) os.GetResult {
+	return os.Get(c, stackName, stackID)
+}
+
+// Update accepts an os.UpdateOpts struct and updates a stack based on the options provided.
+func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, stackName, stackID, opts)
+}
+
+// Delete deletes a stack based on the stack name and stack ID provided.
+func Delete(c *gophercloud.ServiceClient, stackName, stackID string) os.DeleteResult {
+	return os.Delete(c, stackName, stackID)
+}
+
+// Preview provides a preview of a stack based on the options provided.
+func Preview(c *gophercloud.ServiceClient, opts os.PreviewOptsBuilder) os.PreviewResult {
+	return os.Preview(c, opts)
+}
+
+// Abandon abandons a stack, keeping the resources available.
+func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) os.AbandonResult {
+	return os.Abandon(c, stackName, stackID)
+}
diff --git a/rackspace/orchestration/v1/stacks/delegate_test.go b/rackspace/orchestration/v1/stacks/delegate_test.go
new file mode 100644
index 0000000..553ae94
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/delegate_test.go
@@ -0,0 +1,870 @@
+package stacks
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateSuccessfully(t, CreateOutput)
+
+	createOpts := os.CreateOpts{
+		Name:    "stackcreated",
+		Timeout: 60,
+		Template: `{
+      "outputs": {
+        "db_host": {
+          "value": {
+            "get_attr": [
+            "db",
+            "hostname"
+            ]
+          }
+        }
+      },
+      "heat_template_version": "2014-10-16",
+      "description": "HEAT template for creating a Cloud Database.\n",
+      "parameters": {
+        "db_name": {
+          "default": "wordpress",
+          "type": "string",
+          "description": "the name for the database",
+          "constraints": [
+          {
+            "length": {
+              "max": 64,
+              "min": 1
+            },
+            "description": "must be between 1 and 64 characters"
+          },
+          {
+            "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+            "description": "must begin with a letter and contain only alphanumeric characters."
+          }
+          ]
+        },
+        "db_instance_name": {
+          "default": "Cloud_DB",
+          "type": "string",
+          "description": "the database instance name"
+        },
+        "db_username": {
+          "default": "admin",
+          "hidden": true,
+          "type": "string",
+          "description": "database admin account username",
+          "constraints": [
+          {
+            "length": {
+              "max": 16,
+              "min": 1
+                },
+              "description": "must be between 1 and 16 characters"
+            },
+            {
+              "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+              "description": "must begin with a letter and contain only alphanumeric characters."
+            }
+          ]
+          },
+          "db_volume_size": {
+            "default": 30,
+            "type": "number",
+            "description": "database volume size (in GB)",
+            "constraints": [
+            {
+              "range": {
+                "max": 1024,
+                "min": 1
+              },
+              "description": "must be between 1 and 1024 GB"
+            }
+            ]
+          },
+          "db_flavor": {
+            "default": "1GB Instance",
+            "type": "string",
+            "description": "database instance size",
+            "constraints": [
+            {
+              "description": "must be a valid cloud database flavor",
+              "allowed_values": [
+              "1GB Instance",
+              "2GB Instance",
+              "4GB Instance",
+              "8GB Instance",
+              "16GB Instance"
+              ]
+            }
+            ]
+          },
+        "db_password": {
+          "default": "admin",
+          "hidden": true,
+          "type": "string",
+          "description": "database admin account password",
+          "constraints": [
+          {
+            "length": {
+              "max": 41,
+              "min": 1
+            },
+            "description": "must be between 1 and 14 characters"
+          },
+          {
+            "allowed_pattern": "[a-zA-Z0-9]*",
+            "description": "must contain only alphanumeric characters."
+          }
+          ]
+        }
+      },
+      "resources": {
+        "db": {
+          "type": "OS::Trove::Instance",
+          "properties": {
+            "flavor": {
+              "get_param": "db_flavor"
+            },
+            "size": {
+              "get_param": "db_volume_size"
+            },
+            "users": [
+            {
+              "password": {
+                "get_param": "db_password"
+              },
+              "name": {
+                "get_param": "db_username"
+              },
+              "databases": [
+              {
+                "get_param": "db_name"
+              }
+              ]
+            }
+            ],
+            "name": {
+              "get_param": "db_instance_name"
+            },
+            "databases": [
+            {
+              "name": {
+                "get_param": "db_name"
+              }
+            }
+            ]
+          }
+        }
+      }
+    }`,
+		DisableRollback: os.Disable,
+	}
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateSuccessfully(t, CreateOutput)
+
+	createOpts := os.CreateOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    new(os.Template),
+		DisableRollback: os.Disable,
+	}
+	createOpts.TemplateOpts.Bin = []byte(`{
+		    "outputs": {
+		        "db_host": {
+		            "value": {
+		                "get_attr": [
+		                    "db",
+		                    "hostname"
+		                ]
+		            }
+		        }
+		    },
+		    "heat_template_version": "2014-10-16",
+		    "description": "HEAT template for creating a Cloud Database.\n",
+		    "parameters": {
+		        "db_name": {
+		            "default": "wordpress",
+		            "type": "string",
+		            "description": "the name for the database",
+		            "constraints": [
+		                {
+		                    "length": {
+		                        "max": 64,
+		                        "min": 1
+		                    },
+		                    "description": "must be between 1 and 64 characters"
+		                },
+		                {
+		                    "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+		                    "description": "must begin with a letter and contain only alphanumeric characters."
+		                }
+		            ]
+		        },
+		        "db_instance_name": {
+		            "default": "Cloud_DB",
+		            "type": "string",
+		            "description": "the database instance name"
+		        },
+		        "db_username": {
+		            "default": "admin",
+		            "hidden": true,
+		            "type": "string",
+		            "description": "database admin account username",
+		            "constraints": [
+		                {
+		                    "length": {
+		                        "max": 16,
+		                        "min": 1
+		                    },
+		                    "description": "must be between 1 and 16 characters"
+		                },
+		                {
+		                    "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+		                    "description": "must begin with a letter and contain only alphanumeric characters."
+		                }
+		            ]
+		        },
+		        "db_volume_size": {
+		            "default": 30,
+		            "type": "number",
+		            "description": "database volume size (in GB)",
+		            "constraints": [
+		                {
+		                    "range": {
+		                        "max": 1024,
+		                        "min": 1
+		                    },
+		                    "description": "must be between 1 and 1024 GB"
+		                }
+		            ]
+		        },
+		        "db_flavor": {
+		            "default": "1GB Instance",
+		            "type": "string",
+		            "description": "database instance size",
+		            "constraints": [
+		                {
+		                    "description": "must be a valid cloud database flavor",
+		                    "allowed_values": [
+		                        "1GB Instance",
+		                        "2GB Instance",
+		                        "4GB Instance",
+		                        "8GB Instance",
+		                        "16GB Instance"
+		                    ]
+		                }
+		            ]
+		        },
+		        "db_password": {
+		            "default": "admin",
+		            "hidden": true,
+		            "type": "string",
+		            "description": "database admin account password",
+		            "constraints": [
+		                {
+		                    "length": {
+		                        "max": 41,
+		                        "min": 1
+		                    },
+		                    "description": "must be between 1 and 14 characters"
+		                },
+		                {
+		                    "allowed_pattern": "[a-zA-Z0-9]*",
+		                    "description": "must contain only alphanumeric characters."
+		                }
+		            ]
+		        }
+		    },
+		    "resources": {
+		        "db": {
+		            "type": "OS::Trove::Instance",
+		            "properties": {
+		                "flavor": {
+		                    "get_param": "db_flavor"
+		                },
+		                "size": {
+		                    "get_param": "db_volume_size"
+		                },
+		                "users": [
+		                    {
+		                        "password": {
+		                            "get_param": "db_password"
+		                        },
+		                        "name": {
+		                            "get_param": "db_username"
+		                        },
+		                        "databases": [
+		                            {
+		                                "get_param": "db_name"
+		                            }
+		                        ]
+		                    }
+		                ],
+		                "name": {
+		                    "get_param": "db_instance_name"
+		                },
+		                "databases": [
+		                    {
+		                        "name": {
+		                            "get_param": "db_name"
+		                        }
+		                    }
+		                ]
+		            }
+		        }
+		    }
+		}`)
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAdoptStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateSuccessfully(t, CreateOutput)
+
+	adoptOpts := os.AdoptOpts{
+		AdoptStackData: `{\"environment\":{\"parameters\":{}},    \"status\":\"COMPLETE\",\"name\": \"trovestack\",\n  \"template\": {\n    \"outputs\": {\n      \"db_host\": {\n        \"value\": {\n          \"get_attr\": [\n            \"db\",\n            \"hostname\"\n          ]\n        }\n      }\n    },\n    \"heat_template_version\": \"2014-10-16\",\n    \"description\": \"HEAT template for creating a Cloud Database.\\n\",\n    \"parameters\": {\n      \"db_instance_name\": {\n        \"default\": \"Cloud_DB\",\n        \"type\": \"string\",\n        \"description\": \"the database instance name\"\n      },\n      \"db_flavor\": {\n        \"default\": \"1GB Instance\",\n        \"type\": \"string\",\n        \"description\": \"database instance size\",\n        \"constraints\": [\n          {\n            \"description\": \"must be a valid cloud database flavor\",\n            \"allowed_values\": [\n              \"1GB Instance\",\n              \"2GB Instance\",\n              \"4GB Instance\",\n              \"8GB Instance\",\n              \"16GB Instance\"\n            ]\n          }\n        ]\n      },\n      \"db_password\": {\n        \"default\": \"admin\",\n        \"hidden\": true,\n        \"type\": \"string\",\n        \"description\": \"database admin account password\",\n        \"constraints\": [\n          {\n            \"length\": {\n              \"max\": 41,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 14 characters\"\n          },\n          {\n            \"allowed_pattern\": \"[a-zA-Z0-9]*\",\n            \"description\": \"must contain only alphanumeric characters.\"\n          }\n        ]\n      },\n      \"db_name\": {\n        \"default\": \"wordpress\",\n        \"type\": \"string\",\n        \"description\": \"the name for the database\",\n        \"constraints\": [\n          {\n            \"length\": {\n              \"max\": 64,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 64 characters\"\n          },\n          {\n            \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n            \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n          }\n        ]\n      },\n      \"db_username\": {\n        \"default\": \"admin\",\n        \"hidden\": true,\n        \"type\": \"string\",\n        \"description\": \"database admin account username\",\n        \"constraints\": [\n          {\n            \"length\": {\n              \"max\": 16,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 16 characters\"\n          },\n          {\n            \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n            \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n          }\n        ]\n      },\n      \"db_volume_size\": {\n        \"default\": 30,\n        \"type\": \"number\",\n        \"description\": \"database volume size (in GB)\",\n        \"constraints\": [\n          {\n            \"range\": {\n              \"max\": 1024,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 1024 GB\"\n          }\n        ]\n      }\n    },\n    \"resources\": {\n      \"db\": {\n        \"type\": \"OS::Trove::Instance\",\n        \"properties\": {\n          \"flavor\": {\n            \"get_param\": \"db_flavor\"\n          },\n          \"databases\": [\n            {\n              \"name\": {\n                \"get_param\": \"db_name\"\n              }\n            }\n          ],\n          \"users\": [\n            {\n              \"password\": {\n                \"get_param\": \"db_password\"\n              },\n              \"name\": {\n                \"get_param\": \"db_username\"\n              },\n              \"databases\": [\n                {\n                  \"get_param\": \"db_name\"\n                }\n              ]\n            }\n          ],\n          \"name\": {\n            \"get_param\": \"db_instance_name\"\n          },\n          \"size\": {\n            \"get_param\": \"db_volume_size\"\n          }\n        }\n      }\n    }\n  },\n  \"action\": \"CREATE\",\n  \"id\": \"exxxxd-7xx5-4xxb-bxx2-cxxxxxx5\",\n  \"resources\": {\n    \"db\": {\n      \"status\": \"COMPLETE\",\n      \"name\": \"db\",\n      \"resource_data\": {},\n      \"resource_id\": \"exxxx2-9xx0-4xxxb-bxx2-dxxxxxx4\",\n      \"action\": \"CREATE\",\n      \"type\": \"OS::Trove::Instance\",\n      \"metadata\": {}\n    }\n  }\n},`,
+		Name:           "stackadopted",
+		Timeout:        60,
+		Template: `{
+      "outputs": {
+        "db_host": {
+          "value": {
+            "get_attr": [
+            "db",
+            "hostname"
+            ]
+          }
+        }
+      },
+      "heat_template_version": "2014-10-16",
+      "description": "HEAT template for creating a Cloud Database.\n",
+      "parameters": {
+        "db_name": {
+          "default": "wordpress",
+          "type": "string",
+          "description": "the name for the database",
+          "constraints": [
+          {
+            "length": {
+              "max": 64,
+              "min": 1
+            },
+            "description": "must be between 1 and 64 characters"
+          },
+          {
+            "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+            "description": "must begin with a letter and contain only alphanumeric characters."
+          }
+          ]
+        },
+        "db_instance_name": {
+          "default": "Cloud_DB",
+          "type": "string",
+          "description": "the database instance name"
+        },
+        "db_username": {
+          "default": "admin",
+          "hidden": true,
+          "type": "string",
+          "description": "database admin account username",
+          "constraints": [
+          {
+            "length": {
+              "max": 16,
+              "min": 1
+            },
+            "description": "must be between 1 and 16 characters"
+          },
+          {
+            "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+            "description": "must begin with a letter and contain only alphanumeric characters."
+          }
+          ]
+        },
+        "db_volume_size": {
+          "default": 30,
+          "type": "number",
+          "description": "database volume size (in GB)",
+          "constraints": [
+          {
+            "range": {
+              "max": 1024,
+              "min": 1
+            },
+            "description": "must be between 1 and 1024 GB"
+          }
+          ]
+        },
+        "db_flavor": {
+          "default": "1GB Instance",
+          "type": "string",
+          "description": "database instance size",
+          "constraints": [
+          {
+            "description": "must be a valid cloud database flavor",
+            "allowed_values": [
+            "1GB Instance",
+            "2GB Instance",
+            "4GB Instance",
+            "8GB Instance",
+            "16GB Instance"
+            ]
+          }
+          ]
+        },
+        "db_password": {
+          "default": "admin",
+          "hidden": true,
+          "type": "string",
+          "description": "database admin account password",
+          "constraints": [
+          {
+            "length": {
+              "max": 41,
+              "min": 1
+            },
+            "description": "must be between 1 and 14 characters"
+          },
+          {
+            "allowed_pattern": "[a-zA-Z0-9]*",
+            "description": "must contain only alphanumeric characters."
+          }
+          ]
+        }
+      },
+      "resources": {
+        "db": {
+          "type": "OS::Trove::Instance",
+          "properties": {
+            "flavor": {
+              "get_param": "db_flavor"
+            },
+            "size": {
+              "get_param": "db_volume_size"
+            },
+            "users": [
+            {
+              "password": {
+                "get_param": "db_password"
+              },
+              "name": {
+                "get_param": "db_username"
+              },
+              "databases": [
+              {
+                "get_param": "db_name"
+              }
+              ]
+            }
+            ],
+            "name": {
+              "get_param": "db_instance_name"
+            },
+            "databases": [
+            {
+              "name": {
+                "get_param": "db_name"
+              }
+            }
+            ]
+          }
+        }
+      }
+    }`,
+		DisableRollback: os.Disable,
+	}
+	actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAdoptStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateSuccessfully(t, CreateOutput)
+	template := new(os.Template)
+	template.Bin = []byte(`{
+  "outputs": {
+	"db_host": {
+	  "value": {
+		"get_attr": [
+		"db",
+		"hostname"
+		]
+	  }
+	}
+  },
+  "heat_template_version": "2014-10-16",
+  "description": "HEAT template for creating a Cloud Database.\n",
+  "parameters": {
+	"db_name": {
+	  "default": "wordpress",
+	  "type": "string",
+	  "description": "the name for the database",
+	  "constraints": [
+	  {
+		"length": {
+		  "max": 64,
+		  "min": 1
+		},
+		"description": "must be between 1 and 64 characters"
+	  },
+	  {
+		"allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+		"description": "must begin with a letter and contain only alphanumeric characters."
+	  }
+	  ]
+	},
+	"db_instance_name": {
+	  "default": "Cloud_DB",
+	  "type": "string",
+	  "description": "the database instance name"
+	},
+	"db_username": {
+	  "default": "admin",
+	  "hidden": true,
+	  "type": "string",
+	  "description": "database admin account username",
+	  "constraints": [
+	  {
+		"length": {
+		  "max": 16,
+		  "min": 1
+		},
+		"description": "must be between 1 and 16 characters"
+	  },
+	  {
+		"allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+		"description": "must begin with a letter and contain only alphanumeric characters."
+	  }
+	  ]
+	},
+	"db_volume_size": {
+	  "default": 30,
+	  "type": "number",
+	  "description": "database volume size (in GB)",
+	  "constraints": [
+	  {
+		"range": {
+		  "max": 1024,
+		  "min": 1
+		},
+		"description": "must be between 1 and 1024 GB"
+	  }
+	  ]
+	},
+	"db_flavor": {
+	  "default": "1GB Instance",
+	  "type": "string",
+	  "description": "database instance size",
+	  "constraints": [
+	  {
+		"description": "must be a valid cloud database flavor",
+		"allowed_values": [
+		"1GB Instance",
+		"2GB Instance",
+		"4GB Instance",
+		"8GB Instance",
+		"16GB Instance"
+		]
+	  }
+	  ]
+	},
+	"db_password": {
+	  "default": "admin",
+	  "hidden": true,
+	  "type": "string",
+	  "description": "database admin account password",
+	  "constraints": [
+	  {
+		"length": {
+		  "max": 41,
+		  "min": 1
+		},
+		"description": "must be between 1 and 14 characters"
+	  },
+	  {
+		"allowed_pattern": "[a-zA-Z0-9]*",
+		"description": "must contain only alphanumeric characters."
+	  }
+	  ]
+	}
+  },
+  "resources": {
+	"db": {
+	  "type": "OS::Trove::Instance",
+	  "properties": {
+		"flavor": {
+		  "get_param": "db_flavor"
+		},
+		"size": {
+		  "get_param": "db_volume_size"
+		},
+		"users": [
+		{
+		  "password": {
+			"get_param": "db_password"
+		  },
+		  "name": {
+			"get_param": "db_username"
+		  },
+		  "databases": [
+		  {
+			"get_param": "db_name"
+		  }
+		  ]
+		}
+		],
+		"name": {
+		  "get_param": "db_instance_name"
+		},
+		"databases": [
+		{
+		  "name": {
+			"get_param": "db_name"
+		  }
+		}
+		]
+	  }
+	}
+  }
+}`)
+
+	adoptOpts := os.AdoptOpts{
+		AdoptStackData:  `{\"environment\":{\"parameters\":{}},    \"status\":\"COMPLETE\",\"name\": \"trovestack\",\n  \"template\": {\n    \"outputs\": {\n      \"db_host\": {\n        \"value\": {\n          \"get_attr\": [\n            \"db\",\n            \"hostname\"\n          ]\n        }\n      }\n    },\n    \"heat_template_version\": \"2014-10-16\",\n    \"description\": \"HEAT template for creating a Cloud Database.\\n\",\n    \"parameters\": {\n      \"db_instance_name\": {\n        \"default\": \"Cloud_DB\",\n        \"type\": \"string\",\n        \"description\": \"the database instance name\"\n      },\n      \"db_flavor\": {\n        \"default\": \"1GB Instance\",\n        \"type\": \"string\",\n        \"description\": \"database instance size\",\n        \"constraints\": [\n          {\n            \"description\": \"must be a valid cloud database flavor\",\n            \"allowed_values\": [\n              \"1GB Instance\",\n              \"2GB Instance\",\n              \"4GB Instance\",\n              \"8GB Instance\",\n              \"16GB Instance\"\n            ]\n          }\n        ]\n      },\n      \"db_password\": {\n        \"default\": \"admin\",\n        \"hidden\": true,\n        \"type\": \"string\",\n        \"description\": \"database admin account password\",\n        \"constraints\": [\n          {\n            \"length\": {\n              \"max\": 41,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 14 characters\"\n          },\n          {\n            \"allowed_pattern\": \"[a-zA-Z0-9]*\",\n            \"description\": \"must contain only alphanumeric characters.\"\n          }\n        ]\n      },\n      \"db_name\": {\n        \"default\": \"wordpress\",\n        \"type\": \"string\",\n        \"description\": \"the name for the database\",\n        \"constraints\": [\n          {\n            \"length\": {\n              \"max\": 64,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 64 characters\"\n          },\n          {\n            \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n            \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n          }\n        ]\n      },\n      \"db_username\": {\n        \"default\": \"admin\",\n        \"hidden\": true,\n        \"type\": \"string\",\n        \"description\": \"database admin account username\",\n        \"constraints\": [\n          {\n            \"length\": {\n              \"max\": 16,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 16 characters\"\n          },\n          {\n            \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n            \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n          }\n        ]\n      },\n      \"db_volume_size\": {\n        \"default\": 30,\n        \"type\": \"number\",\n        \"description\": \"database volume size (in GB)\",\n        \"constraints\": [\n          {\n            \"range\": {\n              \"max\": 1024,\n              \"min\": 1\n            },\n            \"description\": \"must be between 1 and 1024 GB\"\n          }\n        ]\n      }\n    },\n    \"resources\": {\n      \"db\": {\n        \"type\": \"OS::Trove::Instance\",\n        \"properties\": {\n          \"flavor\": {\n            \"get_param\": \"db_flavor\"\n          },\n          \"databases\": [\n            {\n              \"name\": {\n                \"get_param\": \"db_name\"\n              }\n            }\n          ],\n          \"users\": [\n            {\n              \"password\": {\n                \"get_param\": \"db_password\"\n              },\n              \"name\": {\n                \"get_param\": \"db_username\"\n              },\n              \"databases\": [\n                {\n                  \"get_param\": \"db_name\"\n                }\n              ]\n            }\n          ],\n          \"name\": {\n            \"get_param\": \"db_instance_name\"\n          },\n          \"size\": {\n            \"get_param\": \"db_volume_size\"\n          }\n        }\n      }\n    }\n  },\n  \"action\": \"CREATE\",\n  \"id\": \"exxxxd-7xx5-4xxb-bxx2-cxxxxxx5\",\n  \"resources\": {\n    \"db\": {\n      \"status\": \"COMPLETE\",\n      \"name\": \"db\",\n      \"resource_data\": {},\n      \"resource_id\": \"exxxx2-9xx0-4xxxb-bxx2-dxxxxxx4\",\n      \"action\": \"CREATE\",\n      \"type\": \"OS::Trove::Instance\",\n      \"metadata\": {}\n    }\n  }\n},`,
+		Name:            "stackadopted",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: os.Disable,
+	}
+	actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := CreateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleListSuccessfully(t, os.FullListOutput)
+
+	count := 0
+	err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractStacks(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, os.ListExpected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestUpdateStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateSuccessfully(t)
+
+	updateOpts := os.UpdateOpts{
+		Template: `
+    {
+      "heat_template_version": "2013-05-23",
+      "description": "Simple template to test heat commands",
+      "parameters": {
+        "flavor": {
+          "default": "m1.tiny",
+          "type": "string"
+        }
+      },
+      "resources": {
+        "hello_world": {
+          "type":"OS::Nova::Server",
+          "properties": {
+            "key_name": "heat_key",
+            "flavor": {
+              "get_param": "flavor"
+            },
+            "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+            "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+          }
+        }
+      }
+    }`,
+	}
+	err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdateStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleUpdateSuccessfully(t)
+
+	updateOpts := os.UpdateOpts{
+		TemplateOpts: new(os.Template),
+	}
+	updateOpts.TemplateOpts.Bin = []byte(`
+		{
+			"stack_name": "postman_stack",
+			"template": {
+				"heat_template_version": "2013-05-23",
+				"description": "Simple template to test heat commands",
+				"parameters": {
+					"flavor": {
+						"default": "m1.tiny",
+						"type": "string"
+					}
+				},
+				"resources": {
+					"hello_world": {
+						"type": "OS::Nova::Server",
+						"properties": {
+							"key_name": "heat_key",
+							"flavor": {
+								"get_param": "flavor"
+							},
+							"image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+							"user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+						}
+					}
+				}
+			}
+		}`)
+	err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleDeleteSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestPreviewStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandlePreviewSuccessfully(t, os.GetOutput)
+
+	previewOpts := os.PreviewOpts{
+		Name:    "stackcreated",
+		Timeout: 60,
+		Template: `
+    {
+      "stack_name": "postman_stack",
+      "template": {
+        "heat_template_version": "2013-05-23",
+        "description": "Simple template to test heat commands",
+        "parameters": {
+          "flavor": {
+            "default": "m1.tiny",
+            "type": "string"
+          }
+        },
+        "resources": {
+          "hello_world": {
+            "type":"OS::Nova::Server",
+            "properties": {
+              "key_name": "heat_key",
+              "flavor": {
+                "get_param": "flavor"
+              },
+              "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+              "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+            }
+          }
+        }
+      }
+    }`,
+		DisableRollback: os.Disable,
+	}
+	actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.PreviewExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestPreviewStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandlePreviewSuccessfully(t, os.GetOutput)
+
+	previewOpts := os.PreviewOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    new(os.Template),
+		DisableRollback: os.Disable,
+	}
+	previewOpts.TemplateOpts.Bin = []byte(`
+		{
+		    "stack_name": "postman_stack",
+		    "template": {
+		        "heat_template_version": "2013-05-23",
+		        "description": "Simple template to test heat commands",
+		        "parameters": {
+		            "flavor": {
+		                "default": "m1.tiny",
+		                "type": "string"
+		            }
+		        },
+		        "resources": {
+		            "hello_world": {
+		                "type": "OS::Nova::Server",
+		                "properties": {
+		                    "key_name": "heat_key",
+		                    "flavor": {
+		                        "get_param": "flavor"
+		                    },
+		                    "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+		                    "user_data": "#!/bin/bash -xv\necho \"hello world\" &gt; /root/hello-world.txt\n"
+		                }
+		            }
+		        }
+		    }
+		}`)
+	actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.PreviewExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAbandonStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleAbandonSuccessfully(t, os.AbandonOutput)
+
+	actual, err := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c8").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.AbandonExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/stacks/doc.go b/rackspace/orchestration/v1/stacks/doc.go
new file mode 100644
index 0000000..19231b5
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/doc.go
@@ -0,0 +1,8 @@
+// Package stacks provides operation for working with Heat stacks. A stack is a
+// group of resources (servers, load balancers, databases, and so forth)
+// combined to fulfill a useful purpose. Based on a template, Heat orchestration
+// engine creates an instantiated set of resources (a stack) to run the
+// application framework or component specified (in the template). A stack is a
+// running instance of a template. The result of creating a stack is a deployment
+// of the application framework or component.
+package stacks
diff --git a/rackspace/orchestration/v1/stacks/fixtures.go b/rackspace/orchestration/v1/stacks/fixtures.go
new file mode 100644
index 0000000..c9afeb1
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/fixtures.go
@@ -0,0 +1,32 @@
+package stacks
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+)
+
+// CreateExpected represents the expected object from a Create request.
+var CreateExpected = &os.CreatedStack{
+	ID: "b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+	Links: []gophercloud.Link{
+		gophercloud.Link{
+			Href: "https://ord.orchestration.api.rackspacecloud.com/v1/864477/stacks/stackcreated/b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+			Rel:  "self",
+		},
+	},
+}
+
+// CreateOutput represents the response body from a Create request.
+const CreateOutput = `
+{
+  "stack": {
+    "id": "b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+    "links": [
+    {
+      "href": "https://ord.orchestration.api.rackspacecloud.com/v1/864477/stacks/stackcreated/b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+      "rel": "self"
+    }
+    ]
+  }
+}
+`
diff --git a/rackspace/orchestration/v1/stacktemplates/delegate.go b/rackspace/orchestration/v1/stacktemplates/delegate.go
new file mode 100644
index 0000000..3b5d46e
--- /dev/null
+++ b/rackspace/orchestration/v1/stacktemplates/delegate.go
@@ -0,0 +1,16 @@
+package stacktemplates
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+)
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) os.GetResult {
+	return os.Get(c, stackName, stackID)
+}
+
+// Validate validates the given stack template.
+func Validate(c *gophercloud.ServiceClient, opts os.ValidateOptsBuilder) os.ValidateResult {
+	return os.Validate(c, opts)
+}
diff --git a/rackspace/orchestration/v1/stacktemplates/delegate_test.go b/rackspace/orchestration/v1/stacktemplates/delegate_test.go
new file mode 100644
index 0000000..d4d0f8f
--- /dev/null
+++ b/rackspace/orchestration/v1/stacktemplates/delegate_test.go
@@ -0,0 +1,47 @@
+package stacktemplates
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t, os.GetOutput)
+
+	actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.GetExpected
+	th.AssertDeepEquals(t, expected, string(actual))
+}
+
+func TestValidateTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleValidateSuccessfully(t, os.ValidateOutput)
+
+	opts := os.ValidateOpts{
+		Template: `{
+			"Description": "Simple template to test heat commands",
+			"Parameters": {
+				"flavor": {
+					"Default": "m1.tiny",
+					"Type": "String",
+					"NoEcho": "false",
+					"Description": "",
+					"Label": "flavor"
+				}
+			}
+		}`,
+	}
+	actual, err := Validate(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := os.ValidateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/stacktemplates/doc.go b/rackspace/orchestration/v1/stacktemplates/doc.go
new file mode 100644
index 0000000..5af0bd6
--- /dev/null
+++ b/rackspace/orchestration/v1/stacktemplates/doc.go
@@ -0,0 +1,8 @@
+// Package stacktemplates provides operations for working with Heat templates.
+// A Cloud Orchestration template is a portable file, written in a user-readable
+// language, that describes how a set of resources should be assembled and what
+// software should be installed in order to produce a working stack. The template
+// specifies what resources should be used, what attributes can be set, and other
+// parameters that are critical to the successful, repeatable automation of a
+// specific application stack.
+package stacktemplates
diff --git a/rackspace/rackconnect/v3/cloudnetworks/requests.go b/rackspace/rackconnect/v3/cloudnetworks/requests.go
new file mode 100644
index 0000000..5884303
--- /dev/null
+++ b/rackspace/rackconnect/v3/cloudnetworks/requests.go
@@ -0,0 +1,24 @@
+package cloudnetworks
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns all cloud networks that are associated with RackConnect. The ID
+// returned for each network is the same as the ID returned by the networks package.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(c)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return CloudNetworkPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves a specific cloud network (that is associated with RackConnect)
+// based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
+	return res
+}
diff --git a/rackspace/rackconnect/v3/cloudnetworks/requests_test.go b/rackspace/rackconnect/v3/cloudnetworks/requests_test.go
new file mode 100644
index 0000000..10d15dd
--- /dev/null
+++ b/rackspace/rackconnect/v3/cloudnetworks/requests_test.go
@@ -0,0 +1,87 @@
+package cloudnetworks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListCloudNetworks(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/cloud_networks", 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")
+		fmt.Fprintf(w, `[{
+      "cidr": "192.168.100.0/24",
+      "created": "2014-05-25T01:23:42Z",
+      "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+      "name": "RC-CLOUD",
+      "updated": "2014-05-25T02:28:44Z"
+    }]`)
+	})
+
+	expected := []CloudNetwork{
+		CloudNetwork{
+			CIDR:      "192.168.100.0/24",
+			CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+			ID:        "07426958-1ebf-4c38-b032-d456820ca21a",
+			Name:      "RC-CLOUD",
+			UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+		},
+	}
+
+	count := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractCloudNetworks(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetCloudNetwork(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/cloud_networks/07426958-1ebf-4c38-b032-d456820ca21a", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{
+      "cidr": "192.168.100.0/24",
+      "created": "2014-05-25T01:23:42Z",
+      "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+      "name": "RC-CLOUD",
+      "updated": "2014-05-25T02:28:44Z"
+    }`)
+	})
+
+	expected := &CloudNetwork{
+		CIDR:      "192.168.100.0/24",
+		CreatedAt: time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+		ID:        "07426958-1ebf-4c38-b032-d456820ca21a",
+		Name:      "RC-CLOUD",
+		UpdatedAt: time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+	}
+
+	actual, err := Get(fake.ServiceClient(), "07426958-1ebf-4c38-b032-d456820ca21a").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/rackconnect/v3/cloudnetworks/results.go b/rackspace/rackconnect/v3/cloudnetworks/results.go
new file mode 100644
index 0000000..f554a0d
--- /dev/null
+++ b/rackspace/rackconnect/v3/cloudnetworks/results.go
@@ -0,0 +1,113 @@
+package cloudnetworks
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CloudNetwork represents a network associated with a RackConnect configuration.
+type CloudNetwork struct {
+	// Specifies the ID of the newtork.
+	ID string `mapstructure:"id"`
+	// Specifies the user-provided name of the network.
+	Name string `mapstructure:"name"`
+	// Specifies the IP range for this network.
+	CIDR string `mapstructure:"cidr"`
+	// Specifies the time the network was created.
+	CreatedAt time.Time `mapstructure:"-"`
+	// Specifies the time the network was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+}
+
+// CloudNetworkPage is the page returned by a pager when traversing over a
+// collection of CloudNetworks.
+type CloudNetworkPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a CloudNetworkPage contains no CloudNetworks.
+func (r CloudNetworkPage) IsEmpty() (bool, error) {
+	cns, err := ExtractCloudNetworks(r)
+	if err != nil {
+		return true, err
+	}
+	return len(cns) == 0, nil
+}
+
+// ExtractCloudNetworks extracts and returns CloudNetworks. It is used while iterating over
+// a cloudnetworks.List call.
+func ExtractCloudNetworks(page pagination.Page) ([]CloudNetwork, error) {
+	var res []CloudNetwork
+	casted := page.(CloudNetworkPage).Body
+	err := mapstructure.Decode(casted, &res)
+
+	var rawNets []interface{}
+	switch casted.(type) {
+	case interface{}:
+		rawNets = casted.([]interface{})
+	default:
+		return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i := range rawNets {
+		thisNet := (rawNets[i]).(map[string]interface{})
+
+		if t, ok := thisNet["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].CreatedAt = creationTime
+		}
+
+		if t, ok := thisNet["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].UpdatedAt = updatedTime
+		}
+	}
+
+	return res, err
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a CloudNetwork from a GetResult.
+func (r GetResult) Extract() (*CloudNetwork, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	var res CloudNetwork
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	b := r.Body.(map[string]interface{})
+
+	if date, ok := b["created"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.CreatedAt = t
+	}
+
+	if date, ok := b["updated"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.UpdatedAt = t
+	}
+
+	return &res, err
+}
diff --git a/rackspace/rackconnect/v3/cloudnetworks/urls.go b/rackspace/rackconnect/v3/cloudnetworks/urls.go
new file mode 100644
index 0000000..bd6b098
--- /dev/null
+++ b/rackspace/rackconnect/v3/cloudnetworks/urls.go
@@ -0,0 +1,11 @@
+package cloudnetworks
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("cloud_networks")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("cloud_networks", id)
+}
diff --git a/rackspace/rackconnect/v3/doc.go b/rackspace/rackconnect/v3/doc.go
new file mode 100644
index 0000000..3a8279e
--- /dev/null
+++ b/rackspace/rackconnect/v3/doc.go
@@ -0,0 +1,4 @@
+// Package rackconnect allows Rackspace cloud accounts to leverage version 3 of
+// RackConnect, Rackspace's hybrid connectivity solution connecting dedicated
+// and cloud servers.
+package rackconnect
diff --git a/rackspace/rackconnect/v3/lbpools/doc.go b/rackspace/rackconnect/v3/lbpools/doc.go
new file mode 100644
index 0000000..f4319b8
--- /dev/null
+++ b/rackspace/rackconnect/v3/lbpools/doc.go
@@ -0,0 +1,14 @@
+// Package lbpools provides access to load balancer pools associated with a
+// RackConnect configuration. Load Balancer Pools must be configured in advance
+// by your Network Security team to be eligible for use with RackConnect.
+// If you do not see a pool that you expect to see, contact your Support team
+// for further assistance. The Load Balancer Pool id returned by these calls is
+// automatically generated by the RackConnect automation and will remain constant
+// unless the Load Balancer Pool is renamed on your hardware load balancer.
+// All Load Balancer Pools will currently return a status of ACTIVE. Future
+// features may introduce additional statuses.
+// Node status values are ADDING, ACTIVE, REMOVING, ADD_FAILED, and REMOVE_FAILED.
+// The cloud_servers node count will only include Cloud Servers from the specified
+// cloud account. Any dedicated servers or cloud servers from another cloud account
+// on the same RackConnect Configuration will be counted as external nodes.
+package lbpools
diff --git a/rackspace/rackconnect/v3/lbpools/requests.go b/rackspace/rackconnect/v3/lbpools/requests.go
new file mode 100644
index 0000000..c300c56
--- /dev/null
+++ b/rackspace/rackconnect/v3/lbpools/requests.go
@@ -0,0 +1,146 @@
+package lbpools
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns all load balancer pools that are associated with RackConnect.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(c)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return PoolPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves a specific load balancer pool (that is associated with RackConnect)
+// based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
+	return res
+}
+
+// ListNodes returns all load balancer pool nodes that are associated with RackConnect
+// for the given LB pool ID.
+func ListNodes(c *gophercloud.ServiceClient, id string) pagination.Pager {
+	url := listNodesURL(c, id)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NodePage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// CreateNode adds the cloud server with the given serverID to the load balancer
+// pool with the given poolID.
+func CreateNode(c *gophercloud.ServiceClient, poolID, serverID string) CreateNodeResult {
+	var res CreateNodeResult
+	reqBody := map[string]interface{}{
+		"cloud_server": map[string]string{
+			"id": serverID,
+		},
+	}
+	_, res.Err = c.Post(createNodeURL(c, poolID), reqBody, &res.Body, nil)
+	return res
+}
+
+// ListNodesDetails returns all load balancer pool nodes that are associated with RackConnect
+// for the given LB pool ID with all their details.
+func ListNodesDetails(c *gophercloud.ServiceClient, id string) pagination.Pager {
+	url := listNodesDetailsURL(c, id)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NodeDetailsPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// GetNode retrieves a specific LB pool node (that is associated with RackConnect)
+// based on its unique ID and the LB pool's unique ID.
+func GetNode(c *gophercloud.ServiceClient, poolID, nodeID string) GetNodeResult {
+	var res GetNodeResult
+	_, res.Err = c.Get(nodeURL(c, poolID, nodeID), &res.Body, nil)
+	return res
+}
+
+// DeleteNode removes the node with the given nodeID from the LB pool with the
+// given poolID.
+func DeleteNode(c *gophercloud.ServiceClient, poolID, nodeID string) DeleteNodeResult {
+	var res DeleteNodeResult
+	_, res.Err = c.Delete(deleteNodeURL(c, poolID, nodeID), nil)
+	return res
+}
+
+// GetNodeDetails retrieves a specific LB pool node's details based on its unique
+// ID and the LB pool's unique ID.
+func GetNodeDetails(c *gophercloud.ServiceClient, poolID, nodeID string) GetNodeDetailsResult {
+	var res GetNodeDetailsResult
+	_, res.Err = c.Get(nodeDetailsURL(c, poolID, nodeID), &res.Body, nil)
+	return res
+}
+
+// NodeOpts are options for bulk adding/deleting nodes to LB pools.
+type NodeOpts struct {
+	ServerID string
+	PoolID   string
+}
+
+// NodesOpts are a slice of NodeOpts, passed as options for bulk operations.
+type NodesOpts []NodeOpts
+
+// ToLBPoolCreateNodesMap serializes a NodesOpts into a map to send in the request.
+func (o NodesOpts) ToLBPoolCreateNodesMap() ([]map[string]interface{}, error) {
+	m := make([]map[string]interface{}, len(o))
+	for i := range o {
+		m[i] = map[string]interface{}{
+			"cloud_server": map[string]string{
+				"id": o[i].ServerID,
+			},
+			"load_balancer_pool": map[string]string{
+				"id": o[i].PoolID,
+			},
+		}
+	}
+	return m, nil
+}
+
+// CreateNodes adds the cloud servers with the given serverIDs to the corresponding
+// load balancer pools with the given poolIDs.
+func CreateNodes(c *gophercloud.ServiceClient, opts NodesOpts) CreateNodesResult {
+	var res CreateNodesResult
+	reqBody, err := opts.ToLBPoolCreateNodesMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Post(createNodesURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// DeleteNodes removes the cloud servers with the given serverIDs to the corresponding
+// load balancer pools with the given poolIDs.
+func DeleteNodes(c *gophercloud.ServiceClient, opts NodesOpts) DeleteNodesResult {
+	var res DeleteNodesResult
+	reqBody, err := opts.ToLBPoolCreateNodesMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Request("DELETE", createNodesURL(c), gophercloud.RequestOpts{
+		JSONBody: &reqBody,
+		OkCodes:  []int{204},
+	})
+	return res
+}
+
+// ListNodesDetailsForServer is similar to ListNodesDetails but only returns nodes
+// for the given serverID.
+func ListNodesDetailsForServer(c *gophercloud.ServiceClient, serverID string) pagination.Pager {
+	url := listNodesForServerURL(c, serverID)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NodeDetailsForServerPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
diff --git a/rackspace/rackconnect/v3/lbpools/requests_test.go b/rackspace/rackconnect/v3/lbpools/requests_test.go
new file mode 100644
index 0000000..48ebcec
--- /dev/null
+++ b/rackspace/rackconnect/v3/lbpools/requests_test.go
@@ -0,0 +1,876 @@
+package lbpools
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListPools(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools", 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")
+		fmt.Fprintf(w, `[
+      {
+        "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+        "name": "RCv3Test",
+        "node_counts": {
+          "cloud_servers": 3,
+          "external": 4,
+          "total": 7
+        },
+        "port": 80,
+        "status": "ACTIVE",
+        "status_detail": null,
+        "virtual_ip": "203.0.113.5"
+      },
+      {
+        "id": "33021100-4abf-4836-9080-465a6d87ab68",
+        "name": "RCv3Test2",
+        "node_counts": {
+          "cloud_servers": 1,
+          "external": 0,
+          "total": 1
+        },
+        "port": 80,
+        "status": "ACTIVE",
+        "status_detail": null,
+        "virtual_ip": "203.0.113.7"
+      },
+      {
+        "id": "b644350a-301b-47b5-a411-c6e0f933c347",
+        "name": "RCv3Test3",
+        "node_counts": {
+          "cloud_servers": 2,
+          "external": 3,
+          "total": 5
+        },
+        "port": 443,
+        "status": "ACTIVE",
+        "status_detail": null,
+        "virtual_ip": "203.0.113.15"
+      }
+    ]`)
+	})
+
+	expected := []Pool{
+		Pool{
+			ID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+			Name: "RCv3Test",
+			NodeCounts: struct {
+				CloudServers int `mapstructure:"cloud_servers"`
+				External     int `mapstructure:"external"`
+				Total        int `mapstructure:"total"`
+			}{
+				CloudServers: 3,
+				External:     4,
+				Total:        7,
+			},
+			Port:      80,
+			Status:    "ACTIVE",
+			VirtualIP: "203.0.113.5",
+		},
+		Pool{
+			ID:   "33021100-4abf-4836-9080-465a6d87ab68",
+			Name: "RCv3Test2",
+			NodeCounts: struct {
+				CloudServers int `mapstructure:"cloud_servers"`
+				External     int `mapstructure:"external"`
+				Total        int `mapstructure:"total"`
+			}{
+				CloudServers: 1,
+				External:     0,
+				Total:        1,
+			},
+			Port:      80,
+			Status:    "ACTIVE",
+			VirtualIP: "203.0.113.7",
+		},
+		Pool{
+			ID:   "b644350a-301b-47b5-a411-c6e0f933c347",
+			Name: "RCv3Test3",
+			NodeCounts: struct {
+				CloudServers int `mapstructure:"cloud_servers"`
+				External     int `mapstructure:"external"`
+				Total        int `mapstructure:"total"`
+			}{
+				CloudServers: 2,
+				External:     3,
+				Total:        5,
+			},
+			Port:      443,
+			Status:    "ACTIVE",
+			VirtualIP: "203.0.113.15",
+		},
+	}
+
+	count := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPools(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetLBPool(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{
+      "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+      "name": "RCv3Test",
+      "node_counts": {
+        "cloud_servers": 3,
+        "external": 4,
+        "total": 7
+      },
+      "port": 80,
+      "status": "ACTIVE",
+      "status_detail": null,
+      "virtual_ip": "203.0.113.5"
+    }`)
+	})
+
+	expected := &Pool{
+		ID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+		Name: "RCv3Test",
+		NodeCounts: struct {
+			CloudServers int `mapstructure:"cloud_servers"`
+			External     int `mapstructure:"external"`
+			Total        int `mapstructure:"total"`
+		}{
+			CloudServers: 3,
+			External:     4,
+			Total:        7,
+		},
+		Port:      80,
+		Status:    "ACTIVE",
+		VirtualIP: "203.0.113.5",
+	}
+
+	actual, err := Get(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListNodes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes", 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")
+		fmt.Fprintf(w, `[
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        },
+        "status": "ACTIVE",
+        "updated": "2014-05-30T03:24:18Z"
+      },
+      {
+        "created": "2014-05-31T08:23:12Z",
+        "cloud_server": {
+          "id": "f28b870f-a063-498a-8b12-7025e5b1caa6"
+        },
+        "id": "b70481dd-7edf-4dbb-a44b-41cc7679d4fb",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        },
+        "status": "ADDING",
+        "updated": "2014-05-31T08:23:26Z"
+      },
+      {
+        "created": "2014-05-31T08:23:18Z",
+        "cloud_server": {
+          "id": "a3d3a6b3-e4e4-496f-9a3d-5c987163e458"
+        },
+        "id": "ced9ddc8-6fae-4e72-9457-16ead52b5515",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        },
+        "status": "ADD_FAILED",
+        "status_detail": "Unable to communicate with network device",
+        "updated": "2014-05-31T08:24:36Z"
+      }
+    ]`)
+	})
+
+	expected := []Node{
+		Node{
+			CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+			CloudServer: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			},
+			ID: "1860451d-fb89-45b8-b54e-151afceb50e5",
+			LoadBalancerPool: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+			},
+			Status:    "ACTIVE",
+			UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+		},
+		Node{
+			CreatedAt: time.Date(2014, 5, 31, 8, 23, 12, 0, time.UTC),
+			CloudServer: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "f28b870f-a063-498a-8b12-7025e5b1caa6",
+			},
+			ID: "b70481dd-7edf-4dbb-a44b-41cc7679d4fb",
+			LoadBalancerPool: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+			},
+			Status:    "ADDING",
+			UpdatedAt: time.Date(2014, 5, 31, 8, 23, 26, 0, time.UTC),
+		},
+		Node{
+			CreatedAt: time.Date(2014, 5, 31, 8, 23, 18, 0, time.UTC),
+			CloudServer: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "a3d3a6b3-e4e4-496f-9a3d-5c987163e458",
+			},
+			ID: "ced9ddc8-6fae-4e72-9457-16ead52b5515",
+			LoadBalancerPool: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+			},
+			Status:       "ADD_FAILED",
+			StatusDetail: "Unable to communicate with network device",
+			UpdatedAt:    time.Date(2014, 5, 31, 8, 24, 36, 0, time.UTC),
+		},
+	}
+
+	count := 0
+	err := ListNodes(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNodes(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes", 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.TestJSONRequest(t, r, `
+      {
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        }
+      }
+    `)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        },
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }
+    `)
+	})
+
+	expected := &Node{
+		CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+		CloudServer: struct {
+			ID string `mapstructure:"id"`
+		}{
+			ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+		},
+		ID: "1860451d-fb89-45b8-b54e-151afceb50e5",
+		LoadBalancerPool: struct {
+			ID string `mapstructure:"id"`
+		}{
+			ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+		},
+		Status:    "ACTIVE",
+		UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+	}
+
+	actual, err := CreateNode(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "d95ae0c4-6ab8-4873-b82f-f8433840cff2").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListNodesDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/details", 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")
+		fmt.Fprintf(w, `
+      [
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "cloud_network": {
+            "cidr": "192.168.100.0/24",
+            "created": "2014-05-25T01:23:42Z",
+            "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+            "name": "RC-CLOUD",
+            "private_ip_v4": "192.168.100.5",
+            "updated": "2014-05-25T02:28:44Z"
+          },
+          "created": "2014-05-30T02:18:42Z",
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+          "name": "RCv3TestServer1",
+          "updated": "2014-05-30T02:19:18Z"
+        },
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+          "name": "RCv3Test",
+          "node_counts": {
+            "cloud_servers": 3,
+            "external": 4,
+            "total": 7
+          },
+          "port": 80,
+          "status": "ACTIVE",
+          "status_detail": null,
+          "virtual_ip": "203.0.113.5"
+        },
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }
+      ]
+    `)
+	})
+
+	expected := []NodeDetails{
+		NodeDetails{
+			CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+			CloudServer: struct {
+				ID           string `mapstructure:"id"`
+				Name         string `mapstructure:"name"`
+				CloudNetwork struct {
+					ID          string    `mapstructure:"id"`
+					Name        string    `mapstructure:"name"`
+					PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+					CIDR        string    `mapstructure:"cidr"`
+					CreatedAt   time.Time `mapstructure:"-"`
+					UpdatedAt   time.Time `mapstructure:"-"`
+				} `mapstructure:"cloud_network"`
+				CreatedAt time.Time `mapstructure:"-"`
+				UpdatedAt time.Time `mapstructure:"-"`
+			}{
+				ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+				CloudNetwork: struct {
+					ID          string    `mapstructure:"id"`
+					Name        string    `mapstructure:"name"`
+					PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+					CIDR        string    `mapstructure:"cidr"`
+					CreatedAt   time.Time `mapstructure:"-"`
+					UpdatedAt   time.Time `mapstructure:"-"`
+				}{
+					ID:          "07426958-1ebf-4c38-b032-d456820ca21a",
+					CIDR:        "192.168.100.0/24",
+					CreatedAt:   time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+					Name:        "RC-CLOUD",
+					PrivateIPv4: "192.168.100.5",
+					UpdatedAt:   time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+				},
+				CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC),
+				Name:      "RCv3TestServer1",
+				UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC),
+			},
+			ID: "1860451d-fb89-45b8-b54e-151afceb50e5",
+			LoadBalancerPool: Pool{
+				ID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+				Name: "RCv3Test",
+				NodeCounts: struct {
+					CloudServers int `mapstructure:"cloud_servers"`
+					External     int `mapstructure:"external"`
+					Total        int `mapstructure:"total"`
+				}{
+					CloudServers: 3,
+					External:     4,
+					Total:        7,
+				},
+				Port:      80,
+				Status:    "ACTIVE",
+				VirtualIP: "203.0.113.5",
+			},
+			Status:    "ACTIVE",
+			UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+		},
+	}
+	count := 0
+	err := ListNodesDetails(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNodesDetails(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/1860451d-fb89-45b8-b54e-151afceb50e5", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        },
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }
+    `)
+	})
+
+	expected := &Node{
+		CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+		CloudServer: struct {
+			ID string `mapstructure:"id"`
+		}{
+			ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+		},
+		ID: "1860451d-fb89-45b8-b54e-151afceb50e5",
+		LoadBalancerPool: struct {
+			ID string `mapstructure:"id"`
+		}{
+			ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+		},
+		Status:    "ACTIVE",
+		UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+	}
+
+	actual, err := GetNode(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "1860451d-fb89-45b8-b54e-151afceb50e5").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteNode(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/1860451d-fb89-45b8-b54e-151afceb50e5", 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.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := DeleteNode(client.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "1860451d-fb89-45b8-b54e-151afceb50e5").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetNodeDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2/nodes/d95ae0c4-6ab8-4873-b82f-f8433840cff2/details", 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")
+		fmt.Fprintf(w, `
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "cloud_network": {
+            "cidr": "192.168.100.0/24",
+            "created": "2014-05-25T01:23:42Z",
+            "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+            "name": "RC-CLOUD",
+            "private_ip_v4": "192.168.100.5",
+            "updated": "2014-05-25T02:28:44Z"
+          },
+          "created": "2014-05-30T02:18:42Z",
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+          "name": "RCv3TestServer1",
+          "updated": "2014-05-30T02:19:18Z"
+        },
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+          "name": "RCv3Test",
+          "node_counts": {
+            "cloud_servers": 3,
+            "external": 4,
+            "total": 7
+          },
+          "port": 80,
+          "status": "ACTIVE",
+          "status_detail": null,
+          "virtual_ip": "203.0.113.5"
+        },
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }
+    `)
+	})
+
+	expected := &NodeDetails{
+		CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+		CloudServer: struct {
+			ID           string `mapstructure:"id"`
+			Name         string `mapstructure:"name"`
+			CloudNetwork struct {
+				ID          string    `mapstructure:"id"`
+				Name        string    `mapstructure:"name"`
+				PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+				CIDR        string    `mapstructure:"cidr"`
+				CreatedAt   time.Time `mapstructure:"-"`
+				UpdatedAt   time.Time `mapstructure:"-"`
+			} `mapstructure:"cloud_network"`
+			CreatedAt time.Time `mapstructure:"-"`
+			UpdatedAt time.Time `mapstructure:"-"`
+		}{
+			ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			CloudNetwork: struct {
+				ID          string    `mapstructure:"id"`
+				Name        string    `mapstructure:"name"`
+				PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+				CIDR        string    `mapstructure:"cidr"`
+				CreatedAt   time.Time `mapstructure:"-"`
+				UpdatedAt   time.Time `mapstructure:"-"`
+			}{
+				ID:          "07426958-1ebf-4c38-b032-d456820ca21a",
+				CIDR:        "192.168.100.0/24",
+				CreatedAt:   time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+				Name:        "RC-CLOUD",
+				PrivateIPv4: "192.168.100.5",
+				UpdatedAt:   time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+			},
+			CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC),
+			Name:      "RCv3TestServer1",
+			UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC),
+		},
+		ID: "1860451d-fb89-45b8-b54e-151afceb50e5",
+		LoadBalancerPool: Pool{
+			ID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+			Name: "RCv3Test",
+			NodeCounts: struct {
+				CloudServers int `mapstructure:"cloud_servers"`
+				External     int `mapstructure:"external"`
+				Total        int `mapstructure:"total"`
+			}{
+				CloudServers: 3,
+				External:     4,
+				Total:        7,
+			},
+			Port:      80,
+			Status:    "ACTIVE",
+			VirtualIP: "203.0.113.5",
+		},
+		Status:    "ACTIVE",
+		UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+	}
+
+	actual, err := GetNodeDetails(fake.ServiceClient(), "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2", "d95ae0c4-6ab8-4873-b82f-f8433840cff2").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestCreateNodes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/nodes", 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.TestJSONRequest(t, r, `
+      [
+      {
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        }
+      },
+      {
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "load_balancer_pool": {
+          "id": "33021100-4abf-4836-9080-465a6d87ab68"
+      }
+    }
+    ]
+  `)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `
+      [
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        },
+        "status": "ADDING",
+        "status_detail": null,
+        "updated": null
+      },
+      {
+        "created": "2014-05-31T08:23:12Z",
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "id": "b70481dd-7edf-4dbb-a44b-41cc7679d4fb",
+        "load_balancer_pool": {
+          "id": "33021100-4abf-4836-9080-465a6d87ab68"
+        },
+        "status": "ADDING",
+        "status_detail": null,
+        "updated": null
+      }
+      ]
+    `)
+	})
+
+	expected := []Node{
+		Node{
+			CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+			CloudServer: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			},
+			ID: "1860451d-fb89-45b8-b54e-151afceb50e5",
+			LoadBalancerPool: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+			},
+			Status: "ADDING",
+		},
+		Node{
+			CreatedAt: time.Date(2014, 5, 31, 8, 23, 12, 0, time.UTC),
+			CloudServer: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			},
+			ID: "b70481dd-7edf-4dbb-a44b-41cc7679d4fb",
+			LoadBalancerPool: struct {
+				ID string `mapstructure:"id"`
+			}{
+				ID: "33021100-4abf-4836-9080-465a6d87ab68",
+			},
+			Status: "ADDING",
+		},
+	}
+
+	opts := NodesOpts{
+		NodeOpts{
+			ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			PoolID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+		},
+		NodeOpts{
+			ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			PoolID:   "33021100-4abf-4836-9080-465a6d87ab68",
+		},
+	}
+	actual, err := CreateNodes(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteNodes(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/nodes", 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")
+		th.TestJSONRequest(t, r, `
+      [
+      {
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2"
+        }
+      },
+      {
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        },
+        "load_balancer_pool": {
+          "id": "33021100-4abf-4836-9080-465a6d87ab68"
+        }
+      }
+      ]
+    `)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	opts := NodesOpts{
+		NodeOpts{
+			ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			PoolID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+		},
+		NodeOpts{
+			ServerID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			PoolID:   "33021100-4abf-4836-9080-465a6d87ab68",
+		},
+	}
+	err := DeleteNodes(client.ServiceClient(), opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListNodesForServerDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/load_balancer_pools/nodes/details", 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")
+		fmt.Fprintf(w, `
+      [
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "id": "1860451d-fb89-45b8-b54e-151afceb50e5",
+        "load_balancer_pool": {
+          "id": "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+          "name": "RCv3Test",
+          "node_counts": {
+            "cloud_servers": 3,
+            "external": 4,
+            "total": 7
+          },
+          "port": 80,
+          "status": "ACTIVE",
+          "status_detail": null,
+          "virtual_ip": "203.0.113.5"
+        },
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }
+      ]
+    `)
+	})
+
+	expected := []NodeDetailsForServer{
+		NodeDetailsForServer{
+			CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+			ID:        "1860451d-fb89-45b8-b54e-151afceb50e5",
+			LoadBalancerPool: Pool{
+				ID:   "d6d3aa7c-dfa5-4e61-96ee-1d54ac1075d2",
+				Name: "RCv3Test",
+				NodeCounts: struct {
+					CloudServers int `mapstructure:"cloud_servers"`
+					External     int `mapstructure:"external"`
+					Total        int `mapstructure:"total"`
+				}{
+					CloudServers: 3,
+					External:     4,
+					Total:        7,
+				},
+				Port:      80,
+				Status:    "ACTIVE",
+				VirtualIP: "203.0.113.5",
+			},
+			Status:    "ACTIVE",
+			UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+		},
+	}
+	count := 0
+	err := ListNodesDetailsForServer(fake.ServiceClient(), "07426958-1ebf-4c38-b032-d456820ca21a").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNodesDetailsForServer(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
diff --git a/rackspace/rackconnect/v3/lbpools/results.go b/rackspace/rackconnect/v3/lbpools/results.go
new file mode 100644
index 0000000..e5e914b
--- /dev/null
+++ b/rackspace/rackconnect/v3/lbpools/results.go
@@ -0,0 +1,505 @@
+package lbpools
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Pool represents a load balancer pool associated with a RackConnect configuration.
+type Pool struct {
+	// The unique ID of the load balancer pool.
+	ID string `mapstructure:"id"`
+	// The name of the load balancer pool.
+	Name string `mapstructure:"name"`
+	// The node counts associated witht the load balancer pool.
+	NodeCounts struct {
+		// The number of nodes associated with this LB pool for this account.
+		CloudServers int `mapstructure:"cloud_servers"`
+		// The number of nodes associated with this LB pool from other accounts.
+		External int `mapstructure:"external"`
+		// The total number of nodes associated with this LB pool.
+		Total int `mapstructure:"total"`
+	} `mapstructure:"node_counts"`
+	// The port of the LB pool
+	Port int `mapstructure:"port"`
+	// The status of the LB pool
+	Status string `mapstructure:"status"`
+	// The details of the status of the LB pool
+	StatusDetail string `mapstructure:"status_detail"`
+	// The virtual IP of the LB pool
+	VirtualIP string `mapstructure:"virtual_ip"`
+}
+
+// PoolPage is the page returned by a pager when traversing over a
+// collection of Pools.
+type PoolPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a PoolPage contains no Pools.
+func (r PoolPage) IsEmpty() (bool, error) {
+	cns, err := ExtractPools(r)
+	if err != nil {
+		return true, err
+	}
+	return len(cns) == 0, nil
+}
+
+// ExtractPools extracts and returns Pools. It is used while iterating over
+// an lbpools.List call.
+func ExtractPools(page pagination.Page) ([]Pool, error) {
+	var res []Pool
+	err := mapstructure.Decode(page.(PoolPage).Body, &res)
+	return res, err
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts an LBPool from a GetResult.
+func (r GetResult) Extract() (*Pool, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	var res Pool
+	err := mapstructure.Decode(r.Body, &res)
+	return &res, err
+}
+
+// Node represents a load balancer pool node associated with a RackConnect configuration.
+type Node struct {
+	// The unique ID of the LB node.
+	ID string `mapstructure:"id"`
+	// The cloud server (node) of the load balancer pool.
+	CloudServer struct {
+		// The cloud server ID.
+		ID string `mapstructure:"id"`
+	} `mapstructure:"cloud_server"`
+	// The load balancer pool.
+	LoadBalancerPool struct {
+		// The LB pool ID.
+		ID string `mapstructure:"id"`
+	} `mapstructure:"load_balancer_pool"`
+	// The status of the LB pool.
+	Status string `mapstructure:"status"`
+	// The details of the status of the LB pool.
+	StatusDetail string `mapstructure:"status_detail"`
+	// The time the LB node was created.
+	CreatedAt time.Time `mapstructure:"-"`
+	// The time the LB node was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+}
+
+// NodePage is the page returned by a pager when traversing over a
+// collection of Nodes.
+type NodePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a NodePage contains no Nodes.
+func (r NodePage) IsEmpty() (bool, error) {
+	n, err := ExtractNodes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(n) == 0, nil
+}
+
+// ExtractNodes extracts and returns a slice of Nodes. It is used while iterating over
+// an lbpools.ListNodes call.
+func ExtractNodes(page pagination.Page) ([]Node, error) {
+	var res []Node
+	casted := page.(NodePage).Body
+	err := mapstructure.Decode(casted, &res)
+
+	var rawNodes []interface{}
+	switch casted.(type) {
+	case interface{}:
+		rawNodes = casted.([]interface{})
+	default:
+		return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i := range rawNodes {
+		thisNode := (rawNodes[i]).(map[string]interface{})
+
+		if t, ok := thisNode["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].CreatedAt = creationTime
+		}
+
+		if t, ok := thisNode["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].UpdatedAt = updatedTime
+		}
+	}
+
+	return res, err
+}
+
+// NodeResult represents a result that can be extracted as a Node.
+type NodeResult struct {
+	gophercloud.Result
+}
+
+// CreateNodeResult represents the result of an CreateNode operation.
+type CreateNodeResult struct {
+	NodeResult
+}
+
+// GetNodeResult represents the result of an GetNode operation.
+type GetNodeResult struct {
+	NodeResult
+}
+
+// Extract is a function that extracts a Node from a NodeResult.
+func (r NodeResult) Extract() (*Node, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	var res Node
+	err := mapstructure.Decode(r.Body, &res)
+
+	b := r.Body.(map[string]interface{})
+
+	if date, ok := b["created"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.CreatedAt = t
+	}
+
+	if date, ok := b["updated"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.UpdatedAt = t
+	}
+
+	return &res, err
+}
+
+// NodeDetails represents a load balancer pool node associated with a RackConnect configuration
+// with all its details.
+type NodeDetails struct {
+	// The unique ID of the LB node.
+	ID string `mapstructure:"id"`
+	// The cloud server (node) of the load balancer pool.
+	CloudServer struct {
+		// The cloud server ID.
+		ID string `mapstructure:"id"`
+		// The name of the server.
+		Name string `mapstructure:"name"`
+		// The cloud network for the cloud server.
+		CloudNetwork struct {
+			// The network ID.
+			ID string `mapstructure:"id"`
+			// The network name.
+			Name string `mapstructure:"name"`
+			// The network's private IPv4 address.
+			PrivateIPv4 string `mapstructure:"private_ip_v4"`
+			// The IP range for the network.
+			CIDR string `mapstructure:"cidr"`
+			// The datetime the network was created.
+			CreatedAt time.Time `mapstructure:"-"`
+			// The last datetime the network was updated.
+			UpdatedAt time.Time `mapstructure:"-"`
+		} `mapstructure:"cloud_network"`
+		// The datetime the server was created.
+		CreatedAt time.Time `mapstructure:"-"`
+		// The datetime the server was last updated.
+		UpdatedAt time.Time `mapstructure:"-"`
+	} `mapstructure:"cloud_server"`
+	// The load balancer pool.
+	LoadBalancerPool Pool `mapstructure:"load_balancer_pool"`
+	// The status of the LB pool.
+	Status string `mapstructure:"status"`
+	// The details of the status of the LB pool.
+	StatusDetail string `mapstructure:"status_detail"`
+	// The time the LB node was created.
+	CreatedAt time.Time `mapstructure:"-"`
+	// The time the LB node was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+}
+
+// NodeDetailsPage is the page returned by a pager when traversing over a
+// collection of NodeDetails.
+type NodeDetailsPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a NodeDetailsPage contains no NodeDetails.
+func (r NodeDetailsPage) IsEmpty() (bool, error) {
+	n, err := ExtractNodesDetails(r)
+	if err != nil {
+		return true, err
+	}
+	return len(n) == 0, nil
+}
+
+// ExtractNodesDetails extracts and returns a slice of NodeDetails. It is used while iterating over
+// an lbpools.ListNodesDetails call.
+func ExtractNodesDetails(page pagination.Page) ([]NodeDetails, error) {
+	var res []NodeDetails
+	casted := page.(NodeDetailsPage).Body
+	err := mapstructure.Decode(casted, &res)
+
+	var rawNodesDetails []interface{}
+	switch casted.(type) {
+	case interface{}:
+		rawNodesDetails = casted.([]interface{})
+	default:
+		return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i := range rawNodesDetails {
+		thisNodeDetails := (rawNodesDetails[i]).(map[string]interface{})
+
+		if t, ok := thisNodeDetails["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].CreatedAt = creationTime
+		}
+
+		if t, ok := thisNodeDetails["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].UpdatedAt = updatedTime
+		}
+
+		if cs, ok := thisNodeDetails["cloud_server"].(map[string]interface{}); ok {
+			if t, ok := cs["created"].(string); ok && t != "" {
+				creationTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return res, err
+				}
+				res[i].CloudServer.CreatedAt = creationTime
+			}
+			if t, ok := cs["updated"].(string); ok && t != "" {
+				updatedTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return res, err
+				}
+				res[i].CloudServer.UpdatedAt = updatedTime
+			}
+			if cn, ok := cs["cloud_network"].(map[string]interface{}); ok {
+				if t, ok := cn["created"].(string); ok && t != "" {
+					creationTime, err := time.Parse(time.RFC3339, t)
+					if err != nil {
+						return res, err
+					}
+					res[i].CloudServer.CloudNetwork.CreatedAt = creationTime
+				}
+				if t, ok := cn["updated"].(string); ok && t != "" {
+					updatedTime, err := time.Parse(time.RFC3339, t)
+					if err != nil {
+						return res, err
+					}
+					res[i].CloudServer.CloudNetwork.UpdatedAt = updatedTime
+				}
+			}
+		}
+	}
+
+	return res, err
+}
+
+// GetNodeDetailsResult represents the result of an NodeDetails operation.
+type GetNodeDetailsResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a NodeDetails from a NodeDetailsResult.
+func (r GetNodeDetailsResult) Extract() (*NodeDetails, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	var res NodeDetails
+	err := mapstructure.Decode(r.Body, &res)
+
+	b := r.Body.(map[string]interface{})
+
+	if date, ok := b["created"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.CreatedAt = t
+	}
+
+	if date, ok := b["updated"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.UpdatedAt = t
+	}
+
+	if cs, ok := b["cloud_server"].(map[string]interface{}); ok {
+		if t, ok := cs["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return &res, err
+			}
+			res.CloudServer.CreatedAt = creationTime
+		}
+		if t, ok := cs["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return &res, err
+			}
+			res.CloudServer.UpdatedAt = updatedTime
+		}
+		if cn, ok := cs["cloud_network"].(map[string]interface{}); ok {
+			if t, ok := cn["created"].(string); ok && t != "" {
+				creationTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return &res, err
+				}
+				res.CloudServer.CloudNetwork.CreatedAt = creationTime
+			}
+			if t, ok := cn["updated"].(string); ok && t != "" {
+				updatedTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return &res, err
+				}
+				res.CloudServer.CloudNetwork.UpdatedAt = updatedTime
+			}
+		}
+	}
+
+	return &res, err
+}
+
+// DeleteNodeResult represents the result of a DeleteNode operation.
+type DeleteNodeResult struct {
+	gophercloud.ErrResult
+}
+
+// CreateNodesResult represents the result of a CreateNodes operation.
+type CreateNodesResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a slice of Nodes from a CreateNodesResult.
+func (r CreateNodesResult) Extract() ([]Node, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	var res []Node
+	err := mapstructure.Decode(r.Body, &res)
+
+	b := r.Body.([]interface{})
+	for i := range b {
+		if date, ok := b[i].(map[string]interface{})["created"]; ok && date != nil {
+			t, err := time.Parse(time.RFC3339, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			res[i].CreatedAt = t
+		}
+		if date, ok := b[i].(map[string]interface{})["updated"]; ok && date != nil {
+			t, err := time.Parse(time.RFC3339, date.(string))
+			if err != nil {
+				return nil, err
+			}
+			res[i].UpdatedAt = t
+		}
+	}
+
+	return res, err
+}
+
+// DeleteNodesResult represents the result of a DeleteNodes operation.
+type DeleteNodesResult struct {
+	gophercloud.ErrResult
+}
+
+// NodeDetailsForServer represents a load balancer pool node associated with a RackConnect configuration
+// with all its details for a particular server.
+type NodeDetailsForServer struct {
+	// The unique ID of the LB node.
+	ID string `mapstructure:"id"`
+	// The load balancer pool.
+	LoadBalancerPool Pool `mapstructure:"load_balancer_pool"`
+	// The status of the LB pool.
+	Status string `mapstructure:"status"`
+	// The details of the status of the LB pool.
+	StatusDetail string `mapstructure:"status_detail"`
+	// The time the LB node was created.
+	CreatedAt time.Time `mapstructure:"-"`
+	// The time the LB node was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+}
+
+// NodeDetailsForServerPage is the page returned by a pager when traversing over a
+// collection of NodeDetailsForServer.
+type NodeDetailsForServerPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a NodeDetailsForServerPage contains no NodeDetailsForServer.
+func (r NodeDetailsForServerPage) IsEmpty() (bool, error) {
+	n, err := ExtractNodesDetailsForServer(r)
+	if err != nil {
+		return true, err
+	}
+	return len(n) == 0, nil
+}
+
+// ExtractNodesDetailsForServer extracts and returns a slice of NodeDetailsForServer. It is used while iterating over
+// an lbpools.ListNodesDetailsForServer call.
+func ExtractNodesDetailsForServer(page pagination.Page) ([]NodeDetailsForServer, error) {
+	var res []NodeDetailsForServer
+	casted := page.(NodeDetailsForServerPage).Body
+	err := mapstructure.Decode(casted, &res)
+
+	var rawNodesDetails []interface{}
+	switch casted.(type) {
+	case interface{}:
+		rawNodesDetails = casted.([]interface{})
+	default:
+		return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i := range rawNodesDetails {
+		thisNodeDetails := (rawNodesDetails[i]).(map[string]interface{})
+
+		if t, ok := thisNodeDetails["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].CreatedAt = creationTime
+		}
+
+		if t, ok := thisNodeDetails["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].UpdatedAt = updatedTime
+		}
+	}
+
+	return res, err
+}
diff --git a/rackspace/rackconnect/v3/lbpools/urls.go b/rackspace/rackconnect/v3/lbpools/urls.go
new file mode 100644
index 0000000..c238239
--- /dev/null
+++ b/rackspace/rackconnect/v3/lbpools/urls.go
@@ -0,0 +1,49 @@
+package lbpools
+
+import "github.com/rackspace/gophercloud"
+
+var root = "load_balancer_pools"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(root)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(root, id)
+}
+
+func listNodesURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(root, id, "nodes")
+}
+
+func createNodeURL(c *gophercloud.ServiceClient, id string) string {
+	return listNodesURL(c, id)
+}
+
+func listNodesDetailsURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(root, id, "nodes", "details")
+}
+
+func nodeURL(c *gophercloud.ServiceClient, poolID, nodeID string) string {
+	return c.ServiceURL(root, poolID, "nodes", nodeID)
+}
+
+func deleteNodeURL(c *gophercloud.ServiceClient, poolID, nodeID string) string {
+	return nodeURL(c, poolID, nodeID)
+}
+
+func nodeDetailsURL(c *gophercloud.ServiceClient, poolID, nodeID string) string {
+	return c.ServiceURL(root, poolID, "nodes", nodeID, "details")
+}
+
+func createNodesURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(root, "nodes")
+}
+
+func deleteNodesURL(c *gophercloud.ServiceClient) string {
+	return createNodesURL(c)
+}
+
+func listNodesForServerURL(c *gophercloud.ServiceClient, serverID string) string {
+	return c.ServiceURL(root, "nodes", "details?cloud_server_id="+serverID)
+}
diff --git a/rackspace/rackconnect/v3/publicips/requests.go b/rackspace/rackconnect/v3/publicips/requests.go
new file mode 100644
index 0000000..1164260
--- /dev/null
+++ b/rackspace/rackconnect/v3/publicips/requests.go
@@ -0,0 +1,50 @@
+package publicips
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns all public IPs.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(c)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return PublicIPPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Create adds a public IP to the server with the given serverID.
+func Create(c *gophercloud.ServiceClient, serverID string) CreateResult {
+	var res CreateResult
+	reqBody := map[string]interface{}{
+		"cloud_server": map[string]string{
+			"id": serverID,
+		},
+	}
+	_, res.Err = c.Post(createURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// ListForServer returns all public IPs for the server with the given serverID.
+func ListForServer(c *gophercloud.ServiceClient, serverID string) pagination.Pager {
+	url := listForServerURL(c, serverID)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return PublicIPPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves the public IP with the given id.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
+	return res
+}
+
+// Delete removes the public IP with the given id.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(deleteURL(c, id), nil)
+	return res
+}
diff --git a/rackspace/rackconnect/v3/publicips/requests_test.go b/rackspace/rackconnect/v3/publicips/requests_test.go
new file mode 100644
index 0000000..61da2b0
--- /dev/null
+++ b/rackspace/rackconnect/v3/publicips/requests_test.go
@@ -0,0 +1,378 @@
+package publicips
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListIPs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/public_ips", 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")
+		fmt.Fprintf(w, `[
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "cloud_network": {
+            "cidr": "192.168.100.0/24",
+            "created": "2014-05-25T01:23:42Z",
+            "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+            "name": "RC-CLOUD",
+            "private_ip_v4": "192.168.100.5",
+            "updated": "2014-05-25T02:28:44Z"
+          },
+          "created": "2014-05-30T02:18:42Z",
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+          "name": "RCv3TestServer1",
+          "updated": "2014-05-30T02:19:18Z"
+        },
+        "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+        "public_ip_v4": "203.0.113.110",
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }
+    ]`)
+	})
+
+	expected := []PublicIP{
+		PublicIP{
+			ID:         "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+			PublicIPv4: "203.0.113.110",
+			CreatedAt:  time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+			CloudServer: struct {
+				ID           string `mapstructure:"id"`
+				Name         string `mapstructure:"name"`
+				CloudNetwork struct {
+					ID          string    `mapstructure:"id"`
+					Name        string    `mapstructure:"name"`
+					PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+					CIDR        string    `mapstructure:"cidr"`
+					CreatedAt   time.Time `mapstructure:"-"`
+					UpdatedAt   time.Time `mapstructure:"-"`
+				} `mapstructure:"cloud_network"`
+				CreatedAt time.Time `mapstructure:"-"`
+				UpdatedAt time.Time `mapstructure:"-"`
+			}{
+				ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+				CloudNetwork: struct {
+					ID          string    `mapstructure:"id"`
+					Name        string    `mapstructure:"name"`
+					PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+					CIDR        string    `mapstructure:"cidr"`
+					CreatedAt   time.Time `mapstructure:"-"`
+					UpdatedAt   time.Time `mapstructure:"-"`
+				}{
+					ID:          "07426958-1ebf-4c38-b032-d456820ca21a",
+					CIDR:        "192.168.100.0/24",
+					CreatedAt:   time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+					Name:        "RC-CLOUD",
+					PrivateIPv4: "192.168.100.5",
+					UpdatedAt:   time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+				},
+				CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC),
+				Name:      "RCv3TestServer1",
+				UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC),
+			},
+			Status:    "ACTIVE",
+			UpdatedAt: time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+		},
+	}
+
+	count := 0
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPublicIPs(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestCreateIP(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/public_ips", 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.TestJSONRequest(t, r, `
+      {
+        "cloud_server": {
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2"
+        }
+      }
+    `)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "cloud_network": {
+            "cidr": "192.168.100.0/24",
+            "created": "2014-05-25T01:23:42Z",
+            "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+            "name": "RC-CLOUD",
+            "private_ip_v4": "192.168.100.5",
+            "updated": "2014-05-25T02:28:44Z"
+          },
+          "created": "2014-05-30T02:18:42Z",
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+          "name": "RCv3TestServer1",
+          "updated": "2014-05-30T02:19:18Z"
+        },
+        "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+        "status": "ADDING"
+      }`)
+	})
+
+	expected := &PublicIP{
+		CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+		CloudServer: struct {
+			ID           string `mapstructure:"id"`
+			Name         string `mapstructure:"name"`
+			CloudNetwork struct {
+				ID          string    `mapstructure:"id"`
+				Name        string    `mapstructure:"name"`
+				PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+				CIDR        string    `mapstructure:"cidr"`
+				CreatedAt   time.Time `mapstructure:"-"`
+				UpdatedAt   time.Time `mapstructure:"-"`
+			} `mapstructure:"cloud_network"`
+			CreatedAt time.Time `mapstructure:"-"`
+			UpdatedAt time.Time `mapstructure:"-"`
+		}{
+			ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			CloudNetwork: struct {
+				ID          string    `mapstructure:"id"`
+				Name        string    `mapstructure:"name"`
+				PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+				CIDR        string    `mapstructure:"cidr"`
+				CreatedAt   time.Time `mapstructure:"-"`
+				UpdatedAt   time.Time `mapstructure:"-"`
+			}{
+				ID:          "07426958-1ebf-4c38-b032-d456820ca21a",
+				CIDR:        "192.168.100.0/24",
+				CreatedAt:   time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+				Name:        "RC-CLOUD",
+				PrivateIPv4: "192.168.100.5",
+				UpdatedAt:   time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+			},
+			CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC),
+			Name:      "RCv3TestServer1",
+			UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC),
+		},
+		ID:     "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+		Status: "ADDING",
+	}
+
+	actual, err := Create(fake.ServiceClient(), "d95ae0c4-6ab8-4873-b82f-f8433840cff2").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestGetIP(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/public_ips/2d0f586b-37a7-4ae0-adac-2743d5feb450", 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")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+      {
+        "created": "2014-05-30T03:23:42Z",
+        "cloud_server": {
+          "cloud_network": {
+            "cidr": "192.168.100.0/24",
+            "created": "2014-05-25T01:23:42Z",
+            "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+            "name": "RC-CLOUD",
+            "private_ip_v4": "192.168.100.5",
+            "updated": "2014-05-25T02:28:44Z"
+          },
+          "created": "2014-05-30T02:18:42Z",
+          "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+          "name": "RCv3TestServer1",
+          "updated": "2014-05-30T02:19:18Z"
+        },
+        "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+        "public_ip_v4": "203.0.113.110",
+        "status": "ACTIVE",
+        "status_detail": null,
+        "updated": "2014-05-30T03:24:18Z"
+      }`)
+	})
+
+	expected := &PublicIP{
+		CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+		CloudServer: struct {
+			ID           string `mapstructure:"id"`
+			Name         string `mapstructure:"name"`
+			CloudNetwork struct {
+				ID          string    `mapstructure:"id"`
+				Name        string    `mapstructure:"name"`
+				PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+				CIDR        string    `mapstructure:"cidr"`
+				CreatedAt   time.Time `mapstructure:"-"`
+				UpdatedAt   time.Time `mapstructure:"-"`
+			} `mapstructure:"cloud_network"`
+			CreatedAt time.Time `mapstructure:"-"`
+			UpdatedAt time.Time `mapstructure:"-"`
+		}{
+			ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+			CloudNetwork: struct {
+				ID          string    `mapstructure:"id"`
+				Name        string    `mapstructure:"name"`
+				PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+				CIDR        string    `mapstructure:"cidr"`
+				CreatedAt   time.Time `mapstructure:"-"`
+				UpdatedAt   time.Time `mapstructure:"-"`
+			}{
+				ID:          "07426958-1ebf-4c38-b032-d456820ca21a",
+				CIDR:        "192.168.100.0/24",
+				CreatedAt:   time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+				Name:        "RC-CLOUD",
+				PrivateIPv4: "192.168.100.5",
+				UpdatedAt:   time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+			},
+			CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC),
+			Name:      "RCv3TestServer1",
+			UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC),
+		},
+		ID:         "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+		Status:     "ACTIVE",
+		PublicIPv4: "203.0.113.110",
+		UpdatedAt:  time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+	}
+
+	actual, err := Get(fake.ServiceClient(), "2d0f586b-37a7-4ae0-adac-2743d5feb450").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteIP(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/public_ips/2d0f586b-37a7-4ae0-adac-2743d5feb450", 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.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := Delete(client.ServiceClient(), "2d0f586b-37a7-4ae0-adac-2743d5feb450").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestListForServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	th.Mux.HandleFunc("/public_ips", 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")
+		fmt.Fprintf(w, `
+    [
+    {
+      "created": "2014-05-30T03:23:42Z",
+      "cloud_server": {
+        "cloud_network": {
+          "cidr": "192.168.100.0/24",
+          "created": "2014-05-25T01:23:42Z",
+          "id": "07426958-1ebf-4c38-b032-d456820ca21a",
+          "name": "RC-CLOUD",
+          "private_ip_v4": "192.168.100.5",
+          "updated": "2014-05-25T02:28:44Z"
+        },
+        "created": "2014-05-30T02:18:42Z",
+        "id": "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+        "name": "RCv3TestServer1",
+        "updated": "2014-05-30T02:19:18Z"
+      },
+      "id": "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+      "public_ip_v4": "203.0.113.110",
+      "status": "ACTIVE",
+      "updated": "2014-05-30T03:24:18Z"
+    }
+    ]`)
+	})
+
+	expected := []PublicIP{
+		PublicIP{
+			CreatedAt: time.Date(2014, 5, 30, 3, 23, 42, 0, time.UTC),
+			CloudServer: struct {
+				ID           string `mapstructure:"id"`
+				Name         string `mapstructure:"name"`
+				CloudNetwork struct {
+					ID          string    `mapstructure:"id"`
+					Name        string    `mapstructure:"name"`
+					PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+					CIDR        string    `mapstructure:"cidr"`
+					CreatedAt   time.Time `mapstructure:"-"`
+					UpdatedAt   time.Time `mapstructure:"-"`
+				} `mapstructure:"cloud_network"`
+				CreatedAt time.Time `mapstructure:"-"`
+				UpdatedAt time.Time `mapstructure:"-"`
+			}{
+				ID: "d95ae0c4-6ab8-4873-b82f-f8433840cff2",
+				CloudNetwork: struct {
+					ID          string    `mapstructure:"id"`
+					Name        string    `mapstructure:"name"`
+					PrivateIPv4 string    `mapstructure:"private_ip_v4"`
+					CIDR        string    `mapstructure:"cidr"`
+					CreatedAt   time.Time `mapstructure:"-"`
+					UpdatedAt   time.Time `mapstructure:"-"`
+				}{
+					ID:          "07426958-1ebf-4c38-b032-d456820ca21a",
+					CIDR:        "192.168.100.0/24",
+					CreatedAt:   time.Date(2014, 5, 25, 1, 23, 42, 0, time.UTC),
+					Name:        "RC-CLOUD",
+					PrivateIPv4: "192.168.100.5",
+					UpdatedAt:   time.Date(2014, 5, 25, 2, 28, 44, 0, time.UTC),
+				},
+				CreatedAt: time.Date(2014, 5, 30, 2, 18, 42, 0, time.UTC),
+				Name:      "RCv3TestServer1",
+				UpdatedAt: time.Date(2014, 5, 30, 2, 19, 18, 0, time.UTC),
+			},
+			ID:         "2d0f586b-37a7-4ae0-adac-2743d5feb450",
+			Status:     "ACTIVE",
+			PublicIPv4: "203.0.113.110",
+			UpdatedAt:  time.Date(2014, 5, 30, 3, 24, 18, 0, time.UTC),
+		},
+	}
+	count := 0
+	err := ListForServer(fake.ServiceClient(), "d95ae0c4-6ab8-4873-b82f-f8433840cff2").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPublicIPs(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, expected, actual)
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
diff --git a/rackspace/rackconnect/v3/publicips/results.go b/rackspace/rackconnect/v3/publicips/results.go
new file mode 100644
index 0000000..132cf77
--- /dev/null
+++ b/rackspace/rackconnect/v3/publicips/results.go
@@ -0,0 +1,221 @@
+package publicips
+
+import (
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// PublicIP represents a public IP address.
+type PublicIP struct {
+	// The unique ID of the public IP.
+	ID string `mapstructure:"id"`
+	// The IPv4 address of the public IP.
+	PublicIPv4 string `mapstructure:"public_ip_v4"`
+	// The cloud server (node) of the public IP.
+	CloudServer struct {
+		// The cloud server ID.
+		ID string `mapstructure:"id"`
+		// The name of the server.
+		Name string `mapstructure:"name"`
+		// The cloud network for the cloud server.
+		CloudNetwork struct {
+			// The network ID.
+			ID string `mapstructure:"id"`
+			// The network name.
+			Name string `mapstructure:"name"`
+			// The network's private IPv4 address.
+			PrivateIPv4 string `mapstructure:"private_ip_v4"`
+			// The IP range for the network.
+			CIDR string `mapstructure:"cidr"`
+			// The datetime the network was created.
+			CreatedAt time.Time `mapstructure:"-"`
+			// The last datetime the network was updated.
+			UpdatedAt time.Time `mapstructure:"-"`
+		} `mapstructure:"cloud_network"`
+		// The datetime the server was created.
+		CreatedAt time.Time `mapstructure:"-"`
+		// The datetime the server was last updated.
+		UpdatedAt time.Time `mapstructure:"-"`
+	} `mapstructure:"cloud_server"`
+	// The status of the public IP.
+	Status string `mapstructure:"status"`
+	// The details of the status of the public IP.
+	StatusDetail string `mapstructure:"status_detail"`
+	// The time the public IP was created.
+	CreatedAt time.Time `mapstructure:"-"`
+	// The time the public IP was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+}
+
+// PublicIPPage is the page returned by a pager when traversing over a
+// collection of PublicIPs.
+type PublicIPPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a PublicIPPage contains no PublicIPs.
+func (r PublicIPPage) IsEmpty() (bool, error) {
+	n, err := ExtractPublicIPs(r)
+	if err != nil {
+		return true, err
+	}
+	return len(n) == 0, nil
+}
+
+// ExtractPublicIPs extracts and returns a slice of PublicIPs. It is used while iterating over
+// a publicips.List call.
+func ExtractPublicIPs(page pagination.Page) ([]PublicIP, error) {
+	var res []PublicIP
+	casted := page.(PublicIPPage).Body
+	err := mapstructure.Decode(casted, &res)
+
+	var rawNodesDetails []interface{}
+	switch casted.(type) {
+	case interface{}:
+		rawNodesDetails = casted.([]interface{})
+	default:
+		return res, fmt.Errorf("Unknown type: %v", reflect.TypeOf(casted))
+	}
+
+	for i := range rawNodesDetails {
+		thisNodeDetails := (rawNodesDetails[i]).(map[string]interface{})
+
+		if t, ok := thisNodeDetails["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].CreatedAt = creationTime
+		}
+
+		if t, ok := thisNodeDetails["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return res, err
+			}
+			res[i].UpdatedAt = updatedTime
+		}
+
+		if cs, ok := thisNodeDetails["cloud_server"].(map[string]interface{}); ok {
+			if t, ok := cs["created"].(string); ok && t != "" {
+				creationTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return res, err
+				}
+				res[i].CloudServer.CreatedAt = creationTime
+			}
+			if t, ok := cs["updated"].(string); ok && t != "" {
+				updatedTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return res, err
+				}
+				res[i].CloudServer.UpdatedAt = updatedTime
+			}
+			if cn, ok := cs["cloud_network"].(map[string]interface{}); ok {
+				if t, ok := cn["created"].(string); ok && t != "" {
+					creationTime, err := time.Parse(time.RFC3339, t)
+					if err != nil {
+						return res, err
+					}
+					res[i].CloudServer.CloudNetwork.CreatedAt = creationTime
+				}
+				if t, ok := cn["updated"].(string); ok && t != "" {
+					updatedTime, err := time.Parse(time.RFC3339, t)
+					if err != nil {
+						return res, err
+					}
+					res[i].CloudServer.CloudNetwork.UpdatedAt = updatedTime
+				}
+			}
+		}
+	}
+
+	return res, err
+}
+
+// PublicIPResult represents a result that can be extracted into a PublicIP.
+type PublicIPResult struct {
+	gophercloud.Result
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	PublicIPResult
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	PublicIPResult
+}
+
+// Extract is a function that extracts a PublicIP from a PublicIPResult.
+func (r PublicIPResult) Extract() (*PublicIP, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	var res PublicIP
+	err := mapstructure.Decode(r.Body, &res)
+
+	b := r.Body.(map[string]interface{})
+
+	if date, ok := b["created"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.CreatedAt = t
+	}
+
+	if date, ok := b["updated"]; ok && date != nil {
+		t, err := time.Parse(time.RFC3339, date.(string))
+		if err != nil {
+			return nil, err
+		}
+		res.UpdatedAt = t
+	}
+
+	if cs, ok := b["cloud_server"].(map[string]interface{}); ok {
+		if t, ok := cs["created"].(string); ok && t != "" {
+			creationTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return &res, err
+			}
+			res.CloudServer.CreatedAt = creationTime
+		}
+		if t, ok := cs["updated"].(string); ok && t != "" {
+			updatedTime, err := time.Parse(time.RFC3339, t)
+			if err != nil {
+				return &res, err
+			}
+			res.CloudServer.UpdatedAt = updatedTime
+		}
+		if cn, ok := cs["cloud_network"].(map[string]interface{}); ok {
+			if t, ok := cn["created"].(string); ok && t != "" {
+				creationTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return &res, err
+				}
+				res.CloudServer.CloudNetwork.CreatedAt = creationTime
+			}
+			if t, ok := cn["updated"].(string); ok && t != "" {
+				updatedTime, err := time.Parse(time.RFC3339, t)
+				if err != nil {
+					return &res, err
+				}
+				res.CloudServer.CloudNetwork.UpdatedAt = updatedTime
+			}
+		}
+	}
+
+	return &res, err
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/rackspace/rackconnect/v3/publicips/urls.go b/rackspace/rackconnect/v3/publicips/urls.go
new file mode 100644
index 0000000..6f310be
--- /dev/null
+++ b/rackspace/rackconnect/v3/publicips/urls.go
@@ -0,0 +1,25 @@
+package publicips
+
+import "github.com/rackspace/gophercloud"
+
+var root = "public_ips"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(root)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(root)
+}
+
+func listForServerURL(c *gophercloud.ServiceClient, serverID string) string {
+	return c.ServiceURL(root + "?cloud_server_id=" + serverID)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(root, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
diff --git a/results.go b/results.go
new file mode 100644
index 0000000..27fd1b6
--- /dev/null
+++ b/results.go
@@ -0,0 +1,153 @@
+package gophercloud
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+/*
+Result is an internal type to be used by individual resource packages, but its
+methods will be available on a wide variety of user-facing embedding types.
+
+It acts as a base struct that other Result types, returned from request
+functions, can embed for convenience. All Results capture basic information
+from the HTTP transaction that was performed, including the response body,
+HTTP headers, and any errors that happened.
+
+Generally, each Result type will have an Extract method that can be used to
+further interpret the result's payload in a specific context. Extensions or
+providers can then provide additional extraction functions to pull out
+provider- or extension-specific information as well.
+*/
+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 the Extract call.
+	Err error
+}
+
+// PrettyPrintJSON creates a string containing the full response body as
+// pretty-printed JSON. It's useful for capturing test fixtures and for
+// debugging extraction bugs. If you include its output in an issue related to
+// a buggy extraction function, we will all love you forever.
+func (r Result) PrettyPrintJSON() string {
+	pretty, err := json.MarshalIndent(r.Body, "", "  ")
+	if err != nil {
+		panic(err.Error())
+	}
+	return string(pretty)
+}
+
+// ErrResult is an internal type to be used by individual resource packages, but
+// its methods will be available on a wide variety of user-facing embedding
+// types.
+//
+// It represents results that only contain a potential error and
+// nothing else. Usually, if the operation executed successfully, the Err field
+// will be nil; otherwise it will be stocked with a relevant error. Use the
+// ExtractErr method
+// to cleanly pull it out.
+type ErrResult struct {
+	Result
+}
+
+// ExtractErr is a function that extracts error information, or nil, from a result.
+func (r ErrResult) ExtractErr() error {
+	return r.Err
+}
+
+/*
+HeaderResult is an internal type to be used by individual resource packages, but
+its methods will be available on a wide variety of user-facing embedding types.
+
+It represents a result that only contains an error (possibly nil) and an
+http.Header. This is used, for example, by the objectstorage packages in
+openstack, because most of the operations don't return response bodies, but do
+have relevant information in headers.
+*/
+type HeaderResult struct {
+	Result
+}
+
+// ExtractHeader will return the http.Header and error from the HeaderResult.
+//
+//   header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
+func (hr HeaderResult) ExtractHeader() (http.Header, error) {
+	return hr.Header, hr.Err
+}
+
+// DecodeHeader is a function that decodes a header (usually of type map[string]interface{}) to
+// another type (usually a struct). This function is used by the objectstorage package to give
+// users access to response headers without having to query a map. A DecodeHookFunction is used,
+// because OpenStack-based clients return header values as arrays (Go slices).
+func DecodeHeader(from, to interface{}) error {
+	config := &mapstructure.DecoderConfig{
+		DecodeHook: func(from, to reflect.Kind, data interface{}) (interface{}, error) {
+			if from == reflect.Slice {
+				return data.([]string)[0], nil
+			}
+			return data, nil
+		},
+		Result:           to,
+		WeaklyTypedInput: true,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return err
+	}
+	if err := decoder.Decode(from); err != nil {
+		return err
+	}
+	return nil
+}
+
+// RFC3339Milli describes a common time format used by some API responses.
+const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
+
+// Time format used in cloud orchestration
+const STACK_TIME_FMT = "2006-01-02T15:04:05"
+
+/*
+Link is an internal type to be used in packages of collection resources that are
+paginated in a certain way.
+
+It's a response substructure common to many paginated collection results that is
+used to point to related pages. Usually, the one we care about is the one with
+Rel field set to "next".
+*/
+type Link struct {
+	Href string `mapstructure:"href"`
+	Rel  string `mapstructure:"rel"`
+}
+
+/*
+ExtractNextURL is an internal function useful for packages of collection
+resources that are paginated in a certain way.
+
+It attempts attempts to extract the "next" URL from slice of Link structs, or
+"" if no such URL is present.
+*/
+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/script/bootstrap b/script/bootstrap
new file mode 100755
index 0000000..6bae6e8
--- /dev/null
+++ b/script/bootstrap
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# This script helps new contributors set up their local workstation for
+# gophercloud development and contributions.
+
+# Create the environment
+export GOPATH=$HOME/go/gophercloud
+mkdir -p $GOPATH
+
+# Download gophercloud into that environment
+go get github.com/rackspace/gophercloud
+cd $GOPATH/src/github.com/rackspace/gophercloud
+git checkout master
+
+# Write out the env.sh convenience file.
+cd $GOPATH
+cat <<EOF >env.sh
+#!/bin/bash
+export GOPATH=$(pwd)
+export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud
+EOF
+chmod a+x env.sh
+
+# Make changes immediately available as a convenience.
+. ./env.sh
+
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/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/fixture/helper.go b/testhelper/fixture/helper.go
new file mode 100644
index 0000000..d54355d
--- /dev/null
+++ b/testhelper/fixture/helper.go
@@ -0,0 +1,31 @@
+package fixture
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func SetupHandler(t *testing.T, url, method, requestBody, responseBody string, status int) {
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, method)
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		if requestBody != "" {
+			th.TestJSONRequest(t, r, requestBody)
+		}
+
+		if responseBody != "" {
+			w.Header().Add("Content-Type", "application/json")
+		}
+
+		w.WriteHeader(status)
+
+		if responseBody != "" {
+			fmt.Fprintf(w, responseBody)
+		}
+	})
+}
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/util.go b/util.go
new file mode 100644
index 0000000..3d6a4e4
--- /dev/null
+++ b/util.go
@@ -0,0 +1,82 @@
+package gophercloud
+
+import (
+	"errors"
+	"net/url"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+// WaitFor polls a predicate function, once per second, up to a timeout limit.
+// It usually does this to wait for a resource to transition to a certain state.
+// Resource packages will wrap this in a more convenient function that's
+// specific to a certain resource, but it can also be useful on its own.
+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 is an internal function to be used by provider clients.
+//
+// It ensures that each endpoint URL has a closing `/`, as expected by
+// ServiceClient's methods.
+func NormalizeURL(url string) string {
+	if !strings.HasSuffix(url, "/") {
+		return url + "/"
+	}
+	return url
+}
+
+// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as
+// a reference in the filesystem, if necessary. basePath is assumed to contain
+// either '.' when first used, or the file:// type fqdn of the parent resource.
+// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml
+func NormalizePathURL(basePath, rawPath string) (string, error) {
+	u, err := url.Parse(rawPath)
+	if err != nil {
+		return "", err
+	}
+	// if a scheme is defined, it must be a fqdn already
+	if u.Scheme != "" {
+		return u.String(), nil
+	}
+	// if basePath is a url, then child resources are assumed to be relative to it
+	bu, err := url.Parse(basePath)
+	if err != nil {
+		return "", err
+	}
+	var basePathSys, absPathSys string
+	if bu.Scheme != "" {
+		basePathSys = filepath.FromSlash(bu.Path)
+		absPathSys = filepath.Join(basePathSys, rawPath)
+		bu.Path = filepath.ToSlash(absPathSys)
+		return bu.String(), nil
+	}
+
+	absPathSys = filepath.Join(basePath, rawPath)
+	u.Path = filepath.ToSlash(absPathSys)
+	if err != nil {
+		return "", err
+	}
+	u.Scheme = "file"
+	return u.String(), nil
+
+}
diff --git a/util_test.go b/util_test.go
new file mode 100644
index 0000000..dcec77f
--- /dev/null
+++ b/util_test.go
@@ -0,0 +1,85 @@
+package gophercloud
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestWaitFor(t *testing.T) {
+	err := WaitFor(5, func() (bool, error) {
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
+
+func TestNormalizeURL(t *testing.T) {
+	urls := []string{
+		"NoSlashAtEnd",
+		"SlashAtEnd/",
+	}
+	expected := []string{
+		"NoSlashAtEnd/",
+		"SlashAtEnd/",
+	}
+	for i := 0; i < len(expected); i++ {
+		th.CheckEquals(t, expected[i], NormalizeURL(urls[i]))
+	}
+
+}
+
+func TestNormalizePathURL(t *testing.T) {
+	baseDir, _ := os.Getwd()
+
+	rawPath := "template.yaml"
+	basePath, _ := filepath.Abs(".")
+	result, _ := NormalizePathURL(basePath, rawPath)
+	expected := strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "template.yaml"}, "/")
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "http://www.google.com"
+	basePath, _ = filepath.Abs(".")
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = "http://www.google.com"
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml"
+	basePath, _ = filepath.Abs(".")
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "very/nested/file.yaml"}, "/")
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml"
+	basePath = "http://www.google.com"
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = "http://www.google.com/very/nested/file.yaml"
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml/"
+	basePath = "http://www.google.com/"
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = "http://www.google.com/very/nested/file.yaml"
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml"
+	basePath = "http://www.google.com/even/more"
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = "http://www.google.com/even/more/very/nested/file.yaml"
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml"
+	basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/")
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/")
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml/"
+	basePath = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more"}, "/")
+	result, _ = NormalizePathURL(basePath, rawPath)
+	expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/")
+	th.CheckEquals(t, expected, result)
+
+}