Merge remote-tracking branch 'target/master'
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..e9d5560
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,235 @@
+# Contributing to Gophercloud
+
+- [Getting started](#getting-started)
+- [Tests](#tests)
+- [Style guide](#basic-style-guide)
+- [3 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) but add `-tags "fixtures acceptance"` to
+get dependencies for unit and acceptance tests.
+
+   ```bash
+   go get -tags "fixtures acceptance" github.com/gophercloud/gophercloud
+   ```
+
+2. Move into the directory that houses your local repository:
+
+   ```bash
+   cd ${GOPATH}/src/github.com/gophercloud/gophercloud
+   ```
+
+3. Fork the `gophercloud/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.git
+   ```
+
+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 behavior
+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/gophercloud/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/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+  "github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+)
+
+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 := networks.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 behavior. 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 -tags fixtures ./...
+```
+
+To run all tests with verbose output:
+
+```bash
+go test -v -tags fixtures ./...
+```
+
+To run tests that match certain [build tags]():
+
+```bash
+go test -tags "fixtures foo bar" ./...
+```
+
+To run tests for a particular sub-package:
+
+```bash
+cd ./path/to/package && go test -tags fixtures .
+```
+
+## Style guide
+
+See [here](/STYLEGUIDE.md)
+
+## 3 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 `gophercloud/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. 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/gophercloud/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.
+
+### 2. Improving documentation
+The best source of documentation is on [godoc.org](http://godoc.org). It is
+automatically generated from the source code.
+
+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!
+
+###3. 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/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE
new file mode 100644
index 0000000..1451b81
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE
@@ -0,0 +1 @@
+Before starting a PR, please read the [style guide](https://github.com/gophercloud/gophercloud/blob/master/STYLEGUIDE.md).
diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
new file mode 100644
index 0000000..43aafa0
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE
@@ -0,0 +1,9 @@
+Prior to a PR being reviewed, there needs to be a Github issue that the PR
+addresses. Replace the brackets and text below with that issue number.
+
+For #[PUT ISSUE NUMBER HERE]
+
+Links to the line numbers/files in the OpenStack source code that support the
+code in this PR:
+
+[PUT URLS HERE]
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ead8445
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+**/*.swp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b7cbdb9
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+language: go
+sudo: false
+install:
+- go get golang.org/x/crypto/ssh
+- go get -v -tags 'fixtures acceptance' ./...
+- go get github.com/wadey/gocovmerge
+- go get github.com/mattn/goveralls
+go:
+- 1.7
+- tip
+env:
+  global:
+  - secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ="
+script:
+- ./script/coverage
+after_success:
+- $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/CHANGELOG.md
diff --git a/FAQ.md b/FAQ.md
new file mode 100644
index 0000000..88a366a
--- /dev/null
+++ b/FAQ.md
@@ -0,0 +1,148 @@
+# Tips
+
+## Implementing default logging and re-authentication attempts
+
+You can implement custom logging and/or limit re-auth attempts by creating a custom HTTP client
+like the following and setting it as the provider client's HTTP Client (via the
+`gophercloud.ProviderClient.HTTPClient` field):
+
+```go
+//...
+
+// LogRoundTripper satisfies the http.RoundTripper interface and is used to
+// customize the default Gophercloud RoundTripper to allow for logging.
+type LogRoundTripper struct {
+	rt                http.RoundTripper
+	numReauthAttempts int
+}
+
+// newHTTPClient return a custom HTTP client that allows for logging relevant
+// information before and after the HTTP request.
+func newHTTPClient() http.Client {
+	return http.Client{
+		Transport: &LogRoundTripper{
+			rt: http.DefaultTransport,
+		},
+	}
+}
+
+// RoundTrip performs a round-trip HTTP request and logs relevant information about it.
+func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
+	glog.Infof("Request URL: %s\n", request.URL)
+
+	response, err := lrt.rt.RoundTrip(request)
+	if response == nil {
+		return nil, err
+	}
+
+	if response.StatusCode == http.StatusUnauthorized {
+		if lrt.numReauthAttempts == 3 {
+			return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.")
+		}
+		lrt.numReauthAttempts++
+	}
+
+	glog.Debugf("Response Status: %s\n", response.Status)
+
+	return response, nil
+}
+
+endpoint := "https://127.0.0.1/auth"
+pc := openstack.NewClient(endpoint)
+pc.HTTPClient = newHTTPClient()
+
+//...
+```
+
+
+## Implementing custom objects
+
+OpenStack request/response objects may differ among variable names or types.
+
+### Custom request objects
+
+To pass custom options to a request, implement the desired `<ACTION>OptsBuilder` interface. For
+example, to pass in
+
+```go
+type MyCreateServerOpts struct {
+	Name string
+	Size int
+}
+```
+
+to `servers.Create`, simply implement the `servers.CreateOptsBuilder` interface:
+
+```go
+func (o MyCreateServeropts) ToServerCreateMap() (map[string]interface{}, error) {
+	return map[string]interface{}{
+		"name": o.Name,
+		"size": o.Size,
+	}, nil
+}
+```
+
+create an instance of your custom options object, and pass it to `servers.Create`:
+
+```go
+// ...
+myOpts := MyCreateServerOpts{
+	Name: "s1",
+	Size: "100",
+}
+server, err := servers.Create(computeClient, myOpts).Extract()
+// ...
+```
+
+### Custom response objects
+
+Some OpenStack services have extensions. Extensions that are supported in Gophercloud can be
+combined to create a custom object:
+
+```go
+// ...
+type MyVolume struct {
+  volumes.Volume
+  tenantattr.VolumeExt
+}
+
+var v struct {
+  MyVolume `json:"volume"`
+}
+
+err := volumes.Get(client, volID).ExtractInto(&v)
+// ...
+```
+
+## Overriding default `UnmarshalJSON` method
+
+For some response objects, a field may be a custom type or may be allowed to take on
+different types. In these cases, overriding the default `UnmarshalJSON` method may be
+necessary. To do this, declare the JSON `struct` field tag as "-" and create an `UnmarshalJSON`
+method on the type:
+
+```go
+// ...
+type MyVolume struct {
+	ID string `json: "id"`
+	TimeCreated time.Time `json: "-"`
+}
+
+func (r *MyVolume) UnmarshalJSON(b []byte) error {
+	type tmp MyVolume
+	var s struct {
+		tmp
+		TimeCreated gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Volume(s.tmp)
+
+	r.TimeCreated = time.Time(s.CreatedAt)
+
+	return err
+}
+// ...
+```
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/MIGRATING.md b/MIGRATING.md
new file mode 100644
index 0000000..aa383c9
--- /dev/null
+++ b/MIGRATING.md
@@ -0,0 +1,32 @@
+# Compute
+
+## Floating IPs
+
+* `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingip` is now `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips`
+* `floatingips.Associate` and `floatingips.Disassociate` have been removed.
+* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP.
+
+## Security Groups
+
+* `secgroups.AddServerToGroup` is now `secgroups.AddServer`.
+* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`.
+
+## Servers
+
+* `servers.Reboot` now requires a `servers.RebootOpts` struct:
+
+  ```golang
+  rebootOpts := &servers.RebootOpts{
+          Type: servers.SoftReboot,
+  }
+  res := servers.Reboot(client, server.ID, rebootOpts)
+  ```
+
+# Identity
+
+## V3
+
+### Tokens
+
+* `Token.ExpiresAt` is now of type `gophercloud.JSONRFC3339Milli` instead of
+  `time.Time`
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..60ca479
--- /dev/null
+++ b/README.md
@@ -0,0 +1,143 @@
+# Gophercloud: an OpenStack SDK for Go
+[![Build Status](https://travis-ci.org/gophercloud/gophercloud.svg?branch=master)](https://travis-ci.org/gophercloud/gophercloud)
+[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=master)](https://coveralls.io/github/gophercloud/gophercloud?branch=master)
+
+Gophercloud is an OpenStack Go SDK.
+
+## Useful links
+
+* [Reference documentation](http://godoc.org/github.com/gophercloud/gophercloud)
+* [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/gophercloud/gophercloud
+
+# Edit your code to import relevant packages from "github.com/gophercloud/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
+* 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/gophercloud/gophercloud"
+  "github.com/gophercloud/gophercloud/openstack"
+  "github.com/gophercloud/gophercloud/openstack/utils"
+)
+
+// Option 1: Pass in the values yourself
+opts := gophercloud.AuthOptions{
+  IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
+  Username: "{username}",
+  Password: "{password}",
+}
+
+// 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/gophercloud/gophercloud/openstack/compute/v2/servers"
+
+server, err := servers.Create(client, servers.CreateOpts{
+  Name:      "My new server!",
+  FlavorRef: "flavor_id",
+  ImageRef:  "image_id",
+}).Extract()
+```
+
+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/gophercloud/gophercloud) struct).
+
+## Advanced Usage
+
+Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Gophercloud works.
+
+## Backwards-Compatibility Guarantees
+
+None. Vendor it and write tests covering the parts you use.
+
+## Contributing
+
+See the [contributing guide](./.github/CONTRIBUTING.md).
+
+## 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).
diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md
new file mode 100644
index 0000000..5b49ef4
--- /dev/null
+++ b/STYLEGUIDE.md
@@ -0,0 +1,74 @@
+
+## On Pull Requests
+
+- Before you start a PR there needs to be a Github issue and a discussion about it
+  on that issue with a core contributor, even if it's just a 'SGTM'.
+
+- A PR's description must reference the issue it closes with a `For <ISSUE NUMBER>` (e.g. For #293).
+
+- A PR's description must contain link(s) to the line(s) in the OpenStack
+  source code (on Github) that prove(s) the PR code to be valid. Links to documentation
+  are not good enough. The link(s) should be to a non-`master` branch. For example,
+  a pull request implementing the creation of a Neutron v2 subnet might put the
+  following link in the description:
+
+  https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749
+
+  From that link, a reviewer (or user) can verify the fields in the request/response
+  objects in the PR.
+
+- A PR that is in-progress should have `[wip]` in front of the PR's title. When
+  ready for review, remove the `[wip]` and ping a core contributor with an `@`.
+
+- Forcing PRs to be small can have the effect of users submitting PRs in a hierarchical chain, with
+  one depending on the next. If a PR depends on another one, it should have a [Pending #PRNUM]
+  prefix in the PR title. In addition, it will be the PR submitter's responsibility to remove the
+  [Pending #PRNUM] tag once the PR has been updated with the merged, dependent PR. That will
+  let reviewers know it is ready to review.
+
+- A PR should be small. Even if you intend on implementing an entire
+  service, a PR should only be one route of that service
+  (e.g. create server or get server, but not both).
+
+- Unless explicitly asked, do not squash commits in the middle of a review; only
+  append. It makes it difficult for the reviewer to see what's changed from one
+  review to the next.
+
+## On Code
+
+- In re design: follow as closely as is reasonable the code already in the library.
+  Most operations (e.g. create, delete) admit the same design.
+
+- Unit tests and acceptance (integration) tests must be written to cover each PR.
+  Tests for operations with several options (e.g. list, create) should include all
+  the options in the tests. This will allow users to verify an operation on their
+  own infrastructure and see an example of usage.
+
+- If in doubt, ask in-line on the PR.
+
+### File Structure
+
+- The following should be used in most cases:
+
+  - `requests.go`: contains all the functions that make HTTP requests and the
+    types associated with the HTTP request (parameters for URL, body, etc)
+  - `results.go`: contains all the response objects and their methods
+  - `urls.go`: contains the endpoints to which the requests are made
+
+### Naming
+
+- For methods on a type in `response.go`, the receiver should be named `r` and the
+  variable into which it will be unmarshalled `s`.
+
+- Functions in `requests.go`, with the exception of functions that return a
+  `pagination.Pager`, should be named returns of the name `r`.
+
+- Functions in `requests.go` that accept request bodies should accept as their
+  last parameter an `interface` named `<Action>OptsBuilder` (eg `CreateOptsBuilder`).
+  This `interface` should have at the least a method named `To<Resource><Action>Map`
+  (eg `ToPortCreateMap`).
+
+- Functions in `requests.go` that accept query strings should accept as their
+  last parameter an `interface` named `<Action>OptsBuilder` (eg `ListOptsBuilder`).
+  This `interface` should have at the least a method named `To<Resource><Action>Query`
+  (eg `ToServerListQuery`).
diff --git a/acceptance/README.md b/acceptance/README.md
new file mode 100644
index 0000000..620bdf8
--- /dev/null
+++ b/acceptance/README.md
@@ -0,0 +1,86 @@
+# 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|
+
+#### General
+
+|Name|Description|
+|---|---|
+|`OS_REGION_NAME`|The region you want your resources 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|
+|`OS_POOL_NAME`|The Pool from where to obtain Floating IPs|
+|`OS_NETWORK_NAME`|The network to launch instances on|
+
+#### Shared file systems
+|Name|Description|
+|---|---|
+|`OS_SHARE_NETWORK_ID`| The share network ID to use when creating shares|
+
+### 2. Run the test suite
+
+From the root directory, run:
+
+```
+./script/acceptancetest
+```
+
+Alternatively, add the following to your `.bashrc`:
+
+```bash
+gophercloudtest() {
+  if [[ -n $1 ]] && [[ -n $2 ]]; then
+    pushd  ~/go/src/github.com/gophercloud/gophercloud
+    go test -v -tags "fixtures acceptance" -run "$1" github.com/gophercloud/gophercloud/acceptance/openstack/$2 | tee ~/gophercloud.log
+    popd
+fi
+}
+```
+
+Then run either groups or individual tests by doing:
+
+```shell
+$ gophercloudtest TestFlavorsList compute/v2
+$ gophercloudtest TestFlavors compute/v2
+$ gophercloudtest Test compute/v2
+```
+
+### 3. Notes
+
+#### Compute Tests
+
+* In order to run the `TestBootFromVolumeMultiEphemeral` test, a flavor with ephemeral disk space must be used.
+* The `TestDefSecRules` tests require a compatible network driver and admin privileges.
diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go
new file mode 100644
index 0000000..aa57497
--- /dev/null
+++ b/acceptance/clients/clients.go
@@ -0,0 +1,324 @@
+// Package clients contains functions for creating OpenStack service clients
+// for use in acceptance tests. It also manages the required environment
+// variables to run the tests.
+package clients
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+)
+
+// AcceptanceTestChoices contains image and flavor selections for use by the acceptance tests.
+type AcceptanceTestChoices 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
+
+	// FloatingIPPool contains the name of the pool from where to obtain floating IPs.
+	FloatingIPPoolName string
+
+	// NetworkName is the name of a network to launch the instance on.
+	NetworkName string
+
+	// ExternalNetworkID is the network ID of the external network.
+	ExternalNetworkID string
+
+	// ShareNetworkID is the Manila Share network ID
+	ShareNetworkID string
+}
+
+// AcceptanceTestChoicesFromEnv populates a ComputeChoices struct from environment variables.
+// If any required state is missing, an `error` will be returned that enumerates the missing properties.
+func AcceptanceTestChoicesFromEnv() (*AcceptanceTestChoices, 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")
+	floatingIPPoolName := os.Getenv("OS_POOL_NAME")
+	externalNetworkID := os.Getenv("OS_EXTGW_ID")
+	shareNetworkID := os.Getenv("OS_SHARE_NETWORK_ID")
+
+	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 floatingIPPoolName == "" {
+		missing = append(missing, "OS_POOL_NAME")
+	}
+	if externalNetworkID == "" {
+		missing = append(missing, "OS_EXTGW_ID")
+	}
+	if networkName == "" {
+		networkName = "private"
+	}
+	if shareNetworkID == "" {
+		missing = append(missing, "OS_SHARE_NETWORK_ID")
+	}
+	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 &AcceptanceTestChoices{
+		ImageID:            imageID,
+		FlavorID:           flavorID,
+		FlavorIDResize:     flavorIDResize,
+		FloatingIPPoolName: floatingIPPoolName,
+		NetworkName:        networkName,
+		ExternalNetworkID:  externalNetworkID,
+		ShareNetworkID:     shareNetworkID,
+	}, nil
+}
+
+// NewBlockStorageV1Client returns a *ServiceClient for making calls
+// to the OpenStack Block Storage v1 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewBlockStorageV1Client() (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.AuthenticatedClient(ao)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewBlockStorageV2Client returns a *ServiceClient for making calls
+// to the OpenStack Block Storage v2 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewBlockStorageV2Client() (*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.NewBlockStorageV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewSharedFileSystemV2Client returns a *ServiceClient for making calls
+// to the OpenStack Shared File System v2 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewSharedFileSystemV2Client() (*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.NewSharedFileSystemV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewComputeV2Client returns a *ServiceClient for making calls
+// to the OpenStack Compute v2 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewComputeV2Client() (*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"),
+	})
+}
+
+// NewDNSV2Client returns a *ServiceClient for making calls
+// to the OpenStack Compute v2 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewDNSV2Client() (*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.NewDNSV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewIdentityV2Client returns a *ServiceClient for making calls
+// to the OpenStack Identity v2 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewIdentityV2Client() (*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.NewIdentityV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewIdentityV2AdminClient returns a *ServiceClient for making calls
+// to the Admin Endpoint of the OpenStack Identity v2 API. An error
+// will be returned if authentication or client creation was not possible.
+func NewIdentityV2AdminClient() (*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.NewIdentityV2(client, gophercloud.EndpointOpts{
+		Region:       os.Getenv("OS_REGION_NAME"),
+		Availability: gophercloud.AvailabilityAdmin,
+	})
+}
+
+// NewIdentityV2UnauthenticatedClient returns an unauthenticated *ServiceClient
+// for the OpenStack Identity v2 API. An error  will be returned if
+// authentication or client creation was not possible.
+func NewIdentityV2UnauthenticatedClient() (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.NewClient(ao.IdentityEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{})
+}
+
+// NewIdentityV3Client returns a *ServiceClient for making calls
+// to the OpenStack Identity v3 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewIdentityV3Client() (*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.NewIdentityV3(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewIdentityV3UnauthenticatedClient returns an unauthenticated *ServiceClient
+// for the OpenStack Identity v3 API. An error  will be returned if
+// authentication or client creation was not possible.
+func NewIdentityV3UnauthenticatedClient() (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.NewClient(ao.IdentityEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{})
+}
+
+// NewImageServiceV2Client returns a *ServiceClient for making calls to the
+// OpenStack Image v2 API. An error will be returned if authentication or
+// client creation was not possible.
+func NewImageServiceV2Client() (*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.NewImageServiceV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+// NewNetworkV2Client returns a *ServiceClient for making calls to the
+// OpenStack Networking v2 API. An error will be returned if authentication
+// or client creation was not possible.
+func NewNetworkV2Client() (*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.NewNetworkV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
diff --git a/acceptance/openstack/blockstorage/extensions/extensions.go b/acceptance/openstack/blockstorage/extensions/extensions.go
new file mode 100644
index 0000000..3a859c9
--- /dev/null
+++ b/acceptance/openstack/blockstorage/extensions/extensions.go
@@ -0,0 +1,152 @@
+// Package extensions contains common functions for creating block storage
+// resources that are extensions of the block storage API. See the `*_test.go`
+// files for example usages.
+package extensions
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+)
+
+// CreateUploadImage will upload volume it as volume-baked image. An name of new image or err will be
+// returned
+func CreateUploadImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (string, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume-backed image uploading in short mode.")
+	}
+
+	imageName := tools.RandomString("ACPTTEST", 16)
+	uploadImageOpts := volumeactions.UploadImageOpts{
+		ImageName: imageName,
+		Force:     true,
+	}
+
+	if err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).ExtractErr(); err != nil {
+		return "", err
+	}
+
+	t.Logf("Uploading volume %s as volume-backed image %s", volume.ID, imageName)
+
+	if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil {
+		return "", err
+	}
+
+	t.Logf("Uploaded volume %s as volume-backed image %s", volume.ID, imageName)
+
+	return imageName, nil
+
+}
+
+// DeleteUploadedImage deletes uploaded image. An error will be returned
+// if the deletion request failed.
+func DeleteUploadedImage(t *testing.T, client *gophercloud.ServiceClient, imageName string) error {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume-backed image removing in short mode.")
+	}
+
+	t.Logf("Getting image id for image name %s", imageName)
+
+	imageID, err := images.IDFromName(client, imageName)
+	if err != nil {
+		return err
+	}
+
+	t.Logf("Removing image %s", imageID)
+
+	err = images.Delete(client, imageID).ExtractErr()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// CreateVolumeAttach will attach a volume to an instance. An error will be
+// returned if the attachment failed.
+func CreateVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, server *servers.Server) error {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume attachment in short mode.")
+	}
+
+	attachOpts := volumeactions.AttachOpts{
+		MountPoint:   "/mnt",
+		Mode:         "rw",
+		InstanceUUID: server.ID,
+	}
+
+	t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID)
+
+	if err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr(); err != nil {
+		return err
+	}
+
+	if err := volumes.WaitForStatus(client, volume.ID, "in-use", 60); err != nil {
+		return err
+	}
+
+	t.Logf("Attached volume %s to server %s", volume.ID, server.ID)
+
+	return nil
+}
+
+// CreateVolumeReserve creates a volume reservation. An error will be returned
+// if the reservation failed.
+func CreateVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) error {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume reservation in short mode.")
+	}
+
+	t.Logf("Attempting to reserve volume %s", volume.ID)
+
+	if err := volumeactions.Reserve(client, volume.ID).ExtractErr(); err != nil {
+		return err
+	}
+
+	t.Logf("Reserved volume %s", volume.ID)
+
+	return nil
+}
+
+// DeleteVolumeAttach will detach a volume from an instance. A fatal error will
+// occur if the snapshot failed to be deleted. This works best when used as a
+// deferred function.
+func DeleteVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
+	t.Logf("Attepting to detach volume volume: %s", volume.ID)
+
+	detachOpts := volumeactions.DetachOpts{
+		AttachmentID: volume.Attachments[0].AttachmentID,
+	}
+
+	if err := volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr(); err != nil {
+		t.Fatalf("Unable to detach volume %s: %v", volume.ID, err)
+	}
+
+	if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil {
+		t.Fatalf("Volume %s failed to become unavailable in 60 seconds: %v", volume.ID, err)
+	}
+
+	t.Logf("Detached volume: %s", volume.ID)
+}
+
+// DeleteVolumeReserve deletes a volume reservation. A fatal error will occur
+// if the deletion request failed. This works best when used as a deferred
+// function.
+func DeleteVolumeReserve(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume reservation in short mode.")
+	}
+
+	t.Logf("Attempting to unreserve volume %s", volume.ID)
+
+	if err := volumeactions.Unreserve(client, volume.ID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to unreserve volume %s: %v", volume.ID, err)
+	}
+
+	t.Logf("Unreserved volume %s", volume.ID)
+}
diff --git a/acceptance/openstack/blockstorage/extensions/pkg.go b/acceptance/openstack/blockstorage/extensions/pkg.go
new file mode 100644
index 0000000..f18039d
--- /dev/null
+++ b/acceptance/openstack/blockstorage/extensions/pkg.go
@@ -0,0 +1,3 @@
+// The extensions package contains acceptance tests for the Openstack Cinder extensions service.
+
+package extensions
diff --git a/acceptance/openstack/blockstorage/extensions/volumeactions_test.go b/acceptance/openstack/blockstorage/extensions/volumeactions_test.go
new file mode 100644
index 0000000..d15d17a
--- /dev/null
+++ b/acceptance/openstack/blockstorage/extensions/volumeactions_test.go
@@ -0,0 +1,140 @@
+// +build acceptance blockstorage
+
+package extensions
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+
+	blockstorage "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2"
+	compute "github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2"
+)
+
+func TestVolumeActionsUploadImageDestroy(t *testing.T) {
+	blockClient, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+	computeClient, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	volume, err := blockstorage.CreateVolume(t, blockClient)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer blockstorage.DeleteVolume(t, blockClient, volume)
+
+	imageName, err := CreateUploadImage(t, blockClient, volume)
+	if err != nil {
+		t.Fatalf("Unable to upload volume-backed image: %v", err)
+	}
+
+	err = DeleteUploadedImage(t, computeClient, imageName)
+	if err != nil {
+		t.Fatalf("Unable to delete volume-backed image: %v", err)
+	}
+}
+
+func TestVolumeActionsAttachCreateDestroy(t *testing.T) {
+	blockClient, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	computeClient, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := compute.CreateServer(t, computeClient)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer compute.DeleteServer(t, computeClient, server)
+
+	volume, err := blockstorage.CreateVolume(t, blockClient)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer blockstorage.DeleteVolume(t, blockClient, volume)
+
+	err = CreateVolumeAttach(t, blockClient, volume, server)
+	if err != nil {
+		t.Fatalf("Unable to attach volume: %v", err)
+	}
+
+	newVolume, err := volumes.Get(blockClient, volume.ID).Extract()
+	if err != nil {
+		t.Fatal("Unable to get updated volume information: %v", err)
+	}
+
+	DeleteVolumeAttach(t, blockClient, newVolume)
+}
+
+func TestVolumeActionsReserveUnreserve(t *testing.T) {
+	client, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create blockstorage client: %v", err)
+	}
+
+	volume, err := blockstorage.CreateVolume(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer blockstorage.DeleteVolume(t, client, volume)
+
+	err = CreateVolumeReserve(t, client, volume)
+	if err != nil {
+		t.Fatalf("Unable to create volume reserve: %v", err)
+	}
+	defer DeleteVolumeReserve(t, client, volume)
+}
+
+// Note(jtopjian): I plan to work on this at some point, but it requires
+// setting up a server with iscsi utils.
+/*
+func TestVolumeConns(t *testing.T) {
+    client, err := newClient()
+    th.AssertNoErr(t, err)
+
+    t.Logf("Creating volume")
+    cv, err := volumes.Create(client, &volumes.CreateOpts{
+        Size: 1,
+        Name: "blockv2-volume",
+    }).Extract()
+    th.AssertNoErr(t, err)
+
+    defer func() {
+        err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+        th.AssertNoErr(t, err)
+
+        t.Logf("Deleting volume")
+        err = volumes.Delete(client, cv.ID).ExtractErr()
+        th.AssertNoErr(t, err)
+    }()
+
+    err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+    th.AssertNoErr(t, err)
+
+    connOpts := &volumeactions.ConnectorOpts{
+        IP:        "127.0.0.1",
+        Host:      "stack",
+        Initiator: "iqn.1994-05.com.redhat:17cf566367d2",
+        Multipath: false,
+        Platform:  "x86_64",
+        OSType:    "linux2",
+    }
+
+    t.Logf("Initializing connection")
+    _, err = volumeactions.InitializeConnection(client, cv.ID, connOpts).Extract()
+    th.AssertNoErr(t, err)
+
+    t.Logf("Terminating connection")
+    err = volumeactions.TerminateConnection(client, cv.ID, connOpts).ExtractErr()
+    th.AssertNoErr(t, err)
+}
+*/
diff --git a/acceptance/openstack/blockstorage/v1/blockstorage.go b/acceptance/openstack/blockstorage/v1/blockstorage.go
new file mode 100644
index 0000000..41f24e1
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/blockstorage.go
@@ -0,0 +1,142 @@
+// Package v1 contains common functions for creating block storage based
+// resources for use in acceptance tests. See the `*_test.go` files for
+// example usages.
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes"
+)
+
+// CreateSnapshot will create a volume snapshot based off of a given volume and
+// with a random name. An error will be returned if the snapshot failed to be
+// created.
+func CreateSnapshot(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (*snapshots.Snapshot, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires snapshot creation in short mode.")
+	}
+
+	snapshotName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create snapshot %s based on volume %s", snapshotName, volume.ID)
+
+	createOpts := snapshots.CreateOpts{
+		Name:     snapshotName,
+		VolumeID: volume.ID,
+	}
+
+	snapshot, err := snapshots.Create(client, createOpts).Extract()
+	if err != nil {
+		return snapshot, err
+	}
+
+	err = snapshots.WaitForStatus(client, snapshot.ID, "available", 60)
+	if err != nil {
+		return snapshot, err
+	}
+
+	return snapshot, nil
+}
+
+// CreateVolume will create a volume with a random name and size of 1GB. An
+// error will be returned if the volume was unable to be created.
+func CreateVolume(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume creation in short mode.")
+	}
+
+	volumeName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create volume: %s", volumeName)
+
+	createOpts := volumes.CreateOpts{
+		Size: 1,
+		Name: volumeName,
+	}
+
+	volume, err := volumes.Create(client, createOpts).Extract()
+	if err != nil {
+		return volume, err
+	}
+
+	err = volumes.WaitForStatus(client, volume.ID, "available", 60)
+	if err != nil {
+		return volume, err
+	}
+
+	return volume, nil
+}
+
+// CreateVolumeType will create a volume type with a random name. An error will
+// be returned if the volume type was unable to be created.
+func CreateVolumeType(t *testing.T, client *gophercloud.ServiceClient) (*volumetypes.VolumeType, error) {
+	volumeTypeName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create volume type: %s", volumeTypeName)
+
+	createOpts := volumetypes.CreateOpts{
+		Name: volumeTypeName,
+		ExtraSpecs: map[string]interface{}{
+			"capabilities": "ssd",
+			"priority":     3,
+		},
+	}
+
+	volumeType, err := volumetypes.Create(client, createOpts).Extract()
+	if err != nil {
+		return volumeType, err
+	}
+
+	return volumeType, nil
+}
+
+// DeleteSnapshot will delete a snapshot. A fatal error will occur if the
+// snapshot failed to be deleted. This works best when used as a deferred
+// function.
+func DeleteSnapshotshot(t *testing.T, client *gophercloud.ServiceClient, snapshot *snapshots.Snapshot) {
+	err := snapshots.Delete(client, snapshot.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete snapshot %s: %v", snapshot.ID, err)
+	}
+
+	// Volumes can't be deleted until their snapshots have been,
+	// so block up to 120 seconds for the snapshot to delete.
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := snapshots.Get(client, snapshot.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	if err != nil {
+		t.Fatalf("Unable to wait for snapshot to delete: %v", err)
+	}
+
+	t.Logf("Deleted snapshot: %s", snapshot.ID)
+}
+
+// DeleteVolume will delete a volume. A fatal error will occur if the volume
+// failed to be deleted. This works best when used as a deferred function.
+func DeleteVolume(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
+	err := volumes.Delete(client, volume.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete volume %s: %v", volume.ID, err)
+	}
+
+	t.Logf("Deleted volume: %s", volume.ID)
+}
+
+// DeleteVolumeType will delete a volume type. A fatal error will occur if the
+// volume type failed to be deleted. This works best when used as a deferred
+// function.
+func DeleteVolumeType(t *testing.T, client *gophercloud.ServiceClient, volumeType *volumetypes.VolumeType) {
+	err := volumetypes.Delete(client, volumeType.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete volume type %s: %v", volumeType.ID, err)
+	}
+
+	t.Logf("Deleted volume type: %s", volumeType.ID)
+}
diff --git a/acceptance/openstack/blockstorage/v1/pkg.go b/acceptance/openstack/blockstorage/v1/pkg.go
new file mode 100644
index 0000000..4efa6fb
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/pkg.go
@@ -0,0 +1,2 @@
+// Package v1 contains openstack cinder acceptance tests
+package v1
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..3545371
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,58 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots"
+)
+
+func TestSnapshotsList(t *testing.T) {
+	client, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	allPages, err := snapshots.List(client, snapshots.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve snapshots: %v", err)
+	}
+
+	allSnapshots, err := snapshots.ExtractSnapshots(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract snapshots: %v", err)
+	}
+
+	for _, snapshot := range allSnapshots {
+		tools.PrintResource(t, snapshot)
+	}
+}
+
+func TestSnapshotsCreateDelete(t *testing.T) {
+	client, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	volume, err := CreateVolume(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer DeleteVolume(t, client, volume)
+
+	snapshot, err := CreateSnapshot(t, client, volume)
+	if err != nil {
+		t.Fatalf("Unable to create snapshot: %v", err)
+	}
+	defer DeleteSnapshotshot(t, client, snapshot)
+
+	newSnapshot, err := snapshots.Get(client, snapshot.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve snapshot: %v", err)
+	}
+
+	tools.PrintResource(t, newSnapshot)
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..9a55500
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,52 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestVolumesList(t *testing.T) {
+	client, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	allPages, err := volumes.List(client, volumes.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve volumes: %v", err)
+	}
+
+	allVolumes, err := volumes.ExtractVolumes(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract volumes: %v", err)
+	}
+
+	for _, volume := range allVolumes {
+		tools.PrintResource(t, volume)
+	}
+}
+
+func TestVolumesCreateDestroy(t *testing.T) {
+	client, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create blockstorage client: %v", err)
+	}
+
+	volume, err := CreateVolume(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer DeleteVolume(t, client, volume)
+
+	newVolume, err := volumes.Get(client, volume.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve volume: %v", err)
+	}
+
+	tools.PrintResource(t, newVolume)
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..ace09bc
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,47 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes"
+)
+
+func TestVolumeTypesList(t *testing.T) {
+	client, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	allPages, err := volumetypes.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve volume types: %v", err)
+	}
+
+	allVolumeTypes, err := volumetypes.ExtractVolumeTypes(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract volume types: %v", err)
+	}
+
+	for _, volumeType := range allVolumeTypes {
+		tools.PrintResource(t, volumeType)
+	}
+}
+
+func TestVolumeTypesCreateDestroy(t *testing.T) {
+	client, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	volumeType, err := CreateVolumeType(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create volume type: %v", err)
+	}
+	defer DeleteVolumeType(t, client, volumeType)
+
+	tools.PrintResource(t, volumeType)
+}
diff --git a/acceptance/openstack/blockstorage/v2/blockstorage.go b/acceptance/openstack/blockstorage/v2/blockstorage.go
new file mode 100644
index 0000000..39fb7da
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v2/blockstorage.go
@@ -0,0 +1,86 @@
+// Package v2 contains common functions for creating block storage based
+// resources for use in acceptance tests. See the `*_test.go` files for
+// example usages.
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+)
+
+// CreateVolume will create a volume with a random name and size of 1GB. An
+// error will be returned if the volume was unable to be created.
+func CreateVolume(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume creation in short mode.")
+	}
+
+	volumeName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create volume: %s", volumeName)
+
+	createOpts := volumes.CreateOpts{
+		Size: 1,
+		Name: volumeName,
+	}
+
+	volume, err := volumes.Create(client, createOpts).Extract()
+	if err != nil {
+		return volume, err
+	}
+
+	err = volumes.WaitForStatus(client, volume.ID, "available", 60)
+	if err != nil {
+		return volume, err
+	}
+
+	return volume, nil
+}
+
+// CreateVolumeFromImage will create a volume from with a random name and size of
+// 1GB. An error will be returned if the volume was unable to be created.
+func CreateVolumeFromImage(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume creation in short mode.")
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	volumeName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create volume: %s", volumeName)
+
+	createOpts := volumes.CreateOpts{
+		Size:    1,
+		Name:    volumeName,
+		ImageID: choices.ImageID,
+	}
+
+	volume, err := volumes.Create(client, createOpts).Extract()
+	if err != nil {
+		return volume, err
+	}
+
+	err = volumes.WaitForStatus(client, volume.ID, "available", 60)
+	if err != nil {
+		return volume, err
+	}
+
+	return volume, nil
+}
+
+// DeleteVolume will delete a volume. A fatal error will occur if the volume
+// failed to be deleted. This works best when used as a deferred function.
+func DeleteVolume(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) {
+	err := volumes.Delete(client, volume.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete volume %s: %v", volume.ID, err)
+	}
+
+	t.Logf("Deleted volume: %s", volume.ID)
+}
diff --git a/acceptance/openstack/blockstorage/v2/pkg.go b/acceptance/openstack/blockstorage/v2/pkg.go
new file mode 100644
index 0000000..31dd0ff
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v2/pkg.go
@@ -0,0 +1,3 @@
+// The v2 package contains acceptance tests for the Openstack Cinder V2 service.
+
+package v2
diff --git a/acceptance/openstack/blockstorage/v2/volumes_test.go b/acceptance/openstack/blockstorage/v2/volumes_test.go
new file mode 100644
index 0000000..9003ca7
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v2/volumes_test.go
@@ -0,0 +1,52 @@
+// +build acceptance blockstorage
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+)
+
+func TestVolumesList(t *testing.T) {
+	client, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	allPages, err := volumes.List(client, volumes.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve volumes: %v", err)
+	}
+
+	allVolumes, err := volumes.ExtractVolumes(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract volumes: %v", err)
+	}
+
+	for _, volume := range allVolumes {
+		tools.PrintResource(t, volume)
+	}
+}
+
+func TestVolumesCreateDestroy(t *testing.T) {
+	client, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create blockstorage client: %v", err)
+	}
+
+	volume, err := CreateVolume(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer DeleteVolume(t, client, volume)
+
+	newVolume, err := volumes.Get(client, volume.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve volume: %v", err)
+	}
+
+	tools.PrintResource(t, newVolume)
+}
diff --git a/acceptance/openstack/client_test.go b/acceptance/openstack/client_test.go
new file mode 100644
index 0000000..2758f60
--- /dev/null
+++ b/acceptance/openstack/client_test.go
@@ -0,0 +1,40 @@
+// +build acceptance
+
+package openstack
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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/common.go b/acceptance/openstack/common.go
new file mode 100644
index 0000000..ba78cb6
--- /dev/null
+++ b/acceptance/openstack/common.go
@@ -0,0 +1,19 @@
+// Package openstack contains common functions that can be used
+// across all OpenStack components for acceptance testing.
+package openstack
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/common/extensions"
+)
+
+// PrintExtension prints an extension and all of its attributes.
+func PrintExtension(t *testing.T, extension *extensions.Extension) {
+	t.Logf("Name: %s", extension.Name)
+	t.Logf("Namespace: %s", extension.Namespace)
+	t.Logf("Alias: %s", extension.Alias)
+	t.Logf("Description: %s", extension.Description)
+	t.Logf("Updated: %s", extension.Updated)
+	t.Logf("Links: %v", extension.Links)
+}
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
new file mode 100644
index 0000000..2ba8888
--- /dev/null
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -0,0 +1,261 @@
+// +build acceptance compute bootfromvolume
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	blockstorage "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+)
+
+func TestBootFromImage(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blockDevices := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			BootIndex:           0,
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationLocal,
+			SourceType:          bootfromvolume.SourceImage,
+			UUID:                choices.ImageID,
+		},
+	}
+
+	server, err := CreateBootableVolumeServer(t, client, blockDevices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	tools.PrintResource(t, server)
+}
+
+func TestBootFromNewVolume(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blockDevices := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationVolume,
+			SourceType:          bootfromvolume.SourceImage,
+			UUID:                choices.ImageID,
+			VolumeSize:          2,
+		},
+	}
+
+	server, err := CreateBootableVolumeServer(t, client, blockDevices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	tools.PrintResource(t, server)
+}
+
+func TestBootFromExistingVolume(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	computeClient, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	blockStorageClient, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a block storage client: %v", err)
+	}
+
+	volume, err := blockstorage.CreateVolumeFromImage(t, blockStorageClient)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blockDevices := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationVolume,
+			SourceType:          bootfromvolume.SourceVolume,
+			UUID:                volume.ID,
+		},
+	}
+
+	server, err := CreateBootableVolumeServer(t, computeClient, blockDevices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, computeClient, server)
+
+	tools.PrintResource(t, server)
+}
+
+func TestBootFromMultiEphemeralServer(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blockDevices := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			BootIndex:           0,
+			DestinationType:     bootfromvolume.DestinationLocal,
+			DeleteOnTermination: true,
+			SourceType:          bootfromvolume.SourceImage,
+			UUID:                choices.ImageID,
+			VolumeSize:          5,
+		},
+		bootfromvolume.BlockDevice{
+			BootIndex:           -1,
+			DestinationType:     bootfromvolume.DestinationLocal,
+			DeleteOnTermination: true,
+			GuestFormat:         "ext4",
+			SourceType:          bootfromvolume.SourceBlank,
+			VolumeSize:          1,
+		},
+		bootfromvolume.BlockDevice{
+			BootIndex:           -1,
+			DestinationType:     bootfromvolume.DestinationLocal,
+			DeleteOnTermination: true,
+			GuestFormat:         "ext4",
+			SourceType:          bootfromvolume.SourceBlank,
+			VolumeSize:          1,
+		},
+	}
+
+	server, err := CreateMultiEphemeralServer(t, client, blockDevices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	tools.PrintResource(t, server)
+}
+
+func TestAttachNewVolume(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blockDevices := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			BootIndex:           0,
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationLocal,
+			SourceType:          bootfromvolume.SourceImage,
+			UUID:                choices.ImageID,
+		},
+		bootfromvolume.BlockDevice{
+			BootIndex:           1,
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationVolume,
+			SourceType:          bootfromvolume.SourceBlank,
+			VolumeSize:          2,
+		},
+	}
+
+	server, err := CreateBootableVolumeServer(t, client, blockDevices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	tools.PrintResource(t, server)
+}
+
+func TestAttachExistingVolume(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	computeClient, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	blockStorageClient, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a block storage client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	volume, err := blockstorage.CreateVolume(t, blockStorageClient)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blockDevices := []bootfromvolume.BlockDevice{
+		bootfromvolume.BlockDevice{
+			BootIndex:           0,
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationLocal,
+			SourceType:          bootfromvolume.SourceImage,
+			UUID:                choices.ImageID,
+		},
+		bootfromvolume.BlockDevice{
+			BootIndex:           1,
+			DeleteOnTermination: true,
+			DestinationType:     bootfromvolume.DestinationVolume,
+			SourceType:          bootfromvolume.SourceVolume,
+			UUID:                volume.ID,
+		},
+	}
+
+	server, err := CreateBootableVolumeServer(t, computeClient, blockDevices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, computeClient, server)
+
+	tools.PrintResource(t, server)
+}
diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go
new file mode 100644
index 0000000..1f3dc16
--- /dev/null
+++ b/acceptance/openstack/compute/v2/compute.go
@@ -0,0 +1,767 @@
+// Package v2 contains common functions for creating compute-based resources
+// for use in acceptance tests. See the `*_test.go` files for example usages.
+package v2
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+
+	"golang.org/x/crypto/ssh"
+)
+
+// AssociateFloatingIP will associate a floating IP with an instance. An error
+// will be returned if the floating IP was unable to be associated.
+func AssociateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server) error {
+	associateOpts := floatingips.AssociateOpts{
+		FloatingIP: floatingIP.IP,
+	}
+
+	t.Logf("Attempting to associate floating IP %s to instance %s", floatingIP.IP, server.ID)
+	err := floatingips.AssociateInstance(client, server.ID, associateOpts).ExtractErr()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// AssociateFloatingIPWithFixedIP will associate a floating IP with an
+// instance's specific fixed IP. An error will be returend if the floating IP
+// was unable to be associated.
+func AssociateFloatingIPWithFixedIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server, fixedIP string) error {
+	associateOpts := floatingips.AssociateOpts{
+		FloatingIP: floatingIP.IP,
+		FixedIP:    fixedIP,
+	}
+
+	t.Logf("Attempting to associate floating IP %s to fixed IP %s on instance %s", floatingIP.IP, fixedIP, server.ID)
+	err := floatingips.AssociateInstance(client, server.ID, associateOpts).ExtractErr()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// CreateBootableVolumeServer works like CreateServer but is configured with
+// one or more block devices defined by passing in []bootfromvolume.BlockDevice.
+// An error will be returned if a server was unable to be created.
+func CreateBootableVolumeServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create bootable volume server: %s", name)
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+	}
+
+	if blockDevices[0].SourceType == bootfromvolume.SourceImage && blockDevices[0].DestinationType == bootfromvolume.DestinationLocal {
+		serverCreateOpts.ImageRef = blockDevices[0].UUID
+	}
+
+	server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
+		serverCreateOpts,
+		blockDevices,
+	}).Extract()
+
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	newServer, err := servers.Get(client, server.ID).Extract()
+
+	return newServer, nil
+}
+
+// CreateDefaultRule will create a default security group rule with a
+// random port range between 80 and 90. An error will be returned if
+// a default rule was unable to be created.
+func CreateDefaultRule(t *testing.T, client *gophercloud.ServiceClient) (dsr.DefaultRule, error) {
+	createOpts := dsr.CreateOpts{
+		FromPort:   tools.RandomInt(80, 89),
+		ToPort:     tools.RandomInt(90, 99),
+		IPProtocol: "TCP",
+		CIDR:       "0.0.0.0/0",
+	}
+
+	defaultRule, err := dsr.Create(client, createOpts).Extract()
+	if err != nil {
+		return *defaultRule, err
+	}
+
+	t.Logf("Created default rule: %s", defaultRule.ID)
+
+	return *defaultRule, nil
+}
+
+// CreateFloatingIP will allocate a floating IP.
+// An error will be returend if one was unable to be allocated.
+func CreateFloatingIP(t *testing.T, client *gophercloud.ServiceClient) (*floatingips.FloatingIP, error) {
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	createOpts := floatingips.CreateOpts{
+		Pool: choices.FloatingIPPoolName,
+	}
+	floatingIP, err := floatingips.Create(client, createOpts).Extract()
+	if err != nil {
+		return floatingIP, err
+	}
+
+	t.Logf("Created floating IP: %s", floatingIP.ID)
+	return floatingIP, nil
+}
+
+func createKey() (string, error) {
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return "", err
+	}
+
+	publicKey := privateKey.PublicKey
+	pub, err := ssh.NewPublicKey(&publicKey)
+	if err != nil {
+		return "", err
+	}
+
+	pubBytes := ssh.MarshalAuthorizedKey(pub)
+	pk := string(pubBytes)
+	return pk, nil
+}
+
+// CreateKeyPair will create a KeyPair with a random name. An error will occur
+// if the keypair failed to be created. An error will be returned if the
+// keypair was unable to be created.
+func CreateKeyPair(t *testing.T, client *gophercloud.ServiceClient) (*keypairs.KeyPair, error) {
+	keyPairName := tools.RandomString("keypair_", 5)
+
+	t.Logf("Attempting to create keypair: %s", keyPairName)
+	createOpts := keypairs.CreateOpts{
+		Name: keyPairName,
+	}
+	keyPair, err := keypairs.Create(client, createOpts).Extract()
+	if err != nil {
+		return keyPair, err
+	}
+
+	t.Logf("Created keypair: %s", keyPairName)
+	return keyPair, nil
+}
+
+// CreateMultiEphemeralServer works like CreateServer but is configured with
+// one or more block devices defined by passing in []bootfromvolume.BlockDevice.
+// These block devices act like block devices when booting from a volume but
+// are actually local ephemeral disks.
+// An error will be returned if a server was unable to be created.
+func CreateMultiEphemeralServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create bootable volume server: %s", name)
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+	}
+
+	server, err = bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
+		serverCreateOpts,
+		blockDevices,
+	}).Extract()
+
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	newServer, err := servers.Get(client, server.ID).Extract()
+
+	return newServer, nil
+}
+
+// CreateSecurityGroup will create a security group with a random name.
+// An error will be returned if one was failed to be created.
+func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (secgroups.SecurityGroup, error) {
+	createOpts := secgroups.CreateOpts{
+		Name:        tools.RandomString("secgroup_", 5),
+		Description: "something",
+	}
+
+	securityGroup, err := secgroups.Create(client, createOpts).Extract()
+	if err != nil {
+		return *securityGroup, err
+	}
+
+	t.Logf("Created security group: %s", securityGroup.ID)
+	return *securityGroup, nil
+}
+
+// CreateSecurityGroupRule will create a security group rule with a random name
+// and a random TCP port range between port 80 and 99. An error will be
+// returned if the rule failed to be created.
+func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, securityGroupID string) (secgroups.Rule, error) {
+	createOpts := secgroups.CreateRuleOpts{
+		ParentGroupID: securityGroupID,
+		FromPort:      tools.RandomInt(80, 89),
+		ToPort:        tools.RandomInt(90, 99),
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := secgroups.CreateRule(client, createOpts).Extract()
+	if err != nil {
+		return *rule, err
+	}
+
+	t.Logf("Created security group rule: %s", rule.ID)
+	return *rule, nil
+}
+
+// CreateServer creates a basic instance with a randomly generated name.
+// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable.
+// The image will be the value of the OS_IMAGE_ID environment variable.
+// The instance will be launched on the network specified in OS_NETWORK_NAME.
+// An error will be returned if the instance was unable to be created.
+func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err = servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		AdminPass: pwd,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+		Metadata: map[string]string{
+			"abc": "def",
+		},
+		Personality: servers.Personality{
+			&servers.File{
+				Path:     "/etc/test",
+				Contents: []byte("hello world"),
+			},
+		},
+	}).Extract()
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	return server, nil
+}
+
+// CreateServerWithoutImageRef creates a basic instance with a randomly generated name.
+// The flavor of the instance will be the value of the OS_FLAVOR_ID environment variable.
+// The image is intentionally missing to trigger an error.
+// The instance will be launched on the network specified in OS_NETWORK_NAME.
+// An error will be returned if the instance was unable to be created.
+func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	server, err = servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		AdminPass: pwd,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+		Personality: servers.Personality{
+			&servers.File{
+				Path:     "/etc/test",
+				Contents: []byte("hello world"),
+			},
+		},
+	}).Extract()
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	return server, nil
+}
+
+// CreateServerGroup will create a server with a random name. An error will be
+// returned if the server group failed to be created.
+func CreateServerGroup(t *testing.T, client *gophercloud.ServiceClient, policy string) (*servergroups.ServerGroup, error) {
+	sg, err := servergroups.Create(client, &servergroups.CreateOpts{
+		Name:     "test",
+		Policies: []string{policy},
+	}).Extract()
+
+	if err != nil {
+		return sg, err
+	}
+
+	return sg, nil
+}
+
+// CreateServerInServerGroup works like CreateServer but places the instance in
+// a specified Server Group.
+func CreateServerInServerGroup(t *testing.T, client *gophercloud.ServiceClient, serverGroup *servergroups.ServerGroup) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s", name)
+
+	pwd := tools.MakeNewPassword("")
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		AdminPass: pwd,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+	}
+
+	schedulerHintsOpts := schedulerhints.CreateOptsExt{
+		serverCreateOpts,
+		schedulerhints.SchedulerHints{
+			Group: serverGroup.ID,
+		},
+	}
+	server, err = servers.Create(client, schedulerHintsOpts).Extract()
+	if err != nil {
+		return server, err
+	}
+
+	return server, nil
+}
+
+// CreateServerWithPublicKey works the same as CreateServer, but additionally
+// configures the server with a specified Key Pair name.
+func CreateServerWithPublicKey(t *testing.T, client *gophercloud.ServiceClient, keyPairName string) (*servers.Server, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	var server *servers.Server
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		return server, err
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s", name)
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+		Networks: []servers.Network{
+			servers.Network{UUID: networkID},
+		},
+	}
+
+	server, err = servers.Create(client, keypairs.CreateOptsExt{
+		serverCreateOpts,
+		keyPairName,
+	}).Extract()
+	if err != nil {
+		return server, err
+	}
+
+	if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		return server, err
+	}
+
+	return server, nil
+}
+
+// CreateVolumeAttachment will attach a volume to a server. An error will be
+// returned if the volume failed to attach.
+func CreateVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, server *servers.Server, volume *volumes.Volume) (*volumeattach.VolumeAttachment, error) {
+	volumeAttachOptions := volumeattach.CreateOpts{
+		VolumeID: volume.ID,
+	}
+
+	t.Logf("Attempting to attach volume %s to server %s", volume.ID, server.ID)
+	volumeAttachment, err := volumeattach.Create(client, server.ID, volumeAttachOptions).Extract()
+	if err != nil {
+		return volumeAttachment, err
+	}
+
+	if err := volumes.WaitForStatus(blockClient, volume.ID, "in-use", 60); err != nil {
+		return volumeAttachment, err
+	}
+
+	return volumeAttachment, nil
+}
+
+// DeleteDefaultRule deletes a default security group rule.
+// A fatal error will occur if the rule failed to delete. This works best when
+// using it as a deferred function.
+func DeleteDefaultRule(t *testing.T, client *gophercloud.ServiceClient, defaultRule dsr.DefaultRule) {
+	err := dsr.Delete(client, defaultRule.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete default rule %s: %v", defaultRule.ID, err)
+	}
+
+	t.Logf("Deleted default rule: %s", defaultRule.ID)
+}
+
+// DeleteFloatingIP will de-allocate a floating IP. A fatal error will occur if
+// the floating IP failed to de-allocate. This works best when using it as a
+// deferred function.
+func DeleteFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP) {
+	err := floatingips.Delete(client, floatingIP.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete floating IP %s: %v", floatingIP.ID, err)
+	}
+
+	t.Logf("Deleted floating IP: %s", floatingIP.ID)
+}
+
+// DeleteKeyPair will delete a specified keypair. A fatal error will occur if
+// the keypair failed to be deleted. This works best when used as a deferred
+// function.
+func DeleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, keyPair *keypairs.KeyPair) {
+	err := keypairs.Delete(client, keyPair.Name).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete keypair %s: %v", keyPair.Name, err)
+	}
+
+	t.Logf("Deleted keypair: %s", keyPair.Name)
+}
+
+// DeleteSecurityGroup will delete a security group. A fatal error will occur
+// if the group failed to be deleted. This works best as a deferred function.
+func DeleteSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, securityGroup secgroups.SecurityGroup) {
+	err := secgroups.Delete(client, securityGroup.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete security group %s: %s", securityGroup.ID, err)
+	}
+
+	t.Logf("Deleted security group: %s", securityGroup.ID)
+}
+
+// DeleteSecurityGroupRule will delete a security group rule. A fatal error
+// will occur if the rule failed to be deleted. This works best when used
+// as a deferred function.
+func DeleteSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, rule secgroups.Rule) {
+	err := secgroups.DeleteRule(client, rule.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete rule: %v", err)
+	}
+
+	t.Logf("Deleted security group rule: %s", rule.ID)
+}
+
+// DeleteServer deletes an instance via its UUID.
+// A fatal error will occur if the instance failed to be destroyed. This works
+// best when using it as a deferred function.
+func DeleteServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server) {
+	err := servers.Delete(client, server.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete server %s: %s", server.ID, err)
+	}
+
+	t.Logf("Deleted server: %s", server.ID)
+}
+
+// DeleteServerGroup will delete a server group. A fatal error will occur if
+// the server group failed to be deleted. This works best when used as a
+// deferred function.
+func DeleteServerGroup(t *testing.T, client *gophercloud.ServiceClient, serverGroup *servergroups.ServerGroup) {
+	err := servergroups.Delete(client, serverGroup.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete server group %s: %v", serverGroup.ID, err)
+	}
+
+	t.Logf("Deleted server group %s", serverGroup.ID)
+}
+
+// DeleteVolumeAttachment will disconnect a volume from an instance. A fatal
+// error will occur if the volume failed to detach. This works best when used
+// as a deferred function.
+func DeleteVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, server *servers.Server, volumeAttachment *volumeattach.VolumeAttachment) {
+
+	err := volumeattach.Delete(client, server.ID, volumeAttachment.VolumeID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to detach volume: %v", err)
+	}
+
+	if err := volumes.WaitForStatus(blockClient, volumeAttachment.ID, "available", 60); err != nil {
+		t.Fatalf("Unable to wait for volume: %v", err)
+	}
+	t.Logf("Deleted volume: %s", volumeAttachment.VolumeID)
+}
+
+// DisassociateFloatingIP will disassociate a floating IP from an instance. A
+// fatal error will occur if the floating IP failed to disassociate. This works
+// best when using it as a deferred function.
+func DisassociateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, server *servers.Server) {
+	disassociateOpts := floatingips.DisassociateOpts{
+		FloatingIP: floatingIP.IP,
+	}
+
+	err := floatingips.DisassociateInstance(client, server.ID, disassociateOpts).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to disassociate floating IP %s from server %s: %v", floatingIP.IP, server.ID, err)
+	}
+
+	t.Logf("Disassociated floating IP %s from server %s", floatingIP.IP, server.ID)
+}
+
+// GetNetworkIDFromNetworks will return the network ID from a specified network
+// UUID using the os-networks API extension. An error will be returned if the
+// network could not be retrieved.
+func GetNetworkIDFromNetworks(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", networkName, networkID)
+
+	return networkID, nil
+}
+
+// GetNetworkIDFromTenantNetworks will return the network UUID for a given
+// network name using the os-tenant-networks API extension. An error will be
+// returned if the network could not be retrieved.
+func GetNetworkIDFromTenantNetworks(t *testing.T, client *gophercloud.ServiceClient, networkName string) (string, error) {
+	allPages, err := tenantnetworks.List(client).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, network := range allTenantNetworks {
+		if network.Name == networkName {
+			return network.ID, nil
+		}
+	}
+
+	return "", fmt.Errorf("Failed to obtain network ID for network %s", networkName)
+}
+
+// ImportPublicKey will create a KeyPair with a random name and a specified
+// public key. An error will be returned if the keypair failed to be created.
+func ImportPublicKey(t *testing.T, client *gophercloud.ServiceClient, publicKey string) (*keypairs.KeyPair, error) {
+	keyPairName := tools.RandomString("keypair_", 5)
+
+	t.Logf("Attempting to create keypair: %s", keyPairName)
+	createOpts := keypairs.CreateOpts{
+		Name:      keyPairName,
+		PublicKey: publicKey,
+	}
+	keyPair, err := keypairs.Create(client, createOpts).Extract()
+	if err != nil {
+		return keyPair, err
+	}
+
+	t.Logf("Created keypair: %s", keyPairName)
+	return keyPair, nil
+}
+
+// ResizeServer performs a resize action on an instance. An error will be
+// returned if the instance failed to resize.
+// The new flavor that the instance will be resized to is specified in OS_FLAVOR_ID_RESIZE.
+func ResizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server) error {
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	opts := &servers.ResizeOpts{
+		FlavorRef: choices.FlavorIDResize,
+	}
+	if res := servers.Resize(client, server.ID, opts); res.Err != nil {
+		return res.Err
+	}
+
+	if err := WaitForComputeStatus(client, server, "VERIFY_RESIZE"); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// WaitForComputeStatus will poll an instance's status until it either matches
+// the specified status or the status becomes ERROR.
+func WaitForComputeStatus(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
+		}
+
+		if latest.Status == "ERROR" {
+			return false, fmt.Errorf("Instance in ERROR state")
+		}
+
+		return false, nil
+	})
+}
+
+//Convenience method to fill an QuotaSet-UpdateOpts-struct from a QuotaSet-struct
+func FillUpdateOptsFromQuotaSet(src quotasets.QuotaSet, dest *quotasets.UpdateOpts) {
+	dest.FixedIps = &src.FixedIps
+	dest.FloatingIps = &src.FloatingIps
+	dest.InjectedFileContentBytes = &src.InjectedFileContentBytes
+	dest.InjectedFilePathBytes = &src.InjectedFilePathBytes
+	dest.InjectedFiles = &src.InjectedFiles
+	dest.KeyPairs = &src.KeyPairs
+	dest.Ram = &src.Ram
+	dest.SecurityGroupRules = &src.SecurityGroupRules
+	dest.SecurityGroups = &src.SecurityGroups
+	dest.Cores = &src.Cores
+	dest.Instances = &src.Instances
+	dest.ServerGroups = &src.ServerGroups
+	dest.ServerGroupMembers = &src.ServerGroupMembers
+	dest.MetadataItems = &src.MetadataItems
+}
diff --git a/acceptance/openstack/compute/v2/defsecrules_test.go b/acceptance/openstack/compute/v2/defsecrules_test.go
new file mode 100644
index 0000000..16c43f4
--- /dev/null
+++ b/acceptance/openstack/compute/v2/defsecrules_test.go
@@ -0,0 +1,67 @@
+// +build acceptance compute defsecrules
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules"
+)
+
+func TestDefSecRulesList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := dsr.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list default rules: %v", err)
+	}
+
+	allDefaultRules, err := dsr.ExtractDefaultRules(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract default rules: %v", err)
+	}
+
+	for _, defaultRule := range allDefaultRules {
+		tools.PrintResource(t, defaultRule)
+	}
+}
+
+func TestDefSecRulesCreate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	defaultRule, err := CreateDefaultRule(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create default rule: %v", err)
+	}
+	defer DeleteDefaultRule(t, client, defaultRule)
+
+	tools.PrintResource(t, defaultRule)
+}
+
+func TestDefSecRulesGet(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	defaultRule, err := CreateDefaultRule(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create default rule: %v", err)
+	}
+	defer DeleteDefaultRule(t, client, defaultRule)
+
+	newDefaultRule, err := dsr.Get(client, defaultRule.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get default rule %s: %v", defaultRule.ID, err)
+	}
+
+	tools.PrintResource(t, newDefaultRule)
+}
diff --git a/acceptance/openstack/compute/v2/extension_test.go b/acceptance/openstack/compute/v2/extension_test.go
new file mode 100644
index 0000000..5b2cf4a
--- /dev/null
+++ b/acceptance/openstack/compute/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance compute extensions
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/common/extensions"
+)
+
+func TestExtensionsList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := extensions.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list extensions: %v", err)
+	}
+
+	allExtensions, err := extensions.ExtractExtensions(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract extensions: %v", err)
+	}
+
+	for _, extension := range allExtensions {
+		tools.PrintResource(t, extension)
+	}
+}
+
+func TestExtensionGet(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	extension, err := extensions.Get(client, "os-admin-actions").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get extension os-admin-actions: %v", err)
+	}
+
+	tools.PrintResource(t, extension)
+}
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
new file mode 100644
index 0000000..ee698cc
--- /dev/null
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -0,0 +1,51 @@
+// +build acceptance compute flavors
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
+)
+
+func TestFlavorsList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := flavors.ListDetail(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve flavors: %v", err)
+	}
+
+	allFlavors, err := flavors.ExtractFlavors(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract flavor results: %v", err)
+	}
+
+	for _, flavor := range allFlavors {
+		tools.PrintResource(t, flavor)
+	}
+}
+
+func TestFlavorsGet(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	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)
+	}
+
+	tools.PrintResource(t, 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..26b7bfe
--- /dev/null
+++ b/acceptance/openstack/compute/v2/floatingip_test.go
@@ -0,0 +1,148 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+)
+
+func TestFloatingIPsList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := floatingips.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve floating IPs: %v", err)
+	}
+
+	allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract floating IPs: %v", err)
+	}
+
+	for _, floatingIP := range allFloatingIPs {
+		tools.PrintResource(t, floatingIP)
+	}
+}
+
+func TestFloatingIPsCreate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	floatingIP, err := CreateFloatingIP(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create floating IP: %v", err)
+	}
+	defer DeleteFloatingIP(t, client, floatingIP)
+
+	tools.PrintResource(t, floatingIP)
+}
+
+func TestFloatingIPsAssociate(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	floatingIP, err := CreateFloatingIP(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create floating IP: %v", err)
+	}
+	defer DeleteFloatingIP(t, client, floatingIP)
+
+	tools.PrintResource(t, floatingIP)
+
+	err = AssociateFloatingIP(t, client, floatingIP, server)
+	if err != nil {
+		t.Fatalf("Unable to associate floating IP %s with server %s: %v", floatingIP.IP, server.ID, err)
+	}
+	defer DisassociateFloatingIP(t, client, floatingIP, server)
+
+	newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get floating IP %s: %v", floatingIP.ID, err)
+	}
+
+	t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP)
+
+	tools.PrintResource(t, newFloatingIP)
+}
+
+func TestFloatingIPsFixedIPAssociate(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	newServer, err := servers.Get(client, server.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get server %s: %v", server.ID, err)
+	}
+
+	floatingIP, err := CreateFloatingIP(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create floating IP: %v", err)
+	}
+	defer DeleteFloatingIP(t, client, floatingIP)
+
+	tools.PrintResource(t, floatingIP)
+
+	var fixedIP string
+	for _, networkAddresses := range newServer.Addresses[choices.NetworkName].([]interface{}) {
+		address := networkAddresses.(map[string]interface{})
+		if address["OS-EXT-IPS:type"] == "fixed" {
+			if address["version"].(float64) == 4 {
+				fixedIP = address["addr"].(string)
+			}
+		}
+	}
+
+	err = AssociateFloatingIPWithFixedIP(t, client, floatingIP, newServer, fixedIP)
+	if err != nil {
+		t.Fatalf("Unable to associate floating IP %s with server %s: %v", floatingIP.IP, newServer.ID, err)
+	}
+	defer DisassociateFloatingIP(t, client, floatingIP, newServer)
+
+	newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get floating IP %s: %v", floatingIP.ID, err)
+	}
+
+	t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP)
+
+	tools.PrintResource(t, newFloatingIP)
+}
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
new file mode 100644
index 0000000..a34ce3e
--- /dev/null
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -0,0 +1,51 @@
+// +build acceptance compute images
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
+)
+
+func TestImagesList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute: client: %v", err)
+	}
+
+	allPages, err := images.ListDetail(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve images: %v", err)
+	}
+
+	allImages, err := images.ExtractImages(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract image results: %v", err)
+	}
+
+	for _, image := range allImages {
+		tools.PrintResource(t, image)
+	}
+}
+
+func TestImagesGet(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute: client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	image, err := images.Get(client, choices.ImageID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get image information: %v", err)
+	}
+
+	tools.PrintResource(t, image)
+}
diff --git a/acceptance/openstack/compute/v2/keypairs_test.go b/acceptance/openstack/compute/v2/keypairs_test.go
new file mode 100644
index 0000000..c4b91ec
--- /dev/null
+++ b/acceptance/openstack/compute/v2/keypairs_test.go
@@ -0,0 +1,107 @@
+// +build acceptance compute keypairs
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+)
+
+const keyName = "gophercloud_test_key_pair"
+
+func TestKeypairsList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := keypairs.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve keypairs: %s", err)
+	}
+
+	allKeys, err := keypairs.ExtractKeyPairs(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract keypairs results: %s", err)
+	}
+
+	for _, keypair := range allKeys {
+		tools.PrintResource(t, keypair)
+	}
+}
+
+func TestKeypairsCreate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	keyPair, err := CreateKeyPair(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create key pair: %v", err)
+	}
+	defer DeleteKeyPair(t, client, keyPair)
+
+	tools.PrintResource(t, keyPair)
+}
+
+func TestKeypairsImportPublicKey(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	publicKey, err := createKey()
+	if err != nil {
+		t.Fatalf("Unable to create public key: %s", err)
+	}
+
+	keyPair, err := ImportPublicKey(t, client, publicKey)
+	if err != nil {
+		t.Fatalf("Unable to create keypair: %s", err)
+	}
+	defer DeleteKeyPair(t, client, keyPair)
+
+	tools.PrintResource(t, keyPair)
+}
+
+func TestKeypairsServerCreateWithKey(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	publicKey, err := createKey()
+	if err != nil {
+		t.Fatalf("Unable to create public key: %s", err)
+	}
+
+	keyPair, err := ImportPublicKey(t, client, publicKey)
+	if err != nil {
+		t.Fatalf("Unable to create keypair: %s", err)
+	}
+	defer DeleteKeyPair(t, client, keyPair)
+
+	server, err := CreateServerWithPublicKey(t, client, keyPair.Name)
+	if err != nil {
+		t.Fatalf("Unable to create server: %s", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	server, err = servers.Get(client, server.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to retrieve server: %s", err)
+	}
+
+	if server.KeyName != keyPair.Name {
+		t.Fatalf("key name of server %s is %s, not %s", server.ID, server.KeyName, keyPair.Name)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/limits_test.go b/acceptance/openstack/compute/v2/limits_test.go
new file mode 100644
index 0000000..2bf5ce6
--- /dev/null
+++ b/acceptance/openstack/compute/v2/limits_test.go
@@ -0,0 +1,52 @@
+// +build acceptance compute limits
+
+package v2
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
+)
+
+func TestLimits(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	limits, err := limits.Get(client, nil).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get limits: %v", err)
+	}
+
+	t.Logf("Limits for scoped user:")
+	t.Logf("%#v", limits)
+}
+
+func TestLimitsForTenant(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	// I think this is the easiest way to get the tenant ID while being
+	// agnostic to Identity v2 and v3.
+	// Technically we're just returning the limits for ourselves, but it's
+	// the fact that we're specifying a tenant ID that is important here.
+	endpointParts := strings.Split(client.Endpoint, "/")
+	tenantID := endpointParts[4]
+
+	getOpts := limits.GetOpts{
+		TenantID: tenantID,
+	}
+
+	limits, err := limits.Get(client, getOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get absolute limits: %v", err)
+	}
+
+	t.Logf("Limits for tenant %s:", tenantID)
+	t.Logf("%#v", limits)
+}
diff --git a/acceptance/openstack/compute/v2/network_test.go b/acceptance/openstack/compute/v2/network_test.go
new file mode 100644
index 0000000..7451518
--- /dev/null
+++ b/acceptance/openstack/compute/v2/network_test.go
@@ -0,0 +1,56 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks"
+)
+
+func TestNetworksList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := networks.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	allNetworks, err := networks.ExtractNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	for _, network := range allNetworks {
+		tools.PrintResource(t, network)
+	}
+}
+
+func TestNetworksGet(t *testing.T) {
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	networkID, err := GetNetworkIDFromNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	network, err := networks.Get(client, networkID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get network %s: %v", networkID, err)
+	}
+
+	tools.PrintResource(t, network)
+}
diff --git a/acceptance/openstack/compute/v2/pkg.go b/acceptance/openstack/compute/v2/pkg.go
new file mode 100644
index 0000000..a57c1e7
--- /dev/null
+++ b/acceptance/openstack/compute/v2/pkg.go
@@ -0,0 +1,2 @@
+// Package v2 package contains acceptance tests for the Openstack Compute V2 service.
+package v2
diff --git a/acceptance/openstack/compute/v2/quotaset_test.go b/acceptance/openstack/compute/v2/quotaset_test.go
new file mode 100644
index 0000000..1a691c0
--- /dev/null
+++ b/acceptance/openstack/compute/v2/quotaset_test.go
@@ -0,0 +1,184 @@
+// +build acceptance compute quotasets
+
+package v2
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"os"
+)
+
+func TestQuotasetGet(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	identityClient, err := clients.NewIdentityV2Client()
+	if err != nil {
+		t.Fatalf("Unable to get a new identity client: %v", err)
+	}
+
+	tenantID, err := getTenantID(t, identityClient)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	quotaSet, err := quotasets.Get(client, tenantID).Extract()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	tools.PrintResource(t, quotaSet)
+}
+
+func getTenantID(t *testing.T, client *gophercloud.ServiceClient) (string, error) {
+	allPages, err := tenants.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to get list of tenants: %v", err)
+	}
+
+	allTenants, err := tenants.ExtractTenants(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract tenants: %v", err)
+	}
+
+	for _, tenant := range allTenants {
+		return tenant.ID, nil
+	}
+
+	return "", fmt.Errorf("Unable to get tenant ID")
+}
+
+func getTenantIDByName(t *testing.T, client *gophercloud.ServiceClient, name string) (string, error) {
+	allPages, err := tenants.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to get list of tenants: %v", err)
+	}
+
+	allTenants, err := tenants.ExtractTenants(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract tenants: %v", err)
+	}
+
+	for _, tenant := range allTenants {
+		if tenant.Name == name {
+			return tenant.ID, nil
+		}
+	}
+
+	return "", fmt.Errorf("Unable to get tenant ID")
+}
+
+//What will be sent as desired Quotas to the Server
+var UpdatQuotaOpts = quotasets.UpdateOpts{
+	FixedIps:                 gophercloud.IntToPointer(10),
+	FloatingIps:              gophercloud.IntToPointer(10),
+	InjectedFileContentBytes: gophercloud.IntToPointer(10240),
+	InjectedFilePathBytes:    gophercloud.IntToPointer(255),
+	InjectedFiles:            gophercloud.IntToPointer(5),
+	KeyPairs:                 gophercloud.IntToPointer(10),
+	MetadataItems:            gophercloud.IntToPointer(128),
+	Ram:                      gophercloud.IntToPointer(20000),
+	SecurityGroupRules:       gophercloud.IntToPointer(20),
+	SecurityGroups:           gophercloud.IntToPointer(10),
+	Cores:                    gophercloud.IntToPointer(10),
+	Instances:                gophercloud.IntToPointer(4),
+	ServerGroups:             gophercloud.IntToPointer(2),
+	ServerGroupMembers:       gophercloud.IntToPointer(3),
+}
+
+//What the Server hopefully returns as the new Quotas
+var UpdatedQuotas = quotasets.QuotaSet{
+	FixedIps:                 10,
+	FloatingIps:              10,
+	InjectedFileContentBytes: 10240,
+	InjectedFilePathBytes:    255,
+	InjectedFiles:            5,
+	KeyPairs:                 10,
+	MetadataItems:            128,
+	Ram:                      20000,
+	SecurityGroupRules:       20,
+	SecurityGroups:           10,
+	Cores:                    10,
+	Instances:                4,
+	ServerGroups:             2,
+	ServerGroupMembers:       3,
+}
+
+func TestQuotasetUpdateDelete(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	idclient, err := clients.NewIdentityV2Client()
+	if err != nil {
+		t.Fatalf("Could not create IdentityClient to look up tenant id!")
+	}
+
+	tenantid, err := getTenantIDByName(t, idclient, os.Getenv("OS_TENANT_NAME"))
+	if err != nil {
+		t.Fatalf("Id for Tenant named '%' not found. Please set OS_TENANT_NAME appropriately", os.Getenv("OS_TENANT_NAME"))
+	}
+
+	//save original quotas
+	orig, err := quotasets.Get(client, tenantid).Extract()
+	th.AssertNoErr(t, err)
+
+	//Test Update
+	res, err := quotasets.Update(client, tenantid, UpdatQuotaOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, UpdatedQuotas, *res)
+
+	//Test Delete
+	_, err = quotasets.Delete(client, tenantid).Extract()
+	th.AssertNoErr(t, err)
+	//We dont know the default quotas, so just check if the quotas are not the same as before
+	newres, err := quotasets.Get(client, tenantid).Extract()
+	if newres == res {
+		t.Fatalf("Quotas after delete equal quotas before delete!")
+	}
+
+	restore := quotasets.UpdateOpts{}
+	FillUpdateOptsFromQuotaSet(*orig, &restore)
+
+	//restore original quotas
+	res, err = quotasets.Update(client, tenantid, restore).Extract()
+	th.AssertNoErr(t, err)
+
+	orig.ID = ""
+	th.AssertEquals(t, *orig, *res)
+
+}
+
+// Makes sure that the FillUpdateOptsFromQuotaSet() helper function works properly
+func TestFillFromQuotaSetHelperFunction(t *testing.T) {
+	op := &quotasets.UpdateOpts{}
+	expected := `
+	{
+	"fixed_ips": 10,
+	"floating_ips": 10,
+	"injected_file_content_bytes": 10240,
+	"injected_file_path_bytes": 255,
+	"injected_files": 5,
+	"key_pairs": 10,
+	"metadata_items": 128,
+	"ram": 20000,
+	"security_group_rules": 20,
+	"security_groups": 10,
+	"cores": 10,
+	"instances": 4,
+	"server_groups": 2,
+	"server_group_members": 3
+	}`
+	FillUpdateOptsFromQuotaSet(UpdatedQuotas, op)
+	th.AssertJSONEquals(t, expected, op)
+}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
new file mode 100644
index 0000000..c0d0230
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -0,0 +1,137 @@
+// +build acceptance compute secgroups
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups"
+)
+
+func TestSecGroupsList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := secgroups.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve security groups: %v", err)
+	}
+
+	allSecGroups, err := secgroups.ExtractSecurityGroups(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract security groups: %v", err)
+	}
+
+	for _, secgroup := range allSecGroups {
+		tools.PrintResource(t, secgroup)
+	}
+}
+
+func TestSecGroupsCreate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	securityGroup, err := CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer DeleteSecurityGroup(t, client, securityGroup)
+}
+
+func TestSecGroupsUpdate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	securityGroup, err := CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer DeleteSecurityGroup(t, client, securityGroup)
+
+	updateOpts := secgroups.UpdateOpts{
+		Name:        tools.RandomString("secgroup_", 4),
+		Description: tools.RandomString("dec_", 10),
+	}
+	updatedSecurityGroup, err := secgroups.Update(client, securityGroup.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update security group: %v", err)
+	}
+
+	t.Logf("Updated %s's name to %s", updatedSecurityGroup.ID, updatedSecurityGroup.Name)
+}
+
+func TestSecGroupsRuleCreate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	securityGroup, err := CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer DeleteSecurityGroup(t, client, securityGroup)
+
+	rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID)
+	if err != nil {
+		t.Fatalf("Unable to create rule: %v", err)
+	}
+	defer DeleteSecurityGroupRule(t, client, rule)
+
+	newSecurityGroup, err := secgroups.Get(client, securityGroup.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to obtain security group: %v", err)
+	}
+
+	tools.PrintResource(t, newSecurityGroup)
+
+}
+
+func TestSecGroupsAddGroupToServer(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	securityGroup, err := CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer DeleteSecurityGroup(t, client, securityGroup)
+
+	rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID)
+	if err != nil {
+		t.Fatalf("Unable to create rule: %v", err)
+	}
+	defer DeleteSecurityGroupRule(t, client, rule)
+
+	t.Logf("Adding group %s to server %s", securityGroup.ID, server.ID)
+	err = secgroups.AddServer(client, server.ID, securityGroup.Name).ExtractErr()
+	if err != nil && err.Error() != "EOF" {
+		t.Fatalf("Unable to add group %s to server %s: %s", securityGroup.ID, server.ID, err)
+	}
+
+	t.Logf("Removing group %s from server %s", securityGroup.ID, server.ID)
+	err = secgroups.RemoveServer(client, server.ID, securityGroup.Name).ExtractErr()
+	if err != nil && err.Error() != "EOF" {
+		t.Fatalf("Unable to remove group %s from server %s: %s", securityGroup.ID, server.ID, err)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/servergroup_test.go b/acceptance/openstack/compute/v2/servergroup_test.go
new file mode 100644
index 0000000..ac1915f
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servergroup_test.go
@@ -0,0 +1,90 @@
+// +build acceptance compute servergroups
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+)
+
+func TestServergroupsList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := servergroups.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list server groups: %v", err)
+	}
+
+	allServerGroups, err := servergroups.ExtractServerGroups(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract server groups: %v", err)
+	}
+
+	for _, serverGroup := range allServerGroups {
+		tools.PrintResource(t, serverGroup)
+	}
+}
+
+func TestServergroupsCreate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	serverGroup, err := CreateServerGroup(t, client, "anti-affinity")
+	if err != nil {
+		t.Fatalf("Unable to create server group: %v", err)
+	}
+	defer DeleteServerGroup(t, client, serverGroup)
+
+	serverGroup, err = servergroups.Get(client, serverGroup.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get server group: %v", err)
+	}
+
+	tools.PrintResource(t, serverGroup)
+}
+
+func TestServergroupsAffinityPolicy(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	serverGroup, err := CreateServerGroup(t, client, "affinity")
+	if err != nil {
+		t.Fatalf("Unable to create server group: %v", err)
+	}
+	defer DeleteServerGroup(t, client, serverGroup)
+
+	firstServer, err := CreateServerInServerGroup(t, client, serverGroup)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, firstServer)
+
+	firstServer, err = servers.Get(client, firstServer.ID).Extract()
+
+	secondServer, err := CreateServerInServerGroup(t, client, serverGroup)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	if err = WaitForComputeStatus(client, secondServer, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+	defer DeleteServer(t, client, secondServer)
+
+	secondServer, err = servers.Get(client, secondServer.ID).Extract()
+
+	if firstServer.HostID != secondServer.HostID {
+		t.Fatalf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
new file mode 100644
index 0000000..f43c94d
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -0,0 +1,390 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestServersList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := servers.List(client, servers.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve servers: %v", err)
+	}
+
+	allServers, err := servers.ExtractServers(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract servers: %v", err)
+	}
+
+	for _, server := range allServers {
+		tools.PrintResource(t, server)
+	}
+}
+
+func TestServersCreateDestroy(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	defer DeleteServer(t, client, server)
+
+	newServer, err := servers.Get(client, server.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve server: %v", err)
+	}
+	tools.PrintResource(t, newServer)
+
+	allAddressPages, err := servers.ListAddresses(client, server.ID).AllPages()
+	if err != nil {
+		t.Errorf("Unable to list server addresses: %v", err)
+	}
+
+	allAddresses, err := servers.ExtractAddresses(allAddressPages)
+	if err != nil {
+		t.Errorf("Unable to extract server addresses: %v", err)
+	}
+
+	for network, address := range allAddresses {
+		t.Logf("Addresses on %s: %+v", network, address)
+	}
+
+	allNetworkAddressPages, err := servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName).AllPages()
+	if err != nil {
+		t.Errorf("Unable to list server addresses: %v", err)
+	}
+
+	allNetworkAddresses, err := servers.ExtractNetworkAddresses(allNetworkAddressPages)
+	if err != nil {
+		t.Errorf("Unable to extract server addresses: %v", err)
+	}
+
+	t.Logf("Addresses on %s:", choices.NetworkName)
+	for _, address := range allNetworkAddresses {
+		t.Logf("%+v", address)
+	}
+}
+
+func TestServersCreateDestroyWithExtensions(t *testing.T) {
+	var extendedServer struct {
+		servers.Server
+		availabilityzones.ServerExt
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	err = servers.Get(client, server.ID).ExtractInto(&extendedServer)
+	if err != nil {
+		t.Errorf("Unable to retrieve server: %v", err)
+	}
+	tools.PrintResource(t, extendedServer)
+}
+
+func TestServersWithoutImageRef(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServerWithoutImageRef(t, client)
+	if err != nil {
+		if err400, ok := err.(*gophercloud.ErrUnexpectedResponseCode); ok {
+			if !strings.Contains("Missing imageRef attribute", string(err400.Body)) {
+				defer DeleteServer(t, client, server)
+			}
+		}
+	}
+}
+
+func TestServersUpdate(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	alternateName := tools.RandomString("ACPTTEST", 16)
+	for alternateName == server.Name {
+		alternateName = tools.RandomString("ACPTTEST", 16)
+	}
+
+	t.Logf("Attempting to rename the server to %s.", alternateName)
+
+	updateOpts := servers.UpdateOpts{
+		Name: alternateName,
+	}
+
+	updated, err := servers.Update(client, server.ID, updateOpts).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 TestServersMetadata(t *testing.T) {
+	t.Parallel()
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{
+		"foo":  "bar",
+		"this": "that",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update metadata: %v", err)
+	}
+	t.Logf("UpdateMetadata result: %+v\n", metadata)
+
+	err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete metadatum: %v", err)
+	}
+
+	metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{
+		"foo": "baz",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create metadatum: %v", err)
+	}
+	t.Logf("CreateMetadatum result: %+v\n", metadata)
+
+	metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get metadatum: %v", err)
+	}
+	t.Logf("Metadatum result: %+v\n", metadata)
+	th.AssertEquals(t, "baz", metadata["foo"])
+
+	metadata, err = servers.Metadata(client, server.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get metadata: %v", err)
+	}
+	t.Logf("Metadata result: %+v\n", metadata)
+
+	metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to reset metadata: %v", err)
+	}
+	t.Logf("ResetMetadata result: %+v\n", metadata)
+	th.AssertDeepEquals(t, map[string]string{}, metadata)
+}
+
+func TestServersActionChangeAdminPassword(t *testing.T) {
+	t.Parallel()
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	randomPassword := tools.MakeNewPassword(server.AdminPass)
+	res := servers.ChangeAdminPassword(client, server.ID, randomPassword)
+	if res.Err != nil {
+		t.Fatal(res.Err)
+	}
+
+	if err = WaitForComputeStatus(client, server, "PASSWORD"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestServersActionReboot(t *testing.T) {
+	t.Parallel()
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	rebootOpts := &servers.RebootOpts{
+		Type: servers.SoftReboot,
+	}
+
+	t.Logf("Attempting reboot of server %s", server.ID)
+	res := servers.Reboot(client, server.ID, rebootOpts)
+	if res.Err != nil {
+		t.Fatalf("Unable to reboot server: %v", res.Err)
+	}
+
+	if err = WaitForComputeStatus(client, server, "REBOOT"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestServersActionRebuild(t *testing.T) {
+	t.Parallel()
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	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 = WaitForComputeStatus(client, rebuilt, "REBUILD"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = WaitForComputeStatus(client, rebuilt, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestServersActionResizeConfirm(t *testing.T) {
+	t.Parallel()
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	t.Logf("Attempting to resize server %s", server.ID)
+	ResizeServer(t, client, server)
+
+	t.Logf("Attempting to confirm resize for server %s", server.ID)
+	if res := servers.ConfirmResize(client, server.ID); res.Err != nil {
+		t.Fatal(res.Err)
+	}
+
+	if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestServersActionResizeRevert(t *testing.T) {
+	t.Parallel()
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer DeleteServer(t, client, server)
+
+	t.Logf("Attempting to resize server %s", server.ID)
+	ResizeServer(t, client, server)
+
+	t.Logf("Attempting to revert resize for server %s", server.ID)
+	if res := servers.RevertResize(client, server.ID); res.Err != nil {
+		t.Fatal(res.Err)
+	}
+
+	if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/tenantnetworks_test.go b/acceptance/openstack/compute/v2/tenantnetworks_test.go
new file mode 100644
index 0000000..9b6b527
--- /dev/null
+++ b/acceptance/openstack/compute/v2/tenantnetworks_test.go
@@ -0,0 +1,56 @@
+// +build acceptance compute servers
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
+)
+
+func TestTenantNetworksList(t *testing.T) {
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	allPages, err := tenantnetworks.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	for _, network := range allTenantNetworks {
+		tools.PrintResource(t, network)
+	}
+}
+
+func TestTenantNetworksGet(t *testing.T) {
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	network, err := tenantnetworks.Get(client, networkID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get network %s: %v", networkID, err)
+	}
+
+	tools.PrintResource(t, network)
+}
diff --git a/acceptance/openstack/compute/v2/volumeattach_test.go b/acceptance/openstack/compute/v2/volumeattach_test.go
new file mode 100644
index 0000000..78d85a9
--- /dev/null
+++ b/acceptance/openstack/compute/v2/volumeattach_test.go
@@ -0,0 +1,78 @@
+// +build acceptance compute volumeattach
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestVolumeAttachAttachment(t *testing.T) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	client, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	blockClient, err := clients.NewBlockStorageV1Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+
+	server, err := CreateServer(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer DeleteServer(t, client, server)
+
+	volume, err := createVolume(t, blockClient)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+
+	if err = volumes.WaitForStatus(blockClient, volume.ID, "available", 60); err != nil {
+		t.Fatalf("Unable to wait for volume: %v", err)
+	}
+	defer deleteVolume(t, blockClient, volume)
+
+	volumeAttachment, err := CreateVolumeAttachment(t, client, blockClient, server, volume)
+	if err != nil {
+		t.Fatalf("Unable to attach volume: %v", err)
+	}
+	defer DeleteVolumeAttachment(t, client, blockClient, server, volumeAttachment)
+
+	tools.PrintResource(t, volumeAttachment)
+
+}
+
+func createVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) {
+	volumeName := tools.RandomString("ACPTTEST", 16)
+	createOpts := volumes.CreateOpts{
+		Size: 1,
+		Name: volumeName,
+	}
+
+	volume, err := volumes.Create(blockClient, createOpts).Extract()
+	if err != nil {
+		return volume, err
+	}
+
+	t.Logf("Created volume: %s", volume.ID)
+	return volume, nil
+}
+
+func deleteVolume(t *testing.T, blockClient *gophercloud.ServiceClient, volume *volumes.Volume) {
+	err := volumes.Delete(blockClient, volume.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete volume: %v", err)
+	}
+
+	t.Logf("Deleted volume: %s", volume.ID)
+}
diff --git a/acceptance/openstack/db/v1/common.go b/acceptance/openstack/db/v1/common.go
new file mode 100644
index 0000000..bbe7ebd
--- /dev/null
+++ b/acceptance/openstack/db/v1/common.go
@@ -0,0 +1,70 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/instances"
+	th "github.com/gophercloud/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..c52357a
--- /dev/null
+++ b/acceptance/openstack/db/v1/database_test.go
@@ -0,0 +1,45 @@
+// +build acceptance db
+
+package v1
+
+import (
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/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..6440cc9
--- /dev/null
+++ b/acceptance/openstack/db/v1/flavor_test.go
@@ -0,0 +1,31 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"github.com/gophercloud/gophercloud/openstack/db/v1/flavors"
+	"github.com/gophercloud/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..75668a2
--- /dev/null
+++ b/acceptance/openstack/db/v1/instance_test.go
@@ -0,0 +1,138 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/instances"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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..0f5fcc2
--- /dev/null
+++ b/acceptance/openstack/db/v1/user_test.go
@@ -0,0 +1,70 @@
+// +build acceptance db
+
+package v1
+
+import (
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	u "github.com/gophercloud/gophercloud/openstack/db/v1/users"
+	"github.com/gophercloud/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/dns/v2/zones.go b/acceptance/openstack/dns/v2/zones.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/openstack/dns/v2/zones.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/openstack/dns/v2/zones_test.go b/acceptance/openstack/dns/v2/zones_test.go
new file mode 100644
index 0000000..add964d
--- /dev/null
+++ b/acceptance/openstack/dns/v2/zones_test.go
@@ -0,0 +1,33 @@
+// +build acceptance dns zones
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
+)
+
+func TestZonesList(t *testing.T) {
+	client, err := clients.NewDNSV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a DNS client: %v", err)
+	}
+
+	var allZones []zones.Zone
+	allPages, err := zones.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve zones: %v", err)
+	}
+
+	allZones, err = zones.ExtractZones(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract zones: %v", err)
+	}
+
+	for _, zone := range allZones {
+		tools.PrintResource(t, &zone)
+	}
+}
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
new file mode 100644
index 0000000..c6a2bde
--- /dev/null
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions"
+)
+
+func TestExtensionsList(t *testing.T) {
+	client, err := clients.NewIdentityV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create an identity client: %v", err)
+	}
+
+	allPages, err := extensions.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list extensions: %v", err)
+	}
+
+	allExtensions, err := extensions.ExtractExtensions(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract extensions: %v", err)
+	}
+
+	for _, extension := range allExtensions {
+		tools.PrintResource(t, extension)
+	}
+}
+
+func TestExtensionsGet(t *testing.T) {
+	client, err := clients.NewIdentityV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create an identity client: %v", err)
+	}
+
+	extension, err := extensions.Get(client, "OS-KSCRUD").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get extension OS-KSCRUD: %v", err)
+	}
+
+	tools.PrintResource(t, extension)
+}
diff --git a/acceptance/openstack/identity/v2/identity.go b/acceptance/openstack/identity/v2/identity.go
new file mode 100644
index 0000000..2d3bd09
--- /dev/null
+++ b/acceptance/openstack/identity/v2/identity.go
@@ -0,0 +1,146 @@
+// Package v2 contains common functions for creating identity-based resources
+// for use in acceptance tests. See the `*_test.go` files for example usages.
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+)
+
+// AddUserRole will grant a role to a user in a tenant. An error will be
+// returned if the grant was unsuccessful.
+func AddUserRole(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants.Tenant, user *users.User, role *roles.Role) error {
+	t.Logf("Attempting to grant user %s role %s in tenant %s", user.ID, role.ID, tenant.ID)
+
+	err := roles.AddUser(client, tenant.ID, user.ID, role.ID).ExtractErr()
+	if err != nil {
+		return err
+	}
+
+	t.Logf("Granted user %s role %s in tenant %s", user.ID, role.ID, tenant.ID)
+
+	return nil
+}
+
+// CreateUser will create a user with a random name and adds them to the given
+// tenant. An error will be returned if the user was unable to be created.
+func CreateUser(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants.Tenant) (*users.User, error) {
+	userName := tools.RandomString("user_", 5)
+	userEmail := userName + "@foo.com"
+	t.Logf("Creating user: %s", userName)
+
+	createOpts := users.CreateOpts{
+		Name:     userName,
+		Enabled:  gophercloud.Disabled,
+		TenantID: tenant.ID,
+		Email:    userEmail,
+	}
+
+	user, err := users.Create(client, createOpts).Extract()
+	if err != nil {
+		return user, err
+	}
+
+	return user, nil
+}
+
+// DeleteUser will delete a user. A fatal error will occur if the delete was
+// unsuccessful. This works best when used as a deferred function.
+func DeleteUser(t *testing.T, client *gophercloud.ServiceClient, user *users.User) {
+	t.Logf("Attempting to delete user: %s", user.Name)
+
+	result := users.Delete(client, user.ID)
+	if result.Err != nil {
+		t.Fatalf("Unable to delete user")
+	}
+
+	t.Logf("Deleted user: %s", user.Name)
+}
+
+// DeleteUserRole will revoke a role of a user in a tenant. A fatal error will
+// occur if the revoke was unsuccessful. This works best when used as a
+// deferred function.
+func DeleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants.Tenant, user *users.User, role *roles.Role) {
+	t.Logf("Attempting to remove role %s from user %s in tenant %s", role.ID, user.ID, tenant.ID)
+
+	err := roles.DeleteUser(client, tenant.ID, user.ID, role.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to remove role")
+	}
+
+	t.Logf("Removed role %s from user %s in tenant %s", role.ID, user.ID, tenant.ID)
+}
+
+// FindRole finds all roles that the current authenticated client has access
+// to and returns the first one found. An error will be returned if the lookup
+// was unsuccessful.
+func FindRole(t *testing.T, client *gophercloud.ServiceClient) (*roles.Role, error) {
+	var role *roles.Role
+
+	allPages, err := roles.List(client).AllPages()
+	if err != nil {
+		return role, err
+	}
+
+	allRoles, err := roles.ExtractRoles(allPages)
+	if err != nil {
+		return role, err
+	}
+
+	for _, r := range allRoles {
+		role = &r
+		break
+	}
+
+	return role, nil
+}
+
+// FindTenant finds all tenants that the current authenticated client has access
+// to and returns the first one found. An error will be returned if the lookup
+// was unsuccessful.
+func FindTenant(t *testing.T, client *gophercloud.ServiceClient) (*tenants.Tenant, error) {
+	var tenant *tenants.Tenant
+
+	allPages, err := tenants.List(client, nil).AllPages()
+	if err != nil {
+		return tenant, err
+	}
+
+	allTenants, err := tenants.ExtractTenants(allPages)
+	if err != nil {
+		return tenant, err
+	}
+
+	for _, t := range allTenants {
+		tenant = &t
+		break
+	}
+
+	return tenant, nil
+}
+
+// UpdateUser will update an existing user with a new randomly generated name.
+// An error will be returned if the update was unsuccessful.
+func UpdateUser(t *testing.T, client *gophercloud.ServiceClient, user *users.User) (*users.User, error) {
+	userName := tools.RandomString("user_", 5)
+	userEmail := userName + "@foo.com"
+
+	t.Logf("Attempting to update user name from %s to %s", user.Name, userName)
+
+	updateOpts := users.UpdateOpts{
+		Name:  userName,
+		Email: userEmail,
+	}
+
+	newUser, err := users.Update(client, user.ID, updateOpts).Extract()
+	if err != nil {
+		return newUser, err
+	}
+
+	return newUser, nil
+}
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..83fbd31
--- /dev/null
+++ b/acceptance/openstack/identity/v2/role_test.go
@@ -0,0 +1,77 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+)
+
+func TestRolesAddToUser(t *testing.T) {
+	client, err := clients.NewIdentityV2AdminClient()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	tenant, err := FindTenant(t, client)
+	if err != nil {
+		t.Fatalf("Unable to get a tenant: %v", err)
+	}
+
+	role, err := FindRole(t, client)
+	if err != nil {
+		t.Fatalf("Unable to get a role: %v", err)
+	}
+
+	user, err := CreateUser(t, client, tenant)
+	if err != nil {
+		t.Fatalf("Unable to create a user: %v", err)
+	}
+	defer DeleteUser(t, client, user)
+
+	err = AddUserRole(t, client, tenant, user, role)
+	if err != nil {
+		t.Fatalf("Unable to add role to user: %v", err)
+	}
+	defer DeleteUserRole(t, client, tenant, user, role)
+
+	allPages, err := users.ListRoles(client, tenant.ID, user.ID).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to obtain roles for user: %v", err)
+	}
+
+	allRoles, err := users.ExtractRoles(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract roles: %v", err)
+	}
+
+	t.Logf("Roles of user %s:", user.Name)
+	for _, role := range allRoles {
+		tools.PrintResource(t, role)
+	}
+}
+
+func TestRolesList(t *testing.T) {
+	client, err := clients.NewIdentityV2AdminClient()
+	if err != nil {
+		t.Fatalf("Unable to create an identity client: %v", err)
+	}
+
+	allPages, err := roles.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list all roles: %v", err)
+	}
+
+	allRoles, err := roles.ExtractRoles(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract roles: %v", err)
+	}
+
+	for _, r := range allRoles {
+		tools.PrintResource(t, r)
+	}
+}
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
new file mode 100644
index 0000000..2053614
--- /dev/null
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -0,0 +1,32 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+)
+
+func TestTenantsList(t *testing.T) {
+	client, err := clients.NewIdentityV2Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	allPages, err := tenants.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list tenants: %v", err)
+	}
+
+	allTenants, err := tenants.ExtractTenants(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract tenants: %v", err)
+	}
+
+	for _, tenant := range allTenants {
+		tools.PrintResource(t, tenant)
+	}
+}
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
new file mode 100644
index 0000000..82a317a
--- /dev/null
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -0,0 +1,69 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+)
+
+func TestTokenAuthenticate(t *testing.T) {
+	client, err := clients.NewIdentityV2UnauthenticatedClient()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	authOptions, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to obtain authentication options: %v", err)
+	}
+
+	result := tokens.Create(client, authOptions)
+	token, err := result.ExtractToken()
+	if err != nil {
+		t.Fatalf("Unable to extract token: %v", err)
+	}
+
+	tools.PrintResource(t, token)
+
+	catalog, err := result.ExtractServiceCatalog()
+	if err != nil {
+		t.Fatalf("Unable to extract service catalog: %v", err)
+	}
+
+	for _, entry := range catalog.Entries {
+		tools.PrintResource(t, entry)
+	}
+}
+
+func TestTokenValidate(t *testing.T) {
+	client, err := clients.NewIdentityV2Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	authOptions, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to obtain authentication options: %v", err)
+	}
+
+	result := tokens.Create(client, authOptions)
+	token, err := result.ExtractToken()
+	if err != nil {
+		t.Fatalf("Unable to extract token: %v", err)
+	}
+
+	tools.PrintResource(t, token)
+
+	getResult := tokens.Get(client, token.ID)
+	user, err := getResult.ExtractUser()
+	if err != nil {
+		t.Fatalf("Unable to extract user: %v", err)
+	}
+
+	tools.PrintResource(t, user)
+}
diff --git a/acceptance/openstack/identity/v2/user_test.go b/acceptance/openstack/identity/v2/user_test.go
new file mode 100644
index 0000000..faa5bba
--- /dev/null
+++ b/acceptance/openstack/identity/v2/user_test.go
@@ -0,0 +1,59 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+)
+
+func TestUsersList(t *testing.T) {
+	client, err := clients.NewIdentityV2AdminClient()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	allPages, err := users.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list users: %v", err)
+	}
+
+	allUsers, err := users.ExtractUsers(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract users: %v", err)
+	}
+
+	for _, user := range allUsers {
+		tools.PrintResource(t, user)
+	}
+}
+
+func TestUsersCreateUpdateDelete(t *testing.T) {
+	client, err := clients.NewIdentityV2AdminClient()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	tenant, err := FindTenant(t, client)
+	if err != nil {
+		t.Fatalf("Unable to get a tenant: %v", err)
+	}
+
+	user, err := CreateUser(t, client, tenant)
+	if err != nil {
+		t.Fatalf("Unable to create a user: %v", err)
+	}
+	defer DeleteUser(t, client, user)
+
+	tools.PrintResource(t, user)
+
+	newUser, err := UpdateUser(t, client, user)
+	if err != nil {
+		t.Fatalf("Unable to update user: %v", err)
+	}
+
+	tools.PrintResource(t, newUser)
+}
diff --git a/acceptance/openstack/identity/v3/endpoint_test.go b/acceptance/openstack/identity/v3/endpoint_test.go
new file mode 100644
index 0000000..a589970
--- /dev/null
+++ b/acceptance/openstack/identity/v3/endpoint_test.go
@@ -0,0 +1,86 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
+)
+
+func TestEndpointsList(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	allPages, err := endpoints.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list endpoints: %v", err)
+	}
+
+	allEndpoints, err := endpoints.ExtractEndpoints(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract endpoints: %v", err)
+	}
+
+	for _, endpoint := range allEndpoints {
+		tools.PrintResource(t, endpoint)
+	}
+}
+
+func TestEndpointsNavigateCatalog(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	// Discover the service we're interested in.
+	serviceListOpts := services.ListOpts{
+		ServiceType: "compute",
+	}
+
+	allPages, err := services.List(client, serviceListOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to lookup compute service: %v", err)
+	}
+
+	allServices, err := services.ExtractServices(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract service: %v")
+	}
+
+	if len(allServices) != 1 {
+		t.Fatalf("Expected one service, got %d", len(allServices))
+	}
+
+	computeService := allServices[0]
+	tools.PrintResource(t, computeService)
+
+	// Enumerate the endpoints available for this service.
+	endpointListOpts := endpoints.ListOpts{
+		Availability: gophercloud.AvailabilityPublic,
+		ServiceID:    computeService.ID,
+	}
+
+	allPages, err = endpoints.List(client, endpointListOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to lookup compute endpoint: %v", err)
+	}
+
+	allEndpoints, err := endpoints.ExtractEndpoints(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract endpoint: %v")
+	}
+
+	if len(allEndpoints) != 1 {
+		t.Fatalf("Expected one endpoint, got %d", len(allEndpoints))
+	}
+
+	tools.PrintResource(t, allEndpoints[0])
+
+}
diff --git a/acceptance/openstack/identity/v3/identity.go b/acceptance/openstack/identity/v3/identity.go
new file mode 100644
index 0000000..3276efc
--- /dev/null
+++ b/acceptance/openstack/identity/v3/identity.go
@@ -0,0 +1,48 @@
+package v3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
+)
+
+// CreateProject will create a project with a random name.
+// It takes an optional createOpts parameter since creating a project
+// has so many options. An error will be returned if the project was
+// unable to be created.
+func CreateProject(t *testing.T, client *gophercloud.ServiceClient, c *projects.CreateOpts) (*projects.Project, error) {
+	name := tools.RandomString("ACPTTEST", 8)
+	t.Logf("Attempting to create project: %s", name)
+
+	var createOpts projects.CreateOpts
+	if c != nil {
+		createOpts = *c
+	} else {
+		createOpts = projects.CreateOpts{}
+	}
+
+	createOpts.Name = name
+
+	project, err := projects.Create(client, createOpts).Extract()
+	if err != nil {
+		return project, err
+	}
+
+	t.Logf("Successfully created project %s with ID %s", name, project.ID)
+
+	return project, nil
+}
+
+// DeleteProject will delete a project by ID. A fatal error will occur if
+// the project ID failed to be deleted. This works best when using it as
+// a deferred function.
+func DeleteProject(t *testing.T, client *gophercloud.ServiceClient, projectID string) {
+	err := projects.Delete(client, projectID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete project %s: %v", projectID, err)
+	}
+
+	t.Logf("Deleted project: %s", projectID)
+}
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/projects_test.go b/acceptance/openstack/identity/v3/projects_test.go
new file mode 100644
index 0000000..8325bee
--- /dev/null
+++ b/acceptance/openstack/identity/v3/projects_test.go
@@ -0,0 +1,158 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
+)
+
+func TestProjectsList(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	var iTrue bool = true
+	listOpts := projects.ListOpts{
+		Enabled: &iTrue,
+	}
+
+	allPages, err := projects.List(client, listOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list projects: %v", err)
+	}
+
+	allProjects, err := projects.ExtractProjects(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract projects: %v", err)
+	}
+
+	for _, project := range allProjects {
+		tools.PrintResource(t, project)
+	}
+}
+
+func TestProjectsGet(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v", err)
+	}
+
+	allPages, err := projects.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list projects: %v", err)
+	}
+
+	allProjects, err := projects.ExtractProjects(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract projects: %v", err)
+	}
+
+	project := allProjects[0]
+	p, err := projects.Get(client, project.ID, nil).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get project: %v", err)
+	}
+
+	tools.PrintResource(t, p)
+}
+
+func TestProjectsCRUD(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	project, err := CreateProject(t, client, nil)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+	defer DeleteProject(t, client, project.ID)
+
+	tools.PrintResource(t, project)
+
+	var iFalse bool = false
+	updateOpts := projects.UpdateOpts{
+		Enabled: &iFalse,
+	}
+
+	updatedProject, err := projects.Update(client, project.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update project: %v", err)
+	}
+
+	tools.PrintResource(t, updatedProject)
+}
+
+func TestProjectsDomain(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	var iTrue = true
+	createOpts := projects.CreateOpts{
+		IsDomain: &iTrue,
+	}
+
+	projectDomain, err := CreateProject(t, client, &createOpts)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+	defer DeleteProject(t, client, projectDomain.ID)
+
+	tools.PrintResource(t, projectDomain)
+
+	createOpts = projects.CreateOpts{
+		DomainID: projectDomain.ID,
+	}
+
+	project, err := CreateProject(t, client, &createOpts)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+	defer DeleteProject(t, client, project.ID)
+
+	tools.PrintResource(t, project)
+
+	var iFalse = false
+	updateOpts := projects.UpdateOpts{
+		Enabled: &iFalse,
+	}
+
+	_, err = projects.Update(client, projectDomain.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to disable domain: %v")
+	}
+}
+
+func TestProjectsNested(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	projectMain, err := CreateProject(t, client, nil)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+	defer DeleteProject(t, client, projectMain.ID)
+
+	tools.PrintResource(t, projectMain)
+
+	createOpts := projects.CreateOpts{
+		ParentID: projectMain.ID,
+	}
+
+	project, err := CreateProject(t, client, &createOpts)
+	if err != nil {
+		t.Fatalf("Unable to create project: %v", err)
+	}
+	defer DeleteProject(t, client, project.ID)
+
+	tools.PrintResource(t, project)
+}
diff --git a/acceptance/openstack/identity/v3/service_test.go b/acceptance/openstack/identity/v3/service_test.go
new file mode 100644
index 0000000..7a0c71f
--- /dev/null
+++ b/acceptance/openstack/identity/v3/service_test.go
@@ -0,0 +1,33 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
+)
+
+func TestServicesList(t *testing.T) {
+	client, err := clients.NewIdentityV3Client()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	allPages, err := services.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list services: %v", err)
+	}
+
+	allServices, err := services.ExtractServices(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract services: %v", err)
+	}
+
+	for _, service := range allServices {
+		tools.PrintResource(t, service)
+	}
+
+}
diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go
new file mode 100644
index 0000000..e0f90d9
--- /dev/null
+++ b/acceptance/openstack/identity/v3/token_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+)
+
+func TestGetToken(t *testing.T) {
+	client, err := clients.NewIdentityV3UnauthenticatedClient()
+	if err != nil {
+		t.Fatalf("Unable to obtain an identity client: %v")
+	}
+
+	ao, err := openstack.AuthOptionsFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to obtain environment auth options: %v", err)
+	}
+
+	authOptions := tokens.AuthOptions{
+		Username:   ao.Username,
+		Password:   ao.Password,
+		DomainName: "default",
+	}
+
+	token, err := tokens.Create(client, &authOptions).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get token: %v", err)
+	}
+
+	tools.PrintResource(t, token)
+}
diff --git a/acceptance/openstack/imageservice/v2/images_test.go b/acceptance/openstack/imageservice/v2/images_test.go
new file mode 100644
index 0000000..aa0390b
--- /dev/null
+++ b/acceptance/openstack/imageservice/v2/images_test.go
@@ -0,0 +1,26 @@
+// +build acceptance imageservice images
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+)
+
+func TestImagesCreateDestroyEmptyImage(t *testing.T) {
+	client, err := clients.NewImageServiceV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create an image service client: %v", err)
+	}
+
+	image, err := CreateEmptyImage(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create empty image: %v", err)
+	}
+
+	defer DeleteImage(t, client, image)
+
+	tools.PrintResource(t, image)
+}
diff --git a/acceptance/openstack/imageservice/v2/imageservice.go b/acceptance/openstack/imageservice/v2/imageservice.go
new file mode 100644
index 0000000..8aaeeb7
--- /dev/null
+++ b/acceptance/openstack/imageservice/v2/imageservice.go
@@ -0,0 +1,55 @@
+// Package v2 contains common functions for creating imageservice resources
+// for use in acceptance tests. See the `*_test.go` files for example usages.
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
+)
+
+// CreateEmptyImage will create an image, but with no actual image data.
+// An error will be returned if an image was unable to be created.
+func CreateEmptyImage(t *testing.T, client *gophercloud.ServiceClient) (*images.Image, error) {
+	var image *images.Image
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create image: %s", name)
+
+	protected := false
+	visibility := images.ImageVisibilityPrivate
+	createOpts := &images.CreateOpts{
+		Name:            name,
+		ContainerFormat: "bare",
+		DiskFormat:      "qcow2",
+		MinDisk:         0,
+		MinRAM:          0,
+		Protected:       &protected,
+		Visibility:      &visibility,
+		Properties: map[string]string{
+			"architecture": "x86_64",
+		},
+	}
+
+	image, err := images.Create(client, createOpts).Extract()
+	if err != nil {
+		return image, err
+	}
+
+	t.Logf("Created image %s: %#v", name, image)
+	return image, nil
+}
+
+// DeleteImage deletes an image.
+// A fatal error will occur if the image failed to delete. This works best when
+// used as a deferred function.
+func DeleteImage(t *testing.T, client *gophercloud.ServiceClient, image *images.Image) {
+	err := images.Delete(client, image.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete image %s: %v", image.ID, err)
+	}
+
+	t.Logf("Deleted image: %s", image.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..c6f8f26
--- /dev/null
+++ b/acceptance/openstack/networking/v2/apiversion_test.go
@@ -0,0 +1,53 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions"
+)
+
+func TestAPIVersionsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := apiversions.ListVersions(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list api versions: %v", err)
+	}
+
+	allAPIVersions, err := apiversions.ExtractAPIVersions(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract api versions: %v", err)
+	}
+
+	for _, apiVersion := range allAPIVersions {
+		tools.PrintResource(t, apiVersion)
+	}
+}
+
+func TestAPIResourcesList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := apiversions.ListVersionResources(client, "v2.0").AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list api version reosources: %v", err)
+	}
+
+	allVersionResources, err := apiversions.ExtractVersionResources(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract version resources: %v", err)
+	}
+
+	for _, versionResource := range allVersionResources {
+		tools.PrintResource(t, versionResource)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extension_test.go b/acceptance/openstack/networking/v2/extension_test.go
new file mode 100644
index 0000000..5609e85
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance networking extensions
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/common/extensions"
+)
+
+func TestExtensionsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := extensions.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list extensions: %v", err)
+	}
+
+	allExtensions, err := extensions.ExtractExtensions(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract extensions: %v", err)
+	}
+
+	for _, extension := range allExtensions {
+		tools.PrintResource(t, extension)
+	}
+}
+
+func TestExtensionGet(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	extension, err := extensions.Get(client, "router").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get extension port-security: %v", err)
+	}
+
+	tools.PrintResource(t, extension)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/extensions.go b/acceptance/openstack/networking/v2/extensions/extensions.go
new file mode 100644
index 0000000..154e34e
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/extensions.go
@@ -0,0 +1,138 @@
+package extensions
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+// CreateExternalNetwork will create an external network. An error will be
+// returned if the creation failed.
+func CreateExternalNetwork(t *testing.T, client *gophercloud.ServiceClient) (*networks.Network, error) {
+	networkName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create external network: %s", networkName)
+
+	adminStateUp := true
+	isExternal := true
+	createOpts := external.CreateOpts{
+		External: &isExternal,
+	}
+
+	createOpts.Name = networkName
+	createOpts.AdminStateUp = &adminStateUp
+
+	network, err := networks.Create(client, createOpts).Extract()
+	if err != nil {
+		return network, err
+	}
+
+	t.Logf("Created external network: %s", networkName)
+
+	return network, nil
+}
+
+// CreatePortWithSecurityGroup will create a port with a security group
+// attached. An error will be returned if the port could not be created.
+func CreatePortWithSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID, secGroupID string) (*ports.Port, error) {
+	portName := tools.RandomString("TESTACC-", 8)
+	iFalse := false
+
+	t.Logf("Attempting to create port: %s", portName)
+
+	createOpts := ports.CreateOpts{
+		NetworkID:      networkID,
+		Name:           portName,
+		AdminStateUp:   &iFalse,
+		FixedIPs:       []ports.IP{ports.IP{SubnetID: subnetID}},
+		SecurityGroups: []string{secGroupID},
+	}
+
+	port, err := ports.Create(client, createOpts).Extract()
+	if err != nil {
+		return port, err
+	}
+
+	t.Logf("Successfully created port: %s", portName)
+
+	return port, nil
+}
+
+// CreateSecurityGroup will create a security group with a random name.
+// An error will be returned if one was failed to be created.
+func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (*groups.SecGroup, error) {
+	secGroupName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create security group: %s", secGroupName)
+
+	createOpts := groups.CreateOpts{
+		Name: secGroupName,
+	}
+
+	secGroup, err := groups.Create(client, createOpts).Extract()
+	if err != nil {
+		return secGroup, err
+	}
+
+	t.Logf("Created security group: %s", secGroup.ID)
+
+	return secGroup, nil
+}
+
+// CreateSecurityGroupRule will create a security group rule with a random name
+// and random port between 80 and 99.
+// An error will be returned if one was failed to be created.
+func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, secGroupID string) (*rules.SecGroupRule, error) {
+	t.Logf("Attempting to create security group rule in group: %s", secGroupID)
+
+	fromPort := tools.RandomInt(80, 89)
+	toPort := tools.RandomInt(90, 99)
+
+	createOpts := rules.CreateOpts{
+		Direction:    "ingress",
+		EtherType:    "IPv4",
+		SecGroupID:   secGroupID,
+		PortRangeMin: fromPort,
+		PortRangeMax: toPort,
+		Protocol:     rules.ProtocolTCP,
+	}
+
+	rule, err := rules.Create(client, createOpts).Extract()
+	if err != nil {
+		return rule, err
+	}
+
+	t.Logf("Created security group rule: %s", rule.ID)
+
+	return rule, nil
+}
+
+// DeleteSecurityGroup will delete a security group of a specified ID.
+// A fatal error will occur if the deletion failed. This works best as a
+// deferred function
+func DeleteSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, secGroupID string) {
+	t.Logf("Attempting to delete security group: %s", secGroupID)
+
+	err := groups.Delete(client, secGroupID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete security group: %v", err)
+	}
+}
+
+// DeleteSecurityGroupRule will delete a security group rule of a specified ID.
+// A fatal error will occur if the deletion failed. This works best as a
+// deferred function
+func DeleteSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, ruleID string) {
+	t.Logf("Attempting to delete security group rule: %s", ruleID)
+
+	err := rules.Delete(client, ruleID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete security group rule: %v", err)
+	}
+}
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..473013b
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
@@ -0,0 +1,99 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	layer3 "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion"
+)
+
+func TestFirewallList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := firewalls.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list firewalls: %v", err)
+	}
+
+	allFirewalls, err := firewalls.ExtractFirewalls(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract firewalls: %v", err)
+	}
+
+	for _, firewall := range allFirewalls {
+		tools.PrintResource(t, firewall)
+	}
+}
+
+func TestFirewallCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	router, err := layer3.CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer layer3.DeleteRouter(t, client, router.ID)
+
+	rule, err := CreateRule(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create rule: %v", err)
+	}
+	defer DeleteRule(t, client, rule.ID)
+
+	tools.PrintResource(t, rule)
+
+	policy, err := CreatePolicy(t, client, rule.ID)
+	if err != nil {
+		t.Fatalf("Unable to create policy: %v", err)
+	}
+	defer DeletePolicy(t, client, policy.ID)
+
+	tools.PrintResource(t, policy)
+
+	firewall, err := CreateFirewallOnRouter(t, client, policy.ID, router.ID)
+	if err != nil {
+		t.Fatalf("Unable to create firewall: %v", err)
+	}
+	defer DeleteFirewall(t, client, firewall.ID)
+
+	tools.PrintResource(t, firewall)
+
+	router2, err := layer3.CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer layer3.DeleteRouter(t, client, router2.ID)
+
+	firewallUpdateOpts := firewalls.UpdateOpts{
+		PolicyID:    policy.ID,
+		Description: "Some firewall description",
+	}
+
+	updateOpts := routerinsertion.UpdateOptsExt{
+		firewallUpdateOpts,
+		[]string{router2.ID},
+	}
+
+	_, err = firewalls.Update(client, firewall.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update firewall: %v", err)
+	}
+
+	newFirewall, err := firewalls.Get(client, firewall.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get firewall: %v", err)
+	}
+
+	tools.PrintResource(t, newFirewall)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go b/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
new file mode 100644
index 0000000..204565b
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
@@ -0,0 +1,203 @@
+package fwaas
+
+import (
+	"fmt"
+	"strconv"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+)
+
+// CreateFirewall will create a Firewaill with a random name and a specified
+// policy ID. An error will be returned if the firewall could not be created.
+func CreateFirewall(t *testing.T, client *gophercloud.ServiceClient, policyID string) (*firewalls.Firewall, error) {
+	firewallName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create firewall %s", firewallName)
+
+	iTrue := true
+	createOpts := firewalls.CreateOpts{
+		Name:         firewallName,
+		PolicyID:     policyID,
+		AdminStateUp: &iTrue,
+	}
+
+	firewall, err := firewalls.Create(client, createOpts).Extract()
+	if err != nil {
+		return firewall, err
+	}
+
+	t.Logf("Waiting for firewall to become active.")
+	if err := WaitForFirewallState(client, firewall.ID, "ACTIVE", 60); err != nil {
+		return firewall, err
+	}
+
+	t.Logf("Successfully created firewall %s", firewallName)
+
+	return firewall, nil
+}
+
+// CreateFirewallOnRouter will create a Firewall with a random name and a
+// specified policy ID attached to a specified Router. An error will be
+// returned if the firewall could not be created.
+func CreateFirewallOnRouter(t *testing.T, client *gophercloud.ServiceClient, policyID string, routerID string) (*firewalls.Firewall, error) {
+	firewallName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create firewall %s", firewallName)
+
+	firewallCreateOpts := firewalls.CreateOpts{
+		Name:     firewallName,
+		PolicyID: policyID,
+	}
+
+	createOpts := routerinsertion.CreateOptsExt{
+		firewallCreateOpts,
+		[]string{routerID},
+	}
+
+	firewall, err := firewalls.Create(client, createOpts).Extract()
+	if err != nil {
+		return firewall, err
+	}
+
+	t.Logf("Waiting for firewall to become active.")
+	if err := WaitForFirewallState(client, firewall.ID, "ACTIVE", 60); err != nil {
+		return firewall, err
+	}
+
+	t.Logf("Successfully created firewall %s", firewallName)
+
+	return firewall, nil
+}
+
+// CreatePolicy will create a Firewall Policy with a random name and given
+// rule. An error will be returned if the rule could not be created.
+func CreatePolicy(t *testing.T, client *gophercloud.ServiceClient, ruleID string) (*policies.Policy, error) {
+	policyName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create policy %s", policyName)
+
+	createOpts := policies.CreateOpts{
+		Name: policyName,
+		Rules: []string{
+			ruleID,
+		},
+	}
+
+	policy, err := policies.Create(client, createOpts).Extract()
+	if err != nil {
+		return policy, err
+	}
+
+	t.Logf("Successfully created policy %s", policyName)
+
+	return policy, nil
+}
+
+// CreateRule will create a Firewall Rule with a random source address and
+//source port, destination address and port. An error will be returned if
+// the rule could not be created.
+func CreateRule(t *testing.T, client *gophercloud.ServiceClient) (*rules.Rule, error) {
+	ruleName := tools.RandomString("TESTACC-", 8)
+	sourceAddress := fmt.Sprintf("192.168.1.%d", tools.RandomInt(1, 100))
+	sourcePort := strconv.Itoa(tools.RandomInt(1, 100))
+	destinationAddress := fmt.Sprintf("192.168.2.%d", tools.RandomInt(1, 100))
+	destinationPort := strconv.Itoa(tools.RandomInt(1, 100))
+
+	t.Logf("Attempting to create rule %s with source %s:%s and destination %s:%s",
+		ruleName, sourceAddress, sourcePort, destinationAddress, destinationPort)
+
+	createOpts := rules.CreateOpts{
+		Name:                 ruleName,
+		Protocol:             rules.ProtocolTCP,
+		Action:               "allow",
+		SourceIPAddress:      sourceAddress,
+		SourcePort:           sourcePort,
+		DestinationIPAddress: destinationAddress,
+		DestinationPort:      destinationPort,
+	}
+
+	rule, err := rules.Create(client, createOpts).Extract()
+	if err != nil {
+		return rule, err
+	}
+
+	t.Logf("Rule %s successfully created", ruleName)
+
+	return rule, nil
+}
+
+// DeleteFirewall will delete a firewall with a specified ID. A fatal error
+// will occur if the delete was not successful. This works best when used as
+// a deferred function.
+func DeleteFirewall(t *testing.T, client *gophercloud.ServiceClient, firewallID string) {
+	t.Logf("Attempting to delete firewall: %s", firewallID)
+
+	err := firewalls.Delete(client, firewallID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete firewall %s: %v", firewallID, err)
+	}
+
+	t.Logf("Waiting for firewall to delete.")
+	if err := WaitForFirewallState(client, firewallID, "DELETED", 60); err != nil {
+		t.Logf("Unable to delete firewall: %s", firewallID)
+	}
+
+	t.Logf("Firewall deleted: %s", firewallID)
+}
+
+// DeletePolicy will delete a policy with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeletePolicy(t *testing.T, client *gophercloud.ServiceClient, policyID string) {
+	t.Logf("Attempting to delete policy: %s", policyID)
+
+	err := policies.Delete(client, policyID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete policy %s: %v", policyID, err)
+	}
+
+	t.Logf("Deleted policy: %s", policyID)
+}
+
+// DeleteRule will delete a rule with a specified ID. A fatal error will occur
+// if the delete was not successful. This works best when used as a deferred
+// function.
+func DeleteRule(t *testing.T, client *gophercloud.ServiceClient, ruleID string) {
+	t.Logf("Attempting to delete rule: %s", ruleID)
+
+	err := rules.Delete(client, ruleID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete rule %s: %v", ruleID, err)
+	}
+
+	t.Logf("Deleted rule: %s", ruleID)
+}
+
+// WaitForFirewallState will wait until a firewall reaches a given state.
+func WaitForFirewallState(client *gophercloud.ServiceClient, firewallID, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := firewalls.Get(client, firewallID).Extract()
+		if err != nil {
+			if httpStatus, ok := err.(gophercloud.ErrDefault404); ok {
+				if httpStatus.Actual == 404 {
+					if status == "DELETED" {
+						return true, nil
+					}
+				}
+			}
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
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..3220d82
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go
@@ -0,0 +1,71 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+)
+
+func TestPolicyList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := policies.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list policies: %v", err)
+	}
+
+	allPolicies, err := policies.ExtractPolicies(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract policies: %v", err)
+	}
+
+	for _, policy := range allPolicies {
+		tools.PrintResource(t, policy)
+	}
+}
+
+func TestPolicyCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	rule, err := CreateRule(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create rule: %v", err)
+	}
+	defer DeleteRule(t, client, rule.ID)
+
+	tools.PrintResource(t, rule)
+
+	policy, err := CreatePolicy(t, client, rule.ID)
+	if err != nil {
+		t.Fatalf("Unable to create policy: %v", err)
+	}
+	defer DeletePolicy(t, client, policy.ID)
+
+	tools.PrintResource(t, policy)
+
+	updateOpts := policies.UpdateOpts{
+		Description: "Some policy description",
+	}
+
+	_, err = policies.Update(client, policy.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update policy: %v", err)
+	}
+
+	newPolicy, err := policies.Get(client, policy.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get policy: %v", err)
+	}
+
+	tools.PrintResource(t, newPolicy)
+}
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..4521a60
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go
@@ -0,0 +1,64 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+)
+
+func TestRuleList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := rules.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list rules: %v", err)
+	}
+
+	allRules, err := rules.ExtractRules(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract rules: %v", err)
+	}
+
+	for _, rule := range allRules {
+		tools.PrintResource(t, rule)
+	}
+}
+
+func TestRuleCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	rule, err := CreateRule(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create rule: %v", err)
+	}
+	defer DeleteRule(t, client, rule.ID)
+
+	tools.PrintResource(t, rule)
+
+	ruleDescription := "Some rule description"
+	updateOpts := rules.UpdateOpts{
+		Description: &ruleDescription,
+	}
+
+	_, err = rules.Update(client, rule.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update rule: %v", err)
+	}
+
+	newRule, err := rules.Get(client, rule.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get rule: %v", err)
+	}
+
+	tools.PrintResource(t, newRule)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go b/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go
new file mode 100644
index 0000000..8c1562d
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go
@@ -0,0 +1,98 @@
+// +build acceptance networking layer3 floatingips
+
+package layer3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+)
+
+func TestLayer3FloatingIPsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	listOpts := floatingips.ListOpts{}
+	allPages, err := floatingips.List(client, listOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list floating IPs: %v", err)
+	}
+
+	allFIPs, err := floatingips.ExtractFloatingIPs(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract floating IPs: %v", err)
+	}
+
+	for _, fip := range allFIPs {
+		tools.PrintResource(t, fip)
+	}
+}
+
+func TestLayer3FloatingIPsCreateDelete(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to get choices: %v", err)
+	}
+
+	netid, err := networks.IDFromName(client,chocices.NetworkName)
+	if err != nil {
+		t.Fatalf("Unable to find network id: %v", err)
+	}
+
+	subnet, err := networking.CreateSubnet(t, client, netid)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	router, err := CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer DeleteRouter(t, client, router.ID)
+
+	port, err := networking.CreatePort(t, client, netid, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+
+	_, err = CreateRouterInterface(t, client, port.ID, router.ID)
+	if err != nil {
+		t.Fatalf("Unable to create router interface: %v", err)
+	}
+	defer DeleteRouterInterface(t, client, port.ID, router.ID)
+
+	fip, err := CreateFloatingIP(t, client, choices.ExternalNetworkID, port.ID)
+	if err != nil {
+		t.Fatalf("Unable to create floating IP: %v", err)
+	}
+	defer DeleteFloatingIP(t, client, fip.ID)
+
+	newFip, err := floatingips.Get(client, fip.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get floating ip: %v", err)
+	}
+
+	tools.PrintResource(t, newFip)
+
+	// Disassociate the floating IP
+	updateOpts := floatingips.UpdateOpts{
+		PortID: nil,
+	}
+
+	newFip, err = floatingips.Update(client, fip.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to disassociate floating IP: %v", err)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extensions/layer3/layer3.go b/acceptance/openstack/networking/v2/extensions/layer3/layer3.go
new file mode 100644
index 0000000..7d3a626
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/layer3/layer3.go
@@ -0,0 +1,248 @@
+package layer3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+// CreateFloatingIP creates a floating IP on a given network and port. An error
+// will be returned if the creation failed.
+func CreateFloatingIP(t *testing.T, client *gophercloud.ServiceClient, networkID, portID string) (*floatingips.FloatingIP, error) {
+	t.Logf("Attempting to create floating IP on port: %s", portID)
+
+	createOpts := &floatingips.CreateOpts{
+		FloatingNetworkID: networkID,
+		PortID:            portID,
+	}
+
+	floatingIP, err := floatingips.Create(client, createOpts).Extract()
+	if err != nil {
+		return floatingIP, err
+	}
+
+	t.Logf("Created floating IP.")
+
+	return floatingIP, err
+}
+
+// CreateExternalRouter creates a router on the external network. This requires
+// the OS_EXTGW_ID environment variable to be set. An error is returned if the
+// creation failed.
+func CreateExternalRouter(t *testing.T, client *gophercloud.ServiceClient) (*routers.Router, error) {
+	var router *routers.Router
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		return router, err
+	}
+
+	routerName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create external router: %s", routerName)
+
+	adminStateUp := true
+	gatewayInfo := routers.GatewayInfo{
+		NetworkID: choices.ExternalNetworkID,
+	}
+
+	createOpts := routers.CreateOpts{
+		Name:         routerName,
+		AdminStateUp: &adminStateUp,
+		GatewayInfo:  &gatewayInfo,
+	}
+
+	router, err = routers.Create(client, createOpts).Extract()
+	if err != nil {
+		return router, err
+	}
+
+	if err := WaitForRouterToCreate(client, router.ID, 60); err != nil {
+		return router, err
+	}
+
+	t.Logf("Created router: %s", routerName)
+
+	return router, nil
+}
+
+// CreateRouter creates a router on a specified Network ID. An error will be
+// returned if the creation failed.
+func CreateRouter(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*routers.Router, error) {
+	routerName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create router: %s", routerName)
+
+	adminStateUp := true
+	gatewayInfo := routers.GatewayInfo{
+		NetworkID: networkID,
+	}
+
+	createOpts := routers.CreateOpts{
+		Name:         routerName,
+		AdminStateUp: &adminStateUp,
+		GatewayInfo:  &gatewayInfo,
+	}
+
+	router, err := routers.Create(client, createOpts).Extract()
+	if err != nil {
+		return router, err
+	}
+
+	if err := WaitForRouterToCreate(client, router.ID, 60); err != nil {
+		return router, err
+	}
+
+	t.Logf("Created router: %s", routerName)
+
+	return router, nil
+}
+
+// CreateRouterInterface will attach a subnet to a router. An error will be
+// returned if the operation fails.
+func CreateRouterInterface(t *testing.T, client *gophercloud.ServiceClient, portID, routerID string) (*routers.InterfaceInfo, error) {
+	t.Logf("Attempting to add port %s to router %s", portID, routerID)
+
+	aiOpts := routers.AddInterfaceOpts{
+		PortID: portID,
+	}
+
+	iface, err := routers.AddInterface(client, routerID, aiOpts).Extract()
+	if err != nil {
+		return iface, err
+	}
+
+	if err := WaitForRouterInterfaceToAttach(client, portID, 60); err != nil {
+		return iface, err
+	}
+
+	t.Logf("Successfully added port %s to router %s", portID, routerID)
+	return iface, nil
+}
+
+// DeleteRouter deletes a router of a specified ID. A fatal error will occur
+// if the deletion failed. This works best when used as a deferred function.
+func DeleteRouter(t *testing.T, client *gophercloud.ServiceClient, routerID string) {
+	t.Logf("Attempting to delete router: %s", routerID)
+
+	err := routers.Delete(client, routerID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Error deleting router: %v", err)
+	}
+
+	if err := WaitForRouterToDelete(client, routerID, 60); err != nil {
+		t.Fatalf("Error waiting for router to delete: %v", err)
+	}
+
+	t.Logf("Deleted router: %s", routerID)
+}
+
+// DeleteRouterInterface will detach a subnet to a router. A fatal error will
+// occur if the deletion failed. This works best when used as a deferred
+// function.
+func DeleteRouterInterface(t *testing.T, client *gophercloud.ServiceClient, portID, routerID string) {
+	t.Logf("Attempting to detach port %s from router %s", portID, routerID)
+
+	riOpts := routers.RemoveInterfaceOpts{
+		PortID: portID,
+	}
+
+	_, err := routers.RemoveInterface(client, routerID, riOpts).Extract()
+	if err != nil {
+		t.Fatalf("Failed to detach port %s from router %s", portID, routerID)
+	}
+
+	if err := WaitForRouterInterfaceToDetach(client, portID, 60); err != nil {
+		t.Fatalf("Failed to wait for port %s to detach from router %s", portID, routerID)
+	}
+
+	t.Logf("Successfully detached port %s from router %s", portID, routerID)
+}
+
+// DeleteFloatingIP deletes a floatingIP of a specified ID. A fatal error will
+// occur if the deletion failed. This works best when used as a deferred
+// function.
+func DeleteFloatingIP(t *testing.T, client *gophercloud.ServiceClient, floatingIPID string) {
+	t.Logf("Attempting to delete floating IP: %s", floatingIPID)
+
+	err := floatingips.Delete(client, floatingIPID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Failed to delete floating IP: %v", err)
+	}
+
+	t.Logf("Deleted floating IP: %s", floatingIPID)
+}
+
+func WaitForRouterToCreate(client *gophercloud.ServiceClient, routerID string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		r, err := routers.Get(client, routerID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if r.Status == "ACTIVE" {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
+
+func WaitForRouterToDelete(client *gophercloud.ServiceClient, routerID string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		_, err := routers.Get(client, routerID).Extract()
+		if err != nil {
+			if _, ok := err.(gophercloud.ErrDefault404); ok {
+				return true, nil
+			}
+
+			return false, err
+		}
+
+		return false, nil
+	})
+}
+
+func WaitForRouterInterfaceToAttach(client *gophercloud.ServiceClient, routerInterfaceID string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		r, err := ports.Get(client, routerInterfaceID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if r.Status == "ACTIVE" {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
+
+func WaitForRouterInterfaceToDetach(client *gophercloud.ServiceClient, routerInterfaceID string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		r, err := ports.Get(client, routerInterfaceID).Extract()
+		if err != nil {
+			if _, ok := err.(gophercloud.ErrDefault404); ok {
+				return true, nil
+			}
+
+			if errCode, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok {
+				if errCode.Actual == 409 {
+					return false, nil
+				}
+			}
+
+			return false, err
+		}
+
+		if r.Status == "ACTIVE" {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
\ No newline at end of file
diff --git a/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go b/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go
new file mode 100644
index 0000000..3450ee0
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go
@@ -0,0 +1,118 @@
+// +build acceptance networking layer3 router
+
+package layer3
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+)
+
+func TestLayer3RouterList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	listOpts := routers.ListOpts{}
+	allPages, err := routers.List(client, listOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list routers: %v", err)
+	}
+
+	allRouters, err := routers.ExtractRouters(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract routers: %v", err)
+	}
+
+	for _, router := range allRouters {
+		tools.PrintResource(t, router)
+	}
+}
+
+func TestLayer3RouterCreateDelete(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	router, err := CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer DeleteRouter(t, client, router.ID)
+
+	tools.PrintResource(t, router)
+
+	newName := tools.RandomString("TESTACC-", 8)
+	updateOpts := routers.UpdateOpts{
+		Name: newName,
+	}
+
+	_, err = routers.Update(client, router.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update router: %v", err)
+	}
+
+	newRouter, err := routers.Get(client, router.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get router: %v", err)
+	}
+
+	tools.PrintResource(t, newRouter)
+}
+
+func TestLayer3RouterInterface(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to get choices: %v", err)
+	}
+
+	netid, err := networks.IDFromName(client,chocices.NetworkName)
+	if err != nil {
+		t.Fatalf("Unable to find network id: %v", err)
+	}
+
+	subnet, err := networking.CreateSubnet(t, client, netid)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	tools.PrintResource(t, subnet)
+
+	router, err := CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer DeleteRouter(t, client, router.ID)
+
+	aiOpts := routers.AddInterfaceOpts{
+		SubnetID: subnet.ID,
+	}
+
+	iface, err := routers.AddInterface(client, router.ID, aiOpts).Extract()
+	if err != nil {
+		t.Fatalf("Failed to add interface to router: %v", err)
+	}
+
+	tools.PrintResource(t, router)
+	tools.PrintResource(t, iface)
+
+	riOpts := routers.RemoveInterfaceOpts{
+		SubnetID: subnet.ID,
+	}
+
+	_, err = routers.RemoveInterface(client, router.ID, riOpts).Extract()
+	if err != nil {
+		t.Fatalf("Failed to remove interface from router: %v", err)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/lbaas.go b/acceptance/openstack/networking/v2/extensions/lbaas/lbaas.go
new file mode 100644
index 0000000..1f7b2b0
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/lbaas.go
@@ -0,0 +1,160 @@
+package lbaas
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
+)
+
+// CreateMember will create a load balancer member in a specified pool on a
+// random port. An error will be returned if the member could not be created.
+func CreateMember(t *testing.T, client *gophercloud.ServiceClient, poolID string) (*members.Member, error) {
+	protocolPort := tools.RandomInt(100, 1000)
+	address := tools.RandomInt(2, 200)
+	t.Logf("Attempting to create member in port %d", protocolPort)
+
+	createOpts := members.CreateOpts{
+		PoolID:       poolID,
+		ProtocolPort: protocolPort,
+		Address:      fmt.Sprintf("192.168.1.%d", address),
+	}
+
+	member, err := members.Create(client, createOpts).Extract()
+	if err != nil {
+		return member, err
+	}
+
+	t.Logf("Successfully created member %s")
+
+	return member, nil
+}
+
+// CreateMonitor will create a monitor with a random name for a specific pool.
+// An error will be returned if the monitor could not be created.
+func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient) (*monitors.Monitor, error) {
+	t.Logf("Attempting to create monitor.")
+
+	createOpts := monitors.CreateOpts{
+		Type:         monitors.TypePING,
+		Delay:        90,
+		Timeout:      60,
+		MaxRetries:   10,
+		AdminStateUp: gophercloud.Enabled,
+	}
+
+	monitor, err := monitors.Create(client, createOpts).Extract()
+	if err != nil {
+		return monitor, err
+	}
+
+	t.Logf("Successfully created monitor")
+
+	return monitor, nil
+}
+
+// CreatePool will create a pool with a random name. An error will be returned
+// if the pool could not be deleted.
+func CreatePool(t *testing.T, client *gophercloud.ServiceClient, subnetID string) (*pools.Pool, error) {
+	poolName := tools.RandomString("TESTACCT-", 8)
+
+	t.Logf("Attempting to create pool %s", poolName)
+
+	createOpts := pools.CreateOpts{
+		Name:     poolName,
+		SubnetID: subnetID,
+		Protocol: pools.ProtocolTCP,
+		LBMethod: pools.LBMethodRoundRobin,
+	}
+
+	pool, err := pools.Create(client, createOpts).Extract()
+	if err != nil {
+		return pool, err
+	}
+
+	t.Logf("Successfully created pool %s", poolName)
+
+	return pool, nil
+}
+
+// CreateVIP will create a vip with a random name and a random port in a
+// specified subnet and pool. An error will be returned if the vip could
+// not be created.
+func CreateVIP(t *testing.T, client *gophercloud.ServiceClient, subnetID, poolID string) (*vips.VirtualIP, error) {
+	vipName := tools.RandomString("TESTACCT-", 8)
+	vipPort := tools.RandomInt(100, 10000)
+
+	t.Logf("Attempting to create VIP %s", vipName)
+
+	createOpts := vips.CreateOpts{
+		Name:         vipName,
+		SubnetID:     subnetID,
+		PoolID:       poolID,
+		Protocol:     "TCP",
+		ProtocolPort: vipPort,
+	}
+
+	vip, err := vips.Create(client, createOpts).Extract()
+	if err != nil {
+		return vip, err
+	}
+
+	t.Logf("Successfully created vip %s", vipName)
+
+	return vip, nil
+}
+
+// DeleteMember will delete a specified member. A fatal error will occur if
+// the member could not be deleted. This works best when used as a deferred
+// function.
+func DeleteMember(t *testing.T, client *gophercloud.ServiceClient, memberID string) {
+	t.Logf("Attempting to delete member %s", memberID)
+
+	if err := members.Delete(client, memberID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete member: %v", err)
+	}
+
+	t.Logf("Successfully deleted member %s", memberID)
+}
+
+// DeleteMonitor will delete a specified monitor. A fatal error will occur if
+// the monitor could not be deleted. This works best when used as a deferred
+// function.
+func DeleteMonitor(t *testing.T, client *gophercloud.ServiceClient, monitorID string) {
+	t.Logf("Attempting to delete monitor %s", monitorID)
+
+	if err := monitors.Delete(client, monitorID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete monitor: %v", err)
+	}
+
+	t.Logf("Successfully deleted monitor %s", monitorID)
+}
+
+// DeletePool will delete a specified pool. A fatal error will occur if the
+// pool could not be deleted. This works best when used as a deferred function.
+func DeletePool(t *testing.T, client *gophercloud.ServiceClient, poolID string) {
+	t.Logf("Attempting to delete pool %s", poolID)
+
+	if err := pools.Delete(client, poolID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete pool: %v", err)
+	}
+
+	t.Logf("Successfully deleted pool %s", poolID)
+}
+
+// DeleteVIP will delete a specified vip. A fatal error will occur if the vip
+// could not be deleted. This works best when used as a deferred function.
+func DeleteVIP(t *testing.T, client *gophercloud.ServiceClient, vipID string) {
+	t.Logf("Attempting to delete vip %s", vipID)
+
+	if err := vips.Delete(client, vipID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete vip: %v", err)
+	}
+
+	t.Logf("Successfully deleted vip %s", vipID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/members_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/members_test.go
new file mode 100644
index 0000000..75dec83
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/members_test.go
@@ -0,0 +1,83 @@
+// +build acceptance networking lbaas member
+
+package lbaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members"
+)
+
+func TestMembersList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := members.List(client, members.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list members: %v", err)
+	}
+
+	allMembers, err := members.ExtractMembers(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract members: %v", err)
+	}
+
+	for _, member := range allMembers {
+		tools.PrintResource(t, member)
+	}
+}
+
+func TestMembersCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	pool, err := CreatePool(t, client, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create pool: %v", err)
+	}
+	defer DeletePool(t, client, pool.ID)
+
+	member, err := CreateMember(t, client, pool.ID)
+	if err != nil {
+		t.Fatalf("Unable to create member: %v", err)
+	}
+	defer DeleteMember(t, client, member.ID)
+
+	tools.PrintResource(t, member)
+
+	updateOpts := members.UpdateOpts{
+		AdminStateUp: gophercloud.Enabled,
+	}
+
+	_, err = members.Update(client, member.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update member: %v")
+	}
+
+	newMember, err := members.Get(client, member.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get member: %v")
+	}
+
+	tools.PrintResource(t, newMember)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/monitors_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/monitors_test.go
new file mode 100644
index 0000000..56b413a
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/monitors_test.go
@@ -0,0 +1,63 @@
+// +build acceptance networking lbaas monitors
+
+package lbaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
+)
+
+func TestMonitorsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := monitors.List(client, monitors.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list monitors: %v", err)
+	}
+
+	allMonitors, err := monitors.ExtractMonitors(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract monitors: %v", err)
+	}
+
+	for _, monitor := range allMonitors {
+		tools.PrintResource(t, monitor)
+	}
+}
+
+func TestMonitorsCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	monitor, err := CreateMonitor(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create monitor: %v", err)
+	}
+	defer DeleteMonitor(t, client, monitor.ID)
+
+	tools.PrintResource(t, monitor)
+
+	updateOpts := monitors.UpdateOpts{
+		Delay: 999,
+	}
+
+	_, err = monitors.Update(client, monitor.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update monitor: %v")
+	}
+
+	newMonitor, err := monitors.Get(client, monitor.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get monitor: %v")
+	}
+
+	tools.PrintResource(t, newMonitor)
+}
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/pools_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/pools_test.go
new file mode 100644
index 0000000..b53237c
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/pools_test.go
@@ -0,0 +1,118 @@
+// +build acceptance networking lbaas pool
+
+package lbaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+)
+
+func TestPoolsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := pools.List(client, pools.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list pools: %v", err)
+	}
+
+	allPools, err := pools.ExtractPools(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract pools: %v", err)
+	}
+
+	for _, pool := range allPools {
+		tools.PrintResource(t, pool)
+	}
+}
+
+func TestPoolsCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	pool, err := CreatePool(t, client, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create pool: %v", err)
+	}
+	defer DeletePool(t, client, pool.ID)
+
+	tools.PrintResource(t, pool)
+
+	updateOpts := pools.UpdateOpts{
+		LBMethod: pools.LBMethodLeastConnections,
+	}
+
+	_, err = pools.Update(client, pool.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update pool: %v")
+	}
+
+	newPool, err := pools.Get(client, pool.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get pool: %v")
+	}
+
+	tools.PrintResource(t, newPool)
+}
+
+func TestPoolsMonitors(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	pool, err := CreatePool(t, client, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create pool: %v", err)
+	}
+	defer DeletePool(t, client, pool.ID)
+
+	monitor, err := CreateMonitor(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create monitor: %v", err)
+	}
+	defer DeleteMonitor(t, client, monitor.ID)
+
+	t.Logf("Associating monitor %s with pool %s", monitor.ID, pool.ID)
+	if res := pools.AssociateMonitor(client, pool.ID, monitor.ID); res.Err != nil {
+		t.Fatalf("Unable to associate monitor to pool")
+	}
+
+	t.Logf("Disassociating monitor %s with pool %s", monitor.ID, pool.ID)
+	if res := pools.DisassociateMonitor(client, pool.ID, monitor.ID); res.Err != nil {
+		t.Fatalf("Unable to disassociate monitor from pool")
+	}
+
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/vips_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/vips_test.go
new file mode 100644
index 0000000..ba3f9b4
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/vips_test.go
@@ -0,0 +1,82 @@
+// +build acceptance networking lbaas vip
+
+package lbaas
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
+)
+
+func TestVIPsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := vips.List(client, vips.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list vips: %v", err)
+	}
+
+	allVIPs, err := vips.ExtractVIPs(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract vips: %v", err)
+	}
+
+	for _, vip := range allVIPs {
+		tools.PrintResource(t, vip)
+	}
+}
+
+func TestVIPsCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	pool, err := CreatePool(t, client, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create pool: %v", err)
+	}
+	defer DeletePool(t, client, pool.ID)
+
+	vip, err := CreateVIP(t, client, subnet.ID, pool.ID)
+	if err != nil {
+		t.Fatalf("Unable to create vip: %v", err)
+	}
+	defer DeleteVIP(t, client, vip.ID)
+
+	tools.PrintResource(t, vip)
+
+	connLimit := 100
+	updateOpts := vips.UpdateOpts{
+		ConnLimit: &connLimit,
+	}
+
+	_, err = vips.Update(client, vip.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update vip: %v")
+	}
+
+	newVIP, err := vips.Get(client, vip.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get vip: %v")
+	}
+
+	tools.PrintResource(t, newVIP)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/lbaas_v2.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/lbaas_v2.go
new file mode 100644
index 0000000..093f835
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/lbaas_v2.go
@@ -0,0 +1,282 @@
+package lbaas_v2
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+)
+
+const loadbalancerActiveTimeoutSeconds = 300
+const loadbalancerDeleteTimeoutSeconds = 300
+
+// CreateListener will create a listener for a given load balancer on a random
+// port with a random name. An error will be returned if the listener could not
+// be created.
+func CreateListener(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer) (*listeners.Listener, error) {
+	listenerName := tools.RandomString("TESTACCT-", 8)
+	listenerPort := tools.RandomInt(1, 100)
+
+	t.Logf("Attempting to create listener %s on port %d", listenerName, listenerPort)
+
+	createOpts := listeners.CreateOpts{
+		Name:           listenerName,
+		LoadbalancerID: lb.ID,
+		Protocol:       "TCP",
+		ProtocolPort:   listenerPort,
+	}
+
+	listener, err := listeners.Create(client, createOpts).Extract()
+	if err != nil {
+		return listener, err
+	}
+
+	t.Logf("Successfully created listener %s", listenerName)
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		return listener, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+	}
+
+	return listener, nil
+}
+
+// CreateLoadBalancer will create a load balancer with a random name on a given
+// subnet. An error will be returned if the loadbalancer could not be created.
+func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetID string) (*loadbalancers.LoadBalancer, error) {
+	lbName := tools.RandomString("TESTACCT-", 8)
+
+	t.Logf("Attempting to create loadbalancer %s on subnet %s", lbName, subnetID)
+
+	createOpts := loadbalancers.CreateOpts{
+		Name:         lbName,
+		VipSubnetID:  subnetID,
+		AdminStateUp: gophercloud.Enabled,
+	}
+
+	lb, err := loadbalancers.Create(client, createOpts).Extract()
+	if err != nil {
+		return lb, err
+	}
+
+	t.Logf("Successfully created loadbalancer %s on subnet %s", lbName, subnetID)
+	t.Logf("Waiting for loadbalancer %s to become active", lbName)
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		return lb, err
+	}
+
+	t.Logf("LoadBalancer %s is active", lbName)
+
+	return lb, nil
+}
+
+// CreateMember will create a member with a random name, port, address, and
+// weight. An error will be returned if the member could not be created.
+func CreateMember(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer, pool *pools.Pool, subnetID, subnetCIDR string) (*pools.Member, error) {
+	memberName := tools.RandomString("TESTACCT-", 8)
+	memberPort := tools.RandomInt(100, 1000)
+	memberWeight := tools.RandomInt(1, 10)
+
+	cidrParts := strings.Split(subnetCIDR, "/")
+	subnetParts := strings.Split(cidrParts[0], ".")
+	memberAddress := fmt.Sprintf("%s.%s.%s.%d", subnetParts[0], subnetParts[1], subnetParts[2], tools.RandomInt(10, 100))
+
+	t.Logf("Attempting to create member %s", memberName)
+
+	createOpts := pools.CreateMemberOpts{
+		Name:         memberName,
+		ProtocolPort: memberPort,
+		Weight:       memberWeight,
+		Address:      memberAddress,
+		SubnetID:     subnetID,
+	}
+
+	t.Logf("Member create opts: %#v", createOpts)
+
+	member, err := pools.CreateMember(client, pool.ID, createOpts).Extract()
+	if err != nil {
+		return member, err
+	}
+
+	t.Logf("Successfully created member %s", memberName)
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		return member, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+	}
+
+	return member, nil
+}
+
+// CreateMonitor will create a monitor with a random name for a specific pool.
+// An error will be returned if the monitor could not be created.
+func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer, pool *pools.Pool) (*monitors.Monitor, error) {
+	monitorName := tools.RandomString("TESTACCT-", 8)
+
+	t.Logf("Attempting to create monitor %s", monitorName)
+
+	createOpts := monitors.CreateOpts{
+		PoolID:     pool.ID,
+		Name:       monitorName,
+		Delay:      10,
+		Timeout:    5,
+		MaxRetries: 5,
+		Type:       "PING",
+	}
+
+	monitor, err := monitors.Create(client, createOpts).Extract()
+	if err != nil {
+		return monitor, err
+	}
+
+	t.Logf("Successfully created monitor: %s", monitorName)
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		return monitor, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+	}
+
+	return monitor, nil
+}
+
+// CreatePool will create a pool with a random name with a specified listener
+// and loadbalancer. An error will be returned if the pool could not be
+// created.
+func CreatePool(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer) (*pools.Pool, error) {
+	poolName := tools.RandomString("TESTACCT-", 8)
+
+	t.Logf("Attempting to create pool %s", poolName)
+
+	createOpts := pools.CreateOpts{
+		Name:           poolName,
+		Protocol:       pools.ProtocolTCP,
+		LoadbalancerID: lb.ID,
+		LBMethod:       pools.LBMethodLeastConnections,
+	}
+
+	pool, err := pools.Create(client, createOpts).Extract()
+	if err != nil {
+		return pool, err
+	}
+
+	t.Logf("Successfully created pool %s", poolName)
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		return pool, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+	}
+
+	return pool, nil
+}
+
+// DeleteListener will delete a specified listener. A fatal error will occur if
+// the listener could not be deleted. This works best when used as a deferred
+// function.
+func DeleteListener(t *testing.T, client *gophercloud.ServiceClient, lbID, listenerID string) {
+	t.Logf("Attempting to delete listener %s", listenerID)
+
+	if err := listeners.Delete(client, listenerID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete listener: %v", err)
+	}
+
+	if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	t.Logf("Successfully deleted listener %s", listenerID)
+}
+
+// DeleteMember will delete a specified member. A fatal error will occur if the
+// member could not be deleted. This works best when used as a deferred
+// function.
+func DeleteMember(t *testing.T, client *gophercloud.ServiceClient, lbID, poolID, memberID string) {
+	t.Logf("Attempting to delete member %s", memberID)
+
+	if err := pools.DeleteMember(client, poolID, memberID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete member: %s", memberID)
+	}
+
+	if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	t.Logf("Successfully deleted member %s", memberID)
+}
+
+// DeleteLoadBalancer will delete a specified loadbalancer. A fatal error will
+// occur if the loadbalancer could not be deleted. This works best when used
+// as a deferred function.
+func DeleteLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, lbID string) {
+	t.Logf("Attempting to delete loadbalancer %s", lbID)
+
+	if err := loadbalancers.Delete(client, lbID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete loadbalancer: %v", err)
+	}
+
+	t.Logf("Waiting for loadbalancer %s to delete", lbID)
+
+	if err := WaitForLoadBalancerState(client, lbID, "DELETED", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Loadbalancer did not delete in time.")
+	}
+
+	t.Logf("Successfully deleted loadbalancer %s", lbID)
+}
+
+// DeleteMonitor will delete a specified monitor. A fatal error will occur if
+// the monitor could not be deleted. This works best when used as a deferred
+// function.
+func DeleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID, monitorID string) {
+	t.Logf("Attempting to delete monitor %s", monitorID)
+
+	if err := monitors.Delete(client, monitorID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete monitor: %v", err)
+	}
+
+	if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	t.Logf("Successfully deleted monitor %s", monitorID)
+}
+
+// DeletePool will delete a specified pool. A fatal error will occur if the
+// pool could not be deleted. This works best when used as a deferred function.
+func DeletePool(t *testing.T, client *gophercloud.ServiceClient, lbID, poolID string) {
+	t.Logf("Attempting to delete pool %s", poolID)
+
+	if err := pools.Delete(client, poolID).ExtractErr(); err != nil {
+		t.Fatalf("Unable to delete pool: %v", err)
+	}
+
+	if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	t.Logf("Successfully deleted pool %s", poolID)
+}
+
+// WaitForLoadBalancerState will wait until a loadbalancer reaches a given state.
+func WaitForLoadBalancerState(client *gophercloud.ServiceClient, lbID, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := loadbalancers.Get(client, lbID).Extract()
+		if err != nil {
+			if httpStatus, ok := err.(gophercloud.ErrDefault404); ok {
+				if httpStatus.Actual == 404 {
+					if status == "DELETED" {
+						return true, nil
+					}
+				}
+			}
+			return false, err
+		}
+
+		if current.ProvisioningStatus == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/listeners_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/listeners_test.go
new file mode 100644
index 0000000..2d2dd03
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/listeners_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking lbaas_v2 listeners
+
+package lbaas_v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+)
+
+func TestListenersList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := listeners.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list listeners: %v", err)
+	}
+
+	allListeners, err := listeners.ExtractListeners(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract listeners: %v", err)
+	}
+
+	for _, listener := range allListeners {
+		tools.PrintResource(t, listener)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go
new file mode 100644
index 0000000..650eb2c
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go
@@ -0,0 +1,178 @@
+// +build acceptance networking lbaas_v2 loadbalancers
+
+package lbaas_v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+)
+
+func TestLoadbalancersList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := loadbalancers.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list loadbalancers: %v", err)
+	}
+
+	allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract loadbalancers: %v", err)
+	}
+
+	for _, lb := range allLoadbalancers {
+		tools.PrintResource(t, lb)
+	}
+}
+
+func TestLoadbalancersCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	lb, err := CreateLoadBalancer(t, client, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create loadbalancer: %v", err)
+	}
+	defer DeleteLoadBalancer(t, client, lb.ID)
+
+	newLB, err := loadbalancers.Get(client, lb.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get loadbalancer: %v", err)
+	}
+
+	tools.PrintResource(t, newLB)
+
+	// Because of the time it takes to create a loadbalancer,
+	// this test will include some other resources.
+
+	// Listener
+	listener, err := CreateListener(t, client, lb)
+	if err != nil {
+		t.Fatalf("Unable to create listener: %v", err)
+	}
+	defer DeleteListener(t, client, lb.ID, listener.ID)
+
+	updateListenerOpts := listeners.UpdateOpts{
+		Description: "Some listener description",
+	}
+	_, err = listeners.Update(client, listener.ID, updateListenerOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update listener")
+	}
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	newListener, err := listeners.Get(client, listener.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get listener")
+	}
+
+	tools.PrintResource(t, newListener)
+
+	// Pool
+	pool, err := CreatePool(t, client, lb)
+	if err != nil {
+		t.Fatalf("Unable to create pool: %v", err)
+	}
+	defer DeletePool(t, client, lb.ID, pool.ID)
+
+	updatePoolOpts := pools.UpdateOpts{
+		Description: "Some pool description",
+	}
+	_, err = pools.Update(client, pool.ID, updatePoolOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update pool")
+	}
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	newPool, err := pools.Get(client, pool.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get pool")
+	}
+
+	tools.PrintResource(t, newPool)
+
+	// Member
+	member, err := CreateMember(t, client, lb, newPool, subnet.ID, subnet.CIDR)
+	if err != nil {
+		t.Fatalf("Unable to create member: %v", err)
+	}
+	defer DeleteMember(t, client, lb.ID, pool.ID, member.ID)
+
+	newWeight := tools.RandomInt(11, 100)
+	updateMemberOpts := pools.UpdateMemberOpts{
+		Weight: newWeight,
+	}
+	_, err = pools.UpdateMember(client, pool.ID, member.ID, updateMemberOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update pool")
+	}
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	newMember, err := pools.GetMember(client, pool.ID, member.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get member")
+	}
+
+	tools.PrintResource(t, newMember)
+
+	// Monitor
+	monitor, err := CreateMonitor(t, client, lb, newPool)
+	if err != nil {
+		t.Fatalf("Unable to create monitor: %v", err)
+	}
+	defer DeleteMonitor(t, client, lb.ID, monitor.ID)
+
+	newDelay := tools.RandomInt(20, 30)
+	updateMonitorOpts := monitors.UpdateOpts{
+		Delay: newDelay,
+	}
+	_, err = monitors.Update(client, monitor.ID, updateMonitorOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update monitor")
+	}
+
+	if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+		t.Fatalf("Timed out waiting for loadbalancer to become active")
+	}
+
+	newMonitor, err := monitors.Get(client, monitor.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get monitor")
+	}
+
+	tools.PrintResource(t, newMonitor)
+
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/monitors_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/monitors_test.go
new file mode 100644
index 0000000..b312370
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/monitors_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking lbaas_v2 monitors
+
+package lbaas_v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+)
+
+func TestMonitorsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := monitors.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list monitors: %v", err)
+	}
+
+	allMonitors, err := monitors.ExtractMonitors(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract monitors: %v", err)
+	}
+
+	for _, monitor := range allMonitors {
+		tools.PrintResource(t, monitor)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go
new file mode 100644
index 0000000..24b7482
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go
@@ -0,0 +1 @@
+package lbaas_v2
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/pools_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/pools_test.go
new file mode 100644
index 0000000..b4f55a0
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/pools_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking lbaas_v2 pools
+
+package lbaas_v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+)
+
+func TestPoolsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := pools.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list pools: %v", err)
+	}
+
+	allPools, err := pools.ExtractPools(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract pools: %v", err)
+	}
+
+	for _, pool := range allPools {
+		tools.PrintResource(t, pool)
+	}
+}
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/portsbinding/pkg.go b/acceptance/openstack/networking/v2/extensions/portsbinding/pkg.go
new file mode 100644
index 0000000..5dae1b1
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/portsbinding/pkg.go
@@ -0,0 +1 @@
+package portsbinding
diff --git a/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding.go b/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding.go
new file mode 100644
index 0000000..a6d75f3
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding.go
@@ -0,0 +1,38 @@
+package portsbinding
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+// CreatePortsbinding will create a port on the specified subnet. An error will be
+// returned if the port could not be created.
+func CreatePortsbinding(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID, hostID string) (*portsbinding.Port, error) {
+	portName := tools.RandomString("TESTACC-", 8)
+	iFalse := false
+
+	t.Logf("Attempting to create port: %s", portName)
+
+	createOpts := portsbinding.CreateOpts{
+		CreateOptsBuilder: ports.CreateOpts{
+			NetworkID:    networkID,
+			Name:         portName,
+			AdminStateUp: &iFalse,
+			FixedIPs:     []ports.IP{ports.IP{SubnetID: subnetID}},
+		},
+		HostID: hostID,
+	}
+
+	port, err := portsbinding.Create(client, createOpts).Extract()
+	if err != nil {
+		return port, err
+	}
+
+	t.Logf("Successfully created port: %s", portName)
+
+	return port, nil
+}
diff --git a/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go b/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go
new file mode 100644
index 0000000..803f62a
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go
@@ -0,0 +1,58 @@
+// +build acceptance networking
+
+package portsbinding
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+func TestPortsbindingCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	// Define a host
+	hostID := "localhost"
+
+	// Create port
+	port, err := CreatePortsbinding(t, client, network.ID, subnet.ID, hostID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer networking.DeletePort(t, client, port.ID)
+
+	tools.PrintResource(t, port)
+
+	// Update port
+	newPortName := tools.RandomString("TESTACC-", 8)
+	updateOpts := ports.UpdateOpts{
+		Name: newPortName,
+	}
+	newPort, err := portsbinding.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	tools.PrintResource(t, newPort)
+}
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..b0d5846
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/provider_test.go
@@ -0,0 +1,35 @@
+// +build acceptance networking provider
+
+package extensions
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+)
+
+func TestNetworksProviderCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create a network
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	getResult := networks.Get(client, network.ID)
+	newNetwork, err := provider.ExtractGet(getResult)
+	if err != nil {
+		t.Fatalf("Unable to extract network: %v", err)
+	}
+
+	tools.PrintResource(t, newNetwork)
+}
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..c696377
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -0,0 +1,94 @@
+// +build acceptance networking security
+
+package extensions
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups"
+)
+
+func TestSecurityGroupsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	listOpts := groups.ListOpts{}
+	allPages, err := groups.List(client, listOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list groups: %v", err)
+	}
+
+	allGroups, err := groups.ExtractGroups(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract groups: %v", err)
+	}
+
+	for _, group := range allGroups {
+		tools.PrintResource(t, group)
+	}
+}
+
+func TestSecurityGroupsCreateDelete(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	group, err := CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer DeleteSecurityGroup(t, client, group.ID)
+
+	rule, err := CreateSecurityGroupRule(t, client, group.ID)
+	if err != nil {
+		t.Fatalf("Unable to create security group rule: %v", err)
+	}
+	defer DeleteSecurityGroupRule(t, client, rule.ID)
+
+	tools.PrintResource(t, group)
+}
+
+func TestSecurityGroupsPort(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	network, err := networking.CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer networking.DeleteNetwork(t, client, network.ID)
+
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer networking.DeleteSubnet(t, client, subnet.ID)
+
+	group, err := CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer DeleteSecurityGroup(t, client, group.ID)
+
+	rule, err := CreateSecurityGroupRule(t, client, group.ID)
+	if err != nil {
+		t.Fatalf("Unable to create security group rule: %v", err)
+	}
+	defer DeleteSecurityGroupRule(t, client, rule.ID)
+
+	port, err := CreatePortWithSecurityGroup(t, client, network.ID, subnet.ID, group.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer networking.DeletePort(t, client, port.ID)
+
+	tools.PrintResource(t, port)
+}
diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go
new file mode 100644
index 0000000..c5e7ca2
--- /dev/null
+++ b/acceptance/openstack/networking/v2/networking.go
@@ -0,0 +1,211 @@
+package v2
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
+)
+
+// CreateNetwork will create basic network. An error will be returned if the
+// network could not be created.
+func CreateNetwork(t *testing.T, client *gophercloud.ServiceClient) (*networks.Network, error) {
+	networkName := tools.RandomString("TESTACC-", 8)
+	createOpts := networks.CreateOpts{
+		Name:         networkName,
+		AdminStateUp: gophercloud.Enabled,
+	}
+
+	t.Logf("Attempting to create network: %s", networkName)
+
+	network, err := networks.Create(client, createOpts).Extract()
+	if err != nil {
+		return network, err
+	}
+
+	t.Logf("Successfully created network.")
+	return network, nil
+}
+
+// CreatePort will create a port on the specified subnet. An error will be
+// returned if the port could not be created.
+func CreatePort(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*ports.Port, error) {
+	portName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create port: %s", portName)
+
+	createOpts := ports.CreateOpts{
+		NetworkID:    networkID,
+		Name:         portName,
+		AdminStateUp: gophercloud.Enabled,
+		FixedIPs:     []ports.IP{ports.IP{SubnetID: subnetID}},
+	}
+
+	port, err := ports.Create(client, createOpts).Extract()
+	if err != nil {
+		return port, err
+	}
+
+	if err := WaitForPortToCreate(client, port.ID, 60); err != nil {
+		return port, err
+	}
+
+	newPort, err := ports.Get(client, port.ID).Extract()
+	if err != nil {
+		return newPort, err
+	}
+
+	t.Logf("Successfully created port: %s", portName)
+
+	return newPort, nil
+}
+
+// CreateSubnet will create a subnet on the specified Network ID. An error
+// will be returned if the subnet could not be created.
+func CreateSubnet(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) {
+	subnetName := tools.RandomString("TESTACC-", 8)
+	subnetOctet := tools.RandomInt(1, 250)
+	subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet)
+	subnetGateway := fmt.Sprintf("192.168.%d.1", subnetOctet)
+	createOpts := subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       subnetCIDR,
+		IPVersion:  4,
+		Name:       subnetName,
+		EnableDHCP: gophercloud.Disabled,
+		GatewayIP:  &subnetGateway,
+	}
+
+	t.Logf("Attempting to create subnet: %s", subnetName)
+
+	subnet, err := subnets.Create(client, createOpts).Extract()
+	if err != nil {
+		return subnet, err
+	}
+
+	t.Logf("Successfully created subnet.")
+	return subnet, nil
+}
+
+// CreateSubnetWithDefaultGateway will create a subnet on the specified Network
+// ID and have Neutron set the gateway by default An error will be returned if
+// the subnet could not be created.
+func CreateSubnetWithDefaultGateway(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) {
+	subnetName := tools.RandomString("TESTACC-", 8)
+	subnetOctet := tools.RandomInt(1, 250)
+	subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet)
+	createOpts := subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       subnetCIDR,
+		IPVersion:  4,
+		Name:       subnetName,
+		EnableDHCP: gophercloud.Disabled,
+	}
+
+	t.Logf("Attempting to create subnet: %s", subnetName)
+
+	subnet, err := subnets.Create(client, createOpts).Extract()
+	if err != nil {
+		return subnet, err
+	}
+
+	t.Logf("Successfully created subnet.")
+	return subnet, nil
+}
+
+// CreateSubnetWithNoGateway will create a subnet with no gateway on the
+// specified Network ID.  An error will be returned if the subnet could not be
+// created.
+func CreateSubnetWithNoGateway(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) {
+	var noGateway = ""
+	subnetName := tools.RandomString("TESTACC-", 8)
+	subnetOctet := tools.RandomInt(1, 250)
+	subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet)
+	dhcpStart := fmt.Sprintf("192.168.%d.10", subnetOctet)
+	dhcpEnd := fmt.Sprintf("192.168.%d.200", subnetOctet)
+	createOpts := subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       subnetCIDR,
+		IPVersion:  4,
+		Name:       subnetName,
+		EnableDHCP: gophercloud.Disabled,
+		GatewayIP:  &noGateway,
+		AllocationPools: []subnets.AllocationPool{
+			{
+				Start: dhcpStart,
+				End:   dhcpEnd,
+			},
+		},
+	}
+
+	t.Logf("Attempting to create subnet: %s", subnetName)
+
+	subnet, err := subnets.Create(client, createOpts).Extract()
+	if err != nil {
+		return subnet, err
+	}
+
+	t.Logf("Successfully created subnet.")
+	return subnet, nil
+}
+
+// DeleteNetwork will delete a network with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteNetwork(t *testing.T, client *gophercloud.ServiceClient, networkID string) {
+	t.Logf("Attempting to delete network: %s", networkID)
+
+	err := networks.Delete(client, networkID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete network %s: %v", networkID, err)
+	}
+
+	t.Logf("Deleted network: %s", networkID)
+}
+
+// DeletePort will delete a port with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeletePort(t *testing.T, client *gophercloud.ServiceClient, portID string) {
+	t.Logf("Attempting to delete port: %s", portID)
+
+	err := ports.Delete(client, portID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete port %s: %v", portID, err)
+	}
+
+	t.Logf("Deleted port: %s", portID)
+}
+
+// DeleteSubnet will delete a subnet with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteSubnet(t *testing.T, client *gophercloud.ServiceClient, subnetID string) {
+	t.Logf("Attempting to delete subnet: %s", subnetID)
+
+	err := subnets.Delete(client, subnetID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete subnet %s: %v", subnetID, err)
+	}
+
+	t.Logf("Deleted subnet: %s", subnetID)
+}
+
+func WaitForPortToCreate(client *gophercloud.ServiceClient, portID string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		p, err := ports.Get(client, portID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if p.Status == "ACTIVE" || p.Status == "DOWN" {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/acceptance/openstack/networking/v2/networks_test.go b/acceptance/openstack/networking/v2/networks_test.go
new file mode 100644
index 0000000..66f42f8
--- /dev/null
+++ b/acceptance/openstack/networking/v2/networks_test.go
@@ -0,0 +1,65 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+)
+
+func TestNetworksList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := networks.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list networks: %v", err)
+	}
+
+	allNetworks, err := networks.ExtractNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract networks: %v", err)
+	}
+
+	for _, network := range allNetworks {
+		tools.PrintResource(t, network)
+	}
+}
+
+func TestNetworksCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create a network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	tools.PrintResource(t, network)
+
+	newName := tools.RandomString("TESTACC-", 8)
+	updateOpts := &networks.UpdateOpts{
+		Name: newName,
+	}
+
+	_, err = networks.Update(client, network.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update network: %v", err)
+	}
+
+	newNetwork, err := networks.Get(client, network.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to retrieve network: %v", err)
+	}
+
+	tools.PrintResource(t, newNetwork)
+}
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/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go
new file mode 100644
index 0000000..394e90f
--- /dev/null
+++ b/acceptance/openstack/networking/v2/ports_test.go
@@ -0,0 +1,192 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	extensions "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+func TestPortsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := ports.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list ports: %v", err)
+	}
+
+	allPorts, err := ports.ExtractPorts(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract ports: %v", err)
+	}
+
+	for _, port := range allPorts {
+		tools.PrintResource(t, port)
+	}
+}
+
+func TestPortsCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	// Create port
+	port, err := CreatePort(t, client, network.ID, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer DeletePort(t, client, port.ID)
+
+	tools.PrintResource(t, port)
+
+	// Update port
+	newPortName := tools.RandomString("TESTACC-", 8)
+	updateOpts := ports.UpdateOpts{
+		Name: newPortName,
+	}
+	newPort, err := ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	tools.PrintResource(t, newPort)
+}
+
+func TestPortsRemoveSecurityGroups(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	// Create port
+	port, err := CreatePort(t, client, network.ID, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer DeletePort(t, client, port.ID)
+
+	tools.PrintResource(t, port)
+
+	// Create a Security Group
+	group, err := extensions.CreateSecurityGroup(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security group: %v", err)
+	}
+	defer extensions.DeleteSecurityGroup(t, client, group.ID)
+
+	// Add the group to the port
+	updateOpts := ports.UpdateOpts{
+		SecurityGroups: []string{group.ID},
+	}
+	newPort, err := ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	// Remove the group
+	updateOpts = ports.UpdateOpts{
+		SecurityGroups: []string{},
+	}
+	newPort, err = ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	tools.PrintResource(t, newPort)
+
+	if len(newPort.SecurityGroups) > 0 {
+		t.Fatalf("Unable to remove security group from port")
+	}
+}
+
+func TestPortsRemoveAddressPair(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	// Create port
+	port, err := CreatePort(t, client, network.ID, subnet.ID)
+	if err != nil {
+		t.Fatalf("Unable to create port: %v", err)
+	}
+	defer DeletePort(t, client, port.ID)
+
+	tools.PrintResource(t, port)
+
+	// Add an address pair to the port
+	updateOpts := ports.UpdateOpts{
+		AllowedAddressPairs: []ports.AddressPair{
+			ports.AddressPair{IPAddress: "192.168.255.10", MACAddress: "aa:bb:cc:dd:ee:ff"},
+		},
+	}
+	newPort, err := ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	// Remove the address pair
+	updateOpts = ports.UpdateOpts{
+		AllowedAddressPairs: []ports.AddressPair{},
+	}
+	newPort, err = ports.Update(client, port.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Could not update port: %v", err)
+	}
+
+	tools.PrintResource(t, newPort)
+
+	if len(newPort.AllowedAddressPairs) > 0 {
+		t.Fatalf("Unable to remove the address pair")
+	}
+}
diff --git a/acceptance/openstack/networking/v2/subnets_test.go b/acceptance/openstack/networking/v2/subnets_test.go
new file mode 100644
index 0000000..fd50a1f
--- /dev/null
+++ b/acceptance/openstack/networking/v2/subnets_test.go
@@ -0,0 +1,158 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
+)
+
+func TestSubnetsList(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	allPages, err := subnets.List(client, nil).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list subnets: %v", err)
+	}
+
+	allSubnets, err := subnets.ExtractSubnets(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract subnets: %v", err)
+	}
+
+	for _, subnet := range allSubnets {
+		tools.PrintResource(t, subnet)
+	}
+}
+
+func TestSubnetCRUD(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := CreateSubnet(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	tools.PrintResource(t, subnet)
+
+	// Update Subnet
+	newSubnetName := tools.RandomString("TESTACC-", 8)
+	updateOpts := subnets.UpdateOpts{
+		Name: newSubnetName,
+	}
+	_, err = subnets.Update(client, subnet.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update subnet: %v", err)
+	}
+
+	// Get subnet
+	newSubnet, err := subnets.Get(client, subnet.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get subnet: %v", err)
+	}
+
+	tools.PrintResource(t, newSubnet)
+}
+
+func TestSubnetsDefaultGateway(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := CreateSubnetWithDefaultGateway(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	tools.PrintResource(t, subnet)
+
+	if subnet.GatewayIP == "" {
+		t.Fatalf("A default gateway was not created.")
+	}
+
+	var noGateway = ""
+	updateOpts := subnets.UpdateOpts{
+		GatewayIP: &noGateway,
+	}
+
+	newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update subnet")
+	}
+
+	if newSubnet.GatewayIP != "" {
+		t.Fatalf("Gateway was not updated correctly")
+	}
+}
+
+func TestSubnetsNoGateway(t *testing.T) {
+	client, err := clients.NewNetworkV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a network client: %v", err)
+	}
+
+	// Create Network
+	network, err := CreateNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create network: %v", err)
+	}
+	defer DeleteNetwork(t, client, network.ID)
+
+	// Create Subnet
+	subnet, err := CreateSubnetWithNoGateway(t, client, network.ID)
+	if err != nil {
+		t.Fatalf("Unable to create subnet: %v", err)
+	}
+	defer DeleteSubnet(t, client, subnet.ID)
+
+	tools.PrintResource(t, subnet)
+
+	if subnet.GatewayIP != "" {
+		t.Fatalf("A gateway exists when it shouldn't.")
+	}
+
+	subnetParts := strings.Split(subnet.CIDR, ".")
+	newGateway := fmt.Sprintf("%s.%s.%s.1", subnetParts[0], subnetParts[1], subnetParts[2])
+	updateOpts := subnets.UpdateOpts{
+		GatewayIP: &newGateway,
+	}
+
+	newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to update subnet")
+	}
+
+	if newSubnet.GatewayIP == "" {
+		t.Fatalf("Gateway was not updated correctly")
+	}
+}
diff --git a/acceptance/openstack/objectstorage/v1/accounts_test.go b/acceptance/openstack/objectstorage/v1/accounts_test.go
new file mode 100644
index 0000000..5a29235
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/accounts_test.go
@@ -0,0 +1,50 @@
+// +build acceptance
+
+package v1
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts"
+	th "github.com/gophercloud/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..1114ed5
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/common.go
@@ -0,0 +1,28 @@
+// +build acceptance
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	th "github.com/gophercloud/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..056b2a9
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/containers_test.go
@@ -0,0 +1,137 @@
+// +build acceptance
+
+package v1
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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..3a27738
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/objects_test.go
@@ -0,0 +1,119 @@
+// +build acceptance
+
+package v1
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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/objectstorage/v1/pkg.go b/acceptance/openstack/objectstorage/v1/pkg.go
new file mode 100644
index 0000000..b7b1f99
--- /dev/null
+++ b/acceptance/openstack/objectstorage/v1/pkg.go
@@ -0,0 +1 @@
+package v1
diff --git a/acceptance/openstack/orchestration/v1/buildinfo_test.go b/acceptance/openstack/orchestration/v1/buildinfo_test.go
new file mode 100644
index 0000000..1b48662
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/buildinfo_test.go
@@ -0,0 +1,20 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo"
+	th "github.com/gophercloud/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..4eec2e3
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/common.go
@@ -0,0 +1,44 @@
+// +build acceptance
+
+package v1
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	th "github.com/gophercloud/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/pkg.go b/acceptance/openstack/orchestration/v1/pkg.go
new file mode 100644
index 0000000..b7b1f99
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/pkg.go
@@ -0,0 +1 @@
+package v1
diff --git a/acceptance/openstack/orchestration/v1/stackevents_test.go b/acceptance/openstack/orchestration/v1/stackevents_test.go
new file mode 100644
index 0000000..4be4bf6
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stackevents_test.go
@@ -0,0 +1,68 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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..50a0f06
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stackresources_test.go
@@ -0,0 +1,62 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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..c87cc5d
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stacks_test.go
@@ -0,0 +1,153 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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..9992e0c
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stacktemplates_test.go
@@ -0,0 +1,75 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates"
+	th "github.com/gophercloud/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..ef11064
--- /dev/null
+++ b/acceptance/openstack/pkg.go
@@ -0,0 +1,3 @@
+// +build acceptance
+
+package openstack
diff --git a/acceptance/openstack/sharedfilesystems/v2/availabilityzones_test.go b/acceptance/openstack/sharedfilesystems/v2/availabilityzones_test.go
new file mode 100644
index 0000000..8841160
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/availabilityzones_test.go
@@ -0,0 +1,29 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones"
+)
+
+func TestAvailabilityZonesList(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	allPages, err := availabilityzones.List(client).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to list availability zones: %v", err)
+	}
+
+	zones, err := availabilityzones.ExtractAvailabilityZones(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract availability zones: %v", err)
+	}
+
+	if len(zones) == 0 {
+		t.Fatal("At least one availability zone was expected to be found")
+	}
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/pkg.go b/acceptance/openstack/sharedfilesystems/v2/pkg.go
new file mode 100644
index 0000000..5a5cd2b
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/pkg.go
@@ -0,0 +1,3 @@
+// The v2 package contains acceptance tests for the Openstack Manila V2 service.
+
+package v2
diff --git a/acceptance/openstack/sharedfilesystems/v2/securityservices.go b/acceptance/openstack/sharedfilesystems/v2/securityservices.go
new file mode 100644
index 0000000..265323d
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/securityservices.go
@@ -0,0 +1,60 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices"
+)
+
+// CreateSecurityService will create a security service with a random name. An
+// error will be returned if the security service was unable to be created.
+func CreateSecurityService(t *testing.T, client *gophercloud.ServiceClient) (*securityservices.SecurityService, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires share network creation in short mode.")
+	}
+
+	securityServiceName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create security service: %s", securityServiceName)
+
+	createOpts := securityservices.CreateOpts{
+		Name: securityServiceName,
+		Type: "kerberos",
+	}
+
+	securityService, err := securityservices.Create(client, createOpts).Extract()
+	if err != nil {
+		return securityService, err
+	}
+
+	return securityService, nil
+}
+
+// DeleteSecurityService will delete a security service. An error will occur if
+// the security service was unable to be deleted.
+func DeleteSecurityService(t *testing.T, client *gophercloud.ServiceClient, securityService *securityservices.SecurityService) {
+	err := securityservices.Delete(client, securityService.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Failed to delete security service %s: %v", securityService.ID, err)
+	}
+
+	t.Logf("Deleted security service: %s", securityService.ID)
+}
+
+// PrintSecurityService will print a security service and all of its attributes.
+func PrintSecurityService(t *testing.T, securityService *securityservices.SecurityService) {
+	t.Logf("ID: %s", securityService.ID)
+	t.Logf("Project ID: %s", securityService.ProjectID)
+	t.Logf("Domain: %s", securityService.Domain)
+	t.Logf("Status: %s", securityService.Status)
+	t.Logf("Type: %s", securityService.Type)
+	t.Logf("Name: %s", securityService.Name)
+	t.Logf("Description: %s", securityService.Description)
+	t.Logf("DNS IP: %s", securityService.DNSIP)
+	t.Logf("User: %s", securityService.User)
+	t.Logf("Password: %s", securityService.Password)
+	t.Logf("Server: %s", securityService.Server)
+	t.Logf("Created at: %v", securityService.CreatedAt)
+	t.Logf("Updated at: %v", securityService.UpdatedAt)
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/securityservices_test.go b/acceptance/openstack/sharedfilesystems/v2/securityservices_test.go
new file mode 100644
index 0000000..5e2b45e
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/securityservices_test.go
@@ -0,0 +1,87 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices"
+)
+
+func TestSecurityServiceCreateDelete(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	securityService, err := CreateSecurityService(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security service: %v", err)
+	}
+
+	PrintSecurityService(t, securityService)
+
+	defer DeleteSecurityService(t, client, securityService)
+}
+
+func TestSecurityServiceList(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	allPages, err := securityservices.List(client, securityservices.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve security services: %v", err)
+	}
+
+	allSecurityServices, err := securityservices.ExtractSecurityServices(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract security services: %v", err)
+	}
+
+	for _, securityService := range allSecurityServices {
+		PrintSecurityService(t, &securityService)
+	}
+}
+
+// The test creates 2 security services and verifies that only the one(s) with
+// a particular name are being listed
+func TestSecurityServiceListFiltering(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	securityService, err := CreateSecurityService(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security service: %v", err)
+	}
+	defer DeleteSecurityService(t, client, securityService)
+
+	securityService, err = CreateSecurityService(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create security service: %v", err)
+	}
+	defer DeleteSecurityService(t, client, securityService)
+
+	options := securityservices.ListOpts{
+		Name: securityService.Name,
+	}
+
+	allPages, err := securityservices.List(client, options).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve security services: %v", err)
+	}
+
+	allSecurityServices, err := securityservices.ExtractSecurityServices(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract security services: %v", err)
+	}
+
+	for _, listedSecurityService := range allSecurityServices {
+		if listedSecurityService.Name != securityService.Name {
+			t.Fatalf("The name of the security service was expected to be %s", securityService.Name)
+		}
+		PrintSecurityService(t, &listedSecurityService)
+	}
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/sharenetworks.go b/acceptance/openstack/sharedfilesystems/v2/sharenetworks.go
new file mode 100644
index 0000000..b0aefd8
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/sharenetworks.go
@@ -0,0 +1,60 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks"
+)
+
+// CreateShareNetwork will create a share network with a random name. An
+// error will be returned if the share network was unable to be created.
+func CreateShareNetwork(t *testing.T, client *gophercloud.ServiceClient) (*sharenetworks.ShareNetwork, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires share network creation in short mode.")
+	}
+
+	shareNetworkName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create share network: %s", shareNetworkName)
+
+	createOpts := sharenetworks.CreateOpts{
+		Name:        shareNetworkName,
+		Description: "This is a shared network",
+	}
+
+	shareNetwork, err := sharenetworks.Create(client, createOpts).Extract()
+	if err != nil {
+		return shareNetwork, err
+	}
+
+	return shareNetwork, nil
+}
+
+// DeleteShareNetwork will delete a share network. An error will occur if
+// the share network was unable to be deleted.
+func DeleteShareNetwork(t *testing.T, client *gophercloud.ServiceClient, shareNetwork *sharenetworks.ShareNetwork) {
+	err := sharenetworks.Delete(client, shareNetwork.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Failed to delete share network %s: %v", shareNetwork.ID, err)
+	}
+
+	t.Logf("Deleted share network: %s", shareNetwork.ID)
+}
+
+// PrintShareNetwork will print a share network and all of its attributes.
+func PrintShareNetwork(t *testing.T, sharenetwork *sharenetworks.ShareNetwork) {
+	t.Logf("ID: %s", sharenetwork.ID)
+	t.Logf("Project ID: %s", sharenetwork.ProjectID)
+	t.Logf("Neutron network ID: %s", sharenetwork.NeutronNetID)
+	t.Logf("Neutron sub-network ID: %s", sharenetwork.NeutronSubnetID)
+	t.Logf("Nova network ID: %s", sharenetwork.NovaNetID)
+	t.Logf("Network type: %s", sharenetwork.NetworkType)
+	t.Logf("Segmentation ID: %d", sharenetwork.SegmentationID)
+	t.Logf("CIDR: %s", sharenetwork.CIDR)
+	t.Logf("IP version: %d", sharenetwork.IPVersion)
+	t.Logf("Name: %s", sharenetwork.Name)
+	t.Logf("Description: %s", sharenetwork.Description)
+	t.Logf("Created at: %v", sharenetwork.CreatedAt)
+	t.Logf("Updated at: %v", sharenetwork.UpdatedAt)
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/sharenetworks_test.go b/acceptance/openstack/sharedfilesystems/v2/sharenetworks_test.go
new file mode 100644
index 0000000..1a4ae9b
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/sharenetworks_test.go
@@ -0,0 +1,186 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestShareNetworkCreateDestroy(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	shareNetwork, err := CreateShareNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share network: %v", err)
+	}
+
+	newShareNetwork, err := sharenetworks.Get(client, shareNetwork.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve shareNetwork: %v", err)
+	}
+
+	if newShareNetwork.Name != shareNetwork.Name {
+		t.Fatalf("Share network name was expeted to be: %s", shareNetwork.Name)
+	}
+
+	PrintShareNetwork(t, shareNetwork)
+
+	defer DeleteShareNetwork(t, client, shareNetwork)
+}
+
+// Create a share network and update the name and description. Get the share
+// network and verify that the name and description have been updated
+func TestShareNetworkUpdate(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	shareNetwork, err := CreateShareNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share network: %v", err)
+	}
+
+	expectedShareNetwork, err := sharenetworks.Get(client, shareNetwork.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve shareNetwork: %v", err)
+	}
+
+	options := sharenetworks.UpdateOpts{
+		Name:        "NewName",
+		Description: "New share network description",
+		NovaNetID:   "New_nova_network_id",
+	}
+
+	expectedShareNetwork.Name = options.Name
+	expectedShareNetwork.Description = options.Description
+	expectedShareNetwork.NovaNetID = options.NovaNetID
+
+	_, err = sharenetworks.Update(client, shareNetwork.ID, options).Extract()
+	if err != nil {
+		t.Errorf("Unable to update shareNetwork: %v", err)
+	}
+
+	updatedShareNetwork, err := sharenetworks.Get(client, shareNetwork.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve shareNetwork: %v", err)
+	}
+
+	// Update time has to be set in order to get the assert equal to pass
+	expectedShareNetwork.UpdatedAt = updatedShareNetwork.UpdatedAt
+
+	th.CheckDeepEquals(t, expectedShareNetwork, updatedShareNetwork)
+
+	PrintShareNetwork(t, shareNetwork)
+
+	defer DeleteShareNetwork(t, client, shareNetwork)
+}
+
+func TestShareNetworkListDetail(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	allPages, err := sharenetworks.ListDetail(client, sharenetworks.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve share networks: %v", err)
+	}
+
+	allShareNetworks, err := sharenetworks.ExtractShareNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract share networks: %v", err)
+	}
+
+	for _, shareNetwork := range allShareNetworks {
+		PrintShareNetwork(t, &shareNetwork)
+	}
+}
+
+// The test creates 2 shared networks and verifies that only the one(s) with
+// a particular name are being listed
+func TestShareNetworkListFiltering(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	shareNetwork, err := CreateShareNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share network: %v", err)
+	}
+	defer DeleteShareNetwork(t, client, shareNetwork)
+
+	shareNetwork, err = CreateShareNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share network: %v", err)
+	}
+	defer DeleteShareNetwork(t, client, shareNetwork)
+
+	options := sharenetworks.ListOpts{
+		Name: shareNetwork.Name,
+	}
+
+	allPages, err := sharenetworks.ListDetail(client, options).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve share networks: %v", err)
+	}
+
+	allShareNetworks, err := sharenetworks.ExtractShareNetworks(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract share networks: %v", err)
+	}
+
+	for _, listedShareNetwork := range allShareNetworks {
+		if listedShareNetwork.Name != shareNetwork.Name {
+			t.Fatalf("The name of the share network was expected to be %s", shareNetwork.Name)
+		}
+		PrintShareNetwork(t, &listedShareNetwork)
+	}
+}
+
+func TestShareNetworkListPagination(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	shareNetwork, err := CreateShareNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share network: %v", err)
+	}
+	defer DeleteShareNetwork(t, client, shareNetwork)
+
+	shareNetwork, err = CreateShareNetwork(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share network: %v", err)
+	}
+	defer DeleteShareNetwork(t, client, shareNetwork)
+
+	count := 0
+
+	err = sharenetworks.ListDetail(client, sharenetworks.ListOpts{Offset: 0, Limit: 1}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		_, err := sharenetworks.ExtractShareNetworks(page)
+		if err != nil {
+			t.Fatalf("Failed to extract share networks: %v", err)
+			return false, err
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Fatalf("Unable to retrieve share networks: %v", err)
+	}
+
+	if count < 2 {
+		t.Fatal("Expected to get at least 2 pages")
+	}
+
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/shares.go b/acceptance/openstack/sharedfilesystems/v2/shares.go
new file mode 100644
index 0000000..e9060b4
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/shares.go
@@ -0,0 +1,78 @@
+package v2
+
+import (
+	"encoding/json"
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares"
+	"testing"
+)
+
+// CreateShare will create a share with a name, and a size of 1Gb. An
+// error will be returned if the share could not be created
+func CreateShare(t *testing.T, client *gophercloud.ServiceClient) (*shares.Share, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requres share creation in short mode.")
+	}
+
+	choices, err := clients.AcceptanceTestChoicesFromEnv()
+	if err != nil {
+		t.Fatalf("Unable to fetch environment information")
+	}
+
+	t.Logf("Share network id %s", choices.ShareNetworkID)
+	createOpts := shares.CreateOpts{
+		Size:           1,
+		Name:           "My Test Share",
+		ShareProto:     "NFS",
+		ShareNetworkID: choices.ShareNetworkID,
+	}
+
+	share, err := shares.Create(client, createOpts).Extract()
+	if err != nil {
+		return share, err
+	}
+
+	err = waitForStatus(client, share.ID, "available", 60)
+	if err != nil {
+		return share, err
+	}
+
+	return share, nil
+}
+
+// DeleteShare will delete a share. A fatal error will occur if the share
+// failed to be deleted. This works best when used as a deferred function.
+func DeleteShare(t *testing.T, client *gophercloud.ServiceClient, share *shares.Share) {
+	err := shares.Delete(client, share.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete share %s: %v", share.ID, err)
+	}
+
+	t.Logf("Deleted share: %s", share.ID)
+}
+
+// PrintShare prints some information of the share
+func PrintShare(t *testing.T, share *shares.Share) {
+	asJSON, err := json.MarshalIndent(share, "", " ")
+	if err != nil {
+		t.Logf("Cannot print the contents of %s", share.ID)
+	}
+
+	t.Logf("Share %s", string(asJSON))
+}
+
+func waitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := shares.Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/shares_test.go b/acceptance/openstack/sharedfilesystems/v2/shares_test.go
new file mode 100644
index 0000000..ed5d7cc
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/shares_test.go
@@ -0,0 +1,27 @@
+package v2
+
+import (
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares"
+	"testing"
+)
+
+func TestShareCreate(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a sharedfs client: %v", err)
+	}
+
+	share, err := CreateShare(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create a share: %v", err)
+	}
+
+	defer DeleteShare(t, client, share)
+
+	created, err := shares.Get(client, share.ID).Extract()
+	if err != nil {
+		t.Errorf("Unable to retrieve share: %v", err)
+	}
+	PrintShare(t, created)
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/sharetypes.go b/acceptance/openstack/sharedfilesystems/v2/sharetypes.go
new file mode 100644
index 0000000..97b44bd
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/sharetypes.go
@@ -0,0 +1,56 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes"
+)
+
+// CreateShareType will create a share type with a random name. An
+// error will be returned if the share type was unable to be created.
+func CreateShareType(t *testing.T, client *gophercloud.ServiceClient) (*sharetypes.ShareType, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires share type creation in short mode.")
+	}
+
+	shareTypeName := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create share type: %s", shareTypeName)
+
+	extraSpecsOps := sharetypes.ExtraSpecsOpts{
+		DriverHandlesShareServers: true,
+	}
+
+	createOpts := sharetypes.CreateOpts{
+		Name:       shareTypeName,
+		IsPublic:   false,
+		ExtraSpecs: extraSpecsOps,
+	}
+
+	shareType, err := sharetypes.Create(client, createOpts).Extract()
+	if err != nil {
+		return shareType, err
+	}
+
+	return shareType, nil
+}
+
+// DeleteShareType will delete a share type. An error will occur if
+// the share type was unable to be deleted.
+func DeleteShareType(t *testing.T, client *gophercloud.ServiceClient, shareType *sharetypes.ShareType) {
+	err := sharetypes.Delete(client, shareType.ID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Failed to delete share type %s: %v", shareType.ID, err)
+	}
+
+	t.Logf("Deleted share type: %s", shareType.ID)
+}
+
+// PrintShareType will print a share type and all of its attributes.
+func PrintShareType(t *testing.T, shareType *sharetypes.ShareType) {
+	t.Logf("Name: %s", shareType.Name)
+	t.Logf("ID: %s", shareType.ID)
+	t.Logf("OS share type access is public: %t", shareType.IsPublic)
+	t.Logf("Extra specs: %#v", shareType.ExtraSpecs)
+}
diff --git a/acceptance/openstack/sharedfilesystems/v2/sharetypes_test.go b/acceptance/openstack/sharedfilesystems/v2/sharetypes_test.go
new file mode 100644
index 0000000..7a2c21b
--- /dev/null
+++ b/acceptance/openstack/sharedfilesystems/v2/sharetypes_test.go
@@ -0,0 +1,137 @@
+package v2
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/acceptance/clients"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes"
+)
+
+func TestShareTypeCreateDestroy(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	shareType, err := CreateShareType(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share type: %v", err)
+	}
+
+	PrintShareType(t, shareType)
+
+	defer DeleteShareType(t, client, shareType)
+}
+
+func TestShareTypeList(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	allPages, err := sharetypes.List(client, sharetypes.ListOpts{}).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve share types: %v", err)
+	}
+
+	allShareTypes, err := sharetypes.ExtractShareTypes(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract share types: %v", err)
+	}
+
+	for _, shareType := range allShareTypes {
+		PrintShareType(t, &shareType)
+	}
+}
+
+func TestShareTypeGetDefault(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a shared file system client: %v", err)
+	}
+
+	shareType, err := sharetypes.GetDefault(client).Extract()
+	if err != nil {
+		t.Fatalf("Unable to retrieve the default share type: %v", err)
+	}
+
+	if shareType.Name != "default" {
+		t.Fatal("Share type name was expected to be: default")
+	}
+
+	PrintShareType(t, shareType)
+}
+
+func TestShareTypeExtraSpecs(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	shareType, err := CreateShareType(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share type: %v", err)
+	}
+
+	options := sharetypes.SetExtraSpecsOpts{
+		Specs: map[string]interface{}{"my_new_key": "my_value"},
+	}
+
+	_, err = sharetypes.SetExtraSpecs(client, shareType.ID, options).Extract()
+	if err != nil {
+		t.Fatalf("Unable to set extra specs for Share type: %s", shareType.Name)
+	}
+
+	extraSpecs, err := sharetypes.GetExtraSpecs(client, shareType.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to retrieve share type: %s", shareType.Name)
+	}
+
+	if extraSpecs["driver_handles_share_servers"] != "True" {
+		t.Fatal("driver_handles_share_servers was expected to be true")
+	}
+
+	if extraSpecs["my_new_key"] != "my_value" {
+		t.Fatal("my_new_key was expected to be equal to my_value")
+	}
+
+	err = sharetypes.UnsetExtraSpecs(client, shareType.ID, "my_new_key").ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to unset extra specs for Share type: %s", shareType.Name)
+	}
+
+	extraSpecs, err = sharetypes.GetExtraSpecs(client, shareType.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to retrieve share type: %s", shareType.Name)
+	}
+
+	if _, ok := extraSpecs["my_new_key"]; ok {
+		t.Fatalf("my_new_key was expected to be unset for Share type: %s", shareType.Name)
+	}
+
+	PrintShareType(t, shareType)
+
+	defer DeleteShareType(t, client, shareType)
+}
+
+func TestShareTypeShowAccess(t *testing.T) {
+	client, err := clients.NewSharedFileSystemV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create shared file system client: %v", err)
+	}
+
+	shareType, err := CreateShareType(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create share type: %v", err)
+	}
+
+	_, err = sharetypes.ShowAccess(client, shareType.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to retrieve the access details for a share type: %v", err)
+	}
+
+	PrintShareType(t, shareType)
+
+	defer DeleteShareType(t, client, shareType)
+
+}
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..d2fd298
--- /dev/null
+++ b/acceptance/tools/tools.go
@@ -0,0 +1,73 @@
+package tools
+
+import (
+	"crypto/rand"
+	"encoding/json"
+	"errors"
+	mrand "math/rand"
+	"testing"
+	"time"
+)
+
+// ErrTimeout is returned if WaitFor takes longer than 300 second to happen.
+var ErrTimeout = errors.New("Timed out")
+
+// 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
+}
+
+// PrintResource returns a resource as a readable structure
+func PrintResource(t *testing.T, resource interface{}) {
+	b, _ := json.MarshalIndent(resource, "", "  ")
+	t.Logf(string(b))
+}
diff --git a/auth_options.go b/auth_options.go
new file mode 100644
index 0000000..7a16131
--- /dev/null
+++ b/auth_options.go
@@ -0,0 +1,338 @@
+package gophercloud
+
+/*
+AuthOptions stores information needed to authenticate to an OpenStack Cloud.
+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 `json:"-"`
+
+	// 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 string `json:"username,omitempty"`
+	UserID   string `json:"id,omitempty"`
+
+	Password string `json:"password,omitempty"`
+
+	// At most one of DomainID and DomainName must be provided if using Username
+	// with Identity V3. Otherwise, either are optional.
+	DomainID   string `json:"id,omitempty"`
+	DomainName string `json:"name,omitempty"`
+
+	// The TenantID and TenantName fields are optional for the Identity V2 API.
+	// The same fields are known as project_id and project_name in the Identity
+	// V3 API, but are collected as TenantID and TenantName here in both cases.
+	// 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.
+	// If DomainID or DomainName are provided, they will also apply to TenantName.
+	// It is not currently possible to authenticate with Username and a Domain
+	// and scope to a Project in a different Domain by using TenantName. To
+	// accomplish that, the ProjectID will need to be provided to the TenantID
+	// option.
+	TenantID   string `json:"tenantId,omitempty"`
+	TenantName string `json:"tenantName,omitempty"`
+
+	// 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.
+	//
+	// NOTE: The reauth function will try to re-authenticate endlessly if left unchecked.
+	// The way to limit the number of attempts is to provide a custom HTTP client to the provider client
+	// and provide a transport that implements the RoundTripper interface and stores the number of failed retries.
+	// For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
+	AllowReauth bool `json:"-"`
+
+	// TokenID allows users to authenticate (possibly as another user) with an
+	// authentication token ID.
+	TokenID string `json:"-"`
+}
+
+// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
+// interface in the v2 tokens package
+func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
+	// Populate the request map.
+	authMap := make(map[string]interface{})
+
+	if opts.Username != "" {
+		if opts.Password != "" {
+			authMap["passwordCredentials"] = map[string]interface{}{
+				"username": opts.Username,
+				"password": opts.Password,
+			}
+		} else {
+			return nil, ErrMissingInput{Argument: "Password"}
+		}
+	} else if opts.TokenID != "" {
+		authMap["token"] = map[string]interface{}{
+			"id": opts.TokenID,
+		}
+	} else {
+		return nil, ErrMissingInput{Argument: "Username"}
+	}
+
+	if opts.TenantID != "" {
+		authMap["tenantId"] = opts.TenantID
+	}
+	if opts.TenantName != "" {
+		authMap["tenantName"] = opts.TenantName
+	}
+
+	return map[string]interface{}{"auth": authMap}, nil
+}
+
+func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
+	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 authReq struct {
+		Identity identityReq `json:"identity"`
+	}
+
+	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 opts.TenantID != "" {
+		return nil, ErrTenantIDProvided{}
+	}
+	if opts.TenantName != "" {
+		return nil, ErrTenantNameProvided{}
+	}
+
+	if opts.Password == "" {
+		if opts.TokenID != "" {
+			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
+			// parameters.
+			if opts.Username != "" {
+				return nil, ErrUsernameWithToken{}
+			}
+			if opts.UserID != "" {
+				return nil, ErrUserIDWithToken{}
+			}
+			if opts.DomainID != "" {
+				return nil, ErrDomainIDWithToken{}
+			}
+			if opts.DomainName != "" {
+				return nil, ErrDomainNameWithToken{}
+			}
+
+			// Configure the request for Token authentication.
+			req.Auth.Identity.Methods = []string{"token"}
+			req.Auth.Identity.Token = &tokenReq{
+				ID: opts.TokenID,
+			}
+		} else {
+			// If no password or token ID are available, authentication can't continue.
+			return nil, ErrMissingPassword{}
+		}
+	} else {
+		// Password authentication.
+		req.Auth.Identity.Methods = []string{"password"}
+
+		// At least one of Username and UserID must be specified.
+		if opts.Username == "" && opts.UserID == "" {
+			return nil, ErrUsernameOrUserID{}
+		}
+
+		if opts.Username != "" {
+			// If Username is provided, UserID may not be provided.
+			if opts.UserID != "" {
+				return nil, ErrUsernameOrUserID{}
+			}
+
+			// Either DomainID or DomainName must also be specified.
+			if opts.DomainID == "" && opts.DomainName == "" {
+				return nil, ErrDomainIDOrDomainName{}
+			}
+
+			if opts.DomainID != "" {
+				if opts.DomainName != "" {
+					return nil, ErrDomainIDOrDomainName{}
+				}
+
+				// Configure the request for Username and Password authentication with a DomainID.
+				req.Auth.Identity.Password = &passwordReq{
+					User: userReq{
+						Name:     &opts.Username,
+						Password: opts.Password,
+						Domain:   &domainReq{ID: &opts.DomainID},
+					},
+				}
+			}
+
+			if opts.DomainName != "" {
+				// Configure the request for Username and Password authentication with a DomainName.
+				req.Auth.Identity.Password = &passwordReq{
+					User: userReq{
+						Name:     &opts.Username,
+						Password: opts.Password,
+						Domain:   &domainReq{Name: &opts.DomainName},
+					},
+				}
+			}
+		}
+
+		if opts.UserID != "" {
+			// If UserID is specified, neither DomainID nor DomainName may be.
+			if opts.DomainID != "" {
+				return nil, ErrDomainIDWithUserID{}
+			}
+			if opts.DomainName != "" {
+				return nil, ErrDomainNameWithUserID{}
+			}
+
+			// Configure the request for UserID and Password authentication.
+			req.Auth.Identity.Password = &passwordReq{
+				User: userReq{ID: &opts.UserID, Password: opts.Password},
+			}
+		}
+	}
+
+	b, err := BuildRequestBody(req, "")
+	if err != nil {
+		return nil, err
+	}
+
+	if len(scope) != 0 {
+		b["auth"].(map[string]interface{})["scope"] = scope
+	}
+
+	return b, nil
+}
+
+func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
+
+	var scope struct {
+		ProjectID   string
+		ProjectName string
+		DomainID    string
+		DomainName  string
+	}
+
+	if opts.TenantID != "" {
+		scope.ProjectID = opts.TenantID
+		opts.TenantID = ""
+		opts.TenantName = ""
+	} else {
+		if opts.TenantName != "" {
+			scope.ProjectName = opts.TenantName
+			scope.DomainID = opts.DomainID
+			scope.DomainName = opts.DomainName
+		}
+		opts.TenantName = ""
+	}
+
+	if scope.ProjectName != "" {
+		// ProjectName provided: either DomainID or DomainName must also be supplied.
+		// ProjectID may not be supplied.
+		if scope.DomainID == "" && scope.DomainName == "" {
+			return nil, ErrScopeDomainIDOrDomainName{}
+		}
+		if scope.ProjectID != "" {
+			return nil, ErrScopeProjectIDOrProjectName{}
+		}
+
+		if scope.DomainID != "" {
+			// ProjectName + DomainID
+			return map[string]interface{}{
+				"project": map[string]interface{}{
+					"name":   &scope.ProjectName,
+					"domain": map[string]interface{}{"id": &scope.DomainID},
+				},
+			}, nil
+		}
+
+		if scope.DomainName != "" {
+			// ProjectName + DomainName
+			return map[string]interface{}{
+				"project": map[string]interface{}{
+					"name":   &scope.ProjectName,
+					"domain": map[string]interface{}{"name": &scope.DomainName},
+				},
+			}, nil
+		}
+	} else if scope.ProjectID != "" {
+		// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+		if scope.DomainID != "" {
+			return nil, ErrScopeProjectIDAlone{}
+		}
+		if scope.DomainName != "" {
+			return nil, ErrScopeProjectIDAlone{}
+		}
+
+		// ProjectID
+		return map[string]interface{}{
+			"project": map[string]interface{}{
+				"id": &scope.ProjectID,
+			},
+		}, nil
+	} else if scope.DomainID != "" {
+		// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+		if scope.DomainName != "" {
+			return nil, ErrScopeDomainIDOrDomainName{}
+		}
+
+		// DomainID
+		return map[string]interface{}{
+			"domain": map[string]interface{}{
+				"id": &scope.DomainID,
+			},
+		}, nil
+	} else if scope.DomainName != "" {
+		return nil, ErrScopeDomainName{}
+	}
+
+	return nil, nil
+}
+
+func (opts AuthOptions) CanReauth() bool {
+	return opts.AllowReauth
+}
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..b559516
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,69 @@
+/*
+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. The IdentityEndpoint is typically refered to as
+"auth_url" in information provided by the cloud operator. Additionally,
+the cloud may refer to TenantID or TenantName as project_id and project_name.
+These are defined like so:
+
+  opts := gophercloud.AuthOptions{
+    IdentityEndpoint: "https://openstack.example.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..9887947
--- /dev/null
+++ b/endpoint_search.go
@@ -0,0 +1,76 @@
+package gophercloud
+
+// 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/errors.go b/errors.go
new file mode 100644
index 0000000..e0fe7c1
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,408 @@
+package gophercloud
+
+import "fmt"
+
+// BaseError is an error type that all other error types embed.
+type BaseError struct {
+	DefaultErrString string
+	Info             string
+}
+
+func (e BaseError) Error() string {
+	e.DefaultErrString = "An error occurred while executing a Gophercloud request."
+	return e.choseErrString()
+}
+
+func (e BaseError) choseErrString() string {
+	if e.Info != "" {
+		return e.Info
+	}
+	return e.DefaultErrString
+}
+
+// ErrMissingInput is the error when input is required in a particular
+// situation but not provided by the user
+type ErrMissingInput struct {
+	BaseError
+	Argument string
+}
+
+func (e ErrMissingInput) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument)
+	return e.choseErrString()
+}
+
+// ErrInvalidInput is an error type used for most non-HTTP Gophercloud errors.
+type ErrInvalidInput struct {
+	ErrMissingInput
+	Value interface{}
+}
+
+func (e ErrInvalidInput) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value)
+	return e.choseErrString()
+}
+
+// ErrUnexpectedResponseCode is returned by the Request method when a response code other than
+// those listed in OkCodes is encountered.
+type ErrUnexpectedResponseCode struct {
+	BaseError
+	URL      string
+	Method   string
+	Expected []int
+	Actual   int
+	Body     []byte
+}
+
+func (e ErrUnexpectedResponseCode) Error() string {
+	e.DefaultErrString = fmt.Sprintf(
+		"Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
+		e.Expected, e.Method, e.URL, e.Actual, e.Body,
+	)
+	return e.choseErrString()
+}
+
+// ErrDefault400 is the default error type returned on a 400 HTTP response code.
+type ErrDefault400 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault401 is the default error type returned on a 401 HTTP response code.
+type ErrDefault401 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault404 is the default error type returned on a 404 HTTP response code.
+type ErrDefault404 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault405 is the default error type returned on a 405 HTTP response code.
+type ErrDefault405 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault408 is the default error type returned on a 408 HTTP response code.
+type ErrDefault408 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault429 is the default error type returned on a 429 HTTP response code.
+type ErrDefault429 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault500 is the default error type returned on a 500 HTTP response code.
+type ErrDefault500 struct {
+	ErrUnexpectedResponseCode
+}
+
+// ErrDefault503 is the default error type returned on a 503 HTTP response code.
+type ErrDefault503 struct {
+	ErrUnexpectedResponseCode
+}
+
+func (e ErrDefault400) Error() string {
+	return "Invalid request due to incorrect syntax or missing required parameters."
+}
+func (e ErrDefault401) Error() string {
+	return "Authentication failed"
+}
+func (e ErrDefault404) Error() string {
+	return "Resource not found"
+}
+func (e ErrDefault405) Error() string {
+	return "Method not allowed"
+}
+func (e ErrDefault408) Error() string {
+	return "The server timed out waiting for the request"
+}
+func (e ErrDefault429) Error() string {
+	return "Too many requests have been sent in a given amount of time. Pause" +
+		" requests, wait up to one minute, and try again."
+}
+func (e ErrDefault500) Error() string {
+	return "Internal Server Error"
+}
+func (e ErrDefault503) Error() string {
+	return "The service is currently unable to handle the request due to a temporary" +
+		" overloading or maintenance. This is a temporary condition. Try again later."
+}
+
+// Err400er is the interface resource error types implement to override the error message
+// from a 400 error.
+type Err400er interface {
+	Error400(ErrUnexpectedResponseCode) error
+}
+
+// Err401er is the interface resource error types implement to override the error message
+// from a 401 error.
+type Err401er interface {
+	Error401(ErrUnexpectedResponseCode) error
+}
+
+// Err404er is the interface resource error types implement to override the error message
+// from a 404 error.
+type Err404er interface {
+	Error404(ErrUnexpectedResponseCode) error
+}
+
+// Err405er is the interface resource error types implement to override the error message
+// from a 405 error.
+type Err405er interface {
+	Error405(ErrUnexpectedResponseCode) error
+}
+
+// Err408er is the interface resource error types implement to override the error message
+// from a 408 error.
+type Err408er interface {
+	Error408(ErrUnexpectedResponseCode) error
+}
+
+// Err429er is the interface resource error types implement to override the error message
+// from a 429 error.
+type Err429er interface {
+	Error429(ErrUnexpectedResponseCode) error
+}
+
+// Err500er is the interface resource error types implement to override the error message
+// from a 500 error.
+type Err500er interface {
+	Error500(ErrUnexpectedResponseCode) error
+}
+
+// Err503er is the interface resource error types implement to override the error message
+// from a 503 error.
+type Err503er interface {
+	Error503(ErrUnexpectedResponseCode) error
+}
+
+// ErrTimeOut is the error type returned when an operations times out.
+type ErrTimeOut struct {
+	BaseError
+}
+
+func (e ErrTimeOut) Error() string {
+	e.DefaultErrString = "A time out occurred"
+	return e.choseErrString()
+}
+
+// ErrUnableToReauthenticate is the error type returned when reauthentication fails.
+type ErrUnableToReauthenticate struct {
+	BaseError
+	ErrOriginal error
+}
+
+func (e ErrUnableToReauthenticate) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal)
+	return e.choseErrString()
+}
+
+// ErrErrorAfterReauthentication is the error type returned when reauthentication
+// succeeds, but an error occurs afterword (usually an HTTP error).
+type ErrErrorAfterReauthentication struct {
+	BaseError
+	ErrOriginal error
+}
+
+func (e ErrErrorAfterReauthentication) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal)
+	return e.choseErrString()
+}
+
+// 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.
+type ErrServiceNotFound struct {
+	BaseError
+}
+
+func (e ErrServiceNotFound) Error() string {
+	e.DefaultErrString = "No suitable service could be found in the service catalog."
+	return e.choseErrString()
+}
+
+// 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.
+type ErrEndpointNotFound struct {
+	BaseError
+}
+
+func (e ErrEndpointNotFound) Error() string {
+	e.DefaultErrString = "No suitable endpoint could be found in the service catalog."
+	return e.choseErrString()
+}
+
+// ErrResourceNotFound is the error when trying to retrieve a resource's
+// ID by name and the resource doesn't exist.
+type ErrResourceNotFound struct {
+	BaseError
+	Name         string
+	ResourceType string
+}
+
+func (e ErrResourceNotFound) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name)
+	return e.choseErrString()
+}
+
+// ErrMultipleResourcesFound is the error when trying to retrieve a resource's
+// ID by name and multiple resources have the user-provided name.
+type ErrMultipleResourcesFound struct {
+	BaseError
+	Name         string
+	Count        int
+	ResourceType string
+}
+
+func (e ErrMultipleResourcesFound) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name)
+	return e.choseErrString()
+}
+
+// ErrUnexpectedType is the error when an unexpected type is encountered
+type ErrUnexpectedType struct {
+	BaseError
+	Expected string
+	Actual   string
+}
+
+func (e ErrUnexpectedType) Error() string {
+	e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual)
+	return e.choseErrString()
+}
+
+func unacceptedAttributeErr(attribute string) string {
+	return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute)
+}
+
+func redundantWithTokenErr(attribute string) string {
+	return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute)
+}
+
+func redundantWithUserID(attribute string) string {
+	return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute)
+}
+
+// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
+type ErrAPIKeyProvided struct{ BaseError }
+
+func (e ErrAPIKeyProvided) Error() string {
+	return unacceptedAttributeErr("APIKey")
+}
+
+// ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
+type ErrTenantIDProvided struct{ BaseError }
+
+func (e ErrTenantIDProvided) Error() string {
+	return unacceptedAttributeErr("TenantID")
+}
+
+// ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
+type ErrTenantNameProvided struct{ BaseError }
+
+func (e ErrTenantNameProvided) Error() string {
+	return unacceptedAttributeErr("TenantName")
+}
+
+// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
+type ErrUsernameWithToken struct{ BaseError }
+
+func (e ErrUsernameWithToken) Error() string {
+	return redundantWithTokenErr("Username")
+}
+
+// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
+type ErrUserIDWithToken struct{ BaseError }
+
+func (e ErrUserIDWithToken) Error() string {
+	return redundantWithTokenErr("UserID")
+}
+
+// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
+type ErrDomainIDWithToken struct{ BaseError }
+
+func (e ErrDomainIDWithToken) Error() string {
+	return redundantWithTokenErr("DomainID")
+}
+
+// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
+type ErrDomainNameWithToken struct{ BaseError }
+
+func (e ErrDomainNameWithToken) Error() string {
+	return redundantWithTokenErr("DomainName")
+}
+
+// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
+type ErrUsernameOrUserID struct{ BaseError }
+
+func (e ErrUsernameOrUserID) Error() string {
+	return "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.
+type ErrDomainIDWithUserID struct{ BaseError }
+
+func (e ErrDomainIDWithUserID) Error() string {
+	return redundantWithUserID("DomainID")
+}
+
+// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
+type ErrDomainNameWithUserID struct{ BaseError }
+
+func (e ErrDomainNameWithUserID) Error() string {
+	return 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.
+type ErrDomainIDOrDomainName struct{ BaseError }
+
+func (e ErrDomainIDOrDomainName) Error() string {
+	return "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.
+type ErrMissingPassword struct{ BaseError }
+
+func (e ErrMissingPassword) Error() string {
+	return "You must provide a password to authenticate"
+}
+
+// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
+type ErrScopeDomainIDOrDomainName struct{ BaseError }
+
+func (e ErrScopeDomainIDOrDomainName) Error() string {
+	return "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.
+type ErrScopeProjectIDOrProjectName struct{ BaseError }
+
+func (e ErrScopeProjectIDOrProjectName) Error() string {
+	return "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.
+type ErrScopeProjectIDAlone struct{ BaseError }
+
+func (e ErrScopeProjectIDAlone) Error() string {
+	return "ProjectID must be supplied alone in a Scope"
+}
+
+// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
+type ErrScopeDomainName struct{ BaseError }
+
+func (e ErrScopeDomainName) Error() string {
+	return "DomainName must be supplied with a ProjectName or ProjectID in a Scope"
+}
+
+// ErrScopeEmpty indicates that no credentials were provided in a Scope.
+type ErrScopeEmpty struct{ BaseError }
+
+func (e ErrScopeEmpty) Error() string {
+	return "You must provide either a Project or Domain in a Scope"
+}
diff --git a/internal/pkg.go b/internal/pkg.go
new file mode 100644
index 0000000..5bf0569
--- /dev/null
+++ b/internal/pkg.go
@@ -0,0 +1 @@
+package internal
diff --git a/openstack/auth_env.go b/openstack/auth_env.go
new file mode 100644
index 0000000..f6d2eb1
--- /dev/null
+++ b/openstack/auth_env.go
@@ -0,0 +1,52 @@
+package openstack
+
+import (
+	"os"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+var nilOptions = gophercloud.AuthOptions{}
+
+// AuthOptionsFromEnv 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 == "" {
+		err := gophercloud.ErrMissingInput{Argument: "authURL"}
+		return nilOptions, err
+	}
+
+	if username == "" && userID == "" {
+		err := gophercloud.ErrMissingInput{Argument: "username"}
+		return nilOptions, err
+	}
+
+	if password == "" {
+		err := gophercloud.ErrMissingInput{Argument: "password"}
+		return nilOptions, err
+	}
+
+	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/extensions/volumeactions/doc.go b/openstack/blockstorage/extensions/volumeactions/doc.go
new file mode 100644
index 0000000..0935fdb
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/doc.go
@@ -0,0 +1,5 @@
+// Package volumeactions 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 volumeactions
diff --git a/openstack/blockstorage/extensions/volumeactions/requests.go b/openstack/blockstorage/extensions/volumeactions/requests.go
new file mode 100644
index 0000000..e3c7df3
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/requests.go
@@ -0,0 +1,253 @@
+package volumeactions
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// AttachOptsBuilder allows extensions to add additional parameters to the
+// Attach request.
+type AttachOptsBuilder interface {
+	ToVolumeAttachMap() (map[string]interface{}, error)
+}
+
+// AttachMode describes the attachment mode for volumes.
+type AttachMode string
+
+// These constants determine how a volume is attached
+const (
+	ReadOnly  AttachMode = "ro"
+	ReadWrite AttachMode = "rw"
+)
+
+// AttachOpts contains options for attaching a Volume.
+type AttachOpts struct {
+	// The mountpoint of this volume
+	MountPoint string `json:"mountpoint,omitempty"`
+	// The nova instance ID, can't set simultaneously with HostName
+	InstanceUUID string `json:"instance_uuid,omitempty"`
+	// The hostname of baremetal host, can't set simultaneously with InstanceUUID
+	HostName string `json:"host_name,omitempty"`
+	// Mount mode of this volume
+	Mode AttachMode `json:"mode,omitempty"`
+}
+
+// ToVolumeAttachMap assembles a request body based on the contents of a
+// AttachOpts.
+func (opts AttachOpts) ToVolumeAttachMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "os-attach")
+}
+
+// Attach will attach a volume based on the values in AttachOpts.
+func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder) (r AttachResult) {
+	b, err := opts.ToVolumeAttachMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(attachURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// BeginDetach will mark the volume as detaching
+func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) {
+	b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})}
+	_, r.Err = client.Post(beginDetachingURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// DetachOptsBuilder allows extensions to add additional parameters to the
+// Detach request.
+type DetachOptsBuilder interface {
+	ToVolumeDetachMap() (map[string]interface{}, error)
+}
+
+type DetachOpts struct {
+	AttachmentID string `json:"attachment_id,omitempty"`
+}
+
+// ToVolumeDetachMap assembles a request body based on the contents of a
+// DetachOpts.
+func (opts DetachOpts) ToVolumeDetachMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "os-detach")
+}
+
+// Detach will detach a volume based on volume id.
+func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder) (r DetachResult) {
+	b, err := opts.ToVolumeDetachMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(detachURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// Reserve will reserve a volume based on volume id.
+func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) {
+	b := map[string]interface{}{"os-reserve": make(map[string]interface{})}
+	_, r.Err = client.Post(reserveURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	return
+}
+
+// Unreserve will unreserve a volume based on volume id.
+func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) {
+	b := map[string]interface{}{"os-unreserve": make(map[string]interface{})}
+	_, r.Err = client.Post(unreserveURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	return
+}
+
+// InitializeConnectionOptsBuilder allows extensions to add additional parameters to the
+// InitializeConnection request.
+type InitializeConnectionOptsBuilder interface {
+	ToVolumeInitializeConnectionMap() (map[string]interface{}, error)
+}
+
+// InitializeConnectionOpts hosts options for InitializeConnection.
+type InitializeConnectionOpts struct {
+	IP        string   `json:"ip,omitempty"`
+	Host      string   `json:"host,omitempty"`
+	Initiator string   `json:"initiator,omitempty"`
+	Wwpns     []string `json:"wwpns,omitempty"`
+	Wwnns     string   `json:"wwnns,omitempty"`
+	Multipath *bool    `json:"multipath,omitempty"`
+	Platform  string   `json:"platform,omitempty"`
+	OSType    string   `json:"os_type,omitempty"`
+}
+
+// ToVolumeInitializeConnectionMap assembles a request body based on the contents of a
+// InitializeConnectionOpts.
+func (opts InitializeConnectionOpts) ToVolumeInitializeConnectionMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "connector")
+	return map[string]interface{}{"os-initialize_connection": b}, err
+}
+
+// InitializeConnection initializes iscsi connection.
+func InitializeConnection(client *gophercloud.ServiceClient, id string, opts InitializeConnectionOptsBuilder) (r InitializeConnectionResult) {
+	b, err := opts.ToVolumeInitializeConnectionMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(initializeConnectionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	return
+}
+
+// TerminateConnectionOptsBuilder allows extensions to add additional parameters to the
+// TerminateConnection request.
+type TerminateConnectionOptsBuilder interface {
+	ToVolumeTerminateConnectionMap() (map[string]interface{}, error)
+}
+
+// TerminateConnectionOpts hosts options for TerminateConnection.
+type TerminateConnectionOpts struct {
+	IP        string   `json:"ip,omitempty"`
+	Host      string   `json:"host,omitempty"`
+	Initiator string   `json:"initiator,omitempty"`
+	Wwpns     []string `json:"wwpns,omitempty"`
+	Wwnns     string   `json:"wwnns,omitempty"`
+	Multipath *bool    `json:"multipath,omitempty"`
+	Platform  string   `json:"platform,omitempty"`
+	OSType    string   `json:"os_type,omitempty"`
+}
+
+// ToVolumeTerminateConnectionMap assembles a request body based on the contents of a
+// TerminateConnectionOpts.
+func (opts TerminateConnectionOpts) ToVolumeTerminateConnectionMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "connector")
+	return map[string]interface{}{"os-terminate_connection": b}, err
+}
+
+// TerminateConnection terminates iscsi connection.
+func TerminateConnection(client *gophercloud.ServiceClient, id string, opts TerminateConnectionOptsBuilder) (r TerminateConnectionResult) {
+	b, err := opts.ToVolumeTerminateConnectionMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(teminateConnectionURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// ExtendSizeOptsBuilder allows extensions to add additional parameters to the
+// ExtendSize request.
+type ExtendSizeOptsBuilder interface {
+	ToVolumeExtendSizeMap() (map[string]interface{}, error)
+}
+
+// ExtendSizeOpts contain options for extending the size of an existing Volume. This object is passed
+// to the volumes.ExtendSize function.
+type ExtendSizeOpts struct {
+	// NewSize is the new size of the volume, in GB
+	NewSize int `json:"new_size" required:"true"`
+}
+
+// ToVolumeExtendSizeMap assembles a request body based on the contents of an
+// ExtendSizeOpts.
+func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "os-extend")
+}
+
+// ExtendSize will extend the size of the volume based on the provided information.
+// This operation does not return a response body.
+func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) {
+	b, err := opts.ToVolumeExtendSizeMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(extendSizeURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// UploadImageOptsBuilder allows extensions to add additional parameters to the
+// UploadImage request.
+type UploadImageOptsBuilder interface {
+	ToVolumeUploadImageMap() (map[string]interface{}, error)
+}
+
+// UploadImageOpts contains options for uploading a Volume to image storage.
+type UploadImageOpts struct {
+	// Container format, may be bare, ofv, ova, etc.
+	ContainerFormat string `json:"container_format,omitempty"`
+	// Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc.
+	DiskFormat string `json:"disk_format,omitempty"`
+	// The name of image that will be stored in glance
+	ImageName string `json:"image_name,omitempty"`
+	// Force image creation, usable if volume attached to instance
+	Force bool `json:"force,omitempty"`
+}
+
+// ToVolumeUploadImageMap assembles a request body based on the contents of a
+// UploadImageOpts.
+func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "os-volume_upload_image")
+}
+
+// UploadImage will upload image base on the values in UploadImageOptsBuilder
+func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) {
+	b, err := opts.ToVolumeUploadImageMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(uploadURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
diff --git a/openstack/blockstorage/extensions/volumeactions/results.go b/openstack/blockstorage/extensions/volumeactions/results.go
new file mode 100644
index 0000000..634b04d
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/results.go
@@ -0,0 +1,61 @@
+package volumeactions
+
+import "github.com/gophercloud/gophercloud"
+
+// AttachResult contains the response body and error from a Get request.
+type AttachResult struct {
+	gophercloud.ErrResult
+}
+
+// BeginDetachingResult contains the response body and error from a Get request.
+type BeginDetachingResult struct {
+	gophercloud.ErrResult
+}
+
+// DetachResult contains the response body and error from a Get request.
+type DetachResult struct {
+	gophercloud.ErrResult
+}
+
+// UploadImageResult contains the response body and error from a UploadImage request.
+type UploadImageResult struct {
+	gophercloud.ErrResult
+}
+
+// ReserveResult contains the response body and error from a Get request.
+type ReserveResult struct {
+	gophercloud.ErrResult
+}
+
+// UnreserveResult contains the response body and error from a Get request.
+type UnreserveResult struct {
+	gophercloud.ErrResult
+}
+
+// TerminateConnectionResult contains the response body and error from a Get request.
+type TerminateConnectionResult struct {
+	gophercloud.ErrResult
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (map[string]interface{}, error) {
+	var s struct {
+		ConnectionInfo map[string]interface{} `json:"connection_info"`
+	}
+	err := r.ExtractInto(&s)
+	return s.ConnectionInfo, err
+}
+
+// InitializeConnectionResult contains the response body and error from a Get request.
+type InitializeConnectionResult struct {
+	commonResult
+}
+
+// ExtendSizeResult contains the response body and error from an ExtendSize request.
+type ExtendSizeResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/blockstorage/extensions/volumeactions/testing/doc.go b/openstack/blockstorage/extensions/volumeactions/testing/doc.go
new file mode 100644
index 0000000..e720733
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/testing/doc.go
@@ -0,0 +1,2 @@
+// volumeactions
+package testing
diff --git a/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go b/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go
new file mode 100644
index 0000000..d914097
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go
@@ -0,0 +1,251 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func MockAttachResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-attach":
+    {
+        "mountpoint": "/mnt",
+        "mode": "rw",
+        "instance_uuid": "50902f4f-a974-46a0-85e9-7efc5e22dfdd"
+    }
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockBeginDetachingResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-begin_detaching": {}
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockDetachResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-detach": {}
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockUploadImageResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-volume_upload_image": {
+        "container_format": "bare",
+        "force": true,
+        "image_name": "test",
+        "disk_format": "raw"
+    }
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockReserveResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-reserve": {}
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockUnreserveResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-unreserve": {}
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockInitializeConnectionResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-initialize_connection":
+    {
+        "connector":
+        {
+        "ip":"127.0.0.1",
+        "host":"stack",
+        "initiator":"iqn.1994-05.com.redhat:17cf566367d2",
+        "multipath": false,
+        "platform": "x86_64",
+        "os_type": "linux2"
+        }
+    }
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{
+"connection_info": {
+    "data": {
+      "target_portals": [
+        "172.31.17.48:3260"
+      ],
+      "auth_method": "CHAP",
+      "auth_username": "5MLtcsTEmNN5jFVcT6ui",
+      "access_mode": "rw",
+      "target_lun": 0,
+      "volume_id": "cd281d77-8217-4830-be95-9528227c105c",
+      "target_luns": [
+        0
+      ],
+      "target_iqns": [
+        "iqn.2010-10.org.openstack:volume-cd281d77-8217-4830-be95-9528227c105c"
+      ],
+      "auth_password": "x854ZY5Re3aCkdNL",
+      "target_discovered": false,
+      "encrypted": false,
+      "qos_specs": null,
+      "target_iqn": "iqn.2010-10.org.openstack:volume-cd281d77-8217-4830-be95-9528227c105c",
+      "target_portal": "172.31.17.48:3260"
+    },
+    "driver_volume_type": "iscsi"
+  }
+            }`)
+		})
+}
+
+func MockTerminateConnectionResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-terminate_connection":
+    {
+        "connector":
+        {
+        "ip":"127.0.0.1",
+        "host":"stack",
+        "initiator":"iqn.1994-05.com.redhat:17cf566367d2",
+        "multipath": true,
+        "platform": "x86_64",
+        "os_type": "linux2"
+        }
+    }
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
+func MockExtendSizeResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		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, `
+{
+    "os-extend":
+    {
+        "new_size": 3
+    }
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
diff --git a/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go b/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go
new file mode 100644
index 0000000..6132161
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go
@@ -0,0 +1,130 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestAttach(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAttachResponse(t)
+
+	options := &volumeactions.AttachOpts{
+		MountPoint:   "/mnt",
+		Mode:         "rw",
+		InstanceUUID: "50902f4f-a974-46a0-85e9-7efc5e22dfdd",
+	}
+	err := volumeactions.Attach(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestBeginDetaching(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockBeginDetachingResponse(t)
+
+	err := volumeactions.BeginDetaching(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDetach(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDetachResponse(t)
+
+	err := volumeactions.Detach(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", &volumeactions.DetachOpts{}).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUploadImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	MockUploadImageResponse(t)
+	options := &volumeactions.UploadImageOpts{
+		ContainerFormat: "bare",
+		DiskFormat:      "raw",
+		ImageName:       "test",
+		Force:           true,
+	}
+
+	err := volumeactions.UploadImage(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestReserve(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockReserveResponse(t)
+
+	err := volumeactions.Reserve(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUnreserve(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUnreserveResponse(t)
+
+	err := volumeactions.Unreserve(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestInitializeConnection(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockInitializeConnectionResponse(t)
+
+	options := &volumeactions.InitializeConnectionOpts{
+		IP:        "127.0.0.1",
+		Host:      "stack",
+		Initiator: "iqn.1994-05.com.redhat:17cf566367d2",
+		Multipath: gophercloud.Disabled,
+		Platform:  "x86_64",
+		OSType:    "linux2",
+	}
+	_, err := volumeactions.InitializeConnection(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestTerminateConnection(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockTerminateConnectionResponse(t)
+
+	options := &volumeactions.TerminateConnectionOpts{
+		IP:        "127.0.0.1",
+		Host:      "stack",
+		Initiator: "iqn.1994-05.com.redhat:17cf566367d2",
+		Multipath: gophercloud.Enabled,
+		Platform:  "x86_64",
+		OSType:    "linux2",
+	}
+	err := volumeactions.TerminateConnection(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestExtendSize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockExtendSizeResponse(t)
+
+	options := &volumeactions.ExtendSizeOpts{
+		NewSize: 3,
+	}
+
+	err := volumeactions.ExtendSize(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/extensions/volumeactions/urls.go b/openstack/blockstorage/extensions/volumeactions/urls.go
new file mode 100644
index 0000000..5efd2b2
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumeactions/urls.go
@@ -0,0 +1,39 @@
+package volumeactions
+
+import "github.com/gophercloud/gophercloud"
+
+func attachURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("volumes", id, "action")
+}
+
+func beginDetachingURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func detachURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func uploadURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func reserveURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func unreserveURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func initializeConnectionURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func teminateConnectionURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
+func extendSizeURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
diff --git a/openstack/blockstorage/extensions/volumetenants/results.go b/openstack/blockstorage/extensions/volumetenants/results.go
new file mode 100644
index 0000000..b7d51c7
--- /dev/null
+++ b/openstack/blockstorage/extensions/volumetenants/results.go
@@ -0,0 +1,12 @@
+package volumetenants
+
+// VolumeExt is an extension to the base Volume object
+type VolumeExt struct {
+	// TenantID is the id of the project that owns the volume.
+	TenantID string `json:"os-vol-tenant-attr:tenant_id"`
+}
+
+// UnmarshalJSON to override default
+func (r *VolumeExt) UnmarshalJSON(b []byte) error {
+	return 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..725c13a
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,20 @@
+package apiversions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, v), &r.Body, nil)
+	return
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..f510c6d
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -0,0 +1,49 @@
+package apiversions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// APIVersion represents an API version for Cinder.
+type APIVersion struct {
+	ID      string `json:"id"`      // unique identifier
+	Status  string `json:"status"`  // current status
+	Updated string `json:"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)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]APIVersion, error) {
+	var s struct {
+		Versions []APIVersion `json:"versions"`
+	}
+	err := (r.(APIVersionPage)).ExtractInto(&s)
+	return s.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 s struct {
+		Version *APIVersion `json:"version"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Version, err
+}
diff --git a/openstack/blockstorage/v1/apiversions/testing/doc.go b/openstack/blockstorage/v1/apiversions/testing/doc.go
new file mode 100644
index 0000000..12e4bda
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/testing/doc.go
@@ -0,0 +1,2 @@
+// apiversions_v1
+package testing
diff --git a/openstack/blockstorage/v1/apiversions/testing/fixtures.go b/openstack/blockstorage/v1/apiversions/testing/fixtures.go
new file mode 100644
index 0000000..885fdf6
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/testing/fixtures.go
@@ -0,0 +1,91 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func MockListResponse(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", 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"
+						}
+					]
+				}
+			]
+		}`)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	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"
+					}
+				]
+			}
+		}`)
+	})
+}
diff --git a/openstack/blockstorage/v1/apiversions/testing/requests_test.go b/openstack/blockstorage/v1/apiversions/testing/requests_test.go
new file mode 100644
index 0000000..3103497
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/testing/requests_test.go
@@ -0,0 +1,64 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/apiversions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	apiversions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := apiversions.ExtractAPIVersions(page)
+		th.AssertNoErr(t, err)
+
+		expected := []apiversions.APIVersion{
+			{
+				ID:      "v1.0",
+				Status:  "CURRENT",
+				Updated: "2012-01-04T11:33:21Z",
+			},
+			{
+				ID:      "v2.0",
+				Status:  "CURRENT",
+				Updated: "2012-11-21T11:33:21Z",
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAPIInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	actual, err := apiversions.Get(client.ServiceClient(), "v1").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := apiversions.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/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..d1861ac
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,18 @@
+package apiversions
+
+import (
+	"net/url"
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+	return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	u, _ := url.Parse(c.ServiceURL(""))
+	u.Path = "/"
+	return u.String()
+}
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/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..cb9d0d0
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,158 @@
+package snapshots
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	VolumeID    string                 `json:"volume_id" required:"true"`
+	Description string                 `json:"display_description,omitempty"`
+	Force       bool                   `json:"force,omitempty"`
+	Metadata    map[string]interface{} `json:"metadata,omitempty"`
+	Name        string                 `json:"display_name,omitempty"`
+}
+
+// ToSnapshotCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "snapshot")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToSnapshotCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// 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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// 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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return SnapshotPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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{} `json:"metadata,omitempty"`
+}
+
+// ToSnapshotUpdateMetadataMap assembles a request body based on the contents of
+// an UpdateMetadataOpts.
+func (opts UpdateMetadataOpts) ToSnapshotUpdateMetadataMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// 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) (r UpdateMetadataResult) {
+	b, err := opts.ToSnapshotUpdateMetadataMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(updateMetadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// IDFromName is a convienience function that returns a snapshot's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractSnapshots(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "snapshot"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "snapshot"}
+	}
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..2bcb74f
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,110 @@
+package snapshots
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Snapshot contains all the information associated with an OpenStack Snapshot.
+type Snapshot struct {
+	// Currect status of the Snapshot.
+	Status string `json:"status"`
+
+	// Display name.
+	Name string `json:"display_name"`
+
+	// Instances onto which the Snapshot is attached.
+	Attachments []string `json:"attachments"`
+
+	// Logical group.
+	AvailabilityZone string `json:"availability_zone"`
+
+	// Is the Snapshot bootable?
+	Bootable string `json:"bootable"`
+
+	// Date created.
+	CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+
+	// Display description.
+	Description string `json:"display_description"`
+
+	// See VolumeType object for more information.
+	VolumeType string `json:"volume_type"`
+
+	// ID of the Snapshot from which this Snapshot was created.
+	SnapshotID string `json:"snapshot_id"`
+
+	// ID of the Volume from which this Snapshot was created.
+	VolumeID string `json:"volume_id"`
+
+	// User-defined key-value pairs.
+	Metadata map[string]string `json:"metadata"`
+
+	// Unique identifier.
+	ID string `json:"id"`
+
+	// Size of the Snapshot, in GB.
+	Size int `json:"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
+}
+
+// SnapshotPage is a pagination.Pager that is returned from a call to the List function.
+type SnapshotPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a SnapshotPage contains no Snapshots.
+func (r SnapshotPage) IsEmpty() (bool, error) {
+	volumes, err := ExtractSnapshots(r)
+	return len(volumes) == 0, err
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(r pagination.Page) ([]Snapshot, error) {
+	var s struct {
+		Snapshots []Snapshot `json:"snapshots"`
+	}
+	err := (r.(SnapshotPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Snapshot *Snapshot `json:"snapshot"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Snapshot, err
+}
diff --git a/openstack/blockstorage/v1/snapshots/testing/doc.go b/openstack/blockstorage/v1/snapshots/testing/doc.go
new file mode 100644
index 0000000..85c45f4
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/testing/doc.go
@@ -0,0 +1,2 @@
+// snapshots_v1
+package testing
diff --git a/openstack/blockstorage/v1/snapshots/testing/fixtures.go b/openstack/blockstorage/v1/snapshots/testing/fixtures.go
new file mode 100644
index 0000000..21be6f9
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/testing/fixtures.go
@@ -0,0 +1,134 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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",
+          "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+          "display_description": "Daily Backup",
+          "status": "available",
+          "size": 30,
+          "created_at": "2012-02-14T20:53:07"
+        },
+        {
+          "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+          "display_name": "snapshot-002",
+          "volume_id": "76b8950a-8594-4e5b-8dce-0dfa9c696358",
+          "display_description": "Weekly Backup",
+          "status": "available",
+          "size": 25,
+          "created_at": "2012-02-14T20:53:08"
+        }
+      ]
+    }
+    `)
+	})
+}
+
+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": {
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "display_name": "snapshot-001",
+        "display_description": "Daily backup",
+        "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+        "status": "available",
+        "size": 30,
+        "created_at": "2012-02-29T03:50:07"
+    }
+}
+      `)
+	})
+}
+
+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",
+        "display_description": "Daily backup",
+        "volume_id": "1234",
+        "status": "available",
+        "size": 30,
+        "created_at": "2012-02-29T03:50:07"
+  }
+}
+    `)
+	})
+}
+
+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/testing/requests_test.go b/openstack/blockstorage/v1/snapshots/testing/requests_test.go
new file mode 100644
index 0000000..ad7e0bb
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/testing/requests_test.go
@@ -0,0 +1,117 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	snapshots.List(client.ServiceClient(), &snapshots.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := snapshots.ExtractSnapshots(page)
+		if err != nil {
+			t.Errorf("Failed to extract snapshots: %v", err)
+			return false, err
+		}
+
+		expected := []snapshots.Snapshot{
+			{
+				ID:          "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name:        "snapshot-001",
+				VolumeID:    "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+				Status:      "available",
+				Size:        30,
+				CreatedAt:   gophercloud.JSONRFC3339MilliNoZ(time.Date(2012, 2, 14, 20, 53, 7, 0, time.UTC)),
+				Description: "Daily Backup",
+			},
+			{
+				ID:          "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name:        "snapshot-002",
+				VolumeID:    "76b8950a-8594-4e5b-8dce-0dfa9c696358",
+				Status:      "available",
+				Size:        25,
+				CreatedAt:   gophercloud.JSONRFC3339MilliNoZ(time.Date(2012, 2, 14, 20, 53, 8, 0, time.UTC)),
+				Description: "Weekly Backup",
+			},
+		}
+
+		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 := snapshots.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 := snapshots.CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
+	n, err := snapshots.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 := &snapshots.UpdateMetadataOpts{
+		Metadata: map[string]interface{}{
+			"key": "v1",
+		},
+	}
+
+	actual, err := snapshots.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 := snapshots.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls.go b/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..7780437
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls.go
@@ -0,0 +1,27 @@
+package snapshots
+
+import "github.com/gophercloud/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/util.go b/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..40fbb82
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util.go
@@ -0,0 +1,22 @@
+package snapshots
+
+import (
+	"github.com/gophercloud/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..566def5
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,167 @@
+package volumes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	Size             int               `json:"size" required:"true"`
+	AvailabilityZone string            `json:"availability_zone,omitempty"`
+	Description      string            `json:"display_description,omitempty"`
+	Metadata         map[string]string `json:"metadata,omitempty"`
+	Name             string            `json:"display_name,omitempty"`
+	SnapshotID       string            `json:"snapshot_id,omitempty"`
+	SourceVolID      string            `json:"source_volid,omitempty"`
+	ImageID          string            `json:"imageRef,omitempty"`
+	VolumeType       string            `json:"volume_type,omitempty"`
+}
+
+// ToVolumeCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "volume")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToVolumeCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// 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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// 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:"display_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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return VolumePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToVolumeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+	Name        string            `json:"display_name,omitempty"`
+	Description string            `json:"display_description,omitempty"`
+	Metadata    map[string]string `json:"metadata,omitempty"`
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "volume")
+}
+
+// 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) (r UpdateResult) {
+	b, err := opts.ToVolumeUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// IDFromName is a convienience function that returns a server's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractVolumes(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"}
+	}
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..5c954bf
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,89 @@
+package volumes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+	// Current status of the volume.
+	Status string `json:"status"`
+	// Human-readable display name for the volume.
+	Name string `json:"display_name"`
+	// Instances onto which the volume is attached.
+	Attachments []map[string]interface{} `json:"attachments"`
+	// This parameter is no longer used.
+	AvailabilityZone string `json:"availability_zone"`
+	// Indicates whether this is a bootable volume.
+	Bootable string `json:"bootable"`
+	// The date when this volume was created.
+	CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+	// Human-readable description for the volume.
+	Description string `json:"display_description"`
+	// The type of volume to create, either SATA or SSD.
+	VolumeType string `json:"volume_type"`
+	// The ID of the snapshot from which the volume was created
+	SnapshotID string `json:"snapshot_id"`
+	// The ID of another block storage volume from which the current volume was created
+	SourceVolID string `json:"source_volid"`
+	// Arbitrary key-value pairs defined by the user.
+	Metadata map[string]string `json:"metadata"`
+	// Unique identifier for the volume.
+	ID string `json:"id"`
+	// Size of the volume in GB.
+	Size int `json:"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
+}
+
+// VolumePage is a pagination.pager that is returned from a call to the List function.
+type VolumePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a VolumePage contains no Volumes.
+func (r VolumePage) IsEmpty() (bool, error) {
+	volumes, err := ExtractVolumes(r)
+	return len(volumes) == 0, err
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(r pagination.Page) ([]Volume, error) {
+	var s struct {
+		Volumes []Volume `json:"volumes"`
+	}
+	err := (r.(VolumePage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Volume *Volume `json:"volume"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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..088e43c
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/testing/doc.go
@@ -0,0 +1,9 @@
+// volumes_v1
+package testing
+
+/*
+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
+*/
diff --git a/openstack/blockstorage/v1/volumes/testing/fixtures.go b/openstack/blockstorage/v1/volumes/testing/fixtures.go
new file mode 100644
index 0000000..306901b
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/testing/fixtures.go
@@ -0,0 +1,127 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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": {
+			        "id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+			        "display_name": "vol-001",
+			        "display_description": "Another volume.",
+			        "status": "active",
+			        "size": 30,
+			        "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164",
+			        "metadata": {
+			            "contents": "junk"
+			        },
+			        "availability_zone": "us-east1",
+			        "bootable": "false",
+			        "snapshot_id": null,
+			        "attachments": [
+			            {
+			                "attachment_id": "03987cd1-0ad5-40d1-9b2a-7cc48295d4fa",
+			                "id": "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf",
+			                "volume_id": "6c80f8ac-e3e2-480c-8e6e-f1db92fe4bfe",
+			                "server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
+			                "host_name": "mitaka",
+			                "device": "/"
+			            }
+			        ],
+			        "created_at": "2012-02-14T20:53:07"
+			    }
+			}
+      `)
+	})
+}
+
+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,
+        "availability_zone": "us-east1"
+    }
+}
+      `)
+
+		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/testing/requests_test.go b/openstack/blockstorage/v1/volumes/testing/requests_test.go
new file mode 100644
index 0000000..6e2516c
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/testing/requests_test.go
@@ -0,0 +1,153 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	volumes.List(client.ServiceClient(), &volumes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := volumes.ExtractVolumes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volumes: %v", err)
+			return false, err
+		}
+
+		expected := []volumes.Volume{
+			{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-001",
+			},
+			{
+				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()
+
+	MockListResponse(t)
+
+	allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := volumes.ExtractVolumes(allPages)
+	th.AssertNoErr(t, err)
+
+	expected := []volumes.Volume{
+		{
+			ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+			Name: "vol-001",
+		},
+		{
+			ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+			Name: "vol-002",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	actual, err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &volumes.Volume{
+		Status: "active",
+		Name:   "vol-001",
+		Attachments: []map[string]interface{}{
+			{
+				"attachment_id": "03987cd1-0ad5-40d1-9b2a-7cc48295d4fa",
+				"id":            "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf",
+				"volume_id":     "6c80f8ac-e3e2-480c-8e6e-f1db92fe4bfe",
+				"server_id":     "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
+				"host_name":     "mitaka",
+				"device":        "/",
+			},
+		},
+		AvailabilityZone: "us-east1",
+		Bootable:         "false",
+		CreatedAt:        gophercloud.JSONRFC3339MilliNoZ(time.Date(2012, 2, 14, 20, 53, 07, 0, time.UTC)),
+		Description:      "Another volume.",
+		VolumeType:       "289da7f8-6440-407c-9fb4-7db01ec49164",
+		SnapshotID:       "",
+		SourceVolID:      "",
+		Metadata: map[string]string{
+			"contents": "junk",
+		},
+		ID:   "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
+		Size: 30,
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := &volumes.CreateOpts{
+		Size:             75,
+		AvailabilityZone: "us-east1",
+	}
+	n, err := volumes.Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Size, 4)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	res := volumes.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUpdateResponse(t)
+
+	options := volumes.UpdateOpts{Name: "vol-002"}
+	v, err := volumes.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/urls.go b/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..8a00f97
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/gophercloud/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/util.go b/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..e86c1b4
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util.go
@@ -0,0 +1,22 @@
+package volumes
+
+import (
+	"github.com/gophercloud/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/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..b95c09a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,59 @@
+package volumetypes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	// See VolumeType.
+	ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"`
+	// See VolumeType.
+	Name string `json:"name,omitempty"`
+}
+
+// ToVolumeTypeCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVolumeTypeCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "volume_type")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToVolumeTypeCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// 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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return VolumeTypePage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..2c31238
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,61 @@
+package volumetypes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// VolumeType contains all information associated with an OpenStack Volume Type.
+type VolumeType struct {
+	ExtraSpecs map[string]interface{} `json:"extra_specs"` // user-defined metadata
+	ID         string                 `json:"id"`          // unique identifier
+	Name       string                 `json:"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
+}
+
+// VolumeTypePage is a pagination.Pager that is returned from a call to the List function.
+type VolumeTypePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a VolumeTypePage contains no Volume Types.
+func (r VolumeTypePage) IsEmpty() (bool, error) {
+	volumeTypes, err := ExtractVolumeTypes(r)
+	return len(volumeTypes) == 0, err
+}
+
+// ExtractVolumeTypes extracts and returns Volume Types.
+func ExtractVolumeTypes(r pagination.Page) ([]VolumeType, error) {
+	var s struct {
+		VolumeTypes []VolumeType `json:"volume_types"`
+	}
+	err := (r.(VolumeTypePage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		VolumeType *VolumeType `json:"volume_type"`
+	}
+	err := r.ExtractInto(&s)
+	return s.VolumeType, err
+}
diff --git a/openstack/blockstorage/v1/volumetypes/testing/doc.go b/openstack/blockstorage/v1/volumetypes/testing/doc.go
new file mode 100644
index 0000000..73834ed
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/testing/doc.go
@@ -0,0 +1,2 @@
+// volumetypes_v1
+package testing
diff --git a/openstack/blockstorage/v1/volumetypes/testing/fixtures.go b/openstack/blockstorage/v1/volumetypes/testing/fixtures.go
new file mode 100644
index 0000000..0e2715a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/testing/fixtures.go
@@ -0,0 +1,60 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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/testing/requests_test.go b/openstack/blockstorage/v1/volumetypes/testing/requests_test.go
new file mode 100644
index 0000000..4244615
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/testing/requests_test.go
@@ -0,0 +1,119 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	volumetypes.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := volumetypes.ExtractVolumeTypes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volume types: %v", err)
+			return false, err
+		}
+
+		expected := []volumetypes.VolumeType{
+			{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-type-001",
+				ExtraSpecs: map[string]interface{}{
+					"capabilities": "gpu",
+				},
+			},
+			{
+				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 := volumetypes.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 := &volumetypes.CreateOpts{Name: "vol-type-001"}
+	n, err := volumetypes.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 := volumetypes.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls.go b/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..822c7dd
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls.go
@@ -0,0 +1,19 @@
+package volumetypes
+
+import "github.com/gophercloud/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/v2/volumes/doc.go b/openstack/blockstorage/v2/volumes/doc.go
new file mode 100644
index 0000000..307b8b1
--- /dev/null
+++ b/openstack/blockstorage/v2/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/v2/volumes/requests.go b/openstack/blockstorage/v2/volumes/requests.go
new file mode 100644
index 0000000..18c9cb2
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/requests.go
@@ -0,0 +1,182 @@
+package volumes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	// The size of the volume, in GB
+	Size int `json:"size" required:"true"`
+	// The availability zone
+	AvailabilityZone string `json:"availability_zone,omitempty"`
+	// ConsistencyGroupID is the ID of a consistency group
+	ConsistencyGroupID string `json:"consistencygroup_id,omitempty"`
+	// The volume description
+	Description string `json:"description,omitempty"`
+	// One or more metadata key and value pairs to associate with the volume
+	Metadata map[string]string `json:"metadata,omitempty"`
+	// The volume name
+	Name string `json:"name,omitempty"`
+	// the ID of the existing volume snapshot
+	SnapshotID string `json:"snapshot_id,omitempty"`
+	// SourceReplica is a UUID of an existing volume to replicate with
+	SourceReplica string `json:"source_replica,omitempty"`
+	// the ID of the existing volume
+	SourceVolID string `json:"source_volid,omitempty"`
+	// The ID of the image from which you want to create the volume.
+	// Required to create a bootable volume.
+	ImageID string `json:"imageRef,omitempty"`
+	// The associated volume type
+	VolumeType string `json:"volume_type,omitempty"`
+}
+
+// ToVolumeCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "volume")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToVolumeCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// 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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// 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)
+	return q.String(), err
+}
+
+// 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
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return VolumePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToVolumeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+	Name        string            `json:"name,omitempty"`
+	Description string            `json:"description,omitempty"`
+	Metadata    map[string]string `json:"metadata,omitempty"`
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "volume")
+}
+
+// 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) (r UpdateResult) {
+	b, err := opts.ToVolumeUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// IDFromName is a convienience function that returns a server's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractVolumes(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"}
+	}
+}
diff --git a/openstack/blockstorage/v2/volumes/results.go b/openstack/blockstorage/v2/volumes/results.go
new file mode 100644
index 0000000..674ec34
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/results.go
@@ -0,0 +1,154 @@
+package volumes
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type Attachment struct {
+	AttachedAt   time.Time `json:"-"`
+	AttachmentID string    `json:"attachment_id"`
+	Device       string    `json:"device"`
+	HostName     string    `json:"host_name"`
+	ID           string    `json:"id"`
+	ServerID     string    `json:"server_id"`
+	VolumeID     string    `json:"volume_id"`
+}
+
+func (r *Attachment) UnmarshalJSON(b []byte) error {
+	type tmp Attachment
+	var s struct {
+		tmp
+		AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attached_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Attachment(s.tmp)
+
+	r.AttachedAt = time.Time(s.AttachedAt)
+
+	return err
+}
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+	// Unique identifier for the volume.
+	ID string `json:"id"`
+	// Current status of the volume.
+	Status string `json:"status"`
+	// Size of the volume in GB.
+	Size int `json:"size"`
+	// AvailabilityZone is which availability zone the volume is in.
+	AvailabilityZone string `json:"availability_zone"`
+	// The date when this volume was created.
+	CreatedAt time.Time `json:"-"`
+	// The date when this volume was last updated
+	UpdatedAt time.Time `json:"-"`
+	// Instances onto which the volume is attached.
+	Attachments []Attachment `json:"attachments"`
+	// Human-readable display name for the volume.
+	Name string `json:"name"`
+	// Human-readable description for the volume.
+	Description string `json:"description"`
+	// The type of volume to create, either SATA or SSD.
+	VolumeType string `json:"volume_type"`
+	// The ID of the snapshot from which the volume was created
+	SnapshotID string `json:"snapshot_id"`
+	// The ID of another block storage volume from which the current volume was created
+	SourceVolID string `json:"source_volid"`
+	// Arbitrary key-value pairs defined by the user.
+	Metadata map[string]string `json:"metadata"`
+	// UserID is the id of the user who created the volume.
+	UserID string `json:"user_id"`
+	// Indicates whether this is a bootable volume.
+	Bootable string `json:"bootable"`
+	// Encrypted denotes if the volume is encrypted.
+	Encrypted bool `json:"encrypted"`
+	// ReplicationStatus is the status of replication.
+	ReplicationStatus string `json:"replication_status"`
+	// ConsistencyGroupID is the consistency group ID.
+	ConsistencyGroupID string `json:"consistencygroup_id"`
+	// Multiattach denotes if the volume is multi-attach capable.
+	Multiattach bool `json:"multiattach"`
+}
+
+func (r *Volume) UnmarshalJSON(b []byte) error {
+	type tmp Volume
+	var s struct {
+		tmp
+		CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+		UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Volume(s.tmp)
+
+	r.CreatedAt = time.Time(s.CreatedAt)
+	r.UpdatedAt = time.Time(s.UpdatedAt)
+
+	return err
+}
+
+// VolumePage is a pagination.pager that is returned from a call to the List function.
+type VolumePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volumes.
+func (r VolumePage) IsEmpty() (bool, error) {
+	volumes, err := ExtractVolumes(r)
+	return len(volumes) == 0, err
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(r pagination.Page) ([]Volume, error) {
+	var s []Volume
+	err := ExtractVolumesInto(r, &s)
+	return s, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (*Volume, error) {
+	var s Volume
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+func (r commonResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "volume")
+}
+
+func ExtractVolumesInto(r pagination.Page, v interface{}) error {
+	return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes")
+}
+
+// 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
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult contains the response body and error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/blockstorage/v2/volumes/testing/doc.go b/openstack/blockstorage/v2/volumes/testing/doc.go
new file mode 100644
index 0000000..aa8351a
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/testing/doc.go
@@ -0,0 +1,2 @@
+// volumes_v2
+package testing
diff --git a/openstack/blockstorage/v2/volumes/testing/fixtures.go b/openstack/blockstorage/v2/volumes/testing/fixtures.go
new file mode 100644
index 0000000..44d2ca3
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/testing/fixtures.go
@@ -0,0 +1,203 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/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")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+  {
+  "volumes": [
+    {
+      "volume_type": "lvmdriver-1",
+      "created_at": "2015-09-17T03:35:03.000000",
+      "bootable": "false",
+      "name": "vol-001",
+      "os-vol-mig-status-attr:name_id": null,
+      "consistencygroup_id": null,
+      "source_volid": null,
+      "os-volume-replication:driver_data": null,
+      "multiattach": false,
+      "snapshot_id": null,
+      "replication_status": "disabled",
+      "os-volume-replication:extended_status": null,
+      "encrypted": false,
+      "os-vol-host-attr:host": null,
+      "availability_zone": "nova",
+      "attachments": [{
+        "server_id": "83ec2e3b-4321-422b-8706-a84185f52a0a",
+        "attachment_id": "05551600-a936-4d4a-ba42-79a037c1-c91a",
+        "attached_at": "2016-08-06T14:48:20.000000",
+        "host_name": "foobar",
+        "volume_id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75",
+        "device": "/dev/vdc",
+        "id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75"
+      }],
+      "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+      "size": 75,
+      "user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
+      "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459",
+      "os-vol-mig-status-attr:migstat": null,
+      "metadata": {"foo": "bar"},
+      "status": "available",
+      "description": null
+    },
+    {
+      "volume_type": "lvmdriver-1",
+      "created_at": "2015-09-17T03:32:29.000000",
+      "bootable": "false",
+      "name": "vol-002",
+      "os-vol-mig-status-attr:name_id": null,
+      "consistencygroup_id": null,
+      "source_volid": null,
+      "os-volume-replication:driver_data": null,
+      "multiattach": false,
+      "snapshot_id": null,
+      "replication_status": "disabled",
+      "os-volume-replication:extended_status": null,
+      "encrypted": false,
+      "os-vol-host-attr:host": null,
+      "availability_zone": "nova",
+      "attachments": [],
+      "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+      "size": 75,
+      "user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
+      "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459",
+      "os-vol-mig-status-attr:migstat": null,
+      "metadata": {},
+      "status": "available",
+      "description": null
+    }
+  ]
+}
+  `)
+	})
+}
+
+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": {
+    "volume_type": "lvmdriver-1",
+    "created_at": "2015-09-17T03:32:29.000000",
+    "bootable": "false",
+    "name": "vol-001",
+    "os-vol-mig-status-attr:name_id": null,
+    "consistencygroup_id": null,
+    "source_volid": null,
+    "os-volume-replication:driver_data": null,
+    "multiattach": false,
+    "snapshot_id": null,
+    "replication_status": "disabled",
+    "os-volume-replication:extended_status": null,
+    "encrypted": false,
+    "os-vol-host-attr:host": null,
+    "availability_zone": "nova",
+    "attachments": [{
+      "server_id": "83ec2e3b-4321-422b-8706-a84185f52a0a",
+      "attachment_id": "05551600-a936-4d4a-ba42-79a037c1-c91a",
+      "attached_at": "2016-08-06T14:48:20.000000",
+      "host_name": "foobar",
+      "volume_id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75",
+      "device": "/dev/vdc",
+      "id": "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75"
+    }],
+    "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+    "size": 75,
+    "user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
+    "os-vol-tenant-attr:tenant_id": "304dc00909ac4d0da6c62d816bcb3459",
+    "os-vol-mig-status-attr:migstat": null,
+    "metadata": {},
+    "status": "available",
+    "description": null
+  }
+}
+      `)
+	})
+}
+
+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": {
+    	"name": "vol-001",
+        "size": 75
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+  "volume": {
+    "size": 75,
+    "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+    "metadata": {},
+    "created_at": "2015-09-17T03:32:29.044216",
+    "encrypted": false,
+    "bootable": "false",
+    "availability_zone": "nova",
+    "attachments": [],
+    "user_id": "ff1ce52c03ab433aaba9108c2e3ef541",
+    "status": "creating",
+    "description": null,
+    "volume_type": "lvmdriver-1",
+    "name": "vol-001",
+    "replication_status": "disabled",
+    "consistencygroup_id": null,
+    "source_volid": null,
+    "snapshot_id": null,
+    "multiattach": false
+  }
+}
+    `)
+	})
+}
+
+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.StatusAccepted)
+	})
+}
+
+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": {
+    "name": "vol-002"
+  }
+}
+        `)
+	})
+}
diff --git a/openstack/blockstorage/v2/volumes/testing/requests_test.go b/openstack/blockstorage/v2/volumes/testing/requests_test.go
new file mode 100644
index 0000000..0a18544
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/testing/requests_test.go
@@ -0,0 +1,257 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumetenants"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	count := 0
+
+	volumes.List(client.ServiceClient(), &volumes.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := volumes.ExtractVolumes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volumes: %v", err)
+			return false, err
+		}
+
+		expected := []volumes.Volume{
+			{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-001",
+				Attachments: []volumes.Attachment{{
+					ServerID:     "83ec2e3b-4321-422b-8706-a84185f52a0a",
+					AttachmentID: "05551600-a936-4d4a-ba42-79a037c1-c91a",
+					AttachedAt:   time.Date(2016, 8, 6, 14, 48, 20, 0, time.UTC),
+					HostName:     "foobar",
+					VolumeID:     "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75",
+					Device:       "/dev/vdc",
+					ID:           "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75",
+				}},
+				AvailabilityZone:   "nova",
+				Bootable:           "false",
+				ConsistencyGroupID: "",
+				CreatedAt:          time.Date(2015, 9, 17, 3, 35, 3, 0, time.UTC),
+				Description:        "",
+				Encrypted:          false,
+				Metadata:           map[string]string{"foo": "bar"},
+				Multiattach:        false,
+				//TenantID:                  "304dc00909ac4d0da6c62d816bcb3459",
+				//ReplicationDriverData:     "",
+				//ReplicationExtendedStatus: "",
+				ReplicationStatus: "disabled",
+				Size:              75,
+				SnapshotID:        "",
+				SourceVolID:       "",
+				Status:            "available",
+				UserID:            "ff1ce52c03ab433aaba9108c2e3ef541",
+				VolumeType:        "lvmdriver-1",
+			},
+			{
+				ID:                 "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name:               "vol-002",
+				Attachments:        []volumes.Attachment{},
+				AvailabilityZone:   "nova",
+				Bootable:           "false",
+				ConsistencyGroupID: "",
+				CreatedAt:          time.Date(2015, 9, 17, 3, 32, 29, 0, time.UTC),
+				Description:        "",
+				Encrypted:          false,
+				Metadata:           map[string]string{},
+				Multiattach:        false,
+				//TenantID:                  "304dc00909ac4d0da6c62d816bcb3459",
+				//ReplicationDriverData:     "",
+				//ReplicationExtendedStatus: "",
+				ReplicationStatus: "disabled",
+				Size:              75,
+				SnapshotID:        "",
+				SourceVolID:       "",
+				Status:            "available",
+				UserID:            "ff1ce52c03ab433aaba9108c2e3ef541",
+				VolumeType:        "lvmdriver-1",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestListAllWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	type VolumeWithExt struct {
+		volumes.Volume
+		volumetenants.VolumeExt
+	}
+
+	allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+
+	var actual []VolumeWithExt
+	err = volumes.ExtractVolumesInto(allPages, &actual)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 2, len(actual))
+	th.AssertEquals(t, "304dc00909ac4d0da6c62d816bcb3459", actual[0].TenantID)
+}
+
+func TestListAll(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	allPages, err := volumes.List(client.ServiceClient(), &volumes.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := volumes.ExtractVolumes(allPages)
+	th.AssertNoErr(t, err)
+
+	expected := []volumes.Volume{
+		{
+			ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+			Name: "vol-001",
+			Attachments: []volumes.Attachment{{
+				ServerID:     "83ec2e3b-4321-422b-8706-a84185f52a0a",
+				AttachmentID: "05551600-a936-4d4a-ba42-79a037c1-c91a",
+				AttachedAt:   time.Date(2016, 8, 6, 14, 48, 20, 0, time.UTC),
+				HostName:     "foobar",
+				VolumeID:     "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75",
+				Device:       "/dev/vdc",
+				ID:           "d6cacb1a-8b59-4c88-ad90-d70ebb82bb75",
+			}},
+			AvailabilityZone:   "nova",
+			Bootable:           "false",
+			ConsistencyGroupID: "",
+			CreatedAt:          time.Date(2015, 9, 17, 3, 35, 3, 0, time.UTC),
+			Description:        "",
+			Encrypted:          false,
+			Metadata:           map[string]string{"foo": "bar"},
+			Multiattach:        false,
+			//TenantID:                  "304dc00909ac4d0da6c62d816bcb3459",
+			//ReplicationDriverData:     "",
+			//ReplicationExtendedStatus: "",
+			ReplicationStatus: "disabled",
+			Size:              75,
+			SnapshotID:        "",
+			SourceVolID:       "",
+			Status:            "available",
+			UserID:            "ff1ce52c03ab433aaba9108c2e3ef541",
+			VolumeType:        "lvmdriver-1",
+		},
+		{
+			ID:                 "96c3bda7-c82a-4f50-be73-ca7621794835",
+			Name:               "vol-002",
+			Attachments:        []volumes.Attachment{},
+			AvailabilityZone:   "nova",
+			Bootable:           "false",
+			ConsistencyGroupID: "",
+			CreatedAt:          time.Date(2015, 9, 17, 3, 32, 29, 0, time.UTC),
+			Description:        "",
+			Encrypted:          false,
+			Metadata:           map[string]string{},
+			Multiattach:        false,
+			//TenantID:                  "304dc00909ac4d0da6c62d816bcb3459",
+			//ReplicationDriverData:     "",
+			//ReplicationExtendedStatus: "",
+			ReplicationStatus: "disabled",
+			Size:              75,
+			SnapshotID:        "",
+			SourceVolID:       "",
+			Status:            "available",
+			UserID:            "ff1ce52c03ab433aaba9108c2e3ef541",
+			VolumeType:        "lvmdriver-1",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	v, err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "vol-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := &volumes.CreateOpts{Size: 75, Name: "vol-001"}
+	n, err := volumes.Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Size, 75)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	res := volumes.Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUpdateResponse(t)
+
+	options := volumes.UpdateOpts{Name: "vol-002"}
+	v, err := volumes.Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "vol-002", v.Name)
+}
+
+func TestGetWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	var s struct {
+		volumes.Volume
+		volumetenants.VolumeExt
+	}
+	err := volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractInto(&s)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "304dc00909ac4d0da6c62d816bcb3459", s.TenantID)
+
+	err = volumes.Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").ExtractInto(s)
+	if err == nil {
+		t.Errorf("Expected error when providing non-pointer struct")
+	}
+}
diff --git a/openstack/blockstorage/v2/volumes/urls.go b/openstack/blockstorage/v2/volumes/urls.go
new file mode 100644
index 0000000..1707249
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/gophercloud/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("volumes")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("volumes", "detail")
+}
+
+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/v2/volumes/util.go b/openstack/blockstorage/v2/volumes/util.go
new file mode 100644
index 0000000..e86c1b4
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/util.go
@@ -0,0 +1,22 @@
+package volumes
+
+import (
+	"github.com/gophercloud/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/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/requests.go b/openstack/cdn/v1/base/requests.go
new file mode 100644
index 0000000..34d3b72
--- /dev/null
+++ b/openstack/cdn/v1/base/requests.go
@@ -0,0 +1,19 @@
+package base
+
+import "github.com/gophercloud/gophercloud"
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) (r GetResult) {
+	_, r.Err = c.Get(getURL(c), &r.Body, nil)
+	return
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) (r PingResult) {
+	_, r.Err = c.Get(pingURL(c), nil, &gophercloud.RequestOpts{
+		OkCodes:     []int{204},
+		MoreHeaders: map[string]string{"Accept": ""},
+	})
+	return
+}
diff --git a/openstack/cdn/v1/base/results.go b/openstack/cdn/v1/base/results.go
new file mode 100644
index 0000000..2dfde7d
--- /dev/null
+++ b/openstack/cdn/v1/base/results.go
@@ -0,0 +1,23 @@
+package base
+
+import "github.com/gophercloud/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) {
+	var s HomeDocument
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+// PingResult represents the result of a Ping operation.
+type PingResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/base/testing/doc.go b/openstack/cdn/v1/base/testing/doc.go
new file mode 100644
index 0000000..891c69a
--- /dev/null
+++ b/openstack/cdn/v1/base/testing/doc.go
@@ -0,0 +1,2 @@
+// cdn_base_v1
+package testing
diff --git a/openstack/cdn/v1/base/testing/fixtures.go b/openstack/cdn/v1/base/testing/fixtures.go
new file mode 100644
index 0000000..f1f4ac0
--- /dev/null
+++ b/openstack/cdn/v1/base/testing/fixtures.go
@@ -0,0 +1,53 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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/testing/requests_test.go b/openstack/cdn/v1/base/testing/requests_test.go
new file mode 100644
index 0000000..cd1209b
--- /dev/null
+++ b/openstack/cdn/v1/base/testing/requests_test.go
@@ -0,0 +1,44 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/cdn/v1/base"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := base.Get(fake.ServiceClient()).Extract()
+	th.CheckNoErr(t, err)
+
+	expected := base.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 := base.Ping(fake.ServiceClient()).ExtractErr()
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/base/urls.go b/openstack/cdn/v1/base/urls.go
new file mode 100644
index 0000000..07d892b
--- /dev/null
+++ b/openstack/cdn/v1/base/urls.go
@@ -0,0 +1,11 @@
+package base
+
+import "github.com/gophercloud/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/requests.go b/openstack/cdn/v1/flavors/requests.go
new file mode 100644
index 0000000..1977fe3
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests.go
@@ -0,0 +1,19 @@
+package flavors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/cdn/v1/flavors/results.go b/openstack/cdn/v1/flavors/results.go
new file mode 100644
index 0000000..02c2851
--- /dev/null
+++ b/openstack/cdn/v1/flavors/results.go
@@ -0,0 +1,60 @@
+package flavors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"provider"`
+	// Specifies a list with an href where rel is provider_url.
+	Links []gophercloud.Link `json:"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 `json:"id"`
+	// Specifies the list of providers mapped to this flavor.
+	Providers []Provider `json:"providers"`
+	// Specifies the self-navigating JSON document paths.
+	Links []gophercloud.Link `json:"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)
+	return len(flavors) == 0, err
+}
+
+// ExtractFlavors extracts and returns Flavors. It is used while iterating over
+// a flavors.List call.
+func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
+	var s struct {
+		Flavors []Flavor `json:"flavors"`
+	}
+	err := (r.(FlavorPage)).ExtractInto(&s)
+	return s.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) {
+	var s *Flavor
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/cdn/v1/flavors/testing/doc.go b/openstack/cdn/v1/flavors/testing/doc.go
new file mode 100644
index 0000000..567b67e
--- /dev/null
+++ b/openstack/cdn/v1/flavors/testing/doc.go
@@ -0,0 +1,2 @@
+// cdn_flavors_v1
+package testing
diff --git a/openstack/cdn/v1/flavors/testing/fixtures.go b/openstack/cdn/v1/flavors/testing/fixtures.go
new file mode 100644
index 0000000..ed97247
--- /dev/null
+++ b/openstack/cdn/v1/flavors/testing/fixtures.go
@@ -0,0 +1,82 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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/testing/requests_test.go b/openstack/cdn/v1/flavors/testing/requests_test.go
new file mode 100644
index 0000000..bc4b1a5
--- /dev/null
+++ b/openstack/cdn/v1/flavors/testing/requests_test.go
@@ -0,0 +1,90 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/cdn/v1/flavors"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleListCDNFlavorsSuccessfully(t)
+
+	count := 0
+
+	err := flavors.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := flavors.ExtractFlavors(page)
+		if err != nil {
+			t.Errorf("Failed to extract flavors: %v", err)
+			return false, err
+		}
+
+		expected := []flavors.Flavor{
+			{
+				ID: "europe",
+				Providers: []flavors.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 := &flavors.Flavor{
+		ID: "asia",
+		Providers: []flavors.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 := flavors.Get(fake.ServiceClient(), "asia").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/cdn/v1/flavors/urls.go b/openstack/cdn/v1/flavors/urls.go
new file mode 100644
index 0000000..a8540a2
--- /dev/null
+++ b/openstack/cdn/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/gophercloud/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/requests.go b/openstack/cdn/v1/serviceassets/requests.go
new file mode 100644
index 0000000..80c908f
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests.go
@@ -0,0 +1,51 @@
+package serviceassets
+
+import (
+	"strings"
+
+	"github.com/gophercloud/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)
+	return q.String(), err
+}
+
+// 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) (r DeleteResult) {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = deleteURL(c, idOrURL)
+	}
+	if opts != nil {
+		q, err := opts.ToCDNAssetDeleteParams()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		url += q
+	}
+	_, r.Err = c.Delete(url, nil)
+	return
+}
diff --git a/openstack/cdn/v1/serviceassets/results.go b/openstack/cdn/v1/serviceassets/results.go
new file mode 100644
index 0000000..b6114c6
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/results.go
@@ -0,0 +1,8 @@
+package serviceassets
+
+import "github.com/gophercloud/gophercloud"
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/serviceassets/testing/doc.go b/openstack/cdn/v1/serviceassets/testing/doc.go
new file mode 100644
index 0000000..1adb681
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/testing/doc.go
@@ -0,0 +1,2 @@
+// cdn_serviceassets_v1
+package testing
diff --git a/openstack/cdn/v1/serviceassets/testing/fixtures.go b/openstack/cdn/v1/serviceassets/testing/fixtures.go
new file mode 100644
index 0000000..3172d30
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/testing/fixtures.go
@@ -0,0 +1,19 @@
+package testing
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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/testing/requests_test.go b/openstack/cdn/v1/serviceassets/testing/requests_test.go
new file mode 100644
index 0000000..ff2073b
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/testing/requests_test.go
@@ -0,0 +1,19 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/cdn/v1/serviceassets"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleDeleteCDNAssetSuccessfully(t)
+
+	err := serviceassets.Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/serviceassets/urls.go b/openstack/cdn/v1/serviceassets/urls.go
new file mode 100644
index 0000000..ce17418
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/urls.go
@@ -0,0 +1,7 @@
+package serviceassets
+
+import "github.com/gophercloud/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/requests.go b/openstack/cdn/v1/services/requests.go
new file mode 100644
index 0000000..4c0c626
--- /dev/null
+++ b/openstack/cdn/v1/services/requests.go
@@ -0,0 +1,285 @@
+package services
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		p := ServicePage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	})
+}
+
+// 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 {
+	// Specifies the name of the service. The minimum length for name is
+	// 3. The maximum length is 256.
+	Name string `json:"name" required:"true"`
+	// Specifies a list of domains used by users to access their website.
+	Domains []Domain `json:"domains" required:"true"`
+	// Specifies a list of origin domains or IP addresses where the
+	// original assets are stored.
+	Origins []Origin `json:"origins" required:"true"`
+	// 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 `json:"flavor_id" required:"true"`
+	// Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control.
+	Caching []CacheRule `json:"caching,omitempty"`
+	// Specifies the restrictions that define who can access assets (content from the CDN cache).
+	Restrictions []Restriction `json:"restrictions,omitempty"`
+}
+
+// ToCDNServiceCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToCDNServiceCreateMap()
+	if err != nil {
+		r.Err = err
+		return r
+	}
+	resp, err := c.Post(createURL(c), &b, nil, nil)
+	r.Header = resp.Header
+	r.Err = err
+	return
+}
+
+// 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) (r GetResult) {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = getURL(c, idOrURL)
+	}
+	_, r.Err = c.Get(url, &r.Body, nil)
+	return
+}
+
+// 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 (opts Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  opts.Value.renderRootOr(func(p Path) string { return p.renderIndex(opts.Index) }),
+		"value": opts.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 (opts Removal) ToCDNServiceUpdateMap() map[string]interface{} {
+	b := map[string]interface{}{"op": "remove"}
+	if opts.All {
+		b["path"] = opts.Path.renderRoot()
+	} else {
+		b["path"] = opts.Path.renderIndex(opts.Index)
+	}
+	return b
+}
+
+// UpdateOpts is a slice of Patches used to update a CDN service
+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) (r UpdateResult) {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = updateURL(c, idOrURL)
+	}
+
+	b := make([]map[string]interface{}, len(opts))
+	for i, patch := range opts {
+		b[i] = patch.ToCDNServiceUpdateMap()
+	}
+
+	resp, err := c.Request("PATCH", url, &gophercloud.RequestOpts{
+		JSONBody: &b,
+		OkCodes:  []int{202},
+	})
+	r.Header = resp.Header
+	r.Err = err
+	return
+}
+
+// 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) (r DeleteResult) {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = deleteURL(c, idOrURL)
+	}
+	_, r.Err = c.Delete(url, nil)
+	return
+}
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
new file mode 100644
index 0000000..f9a1caa
--- /dev/null
+++ b/openstack/cdn/v1/services/results.go
@@ -0,0 +1,304 @@
+package services
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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 `json:"domain" required:"true"`
+	// Specifies the protocol used to access the assets on this domain. Only "http"
+	// or "https" are currently allowed. The default is "http".
+	Protocol string `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 `json:"name" required:"true"`
+	// Specifies the request URL this rule should match for this origin to be used. Regex is supported.
+	RequestURL string `json:"request_url" required:"true"`
+}
+
+// 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 `json:"origin" required:"true"`
+	// Specifies the port used to access the origin. The default is port 80.
+	Port int `json:"port,omitempty"`
+	// Specifies whether or not to use HTTPS to access the origin. The default
+	// is false.
+	SSL bool `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 `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 `json:"name" required:"true"`
+	// Specifies the request URL this rule should match for this TTL to be used. Regex is supported.
+	RequestURL string `json:"request_url" required:"true"`
+}
+
+// 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 `json:"name" required:"true"`
+	// Specifies the TTL to apply.
+	TTL int `json:"ttl,omitempty"`
+	// Specifies a collection of rules that determine if this TTL should be applied to an asset.
+	Rules []TTLRule `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 `json:"name" required:"true"`
+	// Specifies the http host that requests must come from.
+	Referrer string `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 `json:"name" required:"true"`
+	// Specifies a collection of rules that determine if this TTL should be applied to an asset.
+	Rules []RestrictionRule `json:"rules,omitempty"`
+}
+
+// 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 `json:"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 `json:"id"`
+	// Specifies the name of the service.
+	Name string `json:"name"`
+	// Specifies a list of domains used by users to access their website.
+	Domains []Domain `json:"domains"`
+	// Specifies a list of origin domains or IP addresses where the original assets are stored.
+	Origins []Origin `json:"origins"`
+	// Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control.
+	Caching []CacheRule `json:"caching"`
+	// Specifies the restrictions that define who can access assets (content from the CDN cache).
+	Restrictions []Restriction `json:"restrictions"`
+	// Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors.
+	FlavorID string `json:"flavor_id"`
+	// Specifies the current status of the service.
+	Status string `json:"status"`
+	// Specifies the list of errors that occurred during the previous service action.
+	Errors []Error `json:"errors"`
+	// Specifies the self-navigating JSON document paths.
+	Links []gophercloud.Link `json:"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)
+	return len(services) == 0, err
+}
+
+// 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(r pagination.Page) ([]Service, error) {
+	var s struct {
+		Services []Service `json:"services"`
+	}
+	err := (r.(ServicePage)).ExtractInto(&s)
+	return s.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) {
+	var s Service
+	err := r.ExtractInto(&s)
+	return &s, 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/testing/doc.go b/openstack/cdn/v1/services/testing/doc.go
new file mode 100644
index 0000000..c72e391
--- /dev/null
+++ b/openstack/cdn/v1/services/testing/doc.go
@@ -0,0 +1,2 @@
+// cdn_services_v1
+package testing
diff --git a/openstack/cdn/v1/services/testing/fixtures.go b/openstack/cdn/v1/services/testing/fixtures.go
new file mode 100644
index 0000000..d4093e0
--- /dev/null
+++ b/openstack/cdn/v1/services/testing/fixtures.go
@@ -0,0 +1,372 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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/testing/requests_test.go b/openstack/cdn/v1/services/testing/requests_test.go
new file mode 100644
index 0000000..0abc98e
--- /dev/null
+++ b/openstack/cdn/v1/services/testing/requests_test.go
@@ -0,0 +1,359 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/cdn/v1/services"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleListCDNServiceSuccessfully(t)
+
+	count := 0
+
+	err := services.List(fake.ServiceClient(), &services.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := services.ExtractServices(page)
+		if err != nil {
+			t.Errorf("Failed to extract services: %v", err)
+			return false, err
+		}
+
+		expected := []services.Service{
+			{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Name: "mywebsite.com",
+				Domains: []services.Domain{
+					{
+						Domain: "www.mywebsite.com",
+					},
+				},
+				Origins: []services.Origin{
+					{
+						Origin: "mywebsite.com",
+						Port:   80,
+						SSL:    false,
+					},
+				},
+				Caching: []services.CacheRule{
+					{
+						Name: "default",
+						TTL:  3600,
+					},
+					{
+						Name: "home",
+						TTL:  17200,
+						Rules: []services.TTLRule{
+							{
+								Name:       "index",
+								RequestURL: "/index.htm",
+							},
+						},
+					},
+					{
+						Name: "images",
+						TTL:  12800,
+						Rules: []services.TTLRule{
+							{
+								Name:       "images",
+								RequestURL: "*.png",
+							},
+						},
+					},
+				},
+				Restrictions: []services.Restriction{
+					{
+						Name: "website only",
+						Rules: []services.RestrictionRule{
+							{
+								Name:     "mywebsite.com",
+								Referrer: "www.mywebsite.com",
+							},
+						},
+					},
+				},
+				FlavorID: "asia",
+				Status:   "deployed",
+				Errors:   []services.Error{},
+				Links: []gophercloud.Link{
+					{
+						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: []services.Domain{
+					{
+						Domain: "www.myothersite.com",
+					},
+				},
+				Origins: []services.Origin{
+					{
+						Origin: "44.33.22.11",
+						Port:   80,
+						SSL:    false,
+					},
+					{
+						Origin: "77.66.55.44",
+						Port:   80,
+						SSL:    false,
+						Rules: []services.OriginRule{
+							{
+								Name:       "videos",
+								RequestURL: "^/videos/*.m3u",
+							},
+						},
+					},
+				},
+				Caching: []services.CacheRule{
+					{
+						Name: "default",
+						TTL:  3600,
+					},
+				},
+				Restrictions: []services.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 := services.CreateOpts{
+		Name: "mywebsite.com",
+		Domains: []services.Domain{
+			{
+				Domain: "www.mywebsite.com",
+			},
+			{
+				Domain: "blog.mywebsite.com",
+			},
+		},
+		Origins: []services.Origin{
+			{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Restrictions: []services.Restriction{
+			{
+				Name: "website only",
+				Rules: []services.RestrictionRule{
+					{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		Caching: []services.CacheRule{
+			{
+				Name: "default",
+				TTL:  3600,
+			},
+		},
+		FlavorID: "cdn",
+	}
+
+	expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	actual, err := services.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 := &services.Service{
+		ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+		Name: "mywebsite.com",
+		Domains: []services.Domain{
+			{
+				Domain:   "www.mywebsite.com",
+				Protocol: "http",
+			},
+		},
+		Origins: []services.Origin{
+			{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Caching: []services.CacheRule{
+			{
+				Name: "default",
+				TTL:  3600,
+			},
+			{
+				Name: "home",
+				TTL:  17200,
+				Rules: []services.TTLRule{
+					{
+						Name:       "index",
+						RequestURL: "/index.htm",
+					},
+				},
+			},
+			{
+				Name: "images",
+				TTL:  12800,
+				Rules: []services.TTLRule{
+					{
+						Name:       "images",
+						RequestURL: "*.png",
+					},
+				},
+			},
+		},
+		Restrictions: []services.Restriction{
+			{
+				Name: "website only",
+				Rules: []services.RestrictionRule{
+					{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		FlavorID: "cdn",
+		Status:   "deployed",
+		Errors:   []services.Error{},
+		Links: []gophercloud.Link{
+			{
+				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",
+			},
+		},
+	}
+
+	actual, err := services.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 := services.UpdateOpts{
+		// Append a single Domain
+		services.Append{Value: services.Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		services.Insertion{
+			Index: 4,
+			Value: services.Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		services.Append{
+			Value: services.DomainList{
+				{Domain: "bulkadded1.mocksite4.com"},
+				{Domain: "bulkadded2.mocksite4.com"},
+			},
+		},
+		// Replace a single Origin
+		services.Replacement{
+			Index: 2,
+			Value: services.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		services.Replacement{
+			Index: 0, // Ignored
+			Value: services.OriginList{
+				{Origin: "44.33.22.11", Port: 80, SSL: false},
+				{Origin: "55.44.33.22", Port: 443, SSL: true},
+			},
+		},
+		// Remove a single CacheRule
+		services.Removal{
+			Index: 8,
+			Path:  services.PathCaching,
+		},
+		// Bulk removal
+		services.Removal{
+			All:  true,
+			Path: services.PathCaching,
+		},
+		// Service name replacement
+		services.NameReplacement{
+			NewName: "differentServiceName",
+		},
+	}
+
+	actual, err := services.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 := services.Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/services/urls.go b/openstack/cdn/v1/services/urls.go
new file mode 100644
index 0000000..5bb3ca9
--- /dev/null
+++ b/openstack/cdn/v1/services/urls.go
@@ -0,0 +1,23 @@
+package services
+
+import "github.com/gophercloud/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..2d30cc6
--- /dev/null
+++ b/openstack/client.go
@@ -0,0 +1,336 @@
+package openstack
+
+import (
+	"fmt"
+	"net/url"
+	"reflect"
+
+	"github.com/gophercloud/gophercloud"
+	tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+	"github.com/gophercloud/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{
+		{ID: v20, Priority: 20, Suffix: "/v2.0/"},
+		{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, gophercloud.EndpointOpts{})
+	case v30:
+		return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{})
+	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, eo gophercloud.EndpointOpts) error {
+	return v2auth(client, "", options, eo)
+}
+
+func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error {
+	v2Client, err := NewIdentityV2(client, eo)
+	if err != nil {
+		return err
+	}
+
+	if endpoint != "" {
+		v2Client.Endpoint = endpoint
+	}
+
+	v2Opts := tokens2.AuthOptions{
+		IdentityEndpoint: options.IdentityEndpoint,
+		Username:         options.Username,
+		Password:         options.Password,
+		TenantID:         options.TenantID,
+		TenantName:       options.TenantName,
+		AllowReauth:      options.AllowReauth,
+		TokenID:          options.TokenID,
+	}
+
+	result := tokens2.Create(v2Client, v2Opts)
+
+	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 v2auth(client, endpoint, options, eo)
+		}
+	}
+	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 tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
+	return v3auth(client, "", options, eo)
+}
+
+func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
+	// Override the generated service endpoint with the one returned by the version endpoint.
+	v3Client, err := NewIdentityV3(client, eo)
+	if err != nil {
+		return err
+	}
+
+	if endpoint != "" {
+		v3Client.Endpoint = endpoint
+	}
+
+	result := tokens3.Create(v3Client, opts)
+
+	token, err := result.ExtractToken()
+	if err != nil {
+		return err
+	}
+
+	catalog, err := result.ExtractServiceCatalog()
+	if err != nil {
+		return err
+	}
+
+	client.TokenID = token.ID
+
+	if opts.CanReauth() {
+		client.ReauthFunc = func() error {
+			client.TokenID = ""
+			return v3auth(client, endpoint, opts, eo)
+		}
+	}
+	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, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	endpoint := client.IdentityBase + "v2.0/"
+	var err error
+	if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
+		eo.ApplyDefaults("identity")
+		endpoint, err = client.EndpointLocator(eo)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       endpoint,
+	}, nil
+}
+
+// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
+func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	endpoint := client.IdentityBase + "v3/"
+	var err error
+	if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
+		eo.ApplyDefaults("identity")
+		endpoint, err = client.EndpointLocator(eo)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       endpoint,
+	}, nil
+}
+
+// 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
+}
+
+// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service.
+func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("volumev2")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service.
+func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("sharev2")
+	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
+}
+
+// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS service.
+func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("dns")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{
+		ProviderClient: client,
+		Endpoint:       url,
+		ResourceBase:   url + "v2/"}, nil
+}
+
+// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service.
+func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("image")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client,
+		Endpoint:     url,
+		ResourceBase: url + "v2/"}, nil
+}
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/requests.go b/openstack/common/extensions/requests.go
new file mode 100755
index 0000000..46b7d60
--- /dev/null
+++ b/openstack/common/extensions/requests.go
@@ -0,0 +1,20 @@
+package extensions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) (r GetResult) {
+	_, r.Err = c.Get(ExtensionURL(c, alias), &r.Body, nil)
+	return
+}
+
+// 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/results.go b/openstack/common/extensions/results.go
new file mode 100755
index 0000000..d5f8650
--- /dev/null
+++ b/openstack/common/extensions/results.go
@@ -0,0 +1,53 @@
+package extensions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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) {
+	var s struct {
+		Extension *Extension `json:"extension"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Extension, err
+}
+
+// Extension is a struct that represents an OpenStack extension.
+type Extension struct {
+	Updated     string        `json:"updated"`
+	Name        string        `json:"name"`
+	Links       []interface{} `json:"links"`
+	Namespace   string        `json:"namespace"`
+	Alias       string        `json:"alias"`
+	Description string        `json:"description"`
+}
+
+// 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)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Extension, error) {
+	var s struct {
+		Extensions []Extension `json:"extensions"`
+	}
+	err := (r.(ExtensionPage)).ExtractInto(&s)
+	return s.Extensions, err
+}
diff --git a/openstack/common/extensions/testing/doc.go b/openstack/common/extensions/testing/doc.go
new file mode 100644
index 0000000..24b0795
--- /dev/null
+++ b/openstack/common/extensions/testing/doc.go
@@ -0,0 +1,2 @@
+// common_extensions
+package testing
diff --git a/openstack/common/extensions/testing/fixtures.go b/openstack/common/extensions/testing/fixtures.go
new file mode 100644
index 0000000..a986c95
--- /dev/null
+++ b/openstack/common/extensions/testing/fixtures.go
@@ -0,0 +1,90 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/common/extensions"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 = extensions.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 = []extensions.Extension{ListedExtension}
+
+// SingleExtension is the Extension that should be parsed from GetOutput.
+var SingleExtension = &extensions.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/testing/requests_test.go b/openstack/common/extensions/testing/requests_test.go
new file mode 100644
index 0000000..fbaedfa
--- /dev/null
+++ b/openstack/common/extensions/testing/requests_test.go
@@ -0,0 +1,39 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/common/extensions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListExtensionsSuccessfully(t)
+
+	count := 0
+
+	extensions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := extensions.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 := extensions.Get(client.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, SingleExtension, actual)
+}
diff --git a/openstack/common/extensions/urls.go b/openstack/common/extensions/urls.go
new file mode 100644
index 0000000..eaf38b2
--- /dev/null
+++ b/openstack/common/extensions/urls.go
@@ -0,0 +1,13 @@
+package extensions
+
+import "github.com/gophercloud/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/compute/v2/extensions/availabilityzones/results.go b/openstack/compute/v2/extensions/availabilityzones/results.go
new file mode 100644
index 0000000..96a6a50
--- /dev/null
+++ b/openstack/compute/v2/extensions/availabilityzones/results.go
@@ -0,0 +1,12 @@
+package availabilityzones
+
+// ServerExt is an extension to the base Server object
+type ServerExt struct {
+	// AvailabilityZone is the availabilty zone the server is in.
+	AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"`
+}
+
+// UnmarshalJSON to override default
+func (r *ServerExt) UnmarshalJSON(b []byte) error {
+	return nil
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/requests.go b/openstack/compute/v2/extensions/bootfromvolume/requests.go
new file mode 100644
index 0000000..9dae14c
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/requests.go
@@ -0,0 +1,120 @@
+package bootfromvolume
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+)
+
+type (
+	// DestinationType represents the type of medium being used as the
+	// destination of the bootable device.
+	DestinationType string
+
+	// SourceType represents the type of medium being used as the source of the
+	// bootable device.
+	SourceType string
+)
+
+const (
+	// DestinationLocal DestinationType is for using an ephemeral disk as the
+	// destination.
+	DestinationLocal DestinationType = "local"
+
+	// DestinationVolume DestinationType is for using a volume as the destination.
+	DestinationVolume DestinationType = "volume"
+
+	// SourceBlank SourceType is for a "blank" or empty source.
+	SourceBlank SourceType = "blank"
+
+	// SourceImage SourceType is for using images as the source of a block device.
+	SourceImage SourceType = "image"
+
+	// SourceSnapshot SourceType is for using a volume snapshot as the source of
+	// a block device.
+	SourceSnapshot SourceType = "snapshot"
+
+	// SourceVolume SourceType is for using a volume as the source of block
+	// device.
+	SourceVolume SourceType = "volume"
+)
+
+// BlockDevice is a structure with options for creating block devices in a
+// server. The block device may be created from an image, snapshot, new volume,
+// or existing volume. The destination may be a new volume, existing volume
+// which will be attached to the instance, ephemeral disk, or boot device.
+type BlockDevice struct {
+	// SourceType must be one of: "volume", "snapshot", "image", or "blank".
+	SourceType SourceType `json:"source_type" required:"true"`
+
+	// UUID is the unique identifier for the existing volume, snapshot, or
+	// image (see above).
+	UUID string `json:"uuid,omitempty"`
+
+	// BootIndex is the boot index. It defaults to 0.
+	BootIndex int `json:"boot_index"`
+
+	// DeleteOnTermination specifies whether or not to delete the attached volume
+	// when the server is deleted. Defaults to `false`.
+	DeleteOnTermination bool `json:"delete_on_termination"`
+
+	// DestinationType is the type that gets created. Possible values are "volume"
+	// and "local".
+	DestinationType DestinationType `json:"destination_type,omitempty"`
+
+	// GuestFormat specifies the format of the block device.
+	GuestFormat string `json:"guest_format,omitempty"`
+
+	// VolumeSize is the size of the volume to create (in gigabytes). This can be
+	// omitted for existing volumes.
+	VolumeSize int `json:"volume_size,omitempty"`
+}
+
+// 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 {
+		err := gophercloud.ErrMissingInput{}
+		err.Argument = "bootfromvolume.CreateOptsExt.BlockDevice"
+		return nil, err
+	}
+
+	serverMap := base["server"].(map[string]interface{})
+
+	blockDevice := make([]map[string]interface{}, len(opts.BlockDevice))
+
+	for i, bd := range opts.BlockDevice {
+		b, err := gophercloud.BuildRequestBody(bd, "")
+		if err != nil {
+			return nil, err
+		}
+		blockDevice[i] = b
+	}
+	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) (r servers.CreateResult) {
+	b, err := opts.ToServerCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/results.go b/openstack/compute/v2/extensions/bootfromvolume/results.go
new file mode 100644
index 0000000..3211fb1
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/results.go
@@ -0,0 +1,10 @@
+package bootfromvolume
+
+import (
+	os "github.com/gophercloud/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/testing/doc.go b/openstack/compute/v2/extensions/bootfromvolume/testing/doc.go
new file mode 100644
index 0000000..cb879d9
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_bootfromvolume_v2
+package testing
diff --git a/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go b/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go
new file mode 100644
index 0000000..7fd3e7d
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/testing/requests_test.go
@@ -0,0 +1,327 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestBootFromNewVolume(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := bootfromvolume.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []bootfromvolume.BlockDevice{
+			{
+				UUID:                "123456",
+				SourceType:          bootfromvolume.SourceImage,
+				DestinationType:     bootfromvolume.DestinationVolume,
+				VolumeSize:          10,
+				DeleteOnTermination: true,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name":"createdserver",
+        "flavorRef":"performance1-1",
+        "imageRef":"",
+        "block_device_mapping_v2":[
+          {
+            "uuid":"123456",
+            "source_type":"image",
+            "destination_type":"volume",
+            "boot_index": 0,
+            "delete_on_termination": true,
+            "volume_size": 10
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestBootFromExistingVolume(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := bootfromvolume.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []bootfromvolume.BlockDevice{
+			{
+				UUID:                "123456",
+				SourceType:          bootfromvolume.SourceVolume,
+				DestinationType:     bootfromvolume.DestinationVolume,
+				DeleteOnTermination: true,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name":"createdserver",
+        "flavorRef":"performance1-1",
+        "imageRef":"",
+        "block_device_mapping_v2":[
+          {
+            "uuid":"123456",
+            "source_type":"volume",
+            "destination_type":"volume",
+            "boot_index": 0,
+            "delete_on_termination": true
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestBootFromImage(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := bootfromvolume.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []bootfromvolume.BlockDevice{
+			{
+				BootIndex:           0,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationLocal,
+				SourceType:          bootfromvolume.SourceImage,
+				UUID:                "asdfasdfasdf",
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name": "createdserver",
+        "imageRef": "asdfasdfasdf",
+        "flavorRef": "performance1-1",
+        "block_device_mapping_v2":[
+          {
+            "boot_index": 0,
+            "delete_on_termination": true,
+            "destination_type":"local",
+            "source_type":"image",
+            "uuid":"asdfasdfasdf"
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestCreateMultiEphemeralOpts(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := bootfromvolume.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []bootfromvolume.BlockDevice{
+			{
+				BootIndex:           0,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationLocal,
+				SourceType:          bootfromvolume.SourceImage,
+				UUID:                "asdfasdfasdf",
+			},
+			{
+				BootIndex:           -1,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationLocal,
+				GuestFormat:         "ext4",
+				SourceType:          bootfromvolume.SourceBlank,
+				VolumeSize:          1,
+			},
+			{
+				BootIndex:           -1,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationLocal,
+				GuestFormat:         "ext4",
+				SourceType:          bootfromvolume.SourceBlank,
+				VolumeSize:          1,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name": "createdserver",
+        "imageRef": "asdfasdfasdf",
+        "flavorRef": "performance1-1",
+        "block_device_mapping_v2":[
+          {
+            "boot_index": 0,
+            "delete_on_termination": true,
+            "destination_type":"local",
+            "source_type":"image",
+            "uuid":"asdfasdfasdf"
+          },
+          {
+            "boot_index": -1,
+            "delete_on_termination": true,
+            "destination_type":"local",
+            "guest_format":"ext4",
+            "source_type":"blank",
+            "volume_size": 1
+          },
+          {
+            "boot_index": -1,
+            "delete_on_termination": true,
+            "destination_type":"local",
+            "guest_format":"ext4",
+            "source_type":"blank",
+            "volume_size": 1
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestAttachNewVolume(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := bootfromvolume.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []bootfromvolume.BlockDevice{
+			{
+				BootIndex:           0,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationLocal,
+				SourceType:          bootfromvolume.SourceImage,
+				UUID:                "asdfasdfasdf",
+			},
+			{
+				BootIndex:           1,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationVolume,
+				SourceType:          bootfromvolume.SourceBlank,
+				VolumeSize:          1,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name": "createdserver",
+        "imageRef": "asdfasdfasdf",
+        "flavorRef": "performance1-1",
+        "block_device_mapping_v2":[
+          {
+            "boot_index": 0,
+            "delete_on_termination": true,
+            "destination_type":"local",
+            "source_type":"image",
+            "uuid":"asdfasdfasdf"
+          },
+          {
+            "boot_index": 1,
+            "delete_on_termination": true,
+            "destination_type":"volume",
+            "source_type":"blank",
+            "volume_size": 1
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestAttachExistingVolume(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := bootfromvolume.CreateOptsExt{
+		CreateOptsBuilder: base,
+		BlockDevice: []bootfromvolume.BlockDevice{
+			{
+				BootIndex:           0,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationLocal,
+				SourceType:          bootfromvolume.SourceImage,
+				UUID:                "asdfasdfasdf",
+			},
+			{
+				BootIndex:           1,
+				DeleteOnTermination: true,
+				DestinationType:     bootfromvolume.DestinationVolume,
+				SourceType:          bootfromvolume.SourceVolume,
+				UUID:                "123456",
+				VolumeSize:          1,
+			},
+		},
+	}
+
+	expected := `
+    {
+      "server": {
+        "name": "createdserver",
+        "imageRef": "asdfasdfasdf",
+        "flavorRef": "performance1-1",
+        "block_device_mapping_v2":[
+          {
+            "boot_index": 0,
+            "delete_on_termination": true,
+            "destination_type":"local",
+            "source_type":"image",
+            "uuid":"asdfasdfasdf"
+          },
+          {
+            "boot_index": 1,
+            "delete_on_termination": true,
+            "destination_type":"volume",
+            "source_type":"volume",
+            "uuid":"123456",
+            "volume_size": 1
+          }
+        ]
+      }
+    }
+  `
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/bootfromvolume/urls.go b/openstack/compute/v2/extensions/bootfromvolume/urls.go
new file mode 100644
index 0000000..dc007ea
--- /dev/null
+++ b/openstack/compute/v2/extensions/bootfromvolume/urls.go
@@ -0,0 +1,7 @@
+package bootfromvolume
+
+import "github.com/gophercloud/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("os-volumes_boot")
+}
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/requests.go b/openstack/compute/v2/extensions/defsecrules/requests.go
new file mode 100644
index 0000000..184fdc9
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests.go
@@ -0,0 +1,70 @@
+package defsecrules
+
+import (
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List will return a collection of default rules.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, rootURL(client), func(r pagination.PageResult) pagination.Page {
+		return DefaultRulePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// CreateOpts represents the configuration for adding a new default rule.
+type CreateOpts struct {
+	// The lower bound of the port range that will be opened.s
+	FromPort int `json:"from_port"`
+	// The upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+	// The protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol" required:"true"`
+	// ONLY required if FromGroupID is blank. This represents the IP range that
+	// will be the source of network traffic to your security group. Use
+	// 0.0.0.0/0 to allow all IP addresses.
+	CIDR string `json:"cidr,omitempty"`
+}
+
+// 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) {
+	if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
+		return nil, gophercloud.ErrMissingInput{Argument: "FromPort"}
+	}
+	if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
+		return nil, gophercloud.ErrMissingInput{Argument: "ToPort"}
+	}
+	return gophercloud.BuildRequestBody(opts, "security_group_default_rule")
+}
+
+// Create is the operation responsible for creating a new default rule.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToRuleCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Get will return details for a particular default rule.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(resourceURL(client, id), &r.Body, nil)
+	return
+}
+
+// Delete will permanently delete a default rule from the project.
+func Delete(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) {
+	_, r.Err = client.Delete(resourceURL(client, id), nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/results.go b/openstack/compute/v2/extensions/defsecrules/results.go
new file mode 100644
index 0000000..f990c99
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/results.go
@@ -0,0 +1,67 @@
+package defsecrules
+
+import (
+	"encoding/json"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// DefaultRule represents a default rule - which is identical to a
+// normal security rule.
+type DefaultRule secgroups.Rule
+
+func (r *DefaultRule) UnmarshalJSON(b []byte) error {
+	var s secgroups.Rule
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = DefaultRule(s)
+	return nil
+}
+
+// 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)
+	return len(users) == 0, err
+}
+
+// ExtractDefaultRules returns a slice of DefaultRules contained in a single
+// page of results.
+func ExtractDefaultRules(r pagination.Page) ([]DefaultRule, error) {
+	var s struct {
+		DefaultRules []DefaultRule `json:"security_group_default_rules"`
+	}
+	err := (r.(DefaultRulePage)).ExtractInto(&s)
+	return s.DefaultRules, 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) {
+	var s struct {
+		DefaultRule DefaultRule `json:"security_group_default_rule"`
+	}
+	err := r.ExtractInto(&s)
+	return &s.DefaultRule, err
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/testing/doc.go b/openstack/compute/v2/extensions/defsecrules/testing/doc.go
new file mode 100644
index 0000000..7e51c8f
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_defsecrules_v2
+package testing
diff --git a/openstack/compute/v2/extensions/defsecrules/testing/fixtures.go b/openstack/compute/v2/extensions/defsecrules/testing/fixtures.go
new file mode 100644
index 0000000..e4a62d4
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/testing/fixtures.go
@@ -0,0 +1,143 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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 mockCreateRuleResponseICMPZero(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group_default_rule": {
+    "ip_protocol": "ICMP",
+    "from_port": 0,
+    "to_port": 0,
+    "cidr": "10.10.12.0/24"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_default_rule": {
+    "from_port": 0,
+    "id": "{ruleID}",
+    "ip_protocol": "ICMP",
+    "ip_range": {
+      "cidr": "10.10.12.0/24"
+    },
+    "to_port": 0
+  }
+}
+`)
+	})
+}
+
+func mockGetRuleResponse(t *testing.T, ruleID string) {
+	url := rootPath + "/" + ruleID
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		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/testing/requests_test.go b/openstack/compute/v2/extensions/defsecrules/testing/requests_test.go
new file mode 100644
index 0000000..1f2fb86
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/testing/requests_test.go
@@ -0,0 +1,127 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const ruleID = "{ruleID}"
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListRulesResponse(t)
+
+	count := 0
+
+	err := defsecrules.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := defsecrules.ExtractDefaultRules(page)
+		th.AssertNoErr(t, err)
+
+		expected := []defsecrules.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 := defsecrules.CreateOpts{
+		IPProtocol: "TCP",
+		FromPort:   80,
+		ToPort:     80,
+		CIDR:       "10.10.12.0/24",
+	}
+
+	group, err := defsecrules.Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &defsecrules.DefaultRule{
+		ID:         ruleID,
+		FromPort:   80,
+		ToPort:     80,
+		IPProtocol: "TCP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestCreateICMPZero(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateRuleResponseICMPZero(t)
+
+	opts := defsecrules.CreateOpts{
+		IPProtocol: "ICMP",
+		FromPort:   0,
+		ToPort:     0,
+		CIDR:       "10.10.12.0/24",
+	}
+
+	group, err := defsecrules.Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &defsecrules.DefaultRule{
+		ID:         ruleID,
+		FromPort:   0,
+		ToPort:     0,
+		IPProtocol: "ICMP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetRuleResponse(t, ruleID)
+
+	group, err := defsecrules.Get(client.ServiceClient(), ruleID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &defsecrules.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 := defsecrules.Delete(client.ServiceClient(), ruleID).ExtractErr()
+	th.AssertNoErr(t, 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..e5fbf82
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/urls.go
@@ -0,0 +1,13 @@
+package defsecrules
+
+import "github.com/gophercloud/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..00e7c3b
--- /dev/null
+++ b/openstack/compute/v2/extensions/delegate.go
@@ -0,0 +1,23 @@
+package extensions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	common "github.com/gophercloud/gophercloud/openstack/common/extensions"
+	"github.com/gophercloud/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/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..41d04b9
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/requests.go
@@ -0,0 +1,103 @@
+package diskconfig
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"
+)
+
+// 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 controls how the rebuilt server's disk is partitioned.
+	DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"`
+}
+
+// ToServerRebuildMap adds the diskconfig option to the base server rebuild options.
+func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) {
+	if opts.DiskConfig != Auto && opts.DiskConfig != Manual {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "diskconfig.RebuildOptsExt.DiskConfig"
+		err.Info = "Must be either diskconfig.Auto or diskconfig.Manual"
+		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) {
+	if opts.DiskConfig != Auto && opts.DiskConfig != Manual {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "diskconfig.ResizeOptsExt.DiskConfig"
+		err.Info = "Must be either diskconfig.Auto or diskconfig.Manual"
+		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/results.go b/openstack/compute/v2/extensions/diskconfig/results.go
new file mode 100644
index 0000000..3ba66f5
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/results.go
@@ -0,0 +1,17 @@
+package diskconfig
+
+import "github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+
+type ServerWithDiskConfig struct {
+	servers.Server
+	DiskConfig DiskConfig `json:"OS-DCF:diskConfig"`
+}
+
+func (s ServerWithDiskConfig) ToServerCreateResult() (m map[string]interface{}) {
+	m["OS-DCF:diskConfig"] = s.DiskConfig
+	return
+}
+
+type CreateServerResultBuilder interface {
+	ToServerCreateResult() map[string]interface{}
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/testing/doc.go b/openstack/compute/v2/extensions/diskconfig/testing/doc.go
new file mode 100644
index 0000000..54c863b
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_diskconfig_v2
+package testing
diff --git a/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go b/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go
new file mode 100644
index 0000000..6ce560a
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/testing/requests_test.go
@@ -0,0 +1,88 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/diskconfig"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	ext := diskconfig.CreateOptsExt{
+		CreateOptsBuilder: base,
+		DiskConfig:        diskconfig.Manual,
+	}
+
+	expected := `
+		{
+			"server": {
+				"name": "createdserver",
+				"imageRef": "asdfasdfasdf",
+				"flavorRef": "performance1-1",
+				"OS-DCF:diskConfig": "MANUAL"
+			}
+		}
+	`
+	actual, err := ext.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestRebuildOpts(t *testing.T) {
+	base := servers.RebuildOpts{
+		Name:      "rebuiltserver",
+		AdminPass: "swordfish",
+		ImageID:   "asdfasdfasdf",
+	}
+
+	ext := diskconfig.RebuildOptsExt{
+		RebuildOptsBuilder: base,
+		DiskConfig:         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 := diskconfig.ResizeOptsExt{
+		ResizeOptsBuilder: base,
+		DiskConfig:        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/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/floatingips/doc.go b/openstack/compute/v2/extensions/floatingips/doc.go
new file mode 100644
index 0000000..6682fa6
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/doc.go
@@ -0,0 +1,3 @@
+// Package floatingips provides the ability to manage floating ips through
+// nova-network
+package floatingips
diff --git a/openstack/compute/v2/extensions/floatingips/requests.go b/openstack/compute/v2/extensions/floatingips/requests.go
new file mode 100644
index 0000000..b36aeba
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/requests.go
@@ -0,0 +1,112 @@
+package floatingips
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 FloatingIPPage{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 `json:"pool" required:"true"`
+}
+
+// ToFloatingIPCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// Create requests the creation of a new floating IP
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToFloatingIPCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Get returns data about a previously created FloatingIP.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// Delete requests the deletion of a previous allocated FloatingIP.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// AssociateOptsBuilder is the interface types must satfisfy to be used as
+// Associate options
+type AssociateOptsBuilder interface {
+	ToFloatingIPAssociateMap() (map[string]interface{}, error)
+}
+
+// AssociateOpts specifies the required information to associate a floating IP with an instance
+type AssociateOpts struct {
+	// FloatingIP is the floating IP to associate with an instance
+	FloatingIP string `json:"address" required:"true"`
+	// FixedIP is an optional fixed IP address of the server
+	FixedIP string `json:"fixed_address,omitempty"`
+}
+
+// ToFloatingIPAssociateMap constructs a request body from AssociateOpts.
+func (opts AssociateOpts) ToFloatingIPAssociateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "addFloatingIp")
+}
+
+// AssociateInstance pairs an allocated floating IP with an instance.
+func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) {
+	b, err := opts.ToFloatingIPAssociateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(associateURL(client, serverID), b, nil, nil)
+	return
+}
+
+// DisassociateOptsBuilder is the interface types must satfisfy to be used as
+// Disassociate options
+type DisassociateOptsBuilder interface {
+	ToFloatingIPDisassociateMap() (map[string]interface{}, error)
+}
+
+// DisassociateOpts specifies the required information to disassociate a floating IP with an instance
+type DisassociateOpts struct {
+	FloatingIP string `json:"address" required:"true"`
+}
+
+// ToFloatingIPDisassociateMap constructs a request body from AssociateOpts.
+func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "removeFloatingIp")
+}
+
+// DisassociateInstance decouples an allocated floating IP from an instance
+func DisassociateInstance(client *gophercloud.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) {
+	b, err := opts.ToFloatingIPDisassociateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(disassociateURL(client, serverID), b, nil, nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/floatingips/results.go b/openstack/compute/v2/extensions/floatingips/results.go
new file mode 100644
index 0000000..2f5b338
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/results.go
@@ -0,0 +1,117 @@
+package floatingips
+
+import (
+	"encoding/json"
+	"strconv"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"-"`
+
+	// FixedIP is the IP of the instance related to the Floating IP
+	FixedIP string `json:"fixed_ip,omitempty"`
+
+	// InstanceID is the ID of the instance that is using the Floating IP
+	InstanceID string `json:"instance_id"`
+
+	// IP is the actual Floating IP
+	IP string `json:"ip"`
+
+	// Pool is the pool of floating IPs that this floating IP belongs to
+	Pool string `json:"pool"`
+}
+
+func (r *FloatingIP) UnmarshalJSON(b []byte) error {
+	type tmp FloatingIP
+	var s struct {
+		tmp
+		ID interface{} `json:"id"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = FloatingIP(s.tmp)
+
+	switch t := s.ID.(type) {
+	case float64:
+		r.ID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ID = t
+	}
+
+	return err
+}
+
+// FloatingIPPage stores a single, only page of FloatingIPs
+// results from a List call.
+type FloatingIPPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a FloatingIPsPage is empty.
+func (page FloatingIPPage) 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(r pagination.Page) ([]FloatingIP, error) {
+	var s struct {
+		FloatingIPs []FloatingIP `json:"floating_ips"`
+	}
+	err := (r.(FloatingIPPage)).ExtractInto(&s)
+	return s.FloatingIPs, err
+}
+
+// FloatingIPResult is the raw result from a FloatingIP request.
+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) {
+	var s struct {
+		FloatingIP *FloatingIP `json:"floating_ip"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/floatingips/testing/doc.go b/openstack/compute/v2/extensions/floatingips/testing/doc.go
new file mode 100644
index 0000000..961aeee
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_floatingips_v2
+package testing
diff --git a/openstack/compute/v2/extensions/floatingips/testing/fixtures.go b/openstack/compute/v2/extensions/floatingips/testing/fixtures.go
new file mode 100644
index 0000000..6866e26
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/testing/fixtures.go
@@ -0,0 +1,223 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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"
+    }
+}
+`
+
+// CreateOutputWithNumericID is a sample response to a Post call
+// with a legacy nova-network-based numeric ID.
+const CreateOutputWithNumericID = `
+{
+    "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 = floatingips.FloatingIP{
+	ID:   "1",
+	IP:   "10.10.10.1",
+	Pool: "nova",
+}
+
+// SecondFloatingIP is the first result in ListOutput.
+var SecondFloatingIP = floatingips.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 = []floatingips.FloatingIP{FirstFloatingIP, SecondFloatingIP}
+
+// CreatedFloatingIP is the parsed result from CreateOutput.
+var CreatedFloatingIP = floatingips.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)
+	})
+}
+
+// HandleCreateWithNumericIDSuccessfully configures the test server to respond to a Create request
+// for a new floating ip
+func HandleCreateWithNumericIDSuccessfully(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, CreateOutputWithNumericID)
+	})
+}
+
+// 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/floatingips/testing/requests_test.go b/openstack/compute/v2/extensions/floatingips/testing/requests_test.go
new file mode 100644
index 0000000..2356671
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/testing/requests_test.go
@@ -0,0 +1,111 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := floatingips.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := floatingips.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 := floatingips.Create(client.ServiceClient(), floatingips.CreateOpts{
+		Pool: "nova",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &CreatedFloatingIP, actual)
+}
+
+func TestCreateWithNumericID(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateWithNumericIDSuccessfully(t)
+
+	actual, err := floatingips.Create(client.ServiceClient(), floatingips.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 := floatingips.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 := floatingips.Delete(client.ServiceClient(), "1").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAssociate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateSuccessfully(t)
+
+	associateOpts := floatingips.AssociateOpts{
+		FloatingIP: "10.10.10.2",
+	}
+
+	err := floatingips.AssociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAssociateFixed(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateFixedSuccessfully(t)
+
+	associateOpts := floatingips.AssociateOpts{
+		FloatingIP: "10.10.10.2",
+		FixedIP:    "166.78.185.201",
+	}
+
+	err := floatingips.AssociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisassociateInstance(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDisassociateSuccessfully(t)
+
+	disassociateOpts := floatingips.DisassociateOpts{
+		FloatingIP: "10.10.10.2",
+	}
+
+	err := floatingips.DisassociateInstance(client.ServiceClient(), "4d8c3732-a248-40ed-bebc-539a6ffd25c0", disassociateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/floatingips/urls.go b/openstack/compute/v2/extensions/floatingips/urls.go
new file mode 100644
index 0000000..4768e5a
--- /dev/null
+++ b/openstack/compute/v2/extensions/floatingips/urls.go
@@ -0,0 +1,37 @@
+package floatingips
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/compute/v2/extensions/keypairs/requests.go
new file mode 100644
index 0000000..adf1e55
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/requests.go
@@ -0,0 +1,84 @@
+package keypairs
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	"github.com/gophercloud/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 is a friendly name to refer to this KeyPair in other services.
+	Name string `json:"name" required:"true"`
+	// 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 `json:"public_key,omitempty"`
+}
+
+// ToKeyPairCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "keypair")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToKeyPairCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Get returns public data about a previously uploaded KeyPair.
+func Get(client *gophercloud.ServiceClient, name string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, name), &r.Body, nil)
+	return
+}
+
+// Delete requests the deletion of a previous stored KeyPair from the server.
+func Delete(client *gophercloud.ServiceClient, name string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, name), nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/keypairs/results.go b/openstack/compute/v2/extensions/keypairs/results.go
new file mode 100644
index 0000000..4c785a2
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/results.go
@@ -0,0 +1,86 @@
+package keypairs
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// KeyPair is an SSH key known to the OpenStack Cloud 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 `json:"name"`
+
+	// Fingerprint is a short sequence of bytes that can be used to authenticate or validate a longer
+	// public key.
+	Fingerprint string `json:"fingerprint"`
+
+	// PublicKey is the public key from this pair, in OpenSSH format. "ssh-rsa AAAAB3Nz..."
+	PublicKey string `json:"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 `json:"private_key"`
+
+	// UserID is the user who owns this keypair.
+	UserID string `json:"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(r pagination.Page) ([]KeyPair, error) {
+	type pair struct {
+		KeyPair KeyPair `json:"keypair"`
+	}
+	var s struct {
+		KeyPairs []pair `json:"keypairs"`
+	}
+	err := (r.(KeyPairPage)).ExtractInto(&s)
+	results := make([]KeyPair, len(s.KeyPairs))
+	for i, pair := range s.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) {
+	var s struct {
+		KeyPair *KeyPair `json:"keypair"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/compute/v2/extensions/keypairs/testing/doc.go
new file mode 100644
index 0000000..8f8aaca
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_keypairs_v2
+package testing
diff --git a/openstack/compute/v2/extensions/keypairs/testing/fixtures.go b/openstack/compute/v2/extensions/keypairs/testing/fixtures.go
new file mode 100644
index 0000000..dc716d8
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/testing/fixtures.go
@@ -0,0 +1,170 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 = keypairs.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 = keypairs.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 = []keypairs.KeyPair{FirstKeyPair, SecondKeyPair}
+
+// CreatedKeyPair is the parsed result from CreatedOutput.
+var CreatedKeyPair = keypairs.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 = keypairs.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/testing/requests_test.go b/openstack/compute/v2/extensions/keypairs/testing/requests_test.go
new file mode 100644
index 0000000..1e05e66
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/testing/requests_test.go
@@ -0,0 +1,72 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := keypairs.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := keypairs.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 := keypairs.Create(client.ServiceClient(), keypairs.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 := keypairs.Create(client.ServiceClient(), keypairs.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 := keypairs.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 := keypairs.Delete(client.ServiceClient(), "deletedkey").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/keypairs/urls.go b/openstack/compute/v2/extensions/keypairs/urls.go
new file mode 100644
index 0000000..fec38f3
--- /dev/null
+++ b/openstack/compute/v2/extensions/keypairs/urls.go
@@ -0,0 +1,25 @@
+package keypairs
+
+import "github.com/gophercloud/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/limits/requests.go b/openstack/compute/v2/extensions/limits/requests.go
new file mode 100644
index 0000000..70324b8
--- /dev/null
+++ b/openstack/compute/v2/extensions/limits/requests.go
@@ -0,0 +1,39 @@
+package limits
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// GetOptsBuilder allows extensions to add additional parameters to the
+// Get request.
+type GetOptsBuilder interface {
+	ToLimitsQuery() (string, error)
+}
+
+// GetOpts enables retrieving limits by a specific tenant.
+type GetOpts struct {
+	// The tenant ID to retrieve limits for
+	TenantID string `q:"tenant_id"`
+}
+
+// ToLimitsQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToLimitsQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// Get returns the limits about the currently scoped tenant.
+func Get(client *gophercloud.ServiceClient, opts GetOptsBuilder) (r GetResult) {
+	url := getURL(client)
+	if opts != nil {
+		query, err := opts.ToLimitsQuery()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		url += query
+	}
+
+	_, r.Err = client.Get(url, &r.Body, nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/limits/results.go b/openstack/compute/v2/extensions/limits/results.go
new file mode 100644
index 0000000..95b794d
--- /dev/null
+++ b/openstack/compute/v2/extensions/limits/results.go
@@ -0,0 +1,90 @@
+package limits
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// Limits is a struct that contains the response of a limit query.
+type Limits struct {
+	// Absolute contains the limits and usage information.
+	Absolute Absolute `json:"absolute"`
+}
+
+// Usage is a struct that contains the current resource usage and limits
+// of a tenant.
+type Absolute struct {
+	// MaxTotalCores is the number of cores available to a tenant.
+	MaxTotalCores int `json:"maxTotalCores"`
+
+	// MaxImageMeta is the amount of image metadata available to a tenant.
+	MaxImageMeta int `json:"maxImageMeta"`
+
+	// MaxServerMeta is the amount of server metadata available to a tenant.
+	MaxServerMeta int `json:"maxServerMeta"`
+
+	// MaxPersonality is the amount of personality/files available to a tenant.
+	MaxPersonality int `json:"maxPersonality"`
+
+	// MaxPersonalitySize is the personality file size available to a tenant.
+	MaxPersonalitySize int `json:"maxPersonalitySize"`
+
+	// MaxTotalKeypairs is the total keypairs available to a tenant.
+	MaxTotalKeypairs int `json:maxTotalKeypairs"`
+
+	// MaxSecurityGroups is the number of security groups available to a tenant.
+	MaxSecurityGroups int `json:"maxSecurityGroups"`
+
+	// MaxSecurityGroupRules is the number of security group rules available to
+	// a tenant.
+	MaxSecurityGroupRules int `json:"maxSecurityGroupRules"`
+
+	// MaxServerGroups is the number of server groups available to a tenant.
+	MaxServerGroups int `json:"maxServerGroups"`
+
+	// MaxServerGroupMembers is the number of server group members available
+	// to a tenant.
+	MaxServerGroupMembers int `json:"maxServerGroupMembers"`
+
+	// MaxTotalFloatingIps is the number of floating IPs available to a tenant.
+	MaxTotalFloatingIps int `json:"maxTotalFloatingIps"`
+
+	// MaxTotalInstances is the number of instances/servers available to a tenant.
+	MaxTotalInstances int `json:"maxTotalInstances"`
+
+	// MaxTotalRAMSize is the total amount of RAM available to a tenant measured
+	// in megabytes (MB).
+	MaxTotalRAMSize int `json:"maxTotalRAMSize"`
+
+	// TotalCoresUsed is the number of cores currently in use.
+	TotalCoresUsed int `json:"totalCoresUsed"`
+
+	// TotalInstancesUsed is the number of instances/servers in use.
+	TotalInstancesUsed int `json:"totalInstancesUsed"`
+
+	// TotalFloatingIpsUsed is the number of floating IPs in use.
+	TotalFloatingIpsUsed int `json:"totalFloatingIpsUsed"`
+
+	// TotalRAMUsed is the total RAM/memory in use measured in megabytes (MB).
+	TotalRAMUsed int `json:"totalRAMUsed"`
+
+	// TotalSecurityGroupsUsed is the total number of security groups in use.
+	TotalSecurityGroupsUsed int `json:"totalSecurityGroupsUsed"`
+
+	// TotalServerGroupsUsed is the total number of server groups in use.
+	TotalServerGroupsUsed int `json:"totalServerGroupsUsed"`
+}
+
+// Extract interprets a limits result as a Limits.
+func (r GetResult) Extract() (*Limits, error) {
+	var s struct {
+		Limits *Limits `json:"limits"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Limits, err
+}
+
+// GetResult is the response from a Get operation. Call its ExtractAbsolute
+// method to interpret it as an Absolute.
+type GetResult struct {
+	gophercloud.Result
+}
diff --git a/openstack/compute/v2/extensions/limits/testing/fixtures.go b/openstack/compute/v2/extensions/limits/testing/fixtures.go
new file mode 100644
index 0000000..9ec24a9
--- /dev/null
+++ b/openstack/compute/v2/extensions/limits/testing/fixtures.go
@@ -0,0 +1,80 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "limits": {
+        "rate": [],
+        "absolute": {
+            "maxServerMeta": 128,
+            "maxPersonality": 5,
+            "totalServerGroupsUsed": 0,
+            "maxImageMeta": 128,
+            "maxPersonalitySize": 10240,
+            "maxTotalKeypairs": 100,
+            "maxSecurityGroupRules": 20,
+            "maxServerGroups": 10,
+            "totalCoresUsed": 1,
+            "totalRAMUsed": 2048,
+            "totalInstancesUsed": 1,
+            "maxSecurityGroups": 10,
+            "totalFloatingIpsUsed": 0,
+            "maxTotalCores": 20,
+            "maxServerGroupMembers": 10,
+            "maxTotalFloatingIps": 10,
+            "totalSecurityGroupsUsed": 1,
+            "maxTotalInstances": 10,
+            "maxTotalRAMSize": 51200
+        }
+    }
+}
+`
+
+// LimitsResult is the result of the limits in GetOutput.
+var LimitsResult = limits.Limits{
+	limits.Absolute{
+		MaxServerMeta:           128,
+		MaxPersonality:          5,
+		TotalServerGroupsUsed:   0,
+		MaxImageMeta:            128,
+		MaxPersonalitySize:      10240,
+		MaxTotalKeypairs:        100,
+		MaxSecurityGroupRules:   20,
+		MaxServerGroups:         10,
+		TotalCoresUsed:          1,
+		TotalRAMUsed:            2048,
+		TotalInstancesUsed:      1,
+		MaxSecurityGroups:       10,
+		TotalFloatingIpsUsed:    0,
+		MaxTotalCores:           20,
+		MaxServerGroupMembers:   10,
+		MaxTotalFloatingIps:     10,
+		TotalSecurityGroupsUsed: 1,
+		MaxTotalInstances:       10,
+		MaxTotalRAMSize:         51200,
+	},
+}
+
+const TenantID = "555544443333222211110000ffffeeee"
+
+// HandleGetSuccessfully configures the test server to respond to a Get request
+// for a limit.
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/limits", 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/limits/testing/requests_test.go b/openstack/compute/v2/extensions/limits/testing/requests_test.go
new file mode 100644
index 0000000..9c8456c
--- /dev/null
+++ b/openstack/compute/v2/extensions/limits/testing/requests_test.go
@@ -0,0 +1,23 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	getOpts := limits.GetOpts{
+		TenantID: TenantID,
+	}
+
+	actual, err := limits.Get(client.ServiceClient(), getOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &LimitsResult, actual)
+}
diff --git a/openstack/compute/v2/extensions/limits/urls.go b/openstack/compute/v2/extensions/limits/urls.go
new file mode 100644
index 0000000..edd97e4
--- /dev/null
+++ b/openstack/compute/v2/extensions/limits/urls.go
@@ -0,0 +1,11 @@
+package limits
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+const resourcePath = "limits"
+
+func getURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
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/requests.go b/openstack/compute/v2/extensions/networks/requests.go
new file mode 100644
index 0000000..5432a10
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/requests.go
@@ -0,0 +1,19 @@
+package networks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of Network.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get returns data about a previously created Network.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/networks/results.go b/openstack/compute/v2/extensions/networks/results.go
new file mode 100644
index 0000000..cbcce31
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/results.go
@@ -0,0 +1,134 @@
+package networks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"bridge"`
+
+	// BridgeInterface is what interface is connected to the Bridge
+	BridgeInterface string `json:"bridge_interface"`
+
+	// The Broadcast address of the network.
+	Broadcast string `json:"broadcast"`
+
+	// CIDR is the IPv4 subnet.
+	CIDR string `json:"cidr"`
+
+	// CIDRv6 is the IPv6 subnet.
+	CIDRv6 string `json:"cidr_v6"`
+
+	// CreatedAt is when the network was created..
+	CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at,omitempty"`
+
+	// Deleted shows if the network has been deleted.
+	Deleted bool `json:"deleted"`
+
+	// DeletedAt is the time when the network was deleted.
+	DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at,omitempty"`
+
+	// DHCPStart is the start of the DHCP address range.
+	DHCPStart string `json:"dhcp_start"`
+
+	// DNS1 is the first DNS server to use through DHCP.
+	DNS1 string `json:"dns_1"`
+
+	// DNS2 is the first DNS server to use through DHCP.
+	DNS2 string `json:"dns_2"`
+
+	// Gateway is the network gateway.
+	Gateway string `json:"gateway"`
+
+	// Gatewayv6 is the IPv6 network gateway.
+	Gatewayv6 string `json:"gateway_v6"`
+
+	// Host is the host that the network service is running on.
+	Host string `json:"host"`
+
+	// ID is the UUID of the network.
+	ID string `json:"id"`
+
+	// Injected determines if network information is injected into the host.
+	Injected bool `json:"injected"`
+
+	// Label is the common name that the network has..
+	Label string `json:"label"`
+
+	// MultiHost is if multi-host networking is enablec..
+	MultiHost bool `json:"multi_host"`
+
+	// Netmask is the network netmask.
+	Netmask string `json:"netmask"`
+
+	// Netmaskv6 is the IPv6 netmask.
+	Netmaskv6 string `json:"netmask_v6"`
+
+	// Priority is the network interface priority.
+	Priority int `json:"priority"`
+
+	// ProjectID is the project associated with this network.
+	ProjectID string `json:"project_id"`
+
+	// RXTXBase configures bandwidth entitlement.
+	RXTXBase int `json:"rxtx_base"`
+
+	// UpdatedAt is the time when the network was last updated.
+	UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at,omitempty"`
+
+	// VLAN is the vlan this network runs on.
+	VLAN int `json:"vlan"`
+
+	// VPNPrivateAddress is the private address of the CloudPipe VPN.
+	VPNPrivateAddress string `json:"vpn_private_address"`
+
+	// VPNPublicAddress is the public address of the CloudPipe VPN.
+	VPNPublicAddress string `json:"vpn_public_address"`
+
+	// VPNPublicPort is the port of the CloudPipe VPN.
+	VPNPublicPort int `json:"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(r pagination.Page) ([]Network, error) {
+	var s struct {
+		Networks []Network `json:"networks"`
+	}
+	err := (r.(NetworkPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Network *Network `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/compute/v2/extensions/networks/testing/doc.go
new file mode 100644
index 0000000..76a18cd
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_networks_v2
+package testing
diff --git a/openstack/compute/v2/extensions/networks/testing/fixtures.go b/openstack/compute/v2/extensions/networks/testing/fixtures.go
new file mode 100644
index 0000000..e2fa49b
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/testing/fixtures.go
@@ -0,0 +1,204 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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-15T06:19:19.387525",
+            "deleted": false,
+            "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-16T09: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-15T06:19:19.387525",
+            "deleted": false,
+            "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,
+            "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-15T06:19:19.387525",
+			"deleted": false,
+			"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,
+			"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 = networks.Network{
+	Bridge:            "br100",
+	BridgeInterface:   "eth0",
+	Broadcast:         "10.0.0.7",
+	CIDR:              "10.0.0.0/29",
+	CIDRv6:            "",
+	CreatedAt:         gophercloud.JSONRFC3339MilliNoZ(time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC)),
+	Deleted:           false,
+	DeletedAt:         gophercloud.JSONRFC3339MilliNoZ(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:         gophercloud.JSONRFC3339MilliNoZ(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 = networks.Network{
+	Bridge:            "br101",
+	BridgeInterface:   "eth0",
+	Broadcast:         "10.0.0.15",
+	CIDR:              "10.0.0.10/29",
+	CIDRv6:            "",
+	CreatedAt:         gophercloud.JSONRFC3339MilliNoZ(time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC)),
+	Deleted:           false,
+	DeletedAt:         gophercloud.JSONRFC3339MilliNoZ(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:         gophercloud.JSONRFC3339MilliNoZ(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 = []networks.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/testing/requests_test.go b/openstack/compute/v2/extensions/networks/testing/requests_test.go
new file mode 100644
index 0000000..36b5463
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/testing/requests_test.go
@@ -0,0 +1,38 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := networks.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := networks.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 := networks.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/urls.go b/openstack/compute/v2/extensions/networks/urls.go
new file mode 100644
index 0000000..491bde6
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/urls.go
@@ -0,0 +1,17 @@
+package networks
+
+import "github.com/gophercloud/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/quotasets/doc.go b/openstack/compute/v2/extensions/quotasets/doc.go
new file mode 100644
index 0000000..721024e
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/doc.go
@@ -0,0 +1,3 @@
+// Package quotasets provides information and interaction with QuotaSet
+// extension for the OpenStack Compute service.
+package quotasets
diff --git a/openstack/compute/v2/extensions/quotasets/requests.go b/openstack/compute/v2/extensions/quotasets/requests.go
new file mode 100644
index 0000000..bb9cb22
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/requests.go
@@ -0,0 +1,76 @@
+package quotasets
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// Get returns public data about a previously created QuotaSet.
+func Get(client *gophercloud.ServiceClient, tenantID string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, tenantID), &res.Body, nil)
+	return res
+}
+
+//Updates the quotas for the given tenantID and returns the new quota-set
+func Update(client *gophercloud.ServiceClient, tenantID string, opts UpdateOptsBuilder) (res UpdateResult) {
+	reqBody, err := opts.ToComputeQuotaUpdateMap()
+	if err != nil {
+		res.Err = err
+		return
+	}
+
+	_, res.Err = client.Put(updateURL(client, tenantID), reqBody, &res.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
+	return res
+}
+
+//Resets the uotas for the given tenant to their default values
+func Delete(client *gophercloud.ServiceClient, tenantID string) (res DeleteResult) {
+	_, res.Err = client.Delete(deleteURL(client, tenantID), nil)
+	return
+}
+
+//Options for Updating the quotas of a Tenant
+//All int-values are pointers so they can be nil if they are not needed
+//you can use gopercloud.IntToPointer() for convenience
+type UpdateOpts struct {
+	//FixedIps is number of fixed ips alloted this quota_set
+	FixedIps *int `json:"fixed_ips,omitempty"`
+	// FloatingIps is number of floating ips alloted this quota_set
+	FloatingIps *int `json:"floating_ips,omitempty"`
+	// InjectedFileContentBytes is content bytes allowed for each injected file
+	InjectedFileContentBytes *int `json:"injected_file_content_bytes,omitempty"`
+	// InjectedFilePathBytes is allowed bytes for each injected file path
+	InjectedFilePathBytes *int `json:"injected_file_path_bytes,omitempty"`
+	// InjectedFiles is injected files allowed for each project
+	InjectedFiles *int `json:"injected_files,omitempty"`
+	// KeyPairs is number of ssh keypairs
+	KeyPairs *int `json:"key_pairs,omitempty"`
+	// MetadataItems is number of metadata items allowed for each instance
+	MetadataItems *int `json:"metadata_items,omitempty"`
+	// Ram is megabytes allowed for each instance
+	Ram *int `json:"ram,omitempty"`
+	// SecurityGroupRules is rules allowed for each security group
+	SecurityGroupRules *int `json:"security_group_rules,omitempty"`
+	// SecurityGroups security groups allowed for each project
+	SecurityGroups *int `json:"security_groups,omitempty"`
+	// Cores is number of instance cores allowed for each project
+	Cores *int `json:"cores,omitempty"`
+	// Instances is number of instances allowed for each project
+	Instances *int `json:"instances,omitempty"`
+	// Number of ServerGroups allowed for the project
+	ServerGroups *int `json:"server_groups,omitempty"`
+	// Max number of Members for each ServerGroup
+	ServerGroupMembers *int `json:"server_group_members,omitempty"`
+	//Users can force the update even if the quota has already been used and the reserved quota exceeds the new quota.
+	Force bool `json:"force,omitempty"`
+}
+
+type UpdateOptsBuilder interface {
+	//Extra specific name to prevent collisions with interfaces for other quotas (e.g. neutron)
+	ToComputeQuotaUpdateMap() (map[string]interface{}, error)
+}
+
+func (opts UpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) {
+
+	return gophercloud.BuildRequestBody(opts, "quota_set")
+}
diff --git a/openstack/compute/v2/extensions/quotasets/results.go b/openstack/compute/v2/extensions/quotasets/results.go
new file mode 100644
index 0000000..44e6b06
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/results.go
@@ -0,0 +1,91 @@
+package quotasets
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// QuotaSet is a set of operational limits that allow for control of compute usage.
+type QuotaSet struct {
+	//ID is tenant associated with this quota_set
+	ID string `json:"id"`
+	//FixedIps is number of fixed ips alloted this quota_set
+	FixedIps int `json:"fixed_ips"`
+	// FloatingIps is number of floating ips alloted this quota_set
+	FloatingIps int `json:"floating_ips"`
+	// InjectedFileContentBytes is content bytes allowed for each injected file
+	InjectedFileContentBytes int `json:"injected_file_content_bytes"`
+	// InjectedFilePathBytes is allowed bytes for each injected file path
+	InjectedFilePathBytes int `json:"injected_file_path_bytes"`
+	// InjectedFiles is injected files allowed for each project
+	InjectedFiles int `json:"injected_files"`
+	// KeyPairs is number of ssh keypairs
+	KeyPairs int `json:"key_pairs"`
+	// MetadataItems is number of metadata items allowed for each instance
+	MetadataItems int `json:"metadata_items"`
+	// Ram is megabytes allowed for each instance
+	Ram int `json:"ram"`
+	// SecurityGroupRules is rules allowed for each security group
+	SecurityGroupRules int `json:"security_group_rules"`
+	// SecurityGroups security groups allowed for each project
+	SecurityGroups int `json:"security_groups"`
+	// Cores is number of instance cores allowed for each project
+	Cores int `json:"cores"`
+	// Instances is number of instances allowed for each project
+	Instances int `json:"instances"`
+	// Number of ServerGroups allowed for the project
+	ServerGroups int `json:"server_groups"`
+	// Max number of Members for each ServerGroup
+	ServerGroupMembers int `json:"server_group_members"`
+}
+
+// QuotaSetPage stores a single, only page of QuotaSet results from a List call.
+type QuotaSetPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a QuotaSetsetPage is empty.
+func (page QuotaSetPage) IsEmpty() (bool, error) {
+	ks, err := ExtractQuotaSets(page)
+	return len(ks) == 0, err
+}
+
+// ExtractQuotaSets interprets a page of results as a slice of QuotaSets.
+func ExtractQuotaSets(r pagination.Page) ([]QuotaSet, error) {
+	var s struct {
+		QuotaSets []QuotaSet `json:"quotas"`
+	}
+	err := (r.(QuotaSetPage)).ExtractInto(&s)
+	return s.QuotaSets, err
+}
+
+type quotaResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any QuotaSet resource response as a QuotaSet struct.
+func (r quotaResult) Extract() (*QuotaSet, error) {
+	var s struct {
+		QuotaSet *QuotaSet `json:"quota_set"`
+	}
+	err := r.ExtractInto(&s)
+	return s.QuotaSet, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a QuotaSet.
+type GetResult struct {
+	quotaResult
+}
+
+// UpdateResult is the response from a Update operation. Call its Extract method to interpret it
+// as a QuotaSet.
+type UpdateResult struct {
+	quotaResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its Extract method to interpret it
+// as a QuotaSet.
+type DeleteResult struct {
+	quotaResult
+}
diff --git a/openstack/compute/v2/extensions/quotasets/testing/doc.go b/openstack/compute/v2/extensions/quotasets/testing/doc.go
new file mode 100644
index 0000000..19ad75d
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_quotasets_v2
+package testing
diff --git a/openstack/compute/v2/extensions/quotasets/testing/fixtures.go b/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
new file mode 100644
index 0000000..79305a7
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
@@ -0,0 +1,118 @@
+package testing
+
+import (
+	"fmt"
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+	"net/http"
+	"testing"
+)
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+   "quota_set" : {
+      "instances" : 25,
+      "security_groups" : 10,
+      "security_group_rules" : 20,
+      "cores" : 200,
+      "injected_file_content_bytes" : 10240,
+      "injected_files" : 5,
+      "metadata_items" : 128,
+      "ram" : 200000,
+      "key_pairs" : 10,
+      "injected_file_path_bytes" : 255,
+	  "server_groups" : 2,
+	  "server_group_members" : 3
+   }
+}
+`
+const FirstTenantID = "555544443333222211110000ffffeeee"
+
+// FirstQuotaSet is the first result in ListOutput.
+var FirstQuotaSet = quotasets.QuotaSet{
+	FixedIps:                 0,
+	FloatingIps:              0,
+	InjectedFileContentBytes: 10240,
+	InjectedFilePathBytes:    255,
+	InjectedFiles:            5,
+	KeyPairs:                 10,
+	MetadataItems:            128,
+	Ram:                      200000,
+	SecurityGroupRules:       20,
+	SecurityGroups:           10,
+	Cores:                    200,
+	Instances:                25,
+	ServerGroups:             2,
+	ServerGroupMembers:       3,
+}
+
+//The expected update Body. Is also returned by PUT request
+const UpdateOutput = `{"quota_set":{"cores":200,"fixed_ips":0,"floating_ips":0,"injected_file_content_bytes":10240,"injected_file_path_bytes":255,"injected_files":5,"instances":25,"key_pairs":10,"metadata_items":128,"ram":200000,"security_group_rules":20,"security_groups":10,"server_groups":2,"server_group_members":3}}`
+
+//The expected partialupdate Body. Is also returned by PUT request
+const PartialUpdateBody = `{"quota_set":{"cores":200, "force":true}}`
+
+//Result of Quota-update
+var UpdatedQuotaSet = quotasets.UpdateOpts{
+	FixedIps:                 gophercloud.IntToPointer(0),
+	FloatingIps:              gophercloud.IntToPointer(0),
+	InjectedFileContentBytes: gophercloud.IntToPointer(10240),
+	InjectedFilePathBytes:    gophercloud.IntToPointer(255),
+	InjectedFiles:            gophercloud.IntToPointer(5),
+	KeyPairs:                 gophercloud.IntToPointer(10),
+	MetadataItems:            gophercloud.IntToPointer(128),
+	Ram:                      gophercloud.IntToPointer(200000),
+	SecurityGroupRules:       gophercloud.IntToPointer(20),
+	SecurityGroups:           gophercloud.IntToPointer(10),
+	Cores:                    gophercloud.IntToPointer(200),
+	Instances:                gophercloud.IntToPointer(25),
+	ServerGroups:             gophercloud.IntToPointer(2),
+	ServerGroupMembers:       gophercloud.IntToPointer(3),
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request for sample tenant
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+}
+
+// HandlePutSuccessfully configures the test server to respond to a Put request for sample tenant
+func HandlePutSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, 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, UpdateOutput)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, UpdateOutput)
+	})
+}
+
+// HandlePartialPutSuccessfully configures the test server to respond to a Put request for sample tenant that only containes specific values
+func HandlePartialPutSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, 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, PartialUpdateBody)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, UpdateOutput)
+	})
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request for sample tenant
+func HandleDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestBody(t, r, "")
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(202)
+	})
+}
diff --git a/openstack/compute/v2/extensions/quotasets/testing/requests_test.go b/openstack/compute/v2/extensions/quotasets/testing/requests_test.go
new file mode 100644
index 0000000..0193f97
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/testing/requests_test.go
@@ -0,0 +1,64 @@
+package testing
+
+import (
+	"testing"
+
+	"errors"
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+	actual, err := quotasets.Get(client.ServiceClient(), FirstTenantID).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePutSuccessfully(t)
+	actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, UpdatedQuotaSet).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
+
+func TestPartialUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePartialPutSuccessfully(t)
+	opts := quotasets.UpdateOpts{Cores: gophercloud.IntToPointer(200), Force: true}
+	actual, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteSuccessfully(t)
+	_, err := quotasets.Delete(client.ServiceClient(), FirstTenantID).Extract()
+	th.AssertNoErr(t, err)
+}
+
+type ErrorUpdateOpts quotasets.UpdateOpts
+
+func (opts ErrorUpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) {
+	return nil, errors.New("This is an error")
+}
+
+func TestErrorInToComputeQuotaUpdateMap(t *testing.T) {
+	opts := &ErrorUpdateOpts{}
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePutSuccessfully(t)
+	_, err := quotasets.Update(client.ServiceClient(), FirstTenantID, opts).Extract()
+	if err == nil {
+		t.Fatal("Error handling failed")
+	}
+}
diff --git a/openstack/compute/v2/extensions/quotasets/urls.go b/openstack/compute/v2/extensions/quotasets/urls.go
new file mode 100644
index 0000000..64190e9
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/urls.go
@@ -0,0 +1,21 @@
+package quotasets
+
+import "github.com/gophercloud/gophercloud"
+
+const resourcePath = "os-quota-sets"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func getURL(c *gophercloud.ServiceClient, tenantID string) string {
+	return c.ServiceURL(resourcePath, tenantID)
+}
+
+func updateURL(c *gophercloud.ServiceClient, tenantID string) string {
+	return getURL(c, tenantID)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, tenantID string) string {
+	return getURL(c, tenantID)
+}
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..1e06b46
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/requests.go
@@ -0,0 +1,148 @@
+package schedulerhints
+
+import (
+	"net"
+	"regexp"
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"target_cell,omitempty"`
+	// BuildNearHostIP specifies a subnet of compute nodes to host the instance.
+	BuildNearHostIP string
+}
+
+// CreateOptsBuilder builds the scheduler hints into a serializable format.
+type CreateOptsBuilder interface {
+	ToServerSchedulerHintsCreateMap() (map[string]interface{}, error)
+}
+
+// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format.
+func (opts SchedulerHints) ToServerSchedulerHintsCreateMap() (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) {
+			err := gophercloud.ErrInvalidInput{}
+			err.Argument = "schedulerhints.SchedulerHints.Group"
+			err.Value = opts.Group
+			err.Info = "Group must be a UUID"
+			return nil, err
+		}
+		sh["group"] = opts.Group
+	}
+
+	if len(opts.DifferentHost) > 0 {
+		for _, diffHost := range opts.DifferentHost {
+			if !uuidRegex.MatchString(diffHost) {
+				err := gophercloud.ErrInvalidInput{}
+				err.Argument = "schedulerhints.SchedulerHints.DifferentHost"
+				err.Value = opts.DifferentHost
+				err.Info = "The hosts must be in UUID format."
+				return nil, err
+			}
+		}
+		sh["different_host"] = opts.DifferentHost
+	}
+
+	if len(opts.SameHost) > 0 {
+		for _, sameHost := range opts.SameHost {
+			if !uuidRegex.MatchString(sameHost) {
+				err := gophercloud.ErrInvalidInput{}
+				err.Argument = "schedulerhints.SchedulerHints.SameHost"
+				err.Value = opts.SameHost
+				err.Info = "The hosts must be in UUID format."
+				return nil, err
+			}
+		}
+		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 {
+			err := gophercloud.ErrInvalidInput{}
+			err.Argument = "schedulerhints.SchedulerHints.Query"
+			err.Value = opts.Query
+			err.Info = "Must be a conditional statement in the format of [op,variable,value]"
+			return nil, err
+		}
+		sh["query"] = opts.Query
+	}
+
+	if opts.TargetCell != "" {
+		sh["target_cell"] = opts.TargetCell
+	}
+
+	if opts.BuildNearHostIP != "" {
+		if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil {
+			err := gophercloud.ErrInvalidInput{}
+			err.Argument = "schedulerhints.SchedulerHints.BuildNearHostIP"
+			err.Value = opts.BuildNearHostIP
+			err.Info = "Must be a valid subnet in the form 192.168.1.1/24"
+			return nil, err
+		}
+		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 CreateOptsBuilder
+}
+
+// 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.ToServerSchedulerHintsCreateMap()
+	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/testing/doc.go b/openstack/compute/v2/extensions/schedulerhints/testing/doc.go
new file mode 100644
index 0000000..0640b5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_schedulerhints_v2
+package testing
diff --git a/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go b/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go
new file mode 100644
index 0000000..cf21c4e
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/testing/requests_test.go
@@ -0,0 +1,127 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+	base := servers.CreateOpts{
+		Name:      "createdserver",
+		ImageRef:  "asdfasdfasdf",
+		FlavorRef: "performance1-1",
+	}
+
+	schedulerHints := 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 := schedulerhints.CreateOptsExt{
+		CreateOptsBuilder: base,
+		SchedulerHints:    schedulerHints,
+	}
+
+	expected := `
+		{
+			"server": {
+				"name": "createdserver",
+				"imageRef": "asdfasdfasdf",
+				"flavorRef": "performance1-1"
+			},
+			"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.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 := schedulerhints.CreateOptsExt{
+		CreateOptsBuilder: base,
+		SchedulerHints:    schedulerHints,
+	}
+
+	expected := `
+		{
+			"server": {
+				"name": "createdserver",
+				"imageRef": "asdfasdfasdf",
+				"flavorRef": "performance1-1"
+			},
+			"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/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
new file mode 100644
index 0000000..ec8019f
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -0,0 +1,171 @@
+package secgroups
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager {
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return SecurityGroupPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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 {
+	// the name of your security group.
+	Name string `json:"name" required:"true"`
+	// the description of your security group.
+	Description string `json:"description" required:"true"`
+}
+
+// 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)
+}
+
+// ToSecGroupCreateMap builds the create options into a serializable format.
+func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "security_group")
+}
+
+// Create will create a new security group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToSecGroupCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// 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) {
+	return gophercloud.BuildRequestBody(opts, "security_group")
+}
+
+// Update will modify the mutable properties of a security group, notably its
+// name and description.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToSecGroupUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(resourceURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Get will return details for a particular security group.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(resourceURL(client, id), &r.Body, nil)
+	return
+}
+
+// Delete will permanently delete a security group from the project.
+func Delete(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) {
+	_, r.Err = client.Delete(resourceURL(client, id), nil)
+	return
+}
+
+// CreateRuleOpts represents the configuration for adding a new rule to an
+// existing security group.
+type CreateRuleOpts struct {
+	// the ID of the group that this rule will be added to.
+	ParentGroupID string `json:"parent_group_id" required:"true"`
+	// the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+	// the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+	// the protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol" required:"true"`
+	// ONLY required if FromGroupID is blank. This represents the IP range that
+	// will be the source of network traffic to your security group. Use
+	// 0.0.0.0/0 to allow all IP addresses.
+	CIDR string `json:"cidr,omitempty" or:"FromGroupID"`
+	// ONLY required if CIDR is blank. This value represents the ID of a group
+	// that forwards traffic to the parent group. So, instead of accepting
+	// network traffic from an entire IP range, you can instead refine the
+	// inbound source by an existing security group.
+	FromGroupID string `json:"group_id,omitempty" or:"CIDR"`
+}
+
+// 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) {
+	return gophercloud.BuildRequestBody(opts, "security_group_rule")
+}
+
+// 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) (r CreateRuleResult) {
+	b, err := opts.ToRuleCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(rootRuleURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// DeleteRule will permanently delete a rule from a security group.
+func DeleteRule(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) {
+	_, r.Err = client.Delete(resourceRuleURL(client, id), nil)
+	return
+}
+
+func actionMap(prefix, groupName string) map[string]map[string]string {
+	return map[string]map[string]string{
+		prefix + "SecurityGroup": map[string]string{"name": groupName},
+	}
+}
+
+// AddServer will associate a server and a security group, enforcing the
+// rules of the group on the server.
+func AddServer(client *gophercloud.ServiceClient, serverID, groupName string) (r gophercloud.ErrResult) {
+	_, r.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), &r.Body, nil)
+	return
+}
+
+// RemoveServer will disassociate a server from a security group.
+func RemoveServer(client *gophercloud.ServiceClient, serverID, groupName string) (r gophercloud.ErrResult) {
+	_, r.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), &r.Body, nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go
new file mode 100644
index 0000000..f49338a
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/results.go
@@ -0,0 +1,184 @@
+package secgroups
+
+import (
+	"encoding/json"
+	"strconv"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"-"`
+
+	// The human-readable name of the group, which needs to be unique.
+	Name string `json:"name"`
+
+	// The human-readable description of the group.
+	Description string `json:"description"`
+
+	// The rules which determine how this security group operates.
+	Rules []Rule `json:"rules"`
+
+	// The ID of the tenant to which this security group belongs.
+	TenantID string `json:"tenant_id"`
+}
+
+func (r *SecurityGroup) UnmarshalJSON(b []byte) error {
+	type tmp SecurityGroup
+	var s struct {
+		tmp
+		ID interface{} `json:"id"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = SecurityGroup(s.tmp)
+
+	switch t := s.ID.(type) {
+	case float64:
+		r.ID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ID = t
+	}
+
+	return err
+}
+
+// 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 `json:"-"`
+
+	// The lower bound of the port range which this security group should open up
+	FromPort int `json:"from_port"`
+
+	// The upper bound of the port range which this security group should open up
+	ToPort int `json:"to_port"`
+
+	// The IP protocol (e.g. TCP) which the security group accepts
+	IPProtocol string `json:"ip_protocol"`
+
+	// The CIDR IP range whose traffic can be received
+	IPRange IPRange `json:"ip_range"`
+
+	// The security group ID to which this rule belongs
+	ParentGroupID string `json:"parent_group_id"`
+
+	// Not documented.
+	Group Group
+}
+
+func (r *Rule) UnmarshalJSON(b []byte) error {
+	type tmp Rule
+	var s struct {
+		tmp
+		ID            interface{} `json:"id"`
+		ParentGroupID interface{} `json:"parent_group_id"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Rule(s.tmp)
+
+	switch t := s.ID.(type) {
+	case float64:
+		r.ID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ID = t
+	}
+
+	switch t := s.ParentGroupID.(type) {
+	case float64:
+		r.ParentGroupID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		r.ParentGroupID = t
+	}
+
+	return err
+}
+
+// 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 `json:"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)
+	return len(users) == 0, err
+}
+
+// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results.
+func ExtractSecurityGroups(r pagination.Page) ([]SecurityGroup, error) {
+	var s struct {
+		SecurityGroups []SecurityGroup `json:"security_groups"`
+	}
+	err := (r.(SecurityGroupPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		SecurityGroup *SecurityGroup `json:"security_group"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Rule *Rule `json:"security_group_rule"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/secgroups/testing/doc.go b/openstack/compute/v2/extensions/secgroups/testing/doc.go
new file mode 100644
index 0000000..fbf4613
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_secgroups_v2
+package testing
diff --git a/openstack/compute/v2/extensions/secgroups/testing/fixtures.go b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
new file mode 100644
index 0000000..536e7f8
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
@@ -0,0 +1,328 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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": %d
+	}
+}
+		`, groupID)
+	})
+}
+
+func mockGetNumericIDGroupRuleResponse(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": %d,
+    "rules": [
+      {
+        "parent_group_id": %d,
+        "id": %d
+      }
+    ]
+  }
+}
+		`, groupID, groupID, groupID)
+	})
+}
+
+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 mockAddRuleResponseICMPZero(t *testing.T) {
+	th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "security_group_rule": {
+    "from_port": 0,
+    "ip_protocol": "ICMP",
+    "to_port": 0,
+    "parent_group_id": "{groupID}",
+    "cidr": "0.0.0.0/0"
+  }
+}	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_rule": {
+    "from_port": 0,
+    "group": {},
+    "ip_protocol": "ICMP",
+    "to_port": 0,
+    "parent_group_id": "{groupID}",
+    "ip_range": {
+      "cidr": "0.0.0.0/0"
+    },
+    "id": "{ruleID}"
+  }
+}`)
+	})
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+	url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
+	th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+		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/testing/requests_test.go b/openstack/compute/v2/extensions/secgroups/testing/requests_test.go
new file mode 100644
index 0000000..b520764
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/testing/requests_test.go
@@ -0,0 +1,302 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 := secgroups.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := secgroups.ExtractSecurityGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []secgroups.SecurityGroup{
+			{
+				ID:          groupID,
+				Description: "default",
+				Name:        "default",
+				Rules:       []secgroups.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 := secgroups.ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := secgroups.ExtractSecurityGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []secgroups.SecurityGroup{
+			{
+				ID:          groupID,
+				Description: "default",
+				Name:        "default",
+				Rules:       []secgroups.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 := secgroups.CreateOpts{
+		Name:        "test",
+		Description: "something",
+	}
+
+	group, err := secgroups.Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.SecurityGroup{
+		ID:          groupID,
+		Name:        "test",
+		Description: "something",
+		TenantID:    "openstack",
+		Rules:       []secgroups.Rule{},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateGroupResponse(t, groupID)
+
+	opts := secgroups.UpdateOpts{
+		Name:        "new_name",
+		Description: "new_desc",
+	}
+
+	group, err := secgroups.Update(client.ServiceClient(), groupID, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.SecurityGroup{
+		ID:          groupID,
+		Name:        "new_name",
+		Description: "something",
+		TenantID:    "openstack",
+		Rules:       []secgroups.Rule{},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetGroupsResponse(t, groupID)
+
+	group, err := secgroups.Get(client.ServiceClient(), groupID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.SecurityGroup{
+		ID:          groupID,
+		Description: "default",
+		Name:        "default",
+		TenantID:    "openstack",
+		Rules: []secgroups.Rule{
+			{
+				FromPort:      80,
+				ToPort:        85,
+				IPProtocol:    "TCP",
+				IPRange:       secgroups.IPRange{CIDR: "0.0.0.0"},
+				Group:         secgroups.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 := secgroups.Get(client.ServiceClient(), "12345").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.SecurityGroup{ID: "12345"}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGetNumericRuleID(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	numericGroupID := 12345
+
+	mockGetNumericIDGroupRuleResponse(t, numericGroupID)
+
+	group, err := secgroups.Get(client.ServiceClient(), "12345").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.SecurityGroup{
+		ID: "12345",
+		Rules: []secgroups.Rule{
+			{
+				ParentGroupID: "12345",
+				ID:            "12345",
+			},
+		},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteGroupResponse(t, groupID)
+
+	err := secgroups.Delete(client.ServiceClient(), groupID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAddRule(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddRuleResponse(t)
+
+	opts := secgroups.CreateRuleOpts{
+		ParentGroupID: groupID,
+		FromPort:      22,
+		ToPort:        22,
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := secgroups.CreateRule(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.Rule{
+		FromPort:      22,
+		ToPort:        22,
+		Group:         secgroups.Group{},
+		IPProtocol:    "TCP",
+		ParentGroupID: groupID,
+		IPRange:       secgroups.IPRange{CIDR: "0.0.0.0/0"},
+		ID:            ruleID,
+	}
+
+	th.AssertDeepEquals(t, expected, rule)
+}
+
+func TestAddRuleICMPZero(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddRuleResponseICMPZero(t)
+
+	opts := secgroups.CreateRuleOpts{
+		ParentGroupID: groupID,
+		FromPort:      0,
+		ToPort:        0,
+		IPProtocol:    "ICMP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := secgroups.CreateRule(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &secgroups.Rule{
+		FromPort:      0,
+		ToPort:        0,
+		Group:         secgroups.Group{},
+		IPProtocol:    "ICMP",
+		ParentGroupID: groupID,
+		IPRange:       secgroups.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 := secgroups.DeleteRule(client.ServiceClient(), ruleID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAddServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddServerToGroupResponse(t, serverID)
+
+	err := secgroups.AddServer(client.ServiceClient(), serverID, "test").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestRemoveServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockRemoveServerFromGroupResponse(t, serverID)
+
+	err := secgroups.RemoveServer(client.ServiceClient(), serverID, "test").ExtractErr()
+	th.AssertNoErr(t, 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..d99746c
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/urls.go
@@ -0,0 +1,32 @@
+package secgroups
+
+import "github.com/gophercloud/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/requests.go b/openstack/compute/v2/extensions/servergroups/requests.go
new file mode 100644
index 0000000..ee98837
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/requests.go
@@ -0,0 +1,57 @@
+package servergroups
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 ServerGroupPage{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 `json:"name" required:"true"`
+	// Policies are the server group policies
+	Policies []string `json:"policies" required:"true"`
+}
+
+// ToServerGroupCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToServerGroupCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "server_group")
+}
+
+// Create requests the creation of a new Server Group
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToServerGroupCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Get returns data about a previously created ServerGroup.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// Delete requests the deletion of a previously allocated ServerGroup.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/servergroups/results.go b/openstack/compute/v2/extensions/servergroups/results.go
new file mode 100644
index 0000000..ab49b35
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/results.go
@@ -0,0 +1,78 @@
+package servergroups
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"id"`
+
+	// Name is the common name of the server group.
+	Name string `json:"name"`
+
+	// Polices are the group policies.
+	Policies []string `json:"policies"`
+
+	// Members are the members of the server group.
+	Members []string `json:"members"`
+
+	// Metadata includes a list of all user-specified key-value pairs attached to the Server Group.
+	Metadata map[string]interface{}
+}
+
+// ServerGroupPage stores a single, only page of ServerGroups
+// results from a List call.
+type ServerGroupPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a ServerGroupsPage is empty.
+func (page ServerGroupPage) 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(r pagination.Page) ([]ServerGroup, error) {
+	var s struct {
+		ServerGroups []ServerGroup `json:"server_groups"`
+	}
+	err := (r.(ServerGroupPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		ServerGroup *ServerGroup `json:"server_group"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/compute/v2/extensions/servergroups/testing/doc.go
new file mode 100644
index 0000000..65433f7
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_servergroups_v2
+package testing
diff --git a/openstack/compute/v2/extensions/servergroups/testing/fixtures.go b/openstack/compute/v2/extensions/servergroups/testing/fixtures.go
new file mode 100644
index 0000000..b53757a
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/testing/fixtures.go
@@ -0,0 +1,160 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 = servergroups.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 = servergroups.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 = []servergroups.ServerGroup{FirstServerGroup, SecondServerGroup}
+
+// CreatedServerGroup is the parsed result from CreateOutput.
+var CreatedServerGroup = servergroups.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/testing/requests_test.go b/openstack/compute/v2/extensions/servergroups/testing/requests_test.go
new file mode 100644
index 0000000..d86fa56
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/testing/requests_test.go
@@ -0,0 +1,60 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := servergroups.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := servergroups.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 := servergroups.Create(client.ServiceClient(), servergroups.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 := servergroups.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 := servergroups.Delete(client.ServiceClient(), "616fb98f-46ca-475e-917e-2563e5a8cd19").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/servergroups/urls.go b/openstack/compute/v2/extensions/servergroups/urls.go
new file mode 100644
index 0000000..9a1f99b
--- /dev/null
+++ b/openstack/compute/v2/extensions/servergroups/urls.go
@@ -0,0 +1,25 @@
+package servergroups
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/compute/v2/extensions/startstop/requests.go
new file mode 100644
index 0000000..1d8a593
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests.go
@@ -0,0 +1,19 @@
+package startstop
+
+import "github.com/gophercloud/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) (r gophercloud.ErrResult) {
+	_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil)
+	return
+}
+
+// Stop is the operation responsible for stopping a Compute server.
+func Stop(client *gophercloud.ServiceClient, id string) (r gophercloud.ErrResult) {
+	_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/startstop/testing/doc.go b/openstack/compute/v2/extensions/startstop/testing/doc.go
new file mode 100644
index 0000000..6135475
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_startstop_v2
+package testing
diff --git a/openstack/compute/v2/extensions/startstop/testing/fixtures.go b/openstack/compute/v2/extensions/startstop/testing/fixtures.go
new file mode 100644
index 0000000..1086b0e
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/testing/fixtures.go
@@ -0,0 +1,27 @@
+package testing
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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/testing/requests_test.go b/openstack/compute/v2/extensions/startstop/testing/requests_test.go
new file mode 100644
index 0000000..be45bf5
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/testing/requests_test.go
@@ -0,0 +1,31 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const serverID = "{serverId}"
+
+func TestStart(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockStartServerResponse(t, serverID)
+
+	err := startstop.Start(client.ServiceClient(), serverID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestStop(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockStopServerResponse(t, serverID)
+
+	err := startstop.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/requests.go b/openstack/compute/v2/extensions/tenantnetworks/requests.go
new file mode 100644
index 0000000..82836d4
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/requests.go
@@ -0,0 +1,19 @@
+package tenantnetworks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of Network.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get returns data about a previously created Network.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/tenantnetworks/results.go b/openstack/compute/v2/extensions/tenantnetworks/results.go
new file mode 100644
index 0000000..88cbc80
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/results.go
@@ -0,0 +1,59 @@
+package tenantnetworks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// A Network represents a nova-network that an instance communicates on
+type Network struct {
+	// CIDR is the IPv4 subnet.
+	CIDR string `json:"cidr"`
+
+	// ID is the UUID of the network.
+	ID string `json:"id"`
+
+	// Name is the common name that the network has.
+	Name string `json:"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(r pagination.Page) ([]Network, error) {
+	var s struct {
+		Networks []Network `json:"networks"`
+	}
+	err := (r.(NetworkPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Network *Network `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go
new file mode 100644
index 0000000..7ed7ce3
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_tenantnetworks_v2
+package testing
diff --git a/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go b/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go
new file mode 100644
index 0000000..ae679b4
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/testing/fixtures.go
@@ -0,0 +1,83 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 = tenantnetworks.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 = tenantnetworks.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 = []tenantnetworks.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/testing/requests_test.go b/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go
new file mode 100644
index 0000000..703c846
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/testing/requests_test.go
@@ -0,0 +1,38 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := tenantnetworks.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := tenantnetworks.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 := tenantnetworks.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/urls.go b/openstack/compute/v2/extensions/tenantnetworks/urls.go
new file mode 100644
index 0000000..683041d
--- /dev/null
+++ b/openstack/compute/v2/extensions/tenantnetworks/urls.go
@@ -0,0 +1,17 @@
+package tenantnetworks
+
+import "github.com/gophercloud/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/testing/delegate_test.go b/openstack/compute/v2/extensions/testing/delegate_test.go
new file mode 100644
index 0000000..822093f
--- /dev/null
+++ b/openstack/compute/v2/extensions/testing/delegate_test.go
@@ -0,0 +1,56 @@
+package testing
+
+import (
+	"testing"
+
+	common "github.com/gophercloud/gophercloud/openstack/common/extensions"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleListExtensionsSuccessfully(t)
+
+	count := 0
+	extensions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := extensions.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()
+
+	HandleGetExtensionsSuccessfully(t)
+
+	ext, err := extensions.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/testing/doc.go b/openstack/compute/v2/extensions/testing/doc.go
new file mode 100644
index 0000000..5818711
--- /dev/null
+++ b/openstack/compute/v2/extensions/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_extensions_v2
+package testing
diff --git a/openstack/compute/v2/extensions/testing/fixtures.go b/openstack/compute/v2/extensions/testing/fixtures.go
new file mode 100644
index 0000000..2a3fb69
--- /dev/null
+++ b/openstack/compute/v2/extensions/testing/fixtures.go
@@ -0,0 +1,57 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+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": [
+				{
+						"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"
+				}
+		]
+}
+			`)
+	})
+}
+
+func HandleGetExtensionsSuccessfully(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, `
+{
+		"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."
+		}
+}
+		`)
+	})
+}
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..ee4d62d
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/requests.go
@@ -0,0 +1,57 @@
+package volumeattach
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 VolumeAttachmentPage{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 `json:"device,omitempty"`
+	// VolumeID is the ID of the volume to attach to the instance
+	VolumeID string `json:"volumeId" required:"true"`
+}
+
+// ToVolumeAttachmentCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToVolumeAttachmentCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "volumeAttachment")
+}
+
+// Create requests the creation of a new volume attachment on the server
+func Create(client *gophercloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToVolumeAttachmentCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Get returns public data about a previously created VolumeAttachment.
+func Get(client *gophercloud.ServiceClient, serverID, attachmentID string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, serverID, attachmentID), &r.Body, nil)
+	return
+}
+
+// Delete requests the deletion of a previous stored VolumeAttachment from the server.
+func Delete(client *gophercloud.ServiceClient, serverID, attachmentID string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, serverID, attachmentID), nil)
+	return
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/results.go b/openstack/compute/v2/extensions/volumeattach/results.go
new file mode 100644
index 0000000..53faf5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/results.go
@@ -0,0 +1,76 @@
+package volumeattach
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// VolumeAttachment controls the attachment of a volume to an instance.
+type VolumeAttachment struct {
+	// ID is a unique id of the attachment
+	ID string `json:"id"`
+
+	// Device is what device the volume is attached as
+	Device string `json:"device"`
+
+	// VolumeID is the ID of the attached volume
+	VolumeID string `json:"volumeId"`
+
+	// ServerID is the ID of the instance that has the volume attached
+	ServerID string `json:"serverId"`
+}
+
+// VolumeAttachmentPage stores a single, only page of VolumeAttachments
+// results from a List call.
+type VolumeAttachmentPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a VolumeAttachmentsPage is empty.
+func (page VolumeAttachmentPage) 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(r pagination.Page) ([]VolumeAttachment, error) {
+	var s struct {
+		VolumeAttachments []VolumeAttachment `json:"volumeAttachments"`
+	}
+	err := (r.(VolumeAttachmentPage)).ExtractInto(&s)
+	return s.VolumeAttachments, err
+}
+
+// VolumeAttachmentResult is the result from a volume attachment operation.
+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) {
+	var s struct {
+		VolumeAttachment *VolumeAttachment `json:"volumeAttachment"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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..2cc0ab4
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/testing/doc.go
@@ -0,0 +1,9 @@
+// compute_extensions_volumeattach_v2
+package testing
+
+/*
+Package testing holds fixtures (which imports testing),
+so that importing volumeattach package does not inadvertently import testing into production code
+More information here:
+https://github.com/gophercloud/gophercloud/issues/473
+*/
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..4f99610
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/testing/fixtures.go
@@ -0,0 +1,108 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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/testing/requests_test.go b/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go
new file mode 100644
index 0000000..9486f9b
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/testing/requests_test.go
@@ -0,0 +1,102 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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()
+
+	HandleListSuccessfully(t)
+
+	serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	count := 0
+	err := volumeattach.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()
+
+	HandleCreateSuccessfully(t)
+
+	serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	actual, err := volumeattach.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()
+
+	HandleGetSuccessfully(t)
+
+	aID := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+	serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	actual, err := volumeattach.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()
+
+	HandleDeleteSuccessfully(t)
+
+	aID := "a26887c6-c47b-4654-abb5-dfadf7d3f804"
+	serverID := "4d8c3732-a248-40ed-bebc-539a6ffd25c0"
+
+	err := volumeattach.Delete(client.ServiceClient(), serverID, aID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/volumeattach/urls.go b/openstack/compute/v2/extensions/volumeattach/urls.go
new file mode 100644
index 0000000..083f8dc
--- /dev/null
+++ b/openstack/compute/v2/extensions/volumeattach/urls.go
@@ -0,0 +1,25 @@
+package volumeattach
+
+import "github.com/gophercloud/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/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..d5d571c
--- /dev/null
+++ b/openstack/compute/v2/flavors/requests.go
@@ -0,0 +1,141 @@
+package flavors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+type CreateOptsBuilder interface {
+	ToFlavorCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is passed to Create to create a flavor
+// Source:
+// https://github.com/openstack/nova/blob/stable/newton/nova/api/openstack/compute/schemas/flavor_manage.py#L20
+type CreateOpts struct {
+	Name string `json:"name" required:"true"`
+	// memory size, in MBs
+	RAM   int `json:"ram" required:"true"`
+	VCPUs int `json:"vcpus" required:"true"`
+	// disk size, in GBs
+	Disk *int   `json:"disk" required:"true"`
+	ID   string `json:"id,omitempty"`
+	// non-zero, positive
+	Swap       *int    `json:"swap,omitempty"`
+	RxTxFactor float64 `json:"rxtx_factor,omitempty"`
+	IsPublic   *bool   `json:"os-flavor-access:is_public,omitempty"`
+	// ephemeral disk size, in GBs, non-zero, positive
+	Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"`
+}
+
+// ToFlavorCreateMap satisfies the CreateOptsBuilder interface
+func (opts *CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "flavor")
+}
+
+// Create a flavor
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToFlavorCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// 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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// IDFromName is a convienience function that returns a flavor's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	allPages, err := ListDetail(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractFlavors(allPages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, f := range all {
+		if f.Name == name {
+			count++
+			id = f.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		err := &gophercloud.ErrResourceNotFound{}
+		err.ResourceType = "flavor"
+		err.Name = name
+		return "", err
+	case 1:
+		return id, nil
+	default:
+		err := &gophercloud.ErrMultipleResourcesFound{}
+		err.ResourceType = "flavor"
+		err.Name = name
+		err.Count = count
+		return "", err
+	}
+}
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
new file mode 100644
index 0000000..18b8434
--- /dev/null
+++ b/openstack/compute/v2/flavors/results.go
@@ -0,0 +1,113 @@
+package flavors
+
+import (
+	"encoding/json"
+	"strconv"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult temporarily holds the response from a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// Extract provides access to the individual Flavor returned by the Get and Create functions.
+func (r commonResult) Extract() (*Flavor, error) {
+	var s struct {
+		Flavor *Flavor `json:"flavor"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 `json:"id"`
+	// The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
+	Disk int `json:"disk"`
+	RAM  int `json:"ram"`
+	// The Name field provides a human-readable moniker for the flavor.
+	Name       string  `json:"name"`
+	RxTxFactor float64 `json:"rxtx_factor"`
+	// Swap indicates how much space is reserved for swap.
+	// If not provided, this field will be set to 0.
+	Swap int `json:"swap"`
+	// VCPUs indicates how many (virtual) CPUs are available for this flavor.
+	VCPUs int `json:"vcpus"`
+}
+
+func (r *Flavor) UnmarshalJSON(b []byte) error {
+	type tmp Flavor
+	var s struct {
+		tmp
+		Swap interface{} `json:"swap"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Flavor(s.tmp)
+
+	switch t := s.Swap.(type) {
+	case float64:
+		r.Swap = int(t)
+	case string:
+		switch t {
+		case "":
+			r.Swap = 0
+		default:
+			swap, err := strconv.ParseFloat(t, 64)
+			if err != nil {
+				return err
+			}
+			r.Swap = int(swap)
+		}
+	}
+
+	return nil
+}
+
+// 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 (page FlavorPage) IsEmpty() (bool, error) {
+	flavors, err := ExtractFlavors(page)
+	return len(flavors) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page FlavorPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"flavors_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
+	var s struct {
+		Flavors []Flavor `json:"flavors"`
+	}
+	err := (r.(FlavorPage)).ExtractInto(&s)
+	return s.Flavors, err
+}
diff --git a/openstack/compute/v2/flavors/testing/doc.go b/openstack/compute/v2/flavors/testing/doc.go
new file mode 100644
index 0000000..0d00761
--- /dev/null
+++ b/openstack/compute/v2/flavors/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_flavors_v2
+package testing
diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go
new file mode 100644
index 0000000..f4f1c8d
--- /dev/null
+++ b/openstack/compute/v2/flavors/testing/requests_test.go
@@ -0,0 +1,186 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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,
+								"swap":""
+							},
+							{
+								"id": "2",
+								"name": "m2.small",
+								"disk": 10,
+								"ram": 1024,
+								"vcpus": 2,
+								"swap": 1000
+							}
+						],
+						"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 := flavors.ListDetail(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := flavors.ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []flavors.Flavor{
+			{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1, Swap: 0},
+			{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2, Swap: 1000},
+		}
+
+		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,
+					"swap": ""
+				}
+			}
+		`)
+	})
+
+	actual, err := flavors.Get(fake.ServiceClient(), "12345").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get flavor: %v", err)
+	}
+
+	expected := &flavors.Flavor{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       1,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1,
+		Swap:       0,
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but was %#v", expected, actual)
+	}
+}
+
+func TestCreateFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors", 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")
+		fmt.Fprintf(w, `
+			{
+				"flavor": {
+					"id": "1",
+					"name": "m1.tiny",
+					"disk": 1,
+					"ram": 512,
+					"vcpus": 1,
+					"rxtx_factor": 1,
+					"swap": ""
+				}
+			}
+		`)
+	})
+
+	disk := 1
+	opts := &flavors.CreateOpts{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       &disk,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1.0,
+	}
+	actual, err := flavors.Create(fake.ServiceClient(), opts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create flavor: %v", err)
+	}
+
+	expected := &flavors.Flavor{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       1,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1,
+		Swap:       0,
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but was %#v", expected, actual)
+	}
+}
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
new file mode 100644
index 0000000..2fc2179
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls.go
@@ -0,0 +1,17 @@
+package flavors
+
+import (
+	"github.com/gophercloud/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")
+}
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("flavors")
+}
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..df9f1da
--- /dev/null
+++ b/openstack/compute/v2/images/requests.go
@@ -0,0 +1,102 @@
+package images
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ImagePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// Delete deletes the specified image ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// IDFromName is a convienience function that returns an image's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	allPages, err := ListDetail(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractImages(allPages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, f := range all {
+		if f.Name == name {
+			count++
+			id = f.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		err := &gophercloud.ErrResourceNotFound{}
+		err.ResourceType = "image"
+		err.Name = name
+		return "", err
+	case 1:
+		return id, nil
+	default:
+		err := &gophercloud.ErrMultipleResourcesFound{}
+		err.ResourceType = "image"
+		err.Name = name
+		err.Count = count
+		return "", err
+	}
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..f9ebc69
--- /dev/null
+++ b/openstack/compute/v2/images/results.go
@@ -0,0 +1,83 @@
+package images
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 (r GetResult) Extract() (*Image, error) {
+	var s struct {
+		Image *Image `json:"image"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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
+
+	Metadata map[string]interface{}
+}
+
+// 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)
+	return len(images) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ImagePage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"images_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractImages converts a page of List results into a slice of usable Image structs.
+func ExtractImages(r pagination.Page) ([]Image, error) {
+	var s struct {
+		Images []Image `json:"images"`
+	}
+	err := (r.(ImagePage)).ExtractInto(&s)
+	return s.Images, err
+}
diff --git a/openstack/compute/v2/images/testing/doc.go b/openstack/compute/v2/images/testing/doc.go
new file mode 100644
index 0000000..6f59ade
--- /dev/null
+++ b/openstack/compute/v2/images/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_images_v2
+package testing
diff --git a/openstack/compute/v2/images/testing/requests_test.go b/openstack/compute/v2/images/testing/requests_test.go
new file mode 100644
index 0000000..1de0303
--- /dev/null
+++ b/openstack/compute/v2/images/testing/requests_test.go
@@ -0,0 +1,225 @@
+package testing
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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": {
+								"architecture": "x86_64",
+								"block_device_mapping": {
+									"guest_format": null,
+									"boot_index": 0,
+									"device_name": "/dev/vda",
+									"delete_on_termination": false
+								}
+              }
+						},
+						{
+							"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
+						}
+					]
+				}
+			`)
+		case "2":
+			fmt.Fprintf(w, `{ "images": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	pages := 0
+	options := &images.ListOpts{Limit: 2}
+	err := images.ListDetail(fake.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := images.ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []images.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",
+				Metadata: map[string]interface{}{
+					"architecture": "x86_64",
+					"block_device_mapping": map[string]interface{}{
+						"guest_format":          interface{}(nil),
+						"boot_index":            float64(0),
+						"device_name":           "/dev/vda",
+						"delete_on_termination": false,
+					},
+				},
+			},
+			{
+				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": {
+						"architecture": "x86_64",
+						"block_device_mapping": {
+							"guest_format": null,
+							"boot_index": 0,
+							"device_name": "/dev/vda",
+							"delete_on_termination": false
+						}
+					}
+				}
+			}
+		`)
+	})
+
+	actual, err := images.Get(fake.ServiceClient(), "12345678").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Get: %v", err)
+	}
+
+	expected := &images.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,
+		Metadata: map[string]interface{}{
+			"architecture": "x86_64",
+			"block_device_mapping": map[string]interface{}{
+				"guest_format":          interface{}(nil),
+				"boot_index":            float64(0),
+				"device_name":           "/dev/vda",
+				"delete_on_termination": false,
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but got %#v", expected, actual)
+	}
+}
+
+func TestNextPageURL(t *testing.T) {
+	var page images.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 := images.Delete(fake.ServiceClient(), "12345678")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go
new file mode 100644
index 0000000..57787fb
--- /dev/null
+++ b/openstack/compute/v2/images/urls.go
@@ -0,0 +1,15 @@
+package images
+
+import "github.com/gophercloud/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/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/errors.go b/openstack/compute/v2/servers/errors.go
new file mode 100644
index 0000000..c9f0e3c
--- /dev/null
+++ b/openstack/compute/v2/servers/errors.go
@@ -0,0 +1,71 @@
+package servers
+
+import (
+	"fmt"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// ErrNeitherImageIDNorImageNameProvided is the error when neither the image
+// ID nor the image name is provided for a server operation
+type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput }
+
+func (e ErrNeitherImageIDNorImageNameProvided) Error() string {
+	return "One and only one of the image ID and the image name must be provided."
+}
+
+// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor
+// ID nor the flavor name is provided for a server operation
+type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput }
+
+func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string {
+	return "One and only one of the flavor ID and the flavor name must be provided."
+}
+
+type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput }
+
+func (e ErrNoClientProvidedForIDByName) Error() string {
+	return "A service client must be provided to find a resource ID by name."
+}
+
+// ErrInvalidHowParameterProvided is the error when an unknown value is given
+// for the `how` argument
+type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput }
+
+// ErrNoAdminPassProvided is the error when an administrative password isn't
+// provided for a server operation
+type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput }
+
+// ErrNoImageIDProvided is the error when an image ID isn't provided for a server
+// operation
+type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput }
+
+// ErrNoIDProvided is the error when a server ID isn't provided for a server
+// operation
+type ErrNoIDProvided struct{ gophercloud.ErrMissingInput }
+
+// ErrServer is a generic error type for servers HTTP operations.
+type ErrServer struct {
+	gophercloud.ErrUnexpectedResponseCode
+	ID string
+}
+
+func (se ErrServer) Error() string {
+	return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID)
+}
+
+// Error404 overrides the generic 404 error message.
+func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error {
+	se.ErrUnexpectedResponseCode = e
+	return &ErrServerNotFound{se}
+}
+
+// ErrServerNotFound is the error when a 404 is received during server HTTP
+// operations.
+type ErrServerNotFound struct {
+	ErrServer
+}
+
+func (e ErrServerNotFound) Error() string {
+	return fmt.Sprintf("I couldn't find server [%s]", e.ID)
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
new file mode 100644
index 0000000..9618637
--- /dev/null
+++ b/openstack/compute/v2/servers/requests.go
@@ -0,0 +1,741 @@
+package servers
+
+import (
+	"encoding/base64"
+	"encoding/json"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
+	"github.com/gophercloud/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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ServerPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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 is the name to assign to the newly launched server.
+	Name string `json:"name" required:"true"`
+
+	// 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 `json:"imageRef"`
+
+	// 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 `json:"-"`
+
+	// 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 `json:"flavorRef"`
+
+	// FlavorName [optional; required if FlavorRef is not provided] is the name of
+	// the flavor that describes the server's specs.
+	FlavorName string `json:"-"`
+
+	// SecurityGroups lists the names of the security groups to which this server should belong.
+	SecurityGroups []string `json:"-"`
+
+	// UserData contains configuration information or scripts to use upon launch.
+	// Create will base64-encode it for you, if it isn't already.
+	UserData []byte `json:"-"`
+
+	// AvailabilityZone in which to launch the server.
+	AvailabilityZone string `json:"availability_zone,omitempty"`
+
+	// Networks 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 `json:"-"`
+
+	// Metadata contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string `json:"metadata,omitempty"`
+
+	// Personality includes files to inject into the server at launch.
+	// Create will base64-encode file contents for you.
+	Personality Personality `json:"personality,omitempty"`
+
+	// ConfigDrive enables metadata injection through a configuration drive.
+	ConfigDrive *bool `json:"config_drive,omitempty"`
+
+	// AdminPass sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the rponse.
+	AdminPass string `json:"adminPass,omitempty"`
+
+	// AccessIPv4 specifies an IPv4 address for the instance.
+	AccessIPv4 string `json:"accessIPv4,omitempty"`
+
+	// AccessIPv6 pecifies an IPv6 address for the instance.
+	AccessIPv6 string `json:"accessIPv6,omitempty"`
+
+	// ServiceClient will allow calls to be made to retrieve an image or
+	// flavor ID by name.
+	ServiceClient *gophercloud.ServiceClient `json:"-"`
+}
+
+// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
+	sc := opts.ServiceClient
+	opts.ServiceClient = nil
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	if opts.UserData != nil {
+		var userData string
+		if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil {
+			userData = base64.StdEncoding.EncodeToString(opts.UserData)
+		} else {
+			userData = string(opts.UserData)
+		}
+		b["user_data"] = &userData
+	}
+
+	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}
+		}
+		b["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
+			}
+		}
+		b["networks"] = networks
+	}
+
+	// If ImageRef isn't provided, check if ImageName was provided to ascertain
+	// the image ID.
+	if opts.ImageRef == "" {
+		if opts.ImageName != "" {
+			if sc == nil {
+				err := ErrNoClientProvidedForIDByName{}
+				err.Argument = "ServiceClient"
+				return nil, err
+			}
+			imageID, err := images.IDFromName(sc, opts.ImageName)
+			if err != nil {
+				return nil, err
+			}
+			b["imageRef"] = imageID
+		}
+	}
+
+	// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
+	if opts.FlavorRef == "" {
+		if opts.FlavorName == "" {
+			err := ErrNeitherFlavorIDNorFlavorNameProvided{}
+			err.Argument = "FlavorRef/FlavorName"
+			return nil, err
+		}
+		if sc == nil {
+			err := ErrNoClientProvidedForIDByName{}
+			err.Argument = "ServiceClient"
+			return nil, err
+		}
+		flavorID, err := flavors.IDFromName(sc, opts.FlavorName)
+		if err != nil {
+			return nil, err
+		}
+		b["flavorRef"] = flavorID
+	}
+
+	return map[string]interface{}{"server": b}, nil
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	reqBody, err := opts.ToServerCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil)
+	return
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// ForceDelete forces the deletion of a server
+func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) {
+	_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil)
+	return
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 203},
+	})
+	return
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToServerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts struct {
+	// Name 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 `json:"name,omitempty"`
+
+	// AccessIPv4 provides a new IPv4 address for the instance.
+	AccessIPv4 string `json:"accessIPv4,omitempty"`
+
+	// AccessIPv6 provides a new IPv6 address for the instance.
+	AccessIPv6 string `json:"accessIPv6,omitempty"`
+}
+
+// ToServerUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "server")
+}
+
+// Update requests that various attributes of the indicated server be changed.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToServerUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) {
+	b := map[string]interface{}{
+		"changePassword": map[string]string{
+			"adminPass": newPassword,
+		},
+	}
+	_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
+	return
+}
+
+// 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
+)
+
+// RebootOptsBuilder is an interface that options must satisfy in order to be
+// used when rebooting a server instance
+type RebootOptsBuilder interface {
+	ToServerRebootMap() (map[string]interface{}, error)
+}
+
+// RebootOpts satisfies the RebootOptsBuilder interface
+type RebootOpts struct {
+	Type RebootMethod `json:"type" required:"true"`
+}
+
+// ToServerRebootMap allows RebootOpts to satisfiy the RebootOptsBuilder
+// interface
+func (opts *RebootOpts) ToServerRebootMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "reboot")
+}
+
+// Reboot requests that a given server reboot.
+// Two methods exist for rebooting a server:
+//
+// HardReboot (aka PowerCycle) starts 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 rtored or the VM instance rtarted.
+//
+// SoftReboot (aka OSReboot) simply tells the OS to rtart under its own procedur.
+// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to rtart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) {
+	b, err := opts.ToServerRebootMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
+	return
+}
+
+// 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 {
+	// The server's admin password
+	AdminPass string `json:"adminPass,omitempty"`
+	// The ID of the image you want your server to be provisioned on
+	ImageID   string `json:"imageRef"`
+	ImageName string `json:"-"`
+	// Name to set the server to
+	Name string `json:"name,omitempty"`
+	// AccessIPv4 [optional] provides a new IPv4 address for the instance.
+	AccessIPv4 string `json:"accessIPv4,omitempty"`
+	// AccessIPv6 [optional] provides a new IPv6 address for the instance.
+	AccessIPv6 string `json:"accessIPv6,omitempty"`
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string `json:"metadata,omitempty"`
+	// Personality [optional] includes files to inject into the server at launch.
+	// Rebuild will base64-encode file contents for you.
+	Personality   Personality                `json:"personality,omitempty"`
+	ServiceClient *gophercloud.ServiceClient `json:"-"`
+}
+
+// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
+func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	// If ImageRef isn't provided, check if ImageName was provided to ascertain
+	// the image ID.
+	if opts.ImageID == "" {
+		if opts.ImageName != "" {
+			if opts.ServiceClient == nil {
+				err := ErrNoClientProvidedForIDByName{}
+				err.Argument = "ServiceClient"
+				return nil, err
+			}
+			imageID, err := images.IDFromName(opts.ServiceClient, opts.ImageName)
+			if err != nil {
+				return nil, err
+			}
+			b["imageRef"] = imageID
+		}
+	}
+
+	return map[string]interface{}{"rebuild": b}, 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) (r RebuildResult) {
+	b, err := opts.ToServerRebuildMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(actionURL(client, id), b, &r.Body, nil)
+	return
+}
+
+// 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 `json:"flavorRef" required:"true"`
+}
+
+// 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) {
+	return gophercloud.BuildRequestBody(opts, "resize")
+}
+
+// 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) (r ActionResult) {
+	b, err := opts.ToServerResizeMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
+	return
+}
+
+// ConfirmResize confirms a previous resize operation on a server.
+// See Resize() for more details.
+func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) {
+	_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{201, 202, 204},
+	})
+	return
+}
+
+// RevertResize cancels a previous resize operation on a server.
+// See Resize() for more details.
+func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) {
+	_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil)
+	return
+}
+
+// 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 `json:"adminPass,omitempty"`
+}
+
+// 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) {
+	return gophercloud.BuildRequestBody(opts, "rescue")
+}
+
+// Rescue instructs the provider to place the server into RESCUE mode.
+func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) (r RescueResult) {
+	b, err := opts.ToServerRescueMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// 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) (r ResetMetadataResult) {
+	b, err := opts.ToMetadataResetMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Metadata requests all the metadata for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) {
+	_, r.Err = client.Get(metadataURL(client, id), &r.Body, nil)
+	return
+}
+
+// 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) (r UpdateMetadataResult) {
+	b, err := opts.ToMetadataUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// 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 {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "servers.MetadatumOpts"
+		err.Info = "Must have 1 and only 1 key-value pair"
+		return nil, "", err
+	}
+	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) (r CreateMetadatumResult) {
+	b, key, err := opts.ToMetadatumCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Metadatum requests the key-value pair with the given key for the given server ID.
+func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) {
+	_, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil)
+	return
+}
+
+// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
+func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) {
+	_, r.Err = client.Delete(metadatumURL(client, id, key), nil)
+	return
+}
+
+// ListAddresses makes a request against the API to list the servers IP addresses.
+func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
+	return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page {
+		return AddressPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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 pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page {
+		return NetworkAddressPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// CreateImageOptsBuilder is the interface types must satisfy in order to be
+// used as CreateImage options
+type CreateImageOptsBuilder interface {
+	ToServerCreateImageMap() (map[string]interface{}, error)
+}
+
+// CreateImageOpts satisfies the CreateImageOptsBuilder
+type CreateImageOpts struct {
+	// Name of the image/snapshot
+	Name string `json:"name" required:"true"`
+	// Metadata contains key-value pairs (up to 255 bytes each) to attach to the created image.
+	Metadata map[string]string `json:"metadata,omitempty"`
+}
+
+// ToServerCreateImageMap formats a CreateImageOpts structure into a request body.
+func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "createImage")
+}
+
+// CreateImage makes a request against the nova API to schedule an image to be created of the server
+func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) {
+	b, err := opts.ToServerCreateImageMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	r.Err = err
+	r.Header = resp.Header
+	return
+}
+
+// IDFromName is a convienience function that returns a server's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	allPages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractServers(allPages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, f := range all {
+		if f.Name == name {
+			count++
+			id = f.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "server"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"}
+	}
+}
+
+// GetPassword makes a request against the nova API to get the encrypted administrative password.
+func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
+	_, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil)
+	return
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..1ae1e91
--- /dev/null
+++ b/openstack/compute/v2/servers/results.go
@@ -0,0 +1,350 @@
+package servers
+
+import (
+	"crypto/rsa"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"path"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type serverResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any serverResult as a Server, if possible.
+func (r serverResult) Extract() (*Server, error) {
+	var s Server
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+func (r serverResult) ExtractInto(v interface{}) error {
+	return r.Result.ExtractIntoStructPtr(v, "server")
+}
+
+func ExtractServersInto(r pagination.Page, v interface{}) error {
+	return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers")
+}
+
+// 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
+}
+
+// GetPasswordResult represent the result of a get os-server-password operation.
+type GetPasswordResult struct {
+	gophercloud.Result
+}
+
+// ExtractPassword gets the encrypted password.
+// If privateKey != nil the password is decrypted with the private key.
+// If privateKey == nil the encrypted password is returned and can be decrypted with:
+//   echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
+func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
+	var s struct {
+		Password string `json:"password"`
+	}
+	err := r.ExtractInto(&s)
+	if err == nil && privateKey != nil && s.Password != "" {
+		return decryptPassword(s.Password, privateKey)
+	}
+	return s.Password, err
+}
+
+func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
+	b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
+
+	n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
+	if err != nil {
+		return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
+	}
+	password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
+	if err != nil {
+		return "", fmt.Errorf("Failed to decrypt password: %s", err)
+	}
+
+	return string(password), nil
+}
+
+// ExtractImageID gets the ID of the newly created server image from the header
+func (r CreateImageResult) ExtractImageID() (string, error) {
+	if r.Err != nil {
+		return "", r.Err
+	}
+	// Get the image id from the header
+	u, err := url.ParseRequestURI(r.Header.Get("Location"))
+	if err != nil {
+		return "", err
+	}
+	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) {
+	var s struct {
+		AdminPass string `json:"adminPass"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 `json:"id"`
+	// TenantID identifies the tenant owning this server resource.
+	TenantID string `json:"tenant_id"`
+	// UserID uniquely identifies the user account owning the tenant.
+	UserID string `json:"user_id"`
+	// Name contains the human-readable name for the server.
+	Name string `json:"name"`
+	// Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
+	Updated time.Time `json:"updated"`
+	Created time.Time `json:"created"`
+	HostID  string    `json:"hostid"`
+	// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
+	Status string `json:"status"`
+	// Progress ranges from 0..100.
+	// A request made against the server completes only once Progress reaches 100.
+	Progress int `json:"progress"`
+	// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
+	AccessIPv4 string `json:"accessIPv4"`
+	AccessIPv6 string `json:"accessIPv6"`
+	// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
+	Image map[string]interface{} `json:"-"`
+	// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
+	Flavor map[string]interface{} `json:"flavor"`
+	// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
+	Addresses map[string]interface{} `json:"addresses"`
+	// Metadata includes a list of all user-specified key-value pairs attached to the server.
+	Metadata map[string]string `json:"metadata"`
+	// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+	Links []interface{} `json:"links"`
+	// KeyName indicates which public key was injected into the server on launch.
+	KeyName string `json:"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"`
+	// SecurityGroups includes the security groups that this instance has applied to it
+	SecurityGroups []map[string]interface{} `json:"security_groups"`
+}
+
+func (r *Server) UnmarshalJSON(b []byte) error {
+	type tmp Server
+	var s struct {
+		tmp
+		Image interface{} `json:"image"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Server(s.tmp)
+
+	switch t := s.Image.(type) {
+	case map[string]interface{}:
+		r.Image = t
+	case string:
+		switch t {
+		case "":
+			r.Image = nil
+		}
+	}
+
+	return err
+}
+
+// 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 (r ServerPage) IsEmpty() (bool, error) {
+	s, err := ExtractServers(r)
+	return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (r ServerPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"servers_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(r pagination.Page) ([]Server, error) {
+	var s []Server
+	err := ExtractServersInto(r, &s)
+	return s, 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) {
+	var s struct {
+		Metadata map[string]string `json:"metadata"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Metadata, err
+}
+
+// Extract interprets any MetadatumResult as a Metadatum, if possible.
+func (r MetadatumResult) Extract() (map[string]string, error) {
+	var s struct {
+		Metadatum map[string]string `json:"meta"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Metadatum, err
+}
+
+// Address represents an IP address.
+type Address struct {
+	Version int    `json:"version"`
+	Address string `json:"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)
+	return len(addresses) == 0, err
+}
+
+// ExtractAddresses interprets the results of a single page from a ListAddresses() call,
+// producing a map of addresses.
+func ExtractAddresses(r pagination.Page) (map[string][]Address, error) {
+	var s struct {
+		Addresses map[string][]Address `json:"addresses"`
+	}
+	err := (r.(AddressPage)).ExtractInto(&s)
+	return s.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)
+	return len(addresses) == 0, err
+}
+
+// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call,
+// producing a slice of addresses.
+func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) {
+	var s map[string][]Address
+	err := (r.(NetworkAddressPage)).ExtractInto(&s)
+	if err != nil {
+		return nil, err
+	}
+
+	var key string
+	for k := range s {
+		key = k
+	}
+
+	return s[key], err
+}
diff --git a/openstack/compute/v2/servers/testing/doc.go b/openstack/compute/v2/servers/testing/doc.go
new file mode 100644
index 0000000..c7c5982
--- /dev/null
+++ b/openstack/compute/v2/servers/testing/doc.go
@@ -0,0 +1,2 @@
+// compute_servers_v2
+package testing
diff --git a/openstack/compute/v2/servers/testing/fixtures.go b/openstack/compute/v2/servers/testing/fixtures.go
new file mode 100644
index 0000000..40d5ed2
--- /dev/null
+++ b/openstack/compute/v2/servers/testing/fixtures.go
@@ -0,0 +1,971 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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": {}
+		},
+		{
+		"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": "",
+		"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-72c93aa682bb",
+		"security_groups": [
+			{
+				"name": "default"
+			}
+		],
+		"OS-SRV-USG:terminated_at": null,
+		"OS-EXT-AZ:availability_zone": "nova",
+		"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+		"name": "merp",
+		"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": {}
+	}
+}
+`
+
+const ServerPasswordBody = `
+{
+    "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
+}
+`
+
+var (
+	herpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:10:02Z")
+	herpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:10:10Z")
+	// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
+	ServerHerp = servers.Server{
+		Status:  "ACTIVE",
+		Updated: herpTimeUpdated,
+		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:  herpTimeCreated,
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]string{},
+		SecurityGroups: []map[string]interface{}{
+			map[string]interface{}{
+				"name": "default",
+			},
+		},
+	}
+
+	derpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:41Z")
+	derpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:49Z")
+	// ServerDerp is a Server struct that should correspond to the second server in ServerListBody.
+	ServerDerp = servers.Server{
+		Status:  "ACTIVE",
+		Updated: derpTimeUpdated,
+		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:  derpTimeCreated,
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]string{},
+		SecurityGroups: []map[string]interface{}{
+			map[string]interface{}{
+				"name": "default",
+			},
+		},
+	}
+
+	merpTimeCreated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:41Z")
+	merpTimeUpdated, _ = time.Parse(time.RFC3339, "2014-09-25T13:04:49Z")
+	// ServerMerp is a Server struct that should correspond to the second server in ServerListBody.
+	ServerMerp = servers.Server{
+		Status:  "ACTIVE",
+		Updated: merpTimeUpdated,
+		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: nil,
+		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-72c93aa682bb",
+		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
+		Name:     "merp",
+		Created:  merpTimeCreated,
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]string{},
+		SecurityGroups: []map[string]interface{}{
+			map[string]interface{}{
+				"name": "default",
+			},
+		},
+	}
+)
+
+type CreateOptsWithCustomField struct {
+	servers.CreateOpts
+	Foo string `json:"foo,omitempty"`
+}
+
+func (opts CreateOptsWithCustomField) ToServerCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "server")
+}
+
+// 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)
+	})
+
+	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, `
+				{
+					"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
+						},
+						{
+							"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
+						}
+					]
+				}
+			`)
+		case "2":
+			fmt.Fprintf(w, `{ "images": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	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, `
+						{
+							"flavors": [
+								{
+									"id": "1",
+									"name": "m1.tiny",
+									"disk": 1,
+									"ram": 512,
+									"vcpus": 1,
+									"swap":""
+								},
+								{
+									"id": "2",
+									"name": "m2.small",
+									"disk": 10,
+									"ram": 1024,
+									"vcpus": 2,
+									"swap": 1000
+								}
+							],
+							"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)
+		}
+	})
+}
+
+// HandleServerCreationWithCustomFieldSuccessfully sets up the test server to respond to a server creation request
+// with a given response.
+func HandleServerCreationWithCustomFieldSuccessfully(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",
+				"foo": "bar"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleServerCreationWithUserdata sets up the test server to respond to a server creation request
+// with a given response.
+func HandleServerCreationWithUserdata(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",
+				"user_data": "dXNlcmRhdGEgc3RyaW5n"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleServerCreationWithMetadata sets up the test server to respond to a server creation request
+// with a given response.
+func HandleServerCreationWithMetadata(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",
+				"metadata": {
+					"abc": "def"
+				}
+			}
+		}`)
+
+		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)
+	})
+}
+
+// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion
+// request.
+func HandleServerForceDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "forceDelete": "" }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// 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][]servers.Address{
+	"public": []servers.Address{
+		{
+			Version: 4,
+			Address: "80.56.136.39",
+		},
+		{
+			Version: 6,
+			Address: "2001:4800:790e:510:be76:4eff:fe04:82a8",
+		},
+	},
+	"private": []servers.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 = []servers.Address{
+	{
+		Version: 4,
+		Address: "50.56.176.35",
+	},
+	{
+		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)
+	})
+}
+
+// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request.
+func HandlePasswordGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, ServerPasswordBody)
+	})
+}
diff --git a/openstack/compute/v2/servers/testing/requests_test.go b/openstack/compute/v2/servers/testing/requests_test.go
new file mode 100644
index 0000000..05712f7
--- /dev/null
+++ b/openstack/compute/v2/servers/testing/requests_test.go
@@ -0,0 +1,522 @@
+package testing
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListServers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerListSuccessfully(t)
+
+	pages := 0
+	err := servers.List(client.ServiceClient(), servers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := servers.ExtractServers(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 3 {
+			t.Fatalf("Expected 3 servers, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, ServerHerp, actual[0])
+		th.CheckDeepEquals(t, ServerDerp, actual[1])
+		th.CheckDeepEquals(t, ServerMerp, actual[2])
+
+		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 := servers.List(client.ServiceClient(), servers.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := servers.ExtractServers(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ServerHerp, actual[0])
+	th.CheckDeepEquals(t, ServerDerp, actual[1])
+}
+
+func TestListAllServersWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerListSuccessfully(t)
+
+	type ServerWithExt struct {
+		servers.Server
+		availabilityzones.ServerExt
+	}
+
+	allPages, err := servers.List(client.ServiceClient(), servers.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+
+	var actual []ServerWithExt
+	err = servers.ExtractServersInto(allPages, &actual)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 3, len(actual))
+	th.AssertEquals(t, "nova", actual[0].AvailabilityZone)
+}
+
+func TestCreateServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationSuccessfully(t, SingleServerBody)
+
+	actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestCreateServerWithCustomField(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationWithCustomFieldSuccessfully(t, SingleServerBody)
+
+	actual, err := servers.Create(client.ServiceClient(), CreateOptsWithCustomField{
+		CreateOpts: servers.CreateOpts{
+			Name:      "derp",
+			ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+			FlavorRef: "1",
+		},
+		Foo: "bar",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestCreateServerWithMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationWithMetadata(t, SingleServerBody)
+
+	actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+		Metadata: map[string]string{
+			"abc": "def",
+		},
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestCreateServerWithUserdataString(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationWithUserdata(t, SingleServerBody)
+
+	actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+		UserData:  []byte("userdata string"),
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestCreateServerWithUserdataEncoded(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationWithUserdata(t, SingleServerBody)
+
+	encoded := base64.StdEncoding.EncodeToString([]byte("userdata string"))
+
+	actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+		UserData:  []byte(encoded),
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestCreateServerWithImageNameAndFlavorName(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationSuccessfully(t, SingleServerBody)
+
+	actual, err := servers.Create(client.ServiceClient(), servers.CreateOpts{
+		Name:          "derp",
+		ImageName:     "cirros-0.3.2-x86_64-disk",
+		FlavorName:    "m1.tiny",
+		ServiceClient: client.ServiceClient(),
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerDeletionSuccessfully(t)
+
+	res := servers.Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestForceDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerForceDeletionSuccessfully(t)
+
+	res := servers.ForceDelete(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 := servers.Get(client, "1234asdf").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, ServerDerp, *actual)
+}
+
+func TestGetServerWithExtensions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerGetSuccessfully(t)
+
+	var s struct {
+		servers.Server
+		availabilityzones.ServerExt
+	}
+
+	err := servers.Get(client.ServiceClient(), "1234asdf").ExtractInto(&s)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "nova", s.AvailabilityZone)
+
+	err = servers.Get(client.ServiceClient(), "1234asdf").ExtractInto(s)
+	if err == nil {
+		t.Errorf("Expected error when providing non-pointer struct")
+	}
+}
+
+func TestUpdateServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerUpdateSuccessfully(t)
+
+	client := client.ServiceClient()
+	actual, err := servers.Update(client, "1234asdf", servers.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 := servers.ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePasswordGetSuccessfully(t)
+
+	res := servers.GetPassword(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRebootServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRebootSuccessfully(t)
+
+	res := servers.Reboot(client.ServiceClient(), "1234asdf", &servers.RebootOpts{
+		Type: servers.SoftReboot,
+	})
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRebuildServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRebuildSuccessfully(t, SingleServerBody)
+
+	opts := 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",
+	}
+
+	actual, err := servers.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 := servers.Resize(client.ServiceClient(), "1234asdf", servers.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 := servers.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 := servers.RevertResize(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRescue(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleServerRescueSuccessfully(t)
+
+	res := servers.Rescue(client.ServiceClient(), "1234asdf", servers.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 := servers.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 := servers.CreateMetadatum(client.ServiceClient(), "1234asdf", servers.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 := servers.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 := servers.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 := servers.ResetMetadata(client.ServiceClient(), "1234asdf", servers.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 := servers.UpdateMetadata(client.ServiceClient(), "1234asdf", servers.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 := servers.ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := servers.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 := servers.ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := servers.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 := servers.CreateImage(client.ServiceClient(), "serverimage", servers.CreateImageOpts{Name: "test"}).ExtractImageID()
+	th.AssertNoErr(t, err)
+}
+
+func TestMarshalPersonality(t *testing.T) {
+	name := "/etc/test"
+	contents := []byte("asdfasdf")
+
+	personality := servers.Personality{
+		&servers.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/testing/results_test.go b/openstack/compute/v2/servers/testing/results_test.go
new file mode 100644
index 0000000..5866957
--- /dev/null
+++ b/openstack/compute/v2/servers/testing/results_test.go
@@ -0,0 +1,110 @@
+package testing
+
+import (
+	"crypto/rsa"
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+	"golang.org/x/crypto/ssh"
+)
+
+// Fail - No password in JSON.
+func TestExtractPassword_no_pwd_data(t *testing.T) {
+
+	var dejson interface{}
+	err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := servers.GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(nil)
+	th.AssertEquals(t, pwd, "")
+}
+
+// Ok - return encrypted password when no private key is given.
+func TestExtractPassword_encrypted_pwd(t *testing.T) {
+
+	var dejson interface{}
+	sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+	err := json.Unmarshal(sejson, &dejson)
+	fmt.Printf("%v\n", dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := servers.GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(nil)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd)
+}
+
+// Ok - return decrypted password when private key is given.
+// Decrytion can be verified by:
+//   echo "<enc_pwd>" | base64 -D | openssl rsautl -decrypt -inkey <privateKey.pem>
+func TestExtractPassword_decrypted_pwd(t *testing.T) {
+
+	privateKey, err := ssh.ParseRawPrivateKey([]byte(`
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+
+yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv
+X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+
+x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw
+JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1
+ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j
+RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1
+g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew
+OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4
+FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm//
+S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS
+/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db
+QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf
+fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm
+AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs
+n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ
+zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ
+xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb
+eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF
+G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R
+1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1
+ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX
+mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg
+09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c
+KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0=
+-----END RSA PRIVATE KEY-----
+`))
+	if err != nil {
+		t.Fatalf("Error parsing private key: %s\n", err)
+	}
+
+	var dejson interface{}
+	sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+	err = json.Unmarshal(sejson, &dejson)
+	fmt.Printf("%v\n", dejson)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	resp := servers.GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+	pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey))
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd)
+}
+
+func TestListAddressesAllPages(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAddressListSuccessfully(t)
+
+	allPages, err := servers.ListAddresses(client.ServiceClient(), "asdfasdfasdf").AllPages()
+	th.AssertNoErr(t, err)
+	_, err = servers.ExtractAddresses(allPages)
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..e892e8d
--- /dev/null
+++ b/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,51 @@
+package servers
+
+import "github.com/gophercloud/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)
+}
+
+func passwordURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "os-server-password")
+}
diff --git a/openstack/compute/v2/servers/util.go b/openstack/compute/v2/servers/util.go
new file mode 100644
index 0000000..494a0e4
--- /dev/null
+++ b/openstack/compute/v2/servers/util.go
@@ -0,0 +1,20 @@
+package servers
+
+import "github.com/gophercloud/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/requests.go b/openstack/db/v1/configurations/requests.go
new file mode 100644
index 0000000..8fc8295
--- /dev/null
+++ b/openstack/db/v1/configurations/requests.go
@@ -0,0 +1,167 @@
+package configurations
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/instances"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List will list all of the available configurations.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, baseURL(client), func(r pagination.PageResult) pagination.Page {
+		return ConfigPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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 {
+	// The type of datastore. Defaults to "MySQL".
+	Type string `json:"type,omitempty"`
+	// The specific version of a datastore. Defaults to "5.6".
+	Version string `json:"version,omitempty"`
+}
+
+// CreateOpts is the struct responsible for configuring new configurations.
+type CreateOpts struct {
+	// The configuration group name
+	Name string `json:"name" required:"true"`
+	// 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{} `json:"values" required:"true"`
+	// Associates the configuration group with a particular datastore.
+	Datastore *DatastoreOpts `json:"datastore,omitempty"`
+	// A human-readable explanation for the group.
+	Description string `json:"description,omitempty"`
+}
+
+// ToConfigCreateMap casts a CreateOpts struct into a JSON map.
+func (opts CreateOpts) ToConfigCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "configuration")
+}
+
+// Create will create a new configuration group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToConfigCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(baseURL(client), &b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
+	return
+}
+
+// Get will retrieve the details for a specified configuration group.
+func Get(client *gophercloud.ServiceClient, configID string) (r GetResult) {
+	_, r.Err = client.Get(resourceURL(client, configID), &r.Body, nil)
+	return
+}
+
+// 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 {
+	// The configuration group name
+	Name string `json:"name,omitempty"`
+	// 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{} `json:"values,omitempty"`
+	// Associates the configuration group with a particular datastore.
+	Datastore *DatastoreOpts `json:"datastore,omitempty"`
+	// A human-readable explanation for the group.
+	Description string `json:"description,omitempty"`
+}
+
+// ToConfigUpdateMap will cast an UpdateOpts struct into a JSON map.
+func (opts UpdateOpts) ToConfigUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "configuration")
+}
+
+// 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) (r UpdateResult) {
+	b, err := opts.ToConfigUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Patch(resourceURL(client, configID), &b, nil, nil)
+	return
+}
+
+// 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) (r ReplaceResult) {
+	b, err := opts.ToConfigUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(resourceURL(client, configID), &b, nil, nil)
+	return
+}
+
+// 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) (r DeleteResult) {
+	_, r.Err = client.Delete(resourceURL(client, configID), nil)
+	return
+}
+
+// ListInstances will list all the instances associated with a particular
+// configuration group.
+func ListInstances(client *gophercloud.ServiceClient, configID string) pagination.Pager {
+	return pagination.NewPager(client, instancesURL(client, configID), func(r pagination.PageResult) pagination.Page {
+		return instances.InstancePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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 pagination.NewPager(client, listDSParamsURL(client, datastoreID, versionID), func(r pagination.PageResult) pagination.Page {
+		return ParamPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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) (r ParamResult) {
+	_, r.Err = client.Get(getDSParamURL(client, datastoreID, versionID, paramID), &r.Body, nil)
+	return
+}
+
+// ListGlobalParams is similar to ListDatastoreParams but does not require a
+// DatastoreID.
+func ListGlobalParams(client *gophercloud.ServiceClient, versionID string) pagination.Pager {
+	return pagination.NewPager(client, listGlobalParamsURL(client, versionID), func(r pagination.PageResult) pagination.Page {
+		return ParamPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// GetGlobalParam is similar to GetDatastoreParam but does not require a
+// DatastoreID.
+func GetGlobalParam(client *gophercloud.ServiceClient, versionID, paramID string) (r ParamResult) {
+	_, r.Err = client.Get(getGlobalParamURL(client, versionID, paramID), &r.Body, nil)
+	return
+}
diff --git a/openstack/db/v1/configurations/results.go b/openstack/db/v1/configurations/results.go
new file mode 100644
index 0000000..c52a654
--- /dev/null
+++ b/openstack/db/v1/configurations/results.go
@@ -0,0 +1,121 @@
+package configurations
+
+import (
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Config represents a configuration group API resource.
+type Config struct {
+	Created              time.Time `json:"created"`
+	Updated              time.Time `json:"updated"`
+	DatastoreName        string    `json:"datastore_name"`
+	DatastoreVersionID   string    `json:"datastore_version_id"`
+	DatastoreVersionName string    `json:"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)
+	return len(is) == 0, err
+}
+
+// ExtractConfigs will retrieve a slice of Config structs from a page.
+func ExtractConfigs(r pagination.Page) ([]Config, error) {
+	var s struct {
+		Configs []Config `json:"configurations"`
+	}
+	err := (r.(ConfigPage)).ExtractInto(&s)
+	return s.Configs, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will retrieve a Config resource from an operation result.
+func (r commonResult) Extract() (*Config, error) {
+	var s struct {
+		Config *Config `json:"configuration"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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             float64
+	Min             float64
+	Name            string
+	RestartRequired bool `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)
+	return len(is) == 0, err
+}
+
+// ExtractParams will retrieve a slice of Param structs from a page.
+func ExtractParams(r pagination.Page) ([]Param, error) {
+	var s struct {
+		Params []Param `json:"configuration-parameters"`
+	}
+	err := (r.(ParamPage)).ExtractInto(&s)
+	return s.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) {
+	var s *Param
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/db/v1/configurations/testing/doc.go b/openstack/db/v1/configurations/testing/doc.go
new file mode 100644
index 0000000..60c997a
--- /dev/null
+++ b/openstack/db/v1/configurations/testing/doc.go
@@ -0,0 +1,2 @@
+// db_configurations_v1
+package testing
diff --git a/openstack/db/v1/configurations/testing/fixtures.go b/openstack/db/v1/configurations/testing/fixtures.go
new file mode 100644
index 0000000..56e10f4
--- /dev/null
+++ b/openstack/db/v1/configurations/testing/fixtures.go
@@ -0,0 +1,159 @@
+package testing
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/gophercloud/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 = configurations.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 = configurations.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/testing/requests_test.go b/openstack/db/v1/configurations/testing/requests_test.go
new file mode 100644
index 0000000..643f363
--- /dev/null
+++ b/openstack/db/v1/configurations/testing/requests_test.go
@@ -0,0 +1,237 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/db/v1/configurations"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/instances"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+	"github.com/gophercloud/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 := configurations.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := configurations.ExtractConfigs(page)
+		th.AssertNoErr(t, err)
+
+		expected := []configurations.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 := configurations.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 := configurations.CreateOpts{
+		Datastore: &configurations.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 := configurations.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 := configurations.UpdateOpts{
+		Values: map[string]interface{}{
+			"connect_timeout": 300,
+		},
+	}
+
+	err := configurations.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 := configurations.UpdateOpts{
+		Values: map[string]interface{}{
+			"connect_timeout": 300,
+		},
+	}
+
+	err := configurations.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 := configurations.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 := configurations.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 := configurations.ListDatastoreParams(fake.ServiceClient(), dsID, versionID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := configurations.ExtractParams(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []configurations.Param{
+			{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+			{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+			{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+			{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 := configurations.GetDatastoreParam(fake.ServiceClient(), dsID, versionID, paramID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &configurations.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 := configurations.ListGlobalParams(fake.ServiceClient(), versionID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := configurations.ExtractParams(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []configurations.Param{
+			{Max: 1, Min: 0, Name: "innodb_file_per_table", RestartRequired: true, Type: "integer"},
+			{Max: 4294967296, Min: 0, Name: "key_buffer_size", RestartRequired: false, Type: "integer"},
+			{Max: 65535, Min: 2, Name: "connect_timeout", RestartRequired: false, Type: "integer"},
+			{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 := configurations.GetGlobalParam(fake.ServiceClient(), versionID, paramID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &configurations.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/urls.go b/openstack/db/v1/configurations/urls.go
new file mode 100644
index 0000000..0a69253
--- /dev/null
+++ b/openstack/db/v1/configurations/urls.go
@@ -0,0 +1,31 @@
+package configurations
+
+import "github.com/gophercloud/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/requests.go b/openstack/db/v1/databases/requests.go
new file mode 100644
index 0000000..ef5394f
--- /dev/null
+++ b/openstack/db/v1/databases/requests.go
@@ -0,0 +1,89 @@
+package databases
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreateOptsBuilder builds create options
+type CreateOptsBuilder interface {
+	ToDBCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the struct responsible for configuring a database; often in
+// the context of an instance.
+type CreateOpts struct {
+	// 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 `json:"name" required:"true"`
+	// 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 `json:"character_set,omitempty"`
+	// 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 `json:"collate,omitempty"`
+}
+
+// ToMap is a helper function to convert individual DB create opt structures
+// into sub-maps.
+func (opts CreateOpts) ToMap() (map[string]interface{}, error) {
+	if len(opts.Name) > 64 {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "databases.CreateOpts.Name"
+		err.Value = opts.Name
+		err.Info = "Must be less than 64 chars long"
+		return nil, err
+	}
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// 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]interface{}, 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) (r CreateResult) {
+	b, err := opts.ToDBCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(baseURL(client, instanceID), &b, nil, nil)
+	return
+}
+
+// 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 {
+	return pagination.NewPager(client, baseURL(client, instanceID), func(r pagination.PageResult) pagination.Page {
+		return DBPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// 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) (r DeleteResult) {
+	_, r.Err = client.Delete(dbURL(client, instanceID, dbName), nil)
+	return
+}
diff --git a/openstack/db/v1/databases/results.go b/openstack/db/v1/databases/results.go
new file mode 100644
index 0000000..0479d0e
--- /dev/null
+++ b/openstack/db/v1/databases/results.go
@@ -0,0 +1,63 @@
+package databases
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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)
+	return len(dbs) == 0, err
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page DBPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"databases_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractDBs will convert a generic pagination struct into a more
+// relevant slice of DB structs.
+func ExtractDBs(page pagination.Page) ([]Database, error) {
+	r := page.(DBPage)
+	var s struct {
+		Databases []Database `json:"databases"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Databases, err
+}
diff --git a/openstack/db/v1/databases/testing/doc.go b/openstack/db/v1/databases/testing/doc.go
new file mode 100644
index 0000000..abdfab9
--- /dev/null
+++ b/openstack/db/v1/databases/testing/doc.go
@@ -0,0 +1,2 @@
+// db_databases_v1
+package testing
diff --git a/openstack/db/v1/databases/testing/fixtures.go b/openstack/db/v1/databases/testing/fixtures.go
new file mode 100644
index 0000000..02b9ecc
--- /dev/null
+++ b/openstack/db/v1/databases/testing/fixtures.go
@@ -0,0 +1,61 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/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/testing/requests_test.go b/openstack/db/v1/databases/testing/requests_test.go
new file mode 100644
index 0000000..a470ffa
--- /dev/null
+++ b/openstack/db/v1/databases/testing/requests_test.go
@@ -0,0 +1,67 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreate(t)
+
+	opts := databases.BatchCreateOpts{
+		databases.CreateOpts{Name: "testingdb", CharSet: "utf8", Collate: "utf8_general_ci"},
+		databases.CreateOpts{Name: "sampledb"},
+	}
+
+	res := databases.Create(fake.ServiceClient(), instanceID, opts)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	expectedDBs := []databases.Database{
+		{Name: "anotherexampledb"},
+		{Name: "exampledb"},
+		{Name: "nextround"},
+		{Name: "sampledb"},
+		{Name: "testingdb"},
+	}
+
+	pages := 0
+	err := databases.List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := databases.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 := databases.Delete(fake.ServiceClient(), instanceID, "{dbName}").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/db/v1/databases/urls.go b/openstack/db/v1/databases/urls.go
new file mode 100644
index 0000000..aba42c9
--- /dev/null
+++ b/openstack/db/v1/databases/urls.go
@@ -0,0 +1,11 @@
+package databases
+
+import "github.com/gophercloud/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/requests.go b/openstack/db/v1/datastores/requests.go
new file mode 100644
index 0000000..134e309
--- /dev/null
+++ b/openstack/db/v1/datastores/requests.go
@@ -0,0 +1,33 @@
+package datastores
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List will list all available datastore types that instances can use.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, baseURL(client), func(r pagination.PageResult) pagination.Page {
+		return DatastorePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get will retrieve the details of a specified datastore type.
+func Get(client *gophercloud.ServiceClient, datastoreID string) (r GetResult) {
+	_, r.Err = client.Get(resourceURL(client, datastoreID), &r.Body, nil)
+	return
+}
+
+// ListVersions will list all of the available versions for a specified
+// datastore type.
+func ListVersions(client *gophercloud.ServiceClient, datastoreID string) pagination.Pager {
+	return pagination.NewPager(client, versionsURL(client, datastoreID), func(r pagination.PageResult) pagination.Page {
+		return VersionPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// GetVersion will retrieve the details of a specified datastore version.
+func GetVersion(client *gophercloud.ServiceClient, datastoreID, versionID string) (r GetVersionResult) {
+	_, r.Err = client.Get(versionURL(client, datastoreID, versionID), &r.Body, nil)
+	return
+}
diff --git a/openstack/db/v1/datastores/results.go b/openstack/db/v1/datastores/results.go
new file mode 100644
index 0000000..a6e27d2
--- /dev/null
+++ b/openstack/db/v1/datastores/results.go
@@ -0,0 +1,100 @@
+package datastores
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+	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"`
+}
+
+// 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)
+	return len(is) == 0, err
+}
+
+// ExtractDatastores retrieves a slice of datastore structs from a paginated
+// collection.
+func ExtractDatastores(r pagination.Page) ([]Datastore, error) {
+	var s struct {
+		Datastores []Datastore `json:"datastores"`
+	}
+	err := (r.(DatastorePage)).ExtractInto(&s)
+	return s.Datastores, err
+}
+
+// Extract retrieves a single Datastore struct from an operation result.
+func (r GetResult) Extract() (*Datastore, error) {
+	var s struct {
+		Datastore *Datastore `json:"datastore"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Datastore, err
+}
+
+// VersionPage 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)
+	return len(is) == 0, err
+}
+
+// ExtractVersions retrieves a slice of versions from a paginated collection.
+func ExtractVersions(r pagination.Page) ([]Version, error) {
+	var s struct {
+		Versions []Version `json:"versions"`
+	}
+	err := (r.(VersionPage)).ExtractInto(&s)
+	return s.Versions, err
+}
+
+// Extract retrieves a single Version struct from an operation result.
+func (r GetVersionResult) Extract() (*Version, error) {
+	var s struct {
+		Version *Version `json:"version"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Version, err
+}
diff --git a/openstack/db/v1/datastores/testing/doc.go b/openstack/db/v1/datastores/testing/doc.go
new file mode 100644
index 0000000..8f06f86
--- /dev/null
+++ b/openstack/db/v1/datastores/testing/doc.go
@@ -0,0 +1,2 @@
+// db_datastores_v1
+package testing
diff --git a/openstack/db/v1/datastores/testing/fixtures.go b/openstack/db/v1/datastores/testing/fixtures.go
new file mode 100644
index 0000000..3b82646
--- /dev/null
+++ b/openstack/db/v1/datastores/testing/fixtures.go
@@ -0,0 +1,101 @@
+package testing
+
+import (
+	"fmt"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/datastores"
+)
+
+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 = datastores.Version{
+	ID: "b00000b0-00b0-0b00-00b0-000b000000bb",
+	Links: []gophercloud.Link{
+		{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
+		{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/b00000b0-00b0-0b00-00b0-000b000000bb"},
+	},
+	Name: "5.1",
+}
+
+var exampleVersion2 = datastores.Version{
+	ID: "c00000b0-00c0-0c00-00c0-000b000000cc",
+	Links: []gophercloud.Link{
+		{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
+		{Rel: "bookmark", Href: "https://10.240.28.38:8779/datastores/versions/c00000b0-00c0-0c00-00c0-000b000000cc"},
+	},
+	Name: "5.2",
+}
+
+var ExampleVersions = []datastores.Version{ExampleVersion1, exampleVersion2}
+
+var ExampleDatastore = datastores.Datastore{
+	DefaultVersion: "c00000b0-00c0-0c00-00c0-000b000000cc",
+	ID:             "10000000-0000-0000-0000-000000000001",
+	Links: []gophercloud.Link{
+		{Rel: "self", Href: "https://10.240.28.38:8779/v1.0/1234/datastores/10000000-0000-0000-0000-000000000001"},
+		{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/testing/requests_test.go b/openstack/db/v1/datastores/testing/requests_test.go
new file mode 100644
index 0000000..b505726
--- /dev/null
+++ b/openstack/db/v1/datastores/testing/requests_test.go
@@ -0,0 +1,79 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/db/v1/datastores"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+	"github.com/gophercloud/gophercloud/testhelper/fixture"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	fixture.SetupHandler(t, "/datastores", "GET", "", ListDSResp, 200)
+
+	pages := 0
+
+	err := datastores.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := datastores.ExtractDatastores(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, []datastores.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 := datastores.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 := datastores.ListVersions(fake.ServiceClient(), "{dsID}").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := datastores.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 := datastores.GetVersion(fake.ServiceClient(), "{dsID}", "{versionID}").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, &ExampleVersion1, ds)
+}
diff --git a/openstack/db/v1/datastores/urls.go b/openstack/db/v1/datastores/urls.go
new file mode 100644
index 0000000..06d1b3d
--- /dev/null
+++ b/openstack/db/v1/datastores/urls.go
@@ -0,0 +1,19 @@
+package datastores
+
+import "github.com/gophercloud/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/requests.go b/openstack/db/v1/flavors/requests.go
new file mode 100644
index 0000000..7fac56a
--- /dev/null
+++ b/openstack/db/v1/flavors/requests.go
@@ -0,0 +1,21 @@
+package flavors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get will retrieve information for a specified hardware flavor.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/db/v1/flavors/results.go b/openstack/db/v1/flavors/results.go
new file mode 100644
index 0000000..0ba515c
--- /dev/null
+++ b/openstack/db/v1/flavors/results.go
@@ -0,0 +1,71 @@
+package flavors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 (r GetResult) Extract() (*Flavor, error) {
+	var s struct {
+		Flavor *Flavor `json:"flavor"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Flavor, err
+}
+
+// Flavor records represent (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+	// The flavor's unique identifier.
+	// Contains 0 if the ID is not an integer.
+	ID int `json:"id"`
+
+	// The RAM capacity for the flavor.
+	RAM int `json:"ram"`
+
+	// The Name field provides a human-readable moniker for the flavor.
+	Name string `json:"name"`
+
+	// Links to access the flavor.
+	Links []gophercloud.Link
+
+	// The flavor's unique identifier as a string
+	StrID string `json:"str_id"`
+}
+
+// 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 (page FlavorPage) IsEmpty() (bool, error) {
+	flavors, err := ExtractFlavors(page)
+	return len(flavors) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page FlavorPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"flavors_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
+	var s struct {
+		Flavors []Flavor `json:"flavors"`
+	}
+	err := (r.(FlavorPage)).ExtractInto(&s)
+	return s.Flavors, err
+}
diff --git a/openstack/db/v1/flavors/testing/doc.go b/openstack/db/v1/flavors/testing/doc.go
new file mode 100644
index 0000000..0809266
--- /dev/null
+++ b/openstack/db/v1/flavors/testing/doc.go
@@ -0,0 +1,2 @@
+// db_flavors_v1
+package testing
diff --git a/openstack/db/v1/flavors/testing/fixtures.go b/openstack/db/v1/flavors/testing/fixtures.go
new file mode 100644
index 0000000..9c323b8
--- /dev/null
+++ b/openstack/db/v1/flavors/testing/fixtures.go
@@ -0,0 +1,52 @@
+package testing
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/testhelper/fixture"
+)
+
+const flavor = `
+{
+	"id": %s,
+	"links": [
+		{
+			"href": "https://openstack.example.com/v1.0/1234/flavors/%s",
+			"rel": "self"
+		},
+		{
+			"href": "https://openstack.example.com/flavors/%s",
+			"rel": "bookmark"
+		}
+	],
+	"name": "%s",
+	"ram": %d,
+	"str_id": "%s"
+}
+`
+
+var (
+	flavorID = "{flavorID}"
+	_baseURL = "/flavors"
+	resURL   = "/flavors/" + flavorID
+)
+
+var (
+	flavor1 = fmt.Sprintf(flavor, "1", "1", "1", "m1.tiny", 512, "1")
+	flavor2 = fmt.Sprintf(flavor, "2", "2", "2", "m1.small", 1024, "2")
+	flavor3 = fmt.Sprintf(flavor, "3", "3", "3", "m1.medium", 2048, "3")
+	flavor4 = fmt.Sprintf(flavor, "4", "4", "4", "m1.large", 4096, "4")
+	flavor5 = fmt.Sprintf(flavor, "null", "d1", "d1", "ds512M", 512, "d1")
+
+	listFlavorsResp = fmt.Sprintf(`{"flavors":[%s, %s, %s, %s, %s]}`, flavor1, flavor2, flavor3, flavor4, flavor5)
+	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/testing/requests_test.go b/openstack/db/v1/flavors/testing/requests_test.go
new file mode 100644
index 0000000..e8b580a
--- /dev/null
+++ b/openstack/db/v1/flavors/testing/requests_test.go
@@ -0,0 +1,107 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/flavors"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	pages := 0
+	err := flavors.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := flavors.ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []flavors.Flavor{
+			{
+				ID:   1,
+				Name: "m1.tiny",
+				RAM:  512,
+				Links: []gophercloud.Link{
+					{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+					{Href: "https://openstack.example.com/flavors/1", Rel: "bookmark"},
+				},
+				StrID: "1",
+			},
+			{
+				ID:   2,
+				Name: "m1.small",
+				RAM:  1024,
+				Links: []gophercloud.Link{
+					{Href: "https://openstack.example.com/v1.0/1234/flavors/2", Rel: "self"},
+					{Href: "https://openstack.example.com/flavors/2", Rel: "bookmark"},
+				},
+				StrID: "2",
+			},
+			{
+				ID:   3,
+				Name: "m1.medium",
+				RAM:  2048,
+				Links: []gophercloud.Link{
+					{Href: "https://openstack.example.com/v1.0/1234/flavors/3", Rel: "self"},
+					{Href: "https://openstack.example.com/flavors/3", Rel: "bookmark"},
+				},
+				StrID: "3",
+			},
+			{
+				ID:   4,
+				Name: "m1.large",
+				RAM:  4096,
+				Links: []gophercloud.Link{
+					{Href: "https://openstack.example.com/v1.0/1234/flavors/4", Rel: "self"},
+					{Href: "https://openstack.example.com/flavors/4", Rel: "bookmark"},
+				},
+				StrID: "4",
+			},
+			{
+				ID:   0,
+				Name: "ds512M",
+				RAM:  512,
+				Links: []gophercloud.Link{
+					{Href: "https://openstack.example.com/v1.0/1234/flavors/d1", Rel: "self"},
+					{Href: "https://openstack.example.com/flavors/d1", Rel: "bookmark"},
+				},
+				StrID: "d1",
+			},
+		}
+
+		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 := flavors.Get(fake.ServiceClient(), flavorID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &flavors.Flavor{
+		ID:   1,
+		Name: "m1.tiny",
+		RAM:  512,
+		Links: []gophercloud.Link{
+			{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+		},
+		StrID: "1",
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/db/v1/flavors/urls.go b/openstack/db/v1/flavors/urls.go
new file mode 100644
index 0000000..a24301b
--- /dev/null
+++ b/openstack/db/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/gophercloud/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/requests.go b/openstack/db/v1/instances/requests.go
new file mode 100644
index 0000000..f8afb73
--- /dev/null
+++ b/openstack/db/v1/instances/requests.go
@@ -0,0 +1,205 @@
+package instances
+
+import (
+	"github.com/gophercloud/gophercloud"
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/users"
+	"github.com/gophercloud/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 `json:"version"`
+	Type    string `json:"type"`
+}
+
+// ToMap converts a DatastoreOpts to a map[string]string (for a request body)
+func (opts DatastoreOpts) ToMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// NetworkOpts is used within CreateOpts to control a new server's network attachments.
+type NetworkOpts struct {
+	// UUID of a nova-network to attach to the newly provisioned server.
+	// Required unless Port is provided.
+	UUID string `json:"net-id,omitempty"`
+
+	// Port of a neutron network to attach to the newly provisioned server.
+	// Required unless UUID is provided.
+	Port string `json:"port-id,omitempty"`
+
+	// V4FixedIP [optional] specifies a fixed IPv4 address to be used on this network.
+	V4FixedIP string `json:"v4-fixed-ip,omitempty"`
+
+	// V6FixedIP [optional] specifies a fixed IPv6 address to be used on this network.
+	V6FixedIP string `json:"v6-fixed-ip,omitempty"`
+}
+
+// ToMap converts a NetworkOpts to a map[string]string (for a request body)
+func (opts NetworkOpts) ToMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// 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
+	// Networks dictates how this server will be attached to available networks.
+	Networks []NetworkOpts
+}
+
+// ToInstanceCreateMap will render a JSON map.
+func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) {
+	if opts.Size > 300 || opts.Size < 1 {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "instances.CreateOpts.Size"
+		err.Value = opts.Size
+		err.Info = "Size (GB) must be between 1-300"
+		return nil, err
+	}
+
+	if opts.FlavorRef == "" {
+		return nil, gophercloud.ErrMissingInput{Argument: "instances.CreateOpts.FlavorRef"}
+	}
+
+	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"]
+	}
+	if opts.Datastore != nil {
+		datastore, err := opts.Datastore.ToMap()
+		if err != nil {
+			return nil, err
+		}
+		instance["datastore"] = datastore
+	}
+
+	if len(opts.Networks) > 0 {
+		networks := make([]map[string]interface{}, len(opts.Networks))
+		for i, net := range opts.Networks {
+			var err error
+			networks[i], err = net.ToMap()
+			if err != nil {
+				return nil, err
+			}
+		}
+		instance["nics"] = networks
+	}
+
+	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) (r CreateResult) {
+	b, err := opts.ToInstanceCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(baseURL(client), &b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
+	return
+}
+
+// List retrieves the status and information for all database instances.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, baseURL(client), func(r pagination.PageResult) pagination.Page {
+		return InstancePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get retrieves the status and information for a specified database instance.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(resourceURL(client, id), &r.Body, nil)
+	return
+}
+
+// Delete permanently destroys the database instance.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(resourceURL(client, id), nil)
+	return
+}
+
+// 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) (r EnableRootUserResult) {
+	_, r.Err = client.Post(userRootURL(client, id), nil, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
+	return
+}
+
+// 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) (r IsRootEnabledResult) {
+	_, r.Err = client.Get(userRootURL(client, id), &r.Body, nil)
+	return
+}
+
+// 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) (r ActionResult) {
+	b := map[string]interface{}{"restart": struct{}{}}
+	_, r.Err = client.Post(actionURL(client, id), &b, nil, nil)
+	return
+}
+
+// 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) (r ActionResult) {
+	b := map[string]interface{}{"resize": map[string]string{"flavorRef": flavorRef}}
+	_, r.Err = client.Post(actionURL(client, id), &b, nil, nil)
+	return
+}
+
+// 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) (r ActionResult) {
+	b := map[string]interface{}{"resize": map[string]interface{}{"volume": map[string]int{"size": size}}}
+	_, r.Err = client.Post(actionURL(client, id), &b, nil, nil)
+	return
+}
diff --git a/openstack/db/v1/instances/results.go b/openstack/db/v1/instances/results.go
new file mode 100644
index 0000000..6bfde15
--- /dev/null
+++ b/openstack/db/v1/instances/results.go
@@ -0,0 +1,181 @@
+package instances
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/datastores"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/users"
+	"github.com/gophercloud/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
+}
+
+// Flavor represents (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+	// The flavor's unique identifier.
+	ID string
+	// Links to access the flavor.
+	Links []gophercloud.Link
+}
+
+// Instance represents a remote MySQL instance.
+type Instance struct {
+	// Indicates the datetime that the instance was created
+	Created time.Time `json:"-"`
+
+	// Indicates the most recent datetime that the instance was updated.
+	Updated time.Time `json:"-"`
+
+	// Indicates the hardware flavor the instance uses.
+	Flavor 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
+
+	// The IP addresses associated with the database instance
+	// Is empty if the instance has a hostname
+	IP []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
+}
+
+func (r *Instance) UnmarshalJSON(b []byte) error {
+	type tmp Instance
+	var s struct {
+		tmp
+		Created gophercloud.JSONRFC3339NoZ `json:"created"`
+		Updated gophercloud.JSONRFC3339NoZ `json:"updated"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Instance(s.tmp)
+
+	r.Created = time.Time(s.Created)
+	r.Updated = time.Time(s.Updated)
+
+	return nil
+}
+
+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) {
+	var s struct {
+		Instance *Instance `json:"instance"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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)
+	return len(instances) == 0, err
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page InstancePage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"instances_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractInstances will convert a generic pagination struct into a more
+// relevant slice of Instance structs.
+func ExtractInstances(r pagination.Page) ([]Instance, error) {
+	var s struct {
+		Instances []Instance `json:"instances"`
+	}
+	err := (r.(InstancePage)).ExtractInto(&s)
+	return s.Instances, err
+}
+
+// EnableRootUserResult represents the result of an operation to enable the root user.
+type EnableRootUserResult struct {
+	gophercloud.Result
+}
+
+// Extract will extract root user information from a UserRootResult.
+func (r EnableRootUserResult) Extract() (*users.User, error) {
+	var s struct {
+		User *users.User `json:"user"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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
+}
+
+// IsRootEnabledResult is the result of a call to IsRootEnabled. To see if
+// root is enabled, call the type's Extract method.
+type IsRootEnabledResult struct {
+	gophercloud.Result
+}
+
+// Extract is used to extract the data from a IsRootEnabledResult.
+func (r IsRootEnabledResult) Extract() (bool, error) {
+	return r.Body.(map[string]interface{})["rootEnabled"] == true, r.Err
+}
diff --git a/openstack/db/v1/instances/testing/doc.go b/openstack/db/v1/instances/testing/doc.go
new file mode 100644
index 0000000..386ac58
--- /dev/null
+++ b/openstack/db/v1/instances/testing/doc.go
@@ -0,0 +1,2 @@
+// db_instances_v1
+package testing
diff --git a/openstack/db/v1/instances/testing/fixtures.go b/openstack/db/v1/instances/testing/fixtures.go
new file mode 100644
index 0000000..9347ee1
--- /dev/null
+++ b/openstack/db/v1/instances/testing/fixtures.go
@@ -0,0 +1,169 @@
+package testing
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/datastores"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/instances"
+	"github.com/gophercloud/gophercloud/testhelper/fixture"
+)
+
+var (
+	timestamp  = "2015-11-12T14:22:42"
+	timeVal, _ = time.Parse(gophercloud.RFC3339NoZ, timestamp)
+)
+
+var instance = `
+{
+  "created": "` + timestamp + `",
+  "datastore": {
+    "type": "mysql",
+    "version": "5.6"
+  },
+  "flavor": {
+    "id": "1",
+    "links": [
+      {
+        "href": "https://openstack.example.com/v1.0/1234/flavors/1",
+        "rel": "self"
+      },
+      {
+        "href": "https://openstack.example.com/v1.0/1234/flavors/1",
+        "rel": "bookmark"
+      }
+    ]
+  },
+  "links": [
+    {
+      "href": "https://openstack.example.com/v1.0/1234/instances/1",
+      "rel": "self"
+    }
+  ],
+  "hostname": "e09ad9a3f73309469cf1f43d11e79549caf9acf2.openstack.example.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 = instances.Instance{
+	Created: timeVal,
+	Updated: timeVal,
+	Flavor: instances.Flavor{
+		ID: "1",
+		Links: []gophercloud.Link{
+			{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "self"},
+			{Href: "https://openstack.example.com/v1.0/1234/flavors/1", Rel: "bookmark"},
+		},
+	},
+	Hostname: "e09ad9a3f73309469cf1f43d11e79549caf9acf2.openstack.example.com",
+	ID:       instanceID,
+	Links: []gophercloud.Link{
+		{Href: "https://openstack.example.com/v1.0/1234/instances/1", Rel: "self"},
+	},
+	Name:   "json_rack_instance",
+	Status: "BUILD",
+	Volume: instances.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/testing/requests_test.go b/openstack/db/v1/instances/testing/requests_test.go
new file mode 100644
index 0000000..e3c81e3
--- /dev/null
+++ b/openstack/db/v1/instances/testing/requests_test.go
@@ -0,0 +1,134 @@
+package testing
+
+import (
+	"testing"
+
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/instances"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/users"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreate(t)
+
+	opts := instances.CreateOpts{
+		Name:      "json_rack_instance",
+		FlavorRef: "1",
+		Databases: db.BatchCreateOpts{
+			{CharSet: "utf8", Collate: "utf8_general_ci", Name: "sampledb"},
+			{Name: "nextround"},
+		},
+		Users: users.BatchCreateOpts{
+			{
+				Name:     "demouser",
+				Password: "demopassword",
+				Databases: db.BatchCreateOpts{
+					{Name: "sampledb"},
+				},
+			},
+		},
+		Size: 2,
+	}
+
+	instance, err := instances.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 := instances.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := instances.ExtractInstances(page)
+		if err != nil {
+			return false, err
+		}
+
+		th.CheckDeepEquals(t, []instances.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 := instances.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 := instances.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 := instances.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 := instances.IsRootEnabled(fake.ServiceClient(), instanceID).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, true, isEnabled)
+}
+
+func TestRestart(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRestart(t)
+
+	res := instances.Restart(fake.ServiceClient(), instanceID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResize(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleResize(t)
+
+	res := instances.Resize(fake.ServiceClient(), instanceID, "2")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResizeVolume(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleResizeVol(t)
+
+	res := instances.ResizeVolume(fake.ServiceClient(), instanceID, 4)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/instances/urls.go b/openstack/db/v1/instances/urls.go
new file mode 100644
index 0000000..76d1ca5
--- /dev/null
+++ b/openstack/db/v1/instances/urls.go
@@ -0,0 +1,19 @@
+package instances
+
+import "github.com/gophercloud/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/requests.go b/openstack/db/v1/users/requests.go
new file mode 100644
index 0000000..d342de3
--- /dev/null
+++ b/openstack/db/v1/users/requests.go
@@ -0,0 +1,91 @@
+package users
+
+import (
+	"github.com/gophercloud/gophercloud"
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/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 {
+	// 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 `json:"name" required:"true"`
+	// Specifies a password for the user.
+	Password string `json:"password" required:"true"`
+	// An array of databases that this user will connect to. The
+	// "name" field is the only requirement for each option.
+	Databases db.BatchCreateOpts `json:"databases,omitempty"`
+	// 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 `json:"host,omitempty"`
+}
+
+// ToMap is a convenience function for creating sub-maps for individual users.
+func (opts CreateOpts) ToMap() (map[string]interface{}, error) {
+	if opts.Name == "root" {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "users.CreateOpts.Name"
+		err.Value = "root"
+		err.Info = "root is a reserved user name and cannot be used"
+		return nil, err
+	}
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToUserCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(baseURL(client, instanceID), &b, nil, nil)
+	return
+}
+
+// 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 {
+	return pagination.NewPager(client, baseURL(client, instanceID), func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Delete will permanently delete a user from a specified database instance.
+func Delete(client *gophercloud.ServiceClient, instanceID, userName string) (r DeleteResult) {
+	_, r.Err = client.Delete(userURL(client, instanceID, userName), nil)
+	return
+}
diff --git a/openstack/db/v1/users/results.go b/openstack/db/v1/users/results.go
new file mode 100644
index 0000000..d12a681
--- /dev/null
+++ b/openstack/db/v1/users/results.go
@@ -0,0 +1,62 @@
+package users
+
+import (
+	"github.com/gophercloud/gophercloud"
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/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)
+	return len(users) == 0, err
+}
+
+// NextPageURL will retrieve the next page URL.
+func (page UserPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"users_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractUsers will convert a generic pagination struct into a more
+// relevant slice of User structs.
+func ExtractUsers(r pagination.Page) ([]User, error) {
+	var s struct {
+		Users []User `json:"users"`
+	}
+	err := (r.(UserPage)).ExtractInto(&s)
+	return s.Users, err
+}
diff --git a/openstack/db/v1/users/testing/doc.go b/openstack/db/v1/users/testing/doc.go
new file mode 100644
index 0000000..3c98966
--- /dev/null
+++ b/openstack/db/v1/users/testing/doc.go
@@ -0,0 +1,2 @@
+// db_users_v1
+package testing
diff --git a/openstack/db/v1/users/testing/fixtures.go b/openstack/db/v1/users/testing/fixtures.go
new file mode 100644
index 0000000..f49f46f
--- /dev/null
+++ b/openstack/db/v1/users/testing/fixtures.go
@@ -0,0 +1,37 @@
+package testing
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gophercloud/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/testing/requests_test.go b/openstack/db/v1/users/testing/requests_test.go
new file mode 100644
index 0000000..952f245
--- /dev/null
+++ b/openstack/db/v1/users/testing/requests_test.go
@@ -0,0 +1,85 @@
+package testing
+
+import (
+	"testing"
+
+	db "github.com/gophercloud/gophercloud/openstack/db/v1/databases"
+	"github.com/gophercloud/gophercloud/openstack/db/v1/users"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreate(t)
+
+	opts := users.BatchCreateOpts{
+		{
+			Databases: db.BatchCreateOpts{
+				db.CreateOpts{Name: "databaseA"},
+			},
+			Name:     "dbuser3",
+			Password: "secretsecret",
+		},
+		{
+			Databases: db.BatchCreateOpts{
+				{Name: "databaseB"},
+				{Name: "databaseC"},
+			},
+			Name:     "dbuser4",
+			Password: "secretsecret",
+		},
+	}
+
+	res := users.Create(fake.ServiceClient(), instanceID, opts)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUserList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleList(t)
+
+	expectedUsers := []users.User{
+		{
+			Databases: []db.Database{
+				db.Database{Name: "databaseA"},
+			},
+			Name: "dbuser3",
+		},
+		{
+			Databases: []db.Database{
+				{Name: "databaseB"},
+				{Name: "databaseC"},
+			},
+			Name: "dbuser4",
+		},
+	}
+
+	pages := 0
+	err := users.List(fake.ServiceClient(), instanceID).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := users.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 := users.Delete(fake.ServiceClient(), instanceID, "{userName}")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/db/v1/users/urls.go b/openstack/db/v1/users/urls.go
new file mode 100644
index 0000000..8c36a39
--- /dev/null
+++ b/openstack/db/v1/users/urls.go
@@ -0,0 +1,11 @@
+package users
+
+import "github.com/gophercloud/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/dns/v2/zones/doc.go b/openstack/dns/v2/zones/doc.go
new file mode 100644
index 0000000..1302cb9
--- /dev/null
+++ b/openstack/dns/v2/zones/doc.go
@@ -0,0 +1,6 @@
+// Package tokens provides information and interaction with the zone API
+// resource for the OpenStack DNS service.
+//
+// For more information, see:
+// http://developer.openstack.org/api-ref/dns/#zone
+package zones
diff --git a/openstack/dns/v2/zones/requests.go b/openstack/dns/v2/zones/requests.go
new file mode 100644
index 0000000..6db7831
--- /dev/null
+++ b/openstack/dns/v2/zones/requests.go
@@ -0,0 +1,57 @@
+package zones
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type ListOptsBuilder interface {
+	ToZoneListQuery() (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.
+// https://developer.openstack.org/api-ref/dns/
+type ListOpts struct {
+	// Integer value for the limit of values to return.
+	Limit int `q:"limit"`
+
+	// UUID of the zone at which you want to set a marker.
+	Marker string `q:"marker"`
+
+	Description string `q:"description"`
+	Email       string `q:"email"`
+	Name        string `q:"name"`
+	SortDir     string `q:"sort_dir"`
+	SortKey     string `q:"sort_key"`
+	Status      string `q:"status"`
+	TTL         int    `q:"ttl"`
+	Type        string `q:"type"`
+}
+
+func (opts ListOpts) ToZoneListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToZoneListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ZonePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get returns additional information about a zone, given its ID.
+func Get(client *gophercloud.ServiceClient, zoneID string) (r GetResult) {
+	_, r.Err = client.Get(zoneURL(client, zoneID), &r.Body, nil)
+	return
+}
diff --git a/openstack/dns/v2/zones/results.go b/openstack/dns/v2/zones/results.go
new file mode 100644
index 0000000..4693b09
--- /dev/null
+++ b/openstack/dns/v2/zones/results.go
@@ -0,0 +1,144 @@
+package zones
+
+import (
+	"encoding/json"
+	"strconv"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Zone.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Zone, error) {
+	var s *Zone
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// ZonePage is a single page of Zone results.
+type ZonePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if the page contains no results.
+func (r ZonePage) IsEmpty() (bool, error) {
+	s, err := ExtractZones(r)
+	return len(s) == 0, err
+}
+
+// ExtractZones extracts a slice of Services from a Collection acquired from List.
+func ExtractZones(r pagination.Page) ([]Zone, error) {
+	var s struct {
+		Zones []Zone `json:"zones"`
+	}
+	err := (r.(ZonePage)).ExtractInto(&s)
+	return s.Zones, err
+}
+
+// Zone represents a DNS zone.
+type Zone struct {
+	// ID uniquely identifies this zone amongst all other zones, including those not accessible to the current tenant.
+	ID string `json:"id"`
+
+	// PoolID is the ID for the pool hosting this zone.
+	PoolID string `json:"pool_id"`
+
+	// ProjectID identifies the project/tenant owning this resource.
+	ProjectID string `json:"project_id"`
+
+	// Name is the DNS Name for the zone.
+	Name string `json:"name"`
+
+	// Email for the zone. Used in SOA records for the zone.
+	Email string `json:"email"`
+
+	// Description for this zone.
+	Description string `json:"description"`
+
+	// TTL is the Time to Live for the zone.
+	TTL int `json:"ttl"`
+
+	// Serial is the current serial number for the zone.
+	Serial int `json:"-"`
+
+	// Status is the status of the resource.
+	Status string `json:"status"`
+
+	// Action is the current action in progress on the resource.
+	Action string `json:"action"`
+
+	// Version of the resource.
+	Version int `json:"version"`
+
+	// Attributes for the zone.
+	Attributes map[string]string `json:"attributes"`
+
+	// Type of zone. Primary is controlled by Designate.
+	// Secondary zones are slaved from another DNS Server.
+	// Defaults to Primary.
+	Type string `json:"type"`
+
+	// Masters is the servers for slave servers to get DNS information from.
+	Masters []string `json:"masters"`
+
+	// CreatedAt is the date when the zone was created.
+	CreatedAt time.Time `json:"-"`
+
+	// UpdatedAt is the date when the last change was made to the zone.
+	UpdatedAt time.Time `json:"-"`
+
+	// TransferredAt is the last time an update was retrieved from the master servers.
+	TransferredAt time.Time `json:"-"`
+
+	// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+	Links map[string]interface{} `json:"links"`
+}
+
+func (r *Zone) UnmarshalJSON(b []byte) error {
+	type tmp Zone
+	var s struct {
+		tmp
+		CreatedAt     gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+		UpdatedAt     gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+		TransferredAt gophercloud.JSONRFC3339MilliNoZ `json:"transferred_at"`
+		Serial        interface{}                     `json:"serial"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Zone(s.tmp)
+
+	r.CreatedAt = time.Time(s.CreatedAt)
+	r.UpdatedAt = time.Time(s.UpdatedAt)
+	r.TransferredAt = time.Time(s.TransferredAt)
+
+	switch t := s.Serial.(type) {
+	case float64:
+		r.Serial = int(t)
+	case string:
+		switch t {
+		case "":
+			r.Serial = 0
+		default:
+			serial, err := strconv.ParseFloat(t, 64)
+			if err != nil {
+				return err
+			}
+			r.Serial = int(serial)
+		}
+	}
+
+	return err
+}
diff --git a/openstack/dns/v2/zones/testing/doc.go b/openstack/dns/v2/zones/testing/doc.go
new file mode 100644
index 0000000..54a0d21
--- /dev/null
+++ b/openstack/dns/v2/zones/testing/doc.go
@@ -0,0 +1,2 @@
+// dns_zones_v2
+package testing
diff --git a/openstack/dns/v2/zones/testing/fixtures.go b/openstack/dns/v2/zones/testing/fixtures.go
new file mode 100644
index 0000000..473ca90
--- /dev/null
+++ b/openstack/dns/v2/zones/testing/fixtures.go
@@ -0,0 +1,165 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// List Output is a sample response to a List call.
+const ListOutput = `
+{
+    "links": {
+      "self": "http://example.com:9001/v2/zones"
+    },
+    "metadata": {
+      "total_count": 2
+    },
+    "zones": [
+        {
+            "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
+            "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
+            "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
+            "name": "example.org.",
+            "email": "joe@example.org",
+            "ttl": 7200,
+            "serial": 1404757531,
+            "status": "ACTIVE",
+            "action": "CREATE",
+            "description": "This is an example zone.",
+            "masters": [],
+            "type": "PRIMARY",
+            "transferred_at": null,
+            "version": 1,
+            "created_at": "2014-07-07T18:25:31.275934",
+            "updated_at": null,
+            "links": {
+              "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
+            }
+        },
+        {
+            "id": "34c4561c-9205-4386-9df5-167436f5a222",
+            "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
+            "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
+            "name": "foo.example.com.",
+            "email": "joe@foo.example.com",
+            "ttl": 7200,
+            "serial": 1488053571,
+            "status": "ACTIVE",
+            "action": "CREATE",
+            "description": "This is another example zone.",
+            "masters": ["example.com."],
+            "type": "PRIMARY",
+            "transferred_at": null,
+            "version": 1,
+            "created_at": "2014-07-07T18:25:31.275934",
+            "updated_at": "2015-02-25T20:23:01.234567",
+            "links": {
+              "self": "https://127.0.0.1:9001/v2/zones/34c4561c-9205-4386-9df5-167436f5a222"
+            }
+        }
+    ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
+    "pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
+    "project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
+    "name": "example.org.",
+    "email": "joe@example.org",
+    "ttl": 7200,
+    "serial": 1404757531,
+    "status": "ACTIVE",
+    "action": "CREATE",
+    "description": "This is an example zone.",
+    "masters": [],
+    "type": "PRIMARY",
+    "transferred_at": null,
+    "version": 1,
+    "created_at": "2014-07-07T18:25:31.275934",
+    "updated_at": null,
+    "links": {
+      "self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
+    }
+}
+`
+
+// FirstZone is the first result in ListOutput
+var FirstZoneCreatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2014-07-07T18:25:31.275934")
+var FirstZone = zones.Zone{
+	ID:          "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
+	PoolID:      "572ba08c-d929-4c70-8e42-03824bb24ca2",
+	ProjectID:   "4335d1f0-f793-11e2-b778-0800200c9a66",
+	Name:        "example.org.",
+	Email:       "joe@example.org",
+	TTL:         7200,
+	Serial:      1404757531,
+	Status:      "ACTIVE",
+	Action:      "CREATE",
+	Description: "This is an example zone.",
+	Masters:     []string{},
+	Type:        "PRIMARY",
+	Version:     1,
+	CreatedAt:   FirstZoneCreatedAt,
+	Links: map[string]interface{}{
+		"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
+	},
+}
+
+var SecondZoneCreatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2014-07-07T18:25:31.275934")
+var SecondZoneUpdatedAt, _ = time.Parse(gophercloud.RFC3339MilliNoZ, "2015-02-25T20:23:01.234567")
+var SecondZone = zones.Zone{
+	ID:          "34c4561c-9205-4386-9df5-167436f5a222",
+	PoolID:      "572ba08c-d929-4c70-8e42-03824bb24ca2",
+	ProjectID:   "4335d1f0-f793-11e2-b778-0800200c9a66",
+	Name:        "foo.example.com.",
+	Email:       "joe@foo.example.com",
+	TTL:         7200,
+	Serial:      1488053571,
+	Status:      "ACTIVE",
+	Action:      "CREATE",
+	Description: "This is another example zone.",
+	Masters:     []string{"example.com."},
+	Type:        "PRIMARY",
+	Version:     1,
+	CreatedAt:   SecondZoneCreatedAt,
+	UpdatedAt:   SecondZoneUpdatedAt,
+	Links: map[string]interface{}{
+		"self": "https://127.0.0.1:9001/v2/zones/34c4561c-9205-4386-9df5-167436f5a222",
+	},
+}
+
+// ExpectedZonesSlice is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedZonesSlice = []zones.Zone{FirstZone, SecondZone}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/zones", 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 List request.
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3", 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/dns/v2/zones/testing/requests_test.go b/openstack/dns/v2/zones/testing/requests_test.go
new file mode 100644
index 0000000..b7dd667
--- /dev/null
+++ b/openstack/dns/v2/zones/testing/requests_test.go
@@ -0,0 +1,50 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := zones.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := zones.ExtractZones(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedZonesSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestListAllPages(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	allPages, err := zones.List(client.ServiceClient(), nil).AllPages()
+	th.AssertNoErr(t, err)
+	allZones, err := zones.ExtractZones(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 2, len(allZones))
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := zones.Get(client.ServiceClient(), "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &FirstZone, actual)
+}
diff --git a/openstack/dns/v2/zones/urls.go b/openstack/dns/v2/zones/urls.go
new file mode 100644
index 0000000..0cd4796
--- /dev/null
+++ b/openstack/dns/v2/zones/urls.go
@@ -0,0 +1,11 @@
+package zones
+
+import "github.com/gophercloud/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("zones")
+}
+
+func zoneURL(c *gophercloud.ServiceClient, zoneID string) string {
+	return c.ServiceURL("zones", zoneID)
+}
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
new file mode 100644
index 0000000..ea37f5b
--- /dev/null
+++ b/openstack/endpoint_location.go
@@ -0,0 +1,99 @@
+package openstack
+
+import (
+	"github.com/gophercloud/gophercloud"
+	tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/gophercloud/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 {
+		err := &ErrMultipleMatchingEndpointsV2{}
+		err.Endpoints = endpoints
+		return "", err
+	}
+
+	// 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:
+			err := &ErrInvalidAvailabilityProvided{}
+			err.Argument = "Availability"
+			err.Value = opts.Availability
+			return "", err
+		}
+	}
+
+	// Report an error if there were no matching endpoints.
+	err := &gophercloud.ErrEndpointNotFound{}
+	return "", err
+}
+
+// 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 {
+					err := &ErrInvalidAvailabilityProvided{}
+					err.Argument = "Availability"
+					err.Value = opts.Availability
+					return "", err
+				}
+				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 "", ErrMultipleMatchingEndpointsV3{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.
+	err := &gophercloud.ErrEndpointNotFound{}
+	return "", err
+}
diff --git a/openstack/errors.go b/openstack/errors.go
new file mode 100644
index 0000000..df410b1
--- /dev/null
+++ b/openstack/errors.go
@@ -0,0 +1,71 @@
+package openstack
+
+import (
+	"fmt"
+
+	"github.com/gophercloud/gophercloud"
+	tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+)
+
+// ErrEndpointNotFound is the error when no suitable endpoint can be found
+// in the user's catalog
+type ErrEndpointNotFound struct{ gophercloud.BaseError }
+
+func (e ErrEndpointNotFound) Error() string {
+	return "No suitable endpoint could be found in the service catalog."
+}
+
+// ErrInvalidAvailabilityProvided is the error when an invalid endpoint
+// availability is provided
+type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput }
+
+func (e ErrInvalidAvailabilityProvided) Error() string {
+	return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value)
+}
+
+// ErrMultipleMatchingEndpointsV2 is the error when more than one endpoint
+// for the given options is found in the v2 catalog
+type ErrMultipleMatchingEndpointsV2 struct {
+	gophercloud.BaseError
+	Endpoints []tokens2.Endpoint
+}
+
+func (e ErrMultipleMatchingEndpointsV2) Error() string {
+	return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints)
+}
+
+// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint
+// for the given options is found in the v3 catalog
+type ErrMultipleMatchingEndpointsV3 struct {
+	gophercloud.BaseError
+	Endpoints []tokens3.Endpoint
+}
+
+func (e ErrMultipleMatchingEndpointsV3) Error() string {
+	return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints)
+}
+
+// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not
+// found
+type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput }
+
+func (e ErrNoAuthURL) Error() string {
+	return "Environment variable OS_AUTH_URL needs to be set."
+}
+
+// ErrNoUsername is the error when the OS_USERNAME environment variable is not
+// found
+type ErrNoUsername struct{ gophercloud.ErrInvalidInput }
+
+func (e ErrNoUsername) Error() string {
+	return "Environment variable OS_USERNAME needs to be set."
+}
+
+// ErrNoPassword is the error when the OS_PASSWORD environment variable is not
+// found
+type ErrNoPassword struct{ gophercloud.ErrInvalidInput }
+
+func (e ErrNoPassword) Error() string {
+	return "Environment variable OS_PASSWORD needs to be set."
+}
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/requests.go b/openstack/identity/v2/extensions/admin/roles/requests.go
new file mode 100644
index 0000000..50228c9
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests.go
@@ -0,0 +1,32 @@
+package roles
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	return pagination.NewPager(client, rootURL(client), func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// AddUser 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 AddUser(client *gophercloud.ServiceClient, tenantID, userID, roleID string) (r UserRoleResult) {
+	_, r.Err = client.Put(userRoleURL(client, tenantID, userID, roleID), nil, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// DeleteUser 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 DeleteUser(client *gophercloud.ServiceClient, tenantID, userID, roleID string) (r UserRoleResult) {
+	_, r.Err = client.Delete(userRoleURL(client, tenantID, userID, roleID), nil)
+	return
+}
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..28de6bb
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/results.go
@@ -0,0 +1,47 @@
+package roles
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 (r RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(r)
+	return len(users) == 0, err
+}
+
+// ExtractRoles returns a slice of roles contained in a single page of results.
+func ExtractRoles(r pagination.Page) ([]Role, error) {
+	var s struct {
+		Roles []Role `json:"roles"`
+	}
+	err := (r.(RolePage)).ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/identity/v2/extensions/admin/roles/testing/doc.go
new file mode 100644
index 0000000..70ba643
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_extensions_admin_roles_v2
+package testing
diff --git a/openstack/identity/v2/extensions/admin/roles/testing/fixtures.go b/openstack/identity/v2/extensions/admin/roles/testing/fixtures.go
new file mode 100644
index 0000000..498c161
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/testing/fixtures.go
@@ -0,0 +1,48 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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/testing/requests_test.go b/openstack/identity/v2/extensions/admin/roles/testing/requests_test.go
new file mode 100644
index 0000000..8cf5395
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/testing/requests_test.go
@@ -0,0 +1,65 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := roles.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := roles.ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []roles.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 TestAddUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := roles.AddUser(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := roles.DeleteUser(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
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..e4661e8
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/urls.go
@@ -0,0 +1,21 @@
+package roles
+
+import "github.com/gophercloud/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..cf6cc81
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,46 @@
+package extensions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	common "github.com/gophercloud/gophercloud/openstack/common/extensions"
+	"github.com/gophercloud/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)
+	return len(is) == 0, err
+}
+
+// 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 s struct {
+		Extensions struct {
+			Values []common.Extension `json:"values"`
+		} `json:"extensions"`
+	}
+	err := page.(ExtensionPage).ExtractInto(&s)
+	return s.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/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/testing/delegate_test.go b/openstack/identity/v2/extensions/testing/delegate_test.go
new file mode 100644
index 0000000..e7869d8
--- /dev/null
+++ b/openstack/identity/v2/extensions/testing/delegate_test.go
@@ -0,0 +1,39 @@
+package testing
+
+import (
+	"testing"
+
+	common "github.com/gophercloud/gophercloud/openstack/common/extensions/testing"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListExtensionsSuccessfully(t)
+
+	count := 0
+	err := extensions.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := extensions.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 := extensions.Get(client.ServiceClient(), "agent").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/openstack/identity/v2/extensions/testing/doc.go b/openstack/identity/v2/extensions/testing/doc.go
new file mode 100644
index 0000000..6d4b67d
--- /dev/null
+++ b/openstack/identity/v2/extensions/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_extensions_v2
+package testing
diff --git a/openstack/identity/v2/extensions/testing/fixtures.go b/openstack/identity/v2/extensions/testing/fixtures.go
new file mode 100644
index 0000000..60afb74
--- /dev/null
+++ b/openstack/identity/v2/extensions/testing/fixtures.go
@@ -0,0 +1,58 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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/requests.go b/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..b9d7de6
--- /dev/null
+++ b/openstack/identity/v2/tenants/requests.go
@@ -0,0 +1,29 @@
+package tenants
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 {
+	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, func(r pagination.PageResult) pagination.Page {
+		return TenantPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
diff --git a/openstack/identity/v2/tenants/results.go b/openstack/identity/v2/tenants/results.go
new file mode 100644
index 0000000..3ce1e67
--- /dev/null
+++ b/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,53 @@
+package tenants
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"id"`
+
+	// Name is a friendlier user-facing name for this tenant.
+	Name string `json:"name"`
+
+	// Description is a human-readable explanation of this Tenant's purpose.
+	Description string `json:"description"`
+
+	// Enabled indicates whether or not a tenant is active.
+	Enabled bool `json:"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 (r TenantPage) IsEmpty() (bool, error) {
+	tenants, err := ExtractTenants(r)
+	return len(tenants) == 0, err
+}
+
+// NextPageURL extracts the "next" link from the tenants_links section of the result.
+func (r TenantPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"tenants_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractTenants returns a slice of Tenants contained in a single page of results.
+func ExtractTenants(r pagination.Page) ([]Tenant, error) {
+	var s struct {
+		Tenants []Tenant `json:"tenants"`
+	}
+	err := (r.(TenantPage)).ExtractInto(&s)
+	return s.Tenants, err
+}
diff --git a/openstack/identity/v2/tenants/testing/doc.go b/openstack/identity/v2/tenants/testing/doc.go
new file mode 100644
index 0000000..57aaa1f
--- /dev/null
+++ b/openstack/identity/v2/tenants/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_tenants_v2
+package testing
diff --git a/openstack/identity/v2/tenants/testing/fixtures.go b/openstack/identity/v2/tenants/testing/fixtures.go
new file mode 100644
index 0000000..7ddba45
--- /dev/null
+++ b/openstack/identity/v2/tenants/testing/fixtures.go
@@ -0,0 +1,64 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 = tenants.Tenant{
+	ID:          "1234",
+	Name:        "Red Team",
+	Description: "The team that is red",
+	Enabled:     true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = tenants.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 = []tenants.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/testing/requests_test.go b/openstack/identity/v2/tenants/testing/requests_test.go
new file mode 100644
index 0000000..2a9b71c
--- /dev/null
+++ b/openstack/identity/v2/tenants/testing/requests_test.go
@@ -0,0 +1,30 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListTenantsSuccessfully(t)
+
+	count := 0
+	err := tenants.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+
+		actual, err := tenants.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/urls.go b/openstack/identity/v2/tenants/urls.go
new file mode 100644
index 0000000..101599b
--- /dev/null
+++ b/openstack/identity/v2/tenants/urls.go
@@ -0,0 +1,7 @@
+package tenants
+
+import "github.com/gophercloud/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/requests.go b/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..4983031
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,99 @@
+package tokens
+
+import "github.com/gophercloud/gophercloud"
+
+type PasswordCredentialsV2 struct {
+	Username string `json:"username" required:"true"`
+	Password string `json:"password" required:"true"`
+}
+
+type TokenCredentialsV2 struct {
+	ID string `json:"id,omitempty" required:"true"`
+}
+
+// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptionsV2 struct {
+	PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"`
+
+	// 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   string `json:"tenantId,omitempty"`
+	TenantName string `json:"tenantName,omitempty"`
+
+	// TokenCredentials allows users to authenticate (possibly as another user) with an
+	// authentication token ID.
+	TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"`
+}
+
+// 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.
+	ToTokenV2CreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions are the valid options for Openstack Identity v2 authentication.
+// For field descriptions, see gophercloud.AuthOptions.
+type AuthOptions struct {
+	IdentityEndpoint string `json:"-"`
+	Username         string `json:"username,omitempty"`
+	Password         string `json:"password,omitempty"`
+	TenantID         string `json:"tenantId,omitempty"`
+	TenantName       string `json:"tenantName,omitempty"`
+	AllowReauth      bool   `json:"-"`
+	TokenID          string
+}
+
+// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
+// interface in the v2 tokens package
+func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
+	v2Opts := AuthOptionsV2{
+		TenantID:   opts.TenantID,
+		TenantName: opts.TenantName,
+	}
+
+	if opts.Password != "" {
+		v2Opts.PasswordCredentials = &PasswordCredentialsV2{
+			Username: opts.Username,
+			Password: opts.Password,
+		}
+	} else {
+		v2Opts.TokenCredentials = &TokenCredentialsV2{
+			ID: opts.TokenID,
+		}
+	}
+
+	b, err := gophercloud.BuildRequestBody(v2Opts, "auth")
+	if err != nil {
+		return nil, err
+	}
+	return b, 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) (r CreateResult) {
+	b, err := auth.ToTokenV2CreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes:     []int{200, 203},
+		MoreHeaders: map[string]string{"X-Auth-Token": ""},
+	})
+	return
+}
+
+// Get validates and retrieves information for user's token.
+func Get(client *gophercloud.ServiceClient, token string) (r GetResult) {
+	_, r.Err = client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 203},
+	})
+	return
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..6b36493
--- /dev/null
+++ b/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,144 @@
+package tokens
+
+import (
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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
+}
+
+// Role is a role for a user.
+type Role struct {
+	Name string `json:"name"`
+}
+
+// User is an OpenStack user.
+type User struct {
+	ID       string `json:"id"`
+	Name     string `json:"name"`
+	UserName string `json:"username"`
+	Roles    []Role `json:"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 `json:"tenantId"`
+	PublicURL   string `json:"publicURL"`
+	InternalURL string `json:"internalURL"`
+	AdminURL    string `json:"adminURL"`
+	Region      string `json:"region"`
+	VersionID   string `json:"versionId"`
+	VersionInfo string `json:"versionInfo"`
+	VersionList string `json:"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 `json:"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 `json:"type"`
+
+	// Endpoints will let the caller iterate over all the different endpoints that may exist for
+	// the service.
+	Endpoints []Endpoint `json:"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 (r CreateResult) ExtractToken() (*Token, error) {
+	var s struct {
+		Access struct {
+			Token struct {
+				Expires string         `json:"expires"`
+				ID      string         `json:"id"`
+				Tenant  tenants.Tenant `json:"tenant"`
+			} `json:"token"`
+		} `json:"access"`
+	}
+
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return nil, err
+	}
+
+	expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Token{
+		ID:        s.Access.Token.ID,
+		ExpiresAt: expiresTs,
+		Tenant:    s.Access.Token.Tenant,
+	}, nil
+}
+
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+	var s struct {
+		Access struct {
+			Entries []CatalogEntry `json:"serviceCatalog"`
+		} `json:"access"`
+	}
+	err := r.ExtractInto(&s)
+	return &ServiceCatalog{Entries: s.Access.Entries}, err
+}
+
+// ExtractUser returns the User from a GetResult.
+func (r GetResult) ExtractUser() (*User, error) {
+	var s struct {
+		Access struct {
+			User User `json:"user"`
+		} `json:"access"`
+	}
+	err := r.ExtractInto(&s)
+	return &s.Access.User, err
+}
diff --git a/openstack/identity/v2/tokens/testing/doc.go b/openstack/identity/v2/tokens/testing/doc.go
new file mode 100644
index 0000000..f9767eb
--- /dev/null
+++ b/openstack/identity/v2/tokens/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_tokens_v2
+package testing
diff --git a/openstack/identity/v2/tokens/testing/fixtures.go b/openstack/identity/v2/tokens/testing/fixtures.go
new file mode 100644
index 0000000..d3a8f24
--- /dev/null
+++ b/openstack/identity/v2/tokens/testing/fixtures.go
@@ -0,0 +1,194 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	thclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &tokens.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 = &tokens.ServiceCatalog{
+	Entries: []tokens.CatalogEntry{
+		{
+			Name: "inscrutablewalrus",
+			Type: "something",
+			Endpoints: []tokens.Endpoint{
+				{
+					PublicURL: "http://something0:1234/v2/",
+					Region:    "region0",
+				},
+				{
+					PublicURL: "http://something1:1234/v2/",
+					Region:    "region1",
+				},
+			},
+		},
+		{
+			Name: "arbitrarypenguin",
+			Type: "else",
+			Endpoints: []tokens.Endpoint{
+				{
+					PublicURL: "http://else0:4321/v3/",
+					Region:    "region0",
+				},
+			},
+		},
+	},
+}
+
+// ExpectedUser is the token that should be parsed from TokenGetResponse.
+var ExpectedUser = &tokens.User{
+	ID:       "a530fefc3d594c4ba2693a4ecd6be74e",
+	Name:     "apiserver",
+	Roles:    []tokens.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 tokens.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 tokens.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/testing/requests_test.go b/openstack/identity/v2/tokens/testing/requests_test.go
new file mode 100644
index 0000000..b687a92
--- /dev/null
+++ b/openstack/identity/v2/tokens/testing/requests_test.go
@@ -0,0 +1,104 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) tokens.CreateResult {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleTokenPost(t, requestJSON)
+
+	return tokens.Create(client.ServiceClient(), options)
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleTokenPost(t, "")
+
+	actualErr := tokens.Create(client.ServiceClient(), 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 TestRequireUsername(t *testing.T) {
+	options := gophercloud.AuthOptions{
+		Password: "thing",
+	}
+
+	tokenPostErr(t, options, gophercloud.ErrMissingInput{Argument: "Username"})
+}
+
+func tokenGet(t *testing.T, tokenId string) tokens.GetResult {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleTokenGet(t, tokenId)
+	return tokens.Get(client.ServiceClient(), tokenId)
+}
+
+func TestGetWithToken(t *testing.T) {
+	GetIsSuccessful(t, tokenGet(t, "db22caf43c934e6c829087c41ff8d8d6"))
+}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..ee0a28f
--- /dev/null
+++ b/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,13 @@
+package tokens
+
+import "github.com/gophercloud/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/requests.go b/openstack/identity/v2/users/requests.go
new file mode 100644
index 0000000..37fcd38
--- /dev/null
+++ b/openstack/identity/v2/users/requests.go
@@ -0,0 +1,106 @@
+package users
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List lists the existing users.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, rootURL(client), func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// 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     string `json:"name,omitempty"`
+	Username string `json:"username,omitempty"`
+	// The ID of the tenant to which you want to assign this user.
+	TenantID string `json:"tenantId,omitempty"`
+	// Indicates whether this user is enabled or not.
+	Enabled *bool `json:"enabled,omitempty"`
+	// The email address of this user.
+	Email string `json:"email,omitempty"`
+}
+
+// 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) {
+	if opts.Name == "" && opts.Username == "" {
+		err := gophercloud.ErrMissingInput{}
+		err.Argument = "users.CreateOpts.Name/users.CreateOpts.Username"
+		err.Info = "Either a Name or Username must be provided"
+		return nil, err
+	}
+	return gophercloud.BuildRequestBody(opts, "user")
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToUserCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(ResourceURL(client, id), &r.Body, nil)
+	return
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() (map[string]interface{}, error)
+}
+
+// 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{}, error) {
+	return gophercloud.BuildRequestBody(opts, "user")
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToUserUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(ResourceURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(ResourceURL(client, id), nil)
+	return
+}
+
+// ListRoles lists the existing roles that can be assigned to users.
+func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager {
+	return pagination.NewPager(client, listRolesURL(client, tenantID, userID), func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/openstack/identity/v2/users/results.go b/openstack/identity/v2/users/results.go
new file mode 100644
index 0000000..c493383
--- /dev/null
+++ b/openstack/identity/v2/users/results.go
@@ -0,0 +1,110 @@
+package users
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"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 (r UserPage) IsEmpty() (bool, error) {
+	users, err := ExtractUsers(r)
+	return len(users) == 0, err
+}
+
+// ExtractUsers returns a slice of Tenants contained in a single page of results.
+func ExtractUsers(r pagination.Page) ([]User, error) {
+	var s struct {
+		Users []User `json:"users"`
+	}
+	err := (r.(UserPage)).ExtractInto(&s)
+	return s.Users, err
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (r RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(r)
+	return len(users) == 0, err
+}
+
+// ExtractRoles returns a slice of Roles contained in a single page of results.
+func ExtractRoles(r pagination.Page) ([]Role, error) {
+	var s struct {
+		Roles []Role `json:"roles"`
+	}
+	err := (r.(RolePage)).ExtractInto(&s)
+	return s.Roles, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as a User, if possible.
+func (r commonResult) Extract() (*User, error) {
+	var s struct {
+		User *User `json:"user"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/identity/v2/users/testing/doc.go
new file mode 100644
index 0000000..a007def
--- /dev/null
+++ b/openstack/identity/v2/users/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_users_v2
+package testing
diff --git a/openstack/identity/v2/users/testing/fixtures.go b/openstack/identity/v2/users/testing/fixtures.go
new file mode 100644
index 0000000..8626da2
--- /dev/null
+++ b/openstack/identity/v2/users/testing/fixtures.go
@@ -0,0 +1,163 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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",
+		    "tenantId": "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/testing/requests_test.go b/openstack/identity/v2/users/testing/requests_test.go
new file mode 100644
index 0000000..3cb047e
--- /dev/null
+++ b/openstack/identity/v2/users/testing/requests_test.go
@@ -0,0 +1,161 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListUserResponse(t)
+
+	count := 0
+
+	err := users.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := users.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		expected := []users.User{
+			{
+				ID:       "u1000",
+				Name:     "John Smith",
+				Username: "jqsmith",
+				Email:    "john.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+			{
+				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 := users.CreateOpts{
+		Name:     "new_user",
+		TenantID: "12345",
+		Enabled:  gophercloud.Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &users.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 := users.Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &users.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 := users.UpdateOpts{
+		Name:    "new_name",
+		Enabled: gophercloud.Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := users.Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &users.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 := users.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 := users.ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := users.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		expected := []users.Role{
+			{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"},
+			{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/users/urls.go b/openstack/identity/v2/users/urls.go
new file mode 100644
index 0000000..89f66f2
--- /dev/null
+++ b/openstack/identity/v2/users/urls.go
@@ -0,0 +1,21 @@
+package users
+
+import "github.com/gophercloud/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/requests.go b/openstack/identity/v3/endpoints/requests.go
new file mode 100644
index 0000000..fc44365
--- /dev/null
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -0,0 +1,102 @@
+package endpoints
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type CreateOptsBuilder interface {
+	ToEndpointCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains the subset of Endpoint attributes that should be used to create an Endpoint.
+type CreateOpts struct {
+	Availability gophercloud.Availability `json:"interface" required:"true"`
+	Name         string                   `json:"name" required:"true"`
+	Region       string                   `json:"region,omitempty"`
+	URL          string                   `json:"url" required:"true"`
+	ServiceID    string                   `json:"service_id" required:"true"`
+}
+
+func (opts CreateOpts) ToEndpointCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "endpoint")
+}
+
+// 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 CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToEndpointCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(listURL(client), &b, &r.Body, nil)
+	return
+}
+
+type ListOptsBuilder interface {
+	ToEndpointListParams() (string, error)
+}
+
+// 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"`
+}
+
+func (opts ListOpts) ToEndpointListParams() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	u := listURL(client)
+	if opts != nil {
+		q, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		u += q.String()
+	}
+	return pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page {
+		return EndpointPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+type UpdateOptsBuilder interface {
+	ToEndpointUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the subset of Endpoint attributes that should be used to update an Endpoint.
+type UpdateOpts struct {
+	Availability gophercloud.Availability `json:"interface,omitempty"`
+	Name         string                   `json:"name,omitempty"`
+	Region       string                   `json:"region,omitempty"`
+	URL          string                   `json:"url,omitempty"`
+	ServiceID    string                   `json:"service_id,omitempty"`
+}
+
+func (opts UpdateOpts) ToEndpointUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "endpoint")
+}
+
+// Update changes an existing endpoint with new data.
+func Update(client *gophercloud.ServiceClient, endpointID string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToEndpointUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Patch(endpointURL(client, endpointID), &b, &r.Body, nil)
+	return
+}
+
+// Delete removes an endpoint from the service catalog.
+func Delete(client *gophercloud.ServiceClient, endpointID string) (r DeleteResult) {
+	_, r.Err = client.Delete(endpointURL(client, endpointID), nil)
+	return
+}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
new file mode 100644
index 0000000..f769881
--- /dev/null
+++ b/openstack/identity/v3/endpoints/results.go
@@ -0,0 +1,65 @@
+package endpoints
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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) {
+	var s struct {
+		Endpoint *Endpoint `json:"endpoint"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Endpoint, err
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult 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
+}
+
+// Endpoint describes the entry point for another service's API.
+type Endpoint struct {
+	ID           string                   `json:"id"`
+	Availability gophercloud.Availability `json:"interface"`
+	Name         string                   `json:"name"`
+	Region       string                   `json:"region"`
+	ServiceID    string                   `json:"service_id"`
+	URL          string                   `json:"url"`
+}
+
+// EndpointPage is a single page of Endpoint results.
+type EndpointPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if no Endpoints were returned.
+func (r EndpointPage) IsEmpty() (bool, error) {
+	es, err := ExtractEndpoints(r)
+	return len(es) == 0, err
+}
+
+// ExtractEndpoints extracts an Endpoint slice from a Page.
+func ExtractEndpoints(r pagination.Page) ([]Endpoint, error) {
+	var s struct {
+		Endpoints []Endpoint `json:"endpoints"`
+	}
+	err := (r.(EndpointPage)).ExtractInto(&s)
+	return s.Endpoints, err
+}
diff --git a/openstack/identity/v3/endpoints/testing/doc.go b/openstack/identity/v3/endpoints/testing/doc.go
new file mode 100644
index 0000000..b6e89ef
--- /dev/null
+++ b/openstack/identity/v3/endpoints/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_endpoints_v3
+package testing
diff --git a/openstack/identity/v3/endpoints/testing/requests_test.go b/openstack/identity/v3/endpoints/testing/requests_test.go
new file mode 100644
index 0000000..53d8488
--- /dev/null
+++ b/openstack/identity/v3/endpoints/testing/requests_test.go
@@ -0,0 +1,214 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreateSuccessful(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/endpoints", 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, `
+      {
+        "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 := endpoints.Create(client.ServiceClient(), endpoints.CreateOpts{
+		Availability: gophercloud.AvailabilityPublic,
+		Name:         "the-endiest-of-points",
+		Region:       "underground",
+		URL:          "https://1.2.3.4:9000/",
+		ServiceID:    "asdfasdfasdfasdf",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &endpoints.Endpoint{
+		ID:           "12",
+		Availability: gophercloud.AvailabilityPublic,
+		Name:         "the-endiest-of-points",
+		Region:       "underground",
+		ServiceID:    "asdfasdfasdfasdf",
+		URL:          "https://1.2.3.4:9000/",
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListEndpoints(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/endpoints", 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, `
+			{
+				"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
+	endpoints.List(client.ServiceClient(), endpoints.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := endpoints.ExtractEndpoints(page)
+		if err != nil {
+			t.Errorf("Failed to extract endpoints: %v", err)
+			return false, err
+		}
+
+		expected := []endpoints.Endpoint{
+			{
+				ID:           "12",
+				Availability: gophercloud.AvailabilityPublic,
+				Name:         "the-endiest-of-points",
+				Region:       "underground",
+				ServiceID:    "asdfasdfasdfasdf",
+				URL:          "https://1.2.3.4:9000/",
+			},
+			{
+				ID:           "13",
+				Availability: gophercloud.AvailabilityInternal,
+				Name:         "shhhh",
+				Region:       "underground",
+				ServiceID:    "asdfasdfasdfasdf",
+				URL:          "https://1.2.3.4:9001/",
+			},
+		}
+		th.AssertDeepEquals(t, expected, actual)
+		return true, nil
+	})
+	th.AssertEquals(t, 1, count)
+}
+
+func TestUpdateEndpoint(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.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 := endpoints.Update(client.ServiceClient(), "12", endpoints.UpdateOpts{
+		Name:   "renamed",
+		Region: "somewhere-else",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Update: %v", err)
+	}
+
+	expected := &endpoints.Endpoint{
+		ID:           "12",
+		Availability: gophercloud.AvailabilityPublic,
+		Name:         "renamed",
+		Region:       "somewhere-else",
+		ServiceID:    "asdfasdfasdfasdf",
+		URL:          "https://1.2.3.4:9000/",
+	}
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteEndpoint(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/endpoints/34", 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)
+	})
+
+	res := endpoints.Delete(client.ServiceClient(), "34")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/identity/v3/endpoints/urls.go b/openstack/identity/v3/endpoints/urls.go
new file mode 100644
index 0000000..80cf57e
--- /dev/null
+++ b/openstack/identity/v3/endpoints/urls.go
@@ -0,0 +1,11 @@
+package endpoints
+
+import "github.com/gophercloud/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/extensions/trusts/requests.go b/openstack/identity/v3/extensions/trusts/requests.go
new file mode 100644
index 0000000..999dd73
--- /dev/null
+++ b/openstack/identity/v3/extensions/trusts/requests.go
@@ -0,0 +1,34 @@
+package trusts
+
+import "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+
+type AuthOptsExt struct {
+	tokens.AuthOptionsBuilder
+	TrustID string `json:"id"`
+}
+
+func (opts AuthOptsExt) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
+	return opts.AuthOptionsBuilder.ToTokenV3CreateMap(scope)
+}
+
+func (opts AuthOptsExt) ToTokenV3ScopeMap() (map[string]interface{}, error) {
+	b, err := opts.AuthOptionsBuilder.ToTokenV3ScopeMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if opts.TrustID != "" {
+		if b == nil {
+			b = make(map[string]interface{})
+		}
+		b["OS-TRUST:trust"] = map[string]interface{}{
+			"id": opts.TrustID,
+		}
+	}
+
+	return b, nil
+}
+
+func (opts AuthOptsExt) CanReauth() bool {
+	return opts.AuthOptionsBuilder.CanReauth()
+}
diff --git a/openstack/identity/v3/extensions/trusts/results.go b/openstack/identity/v3/extensions/trusts/results.go
new file mode 100644
index 0000000..bdd8e84
--- /dev/null
+++ b/openstack/identity/v3/extensions/trusts/results.go
@@ -0,0 +1,22 @@
+package trusts
+
+type TrusteeUser struct {
+	ID string `json:"id"`
+}
+
+type TrustorUser struct {
+	ID string `json:"id"`
+}
+
+type Trust struct {
+	ID                 string      `json:"id"`
+	Impersonation      bool        `json:"impersonation"`
+	TrusteeUser        TrusteeUser `json:"trustee_user"`
+	TrustorUser        TrustorUser `json:"trustor_user"`
+	RedelegatedTrustID string      `json:"redelegated_trust_id"`
+	RedelegationCount  int         `json:"redelegation_count"`
+}
+
+type TokenExt struct {
+	Trust Trust `json:"OS-TRUST:trust"`
+}
diff --git a/openstack/identity/v3/extensions/trusts/testing/doc.go b/openstack/identity/v3/extensions/trusts/testing/doc.go
new file mode 100644
index 0000000..e660e20
--- /dev/null
+++ b/openstack/identity/v3/extensions/trusts/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_extensions_trusts_v3
+package testing
diff --git a/openstack/identity/v3/extensions/trusts/testing/fixtures.go b/openstack/identity/v3/extensions/trusts/testing/fixtures.go
new file mode 100644
index 0000000..e311526
--- /dev/null
+++ b/openstack/identity/v3/extensions/trusts/testing/fixtures.go
@@ -0,0 +1,67 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+	"github.com/gophercloud/gophercloud/testhelper"
+)
+
+// HandleCreateTokenWithTrustID verifies that providing certain AuthOptions and Scope results in an expected JSON structure.
+func HandleCreateTokenWithTrustID(t *testing.T, options tokens.AuthOptionsBuilder, requestJSON string) {
+	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": "2013-02-27T18:30:59.999999Z",
+        "issued_at": "2013-02-27T16:30:59.999999Z",
+        "methods": [
+            "password"
+        ],
+        "OS-TRUST:trust": {
+            "id": "fe0aef",
+            "impersonation": false,
+						"redelegated_trust_id": "3ba234",
+						"redelegation_count": 2,
+            "links": {
+                "self": "http://example.com/identity/v3/trusts/fe0aef"
+            },
+            "trustee_user": {
+                "id": "0ca8f6",
+                "links": {
+                    "self": "http://example.com/identity/v3/users/0ca8f6"
+                }
+            },
+            "trustor_user": {
+                "id": "bd263c",
+                "links": {
+                    "self": "http://example.com/identity/v3/users/bd263c"
+                }
+            }
+        },
+        "user": {
+            "domain": {
+                "id": "1789d1",
+                "links": {
+                    "self": "http://example.com/identity/v3/domains/1789d1"
+                },
+                "name": "example.com"
+            },
+            "email": "joe@example.com",
+            "id": "0ca8f6",
+            "links": {
+                "self": "http://example.com/identity/v3/users/0ca8f6"
+            },
+            "name": "Joe"
+        }
+    }
+}`)
+	})
+}
diff --git a/openstack/identity/v3/extensions/trusts/testing/requests_test.go b/openstack/identity/v3/extensions/trusts/testing/requests_test.go
new file mode 100644
index 0000000..f8a65ad
--- /dev/null
+++ b/openstack/identity/v3/extensions/trusts/testing/requests_test.go
@@ -0,0 +1,74 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreateUserIDPasswordTrustID(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	ao := trusts.AuthOptsExt{
+		TrustID: "de0945a",
+		AuthOptionsBuilder: &tokens.AuthOptions{
+			UserID:   "me",
+			Password: "squirrel!",
+		},
+	}
+	HandleCreateTokenWithTrustID(t, ao, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": { "id": "me", "password": "squirrel!" }
+					}
+				},
+        "scope": {
+            "OS-TRUST:trust": {
+                "id": "de0945a"
+            }
+        }
+			}
+		}
+	`)
+
+	var actual struct {
+		tokens.Token
+		trusts.TokenExt
+	}
+	err := tokens.Create(client.ServiceClient(), ao).ExtractInto(&actual)
+	if err != nil {
+		t.Errorf("Create returned an error: %v", err)
+	}
+	expected := struct {
+		tokens.Token
+		trusts.TokenExt
+	}{
+		tokens.Token{
+			ExpiresAt: time.Date(2013, 02, 27, 18, 30, 59, 999999000, time.UTC),
+		},
+		trusts.TokenExt{
+			Trust: trusts.Trust{
+				ID:            "fe0aef",
+				Impersonation: false,
+				TrusteeUser: trusts.TrusteeUser{
+					ID: "0ca8f6",
+				},
+				TrustorUser: trusts.TrustorUser{
+					ID: "bd263c",
+				},
+				RedelegatedTrustID: "3ba234",
+				RedelegationCount:  2,
+			},
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go
new file mode 100644
index 0000000..74a9b15
--- /dev/null
+++ b/openstack/identity/v3/projects/requests.go
@@ -0,0 +1,179 @@
+package projects
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to
+// the List request
+type ListOptsBuilder interface {
+	ToProjectListQuery() (string, error)
+}
+
+// ListOpts allows you to query the List method.
+type ListOpts struct {
+	// DomainID filters the response by a domain ID.
+	DomainID string `q:"domain_id"`
+
+	// Enabled filters the response by enabled projects.
+	Enabled *bool `q:"enabled"`
+
+	// IsDomain filters the response by projects that are domains.
+	// Setting this to true is effectively listing domains.
+	IsDomain *bool `q:"is_domain"`
+
+	// Name filters the response by project name.
+	Name string `q:"name"`
+
+	// ParentID filters the response by projects of a given parent project.
+	ParentID string `q:"parent_id"`
+}
+
+// ToProjectListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToProjectListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List enumerats the Projects to which the current token has access.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToProjectListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ProjectPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// GetOptsBuilder allows extensions to add additional parameters to
+// the Get request.
+type GetOptsBuilder interface {
+	ToProjectGetQuery() (string, error)
+}
+
+// GetOpts allows you to modify the details included in the Get request.
+type GetOpts struct{}
+
+// ToProjectGetQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToProjectGetQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// Get retrieves details on a single project, by ID.
+func Get(client *gophercloud.ServiceClient, id string, opts GetOptsBuilder) (r GetResult) {
+	url := getURL(client, id)
+	if opts != nil {
+		query, err := opts.ToProjectGetQuery()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		url += query
+	}
+
+	_, r.Err = client.Get(url, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to
+// the Create request.
+type CreateOptsBuilder interface {
+	ToProjectCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts allows you to modify the details included in the Create request.
+type CreateOpts struct {
+	// DomainID is the ID this project will belong under.
+	DomainID string `json:"domain_id,omitempty"`
+
+	// Enabled sets the project status to enabled or disabled.
+	Enabled *bool `json:"enabled,omitempty"`
+
+	// IsDomain indicates if this project is a domain.
+	IsDomain *bool `json:"is_domain,omitempty"`
+
+	// Name is the name of the project.
+	Name string `json:"name,required"`
+
+	// ParentID specifies the parent project of this new project.
+	ParentID string `json:"parent_id,omitempty"`
+
+	// Description is the description of the project.
+	Description string `json:"description,omitempty"`
+}
+
+// ToProjectCreateMap formats a CreateOpts into a create request.
+func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "project")
+}
+
+// Create creates a new Project.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToProjectCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), &b, &r.Body, nil)
+	return
+}
+
+// Delete deletes a project.
+func Delete(client *gophercloud.ServiceClient, projectID string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, projectID), nil)
+	return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to
+// the Update request.
+type UpdateOptsBuilder interface {
+	ToProjectUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts allows you to modify the details included in the Update request.
+type UpdateOpts struct {
+	// DomainID is the ID this project will belong under.
+	DomainID string `json:"domain_id,omitempty"`
+
+	// Enabled sets the project status to enabled or disabled.
+	Enabled *bool `json:"enabled,omitempty"`
+
+	// IsDomain indicates if this project is a domain.
+	IsDomain *bool `json:"is_domain,omitempty"`
+
+	// Name is the name of the project.
+	Name string `json:"name,omitempty"`
+
+	// ParentID specifies the parent project of this new project.
+	ParentID string `json:"parent_id,omitempty"`
+
+	// Description is the description of the project.
+	Description string `json:"description,omitempty"`
+}
+
+// ToUpdateCreateMap formats a UpdateOpts into an update request.
+func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "project")
+}
+
+// Update modifies the attributes of a project.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToProjectUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
diff --git a/openstack/identity/v3/projects/results.go b/openstack/identity/v3/projects/results.go
new file mode 100644
index 0000000..a441e7f
--- /dev/null
+++ b/openstack/identity/v3/projects/results.go
@@ -0,0 +1,98 @@
+package projects
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type projectResult struct {
+	gophercloud.Result
+}
+
+// GetResult temporarily contains the response from the Get call.
+type GetResult struct {
+	projectResult
+}
+
+// CreateResult temporarily contains the reponse from the Create call.
+type CreateResult struct {
+	projectResult
+}
+
+// DeleteResult temporarily contains the response from the Delete call.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// UpdateResult temporarily contains the response from the Update call.
+type UpdateResult struct {
+	projectResult
+}
+
+// Project is a base unit of ownership.
+type Project struct {
+	// IsDomain indicates whether the project is a domain.
+	IsDomain bool `json:"is_domain"`
+
+	// Description is the description of the project.
+	Description string `json:"description"`
+
+	// DomainID is the domain ID the project belongs to.
+	DomainID string `json:"domain_id"`
+
+	// Enabled is whether or not the project is enabled.
+	Enabled bool `json:"enabled"`
+
+	// ID is the unique ID of the project.
+	ID string `json:"id"`
+
+	// Name is the name of the project.
+	Name string `json:"name"`
+
+	// ParentID is the parent_id of the project.
+	ParentID string `json:"parent_id"`
+}
+
+// ProjectPage is a single page of Project results.
+type ProjectPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty determines whether or not a page of Projects contains any results.
+func (r ProjectPage) IsEmpty() (bool, error) {
+	projects, err := ExtractProjects(r)
+	return len(projects) == 0, err
+}
+
+// NextPageURL extracts the "next" link from the links section of the result.
+func (r ProjectPage) NextPageURL() (string, error) {
+	var s struct {
+		Links struct {
+			Next     string `json:"next"`
+			Previous string `json:"previous"`
+		} `json:"links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return s.Links.Next, err
+}
+
+// ExtractProjects returns a slice of Projects contained in a single page of results.
+func ExtractProjects(r pagination.Page) ([]Project, error) {
+	var s struct {
+		Projects []Project `json:"projects"`
+	}
+	err := (r.(ProjectPage)).ExtractInto(&s)
+	return s.Projects, err
+}
+
+// Extract interprets any projectResults as a Project.
+func (r projectResult) Extract() (*Project, error) {
+	var s struct {
+		Project *Project `json:"project"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Project, err
+}
diff --git a/openstack/identity/v3/projects/testing/fixtures.go b/openstack/identity/v3/projects/testing/fixtures.go
new file mode 100644
index 0000000..caa5567
--- /dev/null
+++ b/openstack/identity/v3/projects/testing/fixtures.go
@@ -0,0 +1,192 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Project results.
+const ListOutput = `
+{
+  "projects": [
+    {
+      "is_domain": false,
+      "description": "The team that is red",
+      "domain_id": "default",
+      "enabled": true,
+      "id": "1234",
+      "name": "Red Team",
+      "parent_id": null
+    },
+    {
+      "is_domain": false,
+      "description": "The team that is blue",
+      "domain_id": "default",
+      "enabled": true,
+      "id": "9876",
+      "name": "Blue Team",
+      "parent_id": null
+    }
+  ],
+  "links": {
+    "next": null,
+    "previous": null
+  }
+}
+`
+
+// GetOutput provides a Get result.
+const GetOutput = `
+{
+  "project": {
+		"is_domain": false,
+		"description": "The team that is red",
+		"domain_id": "default",
+		"enabled": true,
+		"id": "1234",
+		"name": "Red Team",
+		"parent_id": null
+  }
+}
+`
+
+// CreateRequest provides the input to a Create request.
+const CreateRequest = `
+{
+  "project": {
+		"description": "The team that is red",
+		"name": "Red Team"
+  }
+}
+`
+
+// UpdateRequest provides the input to an Update request.
+const UpdateRequest = `
+{
+  "project": {
+		"description": "The team that is bright red",
+		"name": "Bright Red Team"
+  }
+}
+`
+
+// UpdateOutput provides an Update response.
+const UpdateOutput = `
+{
+  "project": {
+		"is_domain": false,
+		"description": "The team that is bright red",
+		"domain_id": "default",
+		"enabled": true,
+		"id": "1234",
+		"name": "Bright Red Team",
+		"parent_id": null
+  }
+}
+`
+
+// RedTeam is a Project fixture.
+var RedTeam = projects.Project{
+	IsDomain:    false,
+	Description: "The team that is red",
+	DomainID:    "default",
+	Enabled:     true,
+	ID:          "1234",
+	Name:        "Red Team",
+	ParentID:    "",
+}
+
+// BlueTeam is a Project fixture.
+var BlueTeam = projects.Project{
+	IsDomain:    false,
+	Description: "The team that is blue",
+	DomainID:    "default",
+	Enabled:     true,
+	ID:          "9876",
+	Name:        "Blue Team",
+	ParentID:    "",
+}
+
+// UpdatedRedTeam is a Project Fixture.
+var UpdatedRedTeam = projects.Project{
+	IsDomain:    false,
+	Description: "The team that is bright red",
+	DomainID:    "default",
+	Enabled:     true,
+	ID:          "1234",
+	Name:        "Bright Red Team",
+	ParentID:    "",
+}
+
+// ExpectedProjectSlice is the slice of projects expected to be returned from ListOutput.
+var ExpectedProjectSlice = []projects.Project{RedTeam, BlueTeam}
+
+// HandleListProjectsSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that responds with a list of two tenants.
+func HandleListProjectsSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects", 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)
+	})
+}
+
+// HandleGetProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that responds with a single project.
+func HandleGetProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects/1234", 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, GetOutput)
+	})
+}
+
+// HandleCreateProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that tests project creation.
+func HandleCreateProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects", 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, CreateRequest)
+
+		w.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, GetOutput)
+	})
+}
+
+// HandleDeleteProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that tests project deletion.
+func HandleDeleteProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects/1234", 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)
+	})
+}
+
+// HandleUpdateProjectSuccessfully creates an HTTP handler at `/projects` on the
+// test handler mux that tests project updates.
+func HandleUpdateProjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/projects/1234", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, UpdateRequest)
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, UpdateOutput)
+	})
+}
diff --git a/openstack/identity/v3/projects/testing/requests_test.go b/openstack/identity/v3/projects/testing/requests_test.go
new file mode 100644
index 0000000..5782480
--- /dev/null
+++ b/openstack/identity/v3/projects/testing/requests_test.go
@@ -0,0 +1,79 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListProjects(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListProjectsSuccessfully(t)
+
+	count := 0
+	err := projects.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+
+		actual, err := projects.ExtractProjects(page)
+		th.AssertNoErr(t, err)
+
+		th.CheckDeepEquals(t, ExpectedProjectSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, count, 1)
+}
+
+func TestGetProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetProjectSuccessfully(t)
+
+	actual, err := projects.Get(client.ServiceClient(), "1234", nil).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, RedTeam, *actual)
+}
+
+func TestCreateProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateProjectSuccessfully(t)
+
+	createOpts := projects.CreateOpts{
+		Name:        "Red Team",
+		Description: "The team that is red",
+	}
+
+	actual, err := projects.Create(client.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, RedTeam, *actual)
+}
+
+func TestDeleteProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteProjectSuccessfully(t)
+
+	res := projects.Delete(client.ServiceClient(), "1234")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateProject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateProjectSuccessfully(t)
+
+	updateOpts := projects.UpdateOpts{
+		Name:        "Bright Red Team",
+		Description: "The team that is bright red",
+	}
+
+	actual, err := projects.Update(client.ServiceClient(), "1234", updateOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, UpdatedRedTeam, *actual)
+}
diff --git a/openstack/identity/v3/projects/urls.go b/openstack/identity/v3/projects/urls.go
new file mode 100644
index 0000000..e26cf36
--- /dev/null
+++ b/openstack/identity/v3/projects/urls.go
@@ -0,0 +1,23 @@
+package projects
+
+import "github.com/gophercloud/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("projects")
+}
+
+func getURL(client *gophercloud.ServiceClient, projectID string) string {
+	return client.ServiceURL("projects", projectID)
+}
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("projects")
+}
+
+func deleteURL(client *gophercloud.ServiceClient, projectID string) string {
+	return client.ServiceURL("projects", projectID)
+}
+
+func updateURL(client *gophercloud.ServiceClient, projectID string) string {
+	return client.ServiceURL("projects", projectID)
+}
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..de65c51
--- /dev/null
+++ b/openstack/identity/v3/roles/requests.go
@@ -0,0 +1,47 @@
+package roles
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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)
+	return q.String(), err
+}
+
+// ListAssignments enumerates the roles assigned to a specified resource.
+func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager {
+	url := listAssignmentsURL(client)
+	if opts != nil {
+		query, err := opts.ToRolesListAssignmentsQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return RoleAssignmentPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
diff --git a/openstack/identity/v3/roles/results.go b/openstack/identity/v3/roles/results.go
new file mode 100644
index 0000000..e8a3aa9
--- /dev/null
+++ b/openstack/identity/v3/roles/results.go
@@ -0,0 +1,67 @@
+package roles
+
+import "github.com/gophercloud/gophercloud/pagination"
+
+// 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:"project,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"`
+}
+
+// RoleAssignmentPage is a single page of RoleAssignments results.
+type RoleAssignmentPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if the page contains no results.
+func (r RoleAssignmentPage) IsEmpty() (bool, error) {
+	roleAssignments, err := ExtractRoleAssignments(r)
+	return len(roleAssignments) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (r RoleAssignmentPage) NextPageURL() (string, error) {
+	var s struct {
+		Links struct {
+			Next string `json:"next"`
+		} `json:"links"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Links.Next, err
+}
+
+// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection acquired from List.
+func ExtractRoleAssignments(r pagination.Page) ([]RoleAssignment, error) {
+	var s struct {
+		RoleAssignments []RoleAssignment `json:"role_assignments"`
+	}
+	err := (r.(RoleAssignmentPage)).ExtractInto(&s)
+	return s.RoleAssignments, err
+}
diff --git a/openstack/identity/v3/roles/testing/doc.go b/openstack/identity/v3/roles/testing/doc.go
new file mode 100644
index 0000000..37bcb85
--- /dev/null
+++ b/openstack/identity/v3/roles/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_roles_v3
+package testing
diff --git a/openstack/identity/v3/roles/testing/requests_test.go b/openstack/identity/v3/roles/testing/requests_test.go
new file mode 100644
index 0000000..dd9b704
--- /dev/null
+++ b/openstack/identity/v3/roles/testing/requests_test.go
@@ -0,0 +1,105 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/roles"
+	"github.com/gophercloud/gophercloud/pagination"
+	"github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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 := roles.ListAssignments(client.ServiceClient(), roles.ListAssignmentsOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := roles.ExtractRoleAssignments(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []roles.RoleAssignment{
+			{
+				Role:  roles.Role{ID: "123456"},
+				Scope: roles.Scope{Domain: roles.Domain{ID: "161718"}},
+				User:  roles.User{ID: "313233"},
+				Group: roles.Group{},
+			},
+			{
+				Role:  roles.Role{ID: "123456"},
+				Scope: roles.Scope{Project: roles.Project{ID: "456789"}},
+				User:  roles.User{ID: "313233"},
+				Group: roles.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/urls.go b/openstack/identity/v3/roles/urls.go
new file mode 100644
index 0000000..8d87b6e
--- /dev/null
+++ b/openstack/identity/v3/roles/urls.go
@@ -0,0 +1,7 @@
+package roles
+
+import "github.com/gophercloud/gophercloud"
+
+func listAssignmentsURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("role_assignments")
+}
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..bb7bb04
--- /dev/null
+++ b/openstack/identity/v3/services/requests.go
@@ -0,0 +1,64 @@
+package services
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Create adds a new service of the requested type to the catalog.
+func Create(client *gophercloud.ServiceClient, serviceType string) (r CreateResult) {
+	b := map[string]string{"type": serviceType}
+	_, r.Err = client.Post(listURL(client), b, &r.Body, nil)
+	return
+}
+
+type ListOptsBuilder interface {
+	ToServiceListMap() (string, error)
+}
+
+// ListOpts allows you to query the List method.
+type ListOpts struct {
+	ServiceType string `q:"type"`
+	PerPage     int    `q:"perPage"`
+	Page        int    `q:"page"`
+}
+
+func (opts ListOpts) ToServiceListMap() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List enumerates the services available to a specific user.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	u := listURL(client)
+	if opts != nil {
+		q, err := opts.ToServiceListMap()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		u += q
+	}
+	return pagination.NewPager(client, u, func(r pagination.PageResult) pagination.Page {
+		return ServicePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get returns additional information about a service, given its ID.
+func Get(client *gophercloud.ServiceClient, serviceID string) (r GetResult) {
+	_, r.Err = client.Get(serviceURL(client, serviceID), &r.Body, nil)
+	return
+}
+
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) (r UpdateResult) {
+	b := map[string]string{"type": serviceType}
+	_, r.Err = client.Patch(serviceURL(client, serviceID), &b, &r.Body, nil)
+	return
+}
+
+// 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) (r DeleteResult) {
+	_, r.Err = client.Delete(serviceURL(client, serviceID), nil)
+	return
+}
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
new file mode 100644
index 0000000..9ebcc20
--- /dev/null
+++ b/openstack/identity/v3/services/results.go
@@ -0,0 +1,68 @@
+package services
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+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) {
+	var s struct {
+		Service *Service `json:"service"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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`
+	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)
+	return len(services) == 0, err
+}
+
+// ExtractServices extracts a slice of Services from a Collection acquired from List.
+func ExtractServices(r pagination.Page) ([]Service, error) {
+	var s struct {
+		Services []Service `json:"services"`
+	}
+	err := (r.(ServicePage)).ExtractInto(&s)
+	return s.Services, err
+}
diff --git a/openstack/identity/v3/services/testing/doc.go b/openstack/identity/v3/services/testing/doc.go
new file mode 100644
index 0000000..e4f1167
--- /dev/null
+++ b/openstack/identity/v3/services/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_services_v3
+package testing
diff --git a/openstack/identity/v3/services/testing/requests_test.go b/openstack/identity/v3/services/testing/requests_test.go
new file mode 100644
index 0000000..0a065a2
--- /dev/null
+++ b/openstack/identity/v3/services/testing/requests_test.go
@@ -0,0 +1,187 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreateSuccessful(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services", 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, `{ "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"
+        }
+    }`)
+	})
+
+	expected := &services.Service{
+		Description: "Here's your service",
+		ID:          "1234",
+		Name:        "InscrutableOpenStackProjectName",
+		Type:        "compute",
+	}
+
+	actual, err := services.Create(client.ServiceClient(), "compute").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Create: %v", err)
+	}
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListSinglePage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", 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 := services.List(client.ServiceClient(), services.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := services.ExtractServices(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []services.Service{
+			{
+				Description: "Service One",
+				ID:          "1234",
+				Name:        "service-one",
+				Type:        "identity",
+			},
+			{
+				Description: "Service Two",
+				ID:          "9876",
+				Name:        "service-two",
+				Type:        "compute",
+			},
+		}
+		th.AssertDeepEquals(t, expected, actual)
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestGetSuccessful(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services/12345", 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, `
+			{
+				"service": {
+						"description": "Service One",
+						"id": "12345",
+						"name": "service-one",
+						"type": "identity"
+				}
+			}
+		`)
+	})
+
+	actual, err := services.Get(client.ServiceClient(), "12345").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &services.Service{
+		ID:          "12345",
+		Description: "Service One",
+		Name:        "service-one",
+		Type:        "identity",
+	}
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateSuccessful(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services/12345", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{ "type": "lasermagic" }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"service": {
+						"id": "12345",
+						"type": "lasermagic"
+				}
+			}
+		`)
+	})
+
+	expected := &services.Service{
+		ID:   "12345",
+		Type: "lasermagic",
+	}
+
+	actual, err := services.Update(client.ServiceClient(), "12345", "lasermagic").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteSuccessful(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/services/12345", 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)
+	})
+
+	res := services.Delete(client.ServiceClient(), "12345")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/identity/v3/services/urls.go b/openstack/identity/v3/services/urls.go
new file mode 100644
index 0000000..c5ae268
--- /dev/null
+++ b/openstack/identity/v3/services/urls.go
@@ -0,0 +1,11 @@
+package services
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/identity/v3/tokens/requests.go
new file mode 100644
index 0000000..ba4363b
--- /dev/null
+++ b/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,200 @@
+package tokens
+
+import "github.com/gophercloud/gophercloud"
+
+// Scope allows a created token to be limited to a specific domain or project.
+type Scope struct {
+	ProjectID   string `json:"scope.project.id,omitempty" not:"ProjectName,DomainID,DomainName"`
+	ProjectName string `json:"scope.project.name,omitempty"`
+	DomainID    string `json:"scope.project.id,omitempty" not:"ProjectName,ProjectID,DomainName"`
+	DomainName  string `json:"scope.project.id,omitempty"`
+}
+
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+	// ToTokenV3CreateMap assembles the Create request body, returning an error if parameters are
+	// missing or inconsistent.
+	ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error)
+	ToTokenV3ScopeMap() (map[string]interface{}, error)
+	CanReauth() bool
+}
+
+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 `json:"-"`
+
+	// 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 string `json:"username,omitempty"`
+	UserID   string `json:"id,omitempty"`
+
+	Password string `json:"password,omitempty"`
+
+	// At most one of DomainID and DomainName must be provided if using Username
+	// with Identity V3. Otherwise, either are optional.
+	DomainID   string `json:"id,omitempty"`
+	DomainName string `json:"name,omitempty"`
+
+	// 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 `json:"-"`
+
+	// TokenID allows users to authenticate (possibly as another user) with an
+	// authentication token ID.
+	TokenID string `json:"-"`
+
+	Scope Scope `json:"-"`
+}
+
+func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
+	gophercloudAuthOpts := gophercloud.AuthOptions{
+		Username:    opts.Username,
+		UserID:      opts.UserID,
+		Password:    opts.Password,
+		DomainID:    opts.DomainID,
+		DomainName:  opts.DomainName,
+		AllowReauth: opts.AllowReauth,
+		TokenID:     opts.TokenID,
+	}
+
+	return gophercloudAuthOpts.ToTokenV3CreateMap(scope)
+}
+
+func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
+	if opts.Scope.ProjectName != "" {
+		// ProjectName provided: either DomainID or DomainName must also be supplied.
+		// ProjectID may not be supplied.
+		if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
+			return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
+		}
+		if opts.Scope.ProjectID != "" {
+			return nil, gophercloud.ErrScopeProjectIDOrProjectName{}
+		}
+
+		if opts.Scope.DomainID != "" {
+			// ProjectName + DomainID
+			return map[string]interface{}{
+				"project": map[string]interface{}{
+					"name":   &opts.Scope.ProjectName,
+					"domain": map[string]interface{}{"id": &opts.Scope.DomainID},
+				},
+			}, nil
+		}
+
+		if opts.Scope.DomainName != "" {
+			// ProjectName + DomainName
+			return map[string]interface{}{
+				"project": map[string]interface{}{
+					"name":   &opts.Scope.ProjectName,
+					"domain": map[string]interface{}{"name": &opts.Scope.DomainName},
+				},
+			}, nil
+		}
+	} else if opts.Scope.ProjectID != "" {
+		// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+		if opts.Scope.DomainID != "" {
+			return nil, gophercloud.ErrScopeProjectIDAlone{}
+		}
+		if opts.Scope.DomainName != "" {
+			return nil, gophercloud.ErrScopeProjectIDAlone{}
+		}
+
+		// ProjectID
+		return map[string]interface{}{
+			"project": map[string]interface{}{
+				"id": &opts.Scope.ProjectID,
+			},
+		}, nil
+	} else if opts.Scope.DomainID != "" {
+		// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+		if opts.Scope.DomainName != "" {
+			return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
+		}
+
+		// DomainID
+		return map[string]interface{}{
+			"domain": map[string]interface{}{
+				"id": &opts.Scope.DomainID,
+			},
+		}, nil
+	} else if opts.Scope.DomainName != "" {
+		return nil, gophercloud.ErrScopeDomainName{}
+	}
+
+	return nil, nil
+}
+
+func (opts *AuthOptions) CanReauth() bool {
+	return opts.AllowReauth
+}
+
+func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
+	return map[string]string{
+		"X-Subject-Token": subjectToken,
+	}
+}
+
+// Create authenticates and either generates a new token, or changes the Scope of an existing token.
+func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) {
+	scope, err := opts.ToTokenV3ScopeMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	b, err := opts.ToTokenV3CreateMap(scope)
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{
+		MoreHeaders: map[string]string{"X-Auth-Token": ""},
+	})
+	r.Err = err
+	if resp != nil {
+		r.Header = resp.Header
+	}
+	return
+}
+
+// Get validates and retrieves information about another token.
+func Get(c *gophercloud.ServiceClient, token string) (r GetResult) {
+	resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{
+		MoreHeaders: subjectTokenHeaders(c, token),
+		OkCodes:     []int{200, 203},
+	})
+	if resp != nil {
+		r.Err = err
+		r.Header = resp.Header
+	}
+	return
+}
+
+// Validate determines if a specified token is valid or not.
+func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
+	resp, err := c.Request("HEAD", tokenURL(c), &gophercloud.RequestOpts{
+		MoreHeaders: subjectTokenHeaders(c, token),
+		OkCodes:     []int{204, 404},
+	})
+	if err != nil {
+		return false, err
+	}
+
+	return resp.StatusCode == 204, nil
+}
+
+// Revoke immediately makes specified token invalid.
+func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) {
+	_, r.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{
+		MoreHeaders: subjectTokenHeaders(c, token),
+	})
+	return
+}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
new file mode 100644
index 0000000..0f1e8c2
--- /dev/null
+++ b/openstack/identity/v3/tokens/results.go
@@ -0,0 +1,103 @@
+package tokens
+
+import (
+	"time"
+
+	"github.com/gophercloud/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 `json:"id"`
+	Region    string `json:"region"`
+	Interface string `json:"interface"`
+	URL       string `json:"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 `json:"id"`
+	// Name will contain the provider-specified name for the service.
+	Name string `json:"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 `json:"type"`
+	// Endpoints will let the caller iterate over all the different endpoints that may exist for
+	// the service.
+	Endpoints []Endpoint `json:"endpoints"`
+}
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+type ServiceCatalog struct {
+	Entries []CatalogEntry `json:"catalog"`
+}
+
+// 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) {
+	var s Token
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse the token itself from the stored headers.
+	s.ID = r.Header.Get("X-Subject-Token")
+
+	return &s, err
+}
+
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+	var s ServiceCatalog
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+// 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
+}
+
+// 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 `json:"id"`
+	// ExpiresAt is the timestamp at which this token will no longer be accepted.
+	ExpiresAt time.Time `json:"expires_at"`
+}
+
+func (r commonResult) ExtractInto(v interface{}) error {
+	return r.ExtractIntoStructPtr(v, "token")
+}
diff --git a/openstack/identity/v3/tokens/testing/doc.go b/openstack/identity/v3/tokens/testing/doc.go
new file mode 100644
index 0000000..ad1d35d
--- /dev/null
+++ b/openstack/identity/v3/tokens/testing/doc.go
@@ -0,0 +1,2 @@
+// identity_tokens_v3
+package testing
diff --git a/openstack/identity/v3/tokens/testing/requests_test.go b/openstack/identity/v3/tokens/testing/requests_test.go
new file mode 100644
index 0000000..ddffeb4
--- /dev/null
+++ b/openstack/identity/v3/tokens/testing/requests_test.go
@@ -0,0 +1,532 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+	"github.com/gophercloud/gophercloud/testhelper"
+)
+
+// authTokenPost verifies that providing certain AuthOptions and Scope results in an expected JSON structure.
+func authTokenPost(t *testing.T, options tokens.AuthOptions, scope *tokens.Scope, requestJSON string) {
+	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) {
+		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"
+			}
+		}`)
+	})
+
+	if scope != nil {
+		options.Scope = *scope
+	}
+
+	expected := &tokens.Token{
+		ExpiresAt: time.Date(2014, 10, 2, 13, 45, 0, 0, time.UTC),
+	}
+	actual, err := tokens.Create(&client, &options).Extract()
+	testhelper.AssertNoErr(t, err)
+	testhelper.CheckDeepEquals(t, expected, actual)
+}
+
+func authTokenPostErr(t *testing.T, options tokens.AuthOptions, scope *tokens.Scope, includeToken bool, expectedErr error) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	client := gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{},
+		Endpoint:       testhelper.Endpoint(),
+	}
+	if includeToken {
+		client.TokenID = "abcdef123456"
+	}
+
+	if scope != nil {
+		options.Scope = *scope
+	}
+
+	_, err := tokens.Create(&client, &options).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, tokens.AuthOptions{UserID: "me", Password: "squirrel!"}, nil, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["password"],
+					"password": {
+						"user": { "id": "me", "password": "squirrel!" }
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateUsernameDomainIDPassword(t *testing.T) {
+	authTokenPost(t, tokens.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, tokens.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, tokens.AuthOptions{TokenID: "12345abcdef"}, nil, `
+		{
+			"auth": {
+				"identity": {
+					"methods": ["token"],
+					"token": {
+						"id": "12345abcdef"
+					}
+				}
+			}
+		}
+	`)
+}
+
+func TestCreateProjectIDScope(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &tokens.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 := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &tokens.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 := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &tokens.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 := tokens.AuthOptions{UserID: "fenris", Password: "g0t0h311"}
+	scope := &tokens.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 := tokens.AuthOptions{UserID: "me", Password: "shhh"}
+	token, err := tokens.Create(&client, &options).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, tokens.AuthOptions{}, nil, false, gophercloud.ErrMissingPassword{})
+}
+
+func TestCreateFailureTokenIDUsername(t *testing.T) {
+	authTokenPostErr(t, tokens.AuthOptions{Username: "something", TokenID: "12345"}, nil, true, gophercloud.ErrUsernameWithToken{})
+}
+
+func TestCreateFailureTokenIDUserID(t *testing.T) {
+	authTokenPostErr(t, tokens.AuthOptions{UserID: "something", TokenID: "12345"}, nil, true, gophercloud.ErrUserIDWithToken{})
+}
+
+func TestCreateFailureTokenIDDomainID(t *testing.T) {
+	authTokenPostErr(t, tokens.AuthOptions{DomainID: "something", TokenID: "12345"}, nil, true, gophercloud.ErrDomainIDWithToken{})
+}
+
+func TestCreateFailureTokenIDDomainName(t *testing.T) {
+	authTokenPostErr(t, tokens.AuthOptions{DomainName: "something", TokenID: "12345"}, nil, true, gophercloud.ErrDomainNameWithToken{})
+}
+
+func TestCreateFailureMissingUser(t *testing.T) {
+	options := tokens.AuthOptions{Password: "supersecure"}
+	authTokenPostErr(t, options, nil, false, gophercloud.ErrUsernameOrUserID{})
+}
+
+func TestCreateFailureBothUser(t *testing.T) {
+	options := tokens.AuthOptions{
+		Password: "supersecure",
+		Username: "oops",
+		UserID:   "redundancy",
+	}
+	authTokenPostErr(t, options, nil, false, gophercloud.ErrUsernameOrUserID{})
+}
+
+func TestCreateFailureMissingDomain(t *testing.T) {
+	options := tokens.AuthOptions{
+		Password: "supersecure",
+		Username: "notuniqueenough",
+	}
+	authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainIDOrDomainName{})
+}
+
+func TestCreateFailureBothDomain(t *testing.T) {
+	options := tokens.AuthOptions{
+		Password:   "supersecure",
+		Username:   "someone",
+		DomainID:   "hurf",
+		DomainName: "durf",
+	}
+	authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainIDOrDomainName{})
+}
+
+func TestCreateFailureUserIDDomainID(t *testing.T) {
+	options := tokens.AuthOptions{
+		UserID:   "100",
+		Password: "stuff",
+		DomainID: "oops",
+	}
+	authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainIDWithUserID{})
+}
+
+func TestCreateFailureUserIDDomainName(t *testing.T) {
+	options := tokens.AuthOptions{
+		UserID:     "100",
+		Password:   "sssh",
+		DomainName: "oops",
+	}
+	authTokenPostErr(t, options, nil, false, gophercloud.ErrDomainNameWithUserID{})
+}
+
+func TestCreateFailureScopeProjectNameAlone(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{ProjectName: "notenough"}
+	authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeDomainIDOrDomainName{})
+}
+
+func TestCreateFailureScopeProjectNameAndID(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{ProjectName: "whoops", ProjectID: "toomuch", DomainID: "1234"}
+	authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeProjectIDOrProjectName{})
+}
+
+func TestCreateFailureScopeProjectIDAndDomainID(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{ProjectID: "toomuch", DomainID: "notneeded"}
+	authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeProjectIDAlone{})
+}
+
+func TestCreateFailureScopeProjectIDAndDomainNAme(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{ProjectID: "toomuch", DomainName: "notneeded"}
+	authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeProjectIDAlone{})
+}
+
+func TestCreateFailureScopeDomainIDAndDomainName(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{DomainID: "toomuch", DomainName: "notneeded"}
+	authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeDomainIDOrDomainName{})
+}
+
+func TestCreateFailureScopeDomainNameAlone(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{DomainName: "notenough"}
+	authTokenPostErr(t, options, scope, false, gophercloud.ErrScopeDomainName{})
+}
+
+/*
+func TestCreateFailureEmptyScope(t *testing.T) {
+	options := tokens.AuthOptions{UserID: "myself", Password: "swordfish"}
+	scope := &tokens.Scope{}
+	authTokenPostErr(t, options, scope, false, gophercloud.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 := tokens.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), time.Time(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 := tokens.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 := tokens.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 := tokens.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 := tokens.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 := tokens.Revoke(&client, "abcdef12345")
+	if res.Err == nil {
+		t.Errorf("Missing expected error from Revoke")
+	}
+}
+
+func TestNoTokenInResponse(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.WriteHeader(http.StatusCreated)
+		fmt.Fprintf(w, `{}`)
+	})
+
+	options := tokens.AuthOptions{UserID: "me", Password: "squirrel!"}
+	_, err := tokens.Create(&client, &options).Extract()
+	testhelper.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
new file mode 100644
index 0000000..2f864a3
--- /dev/null
+++ b/openstack/identity/v3/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/gophercloud/gophercloud"
+
+func tokenURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("auth", "tokens")
+}
diff --git a/openstack/imageservice/v2/README.md b/openstack/imageservice/v2/README.md
new file mode 100644
index 0000000..05c19be
--- /dev/null
+++ b/openstack/imageservice/v2/README.md
@@ -0,0 +1 @@
+This provides a Go API which wraps any service implementing the [OpenStack Image Service API, version 2](http://developer.openstack.org/api-ref-image-v2.html).
diff --git a/openstack/imageservice/v2/imagedata/requests.go b/openstack/imageservice/v2/imagedata/requests.go
new file mode 100644
index 0000000..7fd6951
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/requests.go
@@ -0,0 +1,28 @@
+package imagedata
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// Upload uploads image file
+func Upload(client *gophercloud.ServiceClient, id string, data io.Reader) (r UploadResult) {
+	_, r.Err = client.Put(uploadURL(client, id), data, nil, &gophercloud.RequestOpts{
+		MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"},
+		OkCodes:     []int{204},
+	})
+	return
+}
+
+// Download retrieves file
+func Download(client *gophercloud.ServiceClient, id string) (r DownloadResult) {
+	var resp *http.Response
+	resp, r.Err = client.Get(downloadURL(client, id), nil, nil)
+	if resp != nil {
+		r.Body = resp.Body
+		r.Header = resp.Header
+	}
+	return
+}
diff --git a/openstack/imageservice/v2/imagedata/results.go b/openstack/imageservice/v2/imagedata/results.go
new file mode 100644
index 0000000..970b226
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/results.go
@@ -0,0 +1,26 @@
+package imagedata
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// UploadResult is the result of an upload image operation
+type UploadResult struct {
+	gophercloud.ErrResult
+}
+
+// DownloadResult is the result of a download image operation
+type DownloadResult struct {
+	gophercloud.Result
+}
+
+// Extract builds images model from io.Reader
+func (r DownloadResult) Extract() (io.Reader, error) {
+	if r, ok := r.Body.(io.Reader); ok {
+		return r, nil
+	}
+	return nil, fmt.Errorf("Expected io.Reader but got: %T(%#v)", r.Body, r.Body)
+}
diff --git a/openstack/imageservice/v2/imagedata/testing/fixtures.go b/openstack/imageservice/v2/imagedata/testing/fixtures.go
new file mode 100644
index 0000000..fe93fc9
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/testing/fixtures.go
@@ -0,0 +1,40 @@
+package testing
+
+import (
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// HandlePutImageDataSuccessfully setup
+func HandlePutImageDataSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		b, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Errorf("Unable to read request body: %v", err)
+		}
+
+		th.AssertByteArrayEquals(t, []byte{5, 3, 7, 24}, b)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleGetImageDataSuccessfully setup
+func HandleGetImageDataSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.WriteHeader(http.StatusOK)
+
+		_, err := w.Write([]byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0})
+		th.AssertNoErr(t, err)
+	})
+}
diff --git a/openstack/imageservice/v2/imagedata/testing/requests_test.go b/openstack/imageservice/v2/imagedata/testing/requests_test.go
new file mode 100644
index 0000000..4ac42d0
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/testing/requests_test.go
@@ -0,0 +1,87 @@
+package testing
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestUpload(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandlePutImageDataSuccessfully(t)
+
+	err := imagedata.Upload(
+		fakeclient.ServiceClient(),
+		"da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func readSeekerOfBytes(bs []byte) io.ReadSeeker {
+	return &RS{bs: bs}
+}
+
+// implements io.ReadSeeker
+type RS struct {
+	bs     []byte
+	offset int
+}
+
+func (rs *RS) Read(p []byte) (int, error) {
+	leftToRead := len(rs.bs) - rs.offset
+
+	if 0 < leftToRead {
+		bytesToWrite := min(leftToRead, len(p))
+		for i := 0; i < bytesToWrite; i++ {
+			p[i] = rs.bs[rs.offset]
+			rs.offset++
+		}
+		return bytesToWrite, nil
+	}
+	return 0, io.EOF
+}
+
+func min(a int, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func (rs *RS) Seek(offset int64, whence int) (int64, error) {
+	var offsetInt = int(offset)
+	if whence == 0 {
+		rs.offset = offsetInt
+	} else if whence == 1 {
+		rs.offset = rs.offset + offsetInt
+	} else if whence == 2 {
+		rs.offset = len(rs.bs) - offsetInt
+	} else {
+		return 0, fmt.Errorf("For parameter `whence`, expected value in {0,1,2} but got: %#v", whence)
+	}
+
+	return int64(rs.offset), nil
+}
+
+func TestDownload(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleGetImageDataSuccessfully(t)
+
+	rdr, err := imagedata.Download(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea").Extract()
+	th.AssertNoErr(t, err)
+
+	bs, err := ioutil.ReadAll(rdr)
+	th.AssertNoErr(t, err)
+
+	th.AssertByteArrayEquals(t, []byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}, bs)
+}
diff --git a/openstack/imageservice/v2/imagedata/urls.go b/openstack/imageservice/v2/imagedata/urls.go
new file mode 100644
index 0000000..ccd6416
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/urls.go
@@ -0,0 +1,13 @@
+package imagedata
+
+import "github.com/gophercloud/gophercloud"
+
+// `imageDataURL(c,i)` is the URL for the binary image data for the
+// image identified by ID `i` in the service `c`.
+func uploadURL(c *gophercloud.ServiceClient, imageID string) string {
+	return c.ServiceURL("images", imageID, "file")
+}
+
+func downloadURL(c *gophercloud.ServiceClient, imageID string) string {
+	return uploadURL(c, imageID)
+}
diff --git a/openstack/imageservice/v2/images/requests.go b/openstack/imageservice/v2/images/requests.go
new file mode 100644
index 0000000..044b5cb
--- /dev/null
+++ b/openstack/imageservice/v2/images/requests.go
@@ -0,0 +1,238 @@
+package images
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToImageListQuery() (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.
+//http://developer.openstack.org/api-ref-image-v2.html
+type ListOpts struct {
+	// Integer value for the limit of values to return.
+	Limit int `q:"limit"`
+
+	// UUID of the server at which you want to set a marker.
+	Marker string `q:"marker"`
+
+	Name         string            `q:"name"`
+	Visibility   ImageVisibility   `q:"visibility"`
+	MemberStatus ImageMemberStatus `q:"member_status"`
+	Owner        string            `q:"owner"`
+	Status       ImageStatus       `q:"status"`
+	SizeMin      int64             `q:"size_min"`
+	SizeMax      int64             `q:"size_max"`
+	SortKey      string            `q:"sort_key"`
+	SortDir      string            `q:"sort_dir"`
+	Tag          string            `q:"tag"`
+}
+
+// ToImageListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToImageListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List implements image list request
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToImageListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return ImagePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+// The CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+	// Returns value that can be passed to json.Marshal
+	ToImageCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts implements CreateOptsBuilder
+type CreateOpts struct {
+	// Name is the name of the new image.
+	Name string `json:"name" required:"true"`
+
+	// Id is the the image ID.
+	ID string `json:"id,omitempty"`
+
+	// Visibility defines who can see/use the image.
+	Visibility *ImageVisibility `json:"visibility,omitempty"`
+
+	// Tags is a set of image tags.
+	Tags []string `json:"tags,omitempty"`
+
+	// ContainerFormat is the format of the
+	// container. Valid values are ami, ari, aki, bare, and ovf.
+	ContainerFormat string `json:"container_format,omitempty"`
+
+	// DiskFormat is the format of the disk. If set,
+	// valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi,
+	// and iso.
+	DiskFormat string `json:"disk_format,omitempty"`
+
+	// MinDisk is the amount of disk space in
+	// GB that is required to boot the image.
+	MinDisk int `json:"min_disk,omitempty"`
+
+	// MinRAM is the amount of RAM in MB that
+	// is required to boot the image.
+	MinRAM int `json:"min_ram,omitempty"`
+
+	// protected is whether the image is not deletable.
+	Protected *bool `json:"protected,omitempty"`
+
+	// properties is a set of properties, if any, that
+	// are associated with the image.
+	Properties map[string]string `json:"-"`
+}
+
+// ToImageCreateMap assembles a request body based on the contents of
+// a CreateOpts.
+func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	if opts.Properties != nil {
+		for k, v := range opts.Properties {
+			b[k] = v
+		}
+	}
+	return b, nil
+}
+
+// Create implements create image request
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToImageCreateMap()
+	if err != nil {
+		r.Err = err
+		return r
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}})
+	return
+}
+
+// Delete implements image delete request
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// Get implements image get request
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// Update implements image updated request
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToImageUpdateMap()
+	if err != nil {
+		r.Err = err
+		return r
+	}
+	_, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes:     []int{200},
+		MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"},
+	})
+	return
+}
+
+// UpdateOptsBuilder implements UpdateOptsBuilder
+type UpdateOptsBuilder interface {
+	// returns value implementing json.Marshaler which when marshaled matches the patch schema:
+	// http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html
+	ToImageUpdateMap() ([]interface{}, error)
+}
+
+// UpdateOpts implements UpdateOpts
+type UpdateOpts []Patch
+
+// ToImageUpdateMap builder
+func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) {
+	m := make([]interface{}, len(opts))
+	for i, patch := range opts {
+		patchJSON := patch.ToImagePatchMap()
+		m[i] = patchJSON
+	}
+	return m, nil
+}
+
+// Patch represents a single update to an existing image. Multiple updates to an image can be
+// submitted at the same time.
+type Patch interface {
+	ToImagePatchMap() map[string]interface{}
+}
+
+// UpdateVisibility updated visibility
+type UpdateVisibility struct {
+	Visibility ImageVisibility
+}
+
+// ToImagePatchMap builder
+func (u UpdateVisibility) ToImagePatchMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/visibility",
+		"value": u.Visibility,
+	}
+}
+
+// ReplaceImageName implements Patch
+type ReplaceImageName struct {
+	NewName string
+}
+
+// ToImagePatchMap builder
+func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/name",
+		"value": r.NewName,
+	}
+}
+
+// ReplaceImageChecksum implements Patch
+type ReplaceImageChecksum struct {
+	Checksum string
+}
+
+// ReplaceImageChecksum builder
+func (rc ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/checksum",
+		"value": rc.Checksum,
+	}
+}
+
+// ReplaceImageTags implements Patch
+type ReplaceImageTags struct {
+	NewTags []string
+}
+
+// ToImagePatchMap builder
+func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/tags",
+		"value": r.NewTags,
+	}
+}
diff --git a/openstack/imageservice/v2/images/results.go b/openstack/imageservice/v2/images/results.go
new file mode 100644
index 0000000..09996f4
--- /dev/null
+++ b/openstack/imageservice/v2/images/results.go
@@ -0,0 +1,174 @@
+package images
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Image model
+// Does not include the literal image data; just metadata.
+// returned by listing images, and by fetching a specific image.
+type Image struct {
+	// ID is the image UUID
+	ID string `json:"id"`
+
+	// Name is the human-readable display name for the image.
+	Name string `json:"name"`
+
+	// Status is the image status. It can be "queued" or "active"
+	// See imageservice/v2/images/type.go
+	Status ImageStatus `json:"status"`
+
+	// Tags is a list of image tags. Tags are arbitrarily defined strings
+	// attached to an image.
+	Tags []string `json:"tags"`
+
+	// ContainerFormat is the format of the container.
+	// Valid values are ami, ari, aki, bare, and ovf.
+	ContainerFormat string `json:"container_format"`
+
+	// DiskFormat is the format of the disk.
+	// If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso.
+	DiskFormat string `json:"disk_format"`
+
+	// MinDiskGigabytes is the amount of disk space in GB that is required to boot the image.
+	MinDiskGigabytes int `json:"min_disk"`
+
+	// MinRAMMegabytes [optional] is the amount of RAM in MB that is required to boot the image.
+	MinRAMMegabytes int `json:"min_ram"`
+
+	// Owner is the tenant the image belongs to.
+	Owner string `json:"owner"`
+
+	// Protected is whether the image is deletable or not.
+	Protected bool `json:"protected"`
+
+	// Visibility defines who can see/use the image.
+	Visibility ImageVisibility `json:"visibility"`
+
+	// Checksum is the checksum of the data that's associated with the image
+	Checksum string `json:"checksum"`
+
+	// SizeBytes is the size of the data that's associated with the image.
+	SizeBytes int64 `json:"size"`
+
+	// Metadata is a set of metadata associated with the image.
+	// Image metadata allow for meaningfully define the image properties
+	// and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html.
+	Metadata map[string]string `json:"metadata"`
+
+	// Properties is a set of key-value pairs, if any, that are associated with the image.
+	Properties map[string]string `json:"properties"`
+
+	// CreatedAt is the date when the image has been created.
+	CreatedAt time.Time `json:"created_at"`
+
+	// UpdatedAt is the date when the last change has been made to the image or it's properties.
+	UpdatedAt time.Time `json:"updated_at"`
+
+	// File is the trailing path after the glance endpoint that represent the location
+	// of the image or the path to retrieve it.
+	File string `json:"file"`
+
+	// Schema is the path to the JSON-schema that represent the image or image entity.
+	Schema string `json:"schema"`
+}
+
+func (r *Image) UnmarshalJSON(b []byte) error {
+	type tmp Image
+	var s struct {
+		tmp
+		SizeBytes interface{} `json:"size"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Image(s.tmp)
+
+	switch t := s.SizeBytes.(type) {
+	case nil:
+		return nil
+	case float32:
+		r.SizeBytes = int64(t)
+	case float64:
+		r.SizeBytes = int64(t)
+	default:
+		return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t)
+	}
+
+	return err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as an Image.
+func (r commonResult) Extract() (*Image, error) {
+	var s *Image
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+	commonResult
+}
+
+//DeleteResult model
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// ImagePage represents page
+type ImagePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Images results.
+func (r ImagePage) IsEmpty() (bool, error) {
+	images, err := ExtractImages(r)
+	return len(images) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (r ImagePage) NextPageURL() (string, error) {
+	var s struct {
+		Next string `json:"next"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+
+	if s.Next == "" {
+		return "", nil
+	}
+
+	return nextPageURL(r.URL.String(), s.Next), nil
+}
+
+// ExtractImages interprets the results of a single page from a List() call, producing a slice of Image entities.
+func ExtractImages(r pagination.Page) ([]Image, error) {
+	var s struct {
+		Images []Image `json:"images"`
+	}
+	err := (r.(ImagePage)).ExtractInto(&s)
+	return s.Images, err
+}
diff --git a/openstack/imageservice/v2/images/testing/fixtures.go b/openstack/imageservice/v2/images/testing/fixtures.go
new file mode 100644
index 0000000..10a87b4
--- /dev/null
+++ b/openstack/imageservice/v2/images/testing/fixtures.go
@@ -0,0 +1,329 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+type imageEntry struct {
+	ID   string
+	JSON string
+}
+
+// HandleImageListSuccessfully test setup
+func HandleImageListSuccessfully(t *testing.T) {
+
+	images := make([]imageEntry, 3)
+
+	images[0] = imageEntry{"cirros-0.3.4-x86_64-uec",
+		`{
+            "status": "active",
+            "name": "cirros-0.3.4-x86_64-uec",
+            "tags": [],
+            "kernel_id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4",
+            "container_format": "ami",
+            "created_at": "2015-07-15T11:43:35Z",
+            "ramdisk_id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b",
+            "disk_format": "ami",
+            "updated_at": "2015-07-15T11:43:35Z",
+            "visibility": "public",
+            "self": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431",
+            "min_disk": 0,
+            "protected": false,
+            "id": "07aa21a9-fa1a-430e-9a33-185be5982431",
+            "size": 25165824,
+            "file": "/v2/images/07aa21a9-fa1a-430e-9a33-185be5982431/file",
+            "checksum": "eb9139e4942121f22bbc2afc0400b2a4",
+            "owner": "cba624273b8344e59dd1fd18685183b0",
+            "virtual_size": null,
+            "min_ram": 0,
+            "schema": "/v2/schemas/image"
+        }`}
+	images[1] = imageEntry{"cirros-0.3.4-x86_64-uec-ramdisk",
+		`{
+            "status": "active",
+            "name": "cirros-0.3.4-x86_64-uec-ramdisk",
+            "tags": [],
+            "container_format": "ari",
+            "created_at": "2015-07-15T11:43:32Z",
+            "size": 3740163,
+            "disk_format": "ari",
+            "updated_at": "2015-07-15T11:43:32Z",
+            "visibility": "public",
+            "self": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b",
+            "min_disk": 0,
+            "protected": false,
+            "id": "8c64f48a-45a3-4eaa-adff-a8106b6c005b",
+            "file": "/v2/images/8c64f48a-45a3-4eaa-adff-a8106b6c005b/file",
+            "checksum": "be575a2b939972276ef675752936977f",
+            "owner": "cba624273b8344e59dd1fd18685183b0",
+            "virtual_size": null,
+            "min_ram": 0,
+            "schema": "/v2/schemas/image"
+        }`}
+	images[2] = imageEntry{"cirros-0.3.4-x86_64-uec-kernel",
+		`{
+            "status": "active",
+            "name": "cirros-0.3.4-x86_64-uec-kernel",
+            "tags": [],
+            "container_format": "aki",
+            "created_at": "2015-07-15T11:43:29Z",
+            "size": 4979632,
+            "disk_format": "aki",
+            "updated_at": "2015-07-15T11:43:30Z",
+            "visibility": "public",
+            "self": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4",
+            "min_disk": 0,
+            "protected": false,
+            "id": "e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4",
+            "file": "/v2/images/e1b6edd4-bd9b-40ac-b010-8a6c16de4ba4/file",
+            "checksum": "8a40c862b5735975d82605c1dd395796",
+            "owner": "cba624273b8344e59dd1fd18685183b0",
+            "virtual_size": null,
+            "min_ram": 0,
+            "schema": "/v2/schemas/image"
+        }`}
+
+	th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+
+		w.WriteHeader(http.StatusOK)
+
+		limit := 10
+		var err error
+		if r.FormValue("limit") != "" {
+			limit, err = strconv.Atoi(r.FormValue("limit"))
+			if err != nil {
+				t.Errorf("Error value for 'limit' parameter %v (error: %v)", r.FormValue("limit"), err)
+			}
+
+		}
+
+		marker := ""
+		newMarker := ""
+
+		if r.Form["marker"] != nil {
+			marker = r.Form["marker"][0]
+		}
+
+		t.Logf("limit = %v   marker = %v", limit, marker)
+
+		selected := 0
+		addNext := false
+		var imageJSON []string
+
+		fmt.Fprintf(w, `{"images": [`)
+
+		for _, i := range images {
+			if marker == "" || addNext {
+				t.Logf("Adding image %v to page", i.ID)
+				imageJSON = append(imageJSON, i.JSON)
+				newMarker = i.ID
+				selected++
+			} else {
+				if strings.Contains(i.JSON, marker) {
+					addNext = true
+				}
+			}
+
+			if selected == limit {
+				break
+			}
+		}
+		t.Logf("Writing out %v image(s)", len(imageJSON))
+		fmt.Fprintf(w, strings.Join(imageJSON, ","))
+
+		fmt.Fprintf(w, `],
+			    "next": "/images?marker=%s&limit=%v",
+			    "schema": "/schemas/images",
+			    "first": "/images?limit=%v"}`, newMarker, limit, limit)
+
+	})
+}
+
+// HandleImageCreationSuccessfully test setup
+func HandleImageCreationSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+			"name": "Ubuntu 12.10",
+			"architecture": "x86_64",
+			"tags": [
+				"ubuntu",
+				"quantal"
+			]
+		}`)
+
+		w.WriteHeader(http.StatusCreated)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"status": "queued",
+			"name": "Ubuntu 12.10",
+			"protected": false,
+			"tags": ["ubuntu","quantal"],
+			"container_format": "bare",
+			"created_at": "2014-11-11T20:47:55Z",
+			"disk_format": "qcow2",
+			"updated_at": "2014-11-11T20:47:55Z",
+			"visibility": "private",
+			"self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+			"min_disk": 0,
+			"protected": false,
+			"id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+			"file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file",
+			"owner": "b4eedccc6fb74fa8a7ad6b08382b852b",
+			"min_ram": 0,
+			"schema": "/v2/schemas/image",
+			"size": 0,
+			"checksum": "",
+			"virtual_size": 0
+		}`)
+	})
+}
+
+// HandleImageCreationSuccessfullyNulls test setup
+// JSON null values could be also returned according to behaviour https://bugs.launchpad.net/glance/+bug/1481512
+func HandleImageCreationSuccessfullyNulls(t *testing.T) {
+	th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+			"name": "Ubuntu 12.10",
+			"tags": [
+				"ubuntu",
+				"quantal"
+			]
+		}`)
+
+		w.WriteHeader(http.StatusCreated)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"status": "queued",
+			"name": "Ubuntu 12.10",
+			"protected": false,
+			"tags": ["ubuntu","quantal"],
+			"container_format": "bare",
+			"created_at": "2014-11-11T20:47:55Z",
+			"disk_format": "qcow2",
+			"updated_at": "2014-11-11T20:47:55Z",
+			"visibility": "private",
+			"self": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+			"min_disk": 0,
+			"protected": false,
+			"id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+			"file": "/v2/images/e7db3b45-8db7-47ad-8109-3fb55c2c24fd/file",
+			"owner": "b4eedccc6fb74fa8a7ad6b08382b852b",
+			"min_ram": 0,
+			"schema": "/v2/schemas/image",
+			"size": null,
+			"checksum": null,
+			"virtual_size": null
+		}`)
+	})
+}
+
+// HandleImageGetSuccessfully test setup
+func HandleImageGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"status": "active",
+			"name": "cirros-0.3.2-x86_64-disk",
+			"tags": [],
+			"container_format": "bare",
+			"created_at": "2014-05-05T17:15:10Z",
+			"disk_format": "qcow2",
+			"updated_at": "2014-05-05T17:15:11Z",
+			"visibility": "public",
+			"self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+			"min_disk": 0,
+			"protected": false,
+			"id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+			"file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
+			"checksum": "64d7c1cd2b6f60c92c14662941cb7913",
+			"owner": "5ef70662f8b34079a6eddb8da9d75fe8",
+			"size": 13167616,
+			"min_ram": 0,
+			"schema": "/v2/schemas/image",
+			"virtual_size": "None"
+		}`)
+	})
+}
+
+// HandleImageDeleteSuccessfully test setup
+func HandleImageDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleImageUpdateSuccessfully setup
+func HandleImageUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		th.TestJSONRequest(t, r, `[
+			{
+				"op": "replace",
+				"path": "/name",
+				"value": "Fedora 17"
+			},
+			{
+				"op": "replace",
+				"path": "/tags",
+				"value": [
+					"fedora",
+					"beefy"
+				]
+			}
+		]`)
+
+		th.AssertEquals(t, "application/openstack-images-v2.1-json-patch", r.Header.Get("Content-Type"))
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+			"id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+			"name": "Fedora 17",
+			"status": "active",
+			"visibility": "public",
+			"size": 2254249,
+			"checksum": "2cec138d7dae2aa59038ef8c9aec2390",
+			"tags": [
+				"fedora",
+				"beefy"
+			],
+			"created_at": "2012-08-10T19:23:50Z",
+			"updated_at": "2012-08-12T11:11:33Z",
+			"self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+			"file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file",
+			"schema": "/v2/schemas/image",
+			"owner": "",
+			"min_ram": 0,
+			"min_disk": 0,
+			"disk_format": "",
+			"virtual_size": 0,
+			"container_format": ""
+		}`)
+	})
+}
diff --git a/openstack/imageservice/v2/images/testing/requests_test.go b/openstack/imageservice/v2/images/testing/requests_test.go
new file mode 100644
index 0000000..0371e4c
--- /dev/null
+++ b/openstack/imageservice/v2/images/testing/requests_test.go
@@ -0,0 +1,275 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageListSuccessfully(t)
+
+	t.Logf("Test setup %+v\n", th.Server)
+
+	t.Logf("Id\tName\tOwner\tChecksum\tSizeBytes")
+
+	pager := images.List(fakeclient.ServiceClient(), images.ListOpts{Limit: 1})
+	t.Logf("Pager state %v", pager)
+	count, pages := 0, 0
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		t.Logf("Page %v", page)
+		images, err := images.ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, i := range images {
+			t.Logf("%s\t%s\t%s\t%s\t%v\t\n", i.ID, i.Name, i.Owner, i.Checksum, i.SizeBytes)
+			count++
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	t.Logf("--------\n%d images listed on %d pages.\n", count, pages)
+	th.AssertEquals(t, 3, pages)
+	th.AssertEquals(t, 3, count)
+}
+
+func TestAllPagesImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageListSuccessfully(t)
+
+	pages, err := images.List(fakeclient.ServiceClient(), nil).AllPages()
+	th.AssertNoErr(t, err)
+	images, err := images.ExtractImages(pages)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 3, len(images))
+}
+
+func TestCreateImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageCreationSuccessfully(t)
+
+	id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd"
+	name := "Ubuntu 12.10"
+
+	actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{
+		ID:   id,
+		Name: name,
+		Properties: map[string]string{
+			"architecture": "x86_64",
+		},
+		Tags: []string{"ubuntu", "quantal"},
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	containerFormat := "bare"
+	diskFormat := "qcow2"
+	owner := "b4eedccc6fb74fa8a7ad6b08382b852b"
+	minDiskGigabytes := 0
+	minRAMMegabytes := 0
+	file := actualImage.File
+	createdDate := actualImage.CreatedAt
+	lastUpdate := actualImage.UpdatedAt
+	schema := "/v2/schemas/image"
+
+	expectedImage := images.Image{
+		ID:   "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+		Name: "Ubuntu 12.10",
+		Tags: []string{"ubuntu", "quantal"},
+
+		Status: images.ImageStatusQueued,
+
+		ContainerFormat: containerFormat,
+		DiskFormat:      diskFormat,
+
+		MinDiskGigabytes: minDiskGigabytes,
+		MinRAMMegabytes:  minRAMMegabytes,
+
+		Owner: owner,
+
+		Visibility: images.ImageVisibilityPrivate,
+		File:       file,
+		CreatedAt:  createdDate,
+		UpdatedAt:  lastUpdate,
+		Schema:     schema,
+	}
+
+	th.AssertDeepEquals(t, &expectedImage, actualImage)
+}
+
+func TestCreateImageNulls(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageCreationSuccessfullyNulls(t)
+
+	id := "e7db3b45-8db7-47ad-8109-3fb55c2c24fd"
+	name := "Ubuntu 12.10"
+
+	actualImage, err := images.Create(fakeclient.ServiceClient(), images.CreateOpts{
+		ID:   id,
+		Name: name,
+		Tags: []string{"ubuntu", "quantal"},
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	containerFormat := "bare"
+	diskFormat := "qcow2"
+	owner := "b4eedccc6fb74fa8a7ad6b08382b852b"
+	minDiskGigabytes := 0
+	minRAMMegabytes := 0
+	file := actualImage.File
+	createdDate := actualImage.CreatedAt
+	lastUpdate := actualImage.UpdatedAt
+	schema := "/v2/schemas/image"
+
+	expectedImage := images.Image{
+		ID:   "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+		Name: "Ubuntu 12.10",
+		Tags: []string{"ubuntu", "quantal"},
+
+		Status: images.ImageStatusQueued,
+
+		ContainerFormat: containerFormat,
+		DiskFormat:      diskFormat,
+
+		MinDiskGigabytes: minDiskGigabytes,
+		MinRAMMegabytes:  minRAMMegabytes,
+
+		Owner: owner,
+
+		Visibility: images.ImageVisibilityPrivate,
+		File:       file,
+		CreatedAt:  createdDate,
+		UpdatedAt:  lastUpdate,
+		Schema:     schema,
+	}
+
+	th.AssertDeepEquals(t, &expectedImage, actualImage)
+}
+
+func TestGetImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageGetSuccessfully(t)
+
+	actualImage, err := images.Get(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27").Extract()
+
+	th.AssertNoErr(t, err)
+
+	checksum := "64d7c1cd2b6f60c92c14662941cb7913"
+	sizeBytes := int64(13167616)
+	containerFormat := "bare"
+	diskFormat := "qcow2"
+	minDiskGigabytes := 0
+	minRAMMegabytes := 0
+	owner := "5ef70662f8b34079a6eddb8da9d75fe8"
+	file := actualImage.File
+	createdDate := actualImage.CreatedAt
+	lastUpdate := actualImage.UpdatedAt
+	schema := "/v2/schemas/image"
+
+	expectedImage := images.Image{
+		ID:   "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+		Name: "cirros-0.3.2-x86_64-disk",
+		Tags: []string{},
+
+		Status: images.ImageStatusActive,
+
+		ContainerFormat: containerFormat,
+		DiskFormat:      diskFormat,
+
+		MinDiskGigabytes: minDiskGigabytes,
+		MinRAMMegabytes:  minRAMMegabytes,
+
+		Owner: owner,
+
+		Protected:  false,
+		Visibility: images.ImageVisibilityPublic,
+
+		Checksum:  checksum,
+		SizeBytes: sizeBytes,
+		File:      file,
+		CreatedAt: createdDate,
+		UpdatedAt: lastUpdate,
+		Schema:    schema,
+	}
+
+	th.AssertDeepEquals(t, &expectedImage, actualImage)
+}
+
+func TestDeleteImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageDeleteSuccessfully(t)
+
+	result := images.Delete(fakeclient.ServiceClient(), "1bea47ed-f6a9-463b-b423-14b9cca9ad27")
+	th.AssertNoErr(t, result.Err)
+}
+
+func TestUpdateImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageUpdateSuccessfully(t)
+
+	actualImage, err := images.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea", images.UpdateOpts{
+		images.ReplaceImageName{NewName: "Fedora 17"},
+		images.ReplaceImageTags{NewTags: []string{"fedora", "beefy"}},
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	sizebytes := int64(2254249)
+	checksum := "2cec138d7dae2aa59038ef8c9aec2390"
+	file := actualImage.File
+	createdDate := actualImage.CreatedAt
+	lastUpdate := actualImage.UpdatedAt
+	schema := "/v2/schemas/image"
+
+	expectedImage := images.Image{
+		ID:         "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		Name:       "Fedora 17",
+		Status:     images.ImageStatusActive,
+		Visibility: images.ImageVisibilityPublic,
+
+		SizeBytes: sizebytes,
+		Checksum:  checksum,
+
+		Tags: []string{
+			"fedora",
+			"beefy",
+		},
+
+		Owner:            "",
+		MinRAMMegabytes:  0,
+		MinDiskGigabytes: 0,
+
+		DiskFormat:      "",
+		ContainerFormat: "",
+		File:            file,
+		CreatedAt:       createdDate,
+		UpdatedAt:       lastUpdate,
+		Schema:          schema,
+	}
+
+	th.AssertDeepEquals(t, &expectedImage, actualImage)
+}
diff --git a/openstack/imageservice/v2/images/types.go b/openstack/imageservice/v2/images/types.go
new file mode 100644
index 0000000..086e7e5
--- /dev/null
+++ b/openstack/imageservice/v2/images/types.go
@@ -0,0 +1,75 @@
+package images
+
+// ImageStatus image statuses
+// http://docs.openstack.org/developer/glance/statuses.html
+type ImageStatus string
+
+const (
+	// ImageStatusQueued is a status for an image which identifier has
+	// been reserved for an image in the image registry.
+	ImageStatusQueued ImageStatus = "queued"
+
+	// ImageStatusSaving denotes that an image’s raw data is currently being uploaded to Glance
+	ImageStatusSaving ImageStatus = "saving"
+
+	// ImageStatusActive denotes an image that is fully available in Glance.
+	ImageStatusActive ImageStatus = "active"
+
+	// ImageStatusKilled denotes that an error occurred during the uploading
+	// of an image’s data, and that the image is not readable.
+	ImageStatusKilled ImageStatus = "killed"
+
+	// ImageStatusDeleted is used for an image that is no longer available to use.
+	// The image information is retained in the image registry.
+	ImageStatusDeleted ImageStatus = "deleted"
+
+	// ImageStatusPendingDelete is similar to Delete, but the image is not yet deleted.
+	ImageStatusPendingDelete ImageStatus = "pending_delete"
+
+	// ImageStatusDeactivated denotes that access to image data is not allowed to any non-admin user.
+	ImageStatusDeactivated ImageStatus = "deactivated"
+)
+
+// ImageVisibility denotes an image that is fully available in Glance.
+// This occurs when the image data is uploaded, or the image size
+// is explicitly set to zero on creation.
+// According to design
+// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design
+type ImageVisibility string
+
+const (
+	// ImageVisibilityPublic all users
+	ImageVisibilityPublic ImageVisibility = "public"
+
+	// ImageVisibilityPrivate users with tenantId == tenantId(owner)
+	ImageVisibilityPrivate ImageVisibility = "private"
+
+	// ImageVisibilityShared images are visible to:
+	// - users with tenantId == tenantId(owner)
+	// - users with tenantId in the member-list of the image
+	// - users with tenantId in the member-list with member_status == 'accepted'
+	ImageVisibilityShared ImageVisibility = "shared"
+
+	// ImageVisibilityCommunity images:
+	// - all users can see and boot it
+	// - users with tenantId in the member-list of the image with member_status == 'accepted'
+	//   have this image in their default image-list
+	ImageVisibilityCommunity ImageVisibility = "community"
+)
+
+// MemberStatus is a status for adding a new member (tenant) to an image member list.
+type ImageMemberStatus string
+
+const (
+	// ImageMemberStatusAccepted is the status for an accepted image member.
+	ImageMemberStatusAccepted ImageMemberStatus = "accepted"
+
+	// ImageMemberStatusPending shows that the member addition is pending
+	ImageMemberStatusPending ImageMemberStatus = "pending"
+
+	// ImageMemberStatusAccepted is the status for a rejected image member
+	ImageMemberStatusRejected ImageMemberStatus = "rejected"
+
+	// ImageMemberStatusAll
+	ImageMemberStatusAll ImageMemberStatus = "all"
+)
diff --git a/openstack/imageservice/v2/images/urls.go b/openstack/imageservice/v2/images/urls.go
new file mode 100644
index 0000000..58cb8f7
--- /dev/null
+++ b/openstack/imageservice/v2/images/urls.go
@@ -0,0 +1,44 @@
+package images
+
+import (
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// `listURL` is a pure function. `listURL(c)` is a URL for which a GET
+// request will respond with a list of images in the service `c`.
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("images")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("images")
+}
+
+// `imageURL(c,i)` is the URL for the image identified by ID `i` in
+// the service `c`.
+func imageURL(c *gophercloud.ServiceClient, imageID string) string {
+	return c.ServiceURL("images", imageID)
+}
+
+// `getURL(c,i)` is a URL for which a GET request will respond with
+// information about the image identified by ID `i` in the service
+// `c`.
+func getURL(c *gophercloud.ServiceClient, imageID string) string {
+	return imageURL(c, imageID)
+}
+
+func updateURL(c *gophercloud.ServiceClient, imageID string) string {
+	return imageURL(c, imageID)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, imageID string) string {
+	return imageURL(c, imageID)
+}
+
+// builds next page full url based on current url
+func nextPageURL(currentURL string, next string) string {
+	base := currentURL[:strings.Index(currentURL, "/images")]
+	return base + next
+}
diff --git a/openstack/imageservice/v2/members/requests.go b/openstack/imageservice/v2/members/requests.go
new file mode 100644
index 0000000..8c667cb
--- /dev/null
+++ b/openstack/imageservice/v2/members/requests.go
@@ -0,0 +1,77 @@
+package members
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Create member for specific image
+//
+// Preconditions
+//    The specified images must exist.
+//    You can only add a new member to an image which 'visibility' attribute is private.
+//    You must be the owner of the specified image.
+// Synchronous Postconditions
+//    With correct permissions, you can see the member status of the image as pending through API calls.
+//
+// More details here: http://developer.openstack.org/api-ref-image-v2.html#createImageMember-v2
+func Create(client *gophercloud.ServiceClient, id string, member string) (r CreateResult) {
+	b := map[string]interface{}{"member": member}
+	_, r.Err = client.Post(createMemberURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 409, 403},
+	})
+	return
+}
+
+// List members returns list of members for specifed image id
+// More details: http://developer.openstack.org/api-ref-image-v2.html#listImageMembers-v2
+func List(client *gophercloud.ServiceClient, id string) pagination.Pager {
+	return pagination.NewPager(client, listMembersURL(client, id), func(r pagination.PageResult) pagination.Page {
+		return MemberPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get image member details.
+// More details: http://developer.openstack.org/api-ref-image-v2.html#getImageMember-v2
+func Get(client *gophercloud.ServiceClient, imageID string, memberID string) (r DetailsResult) {
+	_, r.Err = client.Get(getMemberURL(client, imageID, memberID), &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
+	return
+}
+
+// Delete membership for given image.
+// Callee should be image owner
+// More details: http://developer.openstack.org/api-ref-image-v2.html#deleteImageMember-v2
+func Delete(client *gophercloud.ServiceClient, imageID string, memberID string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteMemberURL(client, imageID, memberID), &gophercloud.RequestOpts{OkCodes: []int{204, 403}})
+	return
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToImageMemberUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts implements UpdateOptsBuilder
+type UpdateOpts struct {
+	Status string
+}
+
+// ToMemberUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToImageMemberUpdateMap() (map[string]interface{}, error) {
+	return map[string]interface{}{
+		"status": opts.Status,
+	}, nil
+}
+
+// Update function updates member
+// More details: http://developer.openstack.org/api-ref-image-v2.html#updateImageMember-v2
+func Update(client *gophercloud.ServiceClient, imageID string, memberID string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToImageMemberUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body,
+		&gophercloud.RequestOpts{OkCodes: []int{200}})
+	return
+}
diff --git a/openstack/imageservice/v2/members/results.go b/openstack/imageservice/v2/members/results.go
new file mode 100644
index 0000000..d3cc1ce
--- /dev/null
+++ b/openstack/imageservice/v2/members/results.go
@@ -0,0 +1,69 @@
+package members
+
+import (
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Member model
+type Member struct {
+	CreatedAt time.Time `json:"created_at"`
+	ImageID   string    `json:"image_id"`
+	MemberID  string    `json:"member_id"`
+	Schema    string    `json:"schema"`
+	Status    string    `json:"status"`
+	UpdatedAt time.Time `json:"updated_at"`
+}
+
+// Extract Member model from request if possible
+func (r commonResult) Extract() (*Member, error) {
+	var s *Member
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// MemberPage is a single page of Members results.
+type MemberPage struct {
+	pagination.SinglePageBase
+}
+
+// ExtractMembers returns a slice of Members contained in a single page of results.
+func ExtractMembers(r pagination.Page) ([]Member, error) {
+	var s struct {
+		Members []Member `json:"members"`
+	}
+	err := r.(MemberPage).ExtractInto(&s)
+	return s.Members, err
+}
+
+// IsEmpty determines whether or not a page of Members contains any results.
+func (r MemberPage) IsEmpty() (bool, error) {
+	members, err := ExtractMembers(r)
+	return len(members) == 0, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// CreateResult result model
+type CreateResult struct {
+	commonResult
+}
+
+// DetailsResult model
+type DetailsResult struct {
+	commonResult
+}
+
+// UpdateResult model
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult model
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/imageservice/v2/members/testing/fixtures.go b/openstack/imageservice/v2/members/testing/fixtures.go
new file mode 100644
index 0000000..c08fc5e
--- /dev/null
+++ b/openstack/imageservice/v2/members/testing/fixtures.go
@@ -0,0 +1,138 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// HandleCreateImageMemberSuccessfully setup
+func HandleCreateImageMemberSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		th.TestJSONRequest(t, r, `{"member": "8989447062e04a818baf9e073fd04fa7"}`)
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{
+		    "created_at": "2013-09-20T19:22:19Z",
+		    "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		    "member_id": "8989447062e04a818baf9e073fd04fa7",
+		    "schema": "/v2/schemas/member",
+		    "status": "pending",
+		    "updated_at": "2013-09-20T19:25:31Z"
+			}`)
+
+	})
+}
+
+// HandleImageMemberList happy path setup
+func HandleImageMemberList(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+		    "members": [
+		        {
+		            "created_at": "2013-10-07T17:58:03Z",
+		            "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		            "member_id": "123456789",
+		            "schema": "/v2/schemas/member",
+		            "status": "pending",
+		            "updated_at": "2013-10-07T17:58:03Z"
+		        },
+		        {
+		            "created_at": "2013-10-07T17:58:55Z",
+		            "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		            "member_id": "987654321",
+		            "schema": "/v2/schemas/member",
+		            "status": "accepted",
+		            "updated_at": "2013-10-08T12:08:55Z"
+		        }
+		    ],
+		    "schema": "/v2/schemas/members"
+		}`)
+	})
+}
+
+// HandleImageMemberEmptyList happy path setup
+func HandleImageMemberEmptyList(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `{
+		    "members": [],
+		    "schema": "/v2/schemas/members"
+		}`)
+	})
+}
+
+// HandleImageMemberDetails setup
+func HandleImageMemberDetails(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members/8989447062e04a818baf9e073fd04fa7", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `{
+		    "status": "pending",
+		    "created_at": "2013-11-26T07:21:21Z",
+		    "updated_at": "2013-11-26T07:21:21Z",
+		    "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		    "member_id": "8989447062e04a818baf9e073fd04fa7",
+		    "schema": "/v2/schemas/member"
+		}`)
+	})
+}
+
+// HandleImageMemberDeleteSuccessfully setup
+func HandleImageMemberDeleteSuccessfully(t *testing.T) *CallsCounter {
+	var counter CallsCounter
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members/8989447062e04a818baf9e073fd04fa7", func(w http.ResponseWriter, r *http.Request) {
+		counter.Counter = counter.Counter + 1
+
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+	return &counter
+}
+
+// HandleImageMemberUpdate setup
+func HandleImageMemberUpdate(t *testing.T) *CallsCounter {
+	var counter CallsCounter
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/members/8989447062e04a818baf9e073fd04fa7", func(w http.ResponseWriter, r *http.Request) {
+		counter.Counter = counter.Counter + 1
+
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		th.TestJSONRequest(t, r, `{"status": "accepted"}`)
+
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `{
+		    "status": "accepted",
+		    "created_at": "2013-11-26T07:21:21Z",
+		    "updated_at": "2013-11-26T07:21:21Z",
+		    "image_id": "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		    "member_id": "8989447062e04a818baf9e073fd04fa7",
+		    "schema": "/v2/schemas/member"
+		}`)
+	})
+	return &counter
+}
+
+// CallsCounter for checking if request handler was called at all
+type CallsCounter struct {
+	Counter int
+}
diff --git a/openstack/imageservice/v2/members/testing/requests_test.go b/openstack/imageservice/v2/members/testing/requests_test.go
new file mode 100644
index 0000000..04624c9
--- /dev/null
+++ b/openstack/imageservice/v2/members/testing/requests_test.go
@@ -0,0 +1,172 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/members"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const createdAtString = "2013-09-20T19:22:19Z"
+const updatedAtString = "2013-09-20T19:25:31Z"
+
+func TestCreateMemberSuccessfully(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleCreateImageMemberSuccessfully(t)
+	im, err := members.Create(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		"8989447062e04a818baf9e073fd04fa7").Extract()
+	th.AssertNoErr(t, err)
+
+	createdAt, err := time.Parse(time.RFC3339, createdAtString)
+	th.AssertNoErr(t, err)
+
+	updatedAt, err := time.Parse(time.RFC3339, updatedAtString)
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, members.Member{
+		CreatedAt: createdAt,
+		ImageID:   "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		MemberID:  "8989447062e04a818baf9e073fd04fa7",
+		Schema:    "/v2/schemas/member",
+		Status:    "pending",
+		UpdatedAt: updatedAt,
+	}, *im)
+
+}
+
+func TestMemberListSuccessfully(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageMemberList(t)
+
+	pager := members.List(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea")
+	t.Logf("Pager state %v", pager)
+	count, pages := 0, 0
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		t.Logf("Page %v", page)
+		members, err := members.ExtractMembers(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, i := range members {
+			t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema)
+			count++
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, pages)
+	th.AssertEquals(t, 2, count)
+}
+
+func TestMemberListEmpty(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageMemberEmptyList(t)
+
+	pager := members.List(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea")
+	t.Logf("Pager state %v", pager)
+	count, pages := 0, 0
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		t.Logf("Page %v", page)
+		members, err := members.ExtractMembers(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, i := range members {
+			t.Logf("%s\t%s\t%s\t%s\t\n", i.ImageID, i.MemberID, i.Status, i.Schema)
+			count++
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 0, pages)
+	th.AssertEquals(t, 0, count)
+}
+
+func TestShowMemberDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleImageMemberDetails(t)
+	md, err := members.Get(fakeclient.ServiceClient(),
+		"da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		"8989447062e04a818baf9e073fd04fa7").Extract()
+
+	th.AssertNoErr(t, err)
+	if md == nil {
+		t.Errorf("Expected non-nil value for md")
+	}
+
+	createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z")
+	th.AssertNoErr(t, err)
+
+	updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z")
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, members.Member{
+		CreatedAt: createdAt,
+		ImageID:   "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		MemberID:  "8989447062e04a818baf9e073fd04fa7",
+		Schema:    "/v2/schemas/member",
+		Status:    "pending",
+		UpdatedAt: updatedAt,
+	}, *md)
+}
+
+func TestDeleteMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	counter := HandleImageMemberDeleteSuccessfully(t)
+
+	result := members.Delete(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		"8989447062e04a818baf9e073fd04fa7")
+	th.AssertEquals(t, 1, counter.Counter)
+	th.AssertNoErr(t, result.Err)
+}
+
+func TestMemberUpdateSuccessfully(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	counter := HandleImageMemberUpdate(t)
+	im, err := members.Update(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		"8989447062e04a818baf9e073fd04fa7",
+		members.UpdateOpts{
+			Status: "accepted",
+		}).Extract()
+	th.AssertEquals(t, 1, counter.Counter)
+	th.AssertNoErr(t, err)
+
+	createdAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z")
+	th.AssertNoErr(t, err)
+
+	updatedAt, err := time.Parse(time.RFC3339, "2013-11-26T07:21:21Z")
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, members.Member{
+		CreatedAt: createdAt,
+		ImageID:   "da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		MemberID:  "8989447062e04a818baf9e073fd04fa7",
+		Schema:    "/v2/schemas/member",
+		Status:    "accepted",
+		UpdatedAt: updatedAt,
+	}, *im)
+
+}
diff --git a/openstack/imageservice/v2/members/urls.go b/openstack/imageservice/v2/members/urls.go
new file mode 100644
index 0000000..0898364
--- /dev/null
+++ b/openstack/imageservice/v2/members/urls.go
@@ -0,0 +1,31 @@
+package members
+
+import "github.com/gophercloud/gophercloud"
+
+func imageMembersURL(c *gophercloud.ServiceClient, imageID string) string {
+	return c.ServiceURL("images", imageID, "members")
+}
+
+func listMembersURL(c *gophercloud.ServiceClient, imageID string) string {
+	return imageMembersURL(c, imageID)
+}
+
+func createMemberURL(c *gophercloud.ServiceClient, imageID string) string {
+	return imageMembersURL(c, imageID)
+}
+
+func imageMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
+	return c.ServiceURL("images", imageID, "members", memberID)
+}
+
+func getMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
+	return imageMemberURL(c, imageID, memberID)
+}
+
+func updateMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
+	return imageMemberURL(c, imageID, memberID)
+}
+
+func deleteMemberURL(c *gophercloud.ServiceClient, imageID string, memberID string) string {
+	return imageMemberURL(c, imageID, memberID)
+}
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/requests.go b/openstack/networking/v2/apiversions/requests.go
new file mode 100644
index 0000000..59ece85
--- /dev/null
+++ b/openstack/networking/v2/apiversions/requests.go
@@ -0,0 +1,21 @@
+package apiversions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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/results.go b/openstack/networking/v2/apiversions/results.go
new file mode 100644
index 0000000..eff4485
--- /dev/null
+++ b/openstack/networking/v2/apiversions/results.go
@@ -0,0 +1,66 @@
+package apiversions
+
+import (
+	"github.com/gophercloud/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 `son:"status"`
+	ID     string `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)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]APIVersion, error) {
+	var s struct {
+		Versions []APIVersion `json:"versions"`
+	}
+	err := (r.(APIVersionPage)).ExtractInto(&s)
+	return s.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 `json:"name"`
+	Collection string `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)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]APIVersionResource, error) {
+	var s struct {
+		APIVersionResources []APIVersionResource `json:"resources"`
+	}
+	err := (r.(APIVersionResourcePage)).ExtractInto(&s)
+	return s.APIVersionResources, err
+}
diff --git a/openstack/networking/v2/apiversions/testing/doc.go b/openstack/networking/v2/apiversions/testing/doc.go
new file mode 100644
index 0000000..0accd99
--- /dev/null
+++ b/openstack/networking/v2/apiversions/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_apiversions_v2
+package testing
diff --git a/openstack/networking/v2/apiversions/testing/requests_test.go b/openstack/networking/v2/apiversions/testing/requests_test.go
new file mode 100644
index 0000000..5a66a2a
--- /dev/null
+++ b/openstack/networking/v2/apiversions/testing/requests_test.go
@@ -0,0 +1,183 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/apiversions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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
+
+	apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := apiversions.ExtractAPIVersions(page)
+		if err != nil {
+			t.Errorf("Failed to extract API versions: %v", err)
+			return false, err
+		}
+
+		expected := []apiversions.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)
+	})
+
+	apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := apiversions.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
+
+	apiversions.ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := apiversions.ExtractVersionResources(page)
+		if err != nil {
+			t.Errorf("Failed to extract version resources: %v", err)
+			return false, err
+		}
+
+		expected := []apiversions.APIVersionResource{
+			{
+				Name:       "subnet",
+				Collection: "subnets",
+			},
+			{
+				Name:       "network",
+				Collection: "networks",
+			},
+			{
+				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)
+	})
+
+	apiversions.ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := apiversions.ExtractVersionResources(page); err == nil {
+			t.Fatalf("Expected error, got nil")
+		}
+		return true, nil
+	})
+}
diff --git a/openstack/networking/v2/apiversions/urls.go b/openstack/networking/v2/apiversions/urls.go
new file mode 100644
index 0000000..0fa7437
--- /dev/null
+++ b/openstack/networking/v2/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+	"strings"
+
+	"github.com/gophercloud/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/common/common_tests.go b/openstack/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..7e1d917
--- /dev/null
+++ b/openstack/networking/v2/common/common_tests.go
@@ -0,0 +1,14 @@
+package common
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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..0c43689
--- /dev/null
+++ b/openstack/networking/v2/extensions/delegate.go
@@ -0,0 +1,41 @@
+package extensions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	common "github.com/gophercloud/gophercloud/openstack/common/extensions"
+	"github.com/gophercloud/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/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..1ee39d2
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/requests.go
@@ -0,0 +1,32 @@
+package external
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+)
+
+// 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 {
+	networks.CreateOpts
+	External *bool `json:"router:external,omitempty"`
+}
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "network")
+}
+
+// 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 {
+	networks.UpdateOpts
+	External *bool `json:"router:external,omitempty"`
+}
+
+// ToNetworkUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "network")
+}
diff --git a/openstack/networking/v2/extensions/external/results.go b/openstack/networking/v2/extensions/external/results.go
new file mode 100644
index 0000000..7e10c6d
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/results.go
@@ -0,0 +1,76 @@
+package external
+
+import (
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/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 `json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Name string `json:"name"`
+
+	// The administrative state of network. If false (down), the network does not forward packets.
+	AdminStateUp bool `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 `json:"status"`
+
+	// Subnets associated with this network.
+	Subnets []string `json:"subnets"`
+
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `json:"tenant_id"`
+
+	// Specifies whether the network resource can be accessed by any tenant or not.
+	Shared bool `json:"shared"`
+
+	// Specifies whether the network is an external network or not.
+	External bool `json:"router:external"`
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
+	var s struct {
+		Network *NetworkExternal `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Network, err
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) {
+	var s struct {
+		Network *NetworkExternal `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Network, err
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) {
+	var s struct {
+		Network *NetworkExternal `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Network, err
+}
+
+// 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(r pagination.Page) ([]NetworkExternal, error) {
+	var s struct {
+		Networks []NetworkExternal `json:"networks" json:"networks"`
+	}
+	err := (r.(networks.NetworkPage)).ExtractInto(&s)
+	return s.Networks, err
+}
diff --git a/openstack/networking/v2/extensions/external/testing/doc.go b/openstack/networking/v2/extensions/external/testing/doc.go
new file mode 100644
index 0000000..8a30f6b
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_external_v2
+package testing
diff --git a/openstack/networking/v2/extensions/external/testing/results_test.go b/openstack/networking/v2/extensions/external/testing/results_test.go
new file mode 100644
index 0000000..2ddc3ac
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/testing/results_test.go
@@ -0,0 +1,256 @@
+package testing
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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 := external.ExtractList(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []external.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,
+			},
+			{
+				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 := external.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 := external.CreateOpts{networks.CreateOpts{Name: "ext_net", AdminStateUp: gophercloud.Enabled}, gophercloud.Enabled}
+	res := networks.Create(fake.ServiceClient(), options)
+
+	n, err := external.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 := external.UpdateOpts{networks.UpdateOpts{Name: "new_name"}, gophercloud.Enabled}
+	res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+	n, err := external.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 := external.ExtractGet(gr); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+
+	ur := networks.UpdateResult{}
+	ur.Err = errors.New("")
+
+	if _, err := external.ExtractUpdate(ur); err == nil {
+		t.Fatalf("Expected error, got one")
+	}
+
+	cr := networks.CreateResult{}
+	cr.Err = errors.New("")
+
+	if _, err := external.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..21ceb4e
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
@@ -0,0 +1,140 @@
+package firewalls
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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)
+	return q.String(), err
+}
+
+// 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 {
+	PolicyID string `json:"firewall_policy_id" required:"true"`
+	// Only required if the caller has an admin role and wants to create a firewall
+	// for another tenant.
+	TenantID     string `json:"tenant_id,omitempty"`
+	Name         string `json:"name,omitempty"`
+	Description  string `json:"description,omitempty"`
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	Shared       *bool  `json:"shared,omitempty"`
+}
+
+// ToFirewallCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToFirewallCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "firewall")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToFirewallCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular firewall based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	PolicyID     string `json:"firewall_policy_id" required:"true"`
+	Name         string `json:"name,omitempty"`
+	Description  string `json:"description,omitempty"`
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	Shared       *bool  `json:"shared,omitempty"`
+}
+
+// ToFirewallUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToFirewallUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "firewall")
+}
+
+// Update allows firewalls to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToFirewallUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will permanently delete a particular firewall based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..ab6cf8e
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
@@ -0,0 +1,87 @@
+package firewalls
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Firewall is an OpenStack firewall.
+type Firewall struct {
+	ID           string `json:"id"`
+	Name         string `json:"name"`
+	Description  string `json:"description"`
+	AdminStateUp bool   `json:"admin_state_up"`
+	Status       string `json:"status"`
+	PolicyID     string `json:"firewall_policy_id"`
+	TenantID     string `json:"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) {
+	var s struct {
+		Firewall *Firewall `json:"firewall"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 (r FirewallPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"firewalls_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a FirewallPage struct is empty.
+func (r FirewallPage) IsEmpty() (bool, error) {
+	is, err := ExtractFirewalls(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Firewall, error) {
+	var s struct {
+		Firewalls []Firewall `json:"firewalls" json:"firewalls"`
+	}
+	err := (r.(FirewallPage)).ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/fwaas/firewalls/testing/doc.go
new file mode 100644
index 0000000..6b46bba
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_fwaas_firewalls_v2
+package testing
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/testing/requests_test.go b/openstack/networking/v2/extensions/fwaas/firewalls/testing/requests_test.go
new file mode 100644
index 0000000..01ab732
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/testing/requests_test.go
@@ -0,0 +1,241 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	firewalls.List(fake.ServiceClient(), firewalls.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := firewalls.ExtractFirewalls(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []firewalls.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",
+			},
+			{
+				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 := firewalls.CreateOpts{
+		TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+		Name:         "fw",
+		Description:  "OpenStack firewall",
+		AdminStateUp: gophercloud.Enabled,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	_, err := firewalls.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 := firewalls.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 := firewalls.UpdateOpts{
+		Name:         "fw",
+		Description:  "updated fw",
+		AdminStateUp: gophercloud.Disabled,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+
+	_, err := firewalls.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 := firewalls.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..807ea1a
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/urls.go
@@ -0,0 +1,16 @@
+package firewalls
+
+import "github.com/gophercloud/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..437d124
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/requests.go
@@ -0,0 +1,173 @@
+package policies
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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)
+	return q.String(), err
+}
+
+// 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 {
+	ToFirewallPolicyCreateMap() (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   `json:"tenant_id,omitempty"`
+	Name        string   `json:"name,omitempty"`
+	Description string   `json:"description,omitempty"`
+	Shared      *bool    `json:"shared,omitempty"`
+	Audited     *bool    `json:"audited,omitempty"`
+	Rules       []string `json:"firewall_rules,omitempty"`
+}
+
+// ToFirewallPolicyCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToFirewallPolicyCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "firewall_policy")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall policy
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToFirewallPolicyCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular firewall policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	ToFirewallPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall policy.
+type UpdateOpts struct {
+	Name        string   `json:"name,omitempty"`
+	Description string   `json:"description,omitempty"`
+	Shared      *bool    `json:"shared,omitempty"`
+	Audited     *bool    `json:"audited,omitempty"`
+	Rules       []string `json:"firewall_rules,omitempty"`
+}
+
+// ToFirewallPolicyUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToFirewallPolicyUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "firewall_policy")
+}
+
+// Update allows firewall policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToFirewallPolicyUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will permanently delete a particular firewall policy based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
+
+type InsertRuleOptsBuilder interface {
+	ToFirewallPolicyInsertRuleMap() (map[string]interface{}, error)
+}
+
+type InsertRuleOpts struct {
+	ID           string `json:"firewall_rule_id" required:"true"`
+	BeforeRuleID string `json:"insert_before,omitempty"`
+	AfterRuleID  string `json:"insert_after,omitempty"`
+}
+
+func (opts InsertRuleOpts) ToFirewallPolicyInsertRuleMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+func AddRule(c *gophercloud.ServiceClient, id string, opts InsertRuleOptsBuilder) (r InsertRuleResult) {
+	b, err := opts.ToFirewallPolicyInsertRuleMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(insertURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+func RemoveRule(c *gophercloud.ServiceClient, id, ruleID string) (r RemoveRuleResult) {
+	b := map[string]interface{}{"firewall_rule_id": ruleID}
+	_, r.Err = c.Put(removeURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
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..9c5b186
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/results.go
@@ -0,0 +1,97 @@
+package policies
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Policy is a firewall policy.
+type Policy struct {
+	ID          string   `json:"id"`
+	Name        string   `json:"name"`
+	Description string   `json:"description"`
+	TenantID    string   `json:"tenant_id"`
+	Audited     bool     `json:"audited"`
+	Shared      bool     `json:"shared"`
+	Rules       []string `json:"firewall_rules,omitempty"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall policy.
+func (r commonResult) Extract() (*Policy, error) {
+	var s struct {
+		Policy *Policy `json:"firewall_policy"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 (r PolicyPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"firewall_policies_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PolicyPage struct is empty.
+func (r PolicyPage) IsEmpty() (bool, error) {
+	is, err := ExtractPolicies(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Policy, error) {
+	var s struct {
+		Policies []Policy `json:"firewall_policies"`
+	}
+	err := (r.(PolicyPage)).ExtractInto(&s)
+	return s.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
+}
+
+// InsertRuleResult represents the result of an InsertRule operation.
+type InsertRuleResult struct {
+	commonResult
+}
+
+// RemoveRuleResult represents the result of a RemoveRule operation.
+type RemoveRuleResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/testing/doc.go b/openstack/networking/v2/extensions/fwaas/policies/testing/doc.go
new file mode 100644
index 0000000..d2707f0
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_fwaas_policies_v2
+package testing
diff --git a/openstack/networking/v2/extensions/fwaas/policies/testing/requests_test.go b/openstack/networking/v2/extensions/fwaas/policies/testing/requests_test.go
new file mode 100644
index 0000000..11b9848
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/testing/requests_test.go
@@ -0,0 +1,274 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	policies.List(fake.ServiceClient(), policies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := policies.ExtractPolicies(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []policies.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",
+			},
+			{
+				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 := policies.CreateOpts{
+		TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+		Name:        "policy",
+		Description: "Firewall policy",
+		Shared:      gophercloud.Disabled,
+		Audited:     gophercloud.Enabled,
+		Rules: []string{
+			"98a58c87-76be-ae7c-a74e-b77fffb88d95",
+			"11a58c87-76be-ae7c-a74e-b77fffb88a32",
+		},
+	}
+
+	_, err := policies.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 := policies.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 := policies.UpdateOpts{
+		Name:        "policy",
+		Description: "Firewall policy",
+		Rules: []string{
+			"98a58c87-76be-ae7c-a74e-b77fffb88d95",
+			"11a58c87-76be-ae7c-a74e-b77fffb88a32",
+		},
+	}
+
+	_, err := policies.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 := policies.Delete(fake.ServiceClient(), "4ec89077-d057-4a2b-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..c252b79
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/urls.go
@@ -0,0 +1,26 @@
+package policies
+
+import "github.com/gophercloud/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/routerinsertion/doc.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go
new file mode 100644
index 0000000..9b847e2
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go
@@ -0,0 +1,2 @@
+// Package routerinsertion implements the fwaasrouterinsertion FWaaS extension.
+package routerinsertion
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go
new file mode 100644
index 0000000..3a5942e
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go
@@ -0,0 +1,51 @@
+package routerinsertion
+
+import (
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+)
+
+// CreateOptsExt adds a RouterIDs option to the base CreateOpts.
+type CreateOptsExt struct {
+	firewalls.CreateOptsBuilder
+	RouterIDs []string `json:"router_ids"`
+}
+
+// ToFirewallCreateMap adds router_ids to the base firewall creation options.
+func (opts CreateOptsExt) ToFirewallCreateMap() (map[string]interface{}, error) {
+	base, err := opts.CreateOptsBuilder.ToFirewallCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(opts.RouterIDs) == 0 {
+		return base, nil
+	}
+
+	firewallMap := base["firewall"].(map[string]interface{})
+	firewallMap["router_ids"] = opts.RouterIDs
+
+	return base, nil
+}
+
+// UpdateOptsExt updates a RouterIDs option to the base UpdateOpts.
+type UpdateOptsExt struct {
+	firewalls.UpdateOptsBuilder
+	RouterIDs []string `json:"router_ids"`
+}
+
+// ToFirewallUpdateMap adds router_ids to the base firewall update options.
+func (opts UpdateOptsExt) ToFirewallUpdateMap() (map[string]interface{}, error) {
+	base, err := opts.UpdateOptsBuilder.ToFirewallUpdateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(opts.RouterIDs) == 0 {
+		return base, nil
+	}
+
+	firewallMap := base["firewall"].(map[string]interface{})
+	firewallMap["router_ids"] = opts.RouterIDs
+
+	return base, nil
+}
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go
new file mode 100644
index 0000000..36a6c1c
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_fwaas_extensions_routerinsertion_v2
+package testing
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go
new file mode 100644
index 0000000..a4890ee
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go
@@ -0,0 +1,126 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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",
+        "router_ids": [
+          "8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"
+        ]
+    }
+}
+      `)
+
+		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"
+    }
+}
+    `)
+	})
+
+	firewallCreateOpts := firewalls.CreateOpts{
+		TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+		Name:         "fw",
+		Description:  "OpenStack firewall",
+		AdminStateUp: gophercloud.Enabled,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	createOpts := routerinsertion.CreateOptsExt{
+		firewallCreateOpts,
+		[]string{"8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"},
+	}
+
+	_, err := firewalls.Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+}
+
+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",
+        "router_ids": [
+          "8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"
+        ]
+    }
+}
+      `)
+
+		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"
+    }
+}
+    `)
+	})
+
+	firewallUpdateOpts := firewalls.UpdateOpts{
+		Name:         "fw",
+		Description:  "updated fw",
+		AdminStateUp: gophercloud.Disabled,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	updateOpts := routerinsertion.UpdateOptsExt{
+		firewallUpdateOpts,
+		[]string{"8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"},
+	}
+
+	_, err := firewalls.Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", updateOpts).Extract()
+	th.AssertNoErr(t, err)
+}
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..c1784b7
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests.go
@@ -0,0 +1,188 @@
+package rules
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type (
+	// Protocol represents a valid rule protocol
+	Protocol string
+)
+
+const (
+	// ProtocolAny is to allow any protocol
+	ProtocolAny Protocol = "any"
+
+	// ProtocolICMP is to allow the ICMP protocol
+	ProtocolICMP Protocol = "icmp"
+
+	// ProtocolTCP is to allow the TCP protocol
+	ProtocolTCP Protocol = "tcp"
+
+	// ProtocolUDP is to allow the UDP protocol
+	ProtocolUDP Protocol = "udp"
+)
+
+// 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 {
+	Protocol             Protocol              `json:"protocol" required:"true"`
+	Action               string                `json:"action" required:"true"`
+	TenantID             string                `json:"tenant_id,omitempty"`
+	Name                 string                `json:"name,omitempty"`
+	Description          string                `json:"description,omitempty"`
+	IPVersion            gophercloud.IPVersion `json:"ip_version,omitempty"`
+	SourceIPAddress      string                `json:"source_ip_address,omitempty"`
+	DestinationIPAddress string                `json:"destination_ip_address,omitempty"`
+	SourcePort           string                `json:"source_port,omitempty"`
+	DestinationPort      string                `json:"destination_port,omitempty"`
+	Shared               *bool                 `json:"shared,omitempty"`
+	Enabled              *bool                 `json:"enabled,omitempty"`
+}
+
+// ToRuleCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "firewall_rule")
+	if err != nil {
+		return nil, err
+	}
+
+	if m := b["firewall_rule"].(map[string]interface{}); m["protocol"] == "any" {
+		m["protocol"] = nil
+	}
+
+	return b, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall rule
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToRuleCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular firewall rule based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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.
+type UpdateOpts struct {
+	Protocol             *string                `json:"protocol,omitempty"`
+	Action               *string                `json:"action,omitempty"`
+	Name                 *string                `json:"name,omitempty"`
+	Description          *string                `json:"description,omitempty"`
+	IPVersion            *gophercloud.IPVersion `json:"ip_version,omitempty"`
+	SourceIPAddress      *string                `json:"source_ip_address,omitempty"`
+	DestinationIPAddress *string                `json:"destination_ip_address,omitempty"`
+	SourcePort           *string                `json:"source_port,omitempty"`
+	DestinationPort      *string                `json:"destination_port,omitempty"`
+	Shared               *bool                  `json:"shared,omitempty"`
+	Enabled              *bool                  `json:"enabled,omitempty"`
+}
+
+// ToRuleUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToRuleUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "firewall_rule")
+}
+
+// Update allows firewall policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToRuleUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will permanently delete a particular firewall rule based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..c44e5a9
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/results.go
@@ -0,0 +1,95 @@
+package rules
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Rule represents a firewall rule
+type Rule struct {
+	ID                   string `json:"id"`
+	Name                 string `json:"name,omitempty"`
+	Description          string `json:"description,omitempty"`
+	Protocol             string `json:"protocol"`
+	Action               string `json:"action"`
+	IPVersion            int    `json:"ip_version,omitempty"`
+	SourceIPAddress      string `json:"source_ip_address,omitempty"`
+	DestinationIPAddress string `json:"destination_ip_address,omitempty"`
+	SourcePort           string `json:"source_port,omitempty"`
+	DestinationPort      string `json:"destination_port,omitempty"`
+	Shared               bool   `json:"shared,omitempty"`
+	Enabled              bool   `json:"enabled,omitempty"`
+	PolicyID             string `json:"firewall_policy_id"`
+	Position             int    `json:"position"`
+	TenantID             string `json:"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 (r RulePage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"firewall_rules_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a RulePage struct is empty.
+func (r RulePage) IsEmpty() (bool, error) {
+	is, err := ExtractRules(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Rule, error) {
+	var s struct {
+		Rules []Rule `json:"firewall_rules"`
+	}
+	err := (r.(RulePage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Rule *Rule `json:"firewall_rule"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/fwaas/rules/testing/doc.go
new file mode 100644
index 0000000..481ae2e
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_fwaas_rules_v2
+package testing
diff --git a/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go b/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go
new file mode 100644
index 0000000..2fedfa8
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go
@@ -0,0 +1,381 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	rules.List(fake.ServiceClient(), rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := rules.ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []rules.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,
+			},
+			{
+				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 := rules.CreateOpts{
+		TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+		Protocol:             rules.ProtocolTCP,
+		Description:          "ssh rule",
+		DestinationIPAddress: "192.168.1.0/24",
+		DestinationPort:      "22",
+		Name:                 "ssh_form_any",
+		Action:               "allow",
+	}
+
+	_, err := rules.Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestCreateAnyProtocol(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": null,
+		"description": "any to 192.168.1.0/24",
+		"destination_ip_address": "192.168.1.0/24",
+		"name": "any_to_192.168.1.0/24",
+		"action": "allow",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61"
+	}
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": null,
+		"description": "any to 192.168.1.0/24",
+		"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": null,
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "any_to_192.168.1.0/24",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": true,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+        `)
+	})
+
+	options := rules.CreateOpts{
+		TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+		Protocol:             rules.ProtocolAny,
+		Description:          "any to 192.168.1.0/24",
+		DestinationIPAddress: "192.168.1.0/24",
+		Name:                 "any_to_192.168.1.0/24",
+		Action:               "allow",
+	}
+
+	_, err := rules.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 := rules.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",
+		"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",
+		"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
+	}
+}
+		`)
+	})
+
+	newProtocol := "tcp"
+	newDescription := "ssh rule"
+	newDestinationIP := "192.168.1.0/24"
+	newDestintionPort := "22"
+	newName := "ssh_form_any"
+	newAction := "allow"
+
+	options := rules.UpdateOpts{
+		Protocol:             &newProtocol,
+		Description:          &newDescription,
+		DestinationIPAddress: &newDestinationIP,
+		DestinationPort:      &newDestintionPort,
+		Name:                 &newName,
+		Action:               &newAction,
+		Enabled:              gophercloud.Disabled,
+	}
+
+	_, err := rules.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 := rules.Delete(fake.ServiceClient(), "4ec89077-d057-4a2b-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..79654be
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/urls.go
@@ -0,0 +1,16 @@
+package rules
+
+import "github.com/gophercloud/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..8393087
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -0,0 +1,146 @@
+package floatingips
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+	RouterID          string `q:"router_id"`
+}
+
+// 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}}
+	})
+}
+
+// CreateOptsBuilder is the interface type must satisfy to be used as Create
+// options.
+type CreateOptsBuilder interface {
+	ToFloatingIPCreateMap() (map[string]interface{}, error)
+}
+
+// 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 `json:"floating_network_id" required:"true"`
+	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"`
+}
+
+// ToFloatingIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder
+// interface
+func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "floatingip")
+}
+
+// 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 CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToFloatingIPCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular floating IP resource based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// UpdateOptsBuilder is the interface type must satisfy to be used as Update
+// options.
+type UpdateOptsBuilder interface {
+	ToFloatingIPUpdateMap() (map[string]interface{}, error)
+}
+
+// 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 `json:"port_id"`
+}
+
+// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder
+// interface
+func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "floatingip")
+}
+
+// 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 UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToFloatingIPUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// 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) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..29d5b56
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -0,0 +1,110 @@
+package floatingips
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+
+	// UUID of the external network where the floating IP is to be created.
+	FloatingNetworkID string `json:"floating_network_id"`
+
+	// Address of the floating IP on the external network.
+	FloatingIP string `json:"floating_ip_address"`
+
+	// UUID of the port on an internal network that is associated with the floating IP.
+	PortID string `json:"port_id"`
+
+	// The specific IP address of the internal port which should be associated
+	// with the floating IP.
+	FixedIP string `json:"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"`
+
+	// The condition of the API resource.
+	Status string `json:"status"`
+
+	//The ID of the router used for this Floating-IP
+	RouterID string `json:"router_id"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract a result and extracts a FloatingIP resource.
+func (r commonResult) Extract() (*FloatingIP, error) {
+	var s struct {
+		FloatingIP *FloatingIP `json:"floatingip"`
+	}
+	err := r.ExtractInto(&s)
+	return s.FloatingIP, 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 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 (r FloatingIPPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"floatingips_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (r FloatingIPPage) IsEmpty() (bool, error) {
+	is, err := ExtractFloatingIPs(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]FloatingIP, error) {
+	var s struct {
+		FloatingIPs []FloatingIP `json:"floatingips"`
+	}
+	err := (r.(FloatingIPPage)).ExtractInto(&s)
+	return s.FloatingIPs, err
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/testing/doc.go b/openstack/networking/v2/extensions/layer3/floatingips/testing/doc.go
new file mode 100644
index 0000000..aa13338
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_layer3_floatingips_v2
+package testing
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go b/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go
new file mode 100644
index 0000000..c665a2e
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go
@@ -0,0 +1,363 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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",
+            "router_id": "1117c30a-ddb4-49a1-bec3-a65b286b4170"
+        },
+        {
+            "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",
+            "router_id": "2227c30a-ddb4-49a1-bec3-a65b286b4170"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	floatingips.List(fake.ServiceClient(), floatingips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := floatingips.ExtractFloatingIPs(page)
+		if err != nil {
+			t.Errorf("Failed to extract floating IPs: %v", err)
+			return false, err
+		}
+
+		expected := []floatingips.FloatingIP{
+			{
+				FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+				FixedIP:           "",
+				FloatingIP:        "192.0.0.4",
+				TenantID:          "017d8de156df4177889f31a9bd6edc00",
+				Status:            "DOWN",
+				PortID:            "",
+				ID:                "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e",
+				RouterID:          "1117c30a-ddb4-49a1-bec3-a65b286b4170",
+			},
+			{
+				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",
+				RouterID:          "2227c30a-ddb4-49a1-bec3-a65b286b4170",
+			},
+		}
+
+		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": {}}`)
+	})
+
+	floatingips.List(fake.ServiceClient(), floatingips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		floatingips.ExtractFloatingIPs(page)
+		return true, nil
+	})
+}
+
+func TestRequiredFieldsForCreate(t *testing.T) {
+	res1 := floatingips.Create(fake.ServiceClient(), floatingips.CreateOpts{FloatingNetworkID: ""})
+	if res1.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res2 := floatingips.Create(fake.ServiceClient(), floatingips.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 := floatingips.CreateOpts{
+		FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+		PortID:            "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+	}
+
+	ip, err := floatingips.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 := floatingips.CreateOpts{
+		FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+	}
+
+	ip, err := floatingips.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",
+        "router_id": "1117c30a-ddb4-49a1-bec3-a65b286b4170"
+    }
+}
+      `)
+	})
+
+	ip, err := floatingips.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)
+	th.AssertEquals(t, "1117c30a-ddb4-49a1-bec3-a65b286b4170", ip.RouterID)
+}
+
+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"
+	}
+}
+	`)
+	})
+
+	portID := "423abc8d-2991-4a55-ba98-2aaea84cc72e"
+	ip, err := floatingips.Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", floatingips.UpdateOpts{PortID: &portID}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, portID, 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 := floatingips.Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", floatingips.UpdateOpts{PortID: nil}).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 := floatingips.Delete(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7")
+	th.AssertNoErr(t, res.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..1318a18
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
@@ -0,0 +1,13 @@
+package floatingips
+
+import "github.com/gophercloud/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..71b2f62
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -0,0 +1,223 @@
+package routers
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+	Distributed  *bool  `q:"distributed"`
+	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}}
+	})
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToRouterCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new router. There are
+// no required values.
+type CreateOpts struct {
+	Name         string       `json:"name,omitempty"`
+	AdminStateUp *bool        `json:"admin_state_up,omitempty"`
+	Distributed  *bool        `json:"distributed,omitempty"`
+	TenantID     string       `json:"tenant_id,omitempty"`
+	GatewayInfo  *GatewayInfo `json:"external_gateway_info,omitempty"`
+}
+
+func (opts CreateOpts) ToRouterCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "router")
+}
+
+// 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 CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToRouterCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular router based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+type UpdateOptsBuilder interface {
+	ToRouterUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a router.
+type UpdateOpts struct {
+	Name         string       `json:"name,omitempty"`
+	AdminStateUp *bool        `json:"admin_state_up,omitempty"`
+	Distributed  *bool        `json:"distributed,omitempty"`
+	GatewayInfo  *GatewayInfo `json:"external_gateway_info,omitempty"`
+	Routes       []Route      `json:"routes"`
+}
+
+func (opts UpdateOpts) ToRouterUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "router")
+}
+
+// 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 UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToRouterUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will permanently delete a particular router based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
+
+// AddInterfaceOptsBuilder is what types must satisfy to be used as AddInterface
+// options.
+type AddInterfaceOptsBuilder interface {
+	ToRouterAddInterfaceMap() (map[string]interface{}, error)
+}
+
+// AddInterfaceOpts allow you to work with operations that either add
+// an internal interface from a router.
+type AddInterfaceOpts struct {
+	SubnetID string `json:"subnet_id,omitempty" xor:"PortID"`
+	PortID   string `json:"port_id,omitempty" xor:"SubnetID"`
+}
+
+// ToRouterAddInterfaceMap allows InterfaceOpts to satisfy the InterfaceOptsBuilder
+// interface
+func (opts AddInterfaceOpts) ToRouterAddInterfaceMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// 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 AddInterfaceOptsBuilder) (r InterfaceResult) {
+	b, err := opts.ToRouterAddInterfaceMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(addInterfaceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// RemoveInterfaceOptsBuilder is what types must satisfy to be used as RemoveInterface
+// options.
+type RemoveInterfaceOptsBuilder interface {
+	ToRouterRemoveInterfaceMap() (map[string]interface{}, error)
+}
+
+// RemoveInterfaceOpts allow you to work with operations that either add or remote
+// an internal interface from a router.
+type RemoveInterfaceOpts struct {
+	SubnetID string `json:"subnet_id,omitempty" or:"PortID"`
+	PortID   string `json:"port_id,omitempty" or:"SubnetID"`
+}
+
+// ToRouterRemoveInterfaceMap allows RemoveInterfaceOpts to satisfy the RemoveInterfaceOptsBuilder
+// interface
+func (opts RemoveInterfaceOpts) ToRouterRemoveInterfaceMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// 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 RemoveInterfaceOptsBuilder) (r InterfaceResult) {
+	b, err := opts.ToRouterRemoveInterfaceMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(removeInterfaceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
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..d849d45
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -0,0 +1,152 @@
+package routers
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// GatewayInfo represents the information of an external gateway for any
+// particular network router.
+type GatewayInfo struct {
+	NetworkID string `json:"network_id"`
+}
+
+// Route is a possible route in a router.
+type Route struct {
+	NextHop         string `json:"nexthop"`
+	DestinationCIDR string `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"`
+
+	// Information on external gateway for the router.
+	GatewayInfo GatewayInfo `json:"external_gateway_info"`
+
+	// Administrative state of the router.
+	AdminStateUp bool `json:"admin_state_up"`
+
+	// Whether router is disitrubted or not..
+	Distributed bool `json:"distributed"`
+
+	// Human readable name for the router. Does not have to be unique.
+	Name string `json:"name"`
+
+	// Unique identifier for the router.
+	ID string `json:"id"`
+
+	// Owner of the router. Only admin users can specify a tenant identifier
+	// other than its own.
+	TenantID string `json:"tenant_id"`
+
+	Routes []Route `json:"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 (r RouterPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"routers_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (r RouterPage) IsEmpty() (bool, error) {
+	is, err := ExtractRouters(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Router, error) {
+	var s struct {
+		Routers []Router `json:"routers"`
+	}
+	err := (r.(RouterPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Router *Router `json:"router"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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"`
+
+	// The ID of the port that is a part of the subnet.
+	PortID string `json:"port_id"`
+
+	// The UUID of the interface.
+	ID string `json:"id"`
+
+	// Owner of the interface.
+	TenantID string `json:"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) {
+	var s InterfaceInfo
+	err := r.ExtractInto(&s)
+	return &s, err
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/testing/doc.go b/openstack/networking/v2/extensions/layer3/routers/testing/doc.go
new file mode 100644
index 0000000..ef44c2e
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_layer3_routers_v2
+package testing
diff --git a/openstack/networking/v2/extensions/layer3/routers/testing/requests_test.go b/openstack/networking/v2/extensions/layer3/routers/testing/requests_test.go
new file mode 100644
index 0000000..bf7f35e
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/testing/requests_test.go
@@ -0,0 +1,407 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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",
+            "distributed": false,
+            "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",
+            "distributed": false,
+            "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d"
+        }
+    ]
+}
+			`)
+	})
+
+	count := 0
+
+	routers.List(fake.ServiceClient(), routers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := routers.ExtractRouters(page)
+		if err != nil {
+			t.Errorf("Failed to extract routers: %v", err)
+			return false, err
+		}
+
+		expected := []routers.Router{
+			{
+				Status:       "ACTIVE",
+				GatewayInfo:  routers.GatewayInfo{NetworkID: ""},
+				AdminStateUp: true,
+				Distributed:  false,
+				Name:         "second_routers",
+				ID:           "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b",
+				TenantID:     "6b96ff0cb17a4b859e1e575d221683d3",
+			},
+			{
+				Status:       "ACTIVE",
+				GatewayInfo:  routers.GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"},
+				AdminStateUp: true,
+				Distributed:  false,
+				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",
+        "distributed": false,
+        "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+    }
+}
+		`)
+	})
+
+	asu := false
+	gwi := routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+
+	options := routers.CreateOpts{
+		Name:         "foo_router",
+		AdminStateUp: &asu,
+		GatewayInfo:  &gwi,
+	}
+	r, err := routers.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, routers.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",
+        "distributed": false,
+        "id": "a07eea83-7710-4860-931b-5fe220fae533"
+    }
+}
+			`)
+	})
+
+	n, err := routers.Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.GatewayInfo, routers.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, []routers.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",
+        "distributed": false,
+        "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e",
+        "routes": [
+            {
+                "nexthop": "10.1.0.10",
+                "destination": "40.0.1.0/24"
+            }
+        ]
+    }
+}
+		`)
+	})
+
+	gwi := routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+	r := []routers.Route{{DestinationCIDR: "40.0.1.0/24", NextHop: "10.1.0.10"}}
+	options := routers.UpdateOpts{Name: "new_name", GatewayInfo: &gwi, Routes: r}
+
+	n, err := routers.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, routers.GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"})
+	th.AssertDeepEquals(t, n.Routes, []routers.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",
+        "distributed": false,
+        "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e",
+        "routes": []
+    }
+}
+		`)
+	})
+
+	r := []routers.Route{}
+	options := routers.UpdateOpts{Routes: r}
+
+	n, err := routers.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, n.Routes, []routers.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 := routers.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 := routers.AddInterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+	res, err := routers.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 := routers.AddInterface(fake.ServiceClient(), "foo", routers.AddInterfaceOpts{}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	_, err = routers.AddInterface(fake.ServiceClient(), "foo", routers.AddInterfaceOpts{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 := routers.RemoveInterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+	res, err := routers.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/urls.go b/openstack/networking/v2/extensions/layer3/routers/urls.go
new file mode 100644
index 0000000..f9e9da3
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/urls.go
@@ -0,0 +1,21 @@
+package routers
+
+import "github.com/gophercloud/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..7e7b768
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/requests.go
@@ -0,0 +1,115 @@
+package members
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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}}
+	})
+}
+
+type CreateOptsBuilder interface {
+	ToLBMemberCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new pool member.
+type CreateOpts struct {
+	// The IP address of the member.
+	Address string `json:"address" required:"true"`
+	// The port on which the application is hosted.
+	ProtocolPort int `json:"protocol_port" required:"true"`
+	// The pool to which this member will belong.
+	PoolID string `json:"pool_id" required:"true"`
+	// Only required if the caller has an admin role and wants to create a pool
+	// for another tenant.
+	TenantID string `json:"tenant_id,omitempty"`
+}
+
+func (opts CreateOpts) ToLBMemberCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "member")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool member.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToLBMemberCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular pool member based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+type UpdateOptsBuilder interface {
+	ToLBMemberUpdateMap() (map[string]interface{}, error)
+}
+
+// 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 `json:"admin_state_up,omitempty"`
+}
+
+func (opts UpdateOpts) ToLBMemberUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "member")
+}
+
+// Update allows members to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToLBMemberUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	return
+}
+
+// Delete will permanently delete a particular member based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..933e1ae
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -0,0 +1,104 @@
+package members
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+
+	// Owner of the member. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id"`
+
+	// The pool to which the member belongs.
+	PoolID string `json:"pool_id"`
+
+	// The IP address of the member.
+	Address string
+
+	// The port on which the application is hosted.
+	ProtocolPort int `json:"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 (r MemberPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"members_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a MemberPage struct is empty.
+func (r MemberPage) IsEmpty() (bool, error) {
+	is, err := ExtractMembers(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Member, error) {
+	var s struct {
+		Members []Member `json:"members"`
+	}
+	err := (r.(MemberPage)).ExtractInto(&s)
+	return s.Members, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Member, error) {
+	var s struct {
+		Member *Member `json:"member"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/lbaas/members/testing/doc.go
new file mode 100644
index 0000000..3878904
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_members_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas/members/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas/members/testing/requests_test.go
new file mode 100644
index 0000000..3e4f1d4
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/testing/requests_test.go
@@ -0,0 +1,238 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/members"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	members.List(fake.ServiceClient(), members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := members.ExtractMembers(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []members.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",
+			},
+			{
+				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 := members.CreateOpts{
+		TenantID:     "453105b9-1754-413f-aab1-55f1af620750",
+		Address:      "192.0.2.14",
+		ProtocolPort: 8080,
+		PoolID:       "foo",
+	}
+	_, err := members.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 := members.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 := members.UpdateOpts{AdminStateUp: gophercloud.Disabled}
+
+	_, err := members.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 := members.Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+	th.AssertNoErr(t, res.Err)
+}
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..e2248f8
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/urls.go
@@ -0,0 +1,16 @@
+package members
+
+import "github.com/gophercloud/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..f1b964b
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests.go
@@ -0,0 +1,210 @@
+package monitors
+
+import (
+	"fmt"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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}}
+	})
+}
+
+// MonitorType is the type for all the types of LB monitors
+type MonitorType string
+
+// Constants that represent approved monitoring types.
+const (
+	TypePING  MonitorType = "PING"
+	TypeTCP   MonitorType = "TCP"
+	TypeHTTP  MonitorType = "HTTP"
+	TypeHTTPS MonitorType = "HTTPS"
+)
+
+// CreateOptsBuilder is what types must satisfy to be used as Create
+// options.
+type CreateOptsBuilder interface {
+	ToLBMonitorCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new health monitor.
+type CreateOpts struct {
+	// 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 MonitorType `json:"type" required:"true"`
+	// Required. The time, in seconds, between sending probes to members.
+	Delay int `json:"delay" required:"true"`
+	// 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 `json:"timeout" required:"true"`
+	// Required. Number of permissible ping failures before changing the member's
+	// status to INACTIVE. Must be a number between 1 and 10.
+	MaxRetries int `json:"max_retries" required:"true"`
+	// Required for HTTP(S) types. URI path that will be accessed if monitor type
+	// is HTTP or HTTPS.
+	URLPath string `json:"url_path,omitempty"`
+	// 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 `json:"http_method,omitempty"`
+	// 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 `json:"expected_codes,omitempty"`
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID     string `json:"tenant_id,omitempty"`
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+}
+
+// ToLBMonitorCreateMap allows CreateOpts to satisfy the CreateOptsBuilder
+// interface
+func (opts CreateOpts) ToLBMonitorCreateMap() (map[string]interface{}, error) {
+	if opts.Type == TypeHTTP || opts.Type == TypeHTTPS {
+		if opts.URLPath == "" {
+			err := gophercloud.ErrMissingInput{}
+			err.Argument = "monitors.CreateOpts.URLPath"
+			return nil, err
+		}
+		if opts.ExpectedCodes == "" {
+			err := gophercloud.ErrMissingInput{}
+			err.Argument = "monitors.CreateOpts.ExpectedCodes"
+			return nil, err
+		}
+	}
+	if opts.Delay < opts.Timeout {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "monitors.CreateOpts.Delay/monitors.CreateOpts.Timeout"
+		err.Info = "Delay must be greater than or equal to timeout"
+		return nil, err
+	}
+	return gophercloud.BuildRequestBody(opts, "health_monitor")
+}
+
+// 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 CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToLBMonitorCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular health monitor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// UpdateOptsBuilder is what types must satisfy to be used as Update
+// options.
+type UpdateOptsBuilder interface {
+	ToLBMonitorUpdateMap() (map[string]interface{}, error)
+}
+
+// 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 {
+	// The time, in seconds, between sending probes to members.
+	Delay int `json:"delay,omitempty"`
+	// 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 `json:"timeout,omitempty"`
+	// Number of permissible ping failures before changing the member's
+	// status to INACTIVE. Must be a number between 1 and 10.
+	MaxRetries int `json:"max_retries,omitempty"`
+	// URI path that will be accessed if monitor type
+	// is HTTP or HTTPS.
+	URLPath string `json:"url_path,omitempty"`
+	// The HTTP method used for requests by the
+	// monitor. If this attribute is not specified, it defaults to "GET".
+	HTTPMethod string `json:"http_method,omitempty"`
+	// 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 `json:"expected_codes,omitempty"`
+	AdminStateUp  *bool  `json:"admin_state_up,omitempty"`
+}
+
+// ToLBMonitorUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder
+// interface
+func (opts UpdateOpts) ToLBMonitorUpdateMap() (map[string]interface{}, error) {
+	if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout {
+		err := gophercloud.ErrInvalidInput{}
+		err.Argument = "monitors.CreateOpts.Delay/monitors.CreateOpts.Timeout"
+		err.Value = fmt.Sprintf("%d/%d", opts.Delay, opts.Timeout)
+		err.Info = "Delay must be greater than or equal to timeout"
+		return nil, err
+	}
+	return gophercloud.BuildRequestBody(opts, "health_monitor")
+}
+
+// Update is an operation which modifies the attributes of the specified monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToLBMonitorUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will permanently delete a particular monitor based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..0385942
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -0,0 +1,136 @@
+package monitors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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
+
+	// Monitor name. Does not have to be unique.
+	Name string
+
+	// Owner of the VIP. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"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"`
+
+	// The HTTP method that the monitor uses for requests.
+	HTTPMethod string `json:"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"`
+
+	// Expected HTTP codes for a passing HTTP(S) monitor.
+	ExpectedCodes string `json:"expected_codes"`
+
+	// The administrative state of the health monitor, which is up (true) or down (false).
+	AdminStateUp bool `json:"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 (r MonitorPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"health_monitors_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (r MonitorPage) IsEmpty() (bool, error) {
+	is, err := ExtractMonitors(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Monitor, error) {
+	var s struct {
+		Monitors []Monitor `json:"health_monitors"`
+	}
+	err := (r.(MonitorPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Monitor *Monitor `json:"health_monitor"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/lbaas/monitors/testing/doc.go
new file mode 100644
index 0000000..5ee866b
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_monitors_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas/monitors/testing/requests_test.go
new file mode 100644
index 0000000..f736074
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/testing/requests_test.go
@@ -0,0 +1,310 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	monitors.List(fake.ServiceClient(), monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := monitors.ExtractMonitors(page)
+		if err != nil {
+			t.Errorf("Failed to extract monitors: %v", err)
+			return false, err
+		}
+
+		expected := []monitors.Monitor{
+			{
+				AdminStateUp: true,
+				TenantID:     "83657cfcdfe44cd5920adaf26c48ceea",
+				Delay:        10,
+				MaxRetries:   1,
+				Timeout:      1,
+				Type:         "PING",
+				ID:           "466c8345-28d8-4f84-a246-e04380b0461d",
+			},
+			{
+				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 := monitors.Create(fake.ServiceClient(), monitors.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 = monitors.Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", monitors.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 := monitors.Create(fake.ServiceClient(), monitors.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 := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = monitors.Create(fake.ServiceClient(), monitors.CreateOpts{Type: monitors.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 := monitors.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": 30,
+      "timeout": 20,
+      "max_retries": 10,
+      "url_path": "/another_check",
+      "expected_codes": "301",
+			"admin_state_up": true
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+    "health_monitor": {
+        "admin_state_up": true,
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "delay": 30,
+        "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 := monitors.Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", monitors.UpdateOpts{
+		Delay:         30,
+		Timeout:       20,
+		MaxRetries:    10,
+		URLPath:       "/another_check",
+		ExpectedCodes: "301",
+		AdminStateUp:  gophercloud.Enabled,
+	}).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 := monitors.Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7")
+	th.AssertNoErr(t, res.Err)
+}
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..e9d90fc
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import "github.com/gophercloud/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..2a75737
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests.go
@@ -0,0 +1,170 @@
+package pools
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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}}
+	})
+}
+
+// LBMethod is a type used for possible load balancing methods
+type LBMethod string
+
+// LBProtocol is a type used for possible load balancing protocols
+type LBProtocol string
+
+// Supported attributes for create/update operations.
+const (
+	LBMethodRoundRobin       LBMethod = "ROUND_ROBIN"
+	LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS"
+
+	ProtocolTCP   LBProtocol = "TCP"
+	ProtocolHTTP  LBProtocol = "HTTP"
+	ProtocolHTTPS LBProtocol = "HTTPS"
+)
+
+// CreateOptsBuilder is the interface types must satisfy to be used as options
+// for the Create function
+type CreateOptsBuilder interface {
+	ToLBPoolCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new pool.
+type CreateOpts struct {
+	// Name of the pool.
+	Name string `json:"name" required:"true"`
+	// The protocol used by the pool members, you can use either
+	// ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+	Protocol LBProtocol `json:"protocol" required:"true"`
+	// Only required if the caller has an admin role and wants to create a pool
+	// for another tenant.
+	TenantID string `json:"tenant_id,omitempty"`
+	// 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,omitempty"`
+	// 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 LBMethod `json:"lb_method" required:"true"`
+
+	// The provider of the pool
+	Provider string `json:"provider,omitempty"`
+}
+
+// ToLBPoolCreateMap allows CreateOpts to satisfy the CreateOptsBuilder interface
+func (opts CreateOpts) ToLBPoolCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "pool")
+}
+
+// Create accepts a CreateOptsBuilder and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToLBPoolCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular pool based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// UpdateOptsBuilder is the interface types must satisfy to be used as options
+// for the Update function
+type UpdateOptsBuilder interface {
+	ToLBPoolUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a pool.
+type UpdateOpts struct {
+	// Name of the pool.
+	Name string `json:"name,omitempty"`
+	// 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 LBMethod `json:"lb_method,omitempty"`
+}
+
+// ToLBPoolUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder interface
+func (opts UpdateOpts) ToLBPoolUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "pool")
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToLBPoolUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will permanently delete a particular pool based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
+
+// 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) (r AssociateResult) {
+	b := map[string]interface{}{"health_monitor": map[string]string{"id": monitorID}}
+	_, r.Err = c.Post(associateURL(c, poolID), b, &r.Body, nil)
+	return
+}
+
+// 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) (r AssociateResult) {
+	_, r.Err = c.Delete(disassociateURL(c, poolID, monitorID), nil)
+	return
+}
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..2ca1963
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -0,0 +1,131 @@
+package pools
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+
+	// 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"`
+
+	// 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"`
+
+	// Owner of the pool. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id"`
+
+	// The administrative state of the pool, which is up (true) or down (false).
+	AdminStateUp bool `json:"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"`
+
+	// The unique ID for the pool.
+	ID string
+
+	// The ID of the virtual IP associated with this pool
+	VIPID string `json:"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 (r PoolPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"pools_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (r PoolPage) IsEmpty() (bool, error) {
+	is, err := ExtractPools(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Pool, error) {
+	var s struct {
+		Pools []Pool `json:"pools"`
+	}
+	err := (r.(PoolPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Pool *Pool `json:"pool"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/lbaas/pools/testing/doc.go
new file mode 100644
index 0000000..415dd2c
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_pools_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go
new file mode 100644
index 0000000..de038cb
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go
@@ -0,0 +1,316 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	pools.List(fake.ServiceClient(), pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := pools.ExtractPools(page)
+		if err != nil {
+			t.Errorf("Failed to extract pools: %v", err)
+			return false, err
+		}
+
+		expected := []pools.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",
+        "provider": "haproxy"
+    }
+}
+			`)
+
+		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": [],
+        "provider": "haproxy"
+    }
+}
+		`)
+	})
+
+	options := pools.CreateOpts{
+		LBMethod: pools.LBMethodRoundRobin,
+		Protocol: "HTTP",
+		Name:     "Example pool",
+		SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+		Provider: "haproxy",
+	}
+	p, err := pools.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)
+	th.AssertEquals(t, "haproxy", p.Provider)
+}
+
+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 := pools.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 := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections}
+
+	n, err := pools.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 := pools.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 := pools.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 := pools.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/urls.go b/openstack/networking/v2/extensions/lbaas/pools/urls.go
new file mode 100644
index 0000000..fe3601b
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/urls.go
@@ -0,0 +1,25 @@
+package pools
+
+import "github.com/gophercloud/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..f89d769
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests.go
@@ -0,0 +1,163 @@
+package vips
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+	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}}
+	})
+}
+
+// CreateOptsBuilder is what types must satisfy to be used as Create
+// options.
+type CreateOptsBuilder interface {
+	ToVIPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new virtual IP.
+type CreateOpts struct {
+	// Human-readable name for the VIP. Does not have to be unique.
+	Name string `json:"name" required:"true"`
+	// 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 `json:"subnet_id" required:"true"`
+	// The protocol - can either be TCP, HTTP or HTTPS.
+	Protocol string `json:"protocol" required:"true"`
+	// The port on which to listen for client traffic.
+	ProtocolPort int `json:"protocol_port" required:"true"`
+	// The ID of the pool with which the VIP is associated.
+	PoolID string `json:"pool_id" required:"true"`
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID string `json:"tenant_id,omitempty"`
+	// The IP address of the VIP.
+	Address string `json:"address,omitempty"`
+	// Human-readable description for the VIP.
+	Description string `json:"description,omitempty"`
+	// Omit this field to prevent session persistence.
+	Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+	// The maximum number of connections allowed for the VIP.
+	ConnLimit *int `json:"connection_limit,omitempty"`
+	// The administrative state of the VIP. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToVIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder
+// interface
+func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "vip")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToVIPCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular virtual IP based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// UpdateOptsBuilder is what types must satisfy to be used as Update
+// options.
+type UpdateOptsBuilder interface {
+	ToVIPUpdateMap() (map[string]interface{}, error)
+}
+
+// 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 `json:"name,omitempty"`
+	// The ID of the pool with which the VIP is associated.
+	PoolID *string `json:"pool_id,omitempty"`
+	// Human-readable description for the VIP.
+	Description *string `json:"description,omitempty"`
+	// Omit this field to prevent session persistence.
+	Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+	// The maximum number of connections allowed for the VIP.
+	ConnLimit *int `json:"connection_limit,omitempty"`
+	// The administrative state of the VIP. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToVIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder interface
+func (opts UpdateOpts) ToVIPUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "vip")
+}
+
+// Update is an operation which modifies the attributes of the specified VIP.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToVIPUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will permanently delete a particular virtual IP based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..7ac7e79
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -0,0 +1,151 @@
+package vips
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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 `json:"type"`
+
+	// Name of cookie if persistence mode is set appropriately
+	CookieName string `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 `json:"id"`
+
+	// Owner of the VIP. Only an admin user can specify a tenant ID other than its own.
+	TenantID string `json:"tenant_id"`
+
+	// Human-readable name for the VIP. Does not have to be unique.
+	Name string `json:"name"`
+
+	// Human-readable description for the VIP.
+	Description string `json:"description"`
+
+	// The ID of the subnet on which to allocate the VIP address.
+	SubnetID string `json:"subnet_id"`
+
+	// The IP address of the VIP.
+	Address string `json:"address"`
+
+	// The protocol of the VIP address. A valid value is TCP, HTTP, or HTTPS.
+	Protocol string `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 `json:"protocol_port"`
+
+	// The ID of the pool with which the VIP is associated.
+	PoolID string `json:"pool_id"`
+
+	// The ID of the port which belongs to the load balancer
+	PortID string `json:"port_id"`
+
+	// Indicates whether connections in the same session will be processed by the
+	// same pool member or not.
+	Persistence SessionPersistence `json:"session_persistence"`
+
+	// The maximum number of connections allowed for the VIP. Default is -1,
+	// meaning no limit.
+	ConnLimit int `json:"connection_limit"`
+
+	// The administrative state of the VIP. A valid value is true (UP) or false (DOWN).
+	AdminStateUp bool `json:"admin_state_up"`
+
+	// The status of the VIP. Indicates whether the VIP is operational.
+	Status string `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 (r VIPPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"vips_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a VIPPage struct is empty.
+func (r VIPPage) IsEmpty() (bool, error) {
+	is, err := ExtractVIPs(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]VirtualIP, error) {
+	var s struct {
+		VIPs []VirtualIP `json:"vips"`
+	}
+	err := (r.(VIPPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		VirtualIP *VirtualIP `json:"vip" json:"vip"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/lbaas/vips/testing/doc.go
new file mode 100644
index 0000000..8e91e78
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_vips_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas/vips/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas/vips/testing/requests_test.go
new file mode 100644
index 0000000..7f9b6dd
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/testing/requests_test.go
@@ -0,0 +1,330 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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
+
+	vips.List(fake.ServiceClient(), vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := vips.ExtractVIPs(page)
+		if err != nil {
+			t.Errorf("Failed to extract LBs: %v", err)
+			return false, err
+		}
+
+		expected := []vips.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:  vips.SessionPersistence{},
+				ConnLimit:    0,
+				AdminStateUp: true,
+				Status:       "ACTIVE",
+			},
+			{
+				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:  vips.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 := vips.CreateOpts{
+		Protocol:     "HTTP",
+		Name:         "NewVip",
+		AdminStateUp: gophercloud.Enabled,
+		SubnetID:     "8032909d-47a1-4715-90af-5153ffe39861",
+		PoolID:       "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+		ProtocolPort: 80,
+		Persistence:  &vips.SessionPersistence{Type: "SOURCE_IP"},
+	}
+
+	r, err := vips.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 := vips.Create(fake.ServiceClient(), vips.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo", SubnetID: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = vips.Create(fake.ServiceClient(), vips.CreateOpts{Name: "foo", SubnetID: "bar", Protocol: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = vips.Create(fake.ServiceClient(), vips.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 := vips.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, vips.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 := vips.UpdateOpts{
+		ConnLimit:   &i1000,
+		Persistence: &vips.SessionPersistence{Type: "SOURCE_IP"},
+	}
+	vip, err := vips.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 := vips.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..584a1cf
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/urls.go
@@ -0,0 +1,16 @@
+package vips
+
+import "github.com/gophercloud/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/lbaas_v2/doc.go b/openstack/networking/v2/extensions/lbaas_v2/doc.go
new file mode 100644
index 0000000..247a75f
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/doc.go
@@ -0,0 +1,5 @@
+// Package lbaas_v2 provides information and interaction with the Load Balancer
+// as a Service v2 extension for the OpenStack Networking service.
+// lbaas v2 api docs: http://developer.openstack.org/api-ref-networking-v2-ext.html#lbaas-v2.0
+// lbaas v2 api schema: https://github.com/openstack/neutron-lbaas/blob/master/neutron_lbaas/extensions/loadbalancerv2.py
+package lbaas_v2
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
new file mode 100644
index 0000000..4a78447
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
@@ -0,0 +1,182 @@
+package listeners
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type Protocol string
+
+// Supported attributes for create/update operations.
+const (
+	ProtocolTCP   Protocol = "TCP"
+	ProtocolHTTP  Protocol = "HTTP"
+	ProtocolHTTPS Protocol = "HTTPS"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToListenerListQuery() (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 floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular listener 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"`
+	TenantID        string `q:"tenant_id"`
+	LoadbalancerID  string `q:"loadbalancer_id"`
+	DefaultPoolID   string `q:"default_pool_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"`
+}
+
+// ToListenerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToListenerListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// 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 ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+	if opts != nil {
+		query, err := opts.ToListenerListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return ListenerPage{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 {
+	ToListenerCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// The load balancer on which to provision this listener.
+	LoadbalancerID string `json:"loadbalancer_id" required:"true"`
+	// The protocol - can either be TCP, HTTP or HTTPS.
+	Protocol Protocol `json:"protocol" required:"true"`
+	// The port on which to listen for client traffic.
+	ProtocolPort int `json:"protocol_port" required:"true"`
+	// Indicates the owner of the Listener. Required for admins.
+	TenantID string `json:"tenant_id,omitempty"`
+	// Human-readable name for the Listener. Does not have to be unique.
+	Name string `json:"name,omitempty"`
+	// The ID of the default pool with which the Listener is associated.
+	DefaultPoolID string `json:"default_pool_id,omitempty"`
+	// Human-readable description for the Listener.
+	Description string `json:"description,omitempty"`
+	// The maximum number of connections allowed for the Listener.
+	ConnLimit *int `json:"connection_limit,omitempty"`
+	// A reference to a container of TLS secrets.
+	DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"`
+	// A list of references to TLS secrets.
+	SniContainerRefs []string `json:"sni_container_refs,omitempty"`
+	// The administrative state of the Listener. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToListenerCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "listener")
+}
+
+// Create is an operation which provisions a new Listeners 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.
+//
+// Users with an admin role can create Listeners on behalf of other tenants by
+// specifying a TenantID attribute different than their own.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToListenerCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular Listeners based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	ToListenerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	// Human-readable name for the Listener. Does not have to be unique.
+	Name string `json:"name,omitempty"`
+	// Human-readable description for the Listener.
+	Description string `json:"description,omitempty"`
+	// The maximum number of connections allowed for the Listener.
+	ConnLimit *int `json:"connection_limit,omitempty"`
+	// A reference to a container of TLS secrets.
+	DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"`
+	//  A list of references to TLS secrets.
+	SniContainerRefs []string `json:"sni_container_refs,omitempty"`
+	// The administrative state of the Listener. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToListenerUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "listener")
+}
+
+// Update is an operation which modifies the attributes of the specified Listener.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+	b, err := opts.ToListenerUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will permanently delete a particular Listeners based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go
new file mode 100644
index 0000000..aa8ed1b
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go
@@ -0,0 +1,114 @@
+package listeners
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type LoadBalancerID struct {
+	ID string `json:"id"`
+}
+
+// Listener is the primary load balancing configuration object that specifies
+// the loadbalancer and port on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+type Listener struct {
+	// The unique ID for the Listener.
+	ID string `json:"id"`
+	// Owner of the Listener. Only an admin user can specify a tenant ID other than its own.
+	TenantID string `json:"tenant_id"`
+	// Human-readable name for the Listener. Does not have to be unique.
+	Name string `json:"name"`
+	// Human-readable description for the Listener.
+	Description string `json:"description"`
+	// The protocol to loadbalance. A valid value is TCP, HTTP, or HTTPS.
+	Protocol string `json:"protocol"`
+	// The port on which to listen to client traffic that is associated with the
+	// Loadbalancer. A valid value is from 0 to 65535.
+	ProtocolPort int `json:"protocol_port"`
+	// The UUID of default pool. Must have compatible protocol with listener.
+	DefaultPoolID string `json:"default_pool_id"`
+	// A list of load balancer IDs.
+	Loadbalancers []LoadBalancerID `json:"loadbalancers"`
+	// The maximum number of connections allowed for the Loadbalancer. Default is -1,
+	// meaning no limit.
+	ConnLimit int `json:"connection_limit"`
+	// The list of references to TLS secrets.
+	SniContainerRefs []string `json:"sni_container_refs"`
+	// Optional. A reference to a container of TLS secrets.
+	DefaultTlsContainerRef string `json:"default_tls_container_ref"`
+	// The administrative state of the Listener. A valid value is true (UP) or false (DOWN).
+	AdminStateUp bool         `json:"admin_state_up"`
+	Pools        []pools.Pool `json:"pools"`
+}
+
+// ListenerPage is the page returned by a pager when traversing over a
+// collection of routers.
+type ListenerPage 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 (r ListenerPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"listeners_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (r ListenerPage) IsEmpty() (bool, error) {
+	is, err := ExtractListeners(r)
+	return len(is) == 0, err
+}
+
+// ExtractListeners accepts a Page struct, specifically a ListenerPage struct,
+// and extracts the elements into a slice of Listener structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractListeners(r pagination.Page) ([]Listener, error) {
+	var s struct {
+		Listeners []Listener `json:"listeners"`
+	}
+	err := (r.(ListenerPage)).ExtractInto(&s)
+	return s.Listeners, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Listener, error) {
+	var s struct {
+		Listener *Listener `json:"listener"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Listener, 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_v2/listeners/testing/doc.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/doc.go
new file mode 100644
index 0000000..c74a4de
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_v2_listeners_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/fixtures.go
new file mode 100644
index 0000000..fa4fa25
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/fixtures.go
@@ -0,0 +1,213 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ListenersListBody contains the canned body of a listeners list response.
+const ListenersListBody = `
+{
+	"listeners":[
+		{
+			"id": "db902c0c-d5ff-4753-b465-668ad9656918",
+			"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+			"name": "web",
+			"description": "listener config for the web tier",
+			"loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}],
+			"protocol": "HTTP",
+			"protocol_port": 80,
+			"default_pool_id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+			"admin_state_up": true,
+			"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+			"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+		},
+		{
+			"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+			"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+			"name": "db",
+			"description": "listener config for the db tier",
+			"loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+			"protocol": "TCP",
+			"protocol_port": 3306,
+			"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+			"connection_limit": 2000,
+			"admin_state_up": true,
+			"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+			"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+		}
+	]
+}
+`
+
+// SingleServerBody is the canned body of a Get request on an existing listener.
+const SingleListenerBody = `
+{
+	"listener": {
+		"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+		"name": "db",
+		"description": "listener config for the db tier",
+		"loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+		"protocol": "TCP",
+		"protocol_port": 3306,
+		"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+		"connection_limit": 2000,
+		"admin_state_up": true,
+		"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+		"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+	}
+}
+`
+
+// PostUpdateListenerBody is the canned response body of a Update request on an existing listener.
+const PostUpdateListenerBody = `
+{
+	"listener": {
+		"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+		"name": "NewListenerName",
+		"description": "listener config for the db tier",
+		"loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+		"protocol": "TCP",
+		"protocol_port": 3306,
+		"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+		"connection_limit": 1000,
+		"admin_state_up": true,
+		"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+		"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+	}
+}
+`
+
+var (
+	ListenerWeb = listeners.Listener{
+		ID:                     "db902c0c-d5ff-4753-b465-668ad9656918",
+		TenantID:               "310df60f-2a10-4ee5-9554-98393092194c",
+		Name:                   "web",
+		Description:            "listener config for the web tier",
+		Loadbalancers:          []listeners.LoadBalancerID{{ID: "53306cda-815d-4354-9444-59e09da9c3c5"}},
+		Protocol:               "HTTP",
+		ProtocolPort:           80,
+		DefaultPoolID:          "fad389a3-9a4a-4762-a365-8c7038508b5d",
+		AdminStateUp:           true,
+		DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+		SniContainerRefs:       []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
+	}
+	ListenerDb = listeners.Listener{
+		ID:                     "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		TenantID:               "310df60f-2a10-4ee5-9554-98393092194c",
+		Name:                   "db",
+		Description:            "listener config for the db tier",
+		Loadbalancers:          []listeners.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+		Protocol:               "TCP",
+		ProtocolPort:           3306,
+		DefaultPoolID:          "41efe233-7591-43c5-9cf7-923964759f9e",
+		ConnLimit:              2000,
+		AdminStateUp:           true,
+		DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+		SniContainerRefs:       []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
+	}
+	ListenerUpdated = listeners.Listener{
+		ID:                     "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		TenantID:               "310df60f-2a10-4ee5-9554-98393092194c",
+		Name:                   "NewListenerName",
+		Description:            "listener config for the db tier",
+		Loadbalancers:          []listeners.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+		Protocol:               "TCP",
+		ProtocolPort:           3306,
+		DefaultPoolID:          "41efe233-7591-43c5-9cf7-923964759f9e",
+		ConnLimit:              1000,
+		AdminStateUp:           true,
+		DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+		SniContainerRefs:       []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
+	}
+)
+
+// HandleListenerListSuccessfully sets up the test server to respond to a listener List request.
+func HandleListenerListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/listeners", 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, ListenersListBody)
+		case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+			fmt.Fprintf(w, `{ "listeners": [] }`)
+		default:
+			t.Fatalf("/v2.0/lbaas/listeners invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleListenerCreationSuccessfully sets up the test server to respond to a listener creation request
+// with a given response.
+func HandleListenerCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/v2.0/lbaas/listeners", 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, `{
+			    "listener": {
+			        "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab",
+			        "protocol": "TCP",
+			        "name": "db",
+			        "admin_state_up": true,
+			        "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+			        "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+			        "protocol_port": 3306
+			    }
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleListenerGetSuccessfully sets up the test server to respond to a listener Get request.
+func HandleListenerGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", 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, SingleListenerBody)
+	})
+}
+
+// HandleListenerDeletionSuccessfully sets up the test server to respond to a listener deletion request.
+func HandleListenerDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", 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)
+	})
+}
+
+// HandleListenerUpdateSuccessfully sets up the test server to respond to a listener Update request.
+func HandleListenerUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", 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, `{
+			"listener": {
+				"name": "NewListenerName",
+				"connection_limit": 1001
+			}
+		}`)
+
+		fmt.Fprintf(w, PostUpdateListenerBody)
+	})
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go
new file mode 100644
index 0000000..d463f6e
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go
@@ -0,0 +1,137 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListListeners(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListenerListSuccessfully(t)
+
+	pages := 0
+	err := listeners.List(fake.ServiceClient(), listeners.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := listeners.ExtractListeners(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 listeners, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, ListenerWeb, actual[0])
+		th.CheckDeepEquals(t, ListenerDb, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListAllListeners(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListenerListSuccessfully(t)
+
+	allPages, err := listeners.List(fake.ServiceClient(), listeners.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := listeners.ExtractListeners(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ListenerWeb, actual[0])
+	th.CheckDeepEquals(t, ListenerDb, actual[1])
+}
+
+func TestCreateListener(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListenerCreationSuccessfully(t, SingleListenerBody)
+
+	actual, err := listeners.Create(fake.ServiceClient(), listeners.CreateOpts{
+		Protocol:               "TCP",
+		Name:                   "db",
+		LoadbalancerID:         "79e05663-7f03-45d2-a092-8b94062f22ab",
+		AdminStateUp:           gophercloud.Enabled,
+		DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+		DefaultPoolID:          "41efe233-7591-43c5-9cf7-923964759f9e",
+		ProtocolPort:           3306,
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, ListenerDb, *actual)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := listeners.Create(fake.ServiceClient(), listeners.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar", ProtocolPort: 80})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestGetListener(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListenerGetSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := listeners.Get(client, "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, ListenerDb, *actual)
+}
+
+func TestDeleteListener(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListenerDeletionSuccessfully(t)
+
+	res := listeners.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateListener(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListenerUpdateSuccessfully(t)
+
+	client := fake.ServiceClient()
+	i1001 := 1001
+	actual, err := listeners.Update(client, "4ec89087-d057-4e2c-911f-60a3b47ee304", listeners.UpdateOpts{
+		Name:      "NewListenerName",
+		ConnLimit: &i1001,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, ListenerUpdated, *actual)
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go
new file mode 100644
index 0000000..02fb1eb
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go
@@ -0,0 +1,16 @@
+package listeners
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+	rootPath     = "lbaas"
+	resourcePath = "listeners"
+)
+
+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_v2/loadbalancers/requests.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
new file mode 100644
index 0000000..bc4a3c6
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
@@ -0,0 +1,172 @@
+package loadbalancers
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToLoadBalancerListQuery() (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 Loadbalancer attributes you want to see returned. SortKey allows you to
+// sort by a particular attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Description        string `q:"description"`
+	AdminStateUp       *bool  `q:"admin_state_up"`
+	TenantID           string `q:"tenant_id"`
+	ProvisioningStatus string `q:"provisioning_status"`
+	VipAddress         string `q:"vip_address"`
+	VipPortID          string `q:"vip_port_id"`
+	VipSubnetID        string `q:"vip_subnet_id"`
+	ID                 string `q:"id"`
+	OperatingStatus    string `q:"operating_status"`
+	Name               string `q:"name"`
+	Flavor             string `q:"flavor"`
+	Provider           string `q:"provider"`
+	Limit              int    `q:"limit"`
+	Marker             string `q:"marker"`
+	SortKey            string `q:"sort_key"`
+	SortDir            string `q:"sort_dir"`
+}
+
+// ToLoadbalancerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToLoadBalancerListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// 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 ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+	if opts != nil {
+		query, err := opts.ToLoadBalancerListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return LoadBalancerPage{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 {
+	ToLoadBalancerCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// Optional. Human-readable name for the Loadbalancer. Does not have to be unique.
+	Name string `json:"name,omitempty"`
+	// Optional. Human-readable description for the Loadbalancer.
+	Description string `json:"description,omitempty"`
+	// Required. The network on which to allocate the Loadbalancer's address. A tenant can
+	// only create Loadbalancers on networks authorized by policy (e.g. networks that
+	// belong to them or networks that are shared).
+	VipSubnetID string `json:"vip_subnet_id" required:"true"`
+	// Required for admins. The UUID of the tenant who owns the Loadbalancer.
+	// Only administrative users can specify a tenant UUID other than their own.
+	TenantID string `json:"tenant_id,omitempty"`
+	// Optional. The IP address of the Loadbalancer.
+	VipAddress string `json:"vip_address,omitempty"`
+	// Optional. The administrative state of the Loadbalancer. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+	// Optional. The UUID of a flavor.
+	Flavor string `json:"flavor,omitempty"`
+	// Optional. The name of the provider.
+	Provider string `json:"provider,omitempty"`
+}
+
+// ToLoadBalancerCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "loadbalancer")
+}
+
+// Create is an operation which provisions a new loadbalancer 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.
+//
+// Users with an admin role can create loadbalancers on behalf of other tenants by
+// specifying a TenantID attribute different than their own.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToLoadBalancerCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular Loadbalancer based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	ToLoadBalancerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	// Optional. Human-readable name for the Loadbalancer. Does not have to be unique.
+	Name string `json:"name,omitempty"`
+	// Optional. Human-readable description for the Loadbalancer.
+	Description string `json:"description,omitempty"`
+	// Optional. The administrative state of the Loadbalancer. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToLoadBalancerUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "loadbalancer")
+}
+
+// Update is an operation which modifies the attributes of the specified LoadBalancer.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+	b, err := opts.ToLoadBalancerUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will permanently delete a particular LoadBalancer based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
+
+func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) {
+	_, r.Err = c.Get(statusRootURL(c, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
new file mode 100644
index 0000000..4423c24
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
@@ -0,0 +1,125 @@
+package loadbalancers
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// LoadBalancer is the primary load balancing configuration object that specifies
+// the virtual IP address on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+type LoadBalancer struct {
+	// Human-readable description for the Loadbalancer.
+	Description string `json:"description"`
+	// The administrative state of the Loadbalancer. A valid value is true (UP) or false (DOWN).
+	AdminStateUp bool `json:"admin_state_up"`
+	// Owner of the LoadBalancer. Only an admin user can specify a tenant ID other than its own.
+	TenantID string `json:"tenant_id"`
+	// The provisioning status of the LoadBalancer. This value is ACTIVE, PENDING_CREATE or ERROR.
+	ProvisioningStatus string `json:"provisioning_status"`
+	// The IP address of the Loadbalancer.
+	VipAddress string `json:"vip_address"`
+	// The UUID of the port associated with the IP address.
+	VipPortID string `json:"vip_port_id"`
+	// The UUID of the subnet on which to allocate the virtual IP for the Loadbalancer address.
+	VipSubnetID string `json:"vip_subnet_id"`
+	// The unique ID for the LoadBalancer.
+	ID string `json:"id"`
+	// The operating status of the LoadBalancer. This value is ONLINE or OFFLINE.
+	OperatingStatus string `json:"operating_status"`
+	// Human-readable name for the LoadBalancer. Does not have to be unique.
+	Name string `json:"name"`
+	// The UUID of a flavor if set.
+	Flavor string `json:"flavor"`
+	// The name of the provider.
+	Provider  string               `json:"provider"`
+	Listeners []listeners.Listener `json:"listeners"`
+}
+
+type StatusTree struct {
+	Loadbalancer *LoadBalancer `json:"loadbalancer"`
+}
+
+// LoadBalancerPage is the page returned by a pager when traversing over a
+// collection of routers.
+type LoadBalancerPage 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 (r LoadBalancerPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"loadbalancers_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a LoadBalancerPage struct is empty.
+func (p LoadBalancerPage) IsEmpty() (bool, error) {
+	is, err := ExtractLoadBalancers(p)
+	return len(is) == 0, err
+}
+
+// ExtractLoadBalancers accepts a Page struct, specifically a LoadbalancerPage struct,
+// and extracts the elements into a slice of LoadBalancer structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) {
+	var s struct {
+		LoadBalancers []LoadBalancer `json:"loadbalancers"`
+	}
+	err := (r.(LoadBalancerPage)).ExtractInto(&s)
+	return s.LoadBalancers, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*LoadBalancer, error) {
+	var s struct {
+		LoadBalancer *LoadBalancer `json:"loadbalancer"`
+	}
+	err := r.ExtractInto(&s)
+	return s.LoadBalancer, err
+}
+
+type GetStatusesResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a Loadbalancer.
+func (r GetStatusesResult) Extract() (*StatusTree, error) {
+	var s struct {
+		Statuses *StatusTree `json:"statuses"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Statuses, 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_v2/loadbalancers/testing/doc.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/doc.go
new file mode 100644
index 0000000..b06352e
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_v2_loadbalancers_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go
new file mode 100644
index 0000000..a452236
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go
@@ -0,0 +1,284 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+)
+
+// LoadbalancersListBody contains the canned body of a loadbalancer list response.
+const LoadbalancersListBody = `
+{
+	"loadbalancers":[
+	         {
+			"id": "c331058c-6a40-4144-948e-b9fb1df9db4b",
+			"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+			"name": "web_lb",
+			"description": "lb config for the web tier",
+			"vip_subnet_id": "8a49c438-848f-467b-9655-ea1548708154",
+			"vip_address": "10.30.176.47",
+			"vip_port_id": "2a22e552-a347-44fd-b530-1f2b1b2a6735",
+			"flavor": "small",
+			"provider": "haproxy",
+			"admin_state_up": true,
+			"provisioning_status": "ACTIVE",
+			"operating_status": "ONLINE"
+		},
+		{
+			"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+			"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+			"name": "db_lb",
+			"description": "lb config for the db tier",
+			"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+			"vip_address": "10.30.176.48",
+			"vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+			"flavor": "medium",
+			"provider": "haproxy",
+			"admin_state_up": true,
+			"provisioning_status": "PENDING_CREATE",
+			"operating_status": "OFFLINE"
+		}
+	]
+}
+`
+
+// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer.
+const SingleLoadbalancerBody = `
+{
+	"loadbalancer": {
+		"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+		"name": "db_lb",
+		"description": "lb config for the db tier",
+		"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+		"vip_address": "10.30.176.48",
+		"vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+		"flavor": "medium",
+		"provider": "haproxy",
+		"admin_state_up": true,
+		"provisioning_status": "PENDING_CREATE",
+		"operating_status": "OFFLINE"
+	}
+}
+`
+
+// PostUpdateLoadbalancerBody is the canned response body of a Update request on an existing loadbalancer.
+const PostUpdateLoadbalancerBody = `
+{
+	"loadbalancer": {
+		"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+		"name": "NewLoadbalancerName",
+		"description": "lb config for the db tier",
+		"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+		"vip_address": "10.30.176.48",
+		"vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+		"flavor": "medium",
+		"provider": "haproxy",
+		"admin_state_up": true,
+		"provisioning_status": "PENDING_CREATE",
+		"operating_status": "OFFLINE"
+	}
+}
+`
+
+// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer.
+const LoadbalancerStatuesesTree = `
+{
+	"statuses" : {
+		"loadbalancer": {
+			"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+			"name": "db_lb",
+			"provisioning_status": "PENDING_UPDATE",
+			"operating_status": "ACTIVE",
+			"listeners": [{
+				"id": "db902c0c-d5ff-4753-b465-668ad9656918",
+				"name": "db",
+				"pools": [{
+					"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+					"name": "db",
+					"healthmonitor": {
+						"id": "67306cda-815d-4354-9fe4-59e09da9c3c5",
+						"type":"PING"
+					},
+					"members":[{
+						"id": "2a280670-c202-4b0b-a562-34077415aabf",
+						"name": "db",
+						"address": "10.0.2.11",
+						"protocol_port": 80
+					}]
+				}]
+			}]
+		}
+	}
+}
+`
+
+var (
+	LoadbalancerWeb = loadbalancers.LoadBalancer{
+		ID:                 "c331058c-6a40-4144-948e-b9fb1df9db4b",
+		TenantID:           "54030507-44f7-473c-9342-b4d14a95f692",
+		Name:               "web_lb",
+		Description:        "lb config for the web tier",
+		VipSubnetID:        "8a49c438-848f-467b-9655-ea1548708154",
+		VipAddress:         "10.30.176.47",
+		VipPortID:          "2a22e552-a347-44fd-b530-1f2b1b2a6735",
+		Flavor:             "small",
+		Provider:           "haproxy",
+		AdminStateUp:       true,
+		ProvisioningStatus: "ACTIVE",
+		OperatingStatus:    "ONLINE",
+	}
+	LoadbalancerDb = loadbalancers.LoadBalancer{
+		ID:                 "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		TenantID:           "54030507-44f7-473c-9342-b4d14a95f692",
+		Name:               "db_lb",
+		Description:        "lb config for the db tier",
+		VipSubnetID:        "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+		VipAddress:         "10.30.176.48",
+		VipPortID:          "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+		Flavor:             "medium",
+		Provider:           "haproxy",
+		AdminStateUp:       true,
+		ProvisioningStatus: "PENDING_CREATE",
+		OperatingStatus:    "OFFLINE",
+	}
+	LoadbalancerUpdated = loadbalancers.LoadBalancer{
+		ID:                 "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		TenantID:           "54030507-44f7-473c-9342-b4d14a95f692",
+		Name:               "NewLoadbalancerName",
+		Description:        "lb config for the db tier",
+		VipSubnetID:        "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+		VipAddress:         "10.30.176.48",
+		VipPortID:          "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+		Flavor:             "medium",
+		Provider:           "haproxy",
+		AdminStateUp:       true,
+		ProvisioningStatus: "PENDING_CREATE",
+		OperatingStatus:    "OFFLINE",
+	}
+	LoadbalancerStatusesTree = loadbalancers.LoadBalancer{
+		ID:                 "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+		Name:               "db_lb",
+		ProvisioningStatus: "PENDING_UPDATE",
+		OperatingStatus:    "ACTIVE",
+		Listeners: []listeners.Listener{{
+			ID:   "db902c0c-d5ff-4753-b465-668ad9656918",
+			Name: "db",
+			Pools: []pools.Pool{{
+				ID:   "fad389a3-9a4a-4762-a365-8c7038508b5d",
+				Name: "db",
+				Monitor: monitors.Monitor{
+					ID:   "67306cda-815d-4354-9fe4-59e09da9c3c5",
+					Type: "PING",
+				},
+				Members: []pools.Member{{
+					ID:           "2a280670-c202-4b0b-a562-34077415aabf",
+					Name:         "db",
+					Address:      "10.0.2.11",
+					ProtocolPort: 80,
+				}},
+			}},
+		}},
+	}
+)
+
+// HandleLoadbalancerListSuccessfully sets up the test server to respond to a loadbalancer List request.
+func HandleLoadbalancerListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", 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, LoadbalancersListBody)
+		case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+			fmt.Fprintf(w, `{ "loadbalancers": [] }`)
+		default:
+			t.Fatalf("/v2.0/lbaas/loadbalancers invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleLoadbalancerCreationSuccessfully sets up the test server to respond to a loadbalancer creation request
+// with a given response.
+func HandleLoadbalancerCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", 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, `{
+			"loadbalancer": {
+				"name": "db_lb",
+				"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+				"vip_address": "10.30.176.48",
+				"flavor": "medium",
+				"provider": "haproxy",
+				"admin_state_up": true
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleLoadbalancerGetSuccessfully sets up the test server to respond to a loadbalancer Get request.
+func HandleLoadbalancerGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", 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, SingleLoadbalancerBody)
+	})
+}
+
+// HandleLoadbalancerGetStatusesTree sets up the test server to respond to a loadbalancer Get statuses tree request.
+func HandleLoadbalancerGetStatusesTree(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab/statuses", 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, LoadbalancerStatuesesTree)
+	})
+}
+
+// HandleLoadbalancerDeletionSuccessfully sets up the test server to respond to a loadbalancer deletion request.
+func HandleLoadbalancerDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", 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)
+	})
+}
+
+// HandleLoadbalancerUpdateSuccessfully sets up the test server to respond to a loadbalancer Update request.
+func HandleLoadbalancerUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", 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, `{
+			"loadbalancer": {
+				"name": "NewLoadbalancerName"
+			}
+		}`)
+
+		fmt.Fprintf(w, PostUpdateLoadbalancerBody)
+	})
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go
new file mode 100644
index 0000000..270bdf5
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go
@@ -0,0 +1,144 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListLoadbalancers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerListSuccessfully(t)
+
+	pages := 0
+	err := loadbalancers.List(fake.ServiceClient(), loadbalancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := loadbalancers.ExtractLoadBalancers(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 loadbalancers, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, LoadbalancerWeb, actual[0])
+		th.CheckDeepEquals(t, LoadbalancerDb, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListAllLoadbalancers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerListSuccessfully(t)
+
+	allPages, err := loadbalancers.List(fake.ServiceClient(), loadbalancers.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := loadbalancers.ExtractLoadBalancers(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, LoadbalancerWeb, actual[0])
+	th.CheckDeepEquals(t, LoadbalancerDb, actual[1])
+}
+
+func TestCreateLoadbalancer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerCreationSuccessfully(t, SingleLoadbalancerBody)
+
+	actual, err := loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{
+		Name:         "db_lb",
+		AdminStateUp: gophercloud.Enabled,
+		VipSubnetID:  "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+		VipAddress:   "10.30.176.48",
+		Flavor:       "medium",
+		Provider:     "haproxy",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, LoadbalancerDb, *actual)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo", Description: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo", Description: "bar", VipAddress: "bar"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestGetLoadbalancer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerGetSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := loadbalancers.Get(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, LoadbalancerDb, *actual)
+}
+
+func TestGetLoadbalancerStatusesTree(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerGetStatusesTree(t)
+
+	client := fake.ServiceClient()
+	actual, err := loadbalancers.GetStatuses(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, LoadbalancerStatusesTree, *(actual.Loadbalancer))
+}
+
+func TestDeleteLoadbalancer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerDeletionSuccessfully(t)
+
+	res := loadbalancers.Delete(fake.ServiceClient(), "36e08a3e-a78f-4b40-a229-1e7e23eee1ab")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateLoadbalancer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleLoadbalancerUpdateSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := loadbalancers.Update(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", loadbalancers.UpdateOpts{
+		Name: "NewLoadbalancerName",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, LoadbalancerUpdated, *actual)
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go
new file mode 100644
index 0000000..73cf5dc
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go
@@ -0,0 +1,21 @@
+package loadbalancers
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+	rootPath     = "lbaas"
+	resourcePath = "loadbalancers"
+	statusPath   = "statuses"
+)
+
+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 statusRootURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id, statusPath)
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
new file mode 100644
index 0000000..1e776bf
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
@@ -0,0 +1,233 @@
+package monitors
+
+import (
+	"fmt"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToMonitorListQuery() (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 Monitor attributes you want to see returned. SortKey allows you to
+// sort by a particular Monitor 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"`
+	PoolID        string `q:"pool_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"`
+}
+
+// ToMonitorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToMonitorListQuery() (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
+// health monitors. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those health monitors 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.ToMonitorListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(c, url, 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 (
+	errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout")
+)
+
+// 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 {
+	ToMonitorCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// Required. The Pool to Monitor.
+	PoolID string `json:"pool_id" required:"true"`
+	// 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 `json:"type" required:"true"`
+	// Required. The time, in seconds, between sending probes to members.
+	Delay int `json:"delay" required:"true"`
+	// 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 `json:"timeout" required:"true"`
+	// Required. Number of permissible ping failures before changing the member's
+	// status to INACTIVE. Must be a number between 1 and 10.
+	MaxRetries int `json:"max_retries" required:"true"`
+	// Required for HTTP(S) types. URI path that will be accessed if Monitor type
+	// is HTTP or HTTPS.
+	URLPath string `json:"url_path,omitempty"`
+	// 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 `json:"http_method,omitempty"`
+	// 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 `json:"expected_codes,omitempty"`
+	// Indicates the owner of the Loadbalancer. Required for admins.
+	TenantID string `json:"tenant_id,omitempty"`
+	// Optional. The Name of the Monitor.
+	Name         string `json:"name,omitempty"`
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+}
+
+// ToMonitorCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "healthmonitor")
+	if err != nil {
+		return nil, err
+	}
+
+	switch opts.Type {
+	case TypeHTTP, TypeHTTPS:
+		switch opts.URLPath {
+		case "":
+			return nil, fmt.Errorf("URLPath must be provided for HTTP and HTTPS")
+		}
+		switch opts.ExpectedCodes {
+		case "":
+			return nil, fmt.Errorf("ExpectedCodes must be provided for HTTP and HTTPS")
+		}
+	}
+
+	return b, nil
+}
+
+/*
+ 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", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"}
+*/
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToMonitorCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular Health Monitor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	ToMonitorUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	// Required. The time, in seconds, between sending probes to members.
+	Delay int `json:"delay,omitempty"`
+	// 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 `json:"timeout,omitempty"`
+	// Required. Number of permissible ping failures before changing the member's
+	// status to INACTIVE. Must be a number between 1 and 10.
+	MaxRetries int `json:"max_retries,omitempty"`
+	// Required for HTTP(S) types. URI path that will be accessed if Monitor type
+	// is HTTP or HTTPS.
+	URLPath string `json:"url_path,omitempty"`
+	// 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 `json:"http_method,omitempty"`
+	// 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 `json:"expected_codes,omitempty"`
+	// Optional. The Name of the Monitor.
+	Name         string `json:"name,omitempty"`
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+}
+
+// ToMonitorUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "healthmonitor")
+}
+
+// Update is an operation which modifies the attributes of the specified Monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToMonitorUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will permanently delete a particular Monitor based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go
new file mode 100644
index 0000000..05dcf47
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go
@@ -0,0 +1,144 @@
+package monitors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type PoolID struct {
+	ID string `json:"id"`
+}
+
+// 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 Monitor.
+	ID string `json:"id"`
+
+	// The Name of the Monitor.
+	Name string `json:"name"`
+
+	// Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"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 `json:"type"`
+
+	// The time, in seconds, between sending probes to members.
+	Delay int `json:"delay"`
+
+	// 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 `json:"timeout"`
+
+	// 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"`
+
+	// The HTTP method that the monitor uses for requests.
+	HTTPMethod string `json:"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" `
+
+	// Expected HTTP codes for a passing HTTP(S) monitor.
+	ExpectedCodes string `json:"expected_codes"`
+
+	// The administrative state of the health monitor, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up"`
+
+	// The status of the health monitor. Indicates whether the health monitor is
+	// operational.
+	Status string `json:"status"`
+
+	// List of pools that are associated with the health monitor.
+	Pools []PoolID `json:"pools"`
+}
+
+// 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 (r MonitorPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"healthmonitors_links"`
+	}
+
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a MonitorPage struct is empty.
+func (r MonitorPage) IsEmpty() (bool, error) {
+	is, err := ExtractMonitors(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Monitor, error) {
+	var s struct {
+		Monitors []Monitor `json:"healthmonitors"`
+	}
+	err := (r.(MonitorPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Monitor *Monitor `json:"healthmonitor"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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_v2/monitors/testing/doc.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/doc.go
new file mode 100644
index 0000000..443f9ad
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_v2_monitors_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/fixtures.go
new file mode 100644
index 0000000..6d3eb01
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/fixtures.go
@@ -0,0 +1,215 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// HealthmonitorsListBody contains the canned body of a healthmonitor list response.
+const HealthmonitorsListBody = `
+{
+	"healthmonitors":[
+		{
+			"admin_state_up":true,
+			"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+			"delay":10,
+			"name":"web",
+			"max_retries":1,
+			"timeout":1,
+			"type":"PING",
+			"pools": [{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}],
+			"id":"466c8345-28d8-4f84-a246-e04380b0461d"
+		},
+		{
+			"admin_state_up":true,
+			"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+			"delay":5,
+			"name":"db",
+			"expected_codes":"200",
+			"max_retries":2,
+			"http_method":"GET",
+			"timeout":2,
+			"url_path":"/",
+			"type":"HTTP",
+			"pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
+			"id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+		}
+	]
+}
+`
+
+// SingleHealthmonitorBody is the canned body of a Get request on an existing healthmonitor.
+const SingleHealthmonitorBody = `
+{
+	"healthmonitor": {
+		"admin_state_up":true,
+		"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+		"delay":5,
+		"name":"db",
+		"expected_codes":"200",
+		"max_retries":2,
+		"http_method":"GET",
+		"timeout":2,
+		"url_path":"/",
+		"type":"HTTP",
+		"pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
+		"id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+	}
+}
+`
+
+// PostUpdateHealthmonitorBody is the canned response body of a Update request on an existing healthmonitor.
+const PostUpdateHealthmonitorBody = `
+{
+	"healthmonitor": {
+		"admin_state_up":true,
+		"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+		"delay":3,
+		"name":"NewHealthmonitorName",
+		"expected_codes":"301",
+		"max_retries":10,
+		"http_method":"GET",
+		"timeout":20,
+		"url_path":"/another_check",
+		"type":"HTTP",
+		"pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
+		"id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+	}
+}
+`
+
+var (
+	HealthmonitorWeb = monitors.Monitor{
+		AdminStateUp: true,
+		Name:         "web",
+		TenantID:     "83657cfcdfe44cd5920adaf26c48ceea",
+		Delay:        10,
+		MaxRetries:   1,
+		Timeout:      1,
+		Type:         "PING",
+		ID:           "466c8345-28d8-4f84-a246-e04380b0461d",
+		Pools:        []monitors.PoolID{{ID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}},
+	}
+	HealthmonitorDb = monitors.Monitor{
+		AdminStateUp:  true,
+		Name:          "db",
+		TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+		Delay:         5,
+		ExpectedCodes: "200",
+		MaxRetries:    2,
+		Timeout:       2,
+		URLPath:       "/",
+		Type:          "HTTP",
+		HTTPMethod:    "GET",
+		ID:            "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+		Pools:         []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
+	}
+	HealthmonitorUpdated = monitors.Monitor{
+		AdminStateUp:  true,
+		Name:          "NewHealthmonitorName",
+		TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+		Delay:         3,
+		ExpectedCodes: "301",
+		MaxRetries:    10,
+		Timeout:       20,
+		URLPath:       "/another_check",
+		Type:          "HTTP",
+		HTTPMethod:    "GET",
+		ID:            "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+		Pools:         []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
+	}
+)
+
+// HandleHealthmonitorListSuccessfully sets up the test server to respond to a healthmonitor List request.
+func HandleHealthmonitorListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", 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, HealthmonitorsListBody)
+		case "556c8345-28d8-4f84-a246-e04380b0461d":
+			fmt.Fprintf(w, `{ "healthmonitors": [] }`)
+		default:
+			t.Fatalf("/v2.0/lbaas/healthmonitors invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleHealthmonitorCreationSuccessfully sets up the test server to respond to a healthmonitor creation request
+// with a given response.
+func HandleHealthmonitorCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", 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, `{
+			"healthmonitor": {
+				"type":"HTTP",
+				"pool_id":"84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
+				"tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+				"delay":20,
+				"name":"db",
+				"timeout":10,
+				"max_retries":5,
+				"url_path":"/check",
+				"expected_codes":"200-299"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleHealthmonitorGetSuccessfully sets up the test server to respond to a healthmonitor Get request.
+func HandleHealthmonitorGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", 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, SingleHealthmonitorBody)
+	})
+}
+
+// HandleHealthmonitorDeletionSuccessfully sets up the test server to respond to a healthmonitor deletion request.
+func HandleHealthmonitorDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", 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)
+	})
+}
+
+// HandleHealthmonitorUpdateSuccessfully sets up the test server to respond to a healthmonitor Update request.
+func HandleHealthmonitorUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", 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, `{
+			"healthmonitor": {
+				"name": "NewHealthmonitorName",
+				"delay": 3,
+				"timeout": 20,
+				"max_retries": 10,
+				"url_path": "/another_check",
+				"expected_codes": "301"
+			}
+		}`)
+
+		fmt.Fprintf(w, PostUpdateHealthmonitorBody)
+	})
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/requests_test.go
new file mode 100644
index 0000000..743d9c1
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/requests_test.go
@@ -0,0 +1,154 @@
+package testing
+
+import (
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListHealthmonitors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleHealthmonitorListSuccessfully(t)
+
+	pages := 0
+	err := monitors.List(fake.ServiceClient(), monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := monitors.ExtractMonitors(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 healthmonitors, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, HealthmonitorWeb, actual[0])
+		th.CheckDeepEquals(t, HealthmonitorDb, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListAllHealthmonitors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleHealthmonitorListSuccessfully(t)
+
+	allPages, err := monitors.List(fake.ServiceClient(), monitors.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := monitors.ExtractMonitors(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, HealthmonitorWeb, actual[0])
+	th.CheckDeepEquals(t, HealthmonitorDb, actual[1])
+}
+
+func TestCreateHealthmonitor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleHealthmonitorCreationSuccessfully(t, SingleHealthmonitorBody)
+
+	actual, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{
+		Type:          "HTTP",
+		Name:          "db",
+		PoolID:        "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
+		TenantID:      "453105b9-1754-413f-aab1-55f1af620750",
+		Delay:         20,
+		Timeout:       10,
+		MaxRetries:    5,
+		URLPath:       "/check",
+		ExpectedCodes: "200-299",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, HealthmonitorDb, *actual)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = monitors.Create(fake.ServiceClient(), monitors.CreateOpts{Type: monitors.TypeHTTP})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestGetHealthmonitor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleHealthmonitorGetSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := monitors.Get(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, HealthmonitorDb, *actual)
+}
+
+func TestDeleteHealthmonitor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleHealthmonitorDeletionSuccessfully(t)
+
+	res := monitors.Delete(fake.ServiceClient(), "5d4b5228-33b0-4e60-b225-9b727c1a20e7")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateHealthmonitor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleHealthmonitorUpdateSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := monitors.Update(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7", monitors.UpdateOpts{
+		Name:          "NewHealthmonitorName",
+		Delay:         3,
+		Timeout:       20,
+		MaxRetries:    10,
+		URLPath:       "/another_check",
+		ExpectedCodes: "301",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, HealthmonitorUpdated, *actual)
+}
+
+func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) {
+	_, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{
+		Type:          "HTTP",
+		PoolID:        "d459f7d8-c6ee-439d-8713-d3fc08aeed8d",
+		Delay:         1,
+		Timeout:       10,
+		MaxRetries:    5,
+		URLPath:       "/check",
+		ExpectedCodes: "200-299",
+	}).Extract()
+
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	_, err = monitors.Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", monitors.UpdateOpts{
+		Delay:   1,
+		Timeout: 10,
+	}).Extract()
+
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go
new file mode 100644
index 0000000..a222e52
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+	rootPath     = "lbaas"
+	resourcePath = "healthmonitors"
+)
+
+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_v2/pools/requests.go b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
new file mode 100644
index 0000000..093df6a
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
@@ -0,0 +1,334 @@
+package pools
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToPoolListQuery() (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 Pool attributes you want to see returned. SortKey allows you to
+// sort by a particular Pool attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	LBMethod       string `q:"lb_algorithm"`
+	Protocol       string `q:"protocol"`
+	TenantID       string `q:"tenant_id"`
+	AdminStateUp   *bool  `q:"admin_state_up"`
+	Name           string `q:"name"`
+	ID             string `q:"id"`
+	LoadbalancerID string `q:"loadbalancer_id"`
+	ListenerID     string `q:"listener_id"`
+	Limit          int    `q:"limit"`
+	Marker         string `q:"marker"`
+	SortKey        string `q:"sort_key"`
+	SortDir        string `q:"sort_dir"`
+}
+
+// ToPoolListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPoolListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// 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 ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+	if opts != nil {
+		query, err := opts.ToPoolListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return PoolPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+type LBMethod string
+type Protocol string
+
+// Supported attributes for create/update operations.
+const (
+	LBMethodRoundRobin       LBMethod = "ROUND_ROBIN"
+	LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS"
+	LBMethodSourceIp         LBMethod = "SOURCE_IP"
+
+	ProtocolTCP   Protocol = "TCP"
+	ProtocolHTTP  Protocol = "HTTP"
+	ProtocolHTTPS Protocol = "HTTPS"
+)
+
+// 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 {
+	ToPoolCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// The algorithm used to distribute load between the members of the pool. The
+	// current specification supports LBMethodRoundRobin, LBMethodLeastConnections
+	// and LBMethodSourceIp as valid values for this attribute.
+	LBMethod LBMethod `json:"lb_algorithm" required:"true"`
+	// The protocol used by the pool members, you can use either
+	// ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+	Protocol Protocol `json:"protocol" required:"true"`
+	// The Loadbalancer on which the members of the pool will be associated with.
+	// Note:  one of LoadbalancerID or ListenerID must be provided.
+	LoadbalancerID string `json:"loadbalancer_id,omitempty" xor:"ListenerID"`
+	// The Listener on which the members of the pool will be associated with.
+	// Note:  one of LoadbalancerID or ListenerID must be provided.
+	ListenerID string `json:"listener_id,omitempty" xor:"LoadbalancerID"`
+	// Only required if the caller has an admin role and wants to create a pool
+	// for another tenant.
+	TenantID string `json:"tenant_id,omitempty"`
+	// Name of the pool.
+	Name string `json:"name,omitempty"`
+	// Human-readable description for the pool.
+	Description string `json:"description,omitempty"`
+	// Omit this field to prevent session persistence.
+	Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+	// The administrative state of the Pool. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToPoolCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "pool")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToPoolCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular pool based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	ToPoolUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	// Name of the pool.
+	Name string `json:"name,omitempty"`
+	// Human-readable description for the pool.
+	Description string `json:"description,omitempty"`
+	// The algorithm used to distribute load between the members of the pool. The
+	// current specification supports LBMethodRoundRobin, LBMethodLeastConnections
+	// and LBMethodSourceIp as valid values for this attribute.
+	LBMethod LBMethod `json:"lb_algorithm,omitempty"`
+	// The administrative state of the Pool. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToPoolUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "pool")
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToPoolUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will permanently delete a particular pool based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
+
+// ListMemberOptsBuilder allows extensions to add additional parameters to the
+// ListMembers request.
+type ListMembersOptsBuilder interface {
+	ToMembersListQuery() (string, error)
+}
+
+// ListMembersOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the Member attributes you want to see returned. SortKey allows you to
+// sort by a particular Member attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListMembersOpts struct {
+	Name         string `q:"name"`
+	Weight       int    `q:"weight"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	TenantID     string `q:"tenant_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"`
+}
+
+// ToMemberListQuery formats a ListOpts into a query string.
+func (opts ListMembersOpts) ToMembersListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// ListMembers returns a Pager which allows you to iterate over a collection of
+// members. It accepts a ListMembersOptsBuilder, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those members that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func ListMembers(c *gophercloud.ServiceClient, poolID string, opts ListMembersOptsBuilder) pagination.Pager {
+	url := memberRootURL(c, poolID)
+	if opts != nil {
+		query, err := opts.ToMembersListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return MemberPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateMemberOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the CreateMember 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 CreateMemberOptsBuilder interface {
+	ToMemberCreateMap() (map[string]interface{}, error)
+}
+
+// CreateMemberOpts is the common options struct used in this package's CreateMember
+// operation.
+type CreateMemberOpts struct {
+	// Required. The IP address of the member to receive traffic from the load balancer.
+	Address string `json:"address" required:"true"`
+	// Required. The port on which to listen for client traffic.
+	ProtocolPort int `json:"protocol_port" required:"true"`
+	// Optional. Name of the Member.
+	Name string `json:"name,omitempty"`
+	// Only required if the caller has an admin role and wants to create a Member
+	// for another tenant.
+	TenantID string `json:"tenant_id,omitempty"`
+	// Optional. A positive integer value that indicates the relative portion of
+	// traffic that this member should receive from the pool. For example, a
+	// member with a weight of 10 receives five times as much traffic as a member
+	// with a weight of 2.
+	Weight int `json:"weight,omitempty"`
+	// Optional.  If you omit this parameter, LBaaS uses the vip_subnet_id
+	// parameter value for the subnet UUID.
+	SubnetID string `json:"subnet_id,omitempty"`
+	// Optional. The administrative state of the Pool. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToMemberCreateMap casts a CreateOpts struct to a map.
+func (opts CreateMemberOpts) ToMemberCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "member")
+}
+
+// CreateMember will create and associate a Member with a particular Pool.
+func CreateMember(c *gophercloud.ServiceClient, poolID string, opts CreateMemberOpts) (r CreateMemberResult) {
+	b, err := opts.ToMemberCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(memberRootURL(c, poolID), b, &r.Body, nil)
+	return
+}
+
+// GetMember retrieves a particular Pool Member based on its unique ID.
+func GetMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r GetMemberResult) {
+	_, r.Err = c.Get(memberResourceURL(c, poolID, memberID), &r.Body, nil)
+	return
+}
+
+// MemberUpdateOptsBuilder 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 UpdateMemberOptsBuilder interface {
+	ToMemberUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateMemberOpts is the common options struct used in this package's Update
+// operation.
+type UpdateMemberOpts struct {
+	// Optional. Name of the Member.
+	Name string `json:"name,omitempty"`
+	// Optional. A positive integer value that indicates the relative portion of
+	// traffic that this member should receive from the pool. For example, a
+	// member with a weight of 10 receives five times as much traffic as a member
+	// with a weight of 2.
+	Weight int `json:"weight,omitempty"`
+	// Optional. The administrative state of the Pool. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToMemberUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateMemberOpts) ToMemberUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "member")
+}
+
+// Update allows Member to be updated.
+func UpdateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts UpdateMemberOptsBuilder) (r UpdateMemberResult) {
+	b, err := opts.ToMemberUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(memberResourceURL(c, poolID, memberID), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	return
+}
+
+// DisassociateMember will remove and disassociate a Member from a particular Pool.
+func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r DeleteMemberResult) {
+	_, r.Err = c.Delete(memberResourceURL(c, poolID, memberID), nil)
+	return
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/results.go b/openstack/networking/v2/extensions/lbaas_v2/pools/results.go
new file mode 100644
index 0000000..0e0bf36
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/results.go
@@ -0,0 +1,242 @@
+package pools
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+	"github.com/gophercloud/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 `json:"type"`
+
+	// Name of cookie if persistence mode is set appropriately
+	CookieName string `json:"cookie_name,omitempty"`
+}
+
+type LoadBalancerID struct {
+	ID string `json:"id"`
+}
+
+type ListenerID struct {
+	ID string `json:"id"`
+}
+
+// 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.
+type Pool struct {
+	// 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_algorithm"`
+	// The protocol of the Pool, which is TCP, HTTP, or HTTPS.
+	Protocol string `json:"protocol"`
+	// Description for the Pool.
+	Description string `json:"description"`
+	// A list of listeners objects IDs.
+	Listeners []ListenerID `json:"listeners"` //[]map[string]interface{}
+	// A list of member objects IDs.
+	Members []Member `json:"members"`
+	// The ID of associated health monitor.
+	MonitorID string `json:"healthmonitor_id"`
+	// 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"`
+	// Owner of the Pool. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id"`
+	// The administrative state of the Pool, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up"`
+	// Pool name. Does not have to be unique.
+	Name string `json:"name"`
+	// The unique ID for the Pool.
+	ID string `json:"id"`
+	// A list of load balancer objects IDs.
+	Loadbalancers []LoadBalancerID `json:"loadbalancers"`
+	// Indicates whether connections in the same session will be processed by the
+	// same Pool member or not.
+	Persistence SessionPersistence `json:"session_persistence"`
+	// The provider
+	Provider string           `json:"provider"`
+	Monitor  monitors.Monitor `json:"healthmonitor"`
+}
+
+// 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 (r PoolPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"pools_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (r PoolPage) IsEmpty() (bool, error) {
+	is, err := ExtractPools(r)
+	return len(is) == 0, err
+}
+
+// ExtractPools accepts a Page struct, specifically a PoolPage 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(r pagination.Page) ([]Pool, error) {
+	var s struct {
+		Pools []Pool `json:"pools"`
+	}
+	err := (r.(PoolPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		Pool *Pool `json:"pool"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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
+}
+
+// Member represents the application running on a backend server.
+type Member struct {
+	// Name of the Member.
+	Name string `json:"name"`
+	// Weight of Member.
+	Weight int `json:"weight"`
+	// The administrative state of the member, which is up (true) or down (false).
+	AdminStateUp bool `json:"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"`
+	// parameter value for the subnet UUID.
+	SubnetID string `json:"subnet_id"`
+	// The Pool to which the Member belongs.
+	PoolID string `json:"pool_id"`
+	// The IP address of the Member.
+	Address string `json:"address"`
+	// The port on which the application is hosted.
+	ProtocolPort int `json:"protocol_port"`
+	// The unique ID for the Member.
+	ID string `json:"id"`
+}
+
+// MemberPage is the page returned by a pager when traversing over a
+// collection of Members in a Pool.
+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 (r MemberPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"members_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a MemberPage struct is empty.
+func (r MemberPage) IsEmpty() (bool, error) {
+	is, err := ExtractMembers(r)
+	return len(is) == 0, err
+}
+
+// ExtractMembers 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 ExtractMembers(r pagination.Page) ([]Member, error) {
+	var s struct {
+		Members []Member `json:"members"`
+	}
+	err := (r.(MemberPage)).ExtractInto(&s)
+	return s.Members, err
+}
+
+type commonMemberResult struct {
+	gophercloud.Result
+}
+
+// ExtractMember is a function that accepts a result and extracts a router.
+func (r commonMemberResult) Extract() (*Member, error) {
+	var s struct {
+		Member *Member `json:"member"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Member, err
+}
+
+// CreateMemberResult represents the result of a CreateMember operation.
+type CreateMemberResult struct {
+	commonMemberResult
+}
+
+// GetMemberResult represents the result of a GetMember operation.
+type GetMemberResult struct {
+	commonMemberResult
+}
+
+// UpdateMemberResult represents the result of an UpdateMember operation.
+type UpdateMemberResult struct {
+	commonMemberResult
+}
+
+// DeleteMemberResult represents the result of a DeleteMember operation.
+type DeleteMemberResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go
new file mode 100644
index 0000000..65eb521
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_lbaas_v2_pools_v2
+package testing
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/testing/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/fixtures.go
new file mode 100644
index 0000000..df9d1fd
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/fixtures.go
@@ -0,0 +1,388 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// PoolsListBody contains the canned body of a pool list response.
+const PoolsListBody = `
+{
+	"pools":[
+	         {
+			"lb_algorithm":"ROUND_ROBIN",
+			"protocol":"HTTP",
+			"description":"",
+			"healthmonitor_id": "466c8345-28d8-4f84-a246-e04380b0461d",
+			"members":[{"id": "53306cda-815d-4354-9fe4-59e09da9c3c5"}],
+			"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+			"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+			"id":"72741b06-df4d-4715-b142-276b6bce75ab",
+			"name":"web",
+			"admin_state_up":true,
+			"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+			"provider": "haproxy"
+		},
+		{
+			"lb_algorithm":"LEAST_CONNECTION",
+			"protocol":"HTTP",
+			"description":"",
+			"healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
+			"members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
+			"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+			"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+			"id":"c3741b06-df4d-4715-b142-276b6bce75ab",
+			"name":"db",
+			"admin_state_up":true,
+			"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+			"provider": "haproxy"
+		}
+	]
+}
+`
+
+// SinglePoolBody is the canned body of a Get request on an existing pool.
+const SinglePoolBody = `
+{
+	"pool": {
+		"lb_algorithm":"LEAST_CONNECTION",
+		"protocol":"HTTP",
+		"description":"",
+		"healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
+		"members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
+		"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+		"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+		"id":"c3741b06-df4d-4715-b142-276b6bce75ab",
+		"name":"db",
+		"admin_state_up":true,
+		"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+		"provider": "haproxy"
+	}
+}
+`
+
+// PostUpdatePoolBody is the canned response body of a Update request on an existing pool.
+const PostUpdatePoolBody = `
+{
+	"pool": {
+		"lb_algorithm":"LEAST_CONNECTION",
+		"protocol":"HTTP",
+		"description":"",
+		"healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
+		"members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
+		"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+		"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+		"id":"c3741b06-df4d-4715-b142-276b6bce75ab",
+		"name":"db",
+		"admin_state_up":true,
+		"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+		"provider": "haproxy"
+	}
+}
+`
+
+var (
+	PoolWeb = pools.Pool{
+		LBMethod:      "ROUND_ROBIN",
+		Protocol:      "HTTP",
+		Description:   "",
+		MonitorID:     "466c8345-28d8-4f84-a246-e04380b0461d",
+		TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+		AdminStateUp:  true,
+		Name:          "web",
+		Members:       []pools.Member{{ID: "53306cda-815d-4354-9fe4-59e09da9c3c5"}},
+		ID:            "72741b06-df4d-4715-b142-276b6bce75ab",
+		Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+		Listeners:     []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+		Provider:      "haproxy",
+	}
+	PoolDb = pools.Pool{
+		LBMethod:      "LEAST_CONNECTION",
+		Protocol:      "HTTP",
+		Description:   "",
+		MonitorID:     "5f6c8345-28d8-4f84-a246-e04380b0461d",
+		TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+		AdminStateUp:  true,
+		Name:          "db",
+		Members:       []pools.Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
+		ID:            "c3741b06-df4d-4715-b142-276b6bce75ab",
+		Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+		Listeners:     []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+		Provider:      "haproxy",
+	}
+	PoolUpdated = pools.Pool{
+		LBMethod:      "LEAST_CONNECTION",
+		Protocol:      "HTTP",
+		Description:   "",
+		MonitorID:     "5f6c8345-28d8-4f84-a246-e04380b0461d",
+		TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+		AdminStateUp:  true,
+		Name:          "db",
+		Members:       []pools.Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
+		ID:            "c3741b06-df4d-4715-b142-276b6bce75ab",
+		Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+		Listeners:     []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+		Provider:      "haproxy",
+	}
+)
+
+// HandlePoolListSuccessfully sets up the test server to respond to a pool List request.
+func HandlePoolListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools", 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, PoolsListBody)
+		case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+			fmt.Fprintf(w, `{ "pools": [] }`)
+		default:
+			t.Fatalf("/v2.0/lbaas/pools invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandlePoolCreationSuccessfully sets up the test server to respond to a pool creation request
+// with a given response.
+func HandlePoolCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools", 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": {
+			        "lb_algorithm": "ROUND_ROBIN",
+			        "protocol": "HTTP",
+			        "name": "Example pool",
+			        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+			        "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandlePoolGetSuccessfully sets up the test server to respond to a pool Get request.
+func HandlePoolGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", 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, SinglePoolBody)
+	})
+}
+
+// HandlePoolDeletionSuccessfully sets up the test server to respond to a pool deletion request.
+func HandlePoolDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", 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)
+	})
+}
+
+// HandlePoolUpdateSuccessfully sets up the test server to respond to a pool Update request.
+func HandlePoolUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", 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, `{
+			"pool": {
+				"name": "NewPoolName",
+                                "lb_algorithm": "LEAST_CONNECTIONS"
+			}
+		}`)
+
+		fmt.Fprintf(w, PostUpdatePoolBody)
+	})
+}
+
+// MembersListBody contains the canned body of a member list response.
+const MembersListBody = `
+{
+	"members":[
+		{
+			"id": "2a280670-c202-4b0b-a562-34077415aabf",
+			"address": "10.0.2.10",
+			"weight": 5,
+			"name": "web",
+			"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+			"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+			"admin_state_up":true,
+			"protocol_port": 80
+		},
+		{
+			"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+			"address": "10.0.2.11",
+			"weight": 10,
+			"name": "db",
+			"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+			"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+			"admin_state_up":false,
+			"protocol_port": 80
+		}
+	]
+}
+`
+
+// SingleMemberBody is the canned body of a Get request on an existing member.
+const SingleMemberBody = `
+{
+	"member": {
+		"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+		"address": "10.0.2.11",
+		"weight": 10,
+		"name": "db",
+		"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+		"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+		"admin_state_up":false,
+		"protocol_port": 80
+	}
+}
+`
+
+// PostUpdateMemberBody is the canned response body of a Update request on an existing member.
+const PostUpdateMemberBody = `
+{
+	"member": {
+		"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+		"address": "10.0.2.11",
+		"weight": 10,
+		"name": "db",
+		"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+		"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+		"admin_state_up":false,
+		"protocol_port": 80
+	}
+}
+`
+
+var (
+	MemberWeb = pools.Member{
+		SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+		AdminStateUp: true,
+		Name:         "web",
+		ID:           "2a280670-c202-4b0b-a562-34077415aabf",
+		Address:      "10.0.2.10",
+		Weight:       5,
+		ProtocolPort: 80,
+	}
+	MemberDb = pools.Member{
+		SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+		AdminStateUp: false,
+		Name:         "db",
+		ID:           "fad389a3-9a4a-4762-a365-8c7038508b5d",
+		Address:      "10.0.2.11",
+		Weight:       10,
+		ProtocolPort: 80,
+	}
+	MemberUpdated = pools.Member{
+		SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+		AdminStateUp: false,
+		Name:         "db",
+		ID:           "fad389a3-9a4a-4762-a365-8c7038508b5d",
+		Address:      "10.0.2.11",
+		Weight:       10,
+		ProtocolPort: 80,
+	}
+)
+
+// HandleMemberListSuccessfully sets up the test server to respond to a member List request.
+func HandleMemberListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", 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, MembersListBody)
+		case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+			fmt.Fprintf(w, `{ "members": [] }`)
+		default:
+			t.Fatalf("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandleMemberCreationSuccessfully sets up the test server to respond to a member creation request
+// with a given response.
+func HandleMemberCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", 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, `{
+			"member": {
+			        "address": "10.0.2.11",
+			        "weight": 10,
+			        "name": "db",
+			        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+			        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+			        "protocol_port": 80
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleMemberGetSuccessfully sets up the test server to respond to a member Get request.
+func HandleMemberGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", 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, SingleMemberBody)
+	})
+}
+
+// HandleMemberDeletionSuccessfully sets up the test server to respond to a member deletion request.
+func HandleMemberDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", 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)
+	})
+}
+
+// HandleMemberUpdateSuccessfully sets up the test server to respond to a member Update request.
+func HandleMemberUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", 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, `{
+			"member": {
+				"name": "newMemberName",
+                                "weight": 4
+			}
+		}`)
+
+		fmt.Fprintf(w, PostUpdateMemberBody)
+	})
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/requests_test.go
new file mode 100644
index 0000000..4af00ec
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/requests_test.go
@@ -0,0 +1,262 @@
+package testing
+
+import (
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListPools(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePoolListSuccessfully(t)
+
+	pages := 0
+	err := pools.List(fake.ServiceClient(), pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := pools.ExtractPools(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 pools, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, PoolWeb, actual[0])
+		th.CheckDeepEquals(t, PoolDb, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListAllPools(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePoolListSuccessfully(t)
+
+	allPages, err := pools.List(fake.ServiceClient(), pools.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := pools.ExtractPools(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, PoolWeb, actual[0])
+	th.CheckDeepEquals(t, PoolDb, actual[1])
+}
+
+func TestCreatePool(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePoolCreationSuccessfully(t, SinglePoolBody)
+
+	actual, err := pools.Create(fake.ServiceClient(), pools.CreateOpts{
+		LBMethod:       pools.LBMethodRoundRobin,
+		Protocol:       "HTTP",
+		Name:           "Example pool",
+		TenantID:       "2ffc6e22aae24e4795f87155d24c896f",
+		LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, PoolDb, *actual)
+}
+
+func TestGetPool(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePoolGetSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := pools.Get(client, "c3741b06-df4d-4715-b142-276b6bce75ab").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, PoolDb, *actual)
+}
+
+func TestDeletePool(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePoolDeletionSuccessfully(t)
+
+	res := pools.Delete(fake.ServiceClient(), "c3741b06-df4d-4715-b142-276b6bce75ab")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdatePool(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePoolUpdateSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := pools.Update(client, "c3741b06-df4d-4715-b142-276b6bce75ab", pools.UpdateOpts{
+		Name:     "NewPoolName",
+		LBMethod: pools.LBMethodLeastConnections,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, PoolUpdated, *actual)
+}
+
+func TestRequiredPoolCreateOpts(t *testing.T) {
+	res := pools.Create(fake.ServiceClient(), pools.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = pools.Create(fake.ServiceClient(), pools.CreateOpts{
+		LBMethod:       pools.LBMethod("invalid"),
+		Protocol:       pools.ProtocolHTTPS,
+		LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a",
+	})
+	if res.Err == nil {
+		t.Fatalf("Expected error, but got none")
+	}
+
+	res = pools.Create(fake.ServiceClient(), pools.CreateOpts{
+		LBMethod:       pools.LBMethodRoundRobin,
+		Protocol:       pools.Protocol("invalid"),
+		LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a",
+	})
+	if res.Err == nil {
+		t.Fatalf("Expected error, but got none")
+	}
+
+	res = pools.Create(fake.ServiceClient(), pools.CreateOpts{
+		LBMethod: pools.LBMethodRoundRobin,
+		Protocol: pools.ProtocolHTTPS,
+	})
+	if res.Err == nil {
+		t.Fatalf("Expected error, but got none")
+	}
+}
+
+func TestListMembers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMemberListSuccessfully(t)
+
+	pages := 0
+	err := pools.ListMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.ListMembersOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := pools.ExtractMembers(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 members, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, MemberWeb, actual[0])
+		th.CheckDeepEquals(t, MemberDb, actual[1])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListAllMembers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMemberListSuccessfully(t)
+
+	allPages, err := pools.ListMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.ListMembersOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := pools.ExtractMembers(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, MemberWeb, actual[0])
+	th.CheckDeepEquals(t, MemberDb, actual[1])
+}
+
+func TestCreateMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMemberCreationSuccessfully(t, SingleMemberBody)
+
+	actual, err := pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{
+		Name:         "db",
+		SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+		Address:      "10.0.2.11",
+		ProtocolPort: 80,
+		Weight:       10,
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, MemberDb, *actual)
+}
+
+func TestRequiredMemberCreateOpts(t *testing.T) {
+	res := pools.CreateMember(fake.ServiceClient(), "", pools.CreateMemberOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = pools.CreateMember(fake.ServiceClient(), "", pools.CreateMemberOpts{Address: "1.2.3.4", ProtocolPort: 80})
+	if res.Err == nil {
+		t.Fatalf("Expected error, but got none")
+	}
+	res = pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{ProtocolPort: 80})
+	if res.Err == nil {
+		t.Fatalf("Expected error, but got none")
+	}
+	res = pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{Address: "1.2.3.4"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, but got none")
+	}
+}
+
+func TestGetMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMemberGetSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := pools.GetMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, MemberDb, *actual)
+}
+
+func TestDeleteMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMemberDeletionSuccessfully(t)
+
+	res := pools.DeleteMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleMemberUpdateSuccessfully(t)
+
+	client := fake.ServiceClient()
+	actual, err := pools.UpdateMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf", pools.UpdateMemberOpts{
+		Name:   "newMemberName",
+		Weight: 4,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, MemberUpdated, *actual)
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go b/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go
new file mode 100644
index 0000000..bceca67
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go
@@ -0,0 +1,25 @@
+package pools
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+	rootPath     = "lbaas"
+	resourcePath = "pools"
+	memberPath   = "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)
+}
+
+func memberRootURL(c *gophercloud.ServiceClient, poolId string) string {
+	return c.ServiceURL(rootPath, resourcePath, poolId, memberPath)
+}
+
+func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memeberID string) string {
+	return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memeberID)
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/doc.go b/openstack/networking/v2/extensions/portsbinding/doc.go
new file mode 100644
index 0000000..0d2ed58
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/doc.go
@@ -0,0 +1,3 @@
+// Package portsbinding provides information and interaction with the port
+// binding extension for the OpenStack Networking service.
+package portsbinding
diff --git a/openstack/networking/v2/extensions/portsbinding/requests.go b/openstack/networking/v2/extensions/portsbinding/requests.go
new file mode 100644
index 0000000..b46172b
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/requests.go
@@ -0,0 +1,113 @@
+package portsbinding
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+	return
+}
+
+// CreateOpts represents the attributes used when creating a new
+// port with extended attributes.
+type CreateOpts struct {
+	// CreateOptsBuilder is the interface options structs have to satisfy in order
+	// to be used in the main Create operation in this package.
+	ports.CreateOptsBuilder `json:"-"`
+	// The ID of the host where the port is allocated
+	HostID string `json:"binding:host_id,omitempty"`
+	// The virtual network interface card (vNIC) type that is bound to the
+	// neutron port
+	VNICType string `json:"binding:vnic_type,omitempty"`
+	// A dictionary that enables the application running on the specified
+	// host to pass and receive virtual network interface (VIF) port-specific
+	// information to the plug-in
+	Profile map[string]string `json:"binding:profile,omitempty"`
+}
+
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+	b1, err := opts.CreateOptsBuilder.ToPortCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	b2, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	port := b1["port"].(map[string]interface{})
+
+	for k, v := range b2 {
+		port[k] = v
+	}
+
+	return map[string]interface{}{"port": port}, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new port with extended attributes.
+// You must remember to provide a NetworkID value.
+func Create(c *gophercloud.ServiceClient, opts ports.CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToPortCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
+	return
+}
+
+// UpdateOpts represents the attributes used when updating an existing port.
+type UpdateOpts struct {
+	// UpdateOptsBuilder is the interface options structs have to satisfy in order
+	// to be used in the main Update operation in this package.
+	ports.UpdateOptsBuilder `json:"-"`
+	// The ID of the host where the port is allocated
+	HostID string `json:"binding:host_id,omitempty"`
+	// The virtual network interface card (vNIC) type that is bound to the
+	// neutron port
+	VNICType string `json:"binding:vnic_type,omitempty"`
+	// A dictionary that enables the application running on the specified
+	// host to pass and receive virtual network interface (VIF) port-specific
+	// information to the plug-in
+	Profile map[string]string `json:"binding:profile,omitempty"`
+}
+
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+	b1, err := opts.UpdateOptsBuilder.ToPortUpdateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	b2, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	port := b1["port"].(map[string]interface{})
+
+	for k, v := range b2 {
+		port[k] = v
+	}
+
+	return map[string]interface{}{"port": port}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts ports.UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToPortUpdateMap()
+	if err != nil {
+		r.Err = err
+		return r
+	}
+	_, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/results.go b/openstack/networking/v2/extensions/portsbinding/results.go
new file mode 100644
index 0000000..9527473
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/results.go
@@ -0,0 +1,73 @@
+package portsbinding
+
+import (
+	"github.com/gophercloud/gophercloud"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+	"github.com/gophercloud/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) {
+	var s struct {
+		Port *Port `json:"port"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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
+}
+
+// IP is a sub-struct that represents an individual IP.
+type IP struct {
+	SubnetID  string `json:"subnet_id"`
+	IPAddress string `json:"ip_address"`
+}
+
+// Port represents a Neutron port. See package documentation for a top-level
+// description of what this is.
+type Port struct {
+	ports.Port
+	// The ID of the host where the port is allocated
+	HostID string `json:"binding:host_id"`
+	// A dictionary that enables the application to pass information about
+	// functions that the Networking API provides.
+	VIFDetails map[string]interface{} `json:"binding:vif_details"`
+	// The VIF type for the port.
+	VIFType string `json:"binding:vif_type"`
+	// The virtual network interface card (vNIC) type that is bound to the
+	// neutron port
+	VNICType string `json:"binding:vnic_type"`
+	// A dictionary that enables the application running on the specified
+	// host to pass and receive virtual network interface (VIF) port-specific
+	// information to the plug-in
+	Profile map[string]string `json:"binding:profile"`
+}
+
+// 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(r pagination.Page) ([]Port, error) {
+	var s struct {
+		Ports []Port `json:"ports"`
+	}
+	err := (r.(ports.PortPage)).ExtractInto(&s)
+	return s.Ports, err
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/testing/doc.go b/openstack/networking/v2/extensions/portsbinding/testing/doc.go
new file mode 100644
index 0000000..deb52b1
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_portsbinding_v2
+package testing
diff --git a/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go b/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go
new file mode 100644
index 0000000..f688c20
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go
@@ -0,0 +1,207 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func HandleListSuccessfully(t *testing.T) {
+	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",
+            "binding:vnic_type": "normal"
+        }
+    ]
+}
+      `)
+	})
+}
+
+func HandleGet(t *testing.T) {
+	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",
+        "binding:host_id": "devstack",
+        "name": "",
+        "allowed_address_pairs": [],
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "7e02058126cc4950b75f9970368ba177",
+        "extra_dhcp_opts": [],
+        "binding:vif_details": {
+            "port_filter": true,
+            "ovs_hybrid_plug": true
+        },
+        "binding:vif_type": "ovs",
+        "device_owner": "network:router_interface",
+        "port_security_enabled": false,
+        "mac_address": "fa:16:3e:23:fd:d7",
+        "binding:profile": {},
+        "binding:vnic_type": "normal",
+        "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"
+    }
+}
+    `)
+	})
+}
+
+func HandleCreate(t *testing.T) {
+	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"],
+		"binding:host_id": "HOST1",
+        "binding:vnic_type": "normal"
+    }
+}
+			`)
+
+		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"
+            }
+        ],
+        "binding:host_id": "HOST1",
+        "binding:vnic_type": "normal",
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+		`)
+	})
+}
+
+func HandleUpdate(t *testing.T) {
+	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"
+        	],
+        	"binding:host_id": "HOST1",
+        	"binding:vnic_type": "normal",
+					"allowed_address_pairs": null
+		}
+}
+			`)
+
+		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": "",
+        "binding:host_id": "HOST1",
+        "binding:vnic_type": "normal"
+    }
+}
+		`)
+	})
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go b/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go
new file mode 100644
index 0000000..f41f1cc
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go
@@ -0,0 +1,164 @@
+package testing
+
+import (
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleListSuccessfully(t)
+
+	count := 0
+
+	ports.List(fake.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := portsbinding.ExtractPorts(page)
+		th.AssertNoErr(t, err)
+
+		expected := []portsbinding.Port{
+			{
+				Port: ports.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: []ports.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",
+				},
+				VNICType: "normal",
+				HostID:   "devstack",
+			},
+		}
+
+		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()
+
+	HandleGet(t)
+
+	n, err := portsbinding.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, []ports.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.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e")
+
+	th.AssertEquals(t, n.HostID, "devstack")
+	th.AssertEquals(t, n.VNICType, "normal")
+	th.AssertEquals(t, n.VIFType, "ovs")
+	th.AssertDeepEquals(t, n.VIFDetails, map[string]interface{}{"port_filter": true, "ovs_hybrid_plug": true})
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleCreate(t)
+
+	asu := true
+	options := portsbinding.CreateOpts{
+		CreateOptsBuilder: ports.CreateOpts{
+			Name:         "private-port",
+			AdminStateUp: &asu,
+			NetworkID:    "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+			FixedIPs: []ports.IP{
+				{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+			},
+			SecurityGroups: []string{"foo"},
+		},
+		HostID:   "HOST1",
+		VNICType: "normal",
+	}
+	n, err := portsbinding.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, []ports.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"})
+	th.AssertEquals(t, n.HostID, "HOST1")
+	th.AssertEquals(t, n.VNICType, "normal")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := portsbinding.Create(fake.ServiceClient(), portsbinding.CreateOpts{CreateOptsBuilder: ports.CreateOpts{}})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleUpdate(t)
+
+	options := portsbinding.UpdateOpts{
+		UpdateOptsBuilder: ports.UpdateOpts{
+			Name: "new_port_name",
+			FixedIPs: []ports.IP{
+				{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+			},
+			SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+		},
+		HostID:   "HOST1",
+		VNICType: "normal",
+	}
+
+	s, err := portsbinding.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, []ports.IP{
+		{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+	th.AssertEquals(t, s.HostID, "HOST1")
+	th.AssertEquals(t, s.VNICType, "normal")
+}
diff --git a/openstack/networking/v2/extensions/portsbinding/urls.go b/openstack/networking/v2/extensions/portsbinding/urls.go
new file mode 100644
index 0000000..a531a7e
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/urls.go
@@ -0,0 +1,23 @@
+package portsbinding
+
+import "github.com/gophercloud/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 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)
+}
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 100644
index 0000000..37142e8
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/results.go
@@ -0,0 +1,116 @@
+package provider
+
+import (
+	"encoding/json"
+	"strconv"
+
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// NetworkExtAttrs represents an extended form of a Network with additional fields.
+type NetworkExtAttrs struct {
+	// UUID for the network
+	ID string `json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Name string `json:"name"`
+
+	// The administrative state of network. If false (down), the network does not forward packets.
+	AdminStateUp bool `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 `json:"status"`
+
+	// Subnets associated with this network.
+	Subnets []string `json:"subnets"`
+
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `json:"tenant_id"`
+
+	// Specifies whether the network resource can be accessed by any tenant or not.
+	Shared bool `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"`
+
+	// 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"`
+
+	// 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"`
+}
+
+func (n *NetworkExtAttrs) UnmarshalJSON(b []byte) error {
+	type tmp NetworkExtAttrs
+	var networkExtAttrs *struct {
+		tmp
+		SegmentationID interface{} `json:"provider:segmentation_id"`
+	}
+
+	if err := json.Unmarshal(b, &networkExtAttrs); err != nil {
+		return err
+	}
+
+	*n = NetworkExtAttrs(networkExtAttrs.tmp)
+
+	switch t := networkExtAttrs.SegmentationID.(type) {
+	case float64:
+		n.SegmentationID = strconv.FormatFloat(t, 'f', -1, 64)
+	case string:
+		n.SegmentationID = string(t)
+	}
+
+	return nil
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) {
+	var s struct {
+		Network *NetworkExtAttrs `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Network, err
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) {
+	var s struct {
+		Network *NetworkExtAttrs `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Network, err
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) {
+	var s struct {
+		Network *NetworkExtAttrs `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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(r pagination.Page) ([]NetworkExtAttrs, error) {
+	var s struct {
+		Networks []NetworkExtAttrs `json:"networks" json:"networks"`
+	}
+	err := (r.(networks.NetworkPage)).ExtractInto(&s)
+	return s.Networks, err
+}
diff --git a/openstack/networking/v2/extensions/provider/testing/doc.go b/openstack/networking/v2/extensions/provider/testing/doc.go
new file mode 100644
index 0000000..370ce19
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_provider_v2
+package testing
diff --git a/openstack/networking/v2/extensions/provider/testing/results_test.go b/openstack/networking/v2/extensions/provider/testing/results_test.go
new file mode 100644
index 0000000..f6b321a
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/testing/results_test.go
@@ -0,0 +1,255 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/provider"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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 := provider.ExtractList(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []provider.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:  "",
+			},
+			{
+				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 := provider.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: gophercloud.Enabled}
+	res := networks.Create(fake.ServiceClient(), options)
+	n, err := provider.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: gophercloud.Disabled, Shared: &iTrue}
+	res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+	n, err := provider.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..5ca4850
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests.go
@@ -0,0 +1,108 @@
+package groups
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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}}
+	})
+}
+
+type CreateOptsBuilder interface {
+	ToSecGroupCreateMap() (map[string]interface{}, error)
+}
+
+// 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 `json:"name" required:"true"`
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID string `json:"tenant_id,omitempty"`
+	// Optional. Describes the security group.
+	Description string `json:"description,omitempty"`
+}
+
+func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "security_group")
+}
+
+// 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 CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToSecGroupCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
+
+// IDFromName is a convenience function that returns a security group's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, ListOpts{}).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractGroups(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "security group"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "security group"}
+	}
+}
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..ea3ad65
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -0,0 +1,94 @@
+package groups
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/gophercloud/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"`
+
+	// Owner of the security group. Only admin users can specify a TenantID
+	// other than their own.
+	TenantID string `json:"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 (r SecGroupPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"security_groups_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a SecGroupPage struct is empty.
+func (r SecGroupPage) IsEmpty() (bool, error) {
+	is, err := ExtractGroups(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]SecGroup, error) {
+	var s struct {
+		SecGroups []SecGroup `json:"security_groups"`
+	}
+	err := (r.(SecGroupPage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		SecGroup *SecGroup `json:"security_group"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/security/groups/testing/doc.go
new file mode 100644
index 0000000..69d5db7
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_security_groups_v2
+package testing
diff --git a/openstack/networking/v2/extensions/security/groups/testing/requests_test.go b/openstack/networking/v2/extensions/security/groups/testing/requests_test.go
new file mode 100644
index 0000000..acd3230
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/testing/requests_test.go
@@ -0,0 +1,206 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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
+
+	groups.List(fake.ServiceClient(), groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := groups.ExtractGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract secgroups: %v", err)
+			return false, err
+		}
+
+		expected := []groups.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 := groups.CreateOpts{Name: "new-webservers", Description: "security group for webservers"}
+	_, err := groups.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 := groups.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 := groups.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..104cbcc
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/urls.go
@@ -0,0 +1,13 @@
+package groups
+
+import "github.com/gophercloud/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..77f7e37
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -0,0 +1,127 @@
+package rules
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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}}
+	})
+}
+
+type RuleDirection string
+type RuleProtocol string
+type RuleEtherType string
+
+// Constants useful for CreateOpts
+const (
+	DirIngress   RuleDirection = "ingress"
+	DirEgress    RuleDirection = "egress"
+	ProtocolTCP  RuleProtocol  = "tcp"
+	ProtocolUDP  RuleProtocol  = "udp"
+	ProtocolICMP RuleProtocol  = "icmp"
+	EtherType4   RuleEtherType = "IPv4"
+	EtherType6   RuleEtherType = "IPv6"
+)
+
+// CreateOptsBuilder is what types must satisfy to be used as Create
+// options.
+type CreateOptsBuilder interface {
+	ToSecGroupRuleCreateMap() (map[string]interface{}, error)
+}
+
+// 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 RuleDirection `json:"direction" required:"true"`
+	// Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must
+	// match the ingress or egress rules.
+	EtherType RuleEtherType `json:"ethertype" required:"true"`
+	// Required. The security group ID to associate with this security group rule.
+	SecGroupID string `json:"security_group_id" required:"true"`
+	// 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 `json:"port_range_max,omitempty"`
+	// 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 `json:"port_range_min,omitempty"`
+	// Optional. The protocol that is matched by the security group rule. Valid
+	// values are "tcp", "udp", "icmp" or an empty string.
+	Protocol RuleProtocol `json:"protocol,omitempty"`
+	// Optional. 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,omitempty"`
+	// 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 `json:"remote_ip_prefix,omitempty"`
+	// Required for admins. Indicates the owner of the VIP.
+	TenantID string `json:"tenant_id,omitempty"`
+}
+
+// ToSecGroupRuleCreateMap allows CreateOpts to satisfy the CreateOptsBuilder
+// interface
+func (opts CreateOpts) ToSecGroupRuleCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "security_group_rule")
+}
+
+// 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 CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToSecGroupRuleCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+	return
+}
+
+// Get retrieves a particular security group rule based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+	return
+}
+
+// Delete will permanently delete a particular security group rule based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(resourceURL(c, id), nil)
+	return
+}
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..18476a6
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -0,0 +1,118 @@
+package rules
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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"`
+
+	// The security group ID to associate with this security group rule.
+	SecGroupID string `json:"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"`
+
+	// 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"`
+
+	// 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"`
+
+	// 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"`
+
+	// The owner of this security group rule.
+	TenantID string `json:"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 (r SecGroupRulePage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"security_group_rules_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a SecGroupRulePage struct is empty.
+func (r SecGroupRulePage) IsEmpty() (bool, error) {
+	is, err := ExtractRules(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]SecGroupRule, error) {
+	var s struct {
+		SecGroupRules []SecGroupRule `json:"security_group_rules"`
+	}
+	err := (r.(SecGroupRulePage)).ExtractInto(&s)
+	return s.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) {
+	var s struct {
+		SecGroupRule *SecGroupRule `json:"security_group_rule"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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/testing/doc.go b/openstack/networking/v2/extensions/security/rules/testing/doc.go
new file mode 100644
index 0000000..a4f7b43
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_security_rules_v2
+package testing
diff --git a/openstack/networking/v2/extensions/security/rules/testing/requests_test.go b/openstack/networking/v2/extensions/security/rules/testing/requests_test.go
new file mode 100644
index 0000000..968fd04
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/testing/requests_test.go
@@ -0,0 +1,236 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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
+
+	rules.List(fake.ServiceClient(), rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := rules.ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract secrules: %v", err)
+			return false, err
+		}
+
+		expected := []rules.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",
+			},
+			{
+				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 := rules.CreateOpts{
+		Direction:     "ingress",
+		PortRangeMin:  80,
+		EtherType:     rules.EtherType4,
+		PortRangeMax:  80,
+		Protocol:      "tcp",
+		RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+		SecGroupID:    "a7734e61-b545-452d-a3cd-0189cbd9747a",
+	}
+	_, err := rules.Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress, EtherType: rules.EtherType4})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress, EtherType: rules.EtherType4})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+	res = rules.Create(fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress, EtherType: rules.EtherType4, 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 := rules.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 := rules.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..a5ede0e
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -0,0 +1,13 @@
+package rules
+
+import "github.com/gophercloud/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/extensions/testing/delegate_test.go b/openstack/networking/v2/extensions/testing/delegate_test.go
new file mode 100755
index 0000000..618d052
--- /dev/null
+++ b/openstack/networking/v2/extensions/testing/delegate_test.go
@@ -0,0 +1,106 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	common "github.com/gophercloud/gophercloud/openstack/common/extensions"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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
+
+	extensions.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := extensions.ExtractExtensions(page)
+		if err != nil {
+			t.Errorf("Failed to extract extensions: %v", err)
+		}
+
+		expected := []extensions.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 := extensions.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/testing/doc.go b/openstack/networking/v2/extensions/testing/doc.go
new file mode 100644
index 0000000..5a104fb
--- /dev/null
+++ b/openstack/networking/v2/extensions/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_v2
+package testing
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/requests.go b/openstack/networking/v2/networks/requests.go
new file mode 100644
index 0000000..876a00b
--- /dev/null
+++ b/openstack/networking/v2/networks/requests.go
@@ -0,0 +1,168 @@
+package networks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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)
+	return q.String(), err
+}
+
+// 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) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 satisfies the CreateOptsBuilder interface
+type CreateOpts struct {
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	Name         string `json:"name,omitempty"`
+	Shared       *bool  `json:"shared,omitempty"`
+	TenantID     string `json:"tenant_id,omitempty"`
+}
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "network")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToNetworkCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
+	return
+}
+
+// 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 satisfies the UpdateOptsBuilder interface
+type UpdateOpts struct {
+	AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	Name         string `json:"name,omitempty"`
+	Shared       *bool  `json:"shared,omitempty"`
+}
+
+// ToNetworkUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "network")
+}
+
+// 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) (r UpdateResult) {
+	b, err := opts.ToNetworkUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) (r DeleteResult) {
+	_, r.Err = c.Delete(deleteURL(c, networkID), nil)
+	return
+}
+
+// IDFromName is a convenience function that returns a network's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractNetworks(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "network"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "network"}
+	}
+}
diff --git a/openstack/networking/v2/networks/results.go b/openstack/networking/v2/networks/results.go
new file mode 100644
index 0000000..d928980
--- /dev/null
+++ b/openstack/networking/v2/networks/results.go
@@ -0,0 +1,101 @@
+package networks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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) {
+	var s struct {
+		Network *Network `json:"network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 `json:"id"`
+
+	// Human-readable name for the network. Might not be unique.
+	Name string `json:"name"`
+
+	// The administrative state of network. If false (down), the network does not forward packets.
+	AdminStateUp bool `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 `json:"status"`
+
+	// Subnets associated with this network.
+	Subnets []string `json:"subnets"`
+
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `json:"tenant_id"`
+
+	// Specifies whether the network resource can be accessed by any tenant or not.
+	Shared bool `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 (r NetworkPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"networks_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (r NetworkPage) IsEmpty() (bool, error) {
+	is, err := ExtractNetworks(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Network, error) {
+	var s struct {
+		Networks []Network `json:"networks"`
+	}
+	err := (r.(NetworkPage)).ExtractInto(&s)
+	return s.Networks, err
+}
diff --git a/openstack/networking/v2/networks/testing/doc.go b/openstack/networking/v2/networks/testing/doc.go
new file mode 100644
index 0000000..860bd7a
--- /dev/null
+++ b/openstack/networking/v2/networks/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_networks_v2
+package testing
diff --git a/openstack/networking/v2/networks/testing/requests_test.go b/openstack/networking/v2/networks/testing/requests_test.go
new file mode 100644
index 0000000..5b9f03d
--- /dev/null
+++ b/openstack/networking/v2/networks/testing/requests_test.go
@@ -0,0 +1,277 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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
+
+	networks.List(client, networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := networks.ExtractNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []networks.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",
+			},
+			{
+				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 := networks.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 := networks.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue}
+	n, err := networks.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 := networks.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"}
+	_, err := networks.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 := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, Shared: &iTrue}
+	n, err := networks.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 := networks.Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/networks/urls.go b/openstack/networking/v2/networks/urls.go
new file mode 100644
index 0000000..4a8fb1d
--- /dev/null
+++ b/openstack/networking/v2/networks/urls.go
@@ -0,0 +1,31 @@
+package networks
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/networking/v2/ports/requests.go
new file mode 100644
index 0000000..d353b7e
--- /dev/null
+++ b/openstack/networking/v2/ports/requests.go
@@ -0,0 +1,180 @@
+package ports
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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)
+	return q.String(), err
+}
+
+// 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) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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        `json:"network_id" required:"true"`
+	Name                string        `json:"name,omitempty"`
+	AdminStateUp        *bool         `json:"admin_state_up,omitempty"`
+	MACAddress          string        `json:"mac_address,omitempty"`
+	FixedIPs            interface{}   `json:"fixed_ips,omitempty"`
+	DeviceID            string        `json:"device_id,omitempty"`
+	DeviceOwner         string        `json:"device_owner,omitempty"`
+	TenantID            string        `json:"tenant_id,omitempty"`
+	SecurityGroups      []string      `json:"security_groups,omitempty"`
+	AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"`
+}
+
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "port")
+}
+
+// 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) (r CreateResult) {
+	b, err := opts.ToPortCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
+	return
+}
+
+// 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        `json:"name,omitempty"`
+	AdminStateUp        *bool         `json:"admin_state_up,omitempty"`
+	FixedIPs            interface{}   `json:"fixed_ips,omitempty"`
+	DeviceID            string        `json:"device_id,omitempty"`
+	DeviceOwner         string        `json:"device_owner,omitempty"`
+	SecurityGroups      []string      `json:"security_groups"`
+	AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"`
+}
+
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "port")
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToPortUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete accepts a unique ID and deletes the port associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(deleteURL(c, id), nil)
+	return
+}
+
+// IDFromName is a convenience function that returns a port's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractPorts(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "port"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "port"}
+	}
+}
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
new file mode 100644
index 0000000..57a1765
--- /dev/null
+++ b/openstack/networking/v2/ports/results.go
@@ -0,0 +1,119 @@
+package ports
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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) {
+	var s struct {
+		Port *Port `json:"port"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 `json:"subnet_id"`
+	IPAddress string `json:"ip_address,omitempty"`
+}
+
+// AddressPair contains the IP Address and the MAC address.
+type AddressPair struct {
+	IPAddress  string `json:"ip_address,omitempty"`
+	MACAddress string `json:"mac_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 `json:"id"`
+	// Network that this port is associated with.
+	NetworkID string `json:"network_id"`
+	// Human-readable name for the port. Might not be unique.
+	Name string `json:"name"`
+	// Administrative state of port. If false (down), port does not forward packets.
+	AdminStateUp bool `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 `json:"status"`
+	// Mac address to use on this port.
+	MACAddress string `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 `json:"fixed_ips"`
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `json:"tenant_id"`
+	// Identifies the entity (e.g.: dhcp agent) using this port.
+	DeviceOwner string `json:"device_owner"`
+	// Specifies the IDs of any security groups associated with a port.
+	SecurityGroups []string `json:"security_groups"`
+	// Identifies the device (e.g., virtual server) using this port.
+	DeviceID string `json:"device_id"`
+	// Identifies the list of IP addresses the port will recognize/accept
+	AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"`
+}
+
+// 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 (r PortPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"ports_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PortPage struct is empty.
+func (r PortPage) IsEmpty() (bool, error) {
+	is, err := ExtractPorts(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Port, error) {
+	var s struct {
+		Ports []Port `json:"ports"`
+	}
+	err := (r.(PortPage)).ExtractInto(&s)
+	return s.Ports, err
+}
diff --git a/openstack/networking/v2/ports/testing/doc.go b/openstack/networking/v2/ports/testing/doc.go
new file mode 100644
index 0000000..70a559a
--- /dev/null
+++ b/openstack/networking/v2/ports/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_ports_v2
+package testing
diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go
new file mode 100644
index 0000000..1da6ad3
--- /dev/null
+++ b/openstack/networking/v2/ports/testing/requests_test.go
@@ -0,0 +1,519 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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
+
+	ports.List(fake.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ports.ExtractPorts(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []ports.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: []ports.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 := ports.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, []ports.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"],
+        "allowed_address_pairs": [
+          {
+            "ip_address": "10.0.0.4",
+            "mac_address": "fa:16:3e:c9:cb:f0"
+          }
+        ]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "private-port",
+        "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"
+        ],
+        "allowed_address_pairs": [
+          {
+            "ip_address": "10.0.0.4",
+            "mac_address": "fa:16:3e:c9:cb:f0"
+          }
+        ],
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	asu := true
+	options := ports.CreateOpts{
+		Name:         "private-port",
+		AdminStateUp: &asu,
+		NetworkID:    "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+		FixedIPs: []ports.IP{
+			{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+		},
+		SecurityGroups: []string{"foo"},
+		AllowedAddressPairs: []ports.AddressPair{
+			{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+		},
+	}
+	n, err := ports.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, []ports.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"})
+	th.AssertDeepEquals(t, n.AllowedAddressPairs, []ports.AddressPair{
+		{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+	})
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := ports.Create(fake.ServiceClient(), ports.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"
+            }
+        ],
+        "allowed_address_pairs": [
+          {
+            "ip_address": "10.0.0.4",
+            "mac_address": "fa:16:3e:c9:cb:f0"
+          }
+        ],
+				"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"
+            }
+        ],
+        "allowed_address_pairs": [
+          {
+            "ip_address": "10.0.0.4",
+            "mac_address": "fa:16:3e:c9:cb:f0"
+          }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	options := ports.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []ports.IP{
+			{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+		AllowedAddressPairs: []ports.AddressPair{
+			{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+		},
+	}
+
+	s, err := ports.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, []ports.IP{
+		{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{
+		{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+	})
+	th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestRemoveSecurityGroups(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"
+        }
+      ],
+      "allowed_address_pairs": [
+        {
+          "ip_address": "10.0.0.4",
+          "mac_address": "fa:16:3e:c9:cb:f0"
+        }
+      ],
+      "security_groups": []
+    }
+}
+			`)
+
+		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"
+            }
+        ],
+        "allowed_address_pairs": [
+          {
+            "ip_address": "10.0.0.4",
+            "mac_address": "fa:16:3e:c9:cb:f0"
+          }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "device_id": ""
+    }
+}
+		`)
+	})
+
+	options := ports.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []ports.IP{
+			{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups: []string{},
+		AllowedAddressPairs: []ports.AddressPair{
+			{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+		},
+	}
+
+	s, err := ports.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, []ports.IP{
+		{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair{
+		{IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+	})
+	th.AssertDeepEquals(t, s.SecurityGroups, []string(nil))
+}
+
+func TestRemoveAllowedAddressPairs(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"
+        }
+      ],
+      "allowed_address_pairs": [],
+      "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 := ports.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []ports.IP{
+			{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups:      []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+		AllowedAddressPairs: []ports.AddressPair{},
+	}
+
+	s, err := ports.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, []ports.IP{
+		{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.AllowedAddressPairs, []ports.AddressPair(nil))
+	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 := ports.Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/ports/urls.go b/openstack/networking/v2/ports/urls.go
new file mode 100644
index 0000000..600d6f2
--- /dev/null
+++ b/openstack/networking/v2/ports/urls.go
@@ -0,0 +1,31 @@
+package ports
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/networking/v2/subnets/requests.go
new file mode 100644
index 0000000..896f13e
--- /dev/null
+++ b/openstack/networking/v2/subnets/requests.go
@@ -0,0 +1,193 @@
+package subnets
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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)
+	return q.String(), err
+}
+
+// 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) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+	return
+}
+
+// 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 {
+	NetworkID       string                `json:"network_id" required:"true"`
+	CIDR            string                `json:"cidr" required:"true"`
+	Name            string                `json:"name,omitempty"`
+	TenantID        string                `json:"tenant_id,omitempty"`
+	AllocationPools []AllocationPool      `json:"allocation_pools,omitempty"`
+	GatewayIP       *string               `json:"gateway_ip,omitempty"`
+	IPVersion       gophercloud.IPVersion `json:"ip_version,omitempty"`
+	EnableDHCP      *bool                 `json:"enable_dhcp,omitempty"`
+	DNSNameservers  []string              `json:"dns_nameservers,omitempty"`
+	HostRoutes      []HostRoute           `json:"host_routes,omitempty"`
+}
+
+// ToSubnetCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSubnetCreateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "subnet")
+	if err != nil {
+		return nil, err
+	}
+
+	if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" {
+		m["gateway_ip"] = nil
+	}
+
+	return b, 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) (r CreateResult) {
+	b, err := opts.ToSubnetCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
+	return
+}
+
+// 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      `json:"name,omitempty"`
+	GatewayIP      *string     `json:"gateway_ip,omitempty"`
+	DNSNameservers []string    `json:"dns_nameservers,omitempty"`
+	HostRoutes     []HostRoute `json:"host_routes,omitempty"`
+	EnableDHCP     *bool       `json:"enable_dhcp,omitempty"`
+}
+
+// ToSubnetUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "subnet")
+	if err != nil {
+		return nil, err
+	}
+
+	if m := b["subnet"].(map[string]interface{}); m["gateway_ip"] == "" {
+		m["gateway_ip"] = nil
+	}
+
+	return b, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnet using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToSubnetUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete accepts a unique ID and deletes the subnet associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = c.Delete(deleteURL(c, id), nil)
+	return
+}
+
+// IDFromName is a convenience function that returns a subnet's ID given its name.
+func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
+	count := 0
+	id := ""
+	pages, err := List(client, nil).AllPages()
+	if err != nil {
+		return "", err
+	}
+
+	all, err := ExtractSubnets(pages)
+	if err != nil {
+		return "", err
+	}
+
+	for _, s := range all {
+		if s.Name == name {
+			count++
+			id = s.ID
+		}
+	}
+
+	switch count {
+	case 0:
+		return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "subnet"}
+	case 1:
+		return id, nil
+	default:
+		return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "subnet"}
+	}
+}
diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go
new file mode 100644
index 0000000..ab5cce1
--- /dev/null
+++ b/openstack/networking/v2/subnets/results.go
@@ -0,0 +1,117 @@
+package subnets
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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) {
+	var s struct {
+		Subnet *Subnet `json:"subnet"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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 `json:"destination"`
+	NextHop         string `json:"nexthop"`
+}
+
+// Subnet represents a subnet. See package documentation for a top-level
+// description of what this is.
+type Subnet struct {
+	// UUID representing the subnet
+	ID string `json:"id"`
+	// UUID of the parent network
+	NetworkID string `json:"network_id"`
+	// Human-readable name for the subnet. Might not be unique.
+	Name string `json:"name"`
+	// IP version, either `4' or `6'
+	IPVersion int `json:"ip_version"`
+	// CIDR representing IP range for this subnet, based on IP version
+	CIDR string `json:"cidr"`
+	// Default gateway used by devices in this subnet
+	GatewayIP string `json:"gateway_ip"`
+	// DNS name servers used by hosts in this subnet.
+	DNSNameservers []string `json:"dns_nameservers"`
+	// Sub-ranges of CIDR available for dynamic allocation to ports. See AllocationPool.
+	AllocationPools []AllocationPool `json:"allocation_pools"`
+	// Routes that should be used by devices with IPs from this subnet (not including local subnet route).
+	HostRoutes []HostRoute `json:"host_routes"`
+	// Specifies whether DHCP is enabled for this subnet or not.
+	EnableDHCP bool `json:"enable_dhcp"`
+	// Owner of network. Only admin users can specify a tenant_id other than its own.
+	TenantID string `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 (r SubnetPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"subnets_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a SubnetPage struct is empty.
+func (r SubnetPage) IsEmpty() (bool, error) {
+	is, err := ExtractSubnets(r)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Subnet, error) {
+	var s struct {
+		Subnets []Subnet `json:"subnets"`
+	}
+	err := (r.(SubnetPage)).ExtractInto(&s)
+	return s.Subnets, err
+}
diff --git a/openstack/networking/v2/subnets/testing/doc.go b/openstack/networking/v2/subnets/testing/doc.go
new file mode 100644
index 0000000..43be31a
--- /dev/null
+++ b/openstack/networking/v2/subnets/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_subnets_v2
+package testing
diff --git a/openstack/networking/v2/subnets/testing/requests_test.go b/openstack/networking/v2/subnets/testing/requests_test.go
new file mode 100644
index 0000000..9ff9181
--- /dev/null
+++ b/openstack/networking/v2/subnets/testing/requests_test.go
@@ -0,0 +1,687 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/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"
+        },
+        {
+            "name": "my_gatewayless_subnet",
+            "enable_dhcp": true,
+            "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "dns_nameservers": [],
+            "allocation_pools": [
+                {
+                    "start": "192.168.1.2",
+                    "end": "192.168.1.254"
+                }
+            ],
+            "host_routes": [],
+            "ip_version": 4,
+            "gateway_ip": null,
+            "cidr": "192.168.1.0/24",
+            "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	subnets.List(fake.ServiceClient(), subnets.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := subnets.ExtractSubnets(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []subnets.Subnet{
+			{
+				Name:           "private-subnet",
+				EnableDHCP:     true,
+				NetworkID:      "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+				TenantID:       "26a7980765d0414dbc1fc1f88cdb7e6e",
+				DNSNameservers: []string{},
+				AllocationPools: []subnets.AllocationPool{
+					{
+						Start: "10.0.0.2",
+						End:   "10.0.0.254",
+					},
+				},
+				HostRoutes: []subnets.HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "10.0.0.1",
+				CIDR:       "10.0.0.0/24",
+				ID:         "08eae331-0402-425a-923c-34f7cfe39c1b",
+			},
+			{
+				Name:           "my_subnet",
+				EnableDHCP:     true,
+				NetworkID:      "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+				TenantID:       "4fd44f30292945e481c7b8a0c8908869",
+				DNSNameservers: []string{},
+				AllocationPools: []subnets.AllocationPool{
+					{
+						Start: "192.0.0.2",
+						End:   "192.255.255.254",
+					},
+				},
+				HostRoutes: []subnets.HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "192.0.0.1",
+				CIDR:       "192.0.0.0/8",
+				ID:         "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+			},
+			subnets.Subnet{
+				Name:           "my_gatewayless_subnet",
+				EnableDHCP:     true,
+				NetworkID:      "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+				TenantID:       "4fd44f30292945e481c7b8a0c8908869",
+				DNSNameservers: []string{},
+				AllocationPools: []subnets.AllocationPool{
+					{
+						Start: "192.168.1.2",
+						End:   "192.168.1.254",
+					},
+				},
+				HostRoutes: []subnets.HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "",
+				CIDR:       "192.168.1.0/24",
+				ID:         "54d6f61d-db07-451c-9ab3-b9609b6b6f0c",
+			},
+		}
+
+		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 := subnets.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, []subnets.AllocationPool{
+		{
+			Start: "192.0.0.2",
+			End:   "192.255.255.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []subnets.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,
+        "gateway_ip": "192.168.199.1",
+        "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"
+    }
+}
+		`)
+	})
+
+	var gatewayIP = "192.168.199.1"
+	opts := subnets.CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+		IPVersion: 4,
+		CIDR:      "192.168.199.0/24",
+		GatewayIP: &gatewayIP,
+		AllocationPools: []subnets.AllocationPool{
+			{
+				Start: "192.168.199.2",
+				End:   "192.168.199.254",
+			},
+		},
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []subnets.HostRoute{
+			{NextHop: "bar"},
+		},
+	}
+	s, err := subnets.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, []subnets.AllocationPool{
+		{
+			Start: "192.168.199.2",
+			End:   "192.168.199.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []subnets.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 TestCreateNoGateway(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet": {
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+        "ip_version": 4,
+        "cidr": "192.168.1.0/24",
+        "gateway_ip": null,
+        "allocation_pools": [
+            {
+                "start": "192.168.1.2",
+                "end": "192.168.1.254"
+            }
+        ]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "allocation_pools": [
+            {
+                "start": "192.168.1.2",
+                "end": "192.168.1.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": null,
+        "cidr": "192.168.1.0/24",
+        "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+    }
+}
+		`)
+	})
+
+	var noGateway = ""
+	opts := subnets.CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+		IPVersion: 4,
+		CIDR:      "192.168.1.0/24",
+		GatewayIP: &noGateway,
+		AllocationPools: []subnets.AllocationPool{
+			{
+				Start: "192.168.1.2",
+				End:   "192.168.1.254",
+			},
+		},
+		DNSNameservers: []string{},
+	}
+	s, err := subnets.Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{
+		{
+			Start: "192.168.1.2",
+			End:   "192.168.1.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "")
+	th.AssertEquals(t, s.CIDR, "192.168.1.0/24")
+	th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c")
+}
+
+func TestCreateDefaultGateway(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet": {
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+        "ip_version": 4,
+        "cidr": "192.168.1.0/24",
+        "allocation_pools": [
+            {
+                "start": "192.168.1.2",
+                "end": "192.168.1.254"
+            }
+        ]
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "allocation_pools": [
+            {
+                "start": "192.168.1.2",
+                "end": "192.168.1.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "192.168.1.1",
+        "cidr": "192.168.1.0/24",
+        "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+    }
+}
+		`)
+	})
+
+	opts := subnets.CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+		IPVersion: 4,
+		CIDR:      "192.168.1.0/24",
+		AllocationPools: []subnets.AllocationPool{
+			{
+				Start: "192.168.1.2",
+				End:   "192.168.1.254",
+			},
+		},
+		DNSNameservers: []string{},
+	}
+	s, err := subnets.Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{
+		{
+			Start: "192.168.1.2",
+			End:   "192.168.1.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []subnets.HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "192.168.1.1")
+	th.AssertEquals(t, s.CIDR, "192.168.1.0/24")
+	th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := subnets.Create(fake.ServiceClient(), subnets.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = subnets.Create(fake.ServiceClient(), subnets.CreateOpts{NetworkID: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = subnets.Create(fake.ServiceClient(), subnets.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 := subnets.UpdateOpts{
+		Name:           "my_new_subnet",
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []subnets.HostRoute{
+			{NextHop: "bar"},
+		},
+	}
+	s, err := subnets.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 TestUpdateGateway(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",
+        "gateway_ip": "10.0.0.1"
+    }
+}
+		`)
+
+		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"
+    }
+}
+	`)
+	})
+
+	var gatewayIP = "10.0.0.1"
+	opts := subnets.UpdateOpts{
+		Name:      "my_new_subnet",
+		GatewayIP: &gatewayIP,
+	}
+	s, err := subnets.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")
+	th.AssertEquals(t, s.GatewayIP, "10.0.0.1")
+}
+
+func TestUpdateRemoveGateway(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",
+        "gateway_ip": null
+    }
+}
+		`)
+
+		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": null,
+        "cidr": "10.0.0.0/24",
+        "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+    }
+}
+	`)
+	})
+
+	var noGateway = ""
+	opts := subnets.UpdateOpts{
+		Name:      "my_new_subnet",
+		GatewayIP: &noGateway,
+	}
+	s, err := subnets.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")
+	th.AssertEquals(t, s.GatewayIP, "")
+}
+
+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 := subnets.Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/subnets/testing/results_test.go b/openstack/networking/v2/subnets/testing/results_test.go
new file mode 100644
index 0000000..a227ccd
--- /dev/null
+++ b/openstack/networking/v2/subnets/testing/results_test.go
@@ -0,0 +1,59 @@
+package testing
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+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 := gophercloud.Result{Body: dejson}
+	var subnetWrapper struct {
+		Subnet subnets.Subnet `json:"subnet"`
+	}
+	err = resp.ExtractInto(&subnetWrapper)
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+	route := subnetWrapper.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..7a4f2f7
--- /dev/null
+++ b/openstack/networking/v2/subnets/urls.go
@@ -0,0 +1,31 @@
+package subnets
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/objectstorage/v1/accounts/requests.go
new file mode 100644
index 0000000..b5beef2
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/requests.go
@@ -0,0 +1,100 @@
+package accounts
+
+import "github.com/gophercloud/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) (r GetResult) {
+	h := make(map[string]string)
+	if opts != nil {
+		headers, err := opts.ToAccountGetMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		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 {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// 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) (r UpdateResult) {
+	h := make(map[string]string)
+	if opts != nil {
+		headers, err := opts.ToAccountUpdateMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		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 {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
diff --git a/openstack/objectstorage/v1/accounts/results.go b/openstack/objectstorage/v1/accounts/results.go
new file mode 100644
index 0000000..9bc8340
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/results.go
@@ -0,0 +1,167 @@
+package accounts
+
+import (
+	"encoding/json"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gophercloud/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 int64     `json:"-"`
+	ContentType   string    `json:"Content-Type"`
+	TransID       string    `json:"X-Trans-Id"`
+	Date          time.Time `json:"-"`
+}
+
+func (r *UpdateHeader) UnmarshalJSON(b []byte) error {
+	type tmp UpdateHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = UpdateHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+
+	return err
+}
+
+// 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 (r UpdateResult) Extract() (*UpdateHeader, error) {
+	var s *UpdateHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// GetHeader represents the headers returned in the response from a Get request.
+type GetHeader struct {
+	BytesUsed      int64     `json:"-"`
+	ContainerCount int64     `json:"-"`
+	ContentLength  int64     `json:"-"`
+	ObjectCount    int64     `json:"-"`
+	ContentType    string    `json:"Content-Type"`
+	TransID        string    `json:"X-Trans-Id"`
+	TempURLKey     string    `json:"X-Account-Meta-Temp-URL-Key"`
+	TempURLKey2    string    `json:"X-Account-Meta-Temp-URL-Key-2"`
+	Date           time.Time `json:"-"`
+}
+
+func (r *GetHeader) UnmarshalJSON(b []byte) error {
+	type tmp GetHeader
+	var s struct {
+		tmp
+		BytesUsed      string `json:"X-Account-Bytes-Used"`
+		ContentLength  string `json:"Content-Length"`
+		ContainerCount string `json:"X-Account-Container-Count"`
+		ObjectCount    string `json:"X-Account-Object-Count"`
+		Date           string `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = GetHeader(s.tmp)
+
+	switch s.BytesUsed {
+	case "":
+		r.BytesUsed = 0
+	default:
+		r.BytesUsed, err = strconv.ParseInt(s.BytesUsed, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	switch s.ObjectCount {
+	case "":
+		r.ObjectCount = 0
+	default:
+		r.ObjectCount, err = strconv.ParseInt(s.ObjectCount, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	switch s.ContainerCount {
+	case "":
+		r.ContainerCount = 0
+	default:
+		r.ContainerCount, err = strconv.ParseInt(s.ContainerCount, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	if s.Date != "" {
+		r.Date, err = time.Parse(time.RFC1123, s.Date)
+	}
+
+	return err
+}
+
+// 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 (r GetResult) Extract() (*GetHeader, error) {
+	var s *GetHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *htts.Response)
+// and returns the custom metatdata associated with the account.
+func (r GetResult) ExtractMetadata() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	metadata := make(map[string]string)
+	for k, v := range r.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/testing/doc.go b/openstack/objectstorage/v1/accounts/testing/doc.go
new file mode 100644
index 0000000..b8fdf88
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/testing/doc.go
@@ -0,0 +1,2 @@
+// objectstorage_accounts_v1
+package testing
diff --git a/openstack/objectstorage/v1/accounts/testing/fixtures.go b/openstack/objectstorage/v1/accounts/testing/fixtures.go
new file mode 100644
index 0000000..fff3071
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/testing/fixtures.go
@@ -0,0 +1,39 @@
+package testing
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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-Object-Count", "5")
+		w.Header().Set("X-Account-Bytes-Used", "14")
+		w.Header().Set("X-Account-Meta-Subject", "books")
+		w.Header().Set("Date", "Fri, 17 Jan 2014 16:09:56 GMT")
+
+		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.Header().Set("Date", "Fri, 17 Jan 2014 16:09:56 GMT")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/objectstorage/v1/accounts/testing/requests_test.go b/openstack/objectstorage/v1/accounts/testing/requests_test.go
new file mode 100644
index 0000000..97852f1
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/testing/requests_test.go
@@ -0,0 +1,55 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+var (
+	loc, _ = time.LoadLocation("GMT")
+)
+
+func TestUpdateAccount(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateAccountSuccessfully(t)
+
+	options := &accounts.UpdateOpts{Metadata: map[string]string{"gophercloud-test": "accounts"}}
+	res := accounts.Update(fake.ServiceClient(), options)
+	th.AssertNoErr(t, res.Err)
+
+	expected := &accounts.UpdateHeader{
+		Date: time.Date(2014, time.January, 17, 16, 9, 56, 0, loc), // Fri, 17 Jan 2014 16:09:56 GMT
+	}
+	actual, err := res.Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestGetAccount(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetAccountSuccessfully(t)
+
+	expectedMetadata := map[string]string{"Subject": "books"}
+	res := accounts.Get(fake.ServiceClient(), &accounts.GetOpts{})
+	th.AssertNoErr(t, res.Err)
+	actualMetadata, _ := res.ExtractMetadata()
+	th.CheckDeepEquals(t, expectedMetadata, actualMetadata)
+	_, err := res.Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &accounts.GetHeader{
+		ContainerCount: 2,
+		ObjectCount:    5,
+		BytesUsed:      14,
+		Date:           time.Date(2014, time.January, 17, 16, 9, 56, 0, loc), // Fri, 17 Jan 2014 16:09:56 GMT
+	}
+	actual, err := res.Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/accounts/urls.go b/openstack/objectstorage/v1/accounts/urls.go
new file mode 100644
index 0000000..71540b1
--- /dev/null
+++ b/openstack/objectstorage/v1/accounts/urls.go
@@ -0,0 +1,11 @@
+package accounts
+
+import "github.com/gophercloud/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/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/requests.go b/openstack/objectstorage/v1/containers/requests.go
new file mode 100644
index 0000000..a668673
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/requests.go
@@ -0,0 +1,191 @@
+package containers
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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)
+	return opts.Full, q.String(), err
+}
+
+// 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"}
+		}
+	}
+
+	pager := pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		p := ContainerPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	})
+	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) (r CreateResult) {
+	h := make(map[string]string)
+	if opts != nil {
+		headers, err := opts.ToContainerCreateMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		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 {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *gophercloud.ServiceClient, containerName string) (r DeleteResult) {
+	_, r.Err = c.Delete(deleteURL(c, containerName), nil)
+	return
+}
+
+// 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) (r UpdateResult) {
+	h := make(map[string]string)
+	if opts != nil {
+		headers, err := opts.ToContainerUpdateMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+
+		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 {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// 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) (r GetResult) {
+	resp, err := c.Request("HEAD", getURL(c, containerName), &gophercloud.RequestOpts{
+		OkCodes: []int{200, 204},
+	})
+	if resp != nil {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
new file mode 100644
index 0000000..8c11b8c
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -0,0 +1,343 @@
+package containers
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Container represents a container resource.
+type Container struct {
+	// The total number of bytes stored in the container.
+	Bytes int64 `json:"bytes"`
+
+	// The total number of objects stored in the container.
+	Count int64 `json:"count"`
+
+	// The name of the container.
+	Name string `json:"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)
+	return len(names) == 0, err
+}
+
+// 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(r pagination.Page) ([]Container, error) {
+	var s []Container
+	err := (r.(ContainerPage)).ExtractInto(&s)
+	return s, err
+}
+
+// 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    `json:"Accept-Ranges"`
+	BytesUsed        int64     `json:"-"`
+	ContentLength    int64     `json:"-"`
+	ContentType      string    `json:"Content-Type"`
+	Date             time.Time `json:"-"`
+	ObjectCount      int64     `json:"-"`
+	Read             []string  `json:"-"`
+	TransID          string    `json:"X-Trans-Id"`
+	VersionsLocation string    `json:"X-Versions-Location"`
+	Write            []string  `json:"-"`
+}
+
+func (r *GetHeader) UnmarshalJSON(b []byte) error {
+	type tmp GetHeader
+	var s struct {
+		tmp
+		BytesUsed     string                  `json:"X-Container-Bytes-Used"`
+		ContentLength string                  `json:"Content-Length"`
+		ObjectCount   string                  `json:"X-Container-Object-Count"`
+		Write         string                  `json:"X-Container-Write"`
+		Read          string                  `json:"X-Container-Read"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = GetHeader(s.tmp)
+
+	switch s.BytesUsed {
+	case "":
+		r.BytesUsed = 0
+	default:
+		r.BytesUsed, err = strconv.ParseInt(s.BytesUsed, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	switch s.ObjectCount {
+	case "":
+		r.ObjectCount = 0
+	default:
+		r.ObjectCount, err = strconv.ParseInt(s.ObjectCount, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Read = strings.Split(s.Read, ",")
+	r.Write = strings.Split(s.Write, ",")
+
+	r.Date = time.Time(s.Date)
+
+	return err
+}
+
+// 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 (r GetResult) Extract() (*GetHeader, error) {
+	var s *GetHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *stts.Response)
+// and returns the custom metadata associated with the container.
+func (r GetResult) ExtractMetadata() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	metadata := make(map[string]string)
+	for k, v := range r.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     `json:"-"`
+	ContentType   string    `json:"Content-Type"`
+	Date          time.Time `json:"-"`
+	TransID       string    `json:"X-Trans-Id"`
+}
+
+func (r *CreateHeader) UnmarshalJSON(b []byte) error {
+	type tmp CreateHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = CreateHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+
+	return err
+}
+
+// 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 (r CreateResult) Extract() (*CreateHeader, error) {
+	var s *CreateHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// UpdateHeader represents the headers returned in the response from a Update request.
+type UpdateHeader struct {
+	ContentLength int64     `json:"-"`
+	ContentType   string    `json:"Content-Type"`
+	Date          time.Time `json:"-"`
+	TransID       string    `json:"X-Trans-Id"`
+}
+
+func (r *UpdateHeader) UnmarshalJSON(b []byte) error {
+	type tmp UpdateHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = UpdateHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+
+	return err
+}
+
+// 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 (r UpdateResult) Extract() (*UpdateHeader, error) {
+	var s *UpdateHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// DeleteHeader represents the headers returned in the response from a Delete request.
+type DeleteHeader struct {
+	ContentLength int64     `json:"-"`
+	ContentType   string    `json:"Content-Type"`
+	Date          time.Time `json:"-"`
+	TransID       string    `json:"X-Trans-Id"`
+}
+
+func (r *DeleteHeader) UnmarshalJSON(b []byte) error {
+	type tmp DeleteHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = DeleteHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+
+	return err
+}
+
+// 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 (r DeleteResult) Extract() (*DeleteHeader, error) {
+	var s *DeleteHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/objectstorage/v1/containers/testing/doc.go b/openstack/objectstorage/v1/containers/testing/doc.go
new file mode 100644
index 0000000..c27fa49
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/testing/doc.go
@@ -0,0 +1,2 @@
+// objectstorage_containers_v1
+package testing
diff --git a/openstack/objectstorage/v1/containers/testing/fixtures.go b/openstack/objectstorage/v1/containers/testing/fixtures.go
new file mode 100644
index 0000000..b68230a
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/testing/fixtures.go
@@ -0,0 +1,154 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ExpectedListInfo is the result expected from a call to `List` when full
+// info is requested.
+var ExpectedListInfo = []containers.Container{
+	{
+		Count: 0,
+		Bytes: 0,
+		Name:  "janeausten",
+	},
+	{
+		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().Set("Content-Length", "0")
+		w.Header().Set("Content-Type", "text/html; charset=UTF-8")
+		w.Header().Set("Date", "Wed, 17 Aug 2016 19:25:43 GMT")
+		w.Header().Set("X-Trans-Id", "tx554ed59667a64c61866f1-0058b4ba37")
+		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.Header().Set("Accept-Ranges", "bytes")
+		w.Header().Set("Content-Type", "application/json; charset=utf-8")
+		w.Header().Set("Date", "Wed, 17 Aug 2016 19:25:43 GMT")
+		w.Header().Set("X-Container-Bytes-Used", "100")
+		w.Header().Set("X-Container-Object-Count", "4")
+		w.Header().Set("X-Container-Read", "test")
+		w.Header().Set("X-Container-Write", "test2,user4")
+		w.Header().Set("X-Timestamp", "1471298837.95721")
+		w.Header().Set("X-Trans-Id", "tx554ed59667a64c61866f1-0057b4ba37")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/objectstorage/v1/containers/testing/requests_test.go b/openstack/objectstorage/v1/containers/testing/requests_test.go
new file mode 100644
index 0000000..bb0c784
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/testing/requests_test.go
@@ -0,0 +1,144 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+var (
+	metadata = map[string]string{"gophercloud-test": "containers"}
+	loc, _   = time.LoadLocation("GMT")
+)
+
+func TestListContainerInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListContainerInfoSuccessfully(t)
+
+	count := 0
+	err := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := containers.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 := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: true}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := containers.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 := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := containers.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 := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: false}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := containers.ExtractNames(allPages)
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, ExpectedListNames, actual)
+}
+
+func TestCreateContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateContainerSuccessfully(t)
+
+	options := containers.CreateOpts{ContentType: "application/json", Metadata: map[string]string{"foo": "bar"}}
+	res := containers.Create(fake.ServiceClient(), "testContainer", options)
+	th.CheckEquals(t, "bar", res.Header["X-Container-Meta-Foo"][0])
+
+	expected := &containers.CreateHeader{
+		ContentLength: 0,
+		ContentType:   "text/html; charset=UTF-8",
+		Date:          time.Date(2016, time.August, 17, 19, 25, 43, 0, loc), //Wed, 17 Aug 2016 19:25:43 GMT
+		TransID:       "tx554ed59667a64c61866f1-0058b4ba37",
+	}
+	actual, err := res.Extract()
+	th.CheckNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteContainerSuccessfully(t)
+
+	res := containers.Delete(fake.ServiceClient(), "testContainer")
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestUpateContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateContainerSuccessfully(t)
+
+	options := &containers.UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
+	res := containers.Update(fake.ServiceClient(), "testContainer", options)
+	th.CheckNoErr(t, res.Err)
+}
+
+func TestGetContainer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetContainerSuccessfully(t)
+
+	res := containers.Get(fake.ServiceClient(), "testContainer")
+	_, err := res.ExtractMetadata()
+	th.CheckNoErr(t, err)
+
+	expected := &containers.GetHeader{
+		AcceptRanges: "bytes",
+		BytesUsed:    100,
+		ContentType:  "application/json; charset=utf-8",
+		Date:         time.Date(2016, time.August, 17, 19, 25, 43, 0, loc), //Wed, 17 Aug 2016 19:25:43 GMT
+		ObjectCount:  4,
+		Read:         []string{"test"},
+		TransID:      "tx554ed59667a64c61866f1-0057b4ba37",
+		Write:        []string{"test2", "user4"},
+	}
+	actual, err := res.Extract()
+	th.CheckNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/containers/urls.go b/openstack/objectstorage/v1/containers/urls.go
new file mode 100644
index 0000000..9b38047
--- /dev/null
+++ b/openstack/objectstorage/v1/containers/urls.go
@@ -0,0 +1,23 @@
+package containers
+
+import "github.com/gophercloud/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/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/errors.go b/openstack/objectstorage/v1/objects/errors.go
new file mode 100644
index 0000000..5c4ae44
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/errors.go
@@ -0,0 +1,13 @@
+package objects
+
+import "github.com/gophercloud/gophercloud"
+
+// ErrWrongChecksum is the error when the checksum generated for an object
+// doesn't match the ETAG header.
+type ErrWrongChecksum struct {
+	gophercloud.BaseError
+}
+
+func (e ErrWrongChecksum) Error() string {
+	return "Local checksum does not match API ETag header"
+}
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
new file mode 100644
index 0000000..0ab5e17
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -0,0 +1,453 @@
+package objects
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"crypto/md5"
+	"crypto/sha1"
+	"fmt"
+	"io"
+	"strings"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts"
+	"github.com/gophercloud/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)
+	return opts.Full, q.String(), err
+}
+
+// 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"}
+		}
+	}
+
+	pager := pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		p := ObjectPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	})
+	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) (r DownloadResult) {
+	url := downloadURL(c, containerName, objectName)
+	h := make(map[string]string)
+	if opts != nil {
+		headers, query, err := opts.ToObjectDownloadParams()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		for k, v := range headers {
+			h[k] = v
+		}
+		url += query
+	}
+
+	resp, err := c.Get(url, nil, &gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{200, 304},
+	})
+	if resp != nil {
+		r.Header = resp.Header
+		r.Body = resp.Body
+	}
+	r.Err = err
+	return
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToObjectCreateParams() (io.Reader, map[string]string, string, error)
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts struct {
+	Content            io.Reader
+	Metadata           map[string]string
+	CacheControl       string `h:"Cache-Control"`
+	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() (io.Reader, map[string]string, string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return nil, nil, "", err
+	}
+	h, err := gophercloud.BuildHeaders(opts)
+	if err != nil {
+		return nil, nil, "", err
+	}
+
+	for k, v := range opts.Metadata {
+		h["X-Object-Meta-"+k] = v
+	}
+
+	hash := md5.New()
+	buf := bytes.NewBuffer([]byte{})
+	_, err = io.Copy(io.MultiWriter(hash, buf), opts.Content)
+	if err != nil {
+		return nil, nil, "", err
+	}
+	localChecksum := fmt.Sprintf("%x", hash.Sum(nil))
+	h["ETag"] = localChecksum
+
+	return buf, 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, opts CreateOptsBuilder) (r CreateResult) {
+	url := createURL(c, containerName, objectName)
+	h := make(map[string]string)
+	var b io.Reader
+	if opts != nil {
+		tmpB, headers, query, err := opts.ToObjectCreateParams()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		for k, v := range headers {
+			h[k] = v
+		}
+		url += query
+		b = tmpB
+	}
+
+	resp, err := c.Put(url, nil, nil, &gophercloud.RequestOpts{
+		RawBody:     b,
+		MoreHeaders: h,
+	})
+	r.Err = err
+	if resp != nil {
+		r.Header = resp.Header
+	}
+	return
+}
+
+// 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:"true"`
+}
+
+// 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
+	}
+	return h, nil
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOptsBuilder) (r CopyResult) {
+	h := make(map[string]string)
+	headers, err := opts.ToObjectCopyMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	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 {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// 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)
+	return q.String(), err
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOptsBuilder) (r DeleteResult) {
+	url := deleteURL(c, containerName, objectName)
+	if opts != nil {
+		query, err := opts.ToObjectDeleteQuery()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		url += query
+	}
+	resp, err := c.Delete(url, nil)
+	if resp != nil {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// 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)
+	return q.String(), err
+}
+
+// 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) (r GetResult) {
+	url := getURL(c, containerName, objectName)
+	if opts != nil {
+		query, err := opts.ToObjectGetQuery()
+		if err != nil {
+			r.Err = err
+			return
+		}
+		url += query
+	}
+	resp, err := c.Request("HEAD", url, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 204},
+	})
+	if resp != nil {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// 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) (r UpdateResult) {
+	h := make(map[string]string)
+	if opts != nil {
+		headers, err := opts.ToObjectUpdateMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+	url := updateURL(c, containerName, objectName)
+	resp, err := c.Post(url, nil, nil, &gophercloud.RequestOpts{
+		MoreHeaders: h,
+	})
+	if resp != nil {
+		r.Header = resp.Header
+	}
+	r.Err = err
+	return
+}
+
+// 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/results.go b/openstack/objectstorage/v1/objects/results.go
new file mode 100644
index 0000000..0dcdbe2
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -0,0 +1,493 @@
+package objects
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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"`
+
+	// ContentType is the content type of the object.
+	ContentType string `json:"content_type"`
+
+	// Hash represents the MD5 checksum value of the object's content.
+	Hash string `json:"hash"`
+
+	// LastModified is the time the object was last modified, represented
+	// as a string.
+	LastModified time.Time `json:"-"`
+
+	// Name is the unique name for the object.
+	Name string `json:"name"`
+}
+
+func (r *Object) UnmarshalJSON(b []byte) error {
+	type tmp Object
+	var s *struct {
+		tmp
+		LastModified gophercloud.JSONRFC3339MilliNoZ `json:"last_modified"`
+	}
+
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Object(s.tmp)
+
+	r.LastModified = time.Time(s.LastModified)
+
+	return nil
+
+}
+
+// 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)
+	return len(names) == 0, err
+}
+
+// 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(r pagination.Page) ([]Object, error) {
+	var s []Object
+	err := (r.(ObjectPage)).ExtractInto(&s)
+	return s, err
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(r pagination.Page) ([]string, error) {
+	casted := r.(ObjectPage)
+	ct := casted.Header.Get("Content-Type")
+	switch {
+	case strings.HasPrefix(ct, "application/json"):
+		parsed, err := ExtractInfo(r)
+		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(r.(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    `json:"Accept-Ranges"`
+	ContentDisposition string    `json:"Content-Disposition"`
+	ContentEncoding    string    `json:"Content-Encoding"`
+	ContentLength      int64     `json:"-"`
+	ContentType        string    `json:"Content-Type"`
+	Date               time.Time `json:"-"`
+	DeleteAt           time.Time `json:"-"`
+	ETag               string    `json:"Etag"`
+	LastModified       time.Time `json:"-"`
+	ObjectManifest     string    `json:"X-Object-Manifest"`
+	StaticLargeObject  bool      `json:"X-Static-Large-Object"`
+	TransID            string    `json:"X-Trans-Id"`
+}
+
+func (r *DownloadHeader) UnmarshalJSON(b []byte) error {
+	type tmp DownloadHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+		DeleteAt      gophercloud.JSONUnix    `json:"X-Delete-At"`
+		LastModified  gophercloud.JSONRFC1123 `json:"Last-Modified"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = DownloadHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+	r.DeleteAt = time.Time(s.DeleteAt)
+	r.LastModified = time.Time(s.LastModified)
+
+	return nil
+}
+
+// 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 (r DownloadResult) Extract() (*DownloadHeader, error) {
+	var s *DownloadHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// 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 (r *DownloadResult) ExtractContent() ([]byte, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	defer r.Body.Close()
+	body, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		return nil, err
+	}
+	r.Body.Close()
+	return body, nil
+}
+
+// GetHeader represents the headers returned in the response from a Get request.
+type GetHeader struct {
+	ContentDisposition string    `json:"Content-Disposition"`
+	ContentEncoding    string    `json:"Content-Encoding"`
+	ContentLength      int64     `json:"-"`
+	ContentType        string    `json:"Content-Type"`
+	Date               time.Time `json:"-"`
+	DeleteAt           time.Time `json:"-"`
+	ETag               string    `json:"Etag"`
+	LastModified       time.Time `json:"-"`
+	ObjectManifest     string    `json:"X-Object-Manifest"`
+	StaticLargeObject  bool      `json:"X-Static-Large-Object"`
+	TransID            string    `json:"X-Trans-Id"`
+}
+
+func (r *GetHeader) UnmarshalJSON(b []byte) error {
+	type tmp GetHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+		DeleteAt      gophercloud.JSONUnix    `json:"X-Delete-At"`
+		LastModified  gophercloud.JSONRFC1123 `json:"Last-Modified"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = GetHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+	r.DeleteAt = time.Time(s.DeleteAt)
+	r.LastModified = time.Time(s.LastModified)
+
+	return nil
+}
+
+// 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 (r GetResult) Extract() (*GetHeader, error) {
+	var s *GetHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func (r GetResult) ExtractMetadata() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+	metadata := make(map[string]string)
+	for k, v := range r.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     `json:"-"`
+	ContentType   string    `json:"Content-Type"`
+	Date          time.Time `json:"-"`
+	ETag          string    `json:"Etag"`
+	LastModified  time.Time `json:"-"`
+	TransID       string    `json:"X-Trans-Id"`
+}
+
+func (r *CreateHeader) UnmarshalJSON(b []byte) error {
+	type tmp CreateHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+		LastModified  gophercloud.JSONRFC1123 `json:"Last-Modified"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = CreateHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+	r.LastModified = time.Time(s.LastModified)
+
+	return nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	checksum string
+	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 (r CreateResult) Extract() (*CreateHeader, error) {
+	//if r.Header.Get("ETag") != fmt.Sprintf("%x", localChecksum) {
+	//	return nil, ErrWrongChecksum{}
+	//}
+	var s *CreateHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// UpdateHeader represents the headers returned in the response from a Update request.
+type UpdateHeader struct {
+	ContentLength int64     `json:"-"`
+	ContentType   string    `json:"Content-Type"`
+	Date          time.Time `json:"-"`
+	TransID       string    `json:"X-Trans-Id"`
+}
+
+func (r *UpdateHeader) UnmarshalJSON(b []byte) error {
+	type tmp UpdateHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = UpdateHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+
+	return nil
+}
+
+// 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 (r UpdateResult) Extract() (*UpdateHeader, error) {
+	var s *UpdateHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// DeleteHeader represents the headers returned in the response from a Delete request.
+type DeleteHeader struct {
+	ContentLength int64     `json:"Content-Length"`
+	ContentType   string    `json:"Content-Type"`
+	Date          time.Time `json:"-"`
+	TransID       string    `json:"X-Trans-Id"`
+}
+
+func (r *DeleteHeader) UnmarshalJSON(b []byte) error {
+	type tmp DeleteHeader
+	var s struct {
+		tmp
+		ContentLength string                  `json:"Content-Length"`
+		Date          gophercloud.JSONRFC1123 `json:"Date"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = DeleteHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+
+	return nil
+}
+
+// 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 (r DeleteResult) Extract() (*DeleteHeader, error) {
+	var s *DeleteHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// CopyHeader represents the headers returned in the response from a Copy request.
+type CopyHeader struct {
+	ContentLength          int64     `json:"-"`
+	ContentType            string    `json:"Content-Type"`
+	CopiedFrom             string    `json:"X-Copied-From"`
+	CopiedFromLastModified time.Time `json:"-"`
+	Date                   time.Time `json:"-"`
+	ETag                   string    `json:"Etag"`
+	LastModified           time.Time `json:"-"`
+	TransID                string    `json:"X-Trans-Id"`
+}
+
+func (r *CopyHeader) UnmarshalJSON(b []byte) error {
+	type tmp CopyHeader
+	var s struct {
+		tmp
+		ContentLength          string                  `json:"Content-Length"`
+		CopiedFromLastModified gophercloud.JSONRFC1123 `json:"X-Copied-From-Last-Modified"`
+		Date                   gophercloud.JSONRFC1123 `json:"Date"`
+		LastModified           gophercloud.JSONRFC1123 `json:"Last-Modified"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = CopyHeader(s.tmp)
+
+	switch s.ContentLength {
+	case "":
+		r.ContentLength = 0
+	default:
+		r.ContentLength, err = strconv.ParseInt(s.ContentLength, 10, 64)
+		if err != nil {
+			return err
+		}
+	}
+
+	r.Date = time.Time(s.Date)
+	r.CopiedFromLastModified = time.Time(s.CopiedFromLastModified)
+	r.LastModified = time.Time(s.LastModified)
+
+	return nil
+}
+
+// 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 (r CopyResult) Extract() (*CopyHeader, error) {
+	var s *CopyHeader
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/objectstorage/v1/objects/testing/doc.go b/openstack/objectstorage/v1/objects/testing/doc.go
new file mode 100644
index 0000000..f008a80
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/testing/doc.go
@@ -0,0 +1,2 @@
+// objectstorage_objects_v1
+package testing
diff --git a/openstack/objectstorage/v1/objects/testing/fixtures.go b/openstack/objectstorage/v1/objects/testing/fixtures.go
new file mode 100644
index 0000000..08faab8
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/testing/fixtures.go
@@ -0,0 +1,214 @@
+package testing
+
+import (
+	"crypto/md5"
+	"fmt"
+	"io"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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.Header().Set("Date", "Wed, 10 Nov 2009 23:00:00 GMT")
+		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 = []objects.Object{
+	{
+		Hash:         "451e372e48e0f6b1114fa0724aa79fa1",
+		LastModified: time.Date(2016, time.August, 17, 22, 11, 58, 602650000, time.UTC), //"2016-08-17T22:11:58.602650"
+		Bytes:        14,
+		Name:         "goodbye",
+		ContentType:  "application/octet-stream",
+	},
+	{
+		Hash:         "451e372e48e0f6b1114fa0724aa79fa1",
+		LastModified: time.Date(2016, time.August, 17, 22, 11, 58, 602650000, time.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": "2016-08-17T22:11:58.602650",
+        "bytes": 14,
+        "name": "goodbye",
+        "content_type": "application/octet-stream"
+      },
+      {
+        "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+        "last_modified": "2016-08-17T22:11:58.602650",
+        "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)
+	})
+}
+
+// HandleCreateTextWithCacheControlSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
+// mux that responds with a `Create` response. A Cache-Control of `max-age="3600", public` is expected.
+func HandleCreateTextWithCacheControlSuccessfully(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, "Cache-Control", `max-age="3600", public`)
+		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/testing/requests_test.go b/openstack/objectstorage/v1/objects/testing/requests_test.go
new file mode 100644
index 0000000..4f26632
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/testing/requests_test.go
@@ -0,0 +1,196 @@
+package testing
+
+import (
+	"bytes"
+	"io"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+var (
+	loc, _ = time.LoadLocation("GMT")
+)
+
+func TestDownloadReader(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDownloadObjectSuccessfully(t)
+
+	response := objects.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 := objects.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))
+
+	expected := &objects.DownloadHeader{
+		ContentLength: 36,
+		ContentType:   "text/plain; charset=utf-8",
+		Date:          time.Date(2009, time.November, 10, 23, 0, 0, 0, loc),
+	}
+	actual, err := response.Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestListObjectInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListObjectsInfoSuccessfully(t)
+
+	count := 0
+	options := &objects.ListOpts{Full: true}
+	err := objects.List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := objects.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 := &objects.ListOpts{Full: false}
+	err := objects.List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := objects.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 := &objects.CreateOpts{ContentType: "text/plain", Content: strings.NewReader(content)}
+	res := objects.Create(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateObjectWithCacheControl(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	content := "All mimsy were the borogoves"
+
+	HandleCreateTextWithCacheControlSuccessfully(t, content)
+
+	options := &objects.CreateOpts{
+		CacheControl: `max-age="3600", public`,
+		Content:      strings.NewReader(content),
+	}
+	res := objects.Create(fake.ServiceClient(), "testContainer", "testObject", 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 := objects.Create(fake.ServiceClient(), "testContainer", "testObject", &objects.CreateOpts{Content: strings.NewReader(content)})
+	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", &CreateOpts{Content: content})
+
+	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 := &objects.CopyOpts{Destination: "/newTestContainer/newTestObject"}
+	res := objects.Copy(fake.ServiceClient(), "testContainer", "testObject", options)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestDeleteObject(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDeleteObjectSuccessfully(t)
+
+	res := objects.Delete(fake.ServiceClient(), "testContainer", "testObject", nil)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestUpateObjectMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateObjectSuccessfully(t)
+
+	options := &objects.UpdateOpts{Metadata: map[string]string{"Gophercloud-Test": "objects"}}
+	res := objects.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 := objects.Get(fake.ServiceClient(), "testContainer", "testObject", nil).ExtractMetadata()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/objects/urls.go b/openstack/objectstorage/v1/objects/urls.go
new file mode 100644
index 0000000..b3ac304
--- /dev/null
+++ b/openstack/objectstorage/v1/objects/urls.go
@@ -0,0 +1,33 @@
+package objects
+
+import (
+	"github.com/gophercloud/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/swauth/requests.go b/openstack/objectstorage/v1/swauth/requests.go
new file mode 100644
index 0000000..e8589ae
--- /dev/null
+++ b/openstack/objectstorage/v1/swauth/requests.go
@@ -0,0 +1,70 @@
+package swauth
+
+import "github.com/gophercloud/gophercloud"
+
+// AuthOptsBuilder describes struct types that can be accepted by the Auth call.
+// The AuthOpts struct in this package does.
+type AuthOptsBuilder interface {
+	ToAuthOptsMap() (map[string]string, error)
+}
+
+// AuthOpts specifies an authentication request.
+type AuthOpts struct {
+	// User is an Swauth-based username in username:tenant format.
+	User string `h:"X-Auth-User" required:"true"`
+	// Key is a secret/password to authenticate the User with.
+	Key string `h:"X-Auth-Key" required:"true"`
+}
+
+// ToAuthOptsMap formats an AuthOpts structure into a request body.
+func (opts AuthOpts) ToAuthOptsMap() (map[string]string, error) {
+	return gophercloud.BuildHeaders(opts)
+}
+
+// Auth performs an authentication request for a Swauth-based user.
+func Auth(c *gophercloud.ProviderClient, opts AuthOptsBuilder) (r GetAuthResult) {
+	h := make(map[string]string)
+
+	if opts != nil {
+		headers, err := opts.ToAuthOptsMap()
+		if err != nil {
+			r.Err = err
+			return
+		}
+
+		for k, v := range headers {
+			h[k] = v
+		}
+	}
+
+	resp, err := c.Request("GET", getURL(c), &gophercloud.RequestOpts{
+		MoreHeaders: h,
+		OkCodes:     []int{200},
+	})
+
+	if resp != nil {
+		r.Header = resp.Header
+	}
+
+	r.Err = err
+
+	return r
+}
+
+// NewObjectStorageV1 creates a Swauth-authenticated *gophercloud.ServiceClient
+// client that can issue ObjectStorage-based API calls.
+func NewObjectStorageV1(pc *gophercloud.ProviderClient, authOpts AuthOpts) (*gophercloud.ServiceClient, error) {
+	auth, err := Auth(pc, authOpts).Extract()
+	if err != nil {
+		return nil, err
+	}
+
+	swiftClient := &gophercloud.ServiceClient{
+		ProviderClient: pc,
+		Endpoint:       gophercloud.NormalizeURL(auth.StorageURL),
+	}
+
+	swiftClient.TokenID = auth.Token
+
+	return swiftClient, nil
+}
diff --git a/openstack/objectstorage/v1/swauth/results.go b/openstack/objectstorage/v1/swauth/results.go
new file mode 100644
index 0000000..294c43c
--- /dev/null
+++ b/openstack/objectstorage/v1/swauth/results.go
@@ -0,0 +1,27 @@
+package swauth
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// GetAuthResult temporarily contains the response from a Swauth
+// authentication call.
+type GetAuthResult struct {
+	gophercloud.HeaderResult
+}
+
+// AuthResult contains the authentication information from a Swauth
+// authentication request.
+type AuthResult struct {
+	Token      string `json:"X-Auth-Token"`
+	StorageURL string `json:"X-Storage-Url"`
+	CDNURL     string `json:"X-CDN-Management-Url"`
+}
+
+// Extract is a method that attempts to interpret any Swauth authentication
+// response as a AuthResult struct.
+func (r GetAuthResult) Extract() (*AuthResult, error) {
+	var s *AuthResult
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/objectstorage/v1/swauth/testing/doc.go b/openstack/objectstorage/v1/swauth/testing/doc.go
new file mode 100644
index 0000000..ff3bf37
--- /dev/null
+++ b/openstack/objectstorage/v1/swauth/testing/doc.go
@@ -0,0 +1,2 @@
+// objectstorage_swauth_v1
+package testing
diff --git a/openstack/objectstorage/v1/swauth/testing/fixtures.go b/openstack/objectstorage/v1/swauth/testing/fixtures.go
new file mode 100644
index 0000000..79858f5
--- /dev/null
+++ b/openstack/objectstorage/v1/swauth/testing/fixtures.go
@@ -0,0 +1,29 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+// AuthResult is the expected result of AuthOutput
+var AuthResult = swauth.AuthResult{
+	Token:      "AUTH_tk6223e6071f8f4299aa334b48015484a1",
+	StorageURL: "http://127.0.0.1:8080/v1/AUTH_test/",
+}
+
+// HandleAuthSuccessfully configures the test server to respond to an Auth request.
+func HandleAuthSuccessfully(t *testing.T, authOpts swauth.AuthOpts) {
+	th.Mux.HandleFunc("/auth/v1.0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-User", authOpts.User)
+		th.TestHeader(t, r, "X-Auth-Key", authOpts.Key)
+
+		w.Header().Add("X-Auth-Token", AuthResult.Token)
+		w.Header().Add("X-Storage-Url", AuthResult.StorageURL)
+		fmt.Fprintf(w, "")
+	})
+}
diff --git a/openstack/objectstorage/v1/swauth/testing/requests_test.go b/openstack/objectstorage/v1/swauth/testing/requests_test.go
new file mode 100644
index 0000000..57b5034
--- /dev/null
+++ b/openstack/objectstorage/v1/swauth/testing/requests_test.go
@@ -0,0 +1,27 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack"
+	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/swauth"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestAuth(t *testing.T) {
+	authOpts := swauth.AuthOpts{
+		User: "test:tester",
+		Key:  "testing",
+	}
+
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAuthSuccessfully(t, authOpts)
+
+	providerClient, err := openstack.NewClient(th.Endpoint())
+	th.AssertNoErr(t, err)
+
+	swiftClient, err := swauth.NewObjectStorageV1(providerClient, authOpts)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, swiftClient.TokenID, AuthResult.Token)
+}
diff --git a/openstack/objectstorage/v1/swauth/urls.go b/openstack/objectstorage/v1/swauth/urls.go
new file mode 100644
index 0000000..a30cabd
--- /dev/null
+++ b/openstack/objectstorage/v1/swauth/urls.go
@@ -0,0 +1,7 @@
+package swauth
+
+import "github.com/gophercloud/gophercloud"
+
+func getURL(c *gophercloud.ProviderClient) string {
+	return c.IdentityBase + "auth/v1.0"
+}
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..ff383cf
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/requests.go
@@ -0,0 +1,13 @@
+package apiversions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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/results.go b/openstack/orchestration/v1/apiversions/results.go
new file mode 100644
index 0000000..a7c22a2
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/results.go
@@ -0,0 +1,36 @@
+package apiversions
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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             `json:"status"`
+	ID     string             `json:"id"`
+	Links  []gophercloud.Link `json:"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)
+	return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]APIVersion, error) {
+	var s struct {
+		APIVersions []APIVersion `json:"versions"`
+	}
+	err := (r.(APIVersionPage)).ExtractInto(&s)
+	return s.APIVersions, err
+}
diff --git a/openstack/orchestration/v1/apiversions/testing/doc.go b/openstack/orchestration/v1/apiversions/testing/doc.go
new file mode 100644
index 0000000..3d545fd
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/testing/doc.go
@@ -0,0 +1,2 @@
+// orchestration_apiversions_v1
+package testing
diff --git a/openstack/orchestration/v1/apiversions/testing/requests_test.go b/openstack/orchestration/v1/apiversions/testing/requests_test.go
new file mode 100644
index 0000000..ac59b6c
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/testing/requests_test.go
@@ -0,0 +1,90 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/apiversions"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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
+
+	apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := apiversions.ExtractAPIVersions(page)
+		if err != nil {
+			t.Errorf("Failed to extract API versions: %v", err)
+			return false, err
+		}
+
+		expected := []apiversions.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)
+	})
+
+	apiversions.ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		if _, err := apiversions.ExtractAPIVersions(page); err == nil {
+			t.Fatalf("Expected error, got nil")
+		}
+		return true, nil
+	})
+}
diff --git a/openstack/orchestration/v1/apiversions/urls.go b/openstack/orchestration/v1/apiversions/urls.go
new file mode 100644
index 0000000..0205405
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/urls.go
@@ -0,0 +1,7 @@
+package apiversions
+
+import "github.com/gophercloud/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/requests.go b/openstack/orchestration/v1/buildinfo/requests.go
new file mode 100644
index 0000000..32f6032
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/requests.go
@@ -0,0 +1,9 @@
+package buildinfo
+
+import "github.com/gophercloud/gophercloud"
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient) (r GetResult) {
+	_, r.Err = c.Get(getURL(c), &r.Body, nil)
+	return
+}
diff --git a/openstack/orchestration/v1/buildinfo/results.go b/openstack/orchestration/v1/buildinfo/results.go
new file mode 100644
index 0000000..c3d2cdb
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/results.go
@@ -0,0 +1,29 @@
+package buildinfo
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// Revision represents the API/Engine revision of a Heat deployment.
+type Revision struct {
+	Revision string `json:"revision"`
+}
+
+// BuildInfo represents the build information for a Heat deployment.
+type BuildInfo struct {
+	API    Revision `json:"api"`
+	Engine Revision `json:"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) {
+	var s *BuildInfo
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/orchestration/v1/buildinfo/testing/doc.go b/openstack/orchestration/v1/buildinfo/testing/doc.go
new file mode 100644
index 0000000..3655974
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/testing/doc.go
@@ -0,0 +1,2 @@
+// orchestration_buildinfo_v1
+package testing
diff --git a/openstack/orchestration/v1/buildinfo/testing/fixtures.go b/openstack/orchestration/v1/buildinfo/testing/fixtures.go
new file mode 100644
index 0000000..c240d5f
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/testing/fixtures.go
@@ -0,0 +1,46 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &buildinfo.BuildInfo{
+	API: buildinfo.Revision{
+		Revision: "2.4.5",
+	},
+	Engine: buildinfo.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/testing/requests_test.go b/openstack/orchestration/v1/buildinfo/testing/requests_test.go
new file mode 100644
index 0000000..bd2e164
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/testing/requests_test.go
@@ -0,0 +1,21 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/buildinfo"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := buildinfo.Get(fake.ServiceClient()).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := GetExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/buildinfo/urls.go b/openstack/orchestration/v1/buildinfo/urls.go
new file mode 100644
index 0000000..28a2128
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/urls.go
@@ -0,0 +1,7 @@
+package buildinfo
+
+import "github.com/gophercloud/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/requests.go b/openstack/orchestration/v1/stackevents/requests.go
new file mode 100644
index 0000000..e6e7f79
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/requests.go
@@ -0,0 +1,182 @@
+package stackevents
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Find retrieves stack events for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) (r FindResult) {
+	_, r.Err = c.Get(findURL(c, stackName), &r.Body, nil)
+	return
+}
+
+// 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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		p := EventPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	})
+}
+
+// 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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		p := EventPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	})
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, stackName, stackID, resourceName, eventID), &r.Body, nil)
+	return
+}
diff --git a/openstack/orchestration/v1/stackevents/results.go b/openstack/orchestration/v1/stackevents/results.go
new file mode 100644
index 0000000..46fb0ff
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/results.go
@@ -0,0 +1,119 @@
+package stackevents
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Event represents a stack event.
+type Event struct {
+	// The name of the resource for which the event occurred.
+	ResourceName string `json:"resource_name"`
+	// The time the event occurred.
+	Time time.Time `json:"-"`
+	// The URLs to the event.
+	Links []gophercloud.Link `json:"links"`
+	// The logical ID of the stack resource.
+	LogicalResourceID string `json:"logical_resource_id"`
+	// The reason of the status of the event.
+	ResourceStatusReason string `json:"resource_status_reason"`
+	// The status of the event.
+	ResourceStatus string `json:"resource_status"`
+	// The physical ID of the stack resource.
+	PhysicalResourceID string `json:"physical_resource_id"`
+	// The event ID.
+	ID string `json:"id"`
+	// Properties of the stack resource.
+	ResourceProperties map[string]interface{} `json:"resource_properties"`
+}
+
+func (r *Event) UnmarshalJSON(b []byte) error {
+	type tmp Event
+	var s struct {
+		tmp
+		Time gophercloud.JSONRFC3339NoZ `json:"event_time"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Event(s.tmp)
+
+	r.Time = time.Time(s.Time)
+
+	return nil
+}
+
+// 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) {
+	var s struct {
+		Events []Event `json:"events"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Events, err
+}
+
+// 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)
+	return len(events) == 0, err
+}
+
+// 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(r pagination.Page) ([]Event, error) {
+	var s struct {
+		Events []Event `json:"events"`
+	}
+	err := (r.(EventPage)).ExtractInto(&s)
+	return s.Events, err
+}
+
+// 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) {
+	var s struct {
+		Event *Event `json:"event"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Event, err
+}
diff --git a/openstack/orchestration/v1/stackevents/testing/doc.go b/openstack/orchestration/v1/stackevents/testing/doc.go
new file mode 100644
index 0000000..2e22a6c
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/testing/doc.go
@@ -0,0 +1,2 @@
+// orchestration_stackevents_v1
+package testing
diff --git a/openstack/orchestration/v1/stackevents/testing/fixtures.go b/openstack/orchestration/v1/stackevents/testing/fixtures.go
new file mode 100644
index 0000000..a40e8d4
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/testing/fixtures.go
@@ -0,0 +1,447 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// FindExpected represents the expected object from a Find request.
+var FindExpected = []stackevents.Event{
+	{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		Links: []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",
+			},
+			{
+				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",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_IN_PROGRESS",
+		PhysicalResourceID:   "",
+		ID:                   "06feb26f-9298-4a9b-8749-9d770e5d577a",
+	},
+	{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+		Links: []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",
+			},
+			{
+				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",
+			},
+		},
+		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 = []stackevents.Event{
+	{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		Links: []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",
+			},
+			{
+				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",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_IN_PROGRESS",
+		PhysicalResourceID:   "",
+		ID:                   "06feb26f-9298-4a9b-8749-9d770e5d577a",
+	},
+	{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+		Links: []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",
+			},
+			{
+				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",
+			},
+		},
+		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 = []stackevents.Event{
+	{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+		Links: []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",
+			},
+			{
+				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",
+			},
+		},
+		LogicalResourceID:    "hello_world",
+		ResourceStatusReason: "state changed",
+		ResourceStatus:       "CREATE_IN_PROGRESS",
+		PhysicalResourceID:   "",
+		ID:                   "06feb26f-9298-4a9b-8749-9d770e5d577a",
+	},
+	{
+		ResourceName: "hello_world",
+		Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+		Links: []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",
+			},
+			{
+				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",
+			},
+		},
+		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 = &stackevents.Event{
+	ResourceName: "hello_world",
+	Time:         time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+	Links: []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",
+		},
+		{
+			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",
+		},
+	},
+	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/testing/requests_test.go b/openstack/orchestration/v1/stackevents/testing/requests_test.go
new file mode 100644
index 0000000..0ad3fc3
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/testing/requests_test.go
@@ -0,0 +1,72 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackevents"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestFindEvents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleFindSuccessfully(t, FindOutput)
+
+	actual, err := stackevents.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 := stackevents.List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := stackevents.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 := stackevents.ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := stackevents.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 := stackevents.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/urls.go b/openstack/orchestration/v1/stackevents/urls.go
new file mode 100644
index 0000000..6b6b330
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/urls.go
@@ -0,0 +1,19 @@
+package stackevents
+
+import "github.com/gophercloud/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/requests.go b/openstack/orchestration/v1/stackresources/requests.go
new file mode 100644
index 0000000..f368b76
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/requests.go
@@ -0,0 +1,77 @@
+package stackresources
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Find retrieves stack resources for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) (r FindResult) {
+	_, r.Err = c.Get(findURL(c, stackName), &r.Body, nil)
+	return
+}
+
+// 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)
+	return q.String(), err
+}
+
+// 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
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ResourcePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, stackName, stackID, resourceName), &r.Body, nil)
+	return
+}
+
+// Metadata retreives the metadata for the given stack resource.
+func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) (r MetadataResult) {
+	_, r.Err = c.Get(metadataURL(c, stackName, stackID, resourceName), &r.Body, nil)
+	return
+}
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listTypesURL(client), func(r pagination.PageResult) pagination.Page {
+		return ResourceTypePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Schema retreives the schema for the given resource type.
+func Schema(c *gophercloud.ServiceClient, resourceType string) (r SchemaResult) {
+	_, r.Err = c.Get(schemaURL(c, resourceType), &r.Body, nil)
+	return
+}
+
+// Template retreives the template representation for the given resource type.
+func Template(c *gophercloud.ServiceClient, resourceType string) (r TemplateResult) {
+	_, r.Err = c.Get(templateURL(c, resourceType), &r.Body, nil)
+	return
+}
diff --git a/openstack/orchestration/v1/stackresources/results.go b/openstack/orchestration/v1/stackresources/results.go
new file mode 100644
index 0000000..59c02a3
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/results.go
@@ -0,0 +1,185 @@
+package stackresources
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Resource represents a stack resource.
+type Resource struct {
+	Attributes   map[string]interface{} `json:"attributes"`
+	CreationTime time.Time              `json:"-"`
+	Description  string                 `json:"description"`
+	Links        []gophercloud.Link     `json:"links"`
+	LogicalID    string                 `json:"logical_resource_id"`
+	Name         string                 `json:"resource_name"`
+	PhysicalID   string                 `json:"physical_resource_id"`
+	RequiredBy   []interface{}          `json:"required_by"`
+	Status       string                 `json:"resource_status"`
+	StatusReason string                 `json:"resource_status_reason"`
+	Type         string                 `json:"resource_type"`
+	UpdatedTime  time.Time              `json:"-"`
+}
+
+func (r *Resource) UnmarshalJSON(b []byte) error {
+	type tmp Resource
+	var s struct {
+		tmp
+		CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"`
+		UpdatedTime  gophercloud.JSONRFC3339NoZ `json:"updated_time"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Resource(s.tmp)
+
+	r.CreationTime = time.Time(s.CreationTime)
+	r.UpdatedTime = time.Time(s.UpdatedTime)
+
+	return nil
+}
+
+// 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) {
+	var s struct {
+		Resources []Resource `json:"resources"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Resources, err
+}
+
+// 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)
+	return len(resources) == 0, err
+}
+
+// ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities.
+func ExtractResources(r pagination.Page) ([]Resource, error) {
+	var s struct {
+		Resources []Resource `json:"resources"`
+	}
+	err := (r.(ResourcePage)).ExtractInto(&s)
+	return s.Resources, err
+}
+
+// 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) {
+	var s struct {
+		Resource *Resource `json:"resource"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Resource, err
+}
+
+// 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) {
+	var s struct {
+		Meta map[string]string `json:"metadata"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Meta, err
+}
+
+// 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)
+	return len(rts) == 0, err
+}
+
+// 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(r pagination.Page) (ResourceTypes, error) {
+	var s struct {
+		ResourceTypes ResourceTypes `json:"resource_types"`
+	}
+	err := (r.(ResourceTypePage)).ExtractInto(&s)
+	return s.ResourceTypes, err
+}
+
+// TypeSchema represents a stack resource schema.
+type TypeSchema struct {
+	Attributes    map[string]interface{} `json:"attributes"`
+	Properties    map[string]interface{} `json:"properties"`
+	ResourceType  string                 `json:"resource_type"`
+	SupportStatus map[string]interface{} `json:"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) {
+	var s *TypeSchema
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// 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, "", "  ")
+	return template, err
+}
diff --git a/openstack/orchestration/v1/stackresources/testing/doc.go b/openstack/orchestration/v1/stackresources/testing/doc.go
new file mode 100644
index 0000000..16e1dae
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/testing/doc.go
@@ -0,0 +1,2 @@
+// orchestration_stackresources_v1
+package testing
diff --git a/openstack/orchestration/v1/stackresources/testing/fixtures.go b/openstack/orchestration/v1/stackresources/testing/fixtures.go
new file mode 100644
index 0000000..e890337
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/testing/fixtures.go
@@ -0,0 +1,440 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// FindExpected represents the expected object from a Find request.
+var FindExpected = []stackresources.Resource{
+	{
+		Name: "hello_world",
+		Links: []gophercloud.Link{
+			{
+				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",
+			},
+		},
+		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 = []stackresources.Resource{
+	{
+		Name: "hello_world",
+		Links: []gophercloud.Link{
+			{
+				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",
+			},
+		},
+		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 = &stackresources.Resource{
+	Name: "wordpress_instance",
+	Links: []gophercloud.Link{
+		{
+			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",
+		},
+	},
+	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 = stackresources.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 = stackresources.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 = &stackresources.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/testing/requests_test.go b/openstack/orchestration/v1/stackresources/testing/requests_test.go
new file mode 100644
index 0000000..c714047
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/testing/requests_test.go
@@ -0,0 +1,112 @@
+package testing
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stackresources"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleFindSuccessfully(t, FindOutput)
+
+	actual, err := stackresources.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 := stackresources.List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := stackresources.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 := stackresources.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 := stackresources.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 := stackresources.ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := stackresources.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 := stackresources.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 := stackresources.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/urls.go b/openstack/orchestration/v1/stackresources/urls.go
new file mode 100644
index 0000000..bbddc69
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/urls.go
@@ -0,0 +1,31 @@
+package stackresources
+
+import "github.com/gophercloud/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..8698918
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/environment.go
@@ -0,0 +1,134 @@
+package stacks
+
+import "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 ErrInvalidEnvironment{Section: 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..a7e3aae
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/environment_test.go
@@ -0,0 +1,185 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/gophercloud/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/errors.go b/openstack/orchestration/v1/stacks/errors.go
new file mode 100644
index 0000000..cd6c18f
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/errors.go
@@ -0,0 +1,33 @@
+package stacks
+
+import (
+	"fmt"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+type ErrInvalidEnvironment struct {
+	gophercloud.BaseError
+	Section string
+}
+
+func (e ErrInvalidEnvironment) Error() string {
+	return fmt.Sprintf("Environment has wrong section: %s", e.Section)
+}
+
+type ErrInvalidDataFormat struct {
+	gophercloud.BaseError
+}
+
+func (e ErrInvalidDataFormat) Error() string {
+	return fmt.Sprintf("Data in neither json nor yaml format.")
+}
+
+type ErrInvalidTemplateFormatVersion struct {
+	gophercloud.BaseError
+	Version string
+}
+
+func (e ErrInvalidTemplateFormatVersion) Error() string {
+	return fmt.Sprintf("Template format version not found.")
+}
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
new file mode 100644
index 0000000..d6fd075
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -0,0 +1,199 @@
+package stacks
+
+// 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"
+    }
+  }
+}
+`
+
+// 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"
+					]
+				}
+			}
+		}
+	}
+}
+`
+
+// 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
+`
+
+// 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",
+					},
+				},
+			},
+		},
+	},
+}
+
+// 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",
+		},
+	},
+}
diff --git a/openstack/orchestration/v1/stacks/requests.go b/openstack/orchestration/v1/stacks/requests.go
new file mode 100644
index 0000000..91f38ee
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/requests.go
@@ -0,0 +1,440 @@
+package stacks
+
+import (
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// 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 {
+	// The name of the stack. It must start with an alphabetic character.
+	Name string `json:"stack_name" required:"true"`
+	// 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 `json:"-" required:"true"`
+	// 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 *bool `json:"disable_rollback,omitempty"`
+	// A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment `json:"-"`
+	// User-defined parameters to pass to the template.
+	Parameters map[string]string `json:"parameters,omitempty"`
+	// The timeout for stack creation in minutes.
+	Timeout int `json:"timeout_mins,omitempty"`
+	// A list of tags to assosciate with the Stack
+	Tags []string `json:"-"`
+}
+
+// ToStackCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	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()
+	b["template"] = string(opts.TemplateOpts.Bin)
+
+	files := make(map[string]string)
+	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
+		}
+		b["environment"] = string(opts.EnvironmentOpts.Bin)
+	}
+
+	if len(files) > 0 {
+		b["files"] = files
+	}
+
+	if opts.Tags != nil {
+		b["tags"] = strings.Join(opts.Tags, ",")
+	}
+
+	return b, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new stack using the values
+// provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToStackCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
+	return
+}
+
+// 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 {
+	// Existing resources data represented as a string to add to the
+	// new stack. Data returned by Abandon could be provided as AdoptsStackData.
+	AdoptStackData string `json:"adopt_stack_data" required:"true"`
+	// The name of the stack. It must start with an alphabetic character.
+	Name string `json:"stack_name" required:"true"`
+	// 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 `json:"-" required:"true"`
+	// The timeout for stack creation in minutes.
+	Timeout int `json:"timeout_mins,omitempty"`
+	// 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 `json:"-" required:"true"`
+	// 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 *bool `json:"disable_rollback,omitempty"`
+	// A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment `json:"-"`
+	// User-defined parameters to pass to the template.
+	Parameters map[string]string `json:"parameters,omitempty"`
+}
+
+// ToStackAdoptMap casts a CreateOpts struct to a map.
+func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	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()
+	b["template"] = string(opts.TemplateOpts.Bin)
+
+	files := make(map[string]string)
+	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
+		}
+		b["environment"] = string(opts.EnvironmentOpts.Bin)
+	}
+
+	if len(files) > 0 {
+		b["files"] = files
+	}
+
+	return b, nil
+}
+
+// Adopt accepts an AdoptOpts struct and creates a new stack using the resources
+// from another stack.
+func Adopt(c *gophercloud.ServiceClient, opts AdoptOptsBuilder) (r AdoptResult) {
+	b, err := opts.ToStackAdoptMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(adoptURL(c), b, &r.Body, nil)
+	return
+}
+
+// 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) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, stackName, stackID), &r.Body, nil)
+	return
+}
+
+// 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 {
+	// 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 `json:"-" required:"true"`
+	// A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment `json:"-"`
+	// User-defined parameters to pass to the template.
+	Parameters map[string]string `json:"parameters,omitempty"`
+	// The timeout for stack creation in minutes.
+	Timeout int `json:"timeout_mins,omitempty"`
+	// A list of tags to assosciate with the Stack
+	Tags []string `json:"-"`
+}
+
+// ToStackUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	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()
+	b["template"] = string(opts.TemplateOpts.Bin)
+
+	files := make(map[string]string)
+	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
+		}
+		b["environment"] = string(opts.EnvironmentOpts.Bin)
+	}
+
+	if len(files) > 0 {
+		b["files"] = files
+	}
+
+	if opts.Tags != nil {
+		b["tags"] = strings.Join(opts.Tags, ",")
+	}
+
+	return b, 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) (r UpdateResult) {
+	b, err := opts.ToStackUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Put(updateURL(c, stackName, stackID), b, nil, nil)
+	return
+}
+
+// Delete deletes a stack based on the stack name and stack ID.
+func Delete(c *gophercloud.ServiceClient, stackName, stackID string) (r DeleteResult) {
+	_, r.Err = c.Delete(deleteURL(c, stackName, stackID), nil)
+	return
+}
+
+// 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 {
+	// The name of the stack. It must start with an alphabetic character.
+	Name string `json:"stack_name" required:"true"`
+	// The timeout for stack creation in minutes.
+	Timeout int `json:"timeout_mins" required:"true"`
+	// 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 `json:"-" required:"true"`
+	// 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 *bool `json:"disable_rollback,omitempty"`
+	// A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment `json:"-"`
+	// User-defined parameters to pass to the template.
+	Parameters map[string]string `json:"parameters,omitempty"`
+}
+
+// ToStackPreviewMap casts a PreviewOpts struct to a map.
+func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) {
+	b, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	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()
+	b["template"] = string(opts.TemplateOpts.Bin)
+
+	files := make(map[string]string)
+	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
+		}
+		b["environment"] = string(opts.EnvironmentOpts.Bin)
+	}
+
+	if len(files) > 0 {
+		b["files"] = files
+	}
+
+	return b, nil
+}
+
+// Preview accepts a PreviewOptsBuilder interface and creates a preview of a stack using the values
+// provided.
+func Preview(c *gophercloud.ServiceClient, opts PreviewOptsBuilder) (r PreviewResult) {
+	b, err := opts.ToStackPreviewMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(previewURL(c), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// 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) (r AbandonResult) {
+	_, r.Err = c.Delete(abandonURL(c, stackName, stackID), &gophercloud.RequestOpts{
+		JSONResponse: &r.Body,
+		OkCodes:      []int{200},
+	})
+	return
+}
diff --git a/openstack/orchestration/v1/stacks/results.go b/openstack/orchestration/v1/stacks/results.go
new file mode 100644
index 0000000..8df5419
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/results.go
@@ -0,0 +1,238 @@
+package stacks
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreatedStack represents the object extracted from a Create operation.
+type CreatedStack struct {
+	ID    string             `json:"id"`
+	Links []gophercloud.Link `json:"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) {
+	var s struct {
+		CreatedStack *CreatedStack `json:"stack"`
+	}
+	err := r.ExtractInto(&s)
+	return s.CreatedStack, err
+}
+
+// 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)
+	return len(stacks) == 0, err
+}
+
+// ListedStack represents an element in the slice extracted from a List operation.
+type ListedStack struct {
+	CreationTime time.Time          `json:"-"`
+	Description  string             `json:"description"`
+	ID           string             `json:"id"`
+	Links        []gophercloud.Link `json:"links"`
+	Name         string             `json:"stack_name"`
+	Status       string             `json:"stack_status"`
+	StatusReason string             `json:"stack_status_reason"`
+	Tags         []string           `json:"tags"`
+	UpdatedTime  time.Time          `json:"-"`
+}
+
+func (r *ListedStack) UnmarshalJSON(b []byte) error {
+	type tmp ListedStack
+	var s struct {
+		tmp
+		CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"`
+		UpdatedTime  gophercloud.JSONRFC3339NoZ `json:"updated_time"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = ListedStack(s.tmp)
+
+	r.CreationTime = time.Time(s.CreationTime)
+	r.UpdatedTime = time.Time(s.UpdatedTime)
+
+	return nil
+}
+
+// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating
+// over a stacks.List call.
+func ExtractStacks(r pagination.Page) ([]ListedStack, error) {
+	var s struct {
+		ListedStacks []ListedStack `json:"stacks"`
+	}
+	err := (r.(StackPage)).ExtractInto(&s)
+	return s.ListedStacks, err
+}
+
+// RetrievedStack represents the object extracted from a Get operation.
+type RetrievedStack struct {
+	Capabilities        []interface{}            `json:"capabilities"`
+	CreationTime        time.Time                `json:"-"`
+	Description         string                   `json:"description"`
+	DisableRollback     bool                     `json:"disable_rollback"`
+	ID                  string                   `json:"id"`
+	Links               []gophercloud.Link       `json:"links"`
+	NotificationTopics  []interface{}            `json:"notification_topics"`
+	Outputs             []map[string]interface{} `json:"outputs"`
+	Parameters          map[string]string        `json:"parameters"`
+	Name                string                   `json:"stack_name"`
+	Status              string                   `json:"stack_status"`
+	StatusReason        string                   `json:"stack_status_reason"`
+	Tags                []string                 `json:"tags"`
+	TemplateDescription string                   `json:"template_description"`
+	Timeout             int                      `json:"timeout_mins"`
+	UpdatedTime         time.Time                `json:"-"`
+}
+
+func (r *RetrievedStack) UnmarshalJSON(b []byte) error {
+	type tmp RetrievedStack
+	var s struct {
+		tmp
+		CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"`
+		UpdatedTime  gophercloud.JSONRFC3339NoZ `json:"updated_time"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = RetrievedStack(s.tmp)
+
+	r.CreationTime = time.Time(s.CreationTime)
+	r.UpdatedTime = time.Time(s.UpdatedTime)
+
+	return nil
+}
+
+// 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) {
+	var s struct {
+		Stack *RetrievedStack `json:"stack"`
+	}
+	err := r.ExtractInto(&s)
+	return s.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{}      `json:"capabilities"`
+	CreationTime        time.Time          `json:"-"`
+	Description         string             `json:"description"`
+	DisableRollback     bool               `json:"disable_rollback"`
+	ID                  string             `json:"id"`
+	Links               []gophercloud.Link `json:"links"`
+	Name                string             `json:"stack_name"`
+	NotificationTopics  []interface{}      `json:"notification_topics"`
+	Parameters          map[string]string  `json:"parameters"`
+	Resources           []interface{}      `json:"resources"`
+	TemplateDescription string             `json:"template_description"`
+	Timeout             int                `json:"timeout_mins"`
+	UpdatedTime         time.Time          `json:"-"`
+}
+
+func (r *PreviewedStack) UnmarshalJSON(b []byte) error {
+	type tmp PreviewedStack
+	var s struct {
+		tmp
+		CreationTime gophercloud.JSONRFC3339NoZ `json:"creation_time"`
+		UpdatedTime  gophercloud.JSONRFC3339NoZ `json:"updated_time"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = PreviewedStack(s.tmp)
+
+	r.CreationTime = time.Time(s.CreationTime)
+	r.UpdatedTime = time.Time(s.UpdatedTime)
+
+	return nil
+}
+
+// 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) {
+	var s struct {
+		PreviewedStack *PreviewedStack `json:"stack"`
+	}
+	err := r.ExtractInto(&s)
+	return s.PreviewedStack, err
+}
+
+// AbandonedStack represents the result of an Abandon operation.
+type AbandonedStack struct {
+	Status             string                 `json:"status"`
+	Name               string                 `json:"name"`
+	Template           map[string]interface{} `json:"template"`
+	Action             string                 `json:"action"`
+	ID                 string                 `json:"id"`
+	Resources          map[string]interface{} `json:"resources"`
+	Files              map[string]string      `json:"files"`
+	StackUserProjectID string                 `json:"stack_user_project_id"`
+	ProjectID          string                 `json:"project_id"`
+	Environment        map[string]interface{} `json:"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) {
+	var s *AbandonedStack
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// 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)
+	return string(out), err
+}
diff --git a/openstack/orchestration/v1/stacks/template.go b/openstack/orchestration/v1/stacks/template.go
new file mode 100644
index 0000000..4cf5aae
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/template.go
@@ -0,0 +1,141 @@
+package stacks
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// 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
+		}
+	}
+	var invalid string
+	for key := range t.Parsed {
+		if _, ok := TemplateFormatVersions[key]; ok {
+			return nil
+		}
+		invalid = key
+	}
+	return ErrInvalidTemplateFormatVersion{Version: invalid}
+}
+
+// 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 gophercloud.ErrUnexpectedType{Actual: fmt.Sprintf("%v", 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..cbe99ed
--- /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/gophercloud/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/testing/doc.go b/openstack/orchestration/v1/stacks/testing/doc.go
new file mode 100644
index 0000000..5b3703e
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/testing/doc.go
@@ -0,0 +1,2 @@
+// orchestration_stacks_v1
+package testing
diff --git a/openstack/orchestration/v1/stacks/testing/fixtures.go b/openstack/orchestration/v1/stacks/testing/fixtures.go
new file mode 100644
index 0000000..f3e3b57
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/testing/fixtures.go
@@ -0,0 +1,407 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// CreateExpected represents the expected object from a Create request.
+var CreateExpected = &stacks.CreatedStack{
+	ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+	Links: []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 = []stacks.ListedStack{
+	{
+		Description: "Simple template to test heat commands",
+		Links: []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"},
+	},
+	{
+		Description: "Simple template to test heat commands",
+		Links: []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 = &stacks.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{
+		{
+			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 = &stacks.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{
+		{
+			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 = &stacks.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)
+	})
+}
diff --git a/openstack/orchestration/v1/stacks/testing/requests_test.go b/openstack/orchestration/v1/stacks/testing/requests_test.go
new file mode 100644
index 0000000..bdc6229
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/testing/requests_test.go
@@ -0,0 +1,192 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreateStack(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+	template := new(stacks.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 := stacks.CreateOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: gophercloud.Disabled,
+	}
+	actual, err := stacks.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)
+	template := new(stacks.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 := stacks.AdoptOpts{
+		AdoptStackData:  `{environment{parameters{}}}`,
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: gophercloud.Disabled,
+	}
+	actual, err := stacks.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 := stacks.List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := stacks.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 := stacks.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)
+
+	template := new(stacks.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 := stacks.UpdateOpts{
+		TemplateOpts: template,
+	}
+	err := stacks.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 := stacks.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)
+
+	template := new(stacks.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 := stacks.PreviewOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: gophercloud.Disabled,
+	}
+	actual, err := stacks.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 := stacks.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/urls.go b/openstack/orchestration/v1/stacks/urls.go
new file mode 100644
index 0000000..b00be54
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/urls.go
@@ -0,0 +1,35 @@
+package stacks
+
+import "github.com/gophercloud/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..71d9e35
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/utils.go
@@ -0,0 +1,160 @@
+package stacks
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"path/filepath"
+	"reflect"
+	"strings"
+
+	"github.com/gophercloud/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 ErrInvalidDataFormat{}
+		}
+	}
+	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, gophercloud.ErrUnexpectedType{Expected: "map[string]interface{}/map[interface{}]interface{}", Actual: fmt.Sprintf("%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..4d5cc73
--- /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/gophercloud/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/requests.go b/openstack/orchestration/v1/stacktemplates/requests.go
new file mode 100644
index 0000000..d248c24
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/requests.go
@@ -0,0 +1,39 @@
+package stacktemplates
+
+import "github.com/gophercloud/gophercloud"
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) (r GetResult) {
+	_, r.Err = c.Get(getURL(c, stackName, stackID), &r.Body, nil)
+	return
+}
+
+// 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 `json:"template" or:"TemplateURL"`
+	TemplateURL string `json:"template_url" or:"Template"`
+}
+
+// ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts.
+func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// Validate validates the given stack template.
+func Validate(c *gophercloud.ServiceClient, opts ValidateOptsBuilder) (r ValidateResult) {
+	b, err := opts.ToStackTemplateValidateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = c.Post(validateURL(c), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
diff --git a/openstack/orchestration/v1/stacktemplates/results.go b/openstack/orchestration/v1/stacktemplates/results.go
new file mode 100644
index 0000000..bca959b
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/results.go
@@ -0,0 +1,44 @@
+package stacktemplates
+
+import (
+	"encoding/json"
+
+	"github.com/gophercloud/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                 `json:"Description"`
+	Parameters      map[string]interface{} `json:"Parameters"`
+	ParameterGroups map[string]interface{} `json:"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) {
+	var s *ValidatedTemplate
+	err := r.ExtractInto(&s)
+	return s, err
+}
diff --git a/openstack/orchestration/v1/stacktemplates/testing/doc.go b/openstack/orchestration/v1/stacktemplates/testing/doc.go
new file mode 100644
index 0000000..43c6b0f
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/testing/doc.go
@@ -0,0 +1,2 @@
+// orchestration_stacktemplates_v1
+package testing
diff --git a/openstack/orchestration/v1/stacktemplates/testing/fixtures.go b/openstack/orchestration/v1/stacktemplates/testing/fixtures.go
new file mode 100644
index 0000000..23ec579
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/testing/fixtures.go
@@ -0,0 +1,96 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/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 = &stacktemplates.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/testing/requests_test.go b/openstack/orchestration/v1/stacktemplates/testing/requests_test.go
new file mode 100644
index 0000000..442bcb7
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/testing/requests_test.go
@@ -0,0 +1,58 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/orchestration/v1/stacktemplates"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t, GetOutput)
+
+	actual, err := stacktemplates.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 := stacktemplates.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 := stacktemplates.Validate(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := ValidateExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stacktemplates/urls.go b/openstack/orchestration/v1/stacktemplates/urls.go
new file mode 100644
index 0000000..aed6b4b
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/urls.go
@@ -0,0 +1,11 @@
+package stacktemplates
+
+import "github.com/gophercloud/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/sharedfilesystems/v2/availabilityzones/requests.go b/openstack/sharedfilesystems/v2/availabilityzones/requests.go
new file mode 100644
index 0000000..df10b85
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/availabilityzones/requests.go
@@ -0,0 +1,13 @@
+package availabilityzones
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List will return the existing availability zones.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+		return AvailabilityZonePage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/availabilityzones/results.go b/openstack/sharedfilesystems/v2/availabilityzones/results.go
new file mode 100644
index 0000000..83a76c1
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/availabilityzones/results.go
@@ -0,0 +1,59 @@
+package availabilityzones
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// AvailabilityZone contains all the information associated with an OpenStack
+// AvailabilityZone.
+type AvailabilityZone struct {
+	// The availability zone ID.
+	ID string `json:"id"`
+	// The name of the availability zone.
+	Name string `json:"name"`
+	// The date and time stamp when the availability zone was created.
+	CreatedAt time.Time `json:"-"`
+	// The date and time stamp when the availability zone was updated.
+	UpdatedAt time.Time `json:"-"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// ListResult contains the response body and error from a List request.
+type AvailabilityZonePage struct {
+	pagination.SinglePageBase
+}
+
+// ExtractAvailabilityZones will get the AvailabilityZone objects out of the shareTypeAccessResult object.
+func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) {
+	var a struct {
+		AvailabilityZone []AvailabilityZone `json:"availability_zones"`
+	}
+	err := (r.(AvailabilityZonePage)).ExtractInto(&a)
+	return a.AvailabilityZone, err
+}
+
+func (r *AvailabilityZone) UnmarshalJSON(b []byte) error {
+	type tmp AvailabilityZone
+	var s struct {
+		tmp
+		CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+		UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = AvailabilityZone(s.tmp)
+
+	r.CreatedAt = time.Time(s.CreatedAt)
+	r.UpdatedAt = time.Time(s.UpdatedAt)
+
+	return nil
+}
diff --git a/openstack/sharedfilesystems/v2/availabilityzones/testing/fixtures.go b/openstack/sharedfilesystems/v2/availabilityzones/testing/fixtures.go
new file mode 100644
index 0000000..e5db8cd
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/availabilityzones/testing/fixtures.go
@@ -0,0 +1,32 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/os-availability-zone", 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, `
+        {
+            "availability_zones": [
+                {
+                    "name": "nova",
+                    "created_at": "2015-09-18T09:50:55.000000",
+                    "updated_at": null,
+                    "id": "388c983d-258e-4a0e-b1ba-10da37d766db"
+                }
+            ]
+        }`)
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/availabilityzones/testing/requests_test.go b/openstack/sharedfilesystems/v2/availabilityzones/testing/requests_test.go
new file mode 100644
index 0000000..76c8574
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/availabilityzones/testing/requests_test.go
@@ -0,0 +1,34 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/availabilityzones"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// Verifies that availability zones can be listed correctly
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	allPages, err := availabilityzones.List(client.ServiceClient()).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := availabilityzones.ExtractAvailabilityZones(allPages)
+	th.AssertNoErr(t, err)
+	var nilTime time.Time
+	expected := []availabilityzones.AvailabilityZone{
+		{
+			Name:      "nova",
+			CreatedAt: time.Date(2015, 9, 18, 9, 50, 55, 0, time.UTC),
+			UpdatedAt: nilTime,
+			ID:        "388c983d-258e-4a0e-b1ba-10da37d766db",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/sharedfilesystems/v2/availabilityzones/urls.go b/openstack/sharedfilesystems/v2/availabilityzones/urls.go
new file mode 100644
index 0000000..fb4cdcf
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/availabilityzones/urls.go
@@ -0,0 +1,7 @@
+package availabilityzones
+
+import "github.com/gophercloud/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("os-availability-zone")
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/requests.go b/openstack/sharedfilesystems/v2/securityservices/requests.go
new file mode 100644
index 0000000..9de58b9
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/securityservices/requests.go
@@ -0,0 +1,121 @@
+package securityservices
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type SecurityServiceType string
+
+// Valid security service types
+const (
+	LDAP            SecurityServiceType = "ldap"
+	Kerberos        SecurityServiceType = "kerberos"
+	ActiveDirectory SecurityServiceType = "active_directory"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToSecurityServiceCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains options for creating a SecurityService. This object is
+// passed to the securityservices.Create function. For more information about
+// these parameters, see the SecurityService object.
+type CreateOpts struct {
+	// The security service type. A valid value is ldap, kerberos, or active_directory
+	Type SecurityServiceType `json:"type" required:"true"`
+	// The security service name
+	Name string `json:"name,omitempty"`
+	// The security service description
+	Description string `json:"description,omitempty"`
+	// The DNS IP address that is used inside the tenant network
+	DNSIP string `json:"dns_ip,omitempty"`
+	// The security service user or group name that is used by the tenant
+	User string `json:"user,omitempty"`
+	// The user password, if you specify a user
+	Password string `json:"password,omitempty"`
+	// The security service domain
+	Domain string `json:"domain,omitempty"`
+	// The security service host name or IP address
+	Server string `json:"server,omitempty"`
+}
+
+// ToSecurityServicesCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSecurityServiceCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "security_service")
+}
+
+// Create will create a new SecurityService based on the values in CreateOpts. To
+// extract the SecurityService object from the response, call the Extract method
+// on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToSecurityServiceCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
+
+// Delete will delete the existing SecurityService with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToSecurityServiceListQuery() (string, error)
+}
+
+// ListOpts holds options for listing SecurityServices. It is passed to the
+// securityservices.List function.
+type ListOpts struct {
+	// admin-only option. Set it to true to see all tenant security services.
+	AllTenants bool `q:"all_tenants"`
+	// The security service ID
+	ID string `q:"id"`
+	// The security service domain
+	Domain string `q:"domain"`
+	// The security service type. A valid value is ldap, kerberos, or active_directory
+	Type SecurityServiceType `q:"type"`
+	// The security service name
+	Name string `q:"name"`
+	// The DNS IP address that is used inside the tenant network
+	DNSIP string `q:"dns_ip"`
+	// The security service user or group name that is used by the tenant
+	User string `q:"user"`
+	// The security service host name or IP address
+	Server string `q:"server"`
+	// The ID of the share network using security services
+	ShareNetworkID string `q:"share_network_id"`
+}
+
+// ToSecurityServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSecurityServiceListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List returns SecurityServices 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.ToSecurityServiceListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return SecurityServicePage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/results.go b/openstack/sharedfilesystems/v2/securityservices/results.go
new file mode 100644
index 0000000..ab9da7d
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/securityservices/results.go
@@ -0,0 +1,103 @@
+package securityservices
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// SecurityService contains all the information associated with an OpenStack
+// SecurityService.
+type SecurityService struct {
+	// The security service ID
+	ID string `json:"id"`
+	// The UUID of the project where the security service was created
+	ProjectID string `json:"project_id"`
+	// The security service domain
+	Domain string `json:"domain"`
+	// The security service status
+	Status string `json:"status"`
+	// The security service type. A valid value is ldap, kerberos, or active_directory
+	Type string `json:"type"`
+	// The security service name
+	Name string `json:"name"`
+	// The security service description
+	Description string `json:"description"`
+	// The DNS IP address that is used inside the tenant network
+	DNSIP string `json:"dns_ip"`
+	// The security service user or group name that is used by the tenant
+	User string `json:"user"`
+	// The user password, if you specify a user
+	Password string `json:"password"`
+	// The security service host name or IP address
+	Server string `json:"server"`
+	// The date and time stamp when the security service was created
+	CreatedAt time.Time `json:"-"`
+	// The date and time stamp when the security service was updated
+	UpdatedAt time.Time `json:"-"`
+}
+
+func (r *SecurityService) UnmarshalJSON(b []byte) error {
+	type tmp SecurityService
+	var s struct {
+		tmp
+		CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+		UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = SecurityService(s.tmp)
+
+	r.CreatedAt = time.Time(s.CreatedAt)
+	r.UpdatedAt = time.Time(s.UpdatedAt)
+
+	return nil
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// SecurityServicePage is a pagination.pager that is returned from a call to the List function.
+type SecurityServicePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no SecurityServices.
+func (r SecurityServicePage) IsEmpty() (bool, error) {
+	securityServices, err := ExtractSecurityServices(r)
+	return len(securityServices) == 0, err
+}
+
+// ExtractSecurityServices extracts and returns SecurityServices. It is used while
+// iterating over a securityservices.List call.
+func ExtractSecurityServices(r pagination.Page) ([]SecurityService, error) {
+	var s struct {
+		SecurityServices []SecurityService `json:"security_services"`
+	}
+	err := (r.(SecurityServicePage)).ExtractInto(&s)
+	return s.SecurityServices, err
+}
+
+// Extract will get the SecurityService object out of the commonResult object.
+func (r commonResult) Extract() (*SecurityService, error) {
+	var s struct {
+		SecurityService *SecurityService `json:"security_service"`
+	}
+	err := r.ExtractInto(&s)
+	return s.SecurityService, err
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult contains the response body and error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go b/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go
new file mode 100644
index 0000000..c6b5f70
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go
@@ -0,0 +1,137 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/security-services", 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_service": {
+                "description": "Creating my first Security Service",
+                "dns_ip": "10.0.0.0/24",
+                "user": "demo",
+                "password": "***",
+                "type": "kerberos",
+                "name": "SecServ1"
+            }
+        }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+        {
+            "security_service": {
+                "status": "new",
+                "domain": null,
+                "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                "name": "SecServ1",
+                "created_at": "2015-09-07T12:19:10.695211",
+                "updated_at": null,
+                "server": null,
+                "dns_ip": "10.0.0.0/24",
+                "user": "demo",
+                "password": "supersecret",
+                "type": "kerberos",
+                "id": "3c829734-0679-4c17-9637-801da48c0d5f",
+                "description": "Creating my first Security Service"
+            }
+        }`)
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	th.Mux.HandleFunc("/security-services/securityServiceID", 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 MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/security-services/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")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+        {
+            "security_services": [
+                {
+                    "status": "new",
+                    "domain": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "name": "SecServ1",
+                    "created_at": "2015-09-07T12:19:10.000000",
+                    "description": "Creating my first Security Service",
+                    "updated_at": null,
+                    "server": null,
+                    "dns_ip": "10.0.0.0/24",
+                    "user": "demo",
+                    "password": "supersecret",
+                    "type": "kerberos",
+                    "id": "3c829734-0679-4c17-9637-801da48c0d5f"
+                },
+                {
+                    "status": "new",
+                    "domain": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "name": "SecServ2",
+                    "created_at": "2015-09-07T12:25:03.000000",
+                    "description": "Creating my second Security Service",
+                    "updated_at": null,
+                    "server": null,
+                    "dns_ip": "10.0.0.0/24",
+                    "user": null,
+                    "password": null,
+                    "type": "ldap",
+                    "id": "5a1d3a12-34a7-4087-8983-50e9ed03509a"
+                }
+            ]
+        }`)
+	})
+}
+
+func MockFilteredListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/security-services/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")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+        {
+            "security_services": [
+                {
+                    "status": "new",
+                    "domain": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "name": "SecServ1",
+                    "created_at": "2015-09-07T12:19:10.000000",
+                    "description": "Creating my first Security Service",
+                    "updated_at": null,
+                    "server": null,
+                    "dns_ip": "10.0.0.0/24",
+                    "user": "demo",
+                    "password": "supersecret",
+                    "type": "kerberos",
+                    "id": "3c829734-0679-4c17-9637-801da48c0d5f"
+                }
+            ]
+        }`)
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go b/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go
new file mode 100644
index 0000000..9feff53
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go
@@ -0,0 +1,150 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// Verifies that a security service can be created correctly
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := &securityservices.CreateOpts{
+		Name:        "SecServ1",
+		Description: "Creating my first Security Service",
+		DNSIP:       "10.0.0.0/24",
+		User:        "demo",
+		Password:    "***",
+		Type:        "kerberos",
+	}
+
+	s, err := securityservices.Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "SecServ1")
+	th.AssertEquals(t, s.Description, "Creating my first Security Service")
+	th.AssertEquals(t, s.User, "demo")
+	th.AssertEquals(t, s.DNSIP, "10.0.0.0/24")
+	th.AssertEquals(t, s.Password, "supersecret")
+	th.AssertEquals(t, s.Type, "kerberos")
+}
+
+// Verifies that a security service cannot be created without a type
+func TestCreateFails(t *testing.T) {
+	options := &securityservices.CreateOpts{
+		Name:        "SecServ1",
+		Description: "Creating my first Security Service",
+		DNSIP:       "10.0.0.0/24",
+		User:        "demo",
+		Password:    "***",
+	}
+
+	_, err := securityservices.Create(client.ServiceClient(), options).Extract()
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+}
+
+// Verifies that security service deletion works
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	res := securityservices.Delete(client.ServiceClient(), "securityServiceID")
+	th.AssertNoErr(t, res.Err)
+}
+
+// Verifies that security services can be listed correctly
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	allPages, err := securityservices.List(client.ServiceClient(), &securityservices.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := securityservices.ExtractSecurityServices(allPages)
+	th.AssertNoErr(t, err)
+	var nilTime time.Time
+	expected := []securityservices.SecurityService{
+		{
+			Status:      "new",
+			Domain:      "",
+			ProjectID:   "16e1ab15c35a457e9c2b2aa189f544e1",
+			Name:        "SecServ1",
+			CreatedAt:   time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC),
+			Description: "Creating my first Security Service",
+			UpdatedAt:   nilTime,
+			Server:      "",
+			DNSIP:       "10.0.0.0/24",
+			User:        "demo",
+			Password:    "supersecret",
+			Type:        "kerberos",
+			ID:          "3c829734-0679-4c17-9637-801da48c0d5f",
+		},
+		{
+			Status:      "new",
+			Domain:      "",
+			ProjectID:   "16e1ab15c35a457e9c2b2aa189f544e1",
+			Name:        "SecServ2",
+			CreatedAt:   time.Date(2015, 9, 7, 12, 25, 03, 0, time.UTC),
+			Description: "Creating my second Security Service",
+			UpdatedAt:   nilTime,
+			Server:      "",
+			DNSIP:       "10.0.0.0/24",
+			User:        "",
+			Password:    "",
+			Type:        "ldap",
+			ID:          "5a1d3a12-34a7-4087-8983-50e9ed03509a",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+// Verifies that security services list can be called with query parameters
+func TestFilteredList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockFilteredListResponse(t)
+
+	options := &securityservices.ListOpts{
+		Type: "kerberos",
+	}
+
+	allPages, err := securityservices.List(client.ServiceClient(), options).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := securityservices.ExtractSecurityServices(allPages)
+	th.AssertNoErr(t, err)
+	var nilTime time.Time
+	expected := []securityservices.SecurityService{
+		{
+			Status:      "new",
+			Domain:      "",
+			ProjectID:   "16e1ab15c35a457e9c2b2aa189f544e1",
+			Name:        "SecServ1",
+			CreatedAt:   time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC),
+			Description: "Creating my first Security Service",
+			UpdatedAt:   nilTime,
+			Server:      "",
+			DNSIP:       "10.0.0.0/24",
+			User:        "demo",
+			Password:    "supersecret",
+			Type:        "kerberos",
+			ID:          "3c829734-0679-4c17-9637-801da48c0d5f",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/urls.go b/openstack/sharedfilesystems/v2/securityservices/urls.go
new file mode 100644
index 0000000..e4deb5f
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/securityservices/urls.go
@@ -0,0 +1,15 @@
+package securityservices
+
+import "github.com/gophercloud/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("security-services")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("security-services", id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("security-services", "detail")
+}
diff --git a/openstack/sharedfilesystems/v2/sharenetworks/requests.go b/openstack/sharedfilesystems/v2/sharenetworks/requests.go
new file mode 100644
index 0000000..ebaad70
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharenetworks/requests.go
@@ -0,0 +1,167 @@
+package sharenetworks
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToShareNetworkCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains options for creating a ShareNetwork. This object is
+// passed to the sharenetworks.Create function. For more information about
+// these parameters, see the ShareNetwork object.
+type CreateOpts struct {
+	// The UUID of the Neutron network to set up for share servers
+	NeutronNetID string `json:"neutron_net_id,omitempty"`
+	// The UUID of the Neutron subnet to set up for share servers
+	NeutronSubnetID string `json:"neutron_subnet_id,omitempty"`
+	// The UUID of the nova network to set up for share servers
+	NovaNetID string `json:"nova_net_id,omitempty"`
+	// The share network name
+	Name string `json:"name"`
+	// The share network description
+	Description string `json:"description"`
+}
+
+// ToShareNetworkCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToShareNetworkCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "share_network")
+}
+
+// Create will create a new ShareNetwork based on the values in CreateOpts. To
+// extract the ShareNetwork object from the response, call the Extract method
+// on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToShareNetworkCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will delete the existing ShareNetwork with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToShareNetworkListQuery() (string, error)
+}
+
+// ListOpts holds options for listing ShareNetworks. It is passed to the
+// sharenetworks.List function.
+type ListOpts struct {
+	// admin-only option. Set it to true to see all tenant share networks.
+	AllTenants bool `q:"all_tenants"`
+	// The UUID of the project where the share network was created
+	ProjectID string `q:"project_id"`
+	// The neutron network ID
+	NeutronNetID string `q:"neutron_net_id"`
+	// The neutron subnet ID
+	NeutronSubnetID string `q:"neutron_subnet_id"`
+	// The nova network ID
+	NovaNetID string `q:"nova_net_id"`
+	// The network type. A valid value is VLAN, VXLAN, GRE or flat
+	NetworkType string `q:"network_type"`
+	// The Share Network name
+	Name string `q:"name"`
+	// The Share Network description
+	Description string `q:"description"`
+	// The Share Network IP version
+	IPVersion gophercloud.IPVersion `q:"ip_version"`
+	// The Share Network segmentation ID
+	SegmentationID int `q:"segmentation_id"`
+	// List all share networks created after the given date
+	CreatedSince string `q:"created_since"`
+	// List all share networks created before the given date
+	CreatedBefore string `q:"created_before"`
+	// Limit specifies the page size.
+	Limit int `q:"limit"`
+	// Limit specifies the page number.
+	Offset int `q:"offset"`
+}
+
+// ToShareNetworkListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToShareNetworkListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// ListDetail returns ShareNetworks optionally limited by the conditions provided in ListOpts.
+func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listDetailURL(client)
+	if opts != nil {
+		query, err := opts.ToShareNetworkListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		p := ShareNetworkPage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	})
+}
+
+// Get retrieves the ShareNetwork with the provided ID. To extract the ShareNetwork
+// object from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToShareNetworkUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contain options for updating an existing ShareNetwork. This object is passed
+// to the sharenetworks.Update function. For more information about the parameters, see
+// the ShareNetwork object.
+type UpdateOpts struct {
+	// The share network name
+	Name string `json:"name,omitempty"`
+	// The share network description
+	Description string `json:"description,omitempty"`
+	// The UUID of the Neutron network to set up for share servers
+	NeutronNetID string `json:"neutron_net_id,omitempty"`
+	// The UUID of the Neutron subnet to set up for share servers
+	NeutronSubnetID string `json:"neutron_subnet_id,omitempty"`
+	// The UUID of the nova network to set up for share servers
+	NovaNetID string `json:"nova_net_id,omitempty"`
+}
+
+// ToShareNetworkUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToShareNetworkUpdateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "share_network")
+}
+
+// Update will update the ShareNetwork with provided information. To extract the updated
+// ShareNetwork from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+	b, err := opts.ToShareNetworkUpdateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return
+}
diff --git a/openstack/sharedfilesystems/v2/sharenetworks/results.go b/openstack/sharedfilesystems/v2/sharenetworks/results.go
new file mode 100644
index 0000000..6762ef2
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharenetworks/results.go
@@ -0,0 +1,170 @@
+package sharenetworks
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ShareNetwork contains all the information associated with an OpenStack
+// ShareNetwork.
+type ShareNetwork struct {
+	// The Share Network ID
+	ID string `json:"id"`
+	// The UUID of the project where the share network was created
+	ProjectID string `json:"project_id"`
+	// The neutron network ID
+	NeutronNetID string `json:"neutron_net_id"`
+	// The neutron subnet ID
+	NeutronSubnetID string `json:"neutron_subnet_id"`
+	// The nova network ID
+	NovaNetID string `json:"nova_net_id"`
+	// The network type. A valid value is VLAN, VXLAN, GRE or flat
+	NetworkType string `json:"network_type"`
+	// The segmentation ID
+	SegmentationID int `json:"segmentation_id"`
+	// The IP block from which to allocate the network, in CIDR notation
+	CIDR string `json:"cidr"`
+	// The IP version of the network. A valid value is 4 or 6
+	IPVersion int `json:"ip_version"`
+	// The Share Network name
+	Name string `json:"name"`
+	// The Share Network description
+	Description string `json:"description"`
+	// The date and time stamp when the Share Network was created
+	CreatedAt time.Time `json:"-"`
+	// The date and time stamp when the Share Network was updated
+	UpdatedAt time.Time `json:"-"`
+}
+
+func (r *ShareNetwork) UnmarshalJSON(b []byte) error {
+	type tmp ShareNetwork
+	var s struct {
+		tmp
+		CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+		UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = ShareNetwork(s.tmp)
+
+	r.CreatedAt = time.Time(s.CreatedAt)
+	r.UpdatedAt = time.Time(s.UpdatedAt)
+
+	return nil
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// ShareNetworkPage is a pagination.pager that is returned from a call to the List function.
+type ShareNetworkPage struct {
+	pagination.MarkerPageBase
+}
+
+// NextPageURL generates the URL for the page of results after this one.
+func (r ShareNetworkPage) NextPageURL() (string, error) {
+	currentURL := r.URL
+	mark, err := r.Owner.LastMarker()
+	if err != nil {
+		return "", err
+	}
+
+	q := currentURL.Query()
+	q.Set("offset", mark)
+	currentURL.RawQuery = q.Encode()
+	return currentURL.String(), nil
+}
+
+// LastMarker returns the last offset in a ListResult.
+func (r ShareNetworkPage) LastMarker() (string, error) {
+	maxInt := strconv.Itoa(int(^uint(0) >> 1))
+	shareNetworks, err := ExtractShareNetworks(r)
+	if err != nil {
+		return maxInt, err
+	}
+	if len(shareNetworks) == 0 {
+		return maxInt, nil
+	}
+
+	u, err := url.Parse(r.URL.String())
+	if err != nil {
+		return maxInt, err
+	}
+	queryParams := u.Query()
+	offset := queryParams.Get("offset")
+	limit := queryParams.Get("limit")
+
+	// Limit is not present, only one page required
+	if limit == "" {
+		return maxInt, nil
+	}
+
+	iOffset := 0
+	if offset != "" {
+		iOffset, err = strconv.Atoi(offset)
+		if err != nil {
+			return maxInt, err
+		}
+	}
+	iLimit, err := strconv.Atoi(limit)
+	if err != nil {
+		return maxInt, err
+	}
+	iOffset = iOffset + iLimit
+	offset = strconv.Itoa(iOffset)
+
+	return offset, nil
+}
+
+// IsEmpty satisifies the IsEmpty method of the Page interface
+func (r ShareNetworkPage) IsEmpty() (bool, error) {
+	shareNetworks, err := ExtractShareNetworks(r)
+	return len(shareNetworks) == 0, err
+}
+
+// ExtractShareNetworks extracts and returns ShareNetworks. It is used while
+// iterating over a sharenetworks.List call.
+func ExtractShareNetworks(r pagination.Page) ([]ShareNetwork, error) {
+	var s struct {
+		ShareNetworks []ShareNetwork `json:"share_networks"`
+	}
+	err := (r.(ShareNetworkPage)).ExtractInto(&s)
+	return s.ShareNetworks, err
+}
+
+// Extract will get the ShareNetwork object out of the commonResult object.
+func (r commonResult) Extract() (*ShareNetwork, error) {
+	var s struct {
+		ShareNetwork *ShareNetwork `json:"share_network"`
+	}
+	err := r.ExtractInto(&s)
+	return s.ShareNetwork, err
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult contains the response body and error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+	commonResult
+}
diff --git a/openstack/sharedfilesystems/v2/sharenetworks/testing/fixtures.go b/openstack/sharedfilesystems/v2/sharenetworks/testing/fixtures.go
new file mode 100644
index 0000000..8b753e9
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharenetworks/testing/fixtures.go
@@ -0,0 +1,307 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func createReq(name, description, network, subnetwork string) string {
+	return fmt.Sprintf(`{
+        "share_network": {
+            "name": "%s",
+            "description": "%s",
+            "neutron_net_id": "%s",
+            "neutron_subnet_id": "%s"
+        }
+    }`, name, description, network, subnetwork)
+}
+
+func createResp(name, description, network, subnetwork string) string {
+	return fmt.Sprintf(`
+    {
+        "share_network": {
+            "name": "%s",
+            "description": "%s",
+            "segmentation_id": null,
+            "created_at": "2015-09-07T14:37:00.583656",
+            "updated_at": null,
+            "id": "77eb3421-4549-4789-ac39-0d5185d68c29",
+            "neutron_net_id": "%s",
+            "neutron_subnet_id": "%s",
+            "ip_version": null,
+            "nova_net_id": null,
+            "cidr": null,
+            "project_id": "e10a683c20da41248cfd5e1ab3d88c62",
+            "network_type": null
+        }
+    }`, name, description, network, subnetwork)
+}
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-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, createReq("my_network",
+			"This is my share network",
+			"998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+			"53482b62-2c84-4a53-b6ab-30d9d9800d06"))
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, createResp("my_network",
+			"This is my share network",
+			"998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+			"53482b62-2c84-4a53-b6ab-30d9d9800d06"))
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-networks/fa158a3d-6d9f-4187-9ca5-abbb82646eb2", 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 MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-networks/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")
+		w.WriteHeader(http.StatusOK)
+
+		r.ParseForm()
+		marker := r.Form.Get("offset")
+
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `{
+            "share_networks": [
+                {
+                    "name": "net_my1",
+                    "segmentation_id": null,
+                    "created_at": "2015-09-04T14:57:13.000000",
+                    "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+                    "updated_at": null,
+                    "id": "32763294-e3d4-456a-998d-60047677c2fb",
+                    "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+                    "ip_version": null,
+                    "nova_net_id": null,
+                    "cidr": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "network_type": null,
+                    "description": "descr"
+                },
+                {
+                    "name": "net_my",
+                    "segmentation_id": null,
+                    "created_at": "2015-09-04T14:54:25.000000",
+                    "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+                    "updated_at": null,
+                    "id": "713df749-aac0-4a54-af52-10f6c991e80c",
+                    "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+                    "ip_version": null,
+                    "nova_net_id": null,
+                    "cidr": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "network_type": null,
+                    "description": "desecr"
+                },
+                {
+                    "name": null,
+                    "segmentation_id": null,
+                    "created_at": "2015-09-04T14:51:41.000000",
+                    "neutron_subnet_id": null,
+                    "updated_at": null,
+                    "id": "fa158a3d-6d9f-4187-9ca5-abbb82646eb2",
+                    "neutron_net_id": null,
+                    "ip_version": null,
+                    "nova_net_id": null,
+                    "cidr": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "network_type": null,
+                    "description": null
+                }
+            ]
+        }`)
+		default:
+			fmt.Fprintf(w, `
+				{
+					"share_networks": []
+				}`)
+		}
+	})
+}
+
+func MockFilteredListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-networks/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")
+		w.WriteHeader(http.StatusOK)
+
+		r.ParseForm()
+		marker := r.Form.Get("offset")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+				{
+					"share_networks": [
+						{
+							"name": "net_my1",
+							"segmentation_id": null,
+							"created_at": "2015-09-04T14:57:13.000000",
+							"neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+							"updated_at": null,
+							"id": "32763294-e3d4-456a-998d-60047677c2fb",
+							"neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+							"ip_version": null,
+							"nova_net_id": null,
+							"cidr": null,
+							"project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+							"network_type": null,
+							"description": "descr"
+						}
+					]
+				}`)
+		case "1":
+			fmt.Fprintf(w, `
+				{
+					"share_networks": [
+						{
+							"name": "net_my1",
+							"segmentation_id": null,
+							"created_at": "2015-09-04T14:57:13.000000",
+							"neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+							"updated_at": null,
+							"id": "32763294-e3d4-456a-998d-60047677c2fb",
+							"neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+							"ip_version": null,
+							"nova_net_id": null,
+							"cidr": null,
+							"project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+							"network_type": null,
+							"description": "descr"
+						}
+					]
+				}`)
+		case "2":
+			fmt.Fprintf(w, `
+				{
+					"share_networks": [
+						{
+							"name": "net_my1",
+							"segmentation_id": null,
+							"created_at": "2015-09-04T14:57:13.000000",
+							"neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+							"updated_at": null,
+							"id": "32763294-e3d4-456a-998d-60047677c2fb",
+							"neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+							"ip_version": null,
+							"nova_net_id": null,
+							"cidr": null,
+							"project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+							"network_type": null,
+							"description": "descr"
+						}
+					]
+				}`)
+		default:
+			fmt.Fprintf(w, `
+				{
+					"share_networks": []
+				}`)
+		}
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-networks/7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd", 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, `
+        {
+            "share_network": {
+                "name": "net_my1",
+                "segmentation_id": null,
+                "created_at": "2015-09-04T14:56:45.000000",
+                "neutron_subnet_id": "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+                "updated_at": null,
+                "id": "7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd",
+                "neutron_net_id": "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+                "ip_version": null,
+                "nova_net_id": null,
+                "cidr": null,
+                "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                "network_type": null,
+                "description": "descr"
+            }
+        }`)
+	})
+}
+
+func MockUpdateNeutronResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-networks/713df749-aac0-4a54-af52-10f6c991e80c", 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, `
+            {
+                "share_network": {
+                    "name": "net_my2",
+                    "segmentation_id": null,
+                    "created_at": "2015-09-04T14:54:25.000000",
+                    "neutron_subnet_id": "new-neutron-subnet-id",
+                    "updated_at": "2015-09-07T08:02:53.512184",
+                    "id": "713df749-aac0-4a54-af52-10f6c991e80c",
+                    "neutron_net_id": "new-neutron-id",
+                    "ip_version": 4,
+                    "nova_net_id": null,
+                    "cidr": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "network_type": null,
+                    "description": "new description"
+                }
+            }
+        `)
+	})
+}
+
+func MockUpdateNovaResponse(t *testing.T) {
+	th.Mux.HandleFunc("/share-networks/713df749-aac0-4a54-af52-10f6c991e80c", 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, `
+            {
+                "share_network": {
+                    "name": "net_my2",
+                    "segmentation_id": null,
+                    "created_at": "2015-09-04T14:54:25.000000",
+                    "neutron_subnet_id": null,
+                    "updated_at": "2015-09-07T08:02:53.512184",
+                    "id": "713df749-aac0-4a54-af52-10f6c991e80c",
+                    "neutron_net_id": null,
+                    "ip_version": 4,
+                    "nova_net_id": "new-nova-id",
+                    "cidr": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "network_type": null,
+                    "description": "new description"
+                }
+            }
+        `)
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/sharenetworks/testing/requests_test.go b/openstack/sharedfilesystems/v2/sharenetworks/testing/requests_test.go
new file mode 100644
index 0000000..46c2d92
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharenetworks/testing/requests_test.go
@@ -0,0 +1,238 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharenetworks"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// Verifies that a share network can be created correctly
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := &sharenetworks.CreateOpts{
+		Name:            "my_network",
+		Description:     "This is my share network",
+		NeutronNetID:    "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+		NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+	}
+
+	n, err := sharenetworks.Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "my_network")
+	th.AssertEquals(t, n.Description, "This is my share network")
+	th.AssertEquals(t, n.NeutronNetID, "998b42ee-2cee-4d36-8b95-67b5ca1f2109")
+	th.AssertEquals(t, n.NeutronSubnetID, "53482b62-2c84-4a53-b6ab-30d9d9800d06")
+}
+
+// Verifies that share network deletion works
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	res := sharenetworks.Delete(client.ServiceClient(), "fa158a3d-6d9f-4187-9ca5-abbb82646eb2")
+	th.AssertNoErr(t, res.Err)
+}
+
+// Verifies that share networks can be listed correctly
+func TestListDetail(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	allPages, err := sharenetworks.ListDetail(client.ServiceClient(), &sharenetworks.ListOpts{}).AllPages()
+
+	th.AssertNoErr(t, err)
+	actual, err := sharenetworks.ExtractShareNetworks(allPages)
+	th.AssertNoErr(t, err)
+
+	var nilTime time.Time
+	expected := []sharenetworks.ShareNetwork{
+		{
+			ID:              "32763294-e3d4-456a-998d-60047677c2fb",
+			Name:            "net_my1",
+			CreatedAt:       time.Date(2015, 9, 4, 14, 57, 13, 0, time.UTC),
+			Description:     "descr",
+			NetworkType:     "",
+			CIDR:            "",
+			NovaNetID:       "",
+			NeutronNetID:    "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+			NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+			IPVersion:       0,
+			SegmentationID:  0,
+			UpdatedAt:       nilTime,
+			ProjectID:       "16e1ab15c35a457e9c2b2aa189f544e1",
+		},
+		{
+			ID:              "713df749-aac0-4a54-af52-10f6c991e80c",
+			Name:            "net_my",
+			CreatedAt:       time.Date(2015, 9, 4, 14, 54, 25, 0, time.UTC),
+			Description:     "desecr",
+			NetworkType:     "",
+			CIDR:            "",
+			NovaNetID:       "",
+			NeutronNetID:    "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+			NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+			IPVersion:       0,
+			SegmentationID:  0,
+			UpdatedAt:       nilTime,
+			ProjectID:       "16e1ab15c35a457e9c2b2aa189f544e1",
+		},
+		{
+			ID:              "fa158a3d-6d9f-4187-9ca5-abbb82646eb2",
+			Name:            "",
+			CreatedAt:       time.Date(2015, 9, 4, 14, 51, 41, 0, time.UTC),
+			Description:     "",
+			NetworkType:     "",
+			CIDR:            "",
+			NovaNetID:       "",
+			NeutronNetID:    "",
+			NeutronSubnetID: "",
+			IPVersion:       0,
+			SegmentationID:  0,
+			UpdatedAt:       nilTime,
+			ProjectID:       "16e1ab15c35a457e9c2b2aa189f544e1",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+// Verifies that share networks list can be called with query parameters
+func TestPaginatedListDetail(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockFilteredListResponse(t)
+
+	options := &sharenetworks.ListOpts{
+		Offset: 0,
+		Limit:  1,
+	}
+
+	count := 0
+
+	err := sharenetworks.ListDetail(client.ServiceClient(), options).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		_, err := sharenetworks.ExtractShareNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract share networks: %v", err)
+			return false, err
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, count, 3)
+}
+
+// Verifies that it is possible to get a share network
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	var nilTime time.Time
+	expected := sharenetworks.ShareNetwork{
+		ID:              "7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd",
+		Name:            "net_my1",
+		CreatedAt:       time.Date(2015, 9, 4, 14, 56, 45, 0, time.UTC),
+		Description:     "descr",
+		NetworkType:     "",
+		CIDR:            "",
+		NovaNetID:       "",
+		NeutronNetID:    "998b42ee-2cee-4d36-8b95-67b5ca1f2109",
+		NeutronSubnetID: "53482b62-2c84-4a53-b6ab-30d9d9800d06",
+		IPVersion:       0,
+		SegmentationID:  0,
+		UpdatedAt:       nilTime,
+		ProjectID:       "16e1ab15c35a457e9c2b2aa189f544e1",
+	}
+
+	n, err := sharenetworks.Get(client.ServiceClient(), "7f950b52-6141-4a08-bbb5-bb7ffa3ea5fd").Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, &expected, n)
+}
+
+// Verifies that it is possible to update a share network using neutron network
+func TestUpdateNeutron(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUpdateNeutronResponse(t)
+
+	expected := sharenetworks.ShareNetwork{
+		ID:              "713df749-aac0-4a54-af52-10f6c991e80c",
+		Name:            "net_my2",
+		CreatedAt:       time.Date(2015, 9, 4, 14, 54, 25, 0, time.UTC),
+		Description:     "new description",
+		NetworkType:     "",
+		CIDR:            "",
+		NovaNetID:       "",
+		NeutronNetID:    "new-neutron-id",
+		NeutronSubnetID: "new-neutron-subnet-id",
+		IPVersion:       4,
+		SegmentationID:  0,
+		UpdatedAt:       time.Date(2015, 9, 7, 8, 2, 53, 512184000, time.UTC),
+		ProjectID:       "16e1ab15c35a457e9c2b2aa189f544e1",
+	}
+
+	options := sharenetworks.UpdateOpts{
+		Name:            "net_my2",
+		Description:     "new description",
+		NeutronNetID:    "new-neutron-id",
+		NeutronSubnetID: "new-neutron-subnet-id",
+	}
+
+	v, err := sharenetworks.Update(client.ServiceClient(), "713df749-aac0-4a54-af52-10f6c991e80c", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &expected, v)
+}
+
+// Verifies that it is possible to update a share network using nova network
+func TestUpdateNova(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUpdateNovaResponse(t)
+
+	expected := sharenetworks.ShareNetwork{
+		ID:              "713df749-aac0-4a54-af52-10f6c991e80c",
+		Name:            "net_my2",
+		CreatedAt:       time.Date(2015, 9, 4, 14, 54, 25, 0, time.UTC),
+		Description:     "new description",
+		NetworkType:     "",
+		CIDR:            "",
+		NovaNetID:       "new-nova-id",
+		NeutronNetID:    "",
+		NeutronSubnetID: "",
+		IPVersion:       4,
+		SegmentationID:  0,
+		UpdatedAt:       time.Date(2015, 9, 7, 8, 2, 53, 512184000, time.UTC),
+		ProjectID:       "16e1ab15c35a457e9c2b2aa189f544e1",
+	}
+
+	options := sharenetworks.UpdateOpts{
+		Name:        "net_my2",
+		Description: "new description",
+		NovaNetID:   "new-nova-id",
+	}
+
+	v, err := sharenetworks.Update(client.ServiceClient(), "713df749-aac0-4a54-af52-10f6c991e80c", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &expected, v)
+}
diff --git a/openstack/sharedfilesystems/v2/sharenetworks/urls.go b/openstack/sharedfilesystems/v2/sharenetworks/urls.go
new file mode 100644
index 0000000..6198c54
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharenetworks/urls.go
@@ -0,0 +1,23 @@
+package sharenetworks
+
+import "github.com/gophercloud/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("share-networks")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("share-networks", id)
+}
+
+func listDetailURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("share-networks", "detail")
+}
+
+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/sharedfilesystems/v2/shares/requests.go b/openstack/sharedfilesystems/v2/shares/requests.go
new file mode 100644
index 0000000..cfa8460
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/shares/requests.go
@@ -0,0 +1,81 @@
+package shares
+
+import (
+	"github.com/gophercloud/gophercloud"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToShareCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains the options for create a Share. This object is
+// passed to shares.Create(). For more information about these parameters,
+// please refer to the Share object, or the shared file systems API v2
+// documentation
+type CreateOpts struct {
+	// Defines the share protocol to use
+	ShareProto string `json:"share_proto" required:"true"`
+	// Size in GB
+	Size int `json:"size" required:"true"`
+	// Defines the share name
+	Name string `json:"name,omitempty"`
+	// Share description
+	Description string `json:"description,omitempty"`
+	// DisplayName is equivalent to Name. The API supports using both
+	// This is an inherited attribute from the block storage API
+	DisplayName string `json:"display_name,omitempty"`
+	// DisplayDescription is equivalent to Description. The API supports using bot
+	// This is an inherited attribute from the block storage API
+	DisplayDescription string `json:"display_description,omitempty"`
+	// ShareType defines the sharetype. If omitted, a default share type is used
+	ShareType string `json:"share_type,omitempty"`
+	// VolumeType is deprecated but supported. Either ShareType or VolumeType can be used
+	VolumeType string `json:"volume_type,omitempty"`
+	// The UUID from which to create a share
+	SnapshotID string `json:"snapshot_id,omitempty"`
+	// Determines whether or not the share is public
+	IsPublic *bool `json:"is_public,omitempty"`
+	// Key value pairs of user defined metadata
+	Metadata map[string]string `json:"metadata,omitempty"`
+	// The UUID of the share network to which the share belongs to
+	ShareNetworkID string `json:"share_network_id,omitempty"`
+	// The UUID of the consistency group to which the share belongs to
+	ConsistencyGroupID string `json:"consistency_group_id,omitempty"`
+	// The availability zone of the share
+	AvailabilityZone string `json:"availability_zone,omitempty"`
+}
+
+// ToShareCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToShareCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "share")
+}
+
+// Create will create a new Share based on the values in CreateOpts. To extract
+// the Share object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToShareCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
+// Delete will delete an existing Share with the given UUID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// Get will get a single share with given UUID
+func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
+	_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/sharedfilesystems/v2/shares/results.go b/openstack/sharedfilesystems/v2/shares/results.go
new file mode 100644
index 0000000..224d1df
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/shares/results.go
@@ -0,0 +1,112 @@
+package shares
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// Share contains all information associated with an OpenStack Share
+type Share struct {
+	// The availability zone of the share
+	AvailabilityZone string `json:"availability_zone"`
+	// A description of the share
+	Description string `json:"description,omitempty"`
+	// DisplayDescription is inherited from BlockStorage API.
+	// Both Description and DisplayDescription can be used
+	DisplayDescription string `json:"display_description,omitempty"`
+	// DisplayName is inherited from BlockStorage API
+	// Both DisplayName and Name can be used
+	DisplayName string `json:"display_name,omitempty"`
+	// Indicates whether a share has replicas or not.
+	HasReplicas bool `json:"has_replicas"`
+	// The host name of the share
+	Host string `json:"host"`
+	// The UUID of the share
+	ID string `json:"id"`
+	// Indicates the visibility of the share
+	IsPublic bool `json:"is_public,omitempty"`
+	// Share links for pagination
+	Links []map[string]string `json:"links"`
+	// Key, value -pairs of custom metadata
+	Metadata map[string]string `json:"metadata,omitempty"`
+	// The name of the share
+	Name string `json:"name,omitempty"`
+	// The UUID of the project to which this share belongs to
+	ProjectID string `json:"project_id"`
+	// The share replication type
+	ReplicationType string `json:"replication_type,omitempty"`
+	// The UUID of the share network
+	ShareNetworkID string `json:"share_network_id"`
+	// The shared file system protocol
+	ShareProto string `json:"share_proto"`
+	// The UUID of the share server
+	ShareServerID string `json:"share_server_id"`
+	// The UUID of the share type.
+	ShareType string `json:"share_type"`
+	// The name of the share type.
+	ShareTypeName string `json:"share_type_name"`
+	// Size of the share in GB
+	Size int `json:"size"`
+	// UUID of the snapshot from which to create the share
+	SnapshotID string `json:"snapshot_id"`
+	// The share status
+	Status string `json:"status"`
+	// The task state, used for share migration
+	TaskState string `json:"task_state"`
+	// The type of the volume
+	VolumeType string `json:"volume_type,omitempty"`
+	// The UUID of the consistency group this share belongs to
+	ConsistencyGroupID string `json:"consistency_group_id"`
+	// Used for filtering backends which either support or do not support share snapshots
+	SnapshotSupport          bool   `json:"snapshot_support"`
+	SourceCgsnapshotMemberID string `json:"source_cgsnapshot_member_id"`
+	// Timestamp when the share was created
+	CreatedAt time.Time `json:"-"`
+}
+
+func (r *Share) UnmarshalJSON(b []byte) error {
+	type tmp Share
+	var s struct {
+		tmp
+		CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = Share(s.tmp)
+
+	r.CreatedAt = time.Time(s.CreatedAt)
+
+	return nil
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the Share object from the commonResult
+func (r commonResult) Extract() (*Share, error) {
+	var s struct {
+		Share *Share `json:"share"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Share, err
+}
+
+// CreateResult contains the result..
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult contains the delete results
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// GetResult contains the get result
+type GetResult struct {
+	commonResult
+}
diff --git a/openstack/sharedfilesystems/v2/shares/testing/fixtures.go b/openstack/sharedfilesystems/v2/shares/testing/fixtures.go
new file mode 100644
index 0000000..83b174f
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/shares/testing/fixtures.go
@@ -0,0 +1,142 @@
+package testing
+
+import (
+	"fmt"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+	"net/http"
+	"testing"
+)
+
+const (
+	shareEndpoint = "/shares"
+	shareID       = "011d21e2-fbc3-4e4a-9993-9ea223f73264"
+)
+
+var createRequest = `{
+		"share": {
+			"name": "my_test_share",
+			"size": 1,
+			"share_proto": "NFS"
+		}
+	}`
+
+var createResponse = `{
+		"share": {
+			"name": "my_test_share",
+			"share_proto": "NFS",
+			"size": 1,
+			"status": null,
+			"share_server_id": null,
+			"project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+			"share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7",
+			"share_type_name": "default",
+			"availability_zone": null,
+			"created_at": "2015-09-18T10:25:24.533287",
+			"export_location": null,
+			"links": [
+				{
+					"href": "http://172.18.198.54:8786/v1/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264",
+					"rel": "self"
+				},
+				{
+					"href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264",
+					"rel": "bookmark"
+				}
+			],
+			"share_network_id": null,
+			"export_locations": [],
+			"host": null,
+			"access_rules_status": "active",
+			"has_replicas": false,
+			"replication_type": null,
+			"task_state": null,
+			"snapshot_support": true,
+			"consistency_group_id": "9397c191-8427-4661-a2e8-b23820dc01d4",
+			"source_cgsnapshot_member_id": null,
+			"volume_type": "default",
+			"snapshot_id": null,
+			"is_public": true,
+			"metadata": {
+				"project": "my_app",
+				"aim": "doc"
+			},
+			"id": "011d21e2-fbc3-4e4a-9993-9ea223f73264",
+			"description": "My custom share London"
+		}
+	}`
+
+// MockCreateResponse creates a mock response
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc(shareEndpoint, 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, createRequest)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, createResponse)
+	})
+}
+
+// MockDeleteResponse creates a mock delete response
+func MockDeleteResponse(t *testing.T) {
+	th.Mux.HandleFunc(shareEndpoint+"/"+shareID, 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)
+	})
+}
+
+var getResponse = `{
+    "share": {
+        "links": [
+            {
+                "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264",
+                "rel": "self"
+            },
+            {
+                "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264",
+                "rel": "bookmark"
+            }
+        ],
+        "availability_zone": "nova",
+        "share_network_id": "713df749-aac0-4a54-af52-10f6c991e80c",
+        "share_server_id": "e268f4aa-d571-43dd-9ab3-f49ad06ffaef",
+        "snapshot_id": null,
+        "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264",
+        "size": 1,
+        "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7",
+        "share_type_name": "default",
+        "consistency_group_id": "9397c191-8427-4661-a2e8-b23820dc01d4",
+        "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+        "metadata": {
+            "project": "my_app",
+            "aim": "doc"
+        },
+        "status": "available",
+        "description": "My custom share London",
+        "host": "manila2@generic1#GENERIC1",
+        "has_replicas": false,
+        "replication_type": null,
+        "task_state": null,
+        "is_public": true,
+        "snapshot_support": true,
+        "name": "my_test_share",
+        "created_at": "2015-09-18T10:25:24.000000",
+        "share_proto": "NFS",
+        "volume_type": "default",
+        "source_cgsnapshot_member_id": null
+    }
+}`
+
+// MockGetResponse creates a mock get response
+func MockGetResponse(t *testing.T) {
+	th.Mux.HandleFunc(shareEndpoint+"/"+shareID, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, getResponse)
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/shares/testing/request_test.go b/openstack/sharedfilesystems/v2/shares/testing/request_test.go
new file mode 100644
index 0000000..5b700a6
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/shares/testing/request_test.go
@@ -0,0 +1,84 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shares"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	options := &shares.CreateOpts{Size: 1, Name: "my_test_share", ShareProto: "NFS"}
+	n, err := shares.Create(client.ServiceClient(), options).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, n.Name, "my_test_share")
+	th.AssertEquals(t, n.Size, 1)
+	th.AssertEquals(t, n.ShareProto, "NFS")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+
+	result := shares.Delete(client.ServiceClient(), shareID)
+	th.AssertNoErr(t, result.Err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetResponse(t)
+
+	s, err := shares.Get(client.ServiceClient(), shareID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, s, &shares.Share{
+		AvailabilityZone:   "nova",
+		ShareNetworkID:     "713df749-aac0-4a54-af52-10f6c991e80c",
+		ShareServerID:      "e268f4aa-d571-43dd-9ab3-f49ad06ffaef",
+		SnapshotID:         "",
+		ID:                 shareID,
+		Size:               1,
+		ShareType:          "25747776-08e5-494f-ab40-a64b9d20d8f7",
+		ShareTypeName:      "default",
+		ConsistencyGroupID: "9397c191-8427-4661-a2e8-b23820dc01d4",
+		ProjectID:          "16e1ab15c35a457e9c2b2aa189f544e1",
+		Metadata: map[string]string{
+			"project": "my_app",
+			"aim":     "doc",
+		},
+		Status:                   "available",
+		Description:              "My custom share London",
+		Host:                     "manila2@generic1#GENERIC1",
+		HasReplicas:              false,
+		ReplicationType:          "",
+		TaskState:                "",
+		SnapshotSupport:          true,
+		Name:                     "my_test_share",
+		CreatedAt:                time.Date(2015, time.September, 18, 10, 25, 24, 0, time.UTC),
+		ShareProto:               "NFS",
+		VolumeType:               "default",
+		SourceCgsnapshotMemberID: "",
+		IsPublic:                 true,
+		Links: []map[string]string{
+			{
+				"href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264",
+				"rel":  "self",
+			},
+			{
+				"href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264",
+				"rel":  "bookmark",
+			},
+		},
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/shares/urls.go b/openstack/sharedfilesystems/v2/shares/urls.go
new file mode 100644
index 0000000..309f071
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/shares/urls.go
@@ -0,0 +1,15 @@
+package shares
+
+import "github.com/gophercloud/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("shares")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("shares", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("shares", id)
+}
diff --git a/openstack/sharedfilesystems/v2/sharetypes/requests.go b/openstack/sharedfilesystems/v2/sharetypes/requests.go
new file mode 100644
index 0000000..8bfc632
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharetypes/requests.go
@@ -0,0 +1,151 @@
+package sharetypes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToShareTypeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains options for creating a ShareType. This object is
+// passed to the sharetypes.Create function. For more information about
+// these parameters, see the ShareType object.
+type CreateOpts struct {
+	// The share type name
+	Name string `json:"name" required:"true"`
+	// Indicates whether a share type is publicly accessible
+	IsPublic bool `json:"os-share-type-access:is_public"`
+	// The extra specifications for the share type
+	ExtraSpecs ExtraSpecsOpts `json:"extra_specs" required:"true"`
+}
+
+// ExtraSpecsOpts represent the extra specifications that can be selected for a share type
+type ExtraSpecsOpts struct {
+	// An extra specification that defines the driver mode for share server, or storage, life cycle management
+	DriverHandlesShareServers bool `json:"driver_handles_share_servers" required:"true"`
+	// An extra specification that filters back ends by whether they do or do not support share snapshots
+	SnapshotSupport *bool `json:"snapshot_support,omitempty"`
+}
+
+// ToShareTypeCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToShareTypeCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "share_type")
+}
+
+// Create will create a new ShareType based on the values in CreateOpts. To
+// extract the ShareType object from the response, call the Extract method
+// on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToShareTypeCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// Delete will delete the existing ShareType with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	_, r.Err = client.Delete(deleteURL(client, id), nil)
+	return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToShareTypeListQuery() (string, error)
+}
+
+// ListOpts holds options for listing ShareTypes. It is passed to the
+// sharetypes.List function.
+type ListOpts struct {
+	// Select if public types, private types, or both should be listed
+	IsPublic string `q:"is_public"`
+}
+
+// ToShareTypeListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToShareTypeListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List returns ShareTypes 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.ToShareTypeListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return ShareTypePage{pagination.SinglePageBase(r)}
+	})
+}
+
+// GetDefault will retrieve the default ShareType.
+func GetDefault(client *gophercloud.ServiceClient) (r GetDefaultResult) {
+	_, r.Err = client.Get(getDefaultURL(client), &r.Body, nil)
+	return
+}
+
+// GetExtraSpecs will retrieve the extra specifications for a given ShareType.
+func GetExtraSpecs(client *gophercloud.ServiceClient, id string) (r GetExtraSpecsResult) {
+	_, r.Err = client.Get(getExtraSpecsURL(client, id), &r.Body, nil)
+	return
+}
+
+// SetExtraSpecsOptsBuilder allows extensions to add additional parameters to the
+// SetExtraSpecs request.
+type SetExtraSpecsOptsBuilder interface {
+	ToShareTypeSetExtraSpecsMap() (map[string]interface{}, error)
+}
+
+type SetExtraSpecsOpts struct {
+	// A list of all extra specifications to be added to a ShareType
+	Specs map[string]interface{} `json:"extra_specs"`
+}
+
+// ToShareTypeSetExtraSpecsMap assembles a request body based on the contents of a
+// SetExtraSpecsOpts.
+func (opts SetExtraSpecsOpts) ToShareTypeSetExtraSpecsMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// SetExtraSpecs will set new specifications for a ShareType based on the values
+// in SetExtraSpecsOpts. To extract the extra specifications object from the response,
+// call the Extract method on the SetExtraSpecsResult.
+func SetExtraSpecs(client *gophercloud.ServiceClient, id string, opts SetExtraSpecsOptsBuilder) (r SetExtraSpecsResult) {
+	b, err := opts.ToShareTypeSetExtraSpecsMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	_, r.Err = client.Post(setExtraSpecsURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 202},
+	})
+	return
+}
+
+// UnsetExtraSpecs will unset an extra specification for an existing ShareType.
+func UnsetExtraSpecs(client *gophercloud.ServiceClient, id string, key string) (r UnsetExtraSpecsResult) {
+	_, r.Err = client.Delete(unsetExtraSpecsURL(client, id, key), nil)
+	return
+}
+
+// ShowAccess will show access details for an existing ShareType.
+func ShowAccess(client *gophercloud.ServiceClient, id string) (r ShowAccessResult) {
+	_, r.Err = client.Get(showAccessURL(client, id), &r.Body, nil)
+	return
+}
diff --git a/openstack/sharedfilesystems/v2/sharetypes/results.go b/openstack/sharedfilesystems/v2/sharetypes/results.go
new file mode 100644
index 0000000..92a8fab
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharetypes/results.go
@@ -0,0 +1,129 @@
+package sharetypes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// ShareType contains all the information associated with an OpenStack
+// ShareType.
+type ShareType struct {
+	// The Share Type ID
+	ID string `json:"id"`
+	// The Share Type name
+	Name string `json:"name"`
+	// Indicates whether a share type is publicly accessible
+	IsPublic bool `json:"os-share-type-access:is_public"`
+	// The required extra specifications for the share type
+	RequiredExtraSpecs map[string]interface{} `json:"required_extra_specs"`
+	// The extra specifications for the share type
+	ExtraSpecs map[string]interface{} `json:"extra_specs"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the ShareType object out of the commonResult object.
+func (r commonResult) Extract() (*ShareType, error) {
+	var s struct {
+		ShareType *ShareType `json:"share_type"`
+	}
+	err := r.ExtractInto(&s)
+	return s.ShareType, err
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// DeleteResult contains the response body and error from a Delete request.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// ShareTypePage is a pagination.pager that is returned from a call to the List function.
+type ShareTypePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no ShareTypes.
+func (r ShareTypePage) IsEmpty() (bool, error) {
+	shareTypes, err := ExtractShareTypes(r)
+	return len(shareTypes) == 0, err
+}
+
+// ExtractShareTypes extracts and returns ShareTypes. It is used while
+// iterating over a sharetypes.List call.
+func ExtractShareTypes(r pagination.Page) ([]ShareType, error) {
+	var s struct {
+		ShareTypes []ShareType `json:"share_types"`
+	}
+	err := (r.(ShareTypePage)).ExtractInto(&s)
+	return s.ShareTypes, err
+}
+
+// GetDefaultResult contains the response body and error from a Get Default request.
+type GetDefaultResult struct {
+	commonResult
+}
+
+// ExtraSpecs contains all the information associated with extra specifications
+// for an Openstack ShareType.
+type ExtraSpecs map[string]interface{}
+
+type extraSpecsResult struct {
+	gophercloud.Result
+}
+
+// Extract will get the ExtraSpecs object out of the commonResult object.
+func (r extraSpecsResult) Extract() (ExtraSpecs, error) {
+	var s struct {
+		Specs ExtraSpecs `json:"extra_specs"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Specs, err
+}
+
+// GetExtraSpecsResult contains the response body and error from a Get Extra Specs request.
+type GetExtraSpecsResult struct {
+	extraSpecsResult
+}
+
+// SetExtraSpecsResult contains the response body and error from a Set Extra Specs request.
+type SetExtraSpecsResult struct {
+	extraSpecsResult
+}
+
+// UnsetExtraSpecsResult contains the response body and error from a Unset Extra Specs request.
+type UnsetExtraSpecsResult struct {
+	gophercloud.ErrResult
+}
+
+// ShareTypeAccess contains all the information associated with an OpenStack
+// ShareTypeAccess.
+type ShareTypeAccess struct {
+	// The share type ID of the member.
+	ShareTypeID string `json:"share_type_id"`
+	// The UUID of the project for which access to the share type is granted.
+	ProjectID string `json:"project_id"`
+}
+
+type shareTypeAccessResult struct {
+	gophercloud.Result
+}
+
+// ShowAccessResult contains the response body and error from a Show access request.
+type ShowAccessResult struct {
+	shareTypeAccessResult
+}
+
+// Extract will get the ShareTypeAccess objects out of the shareTypeAccessResult object.
+func (r ShowAccessResult) Extract() ([]ShareTypeAccess, error) {
+	var s struct {
+		ShareTypeAccess []ShareTypeAccess `json:"share_type_access"`
+	}
+	err := r.ExtractInto(&s)
+	return s.ShareTypeAccess, err
+}
diff --git a/openstack/sharedfilesystems/v2/sharetypes/testing/fixtures.go b/openstack/sharedfilesystems/v2/sharetypes/testing/fixtures.go
new file mode 100644
index 0000000..03fc83b
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharetypes/testing/fixtures.go
@@ -0,0 +1,240 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types", 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, `
+        {
+            "share_type": {
+                "os-share-type-access:is_public": true,
+                "extra_specs": {
+                    "driver_handles_share_servers": true,
+                    "snapshot_support": true
+                },
+                "name": "my_new_share_type"
+            }
+        }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+        {
+            "volume_type": {
+                "os-share-type-access:is_public": true,
+                "required_extra_specs": {
+                    "driver_handles_share_servers": true
+                },
+                "extra_specs": {
+                    "snapshot_support": "True",
+                    "driver_handles_share_servers": "True"
+                },
+                "name": "my_new_share_type",
+                "id": "1d600d02-26a7-4b23-af3d-7d51860fe858"
+            },
+            "share_type": {
+                "os-share-type-access:is_public": true,
+                "required_extra_specs": {
+                    "driver_handles_share_servers": true
+                },
+                "extra_specs": {
+                    "snapshot_support": "True",
+                    "driver_handles_share_servers": "True"
+                },
+                "name": "my_new_share_type",
+                "id": "1d600d02-26a7-4b23-af3d-7d51860fe858"
+            }
+        }`)
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/shareTypeID", 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 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": [
+                {
+                    "os-share-type-access:is_public": true,
+                    "required_extra_specs": {
+                        "driver_handles_share_servers": "True"
+                    },
+                    "extra_specs": {
+                        "snapshot_support": "True",
+                        "driver_handles_share_servers": "True"
+                    },
+                    "name": "default",
+                    "id": "be27425c-f807-4500-a056-d00721db45cf"
+                },
+                {
+                    "os-share-type-access:is_public": true,
+                    "required_extra_specs": {
+                        "driver_handles_share_servers": "false"
+                    },
+                    "extra_specs": {
+                        "snapshot_support": "True",
+                        "driver_handles_share_servers": "false"
+                    },
+                    "name": "d",
+                    "id": "f015bebe-c38b-4c49-8832-00143b10253b"
+                }
+            ],
+            "share_types": [
+                {
+                    "os-share-type-access:is_public": true,
+                    "required_extra_specs": {
+                        "driver_handles_share_servers": "True"
+                    },
+                    "extra_specs": {
+                        "snapshot_support": "True",
+                        "driver_handles_share_servers": "True"
+                    },
+                    "name": "default",
+                    "id": "be27425c-f807-4500-a056-d00721db45cf"
+                },
+                {
+                    "os-share-type-access:is_public": true,
+                    "required_extra_specs": {
+                        "driver_handles_share_servers": "false"
+                    },
+                    "extra_specs": {
+                        "snapshot_support": "True",
+                        "driver_handles_share_servers": "false"
+                    },
+                    "name": "d",
+                    "id": "f015bebe-c38b-4c49-8832-00143b10253b"
+                }
+            ]
+        }`)
+	})
+}
+
+func MockGetDefaultResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/default", 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": {
+                "required_extra_specs": null,
+                "extra_specs": {
+                    "snapshot_support": "True",
+                    "driver_handles_share_servers": "True"
+                },
+                "name": "default",
+                "id": "be27425c-f807-4500-a056-d00721db45cf"
+            },
+            "share_type": {
+                "required_extra_specs": null,
+                "extra_specs": {
+                    "snapshot_support": "True",
+                    "driver_handles_share_servers": "True"
+                },
+                "name": "default",
+                "id": "be27425c-f807-4500-a056-d00721db45cf"
+            }
+        }`)
+	})
+}
+
+func MockGetExtraSpecsResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/shareTypeID/extra_specs", 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, `
+        {
+            "extra_specs": {
+                "snapshot_support": "True",
+                "driver_handles_share_servers": "True",
+                "my_custom_extra_spec": "False"
+            }
+        }`)
+	})
+}
+
+func MockSetExtraSpecsResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/shareTypeID/extra_specs", 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, `
+        {
+            "extra_specs": {
+                "my_key": "my_value"
+            }
+        }`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+        {
+            "extra_specs": {
+                "my_key": "my_value"
+            }
+        }`)
+	})
+}
+
+func MockUnsetExtraSpecsResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/shareTypeID/extra_specs/my_key", 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 MockShowAccessResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types/shareTypeID/share_type_access", 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, `
+        {
+            "share_type_access": [
+                {
+                    "share_type_id": "1732f284-401d-41d9-a494-425451e8b4b8",
+                    "project_id": "818a3f48dcd644909b3fa2e45a399a27"
+                },
+                {
+                    "share_type_id": "1732f284-401d-41d9-a494-425451e8b4b8",
+                    "project_id": "e1284adea3ee4d2482af5ed214f3ad90"
+                }
+            ]
+        }`)
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/sharetypes/testing/requests_test.go b/openstack/sharedfilesystems/v2/sharetypes/testing/requests_test.go
new file mode 100644
index 0000000..bcc1b16
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharetypes/testing/requests_test.go
@@ -0,0 +1,187 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/sharetypes"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// Verifies that a share type can be created correctly
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockCreateResponse(t)
+
+	snapshotSupport := true
+	extraSpecs := sharetypes.ExtraSpecsOpts{
+		DriverHandlesShareServers: true,
+		SnapshotSupport:           &snapshotSupport,
+	}
+
+	options := &sharetypes.CreateOpts{
+		Name:       "my_new_share_type",
+		IsPublic:   true,
+		ExtraSpecs: extraSpecs,
+	}
+
+	st, err := sharetypes.Create(client.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, st.Name, "my_new_share_type")
+	th.AssertEquals(t, st.IsPublic, true)
+}
+
+// Verifies that a share type can't be created if the required parameters are missing
+func TestCreateFails(t *testing.T) {
+	options := &sharetypes.CreateOpts{
+		Name: "my_new_share_type",
+	}
+
+	_, err := sharetypes.Create(client.ServiceClient(), options).Extract()
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+
+	extraSpecs := sharetypes.ExtraSpecsOpts{
+		DriverHandlesShareServers: true,
+	}
+
+	options = &sharetypes.CreateOpts{
+		ExtraSpecs: extraSpecs,
+	}
+
+	_, err = sharetypes.Create(client.ServiceClient(), options).Extract()
+	if _, ok := err.(gophercloud.ErrMissingInput); !ok {
+		t.Fatal("ErrMissingInput was expected to occur")
+	}
+}
+
+// Verifies that share type deletion works
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteResponse(t)
+	res := sharetypes.Delete(client.ServiceClient(), "shareTypeID")
+	th.AssertNoErr(t, res.Err)
+}
+
+// Verifies that share types can be listed correctly
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	allPages, err := sharetypes.List(client.ServiceClient(), &sharetypes.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := sharetypes.ExtractShareTypes(allPages)
+	th.AssertNoErr(t, err)
+	expected := []sharetypes.ShareType{
+		{
+			ID:                 "be27425c-f807-4500-a056-d00721db45cf",
+			Name:               "default",
+			IsPublic:           true,
+			ExtraSpecs:         map[string]interface{}{"snapshot_support": "True", "driver_handles_share_servers": "True"},
+			RequiredExtraSpecs: map[string]interface{}{"driver_handles_share_servers": "True"},
+		},
+		{
+			ID:                 "f015bebe-c38b-4c49-8832-00143b10253b",
+			Name:               "d",
+			IsPublic:           true,
+			ExtraSpecs:         map[string]interface{}{"driver_handles_share_servers": "false", "snapshot_support": "True"},
+			RequiredExtraSpecs: map[string]interface{}{"driver_handles_share_servers": "True"},
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+// Verifies that it is possible to get the default share type
+func TestGetDefault(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetDefaultResponse(t)
+
+	expected := sharetypes.ShareType{
+		ID:                 "be27425c-f807-4500-a056-d00721db45cf",
+		Name:               "default",
+		ExtraSpecs:         map[string]interface{}{"snapshot_support": "True", "driver_handles_share_servers": "True"},
+		RequiredExtraSpecs: map[string]interface{}(nil),
+	}
+
+	actual, err := sharetypes.GetDefault(client.ServiceClient()).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &expected, actual)
+}
+
+// Verifies that it is possible to get the extra specifications for a share type
+func TestGetExtraSpecs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockGetExtraSpecsResponse(t)
+
+	st, err := sharetypes.GetExtraSpecs(client.ServiceClient(), "shareTypeID").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, st["snapshot_support"], "True")
+	th.AssertEquals(t, st["driver_handles_share_servers"], "True")
+	th.AssertEquals(t, st["my_custom_extra_spec"], "False")
+}
+
+// Verifies that an extra specs can be added to a share type
+func TestSetExtraSpecs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockSetExtraSpecsResponse(t)
+
+	options := &sharetypes.SetExtraSpecsOpts{
+		Specs: map[string]interface{}{"my_key": "my_value"},
+	}
+
+	es, err := sharetypes.SetExtraSpecs(client.ServiceClient(), "shareTypeID", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, es["my_key"], "my_value")
+}
+
+// Verifies that an extra specification can be unset for a share type
+func TestUnsetExtraSpecs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockUnsetExtraSpecsResponse(t)
+	res := sharetypes.UnsetExtraSpecs(client.ServiceClient(), "shareTypeID", "my_key")
+	th.AssertNoErr(t, res.Err)
+}
+
+// Verifies that it is possible to see the access for a share type
+func TestShowAccess(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockShowAccessResponse(t)
+
+	expected := []sharetypes.ShareTypeAccess{
+		{
+			ShareTypeID: "1732f284-401d-41d9-a494-425451e8b4b8",
+			ProjectID:   "818a3f48dcd644909b3fa2e45a399a27",
+		},
+		{
+			ShareTypeID: "1732f284-401d-41d9-a494-425451e8b4b8",
+			ProjectID:   "e1284adea3ee4d2482af5ed214f3ad90",
+		},
+	}
+
+	shareType, err := sharetypes.ShowAccess(client.ServiceClient(), "shareTypeID").Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, expected, shareType)
+}
diff --git a/openstack/sharedfilesystems/v2/sharetypes/urls.go b/openstack/sharedfilesystems/v2/sharetypes/urls.go
new file mode 100644
index 0000000..b963c7b
--- /dev/null
+++ b/openstack/sharedfilesystems/v2/sharetypes/urls.go
@@ -0,0 +1,35 @@
+package sharetypes
+
+import "github.com/gophercloud/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("types")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("types", id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func getDefaultURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("types", "default")
+}
+
+func getExtraSpecsURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("types", id, "extra_specs")
+}
+
+func setExtraSpecsURL(c *gophercloud.ServiceClient, id string) string {
+	return getExtraSpecsURL(c, id)
+}
+
+func unsetExtraSpecsURL(c *gophercloud.ServiceClient, id string, key string) string {
+	return c.ServiceURL("types", id, "extra_specs", key)
+}
+
+func showAccessURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("types", id, "share_type_access")
+}
diff --git a/openstack/testing/client_test.go b/openstack/testing/client_test.go
new file mode 100644
index 0000000..3fe768f
--- /dev/null
+++ b/openstack/testing/client_test.go
@@ -0,0 +1,315 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+const ID = "0123456789"
+
+func TestAuthenticatedClientV3(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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{
+		Username:         "me",
+		Password:         "secret",
+		DomainName:       "default",
+		TenantName:       "project",
+		IdentityEndpoint: th.Endpoint(),
+	}
+	client, err := openstack.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 := openstack.AuthenticatedClient(options)
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "01234567890", client.TokenID)
+}
+
+func TestIdentityAdminV3Client(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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": {
+        "audit_ids": ["VcxU2JYqT8OzfUVvrjEITQ", "qNUTIJntTzO1-XUk5STybw"],
+        "catalog": [
+            {
+                "endpoints": [
+                    {
+                        "id": "39dc322ce86c4111b4f06c2eeae0841b",
+                        "interface": "public",
+                        "region": "RegionOne",
+                        "url": "http://localhost:5000"
+                    },
+                    {
+                        "id": "ec642f27474842e78bf059f6c48f4e99",
+                        "interface": "internal",
+                        "region": "RegionOne",
+                        "url": "http://localhost:5000"
+                    },
+                    {
+                        "id": "c609fc430175452290b62a4242e8a7e8",
+                        "interface": "admin",
+                        "region": "RegionOne",
+                        "url": "http://localhost:35357"
+                    }
+                ],
+                "id": "4363ae44bdf34a3981fde3b823cb9aa2",
+                "type": "identity",
+                "name": "keystone"
+            }
+        ],
+        "expires_at": "2013-02-27T18:30:59.999999Z",
+        "is_domain": false,
+        "issued_at": "2013-02-27T16:30:59.999999Z",
+        "methods": [
+            "password"
+        ],
+        "project": {
+            "domain": {
+                "id": "1789d1",
+                "name": "example.com"
+            },
+            "id": "263fd9",
+            "name": "project-x"
+        },
+        "roles": [
+            {
+                "id": "76e72a",
+                "name": "admin"
+            },
+            {
+                "id": "f4f392",
+                "name": "member"
+            }
+        ],
+        "service_providers": [
+            {
+                "auth_url":"https://example.com:5000/v3/OS-FEDERATION/identity_providers/acme/protocols/saml2/auth",
+                "id": "sp1",
+                "sp_url": "https://example.com:5000/Shibboleth.sso/SAML2/ECP"
+            },
+            {
+                "auth_url":"https://other.example.com:5000/v3/OS-FEDERATION/identity_providers/acme/protocols/saml2/auth",
+                "id": "sp2",
+                "sp_url": "https://other.example.com:5000/Shibboleth.sso/SAML2/ECP"
+            }
+        ],
+        "user": {
+            "domain": {
+                "id": "1789d1",
+                "name": "example.com"
+            },
+            "id": "0ca8f6",
+            "name": "Joe",
+            "password_expires_at": "2016-11-06T15:32:17.000000"
+        }
+    }
+}
+	`)
+	})
+
+	options := gophercloud.AuthOptions{
+		Username:         "me",
+		Password:         "secret",
+		DomainID:         "12345",
+		IdentityEndpoint: th.Endpoint(),
+	}
+	pc, err := openstack.AuthenticatedClient(options)
+	th.AssertNoErr(t, err)
+	sc, err := openstack.NewIdentityV3(pc, gophercloud.EndpointOpts{
+		Availability: gophercloud.AvailabilityAdmin,
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "http://localhost:35357/", sc.Endpoint)
+}
+
+func testAuthenticatedClientFails(t *testing.T, endpoint string) {
+	options := gophercloud.AuthOptions{
+		Username:         "me",
+		Password:         "secret",
+		DomainName:       "default",
+		TenantName:       "project",
+		IdentityEndpoint: endpoint,
+	}
+	_, err := openstack.AuthenticatedClient(options)
+	if err == nil {
+		t.Fatal("expected error but call succeeded")
+	}
+}
+
+func TestAuthenticatedClientV3Fails(t *testing.T) {
+	testAuthenticatedClientFails(t, "http://bad-address.example.com/v3")
+}
+
+func TestAuthenticatedClientV2Fails(t *testing.T) {
+	testAuthenticatedClientFails(t, "http://bad-address.example.com/v2.0")
+}
diff --git a/openstack/testing/doc.go b/openstack/testing/doc.go
new file mode 100644
index 0000000..34cfe7a
--- /dev/null
+++ b/openstack/testing/doc.go
@@ -0,0 +1,2 @@
+// openstack
+package testing
diff --git a/openstack/testing/endpoint_location_test.go b/openstack/testing/endpoint_location_test.go
new file mode 100644
index 0000000..ea7bdd2
--- /dev/null
+++ b/openstack/testing/endpoint_location_test.go
@@ -0,0 +1,231 @@
+package testing
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+	tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+	th "github.com/gophercloud/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 := openstack.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) {
+	_, actual := openstack.V2EndpointURL(&catalog2, gophercloud.EndpointOpts{
+		Type:         "nope",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	expected := &gophercloud.ErrEndpointNotFound{}
+	th.CheckEquals(t, expected.Error(), actual.Error())
+}
+
+func TestV2EndpointMultiple(t *testing.T) {
+	_, err := openstack.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 := openstack.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 := openstack.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) {
+	_, actual := openstack.V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+		Type:         "nope",
+		Availability: gophercloud.AvailabilityPublic,
+	})
+	expected := &gophercloud.ErrEndpointNotFound{}
+	th.CheckEquals(t, expected.Error(), actual.Error())
+}
+
+func TestV3EndpointMultiple(t *testing.T) {
+	_, err := openstack.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 := openstack.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/utils/choose_version.go b/openstack/utils/choose_version.go
new file mode 100644
index 0000000..c605d08
--- /dev/null
+++ b/openstack/utils/choose_version.go
@@ -0,0 +1,114 @@
+package utils
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gophercloud/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/testing/choose_version_test.go b/openstack/utils/testing/choose_version_test.go
new file mode 100644
index 0000000..9c0119c
--- /dev/null
+++ b/openstack/utils/testing/choose_version_test.go
@@ -0,0 +1,119 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/utils"
+	"github.com/gophercloud/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 := &utils.Version{ID: "v2.0", Priority: 2, Suffix: "blarg"}
+	v3 := &utils.Version{ID: "v3.0", Priority: 3, Suffix: "hargl"}
+
+	c := &gophercloud.ProviderClient{
+		IdentityBase:     testhelper.Endpoint(),
+		IdentityEndpoint: "",
+	}
+	v, endpoint, err := utils.ChooseVersion(c, []*utils.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 := &utils.Version{ID: "v2.0", Priority: 2, Suffix: "nope"}
+	v3 := &utils.Version{ID: "v3.0", Priority: 3, Suffix: "northis"}
+
+	c := &gophercloud.ProviderClient{
+		IdentityBase:     testhelper.Endpoint(),
+		IdentityEndpoint: testhelper.Endpoint() + "v2.0/",
+	}
+	v, endpoint, err := utils.ChooseVersion(c, []*utils.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 := &utils.Version{ID: "v2.0", Priority: 2, Suffix: "/v2.0/"}
+	v3 := &utils.Version{ID: "v3.0", Priority: 3, Suffix: "/v3.0/"}
+
+	c := &gophercloud.ProviderClient{
+		IdentityBase:     testhelper.Endpoint(),
+		IdentityEndpoint: testhelper.Endpoint() + "v2.0/",
+	}
+	v, endpoint, err := utils.ChooseVersion(c, []*utils.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/openstack/utils/testing/doc.go b/openstack/utils/testing/doc.go
new file mode 100644
index 0000000..66ecc07
--- /dev/null
+++ b/openstack/utils/testing/doc.go
@@ -0,0 +1,2 @@
+//utils
+package testing
diff --git a/pagination/http.go b/pagination/http.go
new file mode 100644
index 0000000..cb4b4ae
--- /dev/null
+++ b/pagination/http.go
@@ -0,0 +1,60 @@
+package pagination
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/gophercloud/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.Get(url, nil, &gophercloud.RequestOpts{
+		MoreHeaders: headers,
+		OkCodes:     []int{200, 204},
+	})
+}
diff --git a/pagination/linked.go b/pagination/linked.go
new file mode 100644
index 0000000..3656fb7
--- /dev/null
+++ b/pagination/linked.go
@@ -0,0 +1,92 @@
+package pagination
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// 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 {
+		err := gophercloud.ErrUnexpectedType{}
+		err.Expected = "map[string]interface{}"
+		err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
+		return "", err
+	}
+
+	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 {
+				err := gophercloud.ErrUnexpectedType{}
+				err.Expected = "map[string]interface{}"
+				err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value))
+				return "", err
+			}
+		} else {
+			if value == nil {
+				// Actual null element.
+				return "", nil
+			}
+
+			url, ok := value.(string)
+			if !ok {
+				err := gophercloud.ErrUnexpectedType{}
+				err.Expected = "string"
+				err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value))
+				return "", err
+			}
+
+			return url, nil
+		}
+	}
+}
+
+// IsEmpty satisifies the IsEmpty method of the Page interface
+func (current LinkedPageBase) IsEmpty() (bool, error) {
+	if b, ok := current.Body.([]interface{}); ok {
+		return len(b) == 0, nil
+	}
+	err := gophercloud.ErrUnexpectedType{}
+	err.Expected = "[]interface{}"
+	err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
+	return true, err
+}
+
+// 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/marker.go b/pagination/marker.go
new file mode 100644
index 0000000..52e53ba
--- /dev/null
+++ b/pagination/marker.go
@@ -0,0 +1,58 @@
+package pagination
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// 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
+}
+
+// IsEmpty satisifies the IsEmpty method of the Page interface
+func (current MarkerPageBase) IsEmpty() (bool, error) {
+	if b, ok := current.Body.([]interface{}); ok {
+		return len(b) == 0, nil
+	}
+	err := gophercloud.ErrUnexpectedType{}
+	err.Expected = "[]interface{}"
+	err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
+	return true, err
+}
+
+// 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/pager.go b/pagination/pager.go
new file mode 100644
index 0000000..6f1609e
--- /dev/null
+++ b/pagination/pager.go
@@ -0,0 +1,238 @@
+package pagination
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"strings"
+
+	"github.com/gophercloud/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)
+
+	// if it's a single page, just return the testPage (first page)
+	if _, found := pageType.FieldByName("SinglePageBase"); found {
+		return testPage, nil
+	}
+
+	// Switch on the page body type. Recognized types are `map[string]interface{}`,
+	// `[]byte`, and `[]interface{}`.
+	switch pb := 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, v := range b {
+				// If it's a linked page, we don't want the `links`, we want the other one.
+				if !strings.HasSuffix(k, "links") {
+					// check the field's type. we only want []interface{} (which is really []map[string]interface{})
+					switch vt := v.(type) {
+					case []interface{}:
+						key = k
+						pagesSlice = append(pagesSlice, vt...)
+					}
+				}
+			}
+			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:
+		err := gophercloud.ErrUnexpectedType{}
+		err.Expected = "map[string]interface{}/[]byte/[]interface{}"
+		err.Actual = fmt.Sprintf("%T", pb)
+		return nil, err
+	}
+
+	// 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/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..4251d64
--- /dev/null
+++ b/pagination/single.go
@@ -0,0 +1,33 @@
+package pagination
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// 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
+}
+
+// IsEmpty satisifies the IsEmpty method of the Page interface
+func (current SinglePageBase) IsEmpty() (bool, error) {
+	if b, ok := current.Body.([]interface{}); ok {
+		return len(b) == 0, nil
+	}
+	err := gophercloud.ErrUnexpectedType{}
+	err.Expected = "[]interface{}"
+	err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body))
+	return true, err
+}
+
+// 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/testing/doc.go b/pagination/testing/doc.go
new file mode 100644
index 0000000..0bc1eb3
--- /dev/null
+++ b/pagination/testing/doc.go
@@ -0,0 +1,2 @@
+// pagination
+package testing
diff --git a/pagination/testing/linked_test.go b/pagination/testing/linked_test.go
new file mode 100644
index 0000000..3533e44
--- /dev/null
+++ b/pagination/testing/linked_test.go
@@ -0,0 +1,112 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/pagination"
+	"github.com/gophercloud/gophercloud/testhelper"
+)
+
+// LinkedPager sample and test cases.
+
+type LinkedPageResult struct {
+	pagination.LinkedPageBase
+}
+
+func (r LinkedPageResult) IsEmpty() (bool, error) {
+	is, err := ExtractLinkedInts(r)
+	return len(is) == 0, err
+}
+
+func ExtractLinkedInts(r pagination.Page) ([]int, error) {
+	var s struct {
+		Ints []int `json:"ints"`
+	}
+	err := (r.(LinkedPageResult)).ExtractInto(&s)
+	return s.Ints, err
+}
+
+func createLinked(t *testing.T) pagination.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 pagination.PageResult) pagination.Page {
+		return LinkedPageResult{pagination.LinkedPageBase{PageResult: r}}
+	}
+
+	return pagination.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 pagination.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/testing/marker_test.go b/pagination/testing/marker_test.go
new file mode 100644
index 0000000..7b1a6da
--- /dev/null
+++ b/pagination/testing/marker_test.go
@@ -0,0 +1,127 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/pagination"
+	"github.com/gophercloud/gophercloud/testhelper"
+)
+
+// MarkerPager sample and test cases.
+
+type MarkerPageResult struct {
+	pagination.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) pagination.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 pagination.PageResult) pagination.Page {
+		p := MarkerPageResult{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	return pagination.NewPager(client, testhelper.Server.URL+"/page", createPage)
+}
+
+func ExtractMarkerStrings(page pagination.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 pagination.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/testing/pagination_test.go b/pagination/testing/pagination_test.go
new file mode 100644
index 0000000..170dca4
--- /dev/null
+++ b/pagination/testing/pagination_test.go
@@ -0,0 +1,13 @@
+package testing
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/testhelper"
+)
+
+func createClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		ProviderClient: &gophercloud.ProviderClient{TokenID: "abc123"},
+		Endpoint:       testhelper.Endpoint(),
+	}
+}
diff --git a/pagination/testing/single_test.go b/pagination/testing/single_test.go
new file mode 100644
index 0000000..8d95e94
--- /dev/null
+++ b/pagination/testing/single_test.go
@@ -0,0 +1,79 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/pagination"
+	"github.com/gophercloud/gophercloud/testhelper"
+)
+
+// SinglePage sample and test cases.
+
+type SinglePageResult struct {
+	pagination.SinglePageBase
+}
+
+func (r SinglePageResult) IsEmpty() (bool, error) {
+	is, err := ExtractSingleInts(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+func ExtractSingleInts(r pagination.Page) ([]int, error) {
+	var s struct {
+		Ints []int `json:"ints"`
+	}
+	err := (r.(SinglePageResult)).ExtractInto(&s)
+	return s.Ints, err
+}
+
+func setupSinglePaged() pagination.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 pagination.PageResult) pagination.Page {
+		return SinglePageResult{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, testhelper.Server.URL+"/only", createPage)
+}
+
+func TestEnumerateSinglePaged(t *testing.T) {
+	callCount := 0
+	pager := setupSinglePaged()
+	defer testhelper.TeardownHTTP()
+
+	err := pager.EachPage(func(page pagination.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..e484fe1
--- /dev/null
+++ b/params.go
@@ -0,0 +1,445 @@
+package gophercloud
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// BuildRequestBody builds a map[string]interface from the given `struct`. If
+// parent is not the empty string, the final map[string]interface returned will
+// encapsulate the built one
+//
+func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, 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]interface{})
+	if optsValue.Kind() == reflect.Struct {
+		//fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind())
+		for i := 0; i < optsValue.NumField(); i++ {
+			v := optsValue.Field(i)
+			f := optsType.Field(i)
+
+			if f.Name != strings.Title(f.Name) {
+				//fmt.Printf("Skipping field: %s...\n", f.Name)
+				continue
+			}
+
+			//fmt.Printf("Starting on field: %s...\n", f.Name)
+
+			zero := isZero(v)
+			//fmt.Printf("v is zero?: %v\n", zero)
+
+			// if the field has a required tag that's set to "true"
+			if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
+				//fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero)
+				// if the field's value is zero, return a missing-argument error
+				if zero {
+					// if the field has a 'required' tag, it can't have a zero-value
+					err := ErrMissingInput{}
+					err.Argument = f.Name
+					return nil, err
+				}
+			}
+
+			if xorTag := f.Tag.Get("xor"); xorTag != "" {
+				//fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag)
+				xorField := optsValue.FieldByName(xorTag)
+				var xorFieldIsZero bool
+				if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) {
+					xorFieldIsZero = true
+				} else {
+					if xorField.Kind() == reflect.Ptr {
+						xorField = xorField.Elem()
+					}
+					xorFieldIsZero = isZero(xorField)
+				}
+				if !(zero != xorFieldIsZero) {
+					err := ErrMissingInput{}
+					err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag)
+					err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag)
+					return nil, err
+				}
+			}
+
+			if orTag := f.Tag.Get("or"); orTag != "" {
+				//fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag)
+				//fmt.Printf("field is zero?: %v\n", zero)
+				if zero {
+					orField := optsValue.FieldByName(orTag)
+					var orFieldIsZero bool
+					if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) {
+						orFieldIsZero = true
+					} else {
+						if orField.Kind() == reflect.Ptr {
+							orField = orField.Elem()
+						}
+						orFieldIsZero = isZero(orField)
+					}
+					if orFieldIsZero {
+						err := ErrMissingInput{}
+						err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag)
+						err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag)
+						return nil, err
+					}
+				}
+			}
+
+			if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
+				if zero {
+					//fmt.Printf("value before change: %+v\n", optsValue.Field(i))
+					if jsonTag := f.Tag.Get("json"); jsonTag != "" {
+						jsonTagPieces := strings.Split(jsonTag, ",")
+						if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" {
+							if v.CanSet() {
+								if !v.IsNil() {
+									if v.Kind() == reflect.Ptr {
+										v.Set(reflect.Zero(v.Type()))
+									}
+								}
+								//fmt.Printf("value after change: %+v\n", optsValue.Field(i))
+							}
+						}
+					}
+					continue
+				}
+
+				//fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name)
+				_, err := BuildRequestBody(v.Interface(), f.Name)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+
+		//fmt.Printf("opts: %+v \n", opts)
+
+		b, err := json.Marshal(opts)
+		if err != nil {
+			return nil, err
+		}
+
+		//fmt.Printf("string(b): %s\n", string(b))
+
+		err = json.Unmarshal(b, &optsMap)
+		if err != nil {
+			return nil, err
+		}
+
+		//fmt.Printf("optsMap: %+v\n", optsMap)
+
+		if parent != "" {
+			optsMap = map[string]interface{}{parent: optsMap}
+		}
+		//fmt.Printf("optsMap after parent added: %+v\n", optsMap)
+		return optsMap, nil
+	}
+	// Return an error if the underlying type of 'opts' isn't a struct.
+	return nil, fmt.Errorf("Options type is not a struct.")
+}
+
+// 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
+)
+
+// IPVersion is a type for the possible IP address versions. Valid instances
+// are IPv4 and IPv6
+type IPVersion int
+
+const (
+	// IPv4 is used for IP version 4 addresses
+	IPv4 IPVersion = 4
+	// IPv6 is used for IP version 6 addresses
+	IPv6 IPVersion = 6
+)
+
+// 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
+}
+
+/*
+func isUnderlyingStructZero(v reflect.Value) bool {
+	switch v.Kind() {
+	case reflect.Ptr:
+		return isUnderlyingStructZero(v.Elem())
+	default:
+		return isZero(v)
+	}
+}
+*/
+
+var t time.Time
+
+func isZero(v reflect.Value) bool {
+	//fmt.Printf("\n\nchecking isZero for value: %+v\n", v)
+	switch v.Kind() {
+	case reflect.Ptr:
+		if v.IsNil() {
+			return true
+		}
+		return false
+	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())
+	//fmt.Printf("zero type for value: %+v\n\n\n", z)
+	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) {
+				loop:
+					switch v.Kind() {
+					case reflect.Ptr:
+						v = v.Elem()
+						goto loop
+					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/provider_client.go b/provider_client.go
new file mode 100644
index 0000000..f886823
--- /dev/null
+++ b/provider_client.go
@@ -0,0 +1,307 @@
+package gophercloud
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+// DefaultUserAgent is the default User-Agent string set in the request header.
+const DefaultUserAgent = "gophercloud/2.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
+
+	Debug bool
+}
+
+// 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.Reader that will be consumed by the request directly. No content-type
+	// will be set unless one is provided explicitly by MoreHeaders.
+	RawBody io.Reader
+	// 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
+	// ErrorContext specifies the resource error type to return if an error is encountered.
+	// This lets resources override default error messages based on the response status code.
+	ErrorContext error
+}
+
+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.Reader
+	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)
+			}
+		}
+	}
+
+	// Set connection parameter to close the connection immediately when we've got the response
+	req.Close = true
+
+	// Issue the request.
+	resp, err := client.HTTPClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	// 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()
+		//pc := make([]uintptr, 1)
+		//runtime.Callers(2, pc)
+		//f := runtime.FuncForPC(pc[0])
+		respErr := ErrUnexpectedResponseCode{
+			URL:      url,
+			Method:   method,
+			Expected: options.OkCodes,
+			Actual:   resp.StatusCode,
+			Body:     body,
+		}
+		//respErr.Function = "gophercloud.ProviderClient.Request"
+
+		errType := options.ErrorContext
+		switch resp.StatusCode {
+		case http.StatusBadRequest:
+			err = ErrDefault400{respErr}
+			if error400er, ok := errType.(Err400er); ok {
+				err = error400er.Error400(respErr)
+			}
+		case http.StatusUnauthorized:
+			if client.ReauthFunc != nil {
+				err = client.ReauthFunc()
+				if err != nil {
+					e := &ErrUnableToReauthenticate{}
+					e.ErrOriginal = respErr
+					return nil, e
+				}
+				if options.RawBody != nil {
+					if seeker, ok := options.RawBody.(io.Seeker); ok {
+						seeker.Seek(0, 0)
+					}
+				}
+				resp, err = client.Request(method, url, options)
+				if err != nil {
+					switch err.(type) {
+					case *ErrUnexpectedResponseCode:
+						e := &ErrErrorAfterReauthentication{}
+						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
+						return nil, e
+					default:
+						e := &ErrErrorAfterReauthentication{}
+						e.ErrOriginal = err
+						return nil, e
+					}
+				}
+				return resp, nil
+			}
+			err = ErrDefault401{respErr}
+			if error401er, ok := errType.(Err401er); ok {
+				err = error401er.Error401(respErr)
+			}
+		case http.StatusNotFound:
+			err = ErrDefault404{respErr}
+			if error404er, ok := errType.(Err404er); ok {
+				err = error404er.Error404(respErr)
+			}
+		case http.StatusMethodNotAllowed:
+			err = ErrDefault405{respErr}
+			if error405er, ok := errType.(Err405er); ok {
+				err = error405er.Error405(respErr)
+			}
+		case http.StatusRequestTimeout:
+			err = ErrDefault408{respErr}
+			if error408er, ok := errType.(Err408er); ok {
+				err = error408er.Error408(respErr)
+			}
+		case 429:
+			err = ErrDefault429{respErr}
+			if error429er, ok := errType.(Err429er); ok {
+				err = error429er.Error429(respErr)
+			}
+		case http.StatusInternalServerError:
+			err = ErrDefault500{respErr}
+			if error500er, ok := errType.(Err500er); ok {
+				err = error500er.Error500(respErr)
+			}
+		case http.StatusServiceUnavailable:
+			err = ErrDefault503{respErr}
+			if error503er, ok := errType.(Err503er); ok {
+				err = error503er.Error503(respErr)
+			}
+		}
+
+		if err == nil {
+			err = respErr
+		}
+
+		return resp, err
+	}
+
+	// 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 == "PATCH":
+		return []int{200, 204}
+	case method == "DELETE":
+		return []int{202, 204}
+	}
+
+	return []int{}
+}
diff --git a/results.go b/results.go
new file mode 100644
index 0000000..76c16ef
--- /dev/null
+++ b/results.go
@@ -0,0 +1,336 @@
+package gophercloud
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"reflect"
+	"strconv"
+	"time"
+)
+
+/*
+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
+}
+
+// ExtractInto allows users to provide an object into which `Extract` will extract
+// the `Result.Body`. This would be useful for OpenStack providers that have
+// different fields in the response object than OpenStack proper.
+func (r Result) ExtractInto(to interface{}) error {
+	if r.Err != nil {
+		return r.Err
+	}
+
+	if reader, ok := r.Body.(io.Reader); ok {
+		if readCloser, ok := reader.(io.Closer); ok {
+			defer readCloser.Close()
+		}
+		return json.NewDecoder(reader).Decode(to)
+	}
+
+	b, err := json.Marshal(r.Body)
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(b, to)
+
+	return err
+}
+
+func (r Result) extractIntoPtr(to interface{}, label string) error {
+	if label == "" {
+		return r.ExtractInto(&to)
+	}
+
+	var m map[string]interface{}
+	err := r.ExtractInto(&m)
+	if err != nil {
+		return err
+	}
+
+	b, err := json.Marshal(m[label])
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &to)
+	return err
+}
+
+// ExtractIntoStructPtr will unmarshal the Result (r) into the provided
+// interface{} (to).
+//
+// NOTE: For internal use only
+//
+// `to` must be a pointer to an underlying struct type
+//
+// If provided, `label` will be filtered out of the response
+// body prior to `r` being unmarshalled into `to`.
+func (r Result) ExtractIntoStructPtr(to interface{}, label string) error {
+	if r.Err != nil {
+		return r.Err
+	}
+
+	t := reflect.TypeOf(to)
+	if k := t.Kind(); k != reflect.Ptr {
+		return fmt.Errorf("Expected pointer, got %v", k)
+	}
+	switch t.Elem().Kind() {
+	case reflect.Struct:
+		return r.extractIntoPtr(to, label)
+	default:
+		return fmt.Errorf("Expected pointer to struct, got: %v", t)
+	}
+}
+
+// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided
+// interface{} (to).
+//
+// NOTE: For internal use only
+//
+// `to` must be a pointer to an underlying slice type
+//
+// If provided, `label` will be filtered out of the response
+// body prior to `r` being unmarshalled into `to`.
+func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error {
+	if r.Err != nil {
+		return r.Err
+	}
+
+	t := reflect.TypeOf(to)
+	if k := t.Kind(); k != reflect.Ptr {
+		return fmt.Errorf("Expected pointer, got %v", k)
+	}
+	switch t.Elem().Kind() {
+	case reflect.Slice:
+		return r.extractIntoPtr(to, label)
+	default:
+		return fmt.Errorf("Expected pointer to slice, got: %v", t)
+	}
+}
+
+// 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 (r HeaderResult) ExtractInto(to interface{}) error {
+	if r.Err != nil {
+		return r.Err
+	}
+
+	tmpHeaderMap := map[string]string{}
+	for k, v := range r.Header {
+		if len(v) > 0 {
+			tmpHeaderMap[k] = v[0]
+		}
+	}
+
+	b, err := json.Marshal(tmpHeaderMap)
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(b, to)
+
+	return err
+}
+
+// RFC3339Milli describes a common time format used by some API responses.
+const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
+
+type JSONRFC3339Milli time.Time
+
+func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error {
+	b := bytes.NewBuffer(data)
+	dec := json.NewDecoder(b)
+	var s string
+	if err := dec.Decode(&s); err != nil {
+		return err
+	}
+	t, err := time.Parse(RFC3339Milli, s)
+	if err != nil {
+		return err
+	}
+	*jt = JSONRFC3339Milli(t)
+	return nil
+}
+
+const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999"
+
+type JSONRFC3339MilliNoZ time.Time
+
+func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if s == "" {
+		return nil
+	}
+	t, err := time.Parse(RFC3339MilliNoZ, s)
+	if err != nil {
+		return err
+	}
+	*jt = JSONRFC3339MilliNoZ(t)
+	return nil
+}
+
+type JSONRFC1123 time.Time
+
+func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if s == "" {
+		return nil
+	}
+	t, err := time.Parse(time.RFC1123, s)
+	if err != nil {
+		return err
+	}
+	*jt = JSONRFC1123(t)
+	return nil
+}
+
+type JSONUnix time.Time
+
+func (jt *JSONUnix) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if s == "" {
+		return nil
+	}
+	unix, err := strconv.ParseInt(s, 10, 64)
+	if err != nil {
+		return err
+	}
+	t = time.Unix(unix, 0)
+	*jt = JSONUnix(t)
+	return nil
+}
+
+// RFC3339NoZ is the time format used in Heat (Orchestration).
+const RFC3339NoZ = "2006-01-02T15:04:05"
+
+type JSONRFC3339NoZ time.Time
+
+func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if s == "" {
+		return nil
+	}
+	t, err := time.Parse(RFC3339NoZ, s)
+	if err != nil {
+		return err
+	}
+	*jt = JSONRFC3339NoZ(t)
+	return nil
+}
+
+/*
+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 `json:"href"`
+	Rel  string `json:"rel"`
+}
+
+/*
+ExtractNextURL is an internal function useful for packages of collection
+resources that are paginated in a certain way.
+
+It 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..9debd48
--- /dev/null
+++ b/script/acceptancetest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the acceptance tests.
+
+exec go test -p=1 github.com/gophercloud/gophercloud/acceptance/... $@
diff --git a/script/acceptancetest_environments/keystonev2-lbaasv1.sh b/script/acceptancetest_environments/keystonev2-lbaasv1.sh
new file mode 100644
index 0000000..c74db62
--- /dev/null
+++ b/script/acceptancetest_environments/keystonev2-lbaasv1.sh
@@ -0,0 +1,194 @@
+#!/bin/bash
+#
+# This script is useful for creating a devstack environment to run gophercloud
+# acceptance tests on.
+#
+# This can be considered a "legacy" devstack environment since it uses
+# Keystone v2 and LBaaS v1.
+#
+# To run, simply execute this script within a virtual machine.
+#
+# The following OpenStack versions are installed:
+# * OpenStack Mitaka
+# * Keystone v2
+# * Glance v1 and v2
+# * Nova v2 and v2.1
+# * Cinder v1 and v2
+# * Trove v1
+# * Swift v1
+# * Neutron v2
+# * Neutron LBaaS v1.0
+# * Neutron FWaaS v2.0
+# * Manila v2
+#
+# Go 1.6 is also installed.
+
+set -e
+
+cd
+sudo apt-get update
+sudo apt-get install -y git make mercurial
+
+sudo wget -O /usr/local/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme
+sudo chmod +x /usr/local/bin/gimme
+gimme 1.6 >> .bashrc
+
+mkdir ~/go
+eval "$(/usr/local/bin/gimme 1.6)"
+echo 'export GOPATH=$HOME/go' >> .bashrc
+export GOPATH=$HOME/go
+source .bashrc
+
+go get golang.org/x/crypto/ssh
+go get github.com/gophercloud/gophercloud
+
+git clone https://git.openstack.org/openstack-dev/devstack -b stable/mitaka
+cd devstack
+cat >local.conf <<EOF
+[[local|localrc]]
+# OpenStack version
+OPENSTACK_VERSION="mitaka"
+
+# devstack password
+DEVSTACK_PASSWORD="password"
+
+# Configure passwords and the Swift Hash
+MYSQL_PASSWORD=\$DEVSTACK_PASSWORD
+RABBIT_PASSWORD=\$DEVSTACK_PASSWORD
+SERVICE_TOKEN=\$DEVSTACK_PASSWORD
+ADMIN_PASSWORD=\$DEVSTACK_PASSWORD
+SERVICE_PASSWORD=\$DEVSTACK_PASSWORD
+SWIFT_HASH=\$DEVSTACK_PASSWORD
+
+# Configure the stable OpenStack branches used by DevStack
+# For stable branches see
+# http://git.openstack.org/cgit/openstack-dev/devstack/refs/
+CINDER_BRANCH=stable/\$OPENSTACK_VERSION
+CEILOMETER_BRANCH=stable/\$OPENSTACK_VERSION
+GLANCE_BRANCH=stable/\$OPENSTACK_VERSION
+HEAT_BRANCH=stable/\$OPENSTACK_VERSION
+HORIZON_BRANCH=stable/\$OPENSTACK_VERSION
+KEYSTONE_BRANCH=stable/\$OPENSTACK_VERSION
+NEUTRON_BRANCH=stable/\$OPENSTACK_VERSION
+NOVA_BRANCH=stable/\$OPENSTACK_VERSION
+SWIFT_BRANCH=stable/\$OPENSTACK_VERSION
+ZAQAR_BRANCH=stable/\$OPENSTACK_VERSION
+
+# Enable Swift
+enable_service s-proxy
+enable_service s-object
+enable_service s-container
+enable_service s-account
+
+# Disable Nova Network and enable Neutron
+disable_service n-net
+enable_service q-svc
+enable_service q-agt
+enable_service q-dhcp
+enable_service q-l3
+enable_service q-meta
+#enable_service q-flavors
+
+# Disable Neutron metering
+disable_service q-metering
+
+# Enable LBaaS V1
+enable_service q-lbaas
+
+# Enable FWaaS
+enable_service q-fwaas
+
+# Enable LBaaS v2
+#enable_plugin neutron-lbaas https://git.openstack.org/openstack/neutron-lbaas stable/\$OPENSTACK_VERSION
+#enable_plugin octavia https://git.openstack.org/openstack/octavia stable/\$OPENSTACK_VERSION
+#enable_service q-lbaasv2
+#enable_service octavia
+#enable_service o-cw
+#enable_service o-hk
+#enable_service o-hm
+#enable_service o-api
+
+# Enable Trove
+enable_plugin trove git://git.openstack.org/openstack/trove.git stable/\$OPENSTACK_VERSION
+enable_service trove,tr-api,tr-tmgr,tr-cond
+
+# Disable Temptest
+disable_service tempest
+
+# Disable Horizon
+disable_service horizon
+
+# Disable Keystone v2
+#ENABLE_IDENTITY_V2=False
+
+# Enable SSL/tls
+#enable_service tls-proxy
+#USE_SSL=True
+
+# Enable Ceilometer
+#enable_service ceilometer-acompute
+#enable_service ceilometer-acentral
+#enable_service ceilometer-anotification
+#enable_service ceilometer-collector
+#enable_service ceilometer-alarm-evaluator
+#enable_service ceilometer-alarm-notifier
+#enable_service ceilometer-api
+
+# Enable Zaqar
+#enable_plugin zaqar https://github.com/openstack/zaqar
+#enable_service zaqar-server
+
+# Enable Manila
+enable_plugin manila https://github.com/openstack/manila
+
+# Automatically download and register a VM image that Heat can launch
+# For more information on Heat and DevStack see
+# http://docs.openstack.org/developer/heat/getting_started/on_devstack.html
+#IMAGE_URLS+=",http://cloud.fedoraproject.org/fedora-20.x86_64.qcow2"
+#IMAGE_URLS+=",https://cloud-images.ubuntu.com/trusty/current/trusty-server-cloudimg-amd64-disk1.img"
+
+# Logging
+LOGDAYS=1
+LOGFILE=/opt/stack/logs/stack.sh.log
+LOGDIR=/opt/stack/logs
+EOF
+./stack.sh
+
+# Prep the testing environment by creating the required testing resources and environment variables
+source openrc admin
+wget http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img
+glance image-create --name CirrOS --disk-format qcow2 --container-format bare < cirros-0.3.4-x86_64-disk.img
+nova flavor-create m1.acctest 99 512 5 1 --ephemeral 10
+nova flavor-create m1.resize 98 512 6 1 --ephemeral 10
+_NETWORK_ID=$(nova net-list | grep private | awk -F\| '{print $2}' | tr -d ' ')
+_EXTGW_ID=$(nova net-list | grep public | awk -F\| '{print $2}' | tr -d ' ')
+_IMAGE_ID=$(nova image-list | grep CirrOS | awk -F\| '{print $2}' | tr -d ' ' | head -1)
+echo export OS_IMAGE_NAME="cirros-0.3.4-x86_64-uec" >> openrc
+echo export OS_IMAGE_ID="$_IMAGE_ID" >> openrc
+echo export OS_NETWORK_ID=$_NETWORK_ID >> openrc
+echo export OS_EXTGW_ID=$_EXTGW_ID >> openrc
+echo export OS_POOL_NAME="public" >> openrc
+echo export OS_FLAVOR_ID=99 >> openrc
+echo export OS_FLAVOR_ID_RESIZE=98 >> openrc
+
+# Manila share-network needs to be created
+_IDTOVALUE="-F id -f value"
+_NEUTRON_NET_ID=$(neutron net-list --name private $_IDTOVALUE)
+_NEUTRON_IPV4_SUB=$(neutron subnet-list \
+    --ip_version 4 \
+    --network_id "$_NEUTRON_NET_ID" \
+    $_IDTOVALUE)
+
+manila share-network-create \
+    --neutron-net-id "$_NEUTRON_NET_ID" \
+    --neutron-subnet-id "$_NEUTRON_IPV4_SUB" \
+    --name "acc_share_nw"
+
+_SHARE_NETWORK=$(manila share-network-list \
+    --neutron-net-id "$_NEUTRON_NET_ID" \
+    --neutron-subnet-id "$_NEUTRON_IPV4_SUB" \
+    --name "acc_share_nw" \
+    | awk 'FNR == 4 {print $2}')
+
+echo export OS_SHARE_NETWORK_ID="$_SHARE_NETWORK" >> openrc
+source openrc demo
diff --git a/script/acceptancetest_environments/keystonev3-lbaasv2.sh b/script/acceptancetest_environments/keystonev3-lbaasv2.sh
new file mode 100644
index 0000000..5cc9212
--- /dev/null
+++ b/script/acceptancetest_environments/keystonev3-lbaasv2.sh
@@ -0,0 +1,208 @@
+#!/bin/bash
+#
+# This script is useful for creating a devstack environment to run gophercloud
+# acceptance tests on.
+#
+# To run, simply execute this script within a virtual machine.
+#
+# The following OpenStack versions are installed:
+# * OpenStack Mitaka
+# * Keystone v3
+# * Glance v1 and v2
+# * Nova v2 and v2.1
+# * Cinder v1 and v2
+# * Trove v1
+# * Swift v1
+# * Neutron v2
+# * Neutron LBaaS v2.0
+# * Neutron FWaaS v2.0
+#
+# Go 1.6 is also installed.
+
+set -e
+
+cd
+sudo apt-get update
+sudo apt-get install -y git make mercurial
+
+sudo wget -O /usr/local/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme
+sudo chmod +x /usr/local/bin/gimme
+gimme 1.6 >> .bashrc
+
+mkdir ~/go
+eval "$(/usr/local/bin/gimme 1.6)"
+echo 'export GOPATH=$HOME/go' >> .bashrc
+export GOPATH=$HOME/go
+
+export PATH=$PATH:$HOME/terraform:$HOME/go/bin
+echo 'export PATH=$PATH:$HOME/terraform:$HOME/go/bin' >> .bashrc
+source .bashrc
+
+go get golang.org/x/crypto/ssh
+go get github.com/gophercloud/gophercloud
+
+git clone https://git.openstack.org/openstack-dev/devstack -b stable/mitaka
+cd devstack
+cat >local.conf <<EOF
+[[local|localrc]]
+# OpenStack version
+OPENSTACK_VERSION="mitaka"
+
+# devstack password
+DEVSTACK_PASSWORD="password"
+
+# Configure passwords and the Swift Hash
+MYSQL_PASSWORD=\$DEVSTACK_PASSWORD
+RABBIT_PASSWORD=\$DEVSTACK_PASSWORD
+SERVICE_TOKEN=\$DEVSTACK_PASSWORD
+ADMIN_PASSWORD=\$DEVSTACK_PASSWORD
+SERVICE_PASSWORD=\$DEVSTACK_PASSWORD
+SWIFT_HASH=\$DEVSTACK_PASSWORD
+
+# Configure the stable OpenStack branches used by DevStack
+# For stable branches see
+# http://git.openstack.org/cgit/openstack-dev/devstack/refs/
+CINDER_BRANCH=stable/\$OPENSTACK_VERSION
+CEILOMETER_BRANCH=stable/\$OPENSTACK_VERSION
+GLANCE_BRANCH=stable/\$OPENSTACK_VERSION
+HEAT_BRANCH=stable/\$OPENSTACK_VERSION
+HORIZON_BRANCH=stable/\$OPENSTACK_VERSION
+KEYSTONE_BRANCH=stable/\$OPENSTACK_VERSION
+NEUTRON_BRANCH=stable/\$OPENSTACK_VERSION
+NOVA_BRANCH=stable/\$OPENSTACK_VERSION
+SWIFT_BRANCH=stable/\$OPENSTACK_VERSION
+ZAQAR_BRANCH=stable/\$OPENSTACK_VERSION
+
+# Enable Swift
+enable_service s-proxy
+enable_service s-object
+enable_service s-container
+enable_service s-account
+
+# Disable Nova Network and enable Neutron
+disable_service n-net
+enable_service q-svc
+enable_service q-agt
+enable_service q-dhcp
+enable_service q-l3
+enable_service q-meta
+enable_service q-flavors
+
+# Disable Neutron metering
+disable_service q-metering
+
+# Enable LBaaS V1
+#enable_service q-lbaas
+
+# Enable FWaaS
+enable_service q-fwaas
+
+# Enable LBaaS v2
+enable_plugin neutron-lbaas https://git.openstack.org/openstack/neutron-lbaas stable/\$OPENSTACK_VERSION
+#enable_plugin octavia https://git.openstack.org/openstack/octavia stable/\$OPENSTACK_VERSION
+enable_plugin octavia https://git.openstack.org/openstack/octavia
+enable_service q-lbaasv2
+enable_service octavia
+enable_service o-cw
+enable_service o-hm
+enable_service o-hk
+enable_service o-api
+
+# Octavia
+OCTAVIA_AUTH_VERSION=3
+
+# Enable Trove
+#enable_plugin trove git://git.openstack.org/openstack/trove.git stable/\$OPENSTACK_VERSION
+#enable_service trove,tr-api,tr-tmgr,tr-cond
+
+# Disable Temptest
+disable_service tempest
+
+# Disable Horizon
+disable_service horizon
+
+# Disable Keystone v2
+ENABLE_IDENTITY_V2=False
+
+# Enable SSL/tls
+#enable_service tls-proxy
+#USE_SSL=True
+
+# Enable Ceilometer
+#enable_service ceilometer-acompute
+#enable_service ceilometer-acentral
+#enable_service ceilometer-anotification
+#enable_service ceilometer-collector
+#enable_service ceilometer-alarm-evaluator
+#enable_service ceilometer-alarm-notifier
+#enable_service ceilometer-api
+
+# Enable Zaqar
+#enable_plugin zaqar https://github.com/openstack/zaqar
+#enable_service zaqar-server
+
+# Automatically download and register a VM image that Heat can launch
+# For more information on Heat and DevStack see
+# http://docs.openstack.org/developer/heat/getting_started/on_devstack.html
+#IMAGE_URLS+=",http://cloud.fedoraproject.org/fedora-20.x86_64.qcow2"
+#IMAGE_URLS+=",https://cloud-images.ubuntu.com/trusty/current/trusty-server-cloudimg-amd64-disk1.img"
+
+# Logging
+LOGDAYS=1
+LOGFILE=/opt/stack/logs/stack.sh.log
+LOGDIR=/opt/stack/logs
+
+[[post-config|\$OCTAVIA_CONF]]
+[default]
+debug = true
+verbose = true
+[keystone_authtoken]
+auth_uri = http://localhost:5000/v3
+auth_url = http://localhost:35357/v3
+user_domain_id = default
+project_name = default
+auth_type = password
+[keystone_authtoken_v3]
+admin_user_domain = default
+admin_project_domain = default
+[[post-config|\$NEUTRON_CONF]]
+[service_auth]
+auth_version = 3
+auth_uri = http://localhost:5000/v3
+auth_url = http://localhost:35357/v3
+admin_user_domain = default
+admin_project_domain = default
+[[post-config|\$NEUTRON_LBAAS_CONF]]
+[service_auth]
+auth_version = 3
+auth_uri = http://localhost:5000/v3
+auth_url = http://localhost:35357/v3
+admin_user_domain = default
+admin_project_domain = default
+EOF
+./stack.sh
+
+# Patch openrc
+cat >> openrc <<EOF
+if [ "\$OS_IDENTITY_API_VERSION" = "3" ]; then
+  export OS_DOMAIN_NAME=\${OS_DOMAIN_NAME:-"default"}
+fi
+EOF
+
+# Prep the testing environment by creating the required testing resources and environment variables
+source openrc admin
+wget http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img
+glance image-create --name CirrOS --disk-format qcow2 --container-format bare < cirros-0.3.4-x86_64-disk.img
+nova flavor-create m1.acctest 99 512 5 1 --ephemeral 10
+nova flavor-create m1.resize 98 512 6 1 --ephemeral 10
+_NETWORK_ID=$(nova net-list | grep private | awk -F\| '{print $2}' | tr -d ' ')
+_EXTGW_ID=$(nova net-list | grep public | awk -F\| '{print $2}' | tr -d ' ')
+_IMAGE_ID=$(nova image-list | grep CirrOS | awk -F\| '{print $2}' | tr -d ' ' | head -1)
+echo export OS_IMAGE_NAME="cirros-0.3.4-x86_64-uec" >> openrc
+echo export OS_IMAGE_ID="$_IMAGE_ID" >> openrc
+echo export OS_NETWORK_ID=$_NETWORK_ID >> openrc
+echo export OS_EXTGW_ID=$_EXTGW_ID >> openrc
+echo export OS_POOL_NAME="public" >> openrc
+echo export OS_FLAVOR_ID=99 >> openrc
+echo export OS_FLAVOR_ID_RESIZE=98 >> openrc
+source openrc demo
diff --git a/script/bootstrap b/script/bootstrap
new file mode 100755
index 0000000..78a195d
--- /dev/null
+++ b/script/bootstrap
@@ -0,0 +1,25 @@
+#!/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/gophercloud/gophercloud
+cd $GOPATH/src/github.com/gophercloud/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/gophercloud/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/coverage b/script/coverage
new file mode 100755
index 0000000..3efa81b
--- /dev/null
+++ b/script/coverage
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -e
+
+n=1
+for testpkg in $(go list ./testing ./.../testing); do
+  covpkg="${testpkg/"/testing"/}"
+  go test -covermode count -coverprofile "testing_"$n.coverprofile -coverpkg $covpkg $testpkg 2>/dev/null
+  n=$((n+1))
+done
+gocovmerge `ls *.coverprofile` > cover.out
+rm *.coverprofile
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..2c65d06
--- /dev/null
+++ b/script/unittest
@@ -0,0 +1,5 @@
+#!/bin/bash
+#
+# Run the unit tests.
+
+exec go test ./testing ./.../testing $@
diff --git a/service_client.go b/service_client.go
new file mode 100644
index 0000000..7484c67
--- /dev/null
+++ b/service_client.go
@@ -0,0 +1,141 @@
+package gophercloud
+
+import (
+	"io"
+	"net/http"
+	"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
+
+	Microversion 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, "/")
+}
+
+// Get calls `Request` with the "GET" HTTP verb.
+func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+
+	if opts.MoreHeaders == nil {
+		opts.MoreHeaders = make(map[string]string)
+	}
+	opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
+
+	return client.Request("GET", url, opts)
+}
+
+// Post calls `Request` with the "POST" HTTP verb.
+func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	if v, ok := (JSONBody).(io.Reader); ok {
+		opts.RawBody = v
+	} else if JSONBody != nil {
+		opts.JSONBody = JSONBody
+	}
+
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+
+	if opts.MoreHeaders == nil {
+		opts.MoreHeaders = make(map[string]string)
+	}
+	opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
+
+	return client.Request("POST", url, opts)
+}
+
+// Put calls `Request` with the "PUT" HTTP verb.
+func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	if v, ok := (JSONBody).(io.Reader); ok {
+		opts.RawBody = v
+	} else if JSONBody != nil {
+		opts.JSONBody = JSONBody
+	}
+
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+
+	if opts.MoreHeaders == nil {
+		opts.MoreHeaders = make(map[string]string)
+	}
+	opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
+
+	return client.Request("PUT", url, opts)
+}
+
+// Patch calls `Request` with the "PATCH" HTTP verb.
+func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	if v, ok := (JSONBody).(io.Reader); ok {
+		opts.RawBody = v
+	} else if JSONBody != nil {
+		opts.JSONBody = JSONBody
+	}
+
+	if JSONResponse != nil {
+		opts.JSONResponse = JSONResponse
+	}
+
+	if opts.MoreHeaders == nil {
+		opts.MoreHeaders = make(map[string]string)
+	}
+	opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
+
+	return client.Request("PATCH", url, opts)
+}
+
+// Delete calls `Request` with the "DELETE" HTTP verb.
+func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
+	if opts == nil {
+		opts = &RequestOpts{}
+	}
+
+	if opts.MoreHeaders == nil {
+		opts.MoreHeaders = make(map[string]string)
+	}
+	opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
+
+	return client.Request("DELETE", url, opts)
+}
diff --git a/testhelper/client/fake.go b/testhelper/client/fake.go
new file mode 100644
index 0000000..3d81cc9
--- /dev/null
+++ b/testhelper/client/fake.go
@@ -0,0 +1,17 @@
+package client
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/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..f21c3f9
--- /dev/null
+++ b/testhelper/convenience.go
@@ -0,0 +1,348 @@
+package testhelper
+
+import (
+	"bytes"
+	"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))
+	})
+}
+
+func isByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) bool {
+	return bytes.Equal(expectedBytes, actualBytes)
+}
+
+// AssertByteArrayEquals a convenience function for checking whether two byte arrays are equal
+func AssertByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) {
+	if !isByteArrayEquals(t, expectedBytes, actualBytes) {
+		logFatal(t, "The bytes differed.")
+	}
+}
+
+// CheckByteArrayEquals a convenience function for silent checking whether two byte arrays are equal
+func CheckByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) {
+	if !isByteArrayEquals(t, expectedBytes, actualBytes) {
+		logError(t, "The bytes differed.")
+	}
+}
+
+// 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..fe98c86
--- /dev/null
+++ b/testhelper/fixture/helper.go
@@ -0,0 +1,31 @@
+package fixture
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/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/testing/doc.go b/testing/doc.go
new file mode 100644
index 0000000..244a62e
--- /dev/null
+++ b/testing/doc.go
@@ -0,0 +1,2 @@
+// gophercloud
+package testing
diff --git a/testing/endpoint_search_test.go b/testing/endpoint_search_test.go
new file mode 100644
index 0000000..22476cb
--- /dev/null
+++ b/testing/endpoint_search_test.go
@@ -0,0 +1,20 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestApplyDefaultsToEndpointOpts(t *testing.T) {
+	eo := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic}
+	eo.ApplyDefaults("compute")
+	expected := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute"}
+	th.CheckDeepEquals(t, expected, eo)
+
+	eo = gophercloud.EndpointOpts{Type: "compute"}
+	eo.ApplyDefaults("object-store")
+	expected = gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute"}
+	th.CheckDeepEquals(t, expected, eo)
+}
diff --git a/testing/params_test.go b/testing/params_test.go
new file mode 100644
index 0000000..937ff8b
--- /dev/null
+++ b/testing/params_test.go
@@ -0,0 +1,254 @@
+package testing
+
+import (
+	"net/url"
+	"reflect"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestMaybeString(t *testing.T) {
+	testString := ""
+	var expected *string
+	actual := gophercloud.MaybeString(testString)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testString = "carol"
+	expected = &testString
+	actual = gophercloud.MaybeString(testString)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestMaybeInt(t *testing.T) {
+	testInt := 0
+	var expected *int
+	actual := gophercloud.MaybeInt(testInt)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testInt = 4
+	expected = &testInt
+	actual = gophercloud.MaybeInt(testInt)
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBuildQueryString(t *testing.T) {
+	type testVar string
+	iFalse := false
+	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"`
+		F  *bool     `q:"f"`
+	}{
+		J:  2,
+		R:  "red",
+		C:  true,
+		S:  []string{"one", "two", "three"},
+		TS: []testVar{"a", "b"},
+		TI: []int{1, 2},
+		F:  &iFalse,
+	}
+	expected := &url.URL{RawQuery: "c=true&f=false&j=2&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"}
+	actual, err := gophercloud.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"`
+		F  *bool     `q:"f"`
+	}{
+		J: 2,
+		C: true,
+	}
+	_, err = gophercloud.BuildQueryString(&opts)
+	if err == nil {
+		t.Errorf("Expected error: 'Required field not set'")
+	}
+	th.CheckDeepEquals(t, expected, actual)
+
+	_, err = gophercloud.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 := gophercloud.BuildHeaders(&testStruct)
+	th.CheckNoErr(t, err)
+	th.CheckDeepEquals(t, expected, actual)
+
+	testStruct.Num = 0
+	_, err = gophercloud.BuildHeaders(&testStruct)
+	if err == nil {
+		t.Errorf("Expected error: 'Required header not set'")
+	}
+
+	_, err = gophercloud.BuildHeaders(map[string]interface{}{"Number": 4})
+	if err == nil {
+		t.Errorf("Expected error: 'Options type is not a struct'")
+	}
+}
+
+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 := gophercloud.BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"})
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestBuildRequestBody(t *testing.T) {
+	type PasswordCredentials struct {
+		Username string `json:"username" required:"true"`
+		Password string `json:"password" required:"true"`
+	}
+
+	type TokenCredentials struct {
+		ID string `json:"id,omitempty" required:"true"`
+	}
+
+	type orFields struct {
+		Filler int `json:"filler,omitempty"`
+		F1     int `json:"f1,omitempty" or:"F2"`
+		F2     int `json:"f2,omitempty" or:"F1"`
+	}
+
+	// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+	// interface.
+	type AuthOptions struct {
+		PasswordCredentials `json:"passwordCredentials,omitempty" xor:"TokenCredentials"`
+
+		// 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   string `json:"tenantId,omitempty"`
+		TenantName string `json:"tenantName,omitempty"`
+
+		// TokenCredentials allows users to authenticate (possibly as another user) with an
+		// authentication token ID.
+		TokenCredentials `json:"token,omitempty" xor:"PasswordCredentials"`
+
+		OrFields orFields `json:"or_fields,omitempty"`
+	}
+
+	var successCases = []struct {
+		opts     AuthOptions
+		expected map[string]interface{}
+	}{
+		{
+			AuthOptions{
+				PasswordCredentials: PasswordCredentials{
+					Username: "me",
+					Password: "swordfish",
+				},
+			},
+			map[string]interface{}{
+				"auth": map[string]interface{}{
+					"passwordCredentials": map[string]interface{}{
+						"password": "swordfish",
+						"username": "me",
+					},
+				},
+			},
+		},
+		{
+			AuthOptions{
+				TokenCredentials: TokenCredentials{
+					ID: "1234567",
+				},
+			},
+			map[string]interface{}{
+				"auth": map[string]interface{}{
+					"token": map[string]interface{}{
+						"id": "1234567",
+					},
+				},
+			},
+		},
+	}
+
+	for _, successCase := range successCases {
+		actual, err := gophercloud.BuildRequestBody(successCase.opts, "auth")
+		th.AssertNoErr(t, err)
+		th.AssertDeepEquals(t, successCase.expected, actual)
+	}
+
+	var failCases = []struct {
+		opts     AuthOptions
+		expected error
+	}{
+		{
+			AuthOptions{
+				TenantID:   "987654321",
+				TenantName: "me",
+			},
+			gophercloud.ErrMissingInput{},
+		},
+		{
+			AuthOptions{
+				TokenCredentials: TokenCredentials{
+					ID: "1234567",
+				},
+				PasswordCredentials: PasswordCredentials{
+					Username: "me",
+					Password: "swordfish",
+				},
+			},
+			gophercloud.ErrMissingInput{},
+		},
+		{
+			AuthOptions{
+				PasswordCredentials: PasswordCredentials{
+					Password: "swordfish",
+				},
+			},
+			gophercloud.ErrMissingInput{},
+		},
+		{
+			AuthOptions{
+				PasswordCredentials: PasswordCredentials{
+					Username: "me",
+					Password: "swordfish",
+				},
+				OrFields: orFields{
+					Filler: 2,
+				},
+			},
+			gophercloud.ErrMissingInput{},
+		},
+	}
+
+	for _, failCase := range failCases {
+		_, err := gophercloud.BuildRequestBody(failCase.opts, "auth")
+		th.AssertDeepEquals(t, reflect.TypeOf(failCase.expected), reflect.TypeOf(err))
+	}
+}
diff --git a/testing/provider_client_test.go b/testing/provider_client_test.go
new file mode 100644
index 0000000..7c0e84e
--- /dev/null
+++ b/testing/provider_client_test.go
@@ -0,0 +1,36 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestAuthenticatedHeaders(t *testing.T) {
+	p := &gophercloud.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 := &gophercloud.ProviderClient{}
+
+	p.UserAgent.Prepend("custom-user-agent/2.4.0")
+	expected := "custom-user-agent/2.4.0 gophercloud/2.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/2.0.0"
+	actual = p.UserAgent.Join()
+	th.CheckEquals(t, expected, actual)
+
+	p.UserAgent = gophercloud.UserAgent{}
+	expected = "gophercloud/2.0.0"
+	actual = p.UserAgent.Join()
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/testing/service_client_test.go b/testing/service_client_test.go
new file mode 100644
index 0000000..904b303
--- /dev/null
+++ b/testing/service_client_test.go
@@ -0,0 +1,15 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestServiceURL(t *testing.T) {
+	c := &gophercloud.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/testing/util_test.go b/testing/util_test.go
new file mode 100644
index 0000000..ae3e448
--- /dev/null
+++ b/testing/util_test.go
@@ -0,0 +1,122 @@
+package testing
+
+import (
+	"errors"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestWaitFor(t *testing.T) {
+	err := gophercloud.WaitFor(2, func() (bool, error) {
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
+
+func TestWaitForTimeout(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	err := gophercloud.WaitFor(1, func() (bool, error) {
+		return false, nil
+	})
+	th.AssertEquals(t, "A timeout occurred", err.Error())
+}
+
+func TestWaitForError(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	err := gophercloud.WaitFor(2, func() (bool, error) {
+		return false, errors.New("Error has occurred")
+	})
+	th.AssertEquals(t, "Error has occurred", err.Error())
+}
+
+func TestWaitForPredicateExceed(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping test in short mode.")
+	}
+
+	err := gophercloud.WaitFor(1, func() (bool, error) {
+		time.Sleep(4 * time.Second)
+		return false, errors.New("Just wasting time")
+	})
+	th.AssertEquals(t, "A timeout occurred", err.Error())
+}
+
+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], gophercloud.NormalizeURL(urls[i]))
+	}
+
+}
+
+func TestNormalizePathURL(t *testing.T) {
+	baseDir, _ := os.Getwd()
+
+	rawPath := "template.yaml"
+	basePath, _ := filepath.Abs(".")
+	result, _ := gophercloud.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, _ = gophercloud.NormalizePathURL(basePath, rawPath)
+	expected = "http://www.google.com"
+	th.CheckEquals(t, expected, result)
+
+	rawPath = "very/nested/file.yaml"
+	basePath, _ = filepath.Abs(".")
+	result, _ = gophercloud.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, _ = gophercloud.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, _ = gophercloud.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, _ = gophercloud.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, _ = gophercloud.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, _ = gophercloud.NormalizePathURL(basePath, rawPath)
+	expected = strings.Join([]string{"file:/", filepath.ToSlash(baseDir), "only/file/even/more/very/nested/file.yaml"}, "/")
+	th.CheckEquals(t, expected, result)
+
+}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..68f9a5d
--- /dev/null
+++ b/util.go
@@ -0,0 +1,102 @@
+package gophercloud
+
+import (
+	"fmt"
+	"net/url"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+// WaitFor polls a predicate function, once per second, up to a timeout limit.
+// This is useful to wait for a resource to transition to a certain state.
+// To handle situations when the predicate might hang indefinitely, the
+// predicate will be prematurely cancelled after the timeout.
+// 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 {
+	type WaitForResult struct {
+		Success bool
+		Error   error
+	}
+
+	start := time.Now().Unix()
+
+	for {
+		// If a timeout is set, and that's been exceeded, shut it down.
+		if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) {
+			return fmt.Errorf("A timeout occurred")
+		}
+
+		time.Sleep(1 * time.Second)
+
+		var result WaitForResult
+		ch := make(chan bool, 1)
+		go func() {
+			defer close(ch)
+			satisfied, err := predicate()
+			result.Success = satisfied
+			result.Error = err
+		}()
+
+		select {
+		case <-ch:
+			if result.Error != nil {
+				return result.Error
+			}
+			if result.Success {
+				return nil
+			}
+		// If the predicate has not finished by the timeout, cancel it.
+		case <-time.After(time.Duration(timeout) * time.Second):
+			return fmt.Errorf("A timeout occurred")
+		}
+	}
+}
+
+// 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
+
+}