merge lbaasv2, portsbinding, volumes v2; remove 'rackspace' refs; update docs
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/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..ed047d5
--- /dev/null
+++ b/openstack/blockstorage/v2/extensions/volumeactions/testing/requests_test.go
@@ -0,0 +1,91 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/extensions/volumeactions"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+	"github.com/jrperritt/gophercloud"
+)
+
+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/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/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..8042792
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/testing/requests_test.go
@@ -0,0 +1,137 @@
+package testing
+
+import (
+	"testing"
+
+	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"
+	"github.com/jrperritt/gophercloud"
+)
+
+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/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..52da652
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go
@@ -0,0 +1,144 @@
+package testing
+
+import (
+	"testing"
+
+	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"
+	"github.com/jrperritt/gophercloud"
+)
+
+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/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/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/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()