Merge pull request #575 from hpcloud/lbaas-v2
[rfr] Adding LBaaS v2 Support
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancer_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancer_test.go
new file mode 100644
index 0000000..7cc84ab
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancer_test.go
@@ -0,0 +1,493 @@
+// +build acceptance networking lbaas_v2 lbaasloadbalancer
+
+package lbaas_v2
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// Note: when creating a new Loadbalancer (VM), it can take some time before it is ready for use,
+// this timeout is used for waiting until the Loadbalancer provisioning status goes to ACTIVE state.
+const loadbalancerActiveTimeoutSeconds = 120
+const loadbalancerDeleteTimeoutSeconds = 10
+
+func setupTopology(t *testing.T) (string, string) {
+ // create network
+ n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created network, ID %s", n.ID)
+
+ // create subnet
+ s, err := subnets.Create(base.Client, subnets.CreateOpts{
+ NetworkID: n.ID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: subnets.IPv4,
+ Name: "tmp_subnet",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created subnet, ID %s", s.ID)
+
+ return n.ID, s.ID
+}
+
+func deleteTopology(t *testing.T, networkID string) {
+ res := networks.Delete(base.Client, networkID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("deleted network, ID %s", networkID)
+}
+
+func TestLoadbalancers(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup network topology
+ networkID, subnetID := setupTopology(t)
+
+ // create Loadbalancer
+ loadbalancerID := createLoadbalancer(t, subnetID)
+
+ // list Loadbalancers
+ listLoadbalancers(t)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // update Loadbalancer
+ updateLoadbalancer(t, loadbalancerID)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // create listener
+ listenerID := createListener(t, listeners.ProtocolHTTP, 80, loadbalancerID)
+
+ // list listeners
+ listListeners(t)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // update listener
+ updateListener(t, listenerID)
+
+ // get listener
+ getListener(t, listenerID)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // create pool
+ poolID := createPool(t, pools.ProtocolHTTP, listenerID, pools.LBMethodRoundRobin)
+
+ // list pools
+ listPools(t)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // update pool
+ updatePool(t, poolID)
+
+ // get pool
+ getPool(t, poolID)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // create member
+ memberID := createMember(t, subnetID, poolID, "1.2.3.4", 80, 5)
+
+ // list members
+ listMembers(t, poolID)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // update member
+ updateMember(t, poolID, memberID)
+
+ // get member
+ getMember(t, poolID, memberID)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // create monitor
+ monitorID := createMonitor(t, poolID, monitors.TypePING, 10, 10, 3)
+
+ // list monitors
+ listMonitors(t)
+
+ // get Loadbalancer and wait until ACTIVE
+ getLoadbalancerWaitActive(t, loadbalancerID)
+
+ // update monitor
+ updateMonitor(t, monitorID)
+
+ // get monitor
+ getMonitor(t, monitorID)
+
+ // get loadbalancer statuses tree
+ rawStatusTree, err := loadbalancers.GetStatuses(base.Client, loadbalancerID).ExtractStatuses()
+ if err == nil {
+ // verify statuses tree ID's of relevant objects
+ if rawStatusTree.Loadbalancer.ID != loadbalancerID {
+ t.Errorf("Loadbalancer ID did not match")
+ }
+ if rawStatusTree.Loadbalancer.Listeners[0].ID != listenerID {
+ t.Errorf("Listner ID did not match")
+ }
+ if rawStatusTree.Loadbalancer.Listeners[0].Pools[0].ID != poolID {
+ t.Errorf("Pool ID did not match")
+ }
+ if rawStatusTree.Loadbalancer.Listeners[0].Pools[0].Members[0].ID != memberID {
+ t.Errorf("Member ID did not match")
+ }
+ if rawStatusTree.Loadbalancer.Listeners[0].Pools[0].Monitor.ID != monitorID {
+ t.Errorf("Monitor ID did not match")
+ }
+ } else {
+ t.Errorf("Failed to extract Loadbalancer statuses tree: %v", err)
+ }
+
+ getLoadbalancerWaitActive(t, loadbalancerID)
+ deleteMonitor(t, monitorID)
+ getLoadbalancerWaitActive(t, loadbalancerID)
+ deleteMember(t, poolID, memberID)
+ getLoadbalancerWaitActive(t, loadbalancerID)
+ deletePool(t, poolID)
+ getLoadbalancerWaitActive(t, loadbalancerID)
+ deleteListener(t, listenerID)
+ getLoadbalancerWaitActive(t, loadbalancerID)
+ deleteLoadbalancer(t, loadbalancerID)
+ getLoadbalancerWaitDeleted(t, loadbalancerID)
+ deleteTopology(t, networkID)
+}
+
+func createLoadbalancer(t *testing.T, subnetID string) string {
+ lb, err := loadbalancers.Create(base.Client, loadbalancers.CreateOpts{
+ VipSubnetID: subnetID,
+ Name: "tmp_loadbalancer",
+ AdminStateUp: loadbalancers.Up,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+ t.Logf("Created Loadbalancer, ID %s", lb.ID)
+
+ return lb.ID
+}
+
+func deleteLoadbalancer(t *testing.T, loadbalancerID string) {
+ res := loadbalancers.Delete(base.Client, loadbalancerID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("deleted Loadbalancer, ID %s", loadbalancerID)
+}
+
+func listLoadbalancers(t *testing.T) {
+ err := loadbalancers.List(base.Client, loadbalancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ loadbalancerList, err := loadbalancers.ExtractLoadbalancers(page)
+ if err != nil {
+ t.Errorf("Failed to extract Loadbalancers: %v", err)
+ return false, err
+ }
+
+ for _, loadbalancer := range loadbalancerList {
+ t.Logf("Listing Loadbalancer: ID [%s] Name [%s] Address [%s]",
+ loadbalancer.ID, loadbalancer.Name, loadbalancer.VipAddress)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getLoadbalancerWaitDeleted(t *testing.T, loadbalancerID string) {
+ start := time.Now().Second()
+ for {
+ time.Sleep(1 * time.Second)
+
+ if time.Now().Second()-start >= loadbalancerDeleteTimeoutSeconds {
+ t.Errorf("Loadbalancer failed to delete")
+ return
+ }
+
+ _, err := loadbalancers.Get(base.Client, loadbalancerID).Extract()
+ if err != nil {
+ if errData, ok := err.(*(gophercloud.UnexpectedResponseCodeError)); ok {
+ if errData.Actual == 404 {
+ return
+ }
+ } else {
+ th.AssertNoErr(t, err)
+ }
+ }
+ }
+}
+
+func getLoadbalancerWaitActive(t *testing.T, loadbalancerID string) {
+ start := time.Now().Second()
+ for {
+ time.Sleep(1 * time.Second)
+
+ if time.Now().Second()-start >= loadbalancerActiveTimeoutSeconds {
+ t.Errorf("Loadbalancer failed to go into ACTIVE provisioning status")
+ return
+ }
+
+ loadbalancer, err := loadbalancers.Get(base.Client, loadbalancerID).Extract()
+ th.AssertNoErr(t, err)
+ if loadbalancer.ProvisioningStatus == "ACTIVE" {
+ t.Logf("Retrieved Loadbalancer, ID [%s]: OperatingStatus [%s]", loadbalancer.ID, loadbalancer.ProvisioningStatus)
+ return
+ }
+ }
+}
+
+func updateLoadbalancer(t *testing.T, loadbalancerID string) {
+ _, err := loadbalancers.Update(base.Client, loadbalancerID, loadbalancers.UpdateOpts{Name: "tmp_newName"}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated Loadbalancer ID [%s]", loadbalancerID)
+}
+
+func listListeners(t *testing.T) {
+ err := listeners.List(base.Client, listeners.ListOpts{Name: "tmp_listener"}).EachPage(func(page pagination.Page) (bool, error) {
+ listenerList, err := listeners.ExtractListeners(page)
+ if err != nil {
+ t.Errorf("Failed to extract Listeners: %v", err)
+ return false, err
+ }
+
+ for _, listener := range listenerList {
+ t.Logf("Listing Listener: ID [%s] Name [%s] Loadbalancers [%s]",
+ listener.ID, listener.Name, listener.Loadbalancers)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func createListener(t *testing.T, protocol listeners.Protocol, protocolPort int, loadbalancerID string) string {
+ l, err := listeners.Create(base.Client, listeners.CreateOpts{
+ Protocol: protocol,
+ ProtocolPort: protocolPort,
+ LoadbalancerID: loadbalancerID,
+ Name: "tmp_listener",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+ t.Logf("Created Listener, ID %s", l.ID)
+
+ return l.ID
+}
+
+func deleteListener(t *testing.T, listenerID string) {
+ res := listeners.Delete(base.Client, listenerID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted Loadbalancer, ID %s", listenerID)
+}
+
+func getListener(t *testing.T, listenerID string) {
+ listener, err := listeners.Get(base.Client, listenerID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting Listener, ID [%s]: ", listener.ID)
+}
+
+func updateListener(t *testing.T, listenerID string) {
+ _, err := listeners.Update(base.Client, listenerID, listeners.UpdateOpts{Name: "tmp_newName"}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated Listener, ID [%s]", listenerID)
+}
+
+func listPools(t *testing.T) {
+ err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ poolsList, err := pools.ExtractPools(page)
+ if err != nil {
+ t.Errorf("Failed to extract Pools: %v", err)
+ return false, err
+ }
+
+ for _, pool := range poolsList {
+ t.Logf("Listing Pool: ID [%s] Name [%s] Listeners [%s] LBMethod [%s]",
+ pool.ID, pool.Name, pool.Listeners, pool.LBMethod)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func createPool(t *testing.T, protocol pools.Protocol, listenerID string, lbMethod pools.LBMethod) string {
+ p, err := pools.Create(base.Client, pools.CreateOpts{
+ LBMethod: lbMethod,
+ Protocol: protocol,
+ Name: "tmp_pool",
+ ListenerID: listenerID,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created Pool, ID %s", p.ID)
+
+ return p.ID
+}
+
+func deletePool(t *testing.T, poolID string) {
+ res := pools.Delete(base.Client, poolID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted Pool, ID %s", poolID)
+}
+
+func getPool(t *testing.T, poolID string) {
+ pool, err := pools.Get(base.Client, poolID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting Pool, ID [%s]: ", pool.ID)
+}
+
+func updatePool(t *testing.T, poolID string) {
+ _, err := pools.Update(base.Client, poolID, pools.UpdateOpts{Name: "tmp_newName"}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated Pool, ID [%s]", poolID)
+}
+
+func createMember(t *testing.T, subnetID string, poolID string, address string, protocolPort int, weight int) string {
+ m, err := pools.CreateAssociateMember(base.Client, poolID, pools.MemberCreateOpts{
+ SubnetID: subnetID,
+ Address: address,
+ ProtocolPort: protocolPort,
+ Weight: weight,
+ Name: "tmp_member",
+ }).ExtractMember()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created Member, ID %s", m.ID)
+
+ return m.ID
+}
+
+func deleteMember(t *testing.T, poolID string, memberID string) {
+ res := pools.DeleteMember(base.Client, poolID, memberID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted Member, ID %s", memberID)
+}
+
+func listMembers(t *testing.T, poolID string) {
+ err := pools.ListAssociateMembers(base.Client, poolID, pools.MemberListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ membersList, err := pools.ExtractMembers(page)
+ if err != nil {
+ t.Errorf("Failed to extract Members: %v", err)
+ return false, err
+ }
+
+ for _, member := range membersList {
+ t.Logf("Listing Member: ID [%s] Name [%s] Pool ID [%s] Weight [%s]",
+ member.ID, member.Name, member.PoolID, member.Weight)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getMember(t *testing.T, poolID string, memberID string) {
+ member, err := pools.GetAssociateMember(base.Client, poolID, memberID).ExtractMember()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting Member, ID [%s]: ", member.ID)
+}
+
+func updateMember(t *testing.T, poolID string, memberID string) {
+ _, err := pools.UpdateAssociateMember(base.Client, poolID, memberID, pools.MemberUpdateOpts{Name: "tmp_newName"}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated Member, ID [%s], in Pool, ID [%s]", memberID, poolID)
+}
+
+func createMonitor(t *testing.T, poolID string, checkType string, delay int, timeout int, maxRetries int) string {
+ m, err := monitors.Create(base.Client, monitors.CreateOpts{
+ PoolID: poolID,
+ Name: "tmp_monitor",
+ Delay: delay,
+ Timeout: timeout,
+ MaxRetries: maxRetries,
+ Type: checkType,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created Monitor, ID [%s]", m.ID)
+
+ return m.ID
+}
+
+func deleteMonitor(t *testing.T, monitorID string) {
+ res := monitors.Delete(base.Client, monitorID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted Monitor, ID %s", monitorID)
+}
+
+func listMonitors(t *testing.T) {
+ err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ monitorsList, err := monitors.ExtractMonitors(page)
+ if err != nil {
+ t.Errorf("Failed to extract Monitors: %v", err)
+ return false, err
+ }
+
+ for _, monitor := range monitorsList {
+ t.Logf("Listing Monitors: ID [%s] Type [%s] HTTPMethod [%s] URLPath [%s]",
+ monitor.ID, monitor.Type, monitor.HTTPMethod, monitor.URLPath)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getMonitor(t *testing.T, monitorID string) {
+ monitor, err := monitors.Get(base.Client, monitorID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting Monitor, ID [%s]: ", monitor.ID)
+}
+
+func updateMonitor(t *testing.T, monitorID string) {
+ _, err := monitors.Update(base.Client, monitorID, monitors.UpdateOpts{MaxRetries: 10}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated Monitor, ID [%s]", monitorID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go
new file mode 100644
index 0000000..24b7482
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/pkg.go
@@ -0,0 +1 @@
+package lbaas_v2
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/results.go b/openstack/networking/v2/extensions/lbaas/monitors/results.go
index d595abd..ac2801b 100644
--- a/openstack/networking/v2/extensions/lbaas/monitors/results.go
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -25,6 +25,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" mapstructure:"tenant_id"`
diff --git a/openstack/networking/v2/extensions/lbaas_v2/doc.go b/openstack/networking/v2/extensions/lbaas_v2/doc.go
new file mode 100644
index 0000000..247a75f
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/doc.go
@@ -0,0 +1,5 @@
+// Package lbaas_v2 provides information and interaction with the Load Balancer
+// as a Service v2 extension for the OpenStack Networking service.
+// lbaas v2 api docs: http://developer.openstack.org/api-ref-networking-v2-ext.html#lbaas-v2.0
+// lbaas v2 api schema: https://github.com/openstack/neutron-lbaas/blob/master/neutron_lbaas/extensions/loadbalancerv2.py
+package lbaas_v2
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/fixtures.go
new file mode 100644
index 0000000..0c06671
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/fixtures.go
@@ -0,0 +1,213 @@
+// +build fixtures
+package listeners
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/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 = Listener{
+ ID: "db902c0c-d5ff-4753-b465-668ad9656918",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "web",
+ Description: "listener config for the web tier",
+ Loadbalancers: []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 = Listener{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "db",
+ Description: "listener config for the db tier",
+ Loadbalancers: []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 = Listener{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "NewListenerName",
+ Description: "listener config for the db tier",
+ Loadbalancers: []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/requests.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
new file mode 100644
index 0000000..a62e0cd
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
@@ -0,0 +1,279 @@
+package listeners
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+type listenerOpts struct {
+ // Required. The protocol - can either be TCP, HTTP or HTTPS.
+ Protocol Protocol
+
+ // Required. The port on which to listen for client traffic.
+ ProtocolPort int
+
+ // Required for admins. Indicates the owner of the Listener.
+ TenantID string
+
+ // Required. The load balancer on which to provision this listener.
+ LoadbalancerID string
+
+ // Human-readable name for the Listener. Does not have to be unique.
+ Name string
+
+ // Optional. The ID of the default pool with which the Listener is associated.
+ DefaultPoolID string
+
+ // Optional. Human-readable description for the Listener.
+ Description string
+
+ // Optional. The maximum number of connections allowed for the Listener.
+ ConnLimit *int
+
+ // Optional. A reference to a container of TLS secrets.
+ DefaultTlsContainerRef string
+
+ // Optional. A list of references to TLS secrets.
+ SniContainerRefs []string
+
+ // Optional. The administrative state of the Listener. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ 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)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// 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}}
+ })
+}
+
+type Protocol string
+
+// Supported attributes for create/update operations.
+const (
+ ProtocolTCP Protocol = "TCP"
+ ProtocolHTTP Protocol = "HTTP"
+ ProtocolHTTPS Protocol = "HTTPS"
+)
+
+var (
+ errLoadbalancerIdRequired = fmt.Errorf("LoadbalancerID is required")
+ errProtocolRequired = fmt.Errorf("Protocol is required")
+ errProtocolPortRequired = fmt.Errorf("ProtocolPort is required")
+)
+
+// 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 listenerOpts
+
+// ToListenerCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.LoadbalancerID != "" {
+ l["loadbalancer_id"] = opts.LoadbalancerID
+ } else {
+ return nil, errLoadbalancerIdRequired
+ }
+ if opts.Protocol != "" {
+ l["protocol"] = opts.Protocol
+ } else {
+ return nil, errProtocolRequired
+ }
+ if opts.ProtocolPort != 0 {
+ l["protocol_port"] = opts.ProtocolPort
+ } else {
+ return nil, errProtocolPortRequired
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.TenantID != "" {
+ l["tenant_id"] = opts.TenantID
+ }
+ if opts.DefaultPoolID != "" {
+ l["default_pool_id"] = opts.DefaultPoolID
+ }
+ if opts.Description != "" {
+ l["description"] = opts.Description
+ }
+ if opts.ConnLimit != nil {
+ l["connection_limit"] = &opts.ConnLimit
+ }
+ if opts.DefaultTlsContainerRef != "" {
+ l["default_tls_container_ref"] = opts.DefaultTlsContainerRef
+ }
+ if opts.SniContainerRefs != nil {
+ l["sni_container_refs"] = opts.SniContainerRefs
+ }
+
+ return map[string]interface{}{"listener": l}, nil
+}
+
+// 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 CreateOpts) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToListenerCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+ return res
+}
+
+// Get retrieves a particular Listeners based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+ return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+ ToListenerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts listenerOpts
+
+// ToListenerUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.Description != "" {
+ l["description"] = opts.Description
+ }
+ if opts.ConnLimit != nil {
+ l["connection_limit"] = &opts.ConnLimit
+ }
+ if opts.DefaultTlsContainerRef != "" {
+ l["default_tls_container_ref"] = opts.DefaultTlsContainerRef
+ }
+ if opts.SniContainerRefs != nil {
+ l["sni_container_refs"] = opts.SniContainerRefs
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+
+ return map[string]interface{}{"listener": l}, nil
+}
+
+// Update is an operation which modifies the attributes of the specified Listener.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToListenerUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 202},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular Listeners based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = c.Delete(resourceURL(c, id), nil)
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests_test.go
new file mode 100644
index 0000000..4ce3114
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests_test.go
@@ -0,0 +1,142 @@
+package listeners
+
+import (
+ fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "testing"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/listeners", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/listeners/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestListListeners(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerListSuccessfully(t)
+
+ pages := 0
+ err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := 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 := List(fake.ServiceClient(), ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := 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 := Create(fake.ServiceClient(), CreateOpts{
+ Protocol: "TCP",
+ Name: "db",
+ LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+ AdminStateUp: Up,
+ 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 := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", TenantID: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), 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 := 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 := 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 := Update(client, "4ec89087-d057-4e2c-911f-60a3b47ee304", 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/results.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go
new file mode 100644
index 0000000..77a084a
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go
@@ -0,0 +1,139 @@
+package listeners
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type LoadBalancerID struct {
+ ID string `mapstructure:"id" 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 `mapstructure:"id" json:"id"`
+
+ // Owner of the Listener. Only an admin user can specify a tenant ID other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Human-readable name for the Listener. Does not have to be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // Human-readable description for the Listener.
+ Description string `mapstructure:"description" json:"description"`
+
+ // The protocol to loadbalance. A valid value is TCP, HTTP, or HTTPS.
+ Protocol string `mapstructure:"protocol" json:"protocol"`
+
+ // The port on which to listen to client traffic that is associated with the
+ // Loadbalancer. A valid value is from 0 to 65535.
+ ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"`
+
+ // The UUID of default pool. Must have compatible protocol with listener.
+ DefaultPoolID string `mapstructure:"default_pool_id" json:"default_pool_id"`
+
+ // A list of load balancer IDs.
+ Loadbalancers []LoadBalancerID `mapstructure:"loadbalancers" json:"loadbalancers"`
+
+ // The maximum number of connections allowed for the Loadbalancer. Default is -1,
+ // meaning no limit.
+ ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"`
+
+ // The list of references to TLS secrets.
+ SniContainerRefs []string `mapstructure:"sni_container_refs" json:"sni_container_refs"`
+
+ // Optional. A reference to a container of TLS secrets.
+ DefaultTlsContainerRef string `mapstructure:"default_tls_container_ref" json:"default_tls_container_ref"`
+
+ // The administrative state of the Listener. A valid value is true (UP) or false (DOWN).
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ Pools []pools.Pool `mapstructure:"pools" 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 (p ListenerPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"listeners_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p ListenerPage) IsEmpty() (bool, error) {
+ is, err := ExtractListeners(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// 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(page pagination.Page) ([]Listener, error) {
+ var resp struct {
+ Listeners []Listener `mapstructure:"listeners" json:"listeners"`
+ }
+ err := mapstructure.Decode(page.(ListenerPage).Body, &resp)
+ return resp.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) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Listener *Listener `mapstructure:"listener" json:"listener"`
+ }
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return res.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/urls.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go
new file mode 100644
index 0000000..b4cb90b
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/urls.go
@@ -0,0 +1,16 @@
+package listeners
+
+import "github.com/rackspace/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/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/fixtures.go
new file mode 100644
index 0000000..da0d21a
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/fixtures.go
@@ -0,0 +1,277 @@
+// +build fixtures
+package loadbalancers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+ "github.com/rackspace/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 = 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 = 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 = 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 = 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/requests.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
new file mode 100644
index 0000000..6607b8a
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
@@ -0,0 +1,248 @@
+package loadbalancers
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+type loadbalancerOpts struct {
+ // Optional. Human-readable name for the Loadbalancer. Does not have to be unique.
+ Name string
+
+ // Optional. Human-readable description for the Loadbalancer.
+ Description string
+
+ // 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
+
+ // 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
+
+ // Optional. The IP address of the Loadbalancer.
+ VipAddress string
+
+ // Optional. The administrative state of the Loadbalancer. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+
+ // Optional. The UUID of a flavor.
+ Flavor string
+
+ // Optional. The name of the provider.
+ Provider string
+}
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ 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)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// 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}}
+ })
+}
+
+var (
+ errVipSubnetIDRequried = fmt.Errorf("VipSubnetID is required")
+)
+
+// 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 loadbalancerOpts
+
+// ToLoadbalancerCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToLoadbalancerCreateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.VipSubnetID != "" {
+ l["vip_subnet_id"] = opts.VipSubnetID
+ } else {
+ return nil, errVipSubnetIDRequried
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.TenantID != "" {
+ l["tenant_id"] = opts.TenantID
+ }
+ if opts.Description != "" {
+ l["description"] = opts.Description
+ }
+ if opts.VipAddress != "" {
+ l["vip_address"] = opts.VipAddress
+ }
+ if opts.Flavor != "" {
+ l["flavor"] = opts.Flavor
+ }
+ if opts.Provider != "" {
+ l["provider"] = opts.Provider
+ }
+
+ return map[string]interface{}{"loadbalancer": l}, nil
+}
+
+// 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 CreateOpts) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToLoadbalancerCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+ return res
+}
+
+// Get retrieves a particular Loadbalancer based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+ return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+ ToLoadbalancerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts loadbalancerOpts
+
+// ToLoadbalancerUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToLoadbalancerUpdateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.Description != "" {
+ l["description"] = opts.Description
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+
+ return map[string]interface{}{"loadbalancer": l}, nil
+}
+
+// Update is an operation which modifies the attributes of the specified Loadbalancer.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToLoadbalancerUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 202},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular Loadbalancer based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = c.Delete(resourceURL(c, id), nil)
+ return res
+}
+
+func GetStatuses(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = c.Get(statusRootURL(c, id), &res.Body, nil)
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests_test.go
new file mode 100644
index 0000000..4bc1f1f
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests_test.go
@@ -0,0 +1,150 @@
+package loadbalancers
+
+import (
+ "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/loadbalancers", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/loadbalancers/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestListLoadbalancers(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerListSuccessfully(t)
+
+ pages := 0
+ err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := 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 := List(fake.ServiceClient(), ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := 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 := Create(fake.ServiceClient(), CreateOpts{
+ Name: "db_lb",
+ AdminStateUp: Up,
+ 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 := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Name: "foo", Description: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), 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 := 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 := GetStatuses(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").ExtractStatuses()
+ 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 := 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 := Update(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", 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/results.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
new file mode 100644
index 0000000..911c156
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
@@ -0,0 +1,146 @@
+package loadbalancers
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
+ "github.com/rackspace/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 `mapstructure:"description" json:"description"`
+
+ // The administrative state of the Loadbalancer. A valid value is true (UP) or false (DOWN).
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ // Owner of the LoadBalancer. Only an admin user can specify a tenant ID other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // The provisioning status of the LoadBalancer. This value is ACTIVE, PENDING_CREATE or ERROR.
+ ProvisioningStatus string `mapstructure:"provisioning_status" json:"provisioning_status"`
+
+ // The IP address of the Loadbalancer.
+ VipAddress string `mapstructure:"vip_address" json:"vip_address"`
+
+ // The UUID of the subnet on which to allocate the virtual IP for the Loadbalancer address.
+ VipSubnetID string `mapstructure:"vip_subnet_id" json:"vip_subnet_id"`
+
+ // The unique ID for the LoadBalancer.
+ ID string `mapstructure:"id" json:"id"`
+
+ // The operating status of the LoadBalancer. This value is ONLINE or OFFLINE.
+ OperatingStatus string `mapstructure:"operating_status" json:"operating_status"`
+
+ // Human-readable name for the LoadBalancer. Does not have to be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // The UUID of a flavor if set.
+ Flavor string `mapstructure:"flavor" json:"flavor"`
+
+ // The name of the provider.
+ Provider string `mapstructure:"provider" json:"provider"`
+
+ Listeners []listeners.Listener `mapstructure:"listeners" json:"listeners"`
+}
+
+type StatusTree struct {
+ Loadbalancer *LoadBalancer `mapstructure:"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 (p LoadbalancerPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"loadbalancers_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p LoadbalancerPage) IsEmpty() (bool, error) {
+ is, err := ExtractLoadbalancers(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// 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(page pagination.Page) ([]LoadBalancer, error) {
+ var resp struct {
+ LoadBalancers []LoadBalancer `mapstructure:"loadbalancers" json:"loadbalancers"`
+ }
+ err := mapstructure.Decode(page.(LoadbalancerPage).Body, &resp)
+
+ return resp.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) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ LoadBalancer *LoadBalancer `mapstructure:"loadbalancer" json:"loadbalancer"`
+ }
+ err := mapstructure.Decode(r.Body, &res)
+
+ return res.LoadBalancer, err
+}
+
+// Extract is a function that accepts a result and extracts a Loadbalancer.
+func (r commonResult) ExtractStatuses() (*StatusTree, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ LoadBalancer *StatusTree `mapstructure:"statuses" json:"statuses"`
+ }
+ err := mapstructure.Decode(r.Body, &res)
+
+ return res.LoadBalancer, 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/urls.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go
new file mode 100644
index 0000000..a307490
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go
@@ -0,0 +1,21 @@
+package loadbalancers
+
+import "github.com/rackspace/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/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/fixtures.go
new file mode 100644
index 0000000..549dbbc
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/fixtures.go
@@ -0,0 +1,215 @@
+// +build fixtures
+package monitors
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/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 = Monitor{
+ AdminStateUp: true,
+ Name: "web",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 10,
+ MaxRetries: 1,
+ Timeout: 1,
+ Type: "PING",
+ ID: "466c8345-28d8-4f84-a246-e04380b0461d",
+ Pools: []PoolID{{ID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}},
+ }
+ HealthmonitorDb = 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: []PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
+ }
+ HealthmonitorUpdated = 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: []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/requests.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
new file mode 100644
index 0000000..92e7e83
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
@@ -0,0 +1,304 @@
+package monitors
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type monitorOpts struct {
+ // Required. The Pool to Monitor.
+ PoolID string
+
+ // Optional. The Name of the Monitor.
+ Name string
+
+ // Required for admins. Indicates the owner of the Loadbalancer.
+ TenantID string
+
+ // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is
+ // sent by the load balancer to verify the member state.
+ Type string
+
+ // Required. The time, in seconds, between sending probes to members.
+ Delay int
+
+ // Required. Maximum number of seconds for a Monitor to wait for a ping reply
+ // before it times out. The value must be less than the delay value.
+ Timeout int
+
+ // Required. Number of permissible ping failures before changing the member's
+ // status to INACTIVE. Must be a number between 1 and 10.
+ MaxRetries int
+
+ // Required for HTTP(S) types. URI path that will be accessed if Monitor type
+ // is HTTP or HTTPS.
+ URLPath string
+
+ // Required for HTTP(S) types. The HTTP method used for requests by the
+ // Monitor. If this attribute is not specified, it defaults to "GET".
+ HTTPMethod string
+
+ // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S)
+ // Monitor. You can either specify a single status like "200", or a range
+ // like "200-202".
+ ExpectedCodes string
+
+ AdminStateUp *bool
+}
+
+// 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 (
+ errPoolIDRequired = fmt.Errorf("PoolID to monitor is required")
+ errValidTypeRequired = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS")
+ errDelayRequired = fmt.Errorf("Delay is required")
+ errTimeoutRequired = fmt.Errorf("Timeout is required")
+ errMaxRetriesRequired = fmt.Errorf("MaxRetries is required")
+ errURLPathRequired = fmt.Errorf("URL path is required")
+ errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required")
+ errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout")
+)
+
+// 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 monitorOpts
+
+// ToMonitorCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+ allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true}
+
+ if allowed[opts.Type] {
+ l["type"] = opts.Type
+ } else {
+ return nil, errValidTypeRequired
+ }
+ if opts.Type == TypeHTTP || opts.Type == TypeHTTPS {
+ if opts.URLPath != "" {
+ l["url_path"] = opts.URLPath
+ } else {
+ return nil, errURLPathRequired
+ }
+ if opts.ExpectedCodes != "" {
+ l["expected_codes"] = opts.ExpectedCodes
+ } else {
+ return nil, errExpectedCodesRequired
+ }
+ }
+ if opts.PoolID != "" {
+ l["pool_id"] = opts.PoolID
+ } else {
+ return nil, errPoolIDRequired
+ }
+ if opts.Delay != 0 {
+ l["delay"] = opts.Delay
+ } else {
+ return nil, errDelayRequired
+ }
+ if opts.Timeout != 0 {
+ l["timeout"] = opts.Timeout
+ } else {
+ return nil, errMaxRetriesRequired
+ }
+ if opts.MaxRetries != 0 {
+ l["max_retries"] = opts.MaxRetries
+ } else {
+ return nil, errMaxRetriesRequired
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.TenantID != "" {
+ l["tenant_id"] = opts.TenantID
+ }
+ if opts.HTTPMethod != "" {
+ l["http_method"] = opts.HTTPMethod
+ }
+
+ return map[string]interface{}{"healthmonitor": l}, 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) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToMonitorCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+ return res
+}
+
+// Get retrieves a particular Health Monitor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
+ return res
+}
+
+// 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 monitorOpts
+
+// ToMonitorUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.URLPath != "" {
+ l["url_path"] = opts.URLPath
+ }
+ if opts.ExpectedCodes != "" {
+ l["expected_codes"] = opts.ExpectedCodes
+ }
+ if opts.Delay != 0 {
+ l["delay"] = opts.Delay
+ }
+ if opts.Timeout != 0 {
+ l["timeout"] = opts.Timeout
+ }
+ if opts.MaxRetries != 0 {
+ l["max_retries"] = opts.MaxRetries
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.HTTPMethod != "" {
+ l["http_method"] = opts.HTTPMethod
+ }
+
+ return map[string]interface{}{"healthmonitor": l}, nil
+}
+
+// Update is an operation which modifies the attributes of the specified Monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToMonitorUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 202},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular Monitor based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = c.Delete(resourceURL(c, id), nil)
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go
new file mode 100644
index 0000000..202a182
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go
@@ -0,0 +1,159 @@
+package monitors
+
+import (
+ "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/healthmonitors", rootURL(fake.ServiceClient()))
+}
+
+func TestListHealthmonitors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorListSuccessfully(t)
+
+ pages := 0
+ err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := 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 := List(fake.ServiceClient(), ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := 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 := Create(fake.ServiceClient(), 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 := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGetHealthmonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := 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 := 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 := Update(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7", 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 := Create(fake.ServiceClient(), 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 = Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", 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/results.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go
new file mode 100644
index 0000000..da363c0
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go
@@ -0,0 +1,160 @@
+package monitors
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type PoolID struct {
+ ID string `mapstructure:"id" 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
+
+ // The Name of the Monitor.
+ Name string
+
+ // Only an administrative user can specify a tenant ID
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+ // The type of probe sent by the load balancer to verify the member state,
+ // which is PING, TCP, HTTP, or HTTPS.
+ Type string
+
+ // The time, in seconds, between sending probes to members.
+ Delay int
+
+ // The maximum number of seconds for a monitor to wait for a connection to be
+ // established before it times out. This value must be less than the delay value.
+ Timeout int
+
+ // Number of allowed connection failures before changing the status of the
+ // member to INACTIVE. A valid value is from 1 to 10.
+ MaxRetries int `json:"max_retries" mapstructure:"max_retries"`
+
+ // The HTTP method that the monitor uses for requests.
+ HTTPMethod string `json:"http_method" mapstructure:"http_method"`
+
+ // The HTTP path of the request sent by the monitor to test the health of a
+ // member. Must be a string beginning with a forward slash (/).
+ URLPath string `json:"url_path" mapstructure:"url_path"`
+
+ // Expected HTTP codes for a passing HTTP(S) monitor.
+ ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"`
+
+ // The administrative state of the health monitor, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+ // The status of the health monitor. Indicates whether the health monitor is
+ // operational.
+ Status string
+
+ // List of pools that are associated with the health monitor.
+ Pools []PoolID `mapstructure:"pools" json:"pools"`
+}
+
+type Pool struct {
+}
+
+// MonitorPage is the page returned by a pager when traversing over a
+// collection of health monitors.
+type MonitorPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of monitors has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p MonitorPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"healthmonitors_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p MonitorPage) IsEmpty() (bool, error) {
+ is, err := ExtractMonitors(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct,
+// and extracts the elements into a slice of Monitor structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMonitors(page pagination.Page) ([]Monitor, error) {
+ var resp struct {
+ Monitors []Monitor `mapstructure:"healthmonitors" json:"healthmonitors"`
+ }
+
+ err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
+
+ return resp.Monitors, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a monitor.
+func (r commonResult) Extract() (*Monitor, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Monitor *Monitor `json:"healthmonitor" mapstructure:"healthmonitor"`
+ }
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return res.Monitor, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go
new file mode 100644
index 0000000..a3abba4
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import "github.com/rackspace/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/fixtures.go b/openstack/networking/v2/extensions/lbaas_v2/pools/fixtures.go
new file mode 100644
index 0000000..107ed14
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/fixtures.go
@@ -0,0 +1,388 @@
+// +build fixtures
+package pools
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/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 = Pool{
+ LBMethod: "ROUND_ROBIN",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorID: "466c8345-28d8-4f84-a246-e04380b0461d",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "web",
+ Members: []Member{{ID: "53306cda-815d-4354-9fe4-59e09da9c3c5"}},
+ ID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Listeners: []ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+ Provider: "haproxy",
+ }
+ PoolDb = Pool{
+ LBMethod: "LEAST_CONNECTION",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "db",
+ Members: []Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
+ ID: "c3741b06-df4d-4715-b142-276b6bce75ab",
+ Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Listeners: []ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+ Provider: "haproxy",
+ }
+ PoolUpdated = Pool{
+ LBMethod: "LEAST_CONNECTION",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "db",
+ Members: []Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
+ ID: "c3741b06-df4d-4715-b142-276b6bce75ab",
+ Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Listeners: []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 = 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 = 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 = 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/requests.go b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
new file mode 100644
index 0000000..706ce96
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
@@ -0,0 +1,485 @@
+package pools
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+type poolOpts struct {
+ // Only required if the caller has an admin role and wants to create a pool
+ // for another tenant.
+ TenantID 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 Protocol
+
+ // 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
+
+ // Required. 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
+
+ // Optional. Omit this field to prevent session persistence.
+ Persistence *SessionPersistence
+
+ // Optional. The administrative state of the Pool. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ 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)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// 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"
+)
+
+var (
+ errLoadbalancerOrListenerRequired = fmt.Errorf("A ListenerID or LoadbalancerID is required")
+ errValidLBMethodRequired = fmt.Errorf("A valid LBMethod is required. Supported values are ROUND_ROBIN, LEAST_CONNECTIONS, SOURCE_IP")
+ errValidProtocolRequired = fmt.Errorf("A valid Protocol is required. Supported values are TCP, HTTP, 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 poolOpts
+
+// ToPoolCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+ allowedLBMethod := map[LBMethod]bool{LBMethodRoundRobin: true, LBMethodLeastConnections: true, LBMethodSourceIp: true}
+ allowedProtocol := map[Protocol]bool{ProtocolTCP: true, ProtocolHTTP: true, ProtocolHTTPS: true}
+
+ if allowedLBMethod[opts.LBMethod] {
+ l["lb_algorithm"] = opts.LBMethod
+ } else {
+ return nil, errValidLBMethodRequired
+ }
+ if allowedProtocol[opts.Protocol] {
+ l["protocol"] = opts.Protocol
+ } else {
+ return nil, errValidProtocolRequired
+ }
+ if opts.LoadbalancerID == "" && opts.ListenerID == "" {
+ return nil, errLoadbalancerOrListenerRequired
+ } else {
+ if opts.LoadbalancerID != "" {
+ l["loadbalancer_id"] = opts.LoadbalancerID
+ }
+ if opts.ListenerID != "" {
+ l["listener_id"] = opts.ListenerID
+ }
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.TenantID != "" {
+ l["tenant_id"] = opts.TenantID
+ }
+ if opts.Persistence != nil {
+ l["session_persistence"] = &opts.Persistence
+ }
+
+ return map[string]interface{}{"pool": l}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToPoolCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, 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
+}
+
+// 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 poolOpts
+
+// ToPoolUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+ allowedLBMethod := map[LBMethod]bool{LBMethodRoundRobin: true, LBMethodLeastConnections: true, LBMethodSourceIp: true}
+
+ if opts.LBMethod != "" {
+ if allowedLBMethod[opts.LBMethod] {
+ l["lb_algorithm"] = opts.LBMethod
+ } else {
+ return nil, errValidLBMethodRequired
+ }
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.Description != "" {
+ l["description"] = opts.Description
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+
+ return map[string]interface{}{"pool": l}, nil
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToPoolUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular 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
+}
+
+// CreateOpts contains all the values needed to create a new Member for a Pool.
+type memberOpts 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 Pool. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// MemberListOptsBuilder allows extensions to add additional parameters to the
+// Member List request.
+type MemberListOptsBuilder interface {
+ ToMemberListQuery() (string, error)
+}
+
+// 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"`
+ 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 MemberListOpts) ToMemberListQuery() (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
+// 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 MemberListOptsBuilder) pagination.Pager {
+ url := memberRootURL(c, poolID)
+ if opts != nil {
+ query, err := opts.ToMemberListQuery()
+ 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}}
+ })
+}
+
+var (
+ errPoolIdRequired = fmt.Errorf("PoolID is required")
+ errAddressRequired = fmt.Errorf("Address is required")
+ errProtocolPortRequired = fmt.Errorf("ProtocolPort is required")
+)
+
+// MemberCreateOptsBuilder 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 MemberCreateOptsBuilder interface {
+ ToMemberCreateMap() (map[string]interface{}, error)
+}
+
+// MemberCreateOpts is the common options struct used in this package's Create
+// operation.
+type MemberCreateOpts memberOpts
+
+// ToMemberCreateMap casts a CreateOpts struct to a map.
+func (opts MemberCreateOpts) ToMemberCreateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.Address != "" {
+ l["address"] = opts.Address
+ } else {
+ return nil, errAddressRequired
+ }
+ if opts.ProtocolPort != 0 {
+ l["protocol_port"] = opts.ProtocolPort
+ } else {
+ return nil, errProtocolPortRequired
+ }
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.TenantID != "" {
+ l["tenant_id"] = opts.TenantID
+ }
+ if opts.SubnetID != "" {
+ l["subnet_id"] = opts.SubnetID
+ }
+ if opts.Weight != 0 {
+ l["weight"] = opts.Weight
+ }
+
+ return map[string]interface{}{"member": l}, nil
+}
+
+// CreateAssociateMember will create and associate a Member with a particular Pool.
+func CreateAssociateMember(c *gophercloud.ServiceClient, poolID string, opts MemberCreateOpts) AssociateResult {
+ var res AssociateResult
+
+ if poolID == "" {
+ res.Err = errPoolIdRequired
+ return res
+ }
+
+ reqBody, err := opts.ToMemberCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, 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
+}
+
+// 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 MemberUpdateOptsBuilder interface {
+ ToMemberUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type MemberUpdateOpts memberOpts
+
+// ToMemberUpdateMap casts a UpdateOpts struct to a map.
+func (opts MemberUpdateOpts) ToMemberUpdateMap() (map[string]interface{}, error) {
+ l := make(map[string]interface{})
+
+ if opts.AdminStateUp != nil {
+ l["admin_state_up"] = &opts.AdminStateUp
+ }
+ if opts.Name != "" {
+ l["name"] = opts.Name
+ }
+ if opts.Weight != 0 {
+ l["weight"] = opts.Weight
+ }
+
+ return map[string]interface{}{"member": l}, nil
+}
+
+// Update allows Member to be updated.
+func UpdateAssociateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts MemberUpdateOpts) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToMemberUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, 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) DeleteResult {
+ var res DeleteResult
+ _, 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..288637d
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/requests_test.go
@@ -0,0 +1,255 @@
+package pools
+
+import (
+ "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 TestListPools(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolListSuccessfully(t)
+
+ pages := 0
+ err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := 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 := List(fake.ServiceClient(), ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := 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 := Create(fake.ServiceClient(), CreateOpts{
+ LBMethod: 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 := 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 := 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 := Update(client, "c3741b06-df4d-4715-b142-276b6bce75ab", UpdateOpts{
+ Name: "NewPoolName",
+ LBMethod: LBMethodLeastConnections,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, PoolUpdated, *actual)
+}
+
+func TestRequiredPoolCreateOpts(t *testing.T) {
+ res := Create(fake.ServiceClient(), CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{LBMethod: LBMethod("invalid"), Protocol: ProtocolHTTPS, LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a"})
+ if res.Err == nil || res.Err != errValidLBMethodRequired {
+ t.Fatalf("Expected '%s' error, but got '%s'", errValidLBMethodRequired, res.Err)
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{LBMethod: LBMethodRoundRobin, Protocol: Protocol("invalid"), LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a"})
+ if res.Err == nil || res.Err != errValidProtocolRequired {
+ t.Fatalf("Expected '%s' error, but got '%s'", errValidProtocolRequired, res.Err)
+ }
+ res = Create(fake.ServiceClient(), CreateOpts{LBMethod: LBMethodRoundRobin, Protocol: ProtocolHTTPS})
+ if res.Err == nil || res.Err != errLoadbalancerOrListenerRequired {
+ t.Fatalf("Expected '%s' error, but got '%s'", errLoadbalancerOrListenerRequired, res.Err)
+ }
+}
+
+func TestListMembers(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberListSuccessfully(t)
+
+ pages := 0
+ err := ListAssociateMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", MemberListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := 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 := ListAssociateMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", MemberListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := 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 := CreateAssociateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", MemberCreateOpts{
+ Name: "db",
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ Address: "10.0.2.11",
+ ProtocolPort: 80,
+ Weight: 10,
+ }).ExtractMember()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, MemberDb, *actual)
+}
+
+func TestRequiredMemberCreateOpts(t *testing.T) {
+ res := CreateAssociateMember(fake.ServiceClient(), "", MemberCreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = CreateAssociateMember(fake.ServiceClient(), "", MemberCreateOpts{Address: "1.2.3.4", ProtocolPort: 80})
+ if res.Err == nil || res.Err != errPoolIdRequired {
+ t.Fatalf("Expected '%s' error, but got '%s'", errPoolIdRequired, res.Err)
+ }
+ res = CreateAssociateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", MemberCreateOpts{ProtocolPort: 80})
+ if res.Err == nil || res.Err != errAddressRequired {
+ t.Fatalf("Expected '%s' error, but got '%s'", errAddressRequired, res.Err)
+ }
+ res = CreateAssociateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", MemberCreateOpts{Address: "1.2.3.4"})
+ if res.Err == nil || res.Err != errProtocolPortRequired {
+ t.Fatalf("Expected '%s' error, but got '%s'", errProtocolPortRequired, res.Err)
+ }
+}
+
+func TestGetMember(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := GetAssociateMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf").ExtractMember()
+ 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 := 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 := UpdateAssociateMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf", MemberUpdateOpts{
+ Name: "newMemberName",
+ Weight: 4,
+ }).ExtractMember()
+ 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/results.go b/openstack/networking/v2/extensions/lbaas_v2/pools/results.go
new file mode 100644
index 0000000..17f677d
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/results.go
@@ -0,0 +1,274 @@
+package pools
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
+ "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"`
+}
+
+type LoadBalancerID struct {
+ ID string `mapstructure:"id" json:"id"`
+}
+
+type ListenerID struct {
+ ID string `mapstructure:"id" 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" 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 []ListenerID `mapstructure:"listeners" json:"listeners"` //[]map[string]interface{}
+
+ // A list of member objects IDs.
+ Members []Member `mapstructure:"members" json:"members"`
+
+ // The ID of associated health monitor.
+ MonitorID string `json:"healthmonitor_id" mapstructure:"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" 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
+
+ // The unique ID for the Pool.
+ ID string
+
+ // A list of load balancer objects IDs.
+ Loadbalancers []LoadBalancerID `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
+
+ Monitor monitors.Monitor `mapstructure:"healthmonitor" 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 (p PoolPage) NextPageURL() (string, error) {
+ type resp struct {
+ Links []gophercloud.Link `mapstructure:"pools_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (p PoolPage) IsEmpty() (bool, error) {
+ is, err := ExtractPools(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractPools accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPools(page pagination.Page) ([]Pool, error) {
+ var resp struct {
+ Pools []Pool `mapstructure:"pools" json:"pools"`
+ }
+
+ err := mapstructure.Decode(page.(PoolPage).Body, &resp)
+
+ return resp.Pools, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Pool, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Pool *Pool `json:"pool"`
+ }
+
+ err := mapstructure.Decode(r.Body, &res)
+
+ return res.Pool, err
+}
+
+// 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 `json:"address" mapstructure:"address"`
+
+ // 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..e206406
--- /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"
+ 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)
+}