Adding Support for LBaaS v2 - Listeners
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..e903244
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
@@ -0,0 +1,231 @@
+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
+
+// Convenience vars for AdminStateUp values.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Up   AdminState = &iTrue
+	Down AdminState = &iFalse
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	ID              string `q:"id"`
+	Name            string `q:"name"`
+	AdminStateUp    *bool  `q:"admin_state_up"`
+	TenantID        string `q:"tenant_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"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// routers. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those routers that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return pagination.Pager{Err: err}
+	}
+	u := rootURL(c) + q.String()
+	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
+		return ListenerPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+var (
+	errLoadbalancerIdRequired = fmt.Errorf("Loadbalancer ID is required")
+	errProtocolRequired       = fmt.Errorf("Protocol is required")
+	errProtocolPortRequired   = fmt.Errorf("Protocol port is required")
+)
+
+// CreateOpts contains all the values needed to create a new Listener.
+type CreateOpts struct {
+	// Required. The protocol - can either be TCP, HTTP or HTTPS.
+	Protocol string
+
+	// 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
+}
+
+// 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
+
+	// Validate required opts
+	if opts.LoadbalancerID == "" {
+		res.Err = errLoadbalancerIdRequired
+		return res
+	}
+	if opts.Protocol == "" {
+		res.Err = errProtocolRequired
+		return res
+	}
+	if opts.ProtocolPort == 0 {
+		res.Err = errProtocolPortRequired
+		return res
+	}
+
+	type listener struct {
+		Name                   *string  `json:"name,omitempty"`
+		LoadbalancerID         string   `json:"loadbalancer_id,omitempty"`
+		Protocol               string   `json:"protocol"`
+		ProtocolPort           int      `json:"protocol_port"`
+		DefaultPoolID          *string  `json:"default_pool_id,omitempty"`
+		Description            *string  `json:"description,omitempty"`
+		TenantID               *string  `json:"tenant_id,omitempty"`
+		ConnLimit              *int     `json:"connection_limit,omitempty"`
+		AdminStateUp           *bool    `json:"admin_state_up,omitempty"`
+		DefaultTlsContainerRef *string  `json:"default_tls_container_ref,omitempty"`
+		SniContainerRefs       []string `json:"sni_container_refs,omitempty"`
+	}
+
+	type request struct {
+		Listener listener `json:"listener"`
+	}
+
+	reqBody := request{Listener: listener{
+		Name:                   gophercloud.MaybeString(opts.Name),
+		LoadbalancerID:         opts.LoadbalancerID,
+		Protocol:               opts.Protocol,
+		ProtocolPort:           opts.ProtocolPort,
+		DefaultPoolID:          gophercloud.MaybeString(opts.DefaultPoolID),
+		Description:            gophercloud.MaybeString(opts.Description),
+		TenantID:               gophercloud.MaybeString(opts.TenantID),
+		ConnLimit:              opts.ConnLimit,
+		AdminStateUp:           opts.AdminStateUp,
+		DefaultTlsContainerRef: gophercloud.MaybeString(opts.DefaultTlsContainerRef),
+		SniContainerRefs:       opts.SniContainerRefs,
+	}}
+
+	_, 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
+}
+
+// UpdateOpts contains all the values that can be updated on an existing Listener.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+	// Human-readable name for the Listener. Does not have to be unique.
+	Name 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
+}
+
+// Update is an operation which modifies the attributes of the specified Listener.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type listener struct {
+		Name                   string   `json:"name,omitempty"`
+		Description            *string  `json:"description,omitempty"`
+		ConnLimit              *int     `json:"connection_limit,omitempty"`
+		AdminStateUp           *bool    `json:"admin_state_up,omitempty"`
+		DefaultTlsContainerRef *string  `json:"default_tls_container_ref,omitempty"`
+		SniContainerRefs       []string `json:"sni_container_refs,omitempty"`
+	}
+
+	type request struct {
+		Listener listener `json:"listener"`
+	}
+
+	reqBody := request{Listener: listener{
+		Name:                   opts.Name,
+		Description:            gophercloud.MaybeString(opts.Description),
+		ConnLimit:              opts.ConnLimit,
+		AdminStateUp:           opts.AdminStateUp,
+		DefaultTlsContainerRef: gophercloud.MaybeString(opts.DefaultTlsContainerRef),
+		SniContainerRefs:       opts.SniContainerRefs,
+	}}
+
+	var res UpdateResult
+	_, 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..dc14755
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests_test.go
@@ -0,0 +1,322 @@
+package listeners
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	//"github.com/davecgh/go-spew/spew"
+	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/listeners", rootURL(fake.ServiceClient()))
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/listeners/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "listeners":[
+         {
+           "id": "db902c0c-d5ff-4753-b465-668ad9656918",
+           "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+           "name": "web_listener",
+           "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_listener",
+	   "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"]
+         }
+      ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractListeners(page)
+		if err != nil {
+			t.Errorf("Failed to extract LBs: %v", err)
+			return false, err
+		}
+
+		expected := []Listener{
+			{
+				ID:                     "db902c0c-d5ff-4753-b465-668ad9656918",
+				TenantID:               "310df60f-2a10-4ee5-9554-98393092194c",
+				Name:                   "web_listener",
+				Description:            "listener config for the web tier",
+				Loadbalancers:          []map[string]interface{}{{"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"},
+			},
+			{
+				ID:                     "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+				TenantID:               "310df60f-2a10-4ee5-9554-98393092194c",
+				Name:                   "db_listener",
+				Description:            "listener config for the db tier",
+				Loadbalancers:          []map[string]interface{}{{"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"},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "listener": {
+        "loadbalancer_id": "53306cda-815d-4354-9444-59e09da9c3c5",
+        "protocol": "HTTP",
+        "name": "NewListener",
+        "admin_state_up": true,
+        "default_tls_container_ref": "8032909d-47a1-4715-90af-5153ffe39861",
+        "default_pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+        "protocol_port": 80
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "listener": {
+        "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+	"tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+	"name": "NewListener",
+	"description": "",
+	"loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}],
+	"protocol": "HTTP",
+	"protocol_port": 80,
+	"connection_limit": -1,
+	"default_pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+	"admin_state_up": true,
+	"default_tls_container_ref": "8032909d-47a1-4715-90af-5153ffe39861"
+    }
+}
+		`)
+	})
+
+	opts := CreateOpts{
+		Protocol:               "HTTP",
+		Name:                   "NewListener",
+		LoadbalancerID:         "53306cda-815d-4354-9444-59e09da9c3c5",
+		AdminStateUp:           Up,
+		DefaultTlsContainerRef: "8032909d-47a1-4715-90af-5153ffe39861",
+		DefaultPoolID:          "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+		ProtocolPort:           80,
+	}
+
+	r, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "HTTP", r.Protocol)
+	th.AssertEquals(t, "", r.Description)
+	th.AssertEquals(t, true, r.AdminStateUp)
+	th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.DefaultTlsContainerRef)
+	th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID)
+	th.AssertEquals(t, -1, r.ConnLimit)
+	th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.DefaultPoolID)
+	th.AssertEquals(t, 80, r.ProtocolPort)
+	th.AssertEquals(t, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", r.ID)
+	th.AssertEquals(t, "53306cda-815d-4354-9444-59e09da9c3c5", r.Loadbalancers[0]["id"])
+	th.AssertEquals(t, "NewListener", r.Name)
+}
+
+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 TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "listener": {
+        "id": "4ec89087-d057-4e2c-911f-60a3b47ee304",
+	"tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+	"name": "NewListener",
+	"description": "",
+	"loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}],
+	"protocol": "HTTP",
+	"protocol_port": 80,
+	"connection_limit": -1,
+	"default_pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+	"admin_state_up": true,
+	"default_tls_container_ref": "8032909d-47a1-4715-90af-5153ffe39861"
+    }
+}
+			`)
+	})
+
+	l, err := Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "HTTP", l.Protocol)
+	th.AssertEquals(t, "", l.Description)
+	th.AssertEquals(t, true, l.AdminStateUp)
+	th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", l.DefaultTlsContainerRef)
+	th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", l.TenantID)
+	th.AssertEquals(t, -1, l.ConnLimit)
+	th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", l.DefaultPoolID)
+	th.AssertEquals(t, 80, l.ProtocolPort)
+	th.AssertEquals(t, "4ec89087-d057-4e2c-911f-60a3b47ee304", l.ID)
+	th.AssertEquals(t, "53306cda-815d-4354-9444-59e09da9c3c5", l.Loadbalancers[0]["id"])
+	th.AssertEquals(t, "NewListener", l.Name)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "listener": {
+        "name": "NewListenerName",
+        "connection_limit": 1001
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+    "listener": {
+        "id": "4ec89087-d057-4e2c-911f-60a3b47ee304",
+	"tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+	"name": "NewListenerName",
+	"description": "",
+	"loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}],
+	"protocol": "HTTP",
+	"protocol_port": 80,
+	"connection_limit": 1001,
+	"default_pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+	"admin_state_up": true,
+	"default_tls_container_ref": "8032909d-47a1-4715-90af-5153ffe39861"
+    }
+}
+		`)
+	})
+
+	i1001 := 1001
+	options := UpdateOpts{
+		Name:      "NewListenerName",
+		ConnLimit: &i1001,
+	}
+
+	l, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, *(options.ConnLimit), l.ConnLimit)
+	th.AssertEquals(t, options.Name, l.Name)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
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..76b374c
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/results.go
@@ -0,0 +1,133 @@
+package listeners
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	// "github.com/davecgh/go-spew/spew"
+)
+
+// 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 objects IDs.
+	Loadbalancers []map[string]interface{} `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"`
+}
+
+// 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)
+}