Adding Support for LBaaS v2 - Pools and Members
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..d61e4ae
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
@@ -0,0 +1,359 @@
+package pools
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the 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"`
+	SubnetID       string `q:"subnet_id"`
+	TenantID       string `q:"tenant_id"`
+	AdminStateUp   *bool  `q:"admin_state_up"`
+	Name           string `q:"name"`
+	ID             string `q:"id"`
+	LoadbalancerID string `q:"loadbalancer_id"`
+	Limit          int    `q:"limit"`
+	Marker         string `q:"marker"`
+	SortKey        string `q:"sort_key"`
+	SortDir        string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// pools. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those pools that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return PoolPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Supported attributes for create/update operations.
+const (
+	LBMethodRoundRobin       = "ROUND_ROBIN"
+	LBMethodLeastConnections = "LEAST_CONNECTIONS"
+	LBMethodSourceIp         = "SOURCE_IP"
+
+	ProtocolTCP   = "TCP"
+	ProtocolHTTP  = "HTTP"
+	ProtocolHTTPS = "HTTPS"
+)
+
+// CreateOpts contains all the values needed to create a new pool.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a pool
+	// for another tenant.
+	TenantID string
+
+	// Optional. 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
+
+	// Optional. Name of the pool.
+	Name string
+
+	// Optional. Human-readable description for the pool.
+	Description string
+
+	// Required. The protocol used by the pool members, you can use either
+	// ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+	Protocol string
+
+	// The Loadbalancer on which the members of the pool will be associated with.
+	// Note:  one of LoadbalancerID or ListenerID must be provided.
+	LoadbalancerID string
+
+	// The Listener on which the members of the pool will be associated with.
+	// Note:  one of LoadbalancerID or ListenerID must be provided.
+	ListenerID string
+
+	// 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 string
+
+	// Optional. Omit this field to prevent session persistence.
+	Persistence *SessionPersistence
+
+	// Optional. The administrative state of the Listener. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+	type pool struct {
+		Name           string              `json:"name,omitempty"`
+		Description    string              `json:"description,omitempty"`
+		TenantID       string              `json:"tenant_id,omitempty"`
+		SubnetID       string              `json:"subnet_id,omitempty"`
+		Protocol       string              `json:"protocol"`
+		LoadbalancerID string              `json:"loadbalancer_id,omitempty"`
+		ListenerID     string              `json:"listener_id,omitempty"`
+		LBMethod       string              `json:"lb_algorithm"`
+		Persistence    *SessionPersistence `json:"session_persistence,omitempty"`
+		AdminStateUp   *bool               `json:"admin_state_up,omitempty"`
+	}
+
+	type request struct {
+		Pool pool `json:"pool"`
+	}
+
+	reqBody := request{Pool: pool{
+		Name:           opts.Name,
+		Description:    opts.Description,
+		TenantID:       opts.TenantID,
+		SubnetID:       opts.SubnetID,
+		Protocol:       opts.Protocol,
+		LoadbalancerID: opts.LoadbalancerID,
+		ListenerID:     opts.ListenerID,
+		LBMethod:       opts.LBMethod,
+		AdminStateUp:   opts.AdminStateUp,
+	}}
+
+	if opts.Persistence != nil {
+		reqBody.Pool.Persistence = opts.Persistence
+	}
+
+	var res CreateResult
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular pool based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+	return res
+}
+
+// UpdateOpts contains the values used when updating a pool.
+type UpdateOpts struct {
+	// Optional. Name of the pool.
+	Name string
+
+	// Optional. Human-readable description for the pool.
+	Description string
+
+	// 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 string
+
+	// Optional. The administrative state of the Listener. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type pool struct {
+		Name         string `json:"name,omitempty"`
+		Description  string `json:"description,omitempty"`
+		LBMethod     string `json:"lb_algorithm,omitempty"`
+		AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	}
+	type request struct {
+		Pool pool `json:"pool"`
+	}
+
+	reqBody := request{Pool: pool{
+		Name:         opts.Name,
+		Description:  opts.Description,
+		LBMethod:     opts.LBMethod,
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular pool based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Delete(resourceURL(c, id), nil)
+	return res
+}
+
+// MemberListOpts 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 MemberListOpts struct {
+	Name         string `q:"name"`
+	Weight       int    `q:"weight"`
+	AdminStateUp *bool  `q:"admin_state_up"`
+	TenantID     string `q:"tenant_id"`
+	SubnetID     string `q:"subnet_id"`
+	Address      string `q:"address"`
+	ProtocolPort int    `q:"protocol_port"`
+	ID           string `q:"id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// members. It accepts a ListOpts struct, 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 ListAssociateMembers(c *gophercloud.ServiceClient, poolID string, opts MemberListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := memberRootURL(c, poolID) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return MemberPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOpts contains all the values needed to create a new Member for a Pool.
+type MemberCreateOpts struct {
+	// Optional. Name of the Member.
+	Name string
+
+	// Only required if the caller has an admin role and wants to create a Member
+	// for another tenant.
+	TenantID string
+
+	// Required. The IP address of the member to receive traffic from the load balancer.
+	Address string
+
+	// Required. The port on which to listen for client traffic.
+	ProtocolPort int
+
+	// 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
+
+	// Optional.  If you omit this parameter, LBaaS uses the vip_subnet_id
+	// parameter value for the subnet UUID.
+	SubnetID string
+
+	// Optional. The administrative state of the Listener. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool
+}
+
+// CreateAssociateMember will create and associate a Member with a particular Pool.
+func CreateAssociateMember(c *gophercloud.ServiceClient, poolID string, opts MemberCreateOpts) AssociateResult {
+	type member struct {
+		Name         string `json:"name,omitempty"`
+		TenantID     string `json:"tenant_id,omitempty"`
+		Address      string `json:"address,omitempty"`
+		ProtocolPort int    `json:"protocol_port,omitempty"`
+		Weight       int    `json:"weight,omitempty"`
+		SubnetID     string `json:"subnet_id,omitempty"`
+		AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	}
+	type request struct {
+		Member member `json:"member"`
+	}
+
+	reqBody := request{Member: member{
+		Name:         opts.Name,
+		TenantID:     opts.TenantID,
+		Address:      opts.Address,
+		ProtocolPort: opts.ProtocolPort,
+		Weight:       opts.Weight,
+		SubnetID:     opts.SubnetID,
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	var res AssociateResult
+	_, res.Err = c.Post(memberRootURL(c, poolID), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular Pool Member based on its unique ID.
+func GetAssociateMember(c *gophercloud.ServiceClient, poolID string, memberID string) GetResult {
+	var res GetResult
+	_, res.Err = c.Get(memberResourceURL(c, poolID, memberID), &res.Body, nil)
+	return res
+}
+
+// UpdateOpts contains the values used when updating a Pool Member.
+type MemberUpdateOpts struct {
+	// Name of the Member.
+	Name string
+
+	// 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
+
+	// The administrative state of the member, which is up (true) or down (false).
+	AdminStateUp *bool
+}
+
+// Update allows Member to be updated.
+func UpdateAssociateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts MemberUpdateOpts) UpdateResult {
+	type member struct {
+		Name         string `json:"name,omitempty"`
+		Weight       int    `json:"weight,omitempty"`
+		AdminStateUp *bool  `json:"admin_state_up,omitempty"`
+	}
+	type request struct {
+		Member member `json:"member"`
+	}
+
+	reqBody := request{Member: member{
+		Name:         opts.Name,
+		Weight:       opts.Weight,
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	// Send request to API
+	var res UpdateResult
+	_, res.Err = c.Put(memberResourceURL(c, poolID, memberID), reqBody, &res.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201, 202},
+	})
+	return res
+}
+
+// DisassociateMember will remove and disassociate a Member from a particular Pool.
+func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) AssociateResult {
+	var res AssociateResult
+	_, res.Err = c.Delete(memberResourceURL(c, poolID, memberID), nil)
+	return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/pools/requests_test.go
new file mode 100644
index 0000000..5ee8762
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/requests_test.go
@@ -0,0 +1,494 @@
+package pools
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/pools", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "pools":[
+      {
+         "lb_algorithm":"ROUND_ROBIN",
+         "protocol":"HTTP",
+         "description":"",
+         "health_monitors":[
+            "466c8345-28d8-4f84-a246-e04380b0461d",
+            "5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+         ],
+         "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":"app_pool",
+         "admin_state_up":true,
+         "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+         "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+         "provider": "haproxy"
+      }
+   ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPools(page)
+		if err != nil {
+			t.Errorf("Failed to extract pools: %v", err)
+			return false, err
+		}
+
+		expected := []Pool{
+			{
+				LBMethod:    "ROUND_ROBIN",
+				Protocol:    "HTTP",
+				Description: "",
+				MonitorIDs: []string{
+					"466c8345-28d8-4f84-a246-e04380b0461d",
+					"5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+				},
+				SubnetID:      "8032909d-47a1-4715-90af-5153ffe39861",
+				TenantID:      "83657cfcdfe44cd5920adaf26c48ceea",
+				AdminStateUp:  true,
+				Name:          "app_pool",
+				Members:       []map[string]interface{}{{"id": "53306cda-815d-4354-9fe4-59e09da9c3c5"}},
+				ID:            "72741b06-df4d-4715-b142-276b6bce75ab",
+				Loadbalancers: []map[string]interface{}{{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+				Listeners:     []map[string]interface{}{{"id": "2a280670-c202-4b0b-a562-34077415aabf"}},
+				Provider:      "haproxy",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "pool": {
+        "lb_algorithm": "ROUND_ROBIN",
+        "protocol": "HTTP",
+        "name": "Example pool",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "pool": {
+        "lb_algorithm": "ROUND_ROBIN",
+        "protocol": "HTTP",
+        "description": "",
+        "health_monitors": [],
+        "members": [{}],
+        "id": "69055154-f603-4a28-8951-7cc2d9e54a9a",
+        "name": "Example pool",
+        "admin_state_up": true,
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+        "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}]
+    }
+}
+		`)
+	})
+
+	options := CreateOpts{
+		LBMethod:       LBMethodRoundRobin,
+		Protocol:       "HTTP",
+		Name:           "Example pool",
+		SubnetID:       "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID:       "2ffc6e22aae24e4795f87155d24c896f",
+		LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+	}
+	p, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod)
+	th.AssertEquals(t, "HTTP", p.Protocol)
+	th.AssertEquals(t, "", p.Description)
+	th.AssertDeepEquals(t, []string{}, p.MonitorIDs)
+	th.AssertDeepEquals(t, []map[string]interface{}{{}}, p.Members)
+	th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID)
+	th.AssertEquals(t, "79e05663-7f03-45d2-a092-8b94062f22ab", p.Loadbalancers[0]["id"])
+	th.AssertEquals(t, "Example pool", p.Name)
+	th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID)
+	th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "pool":{
+      "id":"332abe93-f488-41ba-870b-2ac66be7f853",
+      "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995",
+      "name":"Example pool",
+      "description":"",
+      "protocol":"tcp",
+      "lb_algorithm":"ROUND_ROBIN",
+      "session_persistence":{
+      },
+      "members":[{}],
+      "admin_state_up":true
+   }
+}
+			`)
+	})
+
+	n, err := Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853")
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "pool":{
+      "name": "SuperPool",
+      "lb_algorithm": "LEAST_CONNECTIONS"
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "pool":{
+      "lb_algorithm":"LEAST_CONNECTIONS",
+      "protocol":"TCP",
+      "description":"",
+      "health_monitors":[],
+      "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+      "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+      "admin_state_up":true,
+      "name":"SuperPool",
+      "members":[{}],
+      "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f"
+   }
+}
+		`)
+	})
+
+	options := UpdateOpts{Name: "SuperPool", LBMethod: LBMethodLeastConnections}
+
+	n, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "SuperPool", n.Name)
+	th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestListAssociateMembers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "members":[
+      {
+        "id": "2a280670-c202-4b0b-a562-34077415aabf",
+        "address": "10.0.2.10",
+        "weight": 5,
+        "name": "member1",
+        "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": "member2",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "admin_state_up":false,
+        "protocol_port": 80
+      }
+   ]
+}
+			`)
+	})
+
+	count := 0
+
+	ListAssociateMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", MemberListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractMembers(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Member{
+			{
+				SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+				TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+				AdminStateUp: true,
+				Name:         "member1",
+				ID:           "2a280670-c202-4b0b-a562-34077415aabf",
+				Address:      "10.0.2.10",
+				Weight:       5,
+				ProtocolPort: 80,
+			},
+			{
+				SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+				TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+				AdminStateUp: false,
+				Name:         "member2",
+				ID:           "fad389a3-9a4a-4762-a365-8c7038508b5d",
+				Address:      "10.0.2.11",
+				Weight:       10,
+				ProtocolPort: 80,
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreateAssociateMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "member": {
+        "address": "10.0.2.10",
+        "weight": 5,
+        "name": "Example member",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "protocol_port": 80
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "member": {
+        "id": "2a280670-c202-4b0b-a562-34077415aabf",
+        "address": "10.0.2.10",
+        "weight": 5,
+        "name": "Example member",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "admin_state_up":true,
+        "protocol_port": 80
+    }
+}
+		`)
+	})
+
+	options := MemberCreateOpts{
+		Name:         "Example member",
+		SubnetID:     "1981f108-3c48-48d2-b908-30f7d28532c9",
+		TenantID:     "2ffc6e22aae24e4795f87155d24c896f",
+		Address:      "10.0.2.10",
+		ProtocolPort: 80,
+		Weight:       5,
+	}
+	p, err := CreateAssociateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).ExtractMember()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "2a280670-c202-4b0b-a562-34077415aabf", p.ID)
+	th.AssertEquals(t, "Example member", p.Name)
+	th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID)
+	th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID)
+}
+
+func TestGetAssociateMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "member": {
+        "id": "2a280670-c202-4b0b-a562-34077415aabf",
+        "address": "10.0.2.10",
+        "weight": 5,
+        "name": "Example member",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "admin_state_up":true,
+        "protocol_port": 80
+    }
+}
+			`)
+	})
+
+	n, err := GetAssociateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf").ExtractMember()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.ID, "2a280670-c202-4b0b-a562-34077415aabf")
+}
+
+func TestUpdateAssociateMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+   "member":{
+      "name": "newMemberName",
+      "weight": 4
+   }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "member": {
+        "id": "2a280670-c202-4b0b-a562-34077415aabf",
+        "address": "10.0.2.10",
+        "weight": 4,
+        "name": "newMemberName",
+        "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+        "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+        "admin_state_up":true,
+        "protocol_port": 80
+    }
+}
+		`)
+	})
+
+	options := MemberUpdateOpts{Name: "newMemberName", Weight: 4}
+
+	n, err := UpdateAssociateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf", options).ExtractMember()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "newMemberName", n.Name)
+	th.AssertDeepEquals(t, 4, n.Weight)
+}
+
+func TestDeleteMember(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := DeleteMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf")
+	th.AssertNoErr(t, res.Err)
+}
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..040be79
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/results.go
@@ -0,0 +1,268 @@
+package pools
+
+import (
+	"github.com/davecgh/go-spew/spew"
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SessionPersistence represents the session persistence feature of the load
+// balancing service. It attempts to force connections or requests in the same
+// session to be processed by the same member as long as it is ative. Three
+// types of persistence are supported:
+//
+// SOURCE_IP:   With this mode, all connections originating from the same source
+//              IP address, will be handled by the same Member of the Pool.
+// HTTP_COOKIE: With this persistence mode, the load balancing function will
+//              create a cookie on the first request from a client. Subsequent
+//              requests containing the same cookie value will be handled by
+//              the same Member of the Pool.
+// APP_COOKIE:  With this persistence mode, the load balancing function will
+//              rely on a cookie established by the backend application. All
+//              requests carrying the same cookie value will be handled by the
+//              same Member of the Pool.
+type SessionPersistence struct {
+	// The type of persistence mode
+	Type string `mapstructure:"type" json:"type"`
+
+	// Name of cookie if persistence mode is set appropriately
+	CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"`
+}
+
+// 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" mapstructure:"lb_algorithm"`
+
+	// The protocol of the Pool, which is TCP, HTTP, or HTTPS.
+	Protocol string
+
+	// Description for the Pool.
+	Description string
+
+	// A list of listeners objects IDs.
+	Listeners []map[string]interface{} `mapstructure:"listeners" json:"listeners"`
+
+	// A list of member objects IDs.
+	Members []map[string]interface{} `mapstructure:"members" json:"members"`
+
+	// The IDs of associated monitors which check the health of the Pool.
+	MonitorIDs []string `json:"health_monitors" mapstructure:"health_monitors"`
+
+	// The network on which the members of the Pool will be located. Only members
+	// that are on this network can be added to the Pool.
+	SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+	// Owner of the Pool. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+	// The administrative state of the Pool, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+	// Pool name. Does not have to be unique.
+	Name string
+
+	// List of member IDs that belong to the Pool.
+	// MemberIDs []string `json:"members" mapstructure:"members"`
+
+	// The unique ID for the Pool.
+	ID string
+
+	// A list of load balancer objects IDs.
+	Loadbalancers []map[string]interface{} `mapstructure:"loadbalancers" json:"loadbalancers"`
+
+	// Indicates whether connections in the same session will be processed by the
+	// same Pool member or not.
+	Persistence SessionPersistence `mapstructure:"session_persistence" json:"session_persistence"`
+
+	// The provider
+	Provider string
+}
+
+// PoolPage is the page returned by a pager when traversing over a
+// collection of pools.
+type PoolPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of pools has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p PoolPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"pools_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p PoolPage) IsEmpty() (bool, error) {
+	is, err := ExtractPools(p)
+	spew.Dump(err)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractPools accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPools(page pagination.Page) ([]Pool, error) {
+	var resp struct {
+		Pools []Pool `mapstructure:"pools" json:"pools"`
+	}
+
+	err := mapstructure.Decode(page.(PoolPage).Body, &resp)
+
+	return resp.Pools, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Pool, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Pool *Pool `json:"pool"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Pool, err
+}
+
+// Member represents the application running on a backend server.
+type Member struct {
+	// Name of the Member.
+	Name string `json:"name" mapstructure:"name"`
+
+	// Weight of Member.
+	Weight int `json:"weight" mapstructure:"weight"`
+
+	// The administrative state of the member, which is up (true) or down (false).
+	AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+	// Owner of the Member. Only an administrative user can specify a tenant ID
+	// other than its own.
+	TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+	// parameter value for the subnet UUID.
+	SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+	// The Pool to which the Member belongs.
+	PoolID string `json:"pool_id" mapstructure:"pool_id"`
+
+	// The IP address of the Member.
+	Address string
+
+	// The port on which the application is hosted.
+	ProtocolPort int `json:"protocol_port" mapstructure:"protocol_port"`
+
+	// The unique ID for the Member.
+	ID string
+}
+
+// MemberPage is the page returned by a pager when traversing over a
+// collection of 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 (p MemberPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"members_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a MemberPage struct is empty.
+func (p MemberPage) IsEmpty() (bool, error) {
+	is, err := ExtractMembers(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractMembers accepts a Page struct, specifically a 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(page pagination.Page) ([]Member, error) {
+	var resp struct {
+		Member []Member `mapstructure:"members" json:"members"`
+	}
+
+	err := mapstructure.Decode(page.(MemberPage).Body, &resp)
+
+	return resp.Member, err
+}
+
+// ExtractMember is a function that accepts a result and extracts a router.
+func (r commonResult) ExtractMember() (*Member, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Member *Member `json:"member"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Member, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// AssociateResult represents the result of an association operation.
+type AssociateResult struct {
+	commonResult
+}
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..0cdbc23
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/urls.go
@@ -0,0 +1,25 @@
+package pools
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "lbaas"
+	resourcePath = "pools"
+	memeberPath  = "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, memeberPath)
+}
+
+func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memeberID string) string {
+	return c.ServiceURL(rootPath, resourcePath, poolID, memeberPath, memeberID)
+}