Merge pull request #4 from jrperritt/master

penultimate additions (or so) from `rackspace/gophercloud`
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1dc23e..e69de29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +0,0 @@
-ExtractJSON method on gophercloud.Result
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2cf355b..0d85ca8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,9 +1,9 @@
-# Contributing to gophercloud
+# Contributing to Gophercloud
 
 - [Getting started](#getting-started)
 - [Tests](#tests)
 - [Style guide](#basic-style-guide)
-- [5 ways to get involved](#5-ways-to-get-involved)
+- [3 ways to get involved](#5-ways-to-get-involved)
 
 ## Setting up your git workspace
 
@@ -97,6 +97,7 @@
 
 	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) {
@@ -131,7 +132,7 @@
 	})
 
 	// Call our API operation
-	network, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	network, err := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
 
 	// Assert no errors and equality
 	th.AssertNoErr(t, err)
@@ -182,15 +183,9 @@
 
 ## Style guide
 
+See [here](/STYLEGUIDE.md)
 
-
-We follow the standard formatting recommendations and language idioms set out
-in the [Effective Go](https://golang.org/doc/effective_go.html) guide. It's
-definitely worth reading - but the relevant sections are
-[formatting](https://golang.org/doc/effective_go.html#formatting)
-and [names](https://golang.org/doc/effective_go.html#names).
-
-## 5 ways to get involved
+## 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
@@ -201,23 +196,7 @@
 2. checkout a [new branch](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches)
 3. submit your branch as a [pull request](https://help.github.com/articles/creating-a-pull-request/)
 
-### 1. Providing feedback
-
-On of the easiest ways to get readily involved in our project is to let us know
-about your experiences using our SDK. Feedback like this is incredibly useful
-to us, because it allows us to refine and change features based on what our
-users want and expect of us. There are a bunch of ways to get in contact! You
-can [ping us](https://developer.rackspace.com/support/) via e-mail, talk to us on irc
-(#rackspace-dev on freenode), [tweet us](https://twitter.com/rackspace), or
-submit an issue on our [bug tracker](/issues). Things you might like to tell us
-are:
-
-* how easy was it to start using our SDK?
-* did it meet your expectations? If not, why not?
-* did our documentation help or hinder you?
-* what could we improve in general?
-
-### 2. Fixing bugs
+### 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
@@ -226,41 +205,16 @@
 thread to see the current state of the issue and if anybody has made any
 breakthroughs on it so far.
 
-### 3. Improving documentation
-
-We have three forms of documentation:
-
-* short README documents that briefly introduce a topic
-* reference documentation on [godoc.org](http://godoc.org) that is automatically
-generated from source code comments
-* user documentation on our [homepage](http://gophercloud.io) that includes
-getting started guides, installation guides and code samples
+### 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!
 
-### 4. Optimizing existing features
-
-If you would like to improve or optimize an existing feature, please be aware
-that we adhere to [semantic versioning](http://semver.org) - which means that
-we cannot introduce breaking changes to the API without a major version change
-(v1.x -> v2.x). Making that leap is a big step, so we encourage contributors to
-refactor rather than rewrite. Running tests will prevent regression and avoid
-the possibility of breaking somebody's current implementation.
-
-Another tip is to keep the focus of your work as small as possible - try not to
-introduce a change that affects lots and lots of files because it introduces
-added risk and increases the cognitive load on the reviewers checking your
-work. Change-sets which are easily understood and will not negatively impact
-users are more likely to be integrated quickly.
-
-Lastly, if you're seeking to optimize a particular operation, you should try to
-demonstrate a negative performance impact - perhaps using Go's inbuilt
-[benchmark capabilities](http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go).
-
-### 5. Working on a new feature
+###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
diff --git a/README.md b/README.md
index 6695047..4d2efaf 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-# WARNING: CURRENTLY NOT SUITABLE FOR CONSUMPTION. API IN FLUX.
 # 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)
@@ -126,27 +125,15 @@
 new resource in the `server` variable (a
 [`servers.Server`](http://godoc.org/github.com/gophercloud/gophercloud) struct).
 
-### Next steps
+## Backwards-Compatibility Guarantees
 
-Cool! You've handled authentication, got your `ProviderClient` and provisioned
-a new server. You're now ready to use more OpenStack services.
-
-* [Getting started with Compute](http://gophercloud.io/docs/compute)
-* [Getting started with Object Storage](http://gophercloud.io/docs/object-storage)
-* [Getting started with Networking](http://gophercloud.io/docs/networking)
-* [Getting started with Block Storage](http://gophercloud.io/docs/block-storage)
-* [Getting started with Identity](http://gophercloud.io/docs/identity)
+None. Vendor it and write tests covering the parts you use.
 
 ## Contributing
 
-Engaging the community and lowering barriers for contributors is something we
-care a lot about. For this reason, we've taken the time to write a [contributing
-guide](./CONTRIBUTING.md) for folks interested in getting involved in our project.
-If you're not sure how you can get involved, feel free to submit an issue or
-[contact us](https://developer.rackspace.com/support/). You don't need to be a
-Go expert - all members of the community are welcome!
+See the [contributing guide](./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) or [contact us directly](https://developer.rackspace.com/support/).
+to submit an issue to our [bug tracker](/issues).
diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md
new file mode 100644
index 0000000..4cc84f6
--- /dev/null
+++ b/STYLEGUIDE.md
@@ -0,0 +1,39 @@
+
+## 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 `@`.
+
+- 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.
diff --git a/UPGRADING.md b/UPGRADING.md
deleted file mode 100644
index 76a94d5..0000000
--- a/UPGRADING.md
+++ /dev/null
@@ -1,338 +0,0 @@
-# Upgrading to v1.0.0
-
-With the arrival of this new major version increment, the unfortunate news is
-that breaking changes have been introduced to existing services. The API
-has been completely rewritten from the ground up to make the library more
-extensible, maintainable and easy-to-use.
-
-Below we've compiled upgrade instructions for the various services that
-existed before. If you have a specific issue that is not addressed below,
-please [submit an issue](/issues/new) or
-[e-mail our support team](https://developer.rackspace.com/support/).
-
-* [Authentication](#authentication)
-* [Servers](#servers)
-  * [List servers](#list-servers)
-  * [Get server details](#get-server-details)
-  * [Create server](#create-server)
-  * [Resize server](#resize-server)
-  * [Reboot server](#reboot-server)
-  * [Update server](#update-server)
-  * [Rebuild server](#rebuild-server)
-  * [Change admin password](#change-admin-password)
-  * [Delete server](#delete-server)
-  * [Rescue server](#rescue-server)
-* [Images and flavors](#images-and-flavors)
-  * [List images](#list-images)
-  * [List flavors](#list-flavors)
-  * [Create/delete image](#createdelete-image)
-* [Other](#other)
-  * [List keypairs](#list-keypairs)
-  * [Create/delete keypair](#createdelete-keypair)
-  * [List IP addresses](#list-ip-addresses)
-
-# Authentication
-
-One of the major differences that this release introduces is the level of
-sub-packaging to differentiate between services and providers. You now have
-the option of authenticating with OpenStack and other providers (like Rackspace).
-
-To authenticate with a vanilla OpenStack installation, you can either specify
-your credentials like this:
-
-```go
-import (
-  "github.com/rackspace/gophercloud"
-  "github.com/rackspace/gophercloud/openstack"
-)
-
-opts := gophercloud.AuthOptions{
-  IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
-  Username: "{username}",
-  Password: "{password}",
-  TenantID: "{tenant_id}",
-}
-```
-
-Or have them pulled in through environment variables, like this:
-
-```go
-opts, err := openstack.AuthOptionsFromEnv()
-```
-
-Once you have your `AuthOptions` struct, you pass it in to get back a `Provider`,
-like so:
-
-```go
-provider, err := openstack.AuthenticatedClient(opts)
-```
-
-This provider is the top-level structure that all services are created from.
-
-# Servers
-
-Before you can interact with the Compute API, you need to retrieve a
-`gophercloud.ServiceClient`. To do this:
-
-```go
-// Define your region, etc.
-opts := gophercloud.EndpointOpts{Region: "RegionOne"}
-
-client, err := openstack.NewComputeV2(provider, opts)
-```
-
-## List servers
-
-All operations that involve API collections (servers, flavors, images) now use
-the `pagination.Pager` interface. This interface represents paginated entities
-that can be iterated over.
-
-Once you have a Pager, you can then pass a callback function into its `EachPage`
-method, and this will allow you to traverse over the collection and execute
-arbitrary functionality. So, an example with list servers:
-
-```go
-import (
-  "fmt"
-  "github.com/rackspace/gophercloud/pagination"
-  "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-)
-
-// We have the option of filtering the server list. If we want the full
-// collection, leave it as an empty struct or nil
-opts := servers.ListOpts{Name: "server_1"}
-
-// Retrieve a pager (i.e. a paginated collection)
-pager := servers.List(client, opts)
-
-// Define an anonymous function to be executed on each page's iteration
-err := pager.EachPage(func(page pagination.Page) (bool, error) {
-  serverList, err := servers.ExtractServers(page)
-
-  // `s' will be a servers.Server struct
-  for _, s := range serverList {
-    fmt.Printf("We have a server. ID=%s, Name=%s", s.ID, s.Name)
-  }
-})
-```
-
-## Get server details
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-// Get the HTTP result
-response := servers.Get(client, "server_id")
-
-// Extract a Server struct from the response
-server, err := response.Extract()
-```
-
-## Create server
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-// Define our options
-opts := servers.CreateOpts{
-  Name: "new_server",
-  FlavorRef: "flavorID",
-  ImageRef: "imageID",
-}
-
-// Get our response
-response := servers.Create(client, opts)
-
-// Extract
-server, err := response.Extract()
-```
-
-## Change admin password
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-result := servers.ChangeAdminPassword(client, "server_id", "newPassword_&123")
-```
-
-## Resize server
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-result := servers.Resize(client, "server_id", "new_flavor_id")
-```
-
-## Reboot server
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-// You have a choice of two reboot methods: servers.SoftReboot or servers.HardReboot
-result := servers.Reboot(client, "server_id", servers.SoftReboot)
-```
-
-## Update server
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-opts := servers.UpdateOpts{Name: "new_name"}
-
-server, err := servers.Update(client, "server_id", opts).Extract()
-```
-
-## Rebuild server
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-// You have the option of specifying additional options
-opts := RebuildOpts{
-  Name:      "new_name",
-  AdminPass: "admin_password",
-  ImageID:   "image_id",
-  Metadata:  map[string]string{"owner": "me"},
-}
-
-result := servers.Rebuild(client, "server_id", opts)
-
-// You can extract a servers.Server struct from the HTTP response
-server, err := result.Extract()
-```
-
-## Delete server
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-
-response := servers.Delete(client, "server_id")
-```
-
-## Rescue server
-
-The server rescue extension for Compute is not currently supported.
-
-# Images and flavors
-
-## List images
-
-As with listing servers (see above), you first retrieve a Pager, and then pass
-in a callback over each page:
-
-```go
-import (
-  "github.com/rackspace/gophercloud/pagination"
-  "github.com/rackspace/gophercloud/openstack/compute/v2/images"
-)
-
-// We have the option of filtering the image list. If we want the full
-// collection, leave it as an empty struct
-opts := images.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", Name: "Ubuntu 12.04"}
-
-// Retrieve a pager (i.e. a paginated collection)
-pager := images.List(client, opts)
-
-// Define an anonymous function to be executed on each page's iteration
-err := pager.EachPage(func(page pagination.Page) (bool, error) {
-  imageList, err := images.ExtractImages(page)
-
-  for _, i := range imageList {
-    // "i" will be an images.Image
-  }
-})
-```
-
-## List flavors
-
-```go
-import (
-  "github.com/rackspace/gophercloud/pagination"
-  "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
-)
-
-// We have the option of filtering the flavor list. If we want the full
-// collection, leave it as an empty struct
-opts := flavors.ListOpts{ChangesSince: "2014-01-01T01:02:03Z", MinRAM: 4}
-
-// Retrieve a pager (i.e. a paginated collection)
-pager := flavors.List(client, opts)
-
-// Define an anonymous function to be executed on each page's iteration
-err := pager.EachPage(func(page pagination.Page) (bool, error) {
-  flavorList, err := networks.ExtractFlavors(page)
-
-  for _, f := range flavorList {
-    // "f" will be a flavors.Flavor
-  }
-})
-```
-
-## Create/delete image
-
-Image management has been shifted to Glance, but unfortunately this service is
-not supported as of yet. You can, however, list Compute images like so:
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/images"
-
-// Retrieve a pager (i.e. a paginated collection)
-pager := images.List(client, opts)
-
-// Define an anonymous function to be executed on each page's iteration
-err := pager.EachPage(func(page pagination.Page) (bool, error) {
-  imageList, err := images.ExtractImages(page)
-
-  for _, i := range imageList {
-    // "i" will be an images.Image
-  }
-})
-```
-
-# Other
-
-## List keypairs
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
-
-// Retrieve a pager (i.e. a paginated collection)
-pager := keypairs.List(client, opts)
-
-// Define an anonymous function to be executed on each page's iteration
-err := pager.EachPage(func(page pagination.Page) (bool, error) {
-  keyList, err := keypairs.ExtractKeyPairs(page)
-
-  for _, k := range keyList {
-    // "k" will be a keypairs.KeyPair
-  }
-})
-```
-
-## Create/delete keypairs
-
-To create a new keypair, you need to specify its name and, optionally, a
-pregenerated OpenSSH-formatted public key.
-
-```go
-import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
-
-opts := keypairs.CreateOpts{
-  Name: "new_key",
-  PublicKey: "...",
-}
-
-response := keypairs.Create(client, opts)
-
-key, err := response.Extract()
-```
-
-To delete an existing keypair:
-
-```go
-response := keypairs.Delete(client, "keypair_id")
-```
-
-## List IP addresses
-
-This operation is not currently supported.
diff --git a/acceptance/openstack/blockstorage/v2/extensions/pkg.go b/acceptance/openstack/blockstorage/v2/extensions/pkg.go
new file mode 100644
index 0000000..89d906d
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v2/extensions/pkg.go
@@ -0,0 +1,3 @@
+// The extensions package contains acceptance tests for the Openstack Cinder V2 extensions service.
+
+package extensions
diff --git a/acceptance/openstack/blockstorage/v2/extensions/volumeactions_test.go b/acceptance/openstack/blockstorage/v2/extensions/volumeactions_test.go
new file mode 100644
index 0000000..20a4597
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v2/extensions/volumeactions_test.go
@@ -0,0 +1,149 @@
+// +build acceptance blockstorage
+
+package extensions
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/extensions/volumeactions"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := openstack.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	return openstack.NewBlockStorageV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func TestVolumeAttach(t *testing.T) {
+	client, err := newClient(t)
+	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)
+
+	instanceID := os.Getenv("OS_INSTANCE_ID")
+	if instanceID == "" {
+		t.Fatal("Environment variable OS_INSTANCE_ID is required")
+	}
+
+	t.Logf("Attaching volume")
+	err = volumeactions.Attach(client, cv.ID, &volumeactions.AttachOpts{
+		MountPoint:   "/mnt",
+		Mode:         "rw",
+		InstanceUUID: instanceID,
+	}).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	err = volumes.WaitForStatus(client, cv.ID, "in-use", 60)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Detaching volume")
+	err = volumeactions.Detach(client, cv.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestVolumeReserve(t *testing.T) {
+	client, err := newClient(t)
+	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)
+
+	t.Logf("Reserving volume")
+	err = volumeactions.Reserve(client, cv.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	err = volumes.WaitForStatus(client, cv.ID, "attaching", 60)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Unreserving volume")
+	err = volumeactions.Unreserve(client, cv.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+	th.AssertNoErr(t, err)
+}
+
+func TestVolumeConns(t *testing.T) {
+	client, err := newClient(t)
+	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/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..9edf31a
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v2/volumes_test.go
@@ -0,0 +1,63 @@
+// +build acceptance blockstorage
+
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
+	ao, err := openstack.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := openstack.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	return openstack.NewBlockStorageV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func TestVolumes(t *testing.T) {
+	client, err := newClient(t)
+	th.AssertNoErr(t, err)
+
+	cv, err := volumes.Create(client, &volumes.CreateOpts{
+		Size: 1,
+		Name: "blockv2-volume",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer func() {
+		err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+		th.AssertNoErr(t, err)
+		err = volumes.Delete(client, cv.ID).ExtractErr()
+		th.AssertNoErr(t, err)
+	}()
+
+	_, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+		Name: "blockv2-updated-volume",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	v, err := volumes.Get(client, cv.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Got volume: %+v\n", v)
+
+	if v.Name != "blockv2-updated-volume" {
+		t.Errorf("Unable to update volume: Expected name: blockv2-updated-volume\nActual name: %s", v.Name)
+	}
+
+	err = volumes.List(client, &volumes.ListOpts{Name: "blockv2-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+		vols, err := volumes.ExtractVolumes(page)
+		th.CheckEquals(t, 1, len(vols))
+		return true, err
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
index ed948bd..760ee5b 100644
--- a/acceptance/openstack/networking/v2/extensions/lbaas/common.go
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
@@ -45,6 +45,7 @@
 		Protocol: "HTTP",
 		Name:     "tmp_pool",
 		SubnetID: subnetID,
+		Provider: "haproxy",
 	}).Extract()
 
 	th.AssertNoErr(t, err)
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
index 70ee844..6151217 100644
--- a/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
@@ -55,7 +55,7 @@
 		}
 
 		for _, p := range poolList {
-			t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s]", p.ID, p.Name, p.Status, p.LBMethod)
+			t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s] Provider [%s]", p.ID, p.Name, p.Status, p.LBMethod, p.Provider)
 		}
 
 		return true, nil
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancer_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancer_test.go
new file mode 100644
index 0000000..051b7eb
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancer_test.go
@@ -0,0 +1,493 @@
+// +build acceptance networking lbaas_v2 lbaasloadbalancer
+
+package lbaas_v2
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	base "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"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"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+// Note: when creating a new Loadbalancer (VM), it can take some time before it is ready for use,
+// this timeout is used for waiting until the Loadbalancer provisioning status goes to ACTIVE state.
+const loadbalancerActiveTimeoutSeconds = 120
+const loadbalancerDeleteTimeoutSeconds = 10
+
+func setupTopology(t *testing.T) (string, string) {
+	// create network
+	n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created network, ID %s", n.ID)
+
+	// create subnet
+	s, err := subnets.Create(base.Client, subnets.CreateOpts{
+		NetworkID: n.ID,
+		CIDR:      "192.168.199.0/24",
+		IPVersion: subnets.IPv4,
+		Name:      "tmp_subnet",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created subnet, ID %s", s.ID)
+
+	return n.ID, s.ID
+}
+
+func deleteTopology(t *testing.T, networkID string) {
+	res := networks.Delete(base.Client, networkID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("deleted network, ID %s", networkID)
+}
+
+func TestLoadbalancers(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// setup network topology
+	networkID, subnetID := setupTopology(t)
+
+	// create Loadbalancer
+	loadbalancerID := createLoadbalancer(t, subnetID)
+
+	// list Loadbalancers
+	listLoadbalancers(t)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// update Loadbalancer
+	updateLoadbalancer(t, loadbalancerID)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// create listener
+	listenerID := createListener(t, listeners.ProtocolHTTP, 80, loadbalancerID)
+
+	// list listeners
+	listListeners(t)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// update listener
+	updateListener(t, listenerID)
+
+	// get listener
+	getListener(t, listenerID)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// create pool
+	poolID := createPool(t, pools.ProtocolHTTP, listenerID, pools.LBMethodRoundRobin)
+
+	// list pools
+	listPools(t)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// update pool
+	updatePool(t, poolID)
+
+	// get pool
+	getPool(t, poolID)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// create member
+	memberID := createMember(t, subnetID, poolID, "1.2.3.4", 80, 5)
+
+	// list members
+	listMembers(t, poolID)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// update member
+	updateMember(t, poolID, memberID)
+
+	// get member
+	getMember(t, poolID, memberID)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// create monitor
+	monitorID := createMonitor(t, poolID, monitors.TypePING, 10, 10, 3)
+
+	// list monitors
+	listMonitors(t)
+
+	// get Loadbalancer and wait until ACTIVE
+	getLoadbalancerWaitActive(t, loadbalancerID)
+
+	// update monitor
+	updateMonitor(t, monitorID)
+
+	// get monitor
+	getMonitor(t, monitorID)
+
+	// get loadbalancer statuses tree
+	rawStatusTree, err := loadbalancers.GetStatuses(base.Client, loadbalancerID).ExtractStatuses()
+	if err == nil {
+		// verify statuses tree ID's of relevant objects
+		if rawStatusTree.Loadbalancer.ID != loadbalancerID {
+			t.Errorf("Loadbalancer ID did not match")
+		}
+		if rawStatusTree.Loadbalancer.Listeners[0].ID != listenerID {
+			t.Errorf("Listner ID did not match")
+		}
+		if rawStatusTree.Loadbalancer.Listeners[0].Pools[0].ID != poolID {
+			t.Errorf("Pool ID did not match")
+		}
+		if rawStatusTree.Loadbalancer.Listeners[0].Pools[0].Members[0].ID != memberID {
+			t.Errorf("Member ID did not match")
+		}
+		if rawStatusTree.Loadbalancer.Listeners[0].Pools[0].Monitor.ID != monitorID {
+			t.Errorf("Monitor ID did not match")
+		}
+	} else {
+		t.Errorf("Failed to extract Loadbalancer statuses tree: %v", err)
+	}
+
+	getLoadbalancerWaitActive(t, loadbalancerID)
+	deleteMonitor(t, monitorID)
+	getLoadbalancerWaitActive(t, loadbalancerID)
+	deleteMember(t, poolID, memberID)
+	getLoadbalancerWaitActive(t, loadbalancerID)
+	deletePool(t, poolID)
+	getLoadbalancerWaitActive(t, loadbalancerID)
+	deleteListener(t, listenerID)
+	getLoadbalancerWaitActive(t, loadbalancerID)
+	deleteLoadbalancer(t, loadbalancerID)
+	getLoadbalancerWaitDeleted(t, loadbalancerID)
+	deleteTopology(t, networkID)
+}
+
+func createLoadbalancer(t *testing.T, subnetID string) string {
+	lb, err := loadbalancers.Create(base.Client, loadbalancers.CreateOpts{
+		VipSubnetID:  subnetID,
+		Name:         "tmp_loadbalancer",
+		AdminStateUp: loadbalancers.Up,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+	t.Logf("Created Loadbalancer, ID %s", lb.ID)
+
+	return lb.ID
+}
+
+func deleteLoadbalancer(t *testing.T, loadbalancerID string) {
+	res := loadbalancers.Delete(base.Client, loadbalancerID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("deleted Loadbalancer, ID %s", loadbalancerID)
+}
+
+func listLoadbalancers(t *testing.T) {
+	err := loadbalancers.List(base.Client, loadbalancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		loadbalancerList, err := loadbalancers.ExtractLoadbalancers(page)
+		if err != nil {
+			t.Errorf("Failed to extract Loadbalancers: %v", err)
+			return false, err
+		}
+
+		for _, loadbalancer := range loadbalancerList {
+			t.Logf("Listing Loadbalancer: ID [%s] Name [%s] Address [%s]",
+				loadbalancer.ID, loadbalancer.Name, loadbalancer.VipAddress)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getLoadbalancerWaitDeleted(t *testing.T, loadbalancerID string) {
+	start := time.Now().Second()
+	for {
+		time.Sleep(1 * time.Second)
+
+		if time.Now().Second()-start >= loadbalancerDeleteTimeoutSeconds {
+			t.Errorf("Loadbalancer failed to delete")
+			return
+		}
+
+		_, err := loadbalancers.Get(base.Client, loadbalancerID).Extract()
+		if err != nil {
+			if errData, ok := err.(*(gophercloud.UnexpectedResponseCodeError)); ok {
+				if errData.Actual == 404 {
+					return
+				}
+			} else {
+				th.AssertNoErr(t, err)
+			}
+		}
+	}
+}
+
+func getLoadbalancerWaitActive(t *testing.T, loadbalancerID string) {
+	start := time.Now().Second()
+	for {
+		time.Sleep(1 * time.Second)
+
+		if time.Now().Second()-start >= loadbalancerActiveTimeoutSeconds {
+			t.Errorf("Loadbalancer failed to go into ACTIVE provisioning status")
+			return
+		}
+
+		loadbalancer, err := loadbalancers.Get(base.Client, loadbalancerID).Extract()
+		th.AssertNoErr(t, err)
+		if loadbalancer.ProvisioningStatus == "ACTIVE" {
+			t.Logf("Retrieved Loadbalancer, ID [%s]: OperatingStatus [%s]", loadbalancer.ID, loadbalancer.ProvisioningStatus)
+			return
+		}
+	}
+}
+
+func updateLoadbalancer(t *testing.T, loadbalancerID string) {
+	_, err := loadbalancers.Update(base.Client, loadbalancerID, loadbalancers.UpdateOpts{Name: "tmp_newName"}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated Loadbalancer ID [%s]", loadbalancerID)
+}
+
+func listListeners(t *testing.T) {
+	err := listeners.List(base.Client, listeners.ListOpts{Name: "tmp_listener"}).EachPage(func(page pagination.Page) (bool, error) {
+		listenerList, err := listeners.ExtractListeners(page)
+		if err != nil {
+			t.Errorf("Failed to extract Listeners: %v", err)
+			return false, err
+		}
+
+		for _, listener := range listenerList {
+			t.Logf("Listing Listener: ID [%s] Name [%s] Loadbalancers [%s]",
+				listener.ID, listener.Name, listener.Loadbalancers)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func createListener(t *testing.T, protocol listeners.Protocol, protocolPort int, loadbalancerID string) string {
+	l, err := listeners.Create(base.Client, listeners.CreateOpts{
+		Protocol:       protocol,
+		ProtocolPort:   protocolPort,
+		LoadbalancerID: loadbalancerID,
+		Name:           "tmp_listener",
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+	t.Logf("Created Listener, ID %s", l.ID)
+
+	return l.ID
+}
+
+func deleteListener(t *testing.T, listenerID string) {
+	res := listeners.Delete(base.Client, listenerID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted Loadbalancer, ID %s", listenerID)
+}
+
+func getListener(t *testing.T, listenerID string) {
+	listener, err := listeners.Get(base.Client, listenerID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting Listener, ID [%s]: ", listener.ID)
+}
+
+func updateListener(t *testing.T, listenerID string) {
+	_, err := listeners.Update(base.Client, listenerID, listeners.UpdateOpts{Name: "tmp_newName"}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated Listener, ID [%s]", listenerID)
+}
+
+func listPools(t *testing.T) {
+	err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		poolsList, err := pools.ExtractPools(page)
+		if err != nil {
+			t.Errorf("Failed to extract Pools: %v", err)
+			return false, err
+		}
+
+		for _, pool := range poolsList {
+			t.Logf("Listing Pool: ID [%s] Name [%s] Listeners [%s] LBMethod [%s]",
+				pool.ID, pool.Name, pool.Listeners, pool.LBMethod)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func createPool(t *testing.T, protocol pools.Protocol, listenerID string, lbMethod pools.LBMethod) string {
+	p, err := pools.Create(base.Client, pools.CreateOpts{
+		LBMethod:   lbMethod,
+		Protocol:   protocol,
+		Name:       "tmp_pool",
+		ListenerID: listenerID,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created Pool, ID %s", p.ID)
+
+	return p.ID
+}
+
+func deletePool(t *testing.T, poolID string) {
+	res := pools.Delete(base.Client, poolID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted Pool, ID %s", poolID)
+}
+
+func getPool(t *testing.T, poolID string) {
+	pool, err := pools.Get(base.Client, poolID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting Pool, ID [%s]: ", pool.ID)
+}
+
+func updatePool(t *testing.T, poolID string) {
+	_, err := pools.Update(base.Client, poolID, pools.UpdateOpts{Name: "tmp_newName"}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated Pool, ID [%s]", poolID)
+}
+
+func createMember(t *testing.T, subnetID string, poolID string, address string, protocolPort int, weight int) string {
+	m, err := pools.CreateAssociateMember(base.Client, poolID, pools.MemberCreateOpts{
+		SubnetID:     subnetID,
+		Address:      address,
+		ProtocolPort: protocolPort,
+		Weight:       weight,
+		Name:         "tmp_member",
+	}).ExtractMember()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created Member, ID %s", m.ID)
+
+	return m.ID
+}
+
+func deleteMember(t *testing.T, poolID string, memberID string) {
+	res := pools.DeleteMember(base.Client, poolID, memberID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted Member, ID %s", memberID)
+}
+
+func listMembers(t *testing.T, poolID string) {
+	err := pools.ListAssociateMembers(base.Client, poolID, pools.MemberListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		membersList, err := pools.ExtractMembers(page)
+		if err != nil {
+			t.Errorf("Failed to extract Members: %v", err)
+			return false, err
+		}
+
+		for _, member := range membersList {
+			t.Logf("Listing Member: ID [%s] Name [%s] Pool ID [%s] Weight [%s]",
+				member.ID, member.Name, member.PoolID, member.Weight)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getMember(t *testing.T, poolID string, memberID string) {
+	member, err := pools.GetAssociateMember(base.Client, poolID, memberID).ExtractMember()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting Member, ID [%s]: ", member.ID)
+}
+
+func updateMember(t *testing.T, poolID string, memberID string) {
+	_, err := pools.UpdateAssociateMember(base.Client, poolID, memberID, pools.MemberUpdateOpts{Name: "tmp_newName"}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated Member, ID [%s], in Pool, ID [%s]", memberID, poolID)
+}
+
+func createMonitor(t *testing.T, poolID string, checkType string, delay int, timeout int, maxRetries int) string {
+	m, err := monitors.Create(base.Client, monitors.CreateOpts{
+		PoolID:     poolID,
+		Name:       "tmp_monitor",
+		Delay:      delay,
+		Timeout:    timeout,
+		MaxRetries: maxRetries,
+		Type:       checkType,
+	}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created Monitor, ID [%s]", m.ID)
+
+	return m.ID
+}
+
+func deleteMonitor(t *testing.T, monitorID string) {
+	res := monitors.Delete(base.Client, monitorID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted Monitor, ID %s", monitorID)
+}
+
+func listMonitors(t *testing.T) {
+	err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		monitorsList, err := monitors.ExtractMonitors(page)
+		if err != nil {
+			t.Errorf("Failed to extract Monitors: %v", err)
+			return false, err
+		}
+
+		for _, monitor := range monitorsList {
+			t.Logf("Listing Monitors: ID [%s] Type [%s] HTTPMethod [%s] URLPath [%s]",
+				monitor.ID, monitor.Type, monitor.HTTPMethod, monitor.URLPath)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getMonitor(t *testing.T, monitorID string) {
+	monitor, err := monitors.Get(base.Client, monitorID).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting Monitor, ID [%s]: ", monitor.ID)
+}
+
+func updateMonitor(t *testing.T, monitorID string) {
+	_, err := monitors.Update(base.Client, monitorID, monitors.UpdateOpts{MaxRetries: 10}).Extract()
+
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated Monitor, ID [%s]", monitorID)
+}
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/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_test.go b/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go
new file mode 100644
index 0000000..b703e3b
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/portsbinding/portsbinding_test.go
@@ -0,0 +1,129 @@
+// +build acceptance networking portsbinding
+
+package portsbinding
+
+import (
+	"testing"
+
+	base "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
+	"github.com/gophercloud/gophercloud/pagination"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestPortBinding(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	// Setup network
+	t.Log("Setting up network")
+	networkID, err := createNetwork()
+	th.AssertNoErr(t, err)
+	defer networks.Delete(base.Client, networkID)
+
+	// Setup subnet
+	t.Logf("Setting up subnet on network %s", networkID)
+	subnetID, err := createSubnet(networkID)
+	th.AssertNoErr(t, err)
+	defer subnets.Delete(base.Client, subnetID)
+
+	// Create port
+	t.Logf("Create port based on subnet %s", subnetID)
+	hostID := "localhost"
+	portID := createPort(t, networkID, subnetID, hostID)
+
+	// Get port
+	if portID == "" {
+		t.Fatalf("In order to retrieve a port, the portID must be set")
+	}
+	p, err := portsbinding.Get(base.Client, portID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, p.ID, portID)
+	th.AssertEquals(t, p.HostID, hostID)
+
+	// Update port
+	newHostID := "openstack"
+	updateOpts := portsbinding.UpdateOpts{
+		HostID: newHostID,
+	}
+	p, err = portsbinding.Update(base.Client, portID, updateOpts).Extract()
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, p.HostID, newHostID)
+
+	// List ports
+	t.Logf("Listing all ports")
+	listPorts(t)
+
+	// Delete port
+	res := ports.Delete(base.Client, portID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func listPorts(t *testing.T) {
+	count := 0
+	pager := ports.List(base.Client, ports.ListOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- Page ---")
+
+		portList, err := portsbinding.ExtractPorts(page)
+		th.AssertNoErr(t, err)
+
+		for _, p := range portList {
+			t.Logf("Port: ID [%s] Name [%s] HostID [%s] VNICType [%s] VIFType [%s]",
+				p.ID, p.Name, p.HostID, p.VNICType, p.VIFType)
+		}
+
+		return true, nil
+	})
+
+	th.CheckNoErr(t, err)
+
+	if count == 0 {
+		t.Logf("No pages were iterated over when listing ports")
+	}
+}
+
+func createPort(t *testing.T, networkID, subnetID, hostID string) string {
+	enable := false
+	opts := portsbinding.CreateOpts{
+		CreateOptsBuilder: ports.CreateOpts{
+			NetworkID:    networkID,
+			Name:         "my_port",
+			AdminStateUp: &enable,
+			FixedIPs:     []ports.IP{{SubnetID: subnetID}},
+		},
+		HostID: hostID,
+	}
+
+	p, err := portsbinding.Create(base.Client, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, p.NetworkID, networkID)
+	th.AssertEquals(t, p.Name, "my_port")
+	th.AssertEquals(t, p.AdminStateUp, false)
+
+	return p.ID
+}
+
+func createNetwork() (string, error) {
+	res, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network", AdminStateUp: networks.Up}).Extract()
+	return res.ID, err
+}
+
+func createSubnet(networkID string) (string, error) {
+	s, err := subnets.Create(base.Client, subnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  subnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: subnets.Down,
+		AllocationPools: []subnets.AllocationPool{
+			{Start: "192.168.199.2", End: "192.168.199.200"},
+		},
+	}).Extract()
+	return s.ID, err
+}
diff --git a/auth_options.go b/auth_options.go
index 922a279..2ef427a 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -42,7 +42,12 @@
 	// 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:"-"`
+	//
+	// 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
 
 	// TokenID allows users to authenticate (possibly as another user) with an
 	// authentication token ID.
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
index 9850cfa..d668591 100644
--- a/openstack/blockstorage/v1/volumes/requests.go
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -17,9 +17,9 @@
 type CreateOpts struct {
 	Size         int               `json:"size" required:"true"`
 	Availability string            `json:"availability,omitempty"`
-	Description  string            `json:"description,omitempty"`
+	Description  string            `json:"display_description,omitempty"`
 	Metadata     map[string]string `json:"metadata,omitempty"`
-	Name         string            `json:"name,omitempty"`
+	Name         string            `json:"display_name,omitempty"`
 	SnapshotID   string            `json:"snapshot_id,omitempty"`
 	SourceVolID  string            `json:"source_volid,omitempty"`
 	ImageID      string            `json:"imageRef,omitempty"`
@@ -74,7 +74,7 @@
 	// 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"`
+	Name string `q:"display_name"`
 	// List only volumes that have a status of Status.
 	Status string `q:"status"`
 }
@@ -110,8 +110,8 @@
 // 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"`
+	Name        string            `json:"display_name,omitempty"`
+	Description string            `json:"display_description,omitempty"`
 	Metadata    map[string]string `json:"metadata,omitempty"`
 }
 
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
index 09d1ba6..b056b8c 100644
--- a/openstack/blockstorage/v1/volumes/results.go
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -9,40 +9,28 @@
 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.JSONRFC3339Milli `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"`
 }
diff --git a/openstack/blockstorage/v2/extensions/volumeactions/doc.go b/openstack/blockstorage/v2/extensions/volumeactions/doc.go
new file mode 100644
index 0000000..0935fdb
--- /dev/null
+++ b/openstack/blockstorage/v2/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/v2/extensions/volumeactions/requests.go b/openstack/blockstorage/v2/extensions/volumeactions/requests.go
new file mode 100644
index 0000000..05a76f7
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/requests.go
@@ -0,0 +1,174 @@
+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
+}
+
+// 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
+}
diff --git a/openstack/blockstorage/v2/extensions/volumeactions/results.go b/openstack/blockstorage/v2/extensions/volumeactions/results.go
new file mode 100644
index 0000000..9bf6a7b
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/results.go
@@ -0,0 +1,46 @@
+package volumeactions
+
+import "github.com/gophercloud/gophercloud"
+
+// AttachResult contains the response body and error from a Get request.
+type AttachResult struct {
+	gophercloud.ErrResult
+}
+
+// DetachResult contains the response body and error from a Get request.
+type DetachResult 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
+}
diff --git a/openstack/blockstorage/v2/extensions/volumeactions/testing/doc.go b/openstack/blockstorage/v2/extensions/volumeactions/testing/doc.go
new file mode 100644
index 0000000..7603f83
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/testing/doc.go
@@ -0,0 +1 @@
+package testing
diff --git a/openstack/blockstorage/v2/extensions/volumeactions/testing/fixtures.go b/openstack/blockstorage/v2/extensions/volumeactions/testing/fixtures.go
new file mode 100644
index 0000000..da661a6
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/testing/fixtures.go
@@ -0,0 +1,183 @@
+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 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 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, `{}`)
+		})
+}
diff --git a/openstack/blockstorage/v2/extensions/volumeactions/testing/requests_test.go b/openstack/blockstorage/v2/extensions/volumeactions/testing/requests_test.go
new file mode 100644
index 0000000..dac4e01
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/testing/requests_test.go
@@ -0,0 +1,91 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/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 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 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)
+}
diff --git a/openstack/blockstorage/v2/extensions/volumeactions/urls.go b/openstack/blockstorage/v2/extensions/volumeactions/urls.go
new file mode 100644
index 0000000..4ddcca0
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/urls.go
@@ -0,0 +1,27 @@
+package volumeactions
+
+import "github.com/gophercloud/gophercloud"
+
+func attachURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("volumes", id, "action")
+}
+
+func detachURL(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)
+}
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..96864ae
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/results.go
@@ -0,0 +1,120 @@
+package volumes
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type Attachment struct {
+	ID         string                          `json:"id"`
+	VolumeID   string                          `json:"volume_id"`
+	ServerID   string                          `json:"instance_uuid"`
+	HostName   string                          `json:"attached_host"`
+	Device     string                          `json:"mountpoint"`
+	AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attach_time"`
+}
+
+// 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 gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+	// The date when this volume was last updated
+	UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+	// 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"`
+}
+
+/*
+THESE BELONG IN EXTENSIONS:
+// ReplicationDriverData contains data about the replication driver.
+ReplicationDriverData string `json:"os-volume-replication:driver_data"`
+// ReplicationExtendedStatus contains extended status about replication.
+ReplicationExtendedStatus string `json:"os-volume-replication:extended_status"`
+// TenantID is the id of the project that owns the volume.
+TenantID string `json:"os-vol-tenant-attr:tenant_id"`
+*/
+
+// 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 struct {
+		Volumes []Volume `json:"volumes"`
+	}
+	err := (r.(VolumePage)).ExtractInto(&s)
+	return s.Volumes, 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 struct {
+		Volume *Volume `json:"volume"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Volume, err
+}
+
+// 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..7603f83
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/testing/doc.go
@@ -0,0 +1 @@
+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..d4b9da0
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/testing/fixtures.go
@@ -0,0 +1,202 @@
+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": [
+        {
+        "id": "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf",
+        "volume_id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+        "instance_uuid": "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
+        "attached_host": "stack",
+        "mountpoint": "/dev/vdc"
+        }
+      ],
+      "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": [{
+        "attachment_id": "dbce64e3-f3b9-4423-a44f-a2b15deffa1b",
+        "id": "3eafc6f5-ed74-456d-90fb-f253f594dbae",
+        "volume_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "server_id": "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
+        "host_name": "stack",
+        "device": "/dev/vdd"
+        }],
+    "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..147beb5
--- /dev/null
+++ b/openstack/blockstorage/v2/volumes/testing/requests_test.go
@@ -0,0 +1,212 @@
+package testing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"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 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",
+				Attachments: []volumes.Attachment{{
+					ID:       "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf",
+					VolumeID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+					ServerID: "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
+					HostName: "stack",
+					Device:   "/dev/vdc",
+				}},
+				AvailabilityZone:   "nova",
+				Bootable:           "false",
+				ConsistencyGroupID: "",
+				CreatedAt:          gophercloud.JSONRFC3339MilliNoZ(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:          gophercloud.JSONRFC3339MilliNoZ(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 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{{
+				ID:       "47e9ecc5-4045-4ee3-9a4b-d859d546a0cf",
+				VolumeID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+				ServerID: "d1c4788b-9435-42e2-9b81-29f3be1cd01f",
+				HostName: "stack",
+				Device:   "/dev/vdc",
+			}},
+			AvailabilityZone:   "nova",
+			Bootable:           "false",
+			ConsistencyGroupID: "",
+			CreatedAt:          gophercloud.JSONRFC3339MilliNoZ(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:          gophercloud.JSONRFC3339MilliNoZ(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)
+}
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/client.go b/openstack/client.go
index 3e11508..680a782 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -293,6 +293,16 @@
 	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
+}
+
 // 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) {
diff --git a/openstack/compute/v2/extensions/diskconfig/results.go b/openstack/compute/v2/extensions/diskconfig/results.go
index 1957e12..3ba66f5 100644
--- a/openstack/compute/v2/extensions/diskconfig/results.go
+++ b/openstack/compute/v2/extensions/diskconfig/results.go
@@ -6,3 +6,12 @@
 	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/images/results.go b/openstack/compute/v2/images/results.go
index f38466b..a55b8f1 100644
--- a/openstack/compute/v2/images/results.go
+++ b/openstack/compute/v2/images/results.go
@@ -45,6 +45,8 @@
 	Status   string
 
 	Updated string
+	
+	Metadata map[string]string
 }
 
 // ImagePage contains a single page of results from a List operation.
diff --git a/openstack/compute/v2/images/testing/requests_test.go b/openstack/compute/v2/images/testing/requests_test.go
index a13b086..e2d64d3 100644
--- a/openstack/compute/v2/images/testing/requests_test.go
+++ b/openstack/compute/v2/images/testing/requests_test.go
@@ -38,8 +38,7 @@
 							"created": "2014-09-23T12:54:52Z",
 							"minDisk": 0,
 							"progress": 100,
-							"minRam": 0,
-							"metadata": {}
+							"minRam": 0
 						},
 						{
 							"status": "ACTIVE",
@@ -50,8 +49,7 @@
 							"created": "2014-09-23T12:51:42Z",
 							"minDisk": 0,
 							"progress": 100,
-							"minRam": 0,
-							"metadata": {}
+							"minRam": 0
 						}
 					]
 				}
@@ -131,8 +129,7 @@
 					"created": "2014-09-23T12:54:52Z",
 					"minDisk": 0,
 					"progress": 100,
-					"minRam": 0,
-					"metadata": {}
+					"minRam": 0
 				}
 			}
 		`)
diff --git a/openstack/compute/v2/servers/testing/results_test.go b/openstack/compute/v2/servers/testing/results_test.go
index e5334f0..5866957 100644
--- a/openstack/compute/v2/servers/testing/results_test.go
+++ b/openstack/compute/v2/servers/testing/results_test.go
@@ -9,6 +9,7 @@
 	"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"
 )
 
@@ -96,3 +97,14 @@
 	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/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
index 1c4ba7c..4983031 100644
--- a/openstack/identity/v2/tokens/requests.go
+++ b/openstack/identity/v2/tokens/requests.go
@@ -84,7 +84,8 @@
 		return
 	}
 	_, r.Err = client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
-		OkCodes: []int{200, 203},
+		OkCodes:     []int{200, 203},
+		MoreHeaders: map[string]string{"X-Auth-Token": ""},
 	})
 	return
 }
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index 12930f9..856c363 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -285,7 +285,9 @@
 		r.Err = err
 		return
 	}
-	resp, err := c.Post(tokenURL(c), b, &r.Body, nil)
+	resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{
+		MoreHeaders: map[string]string{"X-Auth-Token": ""},
+	})
 	if resp != nil {
 		r.Err = err
 		r.Header = resp.Header
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/results.go b/openstack/networking/v2/extensions/lbaas/monitors/results.go
index 11ba7df..0385942 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/results.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -24,6 +24,9 @@
 	// 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"`
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests.go b/openstack/networking/v2/extensions/lbaas/pools/requests.go
index 043945b..2a75737 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/requests.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests.go
@@ -82,6 +82,9 @@
 	// 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
diff --git a/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go
index a42d1aa..de038cb 100644
--- a/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas/pools/testing/requests_test.go
@@ -113,7 +113,8 @@
         "protocol": "HTTP",
         "name": "Example pool",
         "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
-        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f"
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "provider": "haproxy"
     }
 }
 			`)
@@ -137,7 +138,8 @@
         "admin_state_up": true,
         "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
         "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
-        "health_monitors_status": []
+        "health_monitors_status": [],
+        "provider": "haproxy"
     }
 }
 		`)
@@ -149,6 +151,7 @@
 		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)
@@ -163,6 +166,7 @@
 	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) {
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..7603f83
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/doc.go
@@ -0,0 +1 @@
+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..ce493c9
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
@@ -0,0 +1,171 @@
+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"`
+	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..168e531
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
@@ -0,0 +1,123 @@
+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 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..7603f83
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/doc.go
@@ -0,0 +1 @@
+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..f882949
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/fixtures.go
@@ -0,0 +1,277 @@
+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",
+			"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",
+			"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",
+		"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",
+		"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",
+		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",
+		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",
+		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..7603f83
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/testing/doc.go
@@ -0,0 +1 @@
+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..7603f83
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/testing/doc.go
@@ -0,0 +1 @@
+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..7603f83
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/testing/doc.go
@@ -0,0 +1 @@
+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..3231f66
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsbinding/testing/fixtures.go
@@ -0,0 +1,206 @@
+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"
+		}
+}
+			`)
+
+		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/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index 99ad9a7..0ab5e17 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -144,6 +144,7 @@
 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"`
@@ -380,7 +381,7 @@
 
 // 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 := c.AuthenticatedHeaders()
+	h := make(map[string]string)
 	if opts != nil {
 		headers, err := opts.ToObjectUpdateMap()
 		if err != nil {
diff --git a/openstack/objectstorage/v1/objects/testing/fixtures.go b/openstack/objectstorage/v1/objects/testing/fixtures.go
index 5077c3f..cf1d6c5 100644
--- a/openstack/objectstorage/v1/objects/testing/fixtures.go
+++ b/openstack/objectstorage/v1/objects/testing/fixtures.go
@@ -124,6 +124,24 @@
 	})
 }
 
+// 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.
diff --git a/openstack/objectstorage/v1/objects/testing/requests_test.go b/openstack/objectstorage/v1/objects/testing/requests_test.go
index 332028e..2b9306f 100644
--- a/openstack/objectstorage/v1/objects/testing/requests_test.go
+++ b/openstack/objectstorage/v1/objects/testing/requests_test.go
@@ -95,6 +95,22 @@
 	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()
diff --git a/pagination/pager.go b/pagination/pager.go
index 1d3e907..1b5192a 100644
--- a/pagination/pager.go
+++ b/pagination/pager.go
@@ -138,6 +138,11 @@
 	// 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 testPage.GetBody().(type) {
@@ -153,7 +158,14 @@
 					key = k
 				}
 			}
-			pagesSlice = append(pagesSlice, b[key].([]interface{})...)
+			switch keyType := b[key].(type) {
+			case map[string]interface{}:
+				pagesSlice = append(pagesSlice, keyType)
+			case []interface{}:
+				pagesSlice = append(pagesSlice, b[key].([]interface{})...)
+			default:
+				return false, fmt.Errorf("Unsupported page body type: %+v", keyType)
+			}
 			return true, nil
 		})
 		if err != nil {
diff --git a/results.go b/results.go
index 2b7e01f..8cca421 100644
--- a/results.go
+++ b/results.go
@@ -147,7 +147,7 @@
 	return nil
 }
 
-const RFC3339MilliNoZ = "2006-01-02T03:04:05.999999"
+const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999"
 
 type JSONRFC3339MilliNoZ time.Time
 
diff --git a/script/bootstrap b/script/bootstrap
index 6bae6e8..78a195d 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -8,8 +8,8 @@
 mkdir -p $GOPATH
 
 # Download gophercloud into that environment
-go get github.com/rackspace/gophercloud
-cd $GOPATH/src/github.com/rackspace/gophercloud
+go get github.com/gophercloud/gophercloud
+cd $GOPATH/src/github.com/gophercloud/gophercloud
 git checkout master
 
 # Write out the env.sh convenience file.
@@ -17,10 +17,9 @@
 cat <<EOF >env.sh
 #!/bin/bash
 export GOPATH=$(pwd)
-export GOPHERCLOUD=$GOPATH/src/github.com/rackspace/gophercloud
+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/unittest b/script/unittest
index d3440a9..2c65d06 100755
--- a/script/unittest
+++ b/script/unittest
@@ -2,4 +2,4 @@
 #
 # Run the unit tests.
 
-exec go test -tags fixtures ./... $@
+exec go test ./testing ./.../testing $@
diff --git a/testing/doc.go b/testing/doc.go
new file mode 100644
index 0000000..7603f83
--- /dev/null
+++ b/testing/doc.go
@@ -0,0 +1 @@
+package testing