Adding Support for LBaaS v2 - Loadbalancers
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..ec7f9d6
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/doc.go
@@ -0,0 +1,3 @@
+// Package lbaas_v2 provides information and interaction with the Load Balancer
+// as a Service v2 extension for the OpenStack Networking service.
+package lbaas_v2
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..d75171d
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
@@ -0,0 +1,197 @@
+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
+
+// 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 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"`
+}
+
+// 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 LoadbalancerPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+var (
+	errVipSubnetIDRequried = fmt.Errorf("VipSubnetID is required")
+)
+
+// CreateOpts contains all the values needed to create a new Loadbalancer.
+type CreateOpts 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
+}
+
+// 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
+
+	// Validate required opts
+	if opts.VipSubnetID == "" {
+		res.Err = errVipSubnetIDRequried
+		return res
+	}
+
+	type loadbalancer struct {
+		Name         *string `json:"name,omitempty"`
+		Description  *string `json:"description,omitempty"`
+		VipSubnetID  string  `json:"vip_subnet_id"`
+		TenantID     *string `json:"tenant_id,omitempty"`
+		VipAddress   *string `json:"vip_address,omitempty"`
+		AdminStateUp *bool   `json:"admin_state_up,omitempty"`
+		Flavor       *string `json:"flavor,omitempty"`
+		Provider     *string `json:"provider,omitempty"`
+	}
+
+	type request struct {
+		Loadbalancer loadbalancer `json:"loadbalancer"`
+	}
+
+	reqBody := request{Loadbalancer: loadbalancer{
+		Name:         gophercloud.MaybeString(opts.Name),
+		Description:  gophercloud.MaybeString(opts.Description),
+		VipSubnetID:  opts.VipSubnetID,
+		TenantID:     gophercloud.MaybeString(opts.TenantID),
+		VipAddress:   gophercloud.MaybeString(opts.VipAddress),
+		AdminStateUp: opts.AdminStateUp,
+		Flavor:       gophercloud.MaybeString(opts.Flavor),
+		Provider:     gophercloud.MaybeString(opts.Provider),
+	}}
+
+	_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
+	return res
+}
+
+// Get retrieves a particular virtual IP 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 needed to update an existing virtual
+// Loadbalancer. Attributes not listed here but appear in CreateOpts are
+// immutable and cannot be updated.
+type UpdateOpts 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
+
+	// Optional. The administrative state of the Loadbalancer. A valid value is true (UP)
+	// or false (DOWN).
+	AdminStateUp *bool
+}
+
+// Update is an operation which modifies the attributes of the specified Loadbalancer.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+	type loadbalancer struct {
+		Name         *string `json:"name,omitempty"`
+		Description  *string `json:"description,omitempty"`
+		AdminStateUp *bool   `json:"admin_state_up,omitempty"`
+	}
+
+	type request struct {
+		Loadbalancer loadbalancer `json:"loadbalancer"`
+	}
+
+	reqBody := request{Loadbalancer: loadbalancer{
+		Name:         gophercloud.MaybeString(opts.Name),
+		Description:  gophercloud.MaybeString(opts.Description),
+		AdminStateUp: opts.AdminStateUp,
+	}}
+
+	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 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
+}
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..3ff1996
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests_test.go
@@ -0,0 +1,304 @@
+package loadbalancers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/loadbalancers", rootURL(fake.ServiceClient()))
+	th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/loadbalancers/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	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", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "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": "provider_1",
+           "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": "provider_2",
+           "admin_state_up": true,
+           "provisioning_status": "PENDING_CREATE",
+           "operating_status": "OFFLINE"
+         }
+      ]
+}
+			`)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractLoadbalancers(page)
+		if err != nil {
+			t.Errorf("Failed to extract LBs: %v", err)
+			return false, err
+		}
+
+		expected := []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:           "provider_1",
+				AdminStateUp:       true,
+				ProvisioningStatus: "ACTIVE",
+				OperatingStatus:    "ONLINE",
+			},
+			{
+				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:           "provider_2",
+				AdminStateUp:       true,
+				ProvisioningStatus: "PENDING_CREATE",
+				OperatingStatus:    "OFFLINE",
+			},
+		}
+
+		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/loadbalancers", 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, `
+{
+    "loadbalancer": {
+	"name": "NewLb",
+	"vip_subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+	"vip_address": "10.0.0.11",
+	"flavor": "small",
+	"provider": "provider_1",
+	"admin_state_up": true
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "loadbalancer": {
+        "id": "04816630-0320-4f9d-a17b-402b4e145d91",
+	"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+	"name": "NewLb",
+	"description": "",
+	"vip_subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+	"vip_address": "10.0.0.11",
+	"flavor": "small",
+	"provider": "provider_1",
+	"admin_state_up": true,
+	"provisioning_status": "PENDING_CREATE",
+	"operating_status": "OFFLINE"
+    }
+}
+		`)
+	})
+
+	opts := CreateOpts{
+		Name:         "NewLb",
+		AdminStateUp: Up,
+		VipSubnetID:  "8032909d-47a1-4715-90af-5153ffe39861",
+		VipAddress:   "10.0.0.11",
+		Flavor:       "small",
+		Provider:     "provider_1",
+	}
+
+	r, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "PENDING_CREATE", r.ProvisioningStatus)
+	th.AssertEquals(t, "", r.Description)
+	th.AssertEquals(t, true, r.AdminStateUp)
+	th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.VipSubnetID)
+	th.AssertEquals(t, "54030507-44f7-473c-9342-b4d14a95f692", r.TenantID)
+	th.AssertEquals(t, "NewLb", r.Name)
+	th.AssertEquals(t, "OFFLINE", r.OperatingStatus)
+	th.AssertEquals(t, "small", r.Flavor)
+	th.AssertEquals(t, "provider_1", r.Provider)
+	th.AssertEquals(t, "10.0.0.11", r.VipAddress)
+}
+
+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 TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/3c073c6d-18f7-45e4-8921-84358b70542d", 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, `
+{
+   "loadbalancer": {
+	"id": "3c073c6d-18f7-45e4-8921-84358b70542d",
+	"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+	"name": "web_lb",
+	"description": "",
+	"vip_subnet_id": "8a49c438-848f-467b-9655-ea1548708154",
+	"vip_address": "10.30.176.47",
+	"flavor": "small",
+	"provider": "provider_1",
+	"admin_state_up": true,
+	"provisioning_status": "ACTIVE",
+	"operating_status": "ONLINE"
+	}
+}
+			`)
+	})
+
+	loadbalancer, err := Get(fake.ServiceClient(), "3c073c6d-18f7-45e4-8921-84358b70542d").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "ACTIVE", loadbalancer.ProvisioningStatus)
+	th.AssertEquals(t, "ONLINE", loadbalancer.OperatingStatus)
+	th.AssertEquals(t, "", loadbalancer.Description)
+	th.AssertEquals(t, true, loadbalancer.AdminStateUp)
+	th.AssertEquals(t, "web_lb", loadbalancer.Name)
+	th.AssertEquals(t, "10.30.176.47", loadbalancer.VipAddress)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/038d98e2-6a7d-473a-9ce4-76f7e1e0c6a4", 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, `
+{
+    "loadbalancer": {
+        "name": "NewLbName"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+
+		fmt.Fprintf(w, `
+{
+    "loadbalancer": {
+    "id": "038d98e2-6a7d-473a-9ce4-76f7e1e0c6a4",
+	"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+	"name": "NewLbName",
+	"description": "",
+	"vip_subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+	"vip_address": "10.0.0.11",
+	"flavor": "small",
+	"provider": "provider_1",
+	"admin_state_up": true,
+	"provisioning_status": "PENDING_UPDATE",
+	"operating_status": "OFFLINE"
+    }
+}
+		`)
+	})
+
+	options := UpdateOpts{
+		Name: "NewLbName",
+	}
+
+	loadbalancer, err := Update(fake.ServiceClient(), "038d98e2-6a7d-473a-9ce4-76f7e1e0c6a4", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "NewLbName", loadbalancer.Name)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/82c54b9a-cf84-460f-bdcf-fa591e6fd6f0", 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(), "82c54b9a-cf84-460f-bdcf-fa591e6fd6f0")
+	th.AssertNoErr(t, res.Err)
+}
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..4306b06
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
@@ -0,0 +1,128 @@
+package loadbalancers
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"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.
+// This entity is sometimes known in LB products under the name of a "virtual
+// server", a "vserver" or a "listener".
+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"`
+}
+
+// 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
+}
+
+// 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..48bcc39
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/urls.go
@@ -0,0 +1,16 @@
+package loadbalancers
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "lbaas"
+	resourcePath = "loadbalancers"
+)
+
+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)
+}