Merge pull request #318 from jamiehannaford/start-stop-ext
Adding "start stop" Compute extension
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4f596a1..93b798e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,7 @@
way than just downloading it. Here are the basic installation instructions:
1. Configure your `$GOPATH` and run `go get` as described in the main
-[README](/#how-to-install).
+[README](/README.md#how-to-install).
2. Move into the directory that houses your local repository:
diff --git a/acceptance/rackspace/lb/v1/acl_test.go b/acceptance/rackspace/lb/v1/acl_test.go
new file mode 100644
index 0000000..7a38027
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/acl_test.go
@@ -0,0 +1,94 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestACL(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ createACL(t, client, lbID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ networkIDs := showACL(t, client, lbID)
+
+ deleteNetworkItem(t, client, lbID, networkIDs[0])
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ bulkDeleteACL(t, client, lbID, networkIDs[1:2])
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ deleteACL(t, client, lbID)
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ deleteLB(t, client, lbID)
+}
+
+func createACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := acl.CreateOpts{
+ acl.CreateOpt{Address: "206.160.163.21", Type: acl.DENY},
+ acl.CreateOpt{Address: "206.160.165.11", Type: acl.DENY},
+ acl.CreateOpt{Address: "206.160.165.12", Type: acl.DENY},
+ acl.CreateOpt{Address: "206.160.165.13", Type: acl.ALLOW},
+ }
+
+ err := acl.Create(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created ACL items for LB %d", lbID)
+}
+
+func showACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) []int {
+ ids := []int{}
+
+ err := acl.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) {
+ accessList, err := acl.ExtractAccessList(page)
+ th.AssertNoErr(t, err)
+
+ for _, i := range accessList {
+ t.Logf("Listing network item: ID [%s] Address [%s] Type [%s]", i.ID, i.Address, i.Type)
+ ids = append(ids, i.ID)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ return ids
+}
+
+func deleteNetworkItem(t *testing.T, client *gophercloud.ServiceClient, lbID, itemID int) {
+ err := acl.Delete(client, lbID, itemID).ExtractErr()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted network item %d", itemID)
+}
+
+func bulkDeleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int, items []int) {
+ err := acl.BulkDelete(client, lbID, items).ExtractErr()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted network items %s", intsToStr(items))
+}
+
+func deleteACL(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := acl.DeleteAll(client, lbID).ExtractErr()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted ACL from LB %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/common.go b/acceptance/rackspace/lb/v1/common.go
new file mode 100644
index 0000000..4ce05e6
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/common.go
@@ -0,0 +1,62 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newProvider() (*gophercloud.ProviderClient, error) {
+ opts, err := rackspace.AuthOptionsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+ opts = tools.OnlyRS(opts)
+
+ return rackspace.AuthenticatedClient(opts)
+}
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ provider, err := newProvider()
+ if err != nil {
+ return nil, err
+ }
+
+ return rackspace.NewLBV1(provider, gophercloud.EndpointOpts{
+ Region: os.Getenv("RS_REGION"),
+ })
+}
+
+func newComputeClient() (*gophercloud.ServiceClient, error) {
+ provider, err := newProvider()
+ if err != nil {
+ return nil, err
+ }
+
+ return rackspace.NewComputeV2(provider, gophercloud.EndpointOpts{
+ Region: os.Getenv("RS_REGION"),
+ })
+}
+
+func setup(t *testing.T) *gophercloud.ServiceClient {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ return client
+}
+
+func intsToStr(ids []int) string {
+ strIDs := []string{}
+ for _, id := range ids {
+ strIDs = append(strIDs, strconv.Itoa(id))
+ }
+ return strings.Join(strIDs, ", ")
+}
diff --git a/acceptance/rackspace/lb/v1/lb_test.go b/acceptance/rackspace/lb/v1/lb_test.go
new file mode 100644
index 0000000..c67ddec
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/lb_test.go
@@ -0,0 +1,214 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestLBs(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 3)
+ id := ids[0]
+
+ listLBProtocols(t, client)
+
+ listLBAlgorithms(t, client)
+
+ listLBs(t, client)
+
+ getLB(t, client, id)
+
+ checkLBLogging(t, client, id)
+
+ checkErrorPage(t, client, id)
+
+ getStats(t, client, id)
+
+ updateLB(t, client, id)
+
+ deleteLB(t, client, id)
+
+ batchDeleteLBs(t, client, ids[1:])
+}
+
+func createLB(t *testing.T, client *gophercloud.ServiceClient, count int) []int {
+ ids := []int{}
+
+ for i := 0; i < count; i++ {
+ opts := lbs.CreateOpts{
+ Name: tools.RandomString("test_", 5),
+ Port: 80,
+ Protocol: "HTTP",
+ VIPs: []vips.VIP{
+ vips.VIP{Type: vips.PUBLIC},
+ },
+ }
+
+ lb, err := lbs.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created LB %d - waiting for it to build...", lb.ID)
+ waitForLB(client, lb.ID, lbs.ACTIVE)
+ t.Logf("LB %d has reached ACTIVE state", lb.ID)
+
+ ids = append(ids, lb.ID)
+ }
+
+ return ids
+}
+
+func waitForLB(client *gophercloud.ServiceClient, id int, state lbs.Status) {
+ gophercloud.WaitFor(60, func() (bool, error) {
+ lb, err := lbs.Get(client, id).Extract()
+ if err != nil {
+ return false, err
+ }
+ if lb.Status != state {
+ return false, nil
+ }
+ return true, nil
+ })
+}
+
+func listLBProtocols(t *testing.T, client *gophercloud.ServiceClient) {
+ err := lbs.ListProtocols(client).EachPage(func(page pagination.Page) (bool, error) {
+ pList, err := lbs.ExtractProtocols(page)
+ th.AssertNoErr(t, err)
+
+ for _, p := range pList {
+ t.Logf("Listing protocol: Name [%s]", p.Name)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func listLBAlgorithms(t *testing.T, client *gophercloud.ServiceClient) {
+ err := lbs.ListAlgorithms(client).EachPage(func(page pagination.Page) (bool, error) {
+ aList, err := lbs.ExtractAlgorithms(page)
+ th.AssertNoErr(t, err)
+
+ for _, a := range aList {
+ t.Logf("Listing algorithm: Name [%s]", a.Name)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func listLBs(t *testing.T, client *gophercloud.ServiceClient) {
+ err := lbs.List(client, lbs.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ lbList, err := lbs.ExtractLBs(page)
+ th.AssertNoErr(t, err)
+
+ for _, lb := range lbList {
+ t.Logf("Listing LB: ID [%d] Name [%s] Protocol [%s] Status [%s] Node count [%d] Port [%d]",
+ lb.ID, lb.Name, lb.Protocol, lb.Status, lb.NodeCount, lb.Port)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ lb, err := lbs.Get(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting LB %d: Created [%s] VIPs [%#v] Logging [%#v] Persistence [%#v] SourceAddrs [%#v]",
+ lb.ID, lb.Created, lb.VIPs, lb.ConnectionLogging, lb.SessionPersistence, lb.SourceAddrs)
+}
+
+func updateLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ opts := lbs.UpdateOpts{
+ Name: tools.RandomString("new_", 5),
+ Protocol: "TCP",
+ HalfClosed: gophercloud.Enabled,
+ Algorithm: "RANDOM",
+ Port: 8080,
+ Timeout: 100,
+ HTTPSRedirect: gophercloud.Disabled,
+ }
+
+ err := lbs.Update(client, id, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updating LB %d - waiting for it to finish", id)
+ waitForLB(client, id, lbs.ACTIVE)
+ t.Logf("LB %d has reached ACTIVE state", id)
+}
+
+func deleteLB(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ err := lbs.Delete(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted LB %d", id)
+}
+
+func batchDeleteLBs(t *testing.T, client *gophercloud.ServiceClient, ids []int) {
+ err := lbs.BulkDelete(client, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted LB %s", intsToStr(ids))
+}
+
+func checkLBLogging(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ err := lbs.EnableLogging(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enabled logging for LB %d", id)
+
+ res, err := lbs.IsLoggingEnabled(client, id)
+ th.AssertNoErr(t, err)
+ t.Logf("LB %d log enabled? %s", id, strconv.FormatBool(res))
+
+ waitForLB(client, id, lbs.ACTIVE)
+
+ err = lbs.DisableLogging(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disabled logging for LB %d", id)
+}
+
+func checkErrorPage(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ content, err := lbs.SetErrorPage(client, id, "<html>New content!</html>").Extract()
+ t.Logf("Set error page for LB %d", id)
+
+ content, err = lbs.GetErrorPage(client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Error page for LB %d: %s", id, content)
+
+ err = lbs.DeleteErrorPage(client, id).ExtractErr()
+ t.Logf("Deleted error page for LB %d", id)
+}
+
+func getStats(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ waitForLB(client, id, lbs.ACTIVE)
+
+ stats, err := lbs.GetStats(client, id).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Stats for LB %d: %#v", id, stats)
+}
+
+func checkCaching(t *testing.T, client *gophercloud.ServiceClient, id int) {
+ err := lbs.EnableCaching(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enabled caching for LB %d", id)
+
+ res, err := lbs.IsContentCached(client, id)
+ th.AssertNoErr(t, err)
+ t.Logf("Is caching enabled for LB? %s", strconv.FormatBool(res))
+
+ err = lbs.DisableCaching(client, id).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disabled caching for LB %d", id)
+}
diff --git a/acceptance/rackspace/lb/v1/monitor_test.go b/acceptance/rackspace/lb/v1/monitor_test.go
new file mode 100644
index 0000000..c1a8e24
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/monitor_test.go
@@ -0,0 +1,60 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMonitors(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ getMonitor(t, client, lbID)
+
+ updateMonitor(t, client, lbID)
+
+ deleteMonitor(t, client, lbID)
+
+ deleteLB(t, client, lbID)
+}
+
+func getMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ hm, err := monitors.Get(client, lbID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Health monitor for LB %d: Type [%s] Delay [%d] Timeout [%d] AttemptLimit [%d]",
+ lbID, hm.Type, hm.Delay, hm.Timeout, hm.AttemptLimit)
+}
+
+func updateMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := monitors.UpdateHTTPMonitorOpts{
+ AttemptLimit: 3,
+ Delay: 10,
+ Timeout: 10,
+ BodyRegex: "hello is it me you're looking for",
+ Path: "/foo",
+ StatusRegex: "200",
+ Type: monitors.HTTP,
+ }
+
+ err := monitors.Update(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ t.Logf("Updated monitor for LB %d", lbID)
+}
+
+func deleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := monitors.Delete(client, lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ t.Logf("Deleted monitor for LB %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/node_test.go b/acceptance/rackspace/lb/v1/node_test.go
new file mode 100644
index 0000000..18b9fe7
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/node_test.go
@@ -0,0 +1,175 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNodes(t *testing.T) {
+ client := setup(t)
+
+ serverIP := findServer(t)
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ nodeID := addNodes(t, client, lbID, serverIP)
+
+ listNodes(t, client, lbID)
+
+ getNode(t, client, lbID, nodeID)
+
+ updateNode(t, client, lbID, nodeID)
+
+ listEvents(t, client, lbID)
+
+ deleteNode(t, client, lbID, nodeID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ deleteLB(t, client, lbID)
+}
+
+func findServer(t *testing.T) string {
+ var serverIP string
+
+ client, err := newComputeClient()
+ th.AssertNoErr(t, err)
+
+ err = servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ sList, err := servers.ExtractServers(page)
+ th.AssertNoErr(t, err)
+
+ for _, s := range sList {
+ serverIP = s.AccessIPv4
+ t.Logf("Found an existing server: ID [%s] Public IP [%s]", s.ID, serverIP)
+ break
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ if serverIP == "" {
+ t.Log("No server found, creating one")
+
+ imageRef := os.Getenv("RS_IMAGE_ID")
+ if imageRef == "" {
+ t.Fatalf("OS var RS_IMAGE_ID undefined")
+ }
+ flavorRef := os.Getenv("RS_FLAVOR_ID")
+ if flavorRef == "" {
+ t.Fatalf("OS var RS_FLAVOR_ID undefined")
+ }
+
+ opts := &servers.CreateOpts{
+ Name: tools.RandomString("lb_test_", 5),
+ ImageRef: imageRef,
+ FlavorRef: flavorRef,
+ DiskConfig: diskconfig.Manual,
+ }
+
+ s, err := servers.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+ serverIP = s.AccessIPv4
+
+ t.Logf("Created server %s, waiting for it to build", s.ID)
+ err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+ t.Logf("Server created successfully.")
+ }
+
+ return serverIP
+}
+
+func addNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int, serverIP string) int {
+ opts := nodes.CreateOpts{
+ nodes.CreateOpt{
+ Address: serverIP,
+ Port: 80,
+ Condition: nodes.ENABLED,
+ Type: nodes.PRIMARY,
+ },
+ }
+
+ page := nodes.Create(client, lbID, opts)
+
+ nodeList, err := page.ExtractNodes()
+ th.AssertNoErr(t, err)
+
+ var nodeID int
+ for _, n := range nodeList {
+ nodeID = n.ID
+ }
+ if nodeID == 0 {
+ t.Fatalf("nodeID could not be extracted from create response")
+ }
+
+ t.Logf("Added node %d to LB %d", nodeID, lbID)
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ return nodeID
+}
+
+func listNodes(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := nodes.List(client, lbID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ nodeList, err := nodes.ExtractNodes(page)
+ th.AssertNoErr(t, err)
+
+ for _, n := range nodeList {
+ t.Logf("Listing node: ID [%d] Address [%s:%d] Status [%s]", n.ID, n.Address, n.Port, n.Status)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func getNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+ node, err := nodes.Get(client, lbID, nodeID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting node %d: Type [%s] Weight [%d]", nodeID, node.Type, node.Weight)
+}
+
+func updateNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+ opts := nodes.UpdateOpts{
+ Weight: gophercloud.IntToPointer(10),
+ Condition: nodes.DRAINING,
+ Type: nodes.SECONDARY,
+ }
+ err := nodes.Update(client, lbID, nodeID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Updated node %d", nodeID)
+ waitForLB(client, lbID, lbs.ACTIVE)
+}
+
+func listEvents(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ pager := nodes.ListEvents(client, lbID, nodes.ListEventsOpts{})
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ eventList, err := nodes.ExtractNodeEvents(page)
+ th.AssertNoErr(t, err)
+
+ for _, e := range eventList {
+ t.Logf("Listing events for node %d: Type [%s] Msg [%s] Severity [%s] Date [%s]",
+ e.NodeID, e.Type, e.DetailedMessage, e.Severity, e.Created)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func deleteNode(t *testing.T, client *gophercloud.ServiceClient, lbID int, nodeID int) {
+ err := nodes.Delete(client, lbID, nodeID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted node %d", nodeID)
+}
diff --git a/acceptance/rackspace/lb/v1/session_test.go b/acceptance/rackspace/lb/v1/session_test.go
new file mode 100644
index 0000000..8d85655
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/session_test.go
@@ -0,0 +1,47 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSession(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ getSession(t, client, lbID)
+
+ enableSession(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ disableSession(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ deleteLB(t, client, lbID)
+}
+
+func getSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ sp, err := sessions.Get(client, lbID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Session config: Type [%s]", sp.Type)
+}
+
+func enableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := sessions.CreateOpts{Type: sessions.HTTPCOOKIE}
+ err := sessions.Enable(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enable %s sessions for %d", opts.Type, lbID)
+}
+
+func disableSession(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := sessions.Disable(client, lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disable sessions for %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/throttle_test.go b/acceptance/rackspace/lb/v1/throttle_test.go
new file mode 100644
index 0000000..1cc1235
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/throttle_test.go
@@ -0,0 +1,53 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestThrottle(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ getThrottleConfig(t, client, lbID)
+
+ createThrottleConfig(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ deleteThrottleConfig(t, client, lbID)
+ waitForLB(client, lbID, "ACTIVE")
+
+ deleteLB(t, client, lbID)
+}
+
+func getThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ sp, err := throttle.Get(client, lbID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Throttle config: MaxConns [%s]", sp.MaxConnections)
+}
+
+func createThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ opts := throttle.CreateOpts{
+ MaxConnections: 200,
+ MaxConnectionRate: 100,
+ MinConnections: 0,
+ RateInterval: 10,
+ }
+
+ err := throttle.Create(client, lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Enable throttling for %d", lbID)
+}
+
+func deleteThrottleConfig(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := throttle.Delete(client, lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Disable throttling for %d", lbID)
+}
diff --git a/acceptance/rackspace/lb/v1/vip_test.go b/acceptance/rackspace/lb/v1/vip_test.go
new file mode 100644
index 0000000..bc0c2a8
--- /dev/null
+++ b/acceptance/rackspace/lb/v1/vip_test.go
@@ -0,0 +1,83 @@
+// +build acceptance lbs
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/lbs"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVIPs(t *testing.T) {
+ client := setup(t)
+
+ ids := createLB(t, client, 1)
+ lbID := ids[0]
+
+ listVIPs(t, client, lbID)
+
+ vipIDs := addVIPs(t, client, lbID, 3)
+
+ deleteVIP(t, client, lbID, vipIDs[0])
+
+ bulkDeleteVIPs(t, client, lbID, vipIDs[1:])
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+ deleteLB(t, client, lbID)
+}
+
+func listVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int) {
+ err := vips.List(client, lbID).EachPage(func(page pagination.Page) (bool, error) {
+ vipList, err := vips.ExtractVIPs(page)
+ th.AssertNoErr(t, err)
+
+ for _, vip := range vipList {
+ t.Logf("Listing VIP: ID [%s] Address [%s] Type [%s] Version [%s]",
+ vip.ID, vip.Address, vip.Type, vip.Version)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func addVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID, count int) []int {
+ ids := []int{}
+
+ for i := 0; i < count; i++ {
+ opts := vips.CreateOpts{
+ Type: vips.PUBLIC,
+ Version: vips.IPV6,
+ }
+
+ vip, err := vips.Create(client, lbID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created VIP %d", vip.ID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+
+ ids = append(ids, vip.ID)
+ }
+
+ return ids
+}
+
+func deleteVIP(t *testing.T, client *gophercloud.ServiceClient, lbID, vipID int) {
+ err := vips.Delete(client, lbID, vipID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted VIP %d", vipID)
+
+ waitForLB(client, lbID, lbs.ACTIVE)
+}
+
+func bulkDeleteVIPs(t *testing.T, client *gophercloud.ServiceClient, lbID int, ids []int) {
+ err := vips.BulkDelete(client, lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted VIPs %s", intsToStr(ids))
+}
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index e872b07..9ec3def 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -457,3 +457,14 @@
fmt.Fprintf(w, response)
})
}
+
+func HandleServerRescueSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/action", 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, `{ "rescue": { "adminPass": "1234567890" } }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{ "adminPass": "1234567890" }`))
+ })
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 6381419..2740a60 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -482,7 +482,7 @@
FlavorRef string
}
-// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body to the
+// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the
// Resize request.
func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
resize := map[string]interface{}{
@@ -543,3 +543,51 @@
return res
}
+
+// RescueOptsBuilder is an interface that allows extensions to override the
+// default structure of a Rescue request.
+type RescueOptsBuilder interface {
+ ToServerRescueMap() (map[string]interface{}, error)
+}
+
+// RescueOpts represents the configuration options used to control a Rescue
+// option.
+type RescueOpts struct {
+ // AdminPass is the desired administrative password for the instance in
+ // RESCUE mode. If it's left blank, the server will generate a password.
+ AdminPass string
+}
+
+// ToRescueResizeMap formats a RescueOpts as a map that can be used as a JSON
+// request body for the Rescue request.
+func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) {
+ server := make(map[string]interface{})
+ if opts.AdminPass != "" {
+ server["adminPass"] = opts.AdminPass
+ }
+ return map[string]interface{}{"rescue": server}, nil
+}
+
+// Rescue instructs the provider to place the server into RESCUE mode.
+func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) RescueResult {
+ var result RescueResult
+
+ if id == "" {
+ result.Err = fmt.Errorf("ID is required")
+ return result
+ }
+ reqBody, err := opts.ToServerRescueMap()
+ if err != nil {
+ result.Err = err
+ return result
+ }
+
+ _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+ Results: &result.Body,
+ ReqBody: &reqBody,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return result
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 392e2d8..9639702 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -174,3 +174,17 @@
res := RevertResize(client.ServiceClient(), "1234asdf")
th.AssertNoErr(t, res.Err)
}
+
+func TestRescue(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleServerRescueSuccessfully(t)
+
+ res := Rescue(client.ServiceClient(), "1234asdf", RescueOpts{
+ AdminPass: "1234567890",
+ })
+ th.AssertNoErr(t, res.Err)
+ adminPass, _ := res.Extract()
+ th.AssertEquals(t, "1234567890", adminPass)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index 53946ba..fec5345 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -54,6 +54,24 @@
gophercloud.ErrResult
}
+// RescueResult represents the result of a server rescue operation
+type RescueResult struct {
+ ActionResult
+}
+
+func (r RescueResult) Extract() (string, error) {
+ if r.Err != nil {
+ return "", r.Err
+ }
+
+ var response struct {
+ AdminPass string `mapstructure:"adminPass"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.AdminPass, err
+}
+
// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
type Server struct {
// ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
index 32e6d1b..42f05d3 100644
--- a/openstack/identity/v3/services/requests_test.go
+++ b/openstack/identity/v3/services/requests_test.go
@@ -38,7 +38,7 @@
}
if result.Description == nil || *result.Description != "Here's your service" {
- t.Errorf("Service description was unexpected [%s]", result.Description)
+ t.Errorf("Service description was unexpected [%s]", *result.Description)
}
if result.ID != "1234" {
t.Errorf("Service ID was unexpected [%s]", result.ID)
diff --git a/params.go b/params.go
index 68c17eb..a72aaed 100644
--- a/params.go
+++ b/params.go
@@ -9,6 +9,26 @@
"time"
)
+// EnabledState is a convenience type, mostly used in Create and Update
+// operations. Because the zero value of a bool is FALSE, we need to use a
+// pointer instead to indicate zero-ness.
+type EnabledState *bool
+
+// Convenience vars for EnabledState values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Enabled EnabledState = &iTrue
+ Disabled EnabledState = &iFalse
+)
+
+// IntToPointer is a function for converting integers into integer pointers.
+// This is useful when passing in options to operations.
+func IntToPointer(i int) *int {
+ return &i
+}
+
/*
MaybeString is an internal function to be used by request methods in individual
resource packages.
@@ -224,3 +244,25 @@
// Return an error if the underlying type of 'opts' isn't a struct.
return optsMap, fmt.Errorf("Options type is not a struct.")
}
+
+// IDSliceToQueryString takes a slice of elements and converts them into a query
+// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the
+// result would be `?name=20&name=40&name=60'
+func IDSliceToQueryString(name string, ids []int) string {
+ str := ""
+ for k, v := range ids {
+ if k == 0 {
+ str += "?"
+ } else {
+ str += "&"
+ }
+ str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v))
+ }
+ return str
+}
+
+// IntWithinRange returns TRUE if an integer falls within a defined range, and
+// FALSE if not.
+func IntWithinRange(val, min, max int) bool {
+ return val > min && val < max
+}
diff --git a/rackspace/client.go b/rackspace/client.go
index 5f739a8..65b547b 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -154,3 +154,14 @@
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewLBV1 creates a ServiceClient that can be used to access the Rackspace
+// Cloud Load Balancer v1 API.
+func NewLBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("rax:load-balancer")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/rackspace/compute/v2/flavors/fixtures.go b/rackspace/compute/v2/flavors/fixtures.go
index b6dca93..894f916 100644
--- a/rackspace/compute/v2/flavors/fixtures.go
+++ b/rackspace/compute/v2/flavors/fixtures.go
@@ -1,4 +1,5 @@
// +build fixtures
+
package flavors
import (
diff --git a/rackspace/compute/v2/images/fixtures.go b/rackspace/compute/v2/images/fixtures.go
index c46d196..ccfbdc6 100644
--- a/rackspace/compute/v2/images/fixtures.go
+++ b/rackspace/compute/v2/images/fixtures.go
@@ -1,4 +1,5 @@
// +build fixtures
+
package images
import (
diff --git a/rackspace/lb/v1/acl/doc.go b/rackspace/lb/v1/acl/doc.go
new file mode 100644
index 0000000..42325fe
--- /dev/null
+++ b/rackspace/lb/v1/acl/doc.go
@@ -0,0 +1,12 @@
+/*
+Package acl provides information and interaction with the access lists feature
+of the Rackspace Cloud Load Balancer service.
+
+The access list management feature allows fine-grained network access controls
+to be applied to the load balancer's virtual IP address. A single IP address,
+multiple IP addresses, or entire network subnets can be added. Items that are
+configured with the ALLOW type always takes precedence over items with the DENY
+type. To reject traffic from all items except for those with the ALLOW type,
+add a networkItem with an address of "0.0.0.0/0" and a DENY type.
+*/
+package acl
diff --git a/rackspace/lb/v1/acl/fixtures.go b/rackspace/lb/v1/acl/fixtures.go
new file mode 100644
index 0000000..e3c941c
--- /dev/null
+++ b/rackspace/lb/v1/acl/fixtures.go
@@ -0,0 +1,109 @@
+package acl
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/accesslist"
+}
+
+func mockListResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc(_rootURL(id), 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, `
+{
+ "accessList": [
+ {
+ "address": "206.160.163.21",
+ "id": 21,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.22",
+ "id": 22,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.23",
+ "id": 23,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.24",
+ "id": 24,
+ "type": "DENY"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "accessList": [
+ {
+ "address": "206.160.163.21",
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.165.11",
+ "type": "DENY"
+ }
+ ]
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteAllResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID, networkID int) {
+ th.Mux.HandleFunc(_rootURL(lbID)+"/"+strconv.Itoa(networkID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/acl/requests.go b/rackspace/lb/v1/acl/requests.go
new file mode 100644
index 0000000..e1e92ac
--- /dev/null
+++ b/rackspace/lb/v1/acl/requests.go
@@ -0,0 +1,127 @@
+package acl
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// network items that define a load balancer's access list.
+func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager {
+ url := rootURL(client, lbID)
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return AccessListPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+ ToAccessListCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+ // Required - the IP address or CIDR for item to add to access list.
+ Address string
+
+ // Required - the type of the node. Either ALLOW or DENY.
+ Type Type
+}
+
+// ToAccessListCreateMap converts a slice of options into a map that can be
+// used for the JSON.
+func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) {
+ type itemMap map[string]interface{}
+ items := []itemMap{}
+
+ for k, v := range opts {
+ if v.Address == "" {
+ return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k)
+ }
+ if v.Type != ALLOW && v.Type != DENY {
+ return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY")
+ }
+
+ item := make(itemMap)
+ item["address"] = v.Address
+ item["type"] = v.Type
+
+ items = append(items, item)
+ }
+
+ return itemMap{"accessList": items}, nil
+}
+
+// Create is the operation responsible for adding network items to the access
+// rules for a particular load balancer. If network items already exist, the
+// new item will be appended. A single IP address or subnet range is considered
+// unique and cannot be duplicated.
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToAccessListCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// BulkDelete will delete multiple network items from a load balancer's access
+// list in a single operation.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(itemIDs) > 10 || len(itemIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += gophercloud.IDSliceToQueryString("id", itemIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete will remove a single network item from a load balancer's access list.
+func Delete(c *gophercloud.ServiceClient, lbID, itemID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, itemID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
+
+// DeleteAll will delete the entire contents of a load balancer's access list,
+// effectively resetting it and allowing all traffic.
+func DeleteAll(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/rackspace/lb/v1/acl/requests_test.go b/rackspace/lb/v1/acl/requests_test.go
new file mode 100644
index 0000000..c4961a3
--- /dev/null
+++ b/rackspace/lb/v1/acl/requests_test.go
@@ -0,0 +1,91 @@
+package acl
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ itemID1 = 67890
+ itemID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAccessList(page)
+ th.AssertNoErr(t, err)
+
+ expected := AccessList{
+ NetworkItem{Address: "206.160.163.21", ID: 21, Type: DENY},
+ NetworkItem{Address: "206.160.163.22", ID: 22, Type: DENY},
+ NetworkItem{Address: "206.160.163.23", ID: 23, Type: DENY},
+ NetworkItem{Address: "206.160.163.24", ID: 24, Type: DENY},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ CreateOpt{Address: "206.160.163.21", Type: DENY},
+ CreateOpt{Address: "206.160.165.11", Type: DENY},
+ }
+
+ err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{itemID1, itemID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID, itemID1)
+
+ err := Delete(client.ServiceClient(), lbID, itemID1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDeleteAll(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteAllResponse(t, lbID)
+
+ err := DeleteAll(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/acl/results.go b/rackspace/lb/v1/acl/results.go
new file mode 100644
index 0000000..9ea5ea2
--- /dev/null
+++ b/rackspace/lb/v1/acl/results.go
@@ -0,0 +1,72 @@
+package acl
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// AccessList represents the rules of network access to a particular load
+// balancer.
+type AccessList []NetworkItem
+
+// NetworkItem describes how an IP address or entire subnet may interact with a
+// load balancer.
+type NetworkItem struct {
+ // The IP address or subnet (CIDR) that defines the network item.
+ Address string
+
+ // The numeric unique ID for this item.
+ ID int
+
+ // Either ALLOW or DENY.
+ Type Type
+}
+
+// Type defines how an item may connect to the load balancer.
+type Type string
+
+// Convenience consts.
+const (
+ ALLOW Type = "ALLOW"
+ DENY Type = "DENY"
+)
+
+// AccessListPage is the page returned by a pager for traversing over a
+// collection of network items in an access list.
+type AccessListPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AccessListPage struct is empty.
+func (p AccessListPage) IsEmpty() (bool, error) {
+ is, err := ExtractAccessList(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAccessList accepts a Page struct, specifically an AccessListPage
+// struct, and extracts the elements into a slice of NetworkItem structs. In
+// other words, a generic collection is mapped into a relevant slice.
+func ExtractAccessList(page pagination.Page) (AccessList, error) {
+ var resp struct {
+ List AccessList `mapstructure:"accessList" json:"accessList"`
+ }
+
+ err := mapstructure.Decode(page.(AccessListPage).Body, &resp)
+
+ return resp.List, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/acl/urls.go b/rackspace/lb/v1/acl/urls.go
new file mode 100644
index 0000000..e373fa1
--- /dev/null
+++ b/rackspace/lb/v1/acl/urls.go
@@ -0,0 +1,20 @@
+package acl
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ aclPath = "accesslist"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), aclPath)
+}
diff --git a/rackspace/lb/v1/lbs/doc.go b/rackspace/lb/v1/lbs/doc.go
new file mode 100644
index 0000000..05f0032
--- /dev/null
+++ b/rackspace/lb/v1/lbs/doc.go
@@ -0,0 +1,44 @@
+/*
+Package lbs provides information and interaction with the Load Balancer API
+resource for the Rackspace Cloud Load Balancer service.
+
+A load balancer is a logical device which belongs to a cloud account. It is
+used to distribute workloads between multiple back-end systems or services,
+based on the criteria defined as part of its configuration. This configuration
+is defined using the Create operation, and can be updated with Update.
+
+To conserve IPv4 address space, it is highly recommended that you share Virtual
+IPs between load balancers. If you have at least one load balancer, you may
+create subsequent ones that share a single virtual IPv4 and/or a single IPv6 by
+passing in a virtual IP ID to the Update operation (instead of a type). This
+feature is also highly desirable if you wish to load balance both an insecure
+and secure protocol using one IP or DNS name. In order to share a virtual IP,
+each Load Balancer must utilize a unique port.
+
+All load balancers have a Status attribute that shows the current configuration
+status of the device. This status is immutable by the caller and is updated
+automatically based on state changes within the service. When a load balancer
+is first created, it is placed into a BUILD state while the configuration is
+being generated and applied based on the request. Once the configuration is
+applied and finalized, it is in an ACTIVE status. In the event of a
+configuration change or update, the status of the load balancer changes to
+PENDING_UPDATE to signify configuration changes are in progress but have not yet
+been finalized. Load balancers in a SUSPENDED status are configured to reject
+traffic and do not forward requests to back-end nodes.
+
+An HTTP load balancer has the X-Forwarded-For (XFF) HTTP header set by default.
+This header contains the originating IP address of a client connecting to a web
+server through an HTTP proxy or load balancer, which many web applications are
+already designed to use when determining the source address for a request.
+
+It also includes the X-Forwarded-Proto (XFP) HTTP header, which has been added
+for identifying the originating protocol of an HTTP request as "http" or
+"https" depending on which protocol the client requested. This is useful when
+using SSL termination.
+
+Finally, it also includes the X-Forwarded-Port HTTP header, which has been
+added for being able to generate secure URLs containing the specified port.
+This header, along with the X-Forwarded-For header, provides the needed
+information to the underlying application servers.
+*/
+package lbs
diff --git a/rackspace/lb/v1/lbs/fixtures.go b/rackspace/lb/v1/lbs/fixtures.go
new file mode 100644
index 0000000..6325310
--- /dev/null
+++ b/rackspace/lb/v1/lbs/fixtures.go
@@ -0,0 +1,584 @@
+package lbs
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockListLBResponse(t *testing.T) {
+ th.Mux.HandleFunc("/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":[
+ {
+ "name":"lb-site1",
+ "id":71,
+ "protocol":"HTTP",
+ "port":80,
+ "algorithm":"RANDOM",
+ "status":"ACTIVE",
+ "nodeCount":3,
+ "virtualIps":[
+ {
+ "id":403,
+ "address":"206.55.130.1",
+ "type":"PUBLIC",
+ "ipVersion":"IPV4"
+ }
+ ],
+ "created":{
+ "time":"2010-11-30T03:23:42Z"
+ },
+ "updated":{
+ "time":"2010-11-30T03:23:44Z"
+ }
+ },
+ {
+ "name":"lb-site2",
+ "id":72,
+ "created":{
+ "time":"2011-11-30T03:23:42Z"
+ },
+ "updated":{
+ "time":"2011-11-30T03:23:44Z"
+ }
+ },
+ {
+ "name":"lb-site3",
+ "id":73,
+ "created":{
+ "time":"2012-11-30T03:23:42Z"
+ },
+ "updated":{
+ "time":"2012-11-30T03:23:44Z"
+ }
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateLBResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "loadBalancer": {
+ "name": "a-new-loadbalancer",
+ "port": 80,
+ "protocol": "HTTP",
+ "virtualIps": [
+ {
+ "id": 2341
+ },
+ {
+ "id": 900001
+ }
+ ],
+ "nodes": [
+ {
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED"
+ }
+ ]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "loadBalancer": {
+ "name": "a-new-loadbalancer",
+ "id": 144,
+ "protocol": "HTTP",
+ "halfClosed": false,
+ "port": 83,
+ "algorithm": "RANDOM",
+ "status": "BUILD",
+ "timeout": 30,
+ "cluster": {
+ "name": "ztm-n01.staging1.lbaas.rackspace.net"
+ },
+ "nodes": [
+ {
+ "address": "10.1.1.1",
+ "id": 653,
+ "port": 80,
+ "status": "ONLINE",
+ "condition": "ENABLED",
+ "weight": 1
+ }
+ ],
+ "virtualIps": [
+ {
+ "address": "206.10.10.210",
+ "id": 39,
+ "type": "PUBLIC",
+ "ipVersion": "IPV4"
+ },
+ {
+ "address": "2001:4801:79f1:0002:711b:be4c:0000:0021",
+ "id": 900001,
+ "type": "PUBLIC",
+ "ipVersion": "IPV6"
+ }
+ ],
+ "created": {
+ "time": "2010-11-30T03:23:42Z"
+ },
+ "updated": {
+ "time": "2010-11-30T03:23:44Z"
+ },
+ "connectionLogging": {
+ "enabled": false
+ }
+ }
+}
+ `)
+ })
+}
+
+func mockBatchDeleteLBResponse(t *testing.T, ids []int) {
+ th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteLBResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockGetLBResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), 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": 2000,
+ "name": "sample-loadbalancer",
+ "protocol": "HTTP",
+ "port": 80,
+ "algorithm": "RANDOM",
+ "status": "ACTIVE",
+ "timeout": 30,
+ "connectionLogging": {
+ "enabled": true
+ },
+ "virtualIps": [
+ {
+ "id": 1000,
+ "address": "206.10.10.210",
+ "type": "PUBLIC",
+ "ipVersion": "IPV4"
+ }
+ ],
+ "nodes": [
+ {
+ "id": 1041,
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE"
+ },
+ {
+ "id": 1411,
+ "address": "10.1.1.2",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE"
+ }
+ ],
+ "sessionPersistence": {
+ "persistenceType": "HTTP_COOKIE"
+ },
+ "connectionThrottle": {
+ "maxConnections": 100
+ },
+ "cluster": {
+ "name": "c1.dfw1"
+ },
+ "created": {
+ "time": "2010-11-30T03:23:42Z"
+ },
+ "updated": {
+ "time": "2010-11-30T03:23:44Z"
+ },
+ "sourceAddresses": {
+ "ipv6Public": "2001:4801:79f1:1::1/64",
+ "ipv4Servicenet": "10.0.0.0",
+ "ipv4Public": "10.12.99.28"
+ }
+ }
+}
+ `)
+ })
+}
+
+func mockUpdateLBResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "loadBalancer": {
+ "name": "a-new-loadbalancer",
+ "protocol": "TCP",
+ "halfClosed": true,
+ "algorithm": "RANDOM",
+ "port": 8080,
+ "timeout": 100,
+ "httpsRedirect": false
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockListProtocolsResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers/protocols", 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, `
+{
+ "protocols": [
+ {
+ "name": "DNS_TCP",
+ "port": 53
+ },
+ {
+ "name": "DNS_UDP",
+ "port": 53
+ },
+ {
+ "name": "FTP",
+ "port": 21
+ },
+ {
+ "name": "HTTP",
+ "port": 80
+ },
+ {
+ "name": "HTTPS",
+ "port": 443
+ },
+ {
+ "name": "IMAPS",
+ "port": 993
+ },
+ {
+ "name": "IMAPv4",
+ "port": 143
+ },
+ {
+ "name": "LDAP",
+ "port": 389
+ },
+ {
+ "name": "LDAPS",
+ "port": 636
+ },
+ {
+ "name": "MYSQL",
+ "port": 3306
+ },
+ {
+ "name": "POP3",
+ "port": 110
+ },
+ {
+ "name": "POP3S",
+ "port": 995
+ },
+ {
+ "name": "SMTP",
+ "port": 25
+ },
+ {
+ "name": "TCP",
+ "port": 0
+ },
+ {
+ "name": "TCP_CLIENT_FIRST",
+ "port": 0
+ },
+ {
+ "name": "UDP",
+ "port": 0
+ },
+ {
+ "name": "UDP_STREAM",
+ "port": 0
+ },
+ {
+ "name": "SFTP",
+ "port": 22
+ },
+ {
+ "name": "TCP_STREAM",
+ "port": 0
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockListAlgorithmsResponse(t *testing.T) {
+ th.Mux.HandleFunc("/loadbalancers/algorithms", 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, `
+{
+ "algorithms": [
+ {
+ "name": "LEAST_CONNECTIONS"
+ },
+ {
+ "name": "RANDOM"
+ },
+ {
+ "name": "ROUND_ROBIN"
+ },
+ {
+ "name": "WEIGHTED_LEAST_CONNECTIONS"
+ },
+ {
+ "name": "WEIGHTED_ROUND_ROBIN"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockGetLoggingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", 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, `
+{
+ "connectionLogging": {
+ "enabled": true
+ }
+}
+ `)
+ })
+}
+
+func mockEnableLoggingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "connectionLogging":{
+ "enabled":true
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDisableLoggingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/connectionlogging", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "connectionLogging":{
+ "enabled":false
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockGetErrorPageResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", 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, `
+{
+ "errorpage": {
+ "content": "<html>DEFAULT ERROR PAGE</html>"
+ }
+}
+ `)
+ })
+}
+
+func mockSetErrorPageResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "errorpage": {
+ "content": "<html>New error page</html>"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "errorpage": {
+ "content": "<html>New error page</html>"
+ }
+}
+ `)
+ })
+}
+
+func mockDeleteErrorPageResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/errorpage", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func mockGetStatsResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/stats", 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, `
+{
+ "connectTimeOut": 10,
+ "connectError": 20,
+ "connectFailure": 30,
+ "dataTimedOut": 40,
+ "keepAliveTimedOut": 50,
+ "maxConn": 60,
+ "currentConn": 40,
+ "connectTimeOutSsl": 10,
+ "connectErrorSsl": 20,
+ "connectFailureSsl": 30,
+ "dataTimedOutSsl": 40,
+ "keepAliveTimedOutSsl": 50,
+ "maxConnSsl": 60,
+ "currentConnSsl": 40
+}
+ `)
+ })
+}
+
+func mockGetCachingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", 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, `
+{
+ "contentCaching": {
+ "enabled": true
+ }
+}
+ `)
+ })
+}
+
+func mockEnableCachingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "contentCaching":{
+ "enabled":true
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDisableCachingResponse(t *testing.T, id int) {
+ th.Mux.HandleFunc("/loadbalancers/"+strconv.Itoa(id)+"/contentcaching", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "contentCaching":{
+ "enabled":false
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/lbs/requests.go b/rackspace/lb/v1/lbs/requests.go
new file mode 100644
index 0000000..342f107
--- /dev/null
+++ b/rackspace/lb/v1/lbs/requests.go
@@ -0,0 +1,574 @@
+package lbs
+
+import (
+ "errors"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/monitors"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+)
+
+var (
+ errNameRequired = errors.New("Name is a required attribute")
+ errTimeoutExceeded = errors.New("Timeout must be less than 120")
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToLBListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListOpts struct {
+ ChangesSince string `q:"changes-since"`
+ Status Status `q:"status"`
+ NodeAddr string `q:"nodeaddress"`
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+}
+
+// ToLBListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToLBListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List is the operation responsible for returning a paginated collection of
+// load balancers. You may pass in a ListOpts struct to filter results.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(client)
+ if opts != nil {
+ query, err := opts.ToLBListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return LBPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToLBCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Required - name of the load balancer to create. The name must be 128
+ // characters or fewer in length, and all UTF-8 characters are valid.
+ Name string
+
+ // Optional - nodes to be added.
+ Nodes []nodes.Node
+
+ // Required - protocol of the service that is being load balanced.
+ // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+ // for a full list of supported protocols.
+ Protocol string
+
+ // Optional - enables or disables Half-Closed support for the load balancer.
+ // Half-Closed support provides the ability for one end of the connection to
+ // terminate its output, while still receiving data from the other end. Only
+ // available for TCP/TCP_CLIENT_FIRST protocols.
+ HalfClosed gophercloud.EnabledState
+
+ // Optional - the type of virtual IPs you want associated with the load
+ // balancer.
+ VIPs []vips.VIP
+
+ // Optional - the access list management feature allows fine-grained network
+ // access controls to be applied to the load balancer virtual IP address.
+ AccessList *acl.AccessList
+
+ // Optional - algorithm that defines how traffic should be directed between
+ // back-end nodes.
+ Algorithm string
+
+ // Optional - current connection logging configuration.
+ ConnectionLogging *ConnectionLogging
+
+ // Optional - specifies a limit on the number of connections per IP address
+ // to help mitigate malicious or abusive traffic to your applications.
+ ConnThrottle *throttle.ConnectionThrottle
+
+ // Optional - the type of health monitor check to perform to ensure that the
+ // service is performing properly.
+ HealthMonitor *monitors.Monitor
+
+ // Optional - arbitrary information that can be associated with each LB.
+ Metadata map[string]interface{}
+
+ // Optional - port number for the service you are load balancing.
+ Port int
+
+ // Optional - the timeout value for the load balancer and communications with
+ // its nodes. Defaults to 30 seconds with a maximum of 120 seconds.
+ Timeout int
+
+ // Optional - specifies whether multiple requests from clients are directed
+ // to the same node.
+ SessionPersistence *sessions.SessionPersistence
+
+ // Optional - enables or disables HTTP to HTTPS redirection for the load
+ // balancer. When enabled, any HTTP request returns status code 301 (Moved
+ // Permanently), and the requester is redirected to the requested URL via the
+ // HTTPS protocol on port 443. For example, http://example.com/page.html
+ // would be redirected to https://example.com/page.html. Only available for
+ // HTTPS protocol (port=443), or HTTP protocol with a properly configured SSL
+ // termination (secureTrafficOnly=true, securePort=443).
+ HTTPSRedirect gophercloud.EnabledState
+}
+
+// ToLBCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToLBCreateMap() (map[string]interface{}, error) {
+ lb := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return lb, errNameRequired
+ }
+ if opts.Timeout > 120 {
+ return lb, errTimeoutExceeded
+ }
+
+ lb["name"] = opts.Name
+
+ if len(opts.Nodes) > 0 {
+ nodes := []map[string]interface{}{}
+ for _, n := range opts.Nodes {
+ nodes = append(nodes, map[string]interface{}{
+ "address": n.Address,
+ "port": n.Port,
+ "condition": n.Condition,
+ })
+ }
+ lb["nodes"] = nodes
+ }
+
+ if opts.Protocol != "" {
+ lb["protocol"] = opts.Protocol
+ }
+ if opts.HalfClosed != nil {
+ lb["halfClosed"] = opts.HalfClosed
+ }
+ if len(opts.VIPs) > 0 {
+ lb["virtualIps"] = opts.VIPs
+ }
+ if opts.AccessList != nil {
+ lb["accessList"] = &opts.AccessList
+ }
+ if opts.Algorithm != "" {
+ lb["algorithm"] = opts.Algorithm
+ }
+ if opts.ConnectionLogging != nil {
+ lb["connectionLogging"] = &opts.ConnectionLogging
+ }
+ if opts.ConnThrottle != nil {
+ lb["connectionThrottle"] = &opts.ConnThrottle
+ }
+ if opts.HealthMonitor != nil {
+ lb["healthMonitor"] = &opts.HealthMonitor
+ }
+ if len(opts.Metadata) != 0 {
+ lb["metadata"] = opts.Metadata
+ }
+ if opts.Port > 0 {
+ lb["port"] = opts.Port
+ }
+ if opts.Timeout > 0 {
+ lb["timeout"] = opts.Timeout
+ }
+ if opts.SessionPersistence != nil {
+ lb["sessionPersistence"] = &opts.SessionPersistence
+ }
+ if opts.HTTPSRedirect != nil {
+ lb["httpsRedirect"] = &opts.HTTPSRedirect
+ }
+
+ return map[string]interface{}{"loadBalancer": lb}, nil
+}
+
+// Create is the operation responsible for asynchronously provisioning a new
+// load balancer based on the configuration defined in CreateOpts. Once the
+// request is validated and progress has started on the provisioning process, a
+// response struct is returned. When extracted (with Extract()), you have
+// to the load balancer's unique ID and status.
+//
+// Once an ID is attained, you can check on the progress of the operation by
+// calling Get and passing in the ID. If the corresponding request cannot be
+// fulfilled due to insufficient or invalid data, an HTTP 400 (Bad Request)
+// error response is returned with information regarding the nature of the
+// failure in the body of the response. Failures in the validation process are
+// non-recoverable and require the caller to correct the cause of the failure.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToLBCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for providing detailed information
+// regarding a specific load balancer which is configured and associated with
+// your account. This operation is not capable of returning details for a load
+// balancer which has been deleted.
+func Get(c *gophercloud.ServiceClient, id int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// BulkDelete removes all the load balancers referenced in the slice of IDs.
+// Any and all configuration data associated with these load balancers is
+// immediately purged and is not recoverable.
+//
+// If one of the items in the list cannot be removed due to its current status,
+// a 400 Bad Request error is returned along with the IDs of the ones the
+// system identified as potential failures for this request.
+func BulkDelete(c *gophercloud.ServiceClient, ids []int) DeleteResult {
+ var res DeleteResult
+
+ if len(ids) > 10 || len(ids) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 LB IDs")
+ return res
+ }
+
+ url := rootURL(c)
+ url += gophercloud.IDSliceToQueryString("id", ids)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete removes a single load balancer.
+func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder represents a type that can be converted into a JSON-like
+// map structure.
+type UpdateOptsBuilder interface {
+ ToLBUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the options for updating an existing load balancer.
+type UpdateOpts struct {
+ // Optional - new name of the load balancer.
+ Name string
+
+ // Optional - the new protocol you want your load balancer to have.
+ // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+ // for a full list of supported protocols.
+ Protocol string
+
+ // Optional - see the HalfClosed field in CreateOpts for more information.
+ HalfClosed gophercloud.EnabledState
+
+ // Optional - see the Algorithm field in CreateOpts for more information.
+ Algorithm string
+
+ // Optional - see the Port field in CreateOpts for more information.
+ Port int
+
+ // Optional - see the Timeout field in CreateOpts for more information.
+ Timeout int
+
+ // Optional - see the HTTPSRedirect field in CreateOpts for more information.
+ HTTPSRedirect gophercloud.EnabledState
+}
+
+// ToLBUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToLBUpdateMap() (map[string]interface{}, error) {
+ lb := make(map[string]interface{})
+
+ if opts.Name != "" {
+ lb["name"] = opts.Name
+ }
+ if opts.Protocol != "" {
+ lb["protocol"] = opts.Protocol
+ }
+ if opts.HalfClosed != nil {
+ lb["halfClosed"] = opts.HalfClosed
+ }
+ if opts.Algorithm != "" {
+ lb["algorithm"] = opts.Algorithm
+ }
+ if opts.Port > 0 {
+ lb["port"] = opts.Port
+ }
+ if opts.Timeout > 0 {
+ lb["timeout"] = opts.Timeout
+ }
+ if opts.HTTPSRedirect != nil {
+ lb["httpsRedirect"] = &opts.HTTPSRedirect
+ }
+
+ return map[string]interface{}{"loadBalancer": lb}, nil
+}
+
+// Update is the operation responsible for asynchronously updating the
+// attributes of a specific load balancer. Upon successful validation of the
+// request, the service returns a 202 Accepted response, and the load balancer
+// enters a PENDING_UPDATE state. A user can poll the load balancer with Get to
+// wait for the changes to be applied. When this happens, the load balancer will
+// return to an ACTIVE state.
+func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToLBUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// ListProtocols is the operation responsible for returning a paginated
+// collection of load balancer protocols.
+func ListProtocols(client *gophercloud.ServiceClient) pagination.Pager {
+ url := protocolsURL(client)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return ProtocolPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// ListAlgorithms is the operation responsible for returning a paginated
+// collection of load balancer algorithms.
+func ListAlgorithms(client *gophercloud.ServiceClient) pagination.Pager {
+ url := algorithmsURL(client)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return AlgorithmPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// IsLoggingEnabled returns true if the load balancer has connection logging
+// enabled and false if not.
+func IsLoggingEnabled(client *gophercloud.ServiceClient, id int) (bool, error) {
+ var body interface{}
+
+ _, err := perigee.Request("GET", loggingURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &body,
+ OkCodes: []int{200},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ var resp struct {
+ CL struct {
+ Enabled bool `mapstructure:"enabled"`
+ } `mapstructure:"connectionLogging"`
+ }
+
+ err = mapstructure.Decode(body, &resp)
+ return resp.CL.Enabled, err
+}
+
+func toConnLoggingMap(state bool) map[string]map[string]bool {
+ return map[string]map[string]bool{
+ "connectionLogging": map[string]bool{"enabled": state},
+ }
+}
+
+// EnableLogging will enable connection logging for a specified load balancer.
+func EnableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toConnLoggingMap(true)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DisableLogging will disable connection logging for a specified load balancer.
+func DisableLogging(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toConnLoggingMap(false)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", loggingURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// GetErrorPage will retrieve the current error page for the load balancer.
+func GetErrorPage(client *gophercloud.ServiceClient, id int) ErrorPageResult {
+ var res ErrorPageResult
+
+ _, res.Err = perigee.Request("GET", errorPageURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// SetErrorPage will set the HTML of the load balancer's error page to a
+// specific value.
+func SetErrorPage(client *gophercloud.ServiceClient, id int, html string) ErrorPageResult {
+ var res ErrorPageResult
+
+ type stringMap map[string]string
+ reqBody := map[string]stringMap{"errorpage": stringMap{"content": html}}
+
+ _, res.Err = perigee.Request("PUT", errorPageURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ ReqBody: &reqBody,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// DeleteErrorPage will delete the current error page for the load balancer.
+func DeleteErrorPage(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("DELETE", errorPageURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// GetStats will retrieve detailed stats related to the load balancer's usage.
+func GetStats(client *gophercloud.ServiceClient, id int) StatsResult {
+ var res StatsResult
+
+ _, res.Err = perigee.Request("GET", statsURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// IsContentCached will check to see whether the specified load balancer caches
+// content. When content caching is enabled, recently-accessed files are stored
+// on the load balancer for easy retrieval by web clients. Content caching
+// improves the performance of high traffic web sites by temporarily storing
+// data that was recently accessed. While it's cached, requests for that data
+// are served by the load balancer, which in turn reduces load off the back-end
+// nodes. The result is improved response times for those requests and less
+// load on the web server.
+func IsContentCached(client *gophercloud.ServiceClient, id int) (bool, error) {
+ var body interface{}
+
+ _, err := perigee.Request("GET", cacheURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ Results: &body,
+ OkCodes: []int{200},
+ })
+ if err != nil {
+ return false, err
+ }
+
+ var resp struct {
+ CC struct {
+ Enabled bool `mapstructure:"enabled"`
+ } `mapstructure:"contentCaching"`
+ }
+
+ err = mapstructure.Decode(body, &resp)
+ return resp.CC.Enabled, err
+}
+
+func toCachingMap(state bool) map[string]map[string]bool {
+ return map[string]map[string]bool{
+ "contentCaching": map[string]bool{"enabled": state},
+ }
+}
+
+// EnableCaching will enable content-caching for the specified load balancer.
+func EnableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toCachingMap(true)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DisableCaching will disable content-caching for the specified load balancer.
+func DisableCaching(client *gophercloud.ServiceClient, id int) gophercloud.ErrResult {
+ reqBody := toCachingMap(false)
+ var res gophercloud.ErrResult
+
+ _, res.Err = perigee.Request("PUT", cacheURL(client, id), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/lbs/requests_test.go b/rackspace/lb/v1/lbs/requests_test.go
new file mode 100644
index 0000000..a8ec19e
--- /dev/null
+++ b/rackspace/lb/v1/lbs/requests_test.go
@@ -0,0 +1,438 @@
+package lbs
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ id1 = 12345
+ id2 = 67890
+ ts1 = "2010-11-30T03:23:42Z"
+ ts2 = "2010-11-30T03:23:44Z"
+)
+
+func toTime(t *testing.T, str string) time.Time {
+ ts, err := time.Parse(time.RFC3339, str)
+ if err != nil {
+ t.Fatalf("Could not parse time: %s", err.Error())
+ }
+ return ts
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListLBResponse(t)
+
+ count := 0
+
+ err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractLBs(page)
+ th.AssertNoErr(t, err)
+
+ expected := []LoadBalancer{
+ LoadBalancer{
+ Name: "lb-site1",
+ ID: 71,
+ Protocol: "HTTP",
+ Port: 80,
+ Algorithm: "RANDOM",
+ Status: ACTIVE,
+ NodeCount: 3,
+ VIPs: []vips.VIP{
+ vips.VIP{
+ ID: 403,
+ Address: "206.55.130.1",
+ Type: "PUBLIC",
+ Version: "IPV4",
+ },
+ },
+ Created: Datetime{Time: toTime(t, ts1)},
+ Updated: Datetime{Time: toTime(t, ts2)},
+ },
+ LoadBalancer{
+ ID: 72,
+ Name: "lb-site2",
+ Created: Datetime{Time: toTime(t, "2011-11-30T03:23:42Z")},
+ Updated: Datetime{Time: toTime(t, "2011-11-30T03:23:44Z")},
+ },
+ LoadBalancer{
+ ID: 73,
+ Name: "lb-site3",
+ Created: Datetime{Time: toTime(t, "2012-11-30T03:23:42Z")},
+ Updated: Datetime{Time: toTime(t, "2012-11-30T03:23:44Z")},
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateLBResponse(t)
+
+ opts := CreateOpts{
+ Name: "a-new-loadbalancer",
+ Port: 80,
+ Protocol: "HTTP",
+ VIPs: []vips.VIP{
+ vips.VIP{ID: 2341},
+ vips.VIP{ID: 900001},
+ },
+ Nodes: []nodes.Node{
+ nodes.Node{Address: "10.1.1.1", Port: 80, Condition: "ENABLED"},
+ },
+ }
+
+ lb, err := Create(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &LoadBalancer{
+ Name: "a-new-loadbalancer",
+ ID: 144,
+ Protocol: "HTTP",
+ HalfClosed: false,
+ Port: 83,
+ Algorithm: "RANDOM",
+ Status: BUILD,
+ Timeout: 30,
+ Cluster: Cluster{Name: "ztm-n01.staging1.lbaas.rackspace.net"},
+ Nodes: []nodes.Node{
+ nodes.Node{
+ Address: "10.1.1.1",
+ ID: 653,
+ Port: 80,
+ Status: "ONLINE",
+ Condition: "ENABLED",
+ Weight: 1,
+ },
+ },
+ VIPs: []vips.VIP{
+ vips.VIP{
+ ID: 39,
+ Address: "206.10.10.210",
+ Type: vips.PUBLIC,
+ Version: vips.IPV4,
+ },
+ vips.VIP{
+ ID: 900001,
+ Address: "2001:4801:79f1:0002:711b:be4c:0000:0021",
+ Type: vips.PUBLIC,
+ Version: vips.IPV6,
+ },
+ },
+ Created: Datetime{Time: toTime(t, ts1)},
+ Updated: Datetime{Time: toTime(t, ts2)},
+ ConnectionLogging: ConnectionLogging{Enabled: false},
+ }
+
+ th.AssertDeepEquals(t, expected, lb)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{id1, id2}
+
+ mockBatchDeleteLBResponse(t, ids)
+
+ err := BulkDelete(client.ServiceClient(), ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteLBResponse(t, id1)
+
+ err := Delete(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetLBResponse(t, id1)
+
+ lb, err := Get(client.ServiceClient(), id1).Extract()
+
+ expected := &LoadBalancer{
+ Name: "sample-loadbalancer",
+ ID: 2000,
+ Protocol: "HTTP",
+ Port: 80,
+ Algorithm: "RANDOM",
+ Status: ACTIVE,
+ Timeout: 30,
+ ConnectionLogging: ConnectionLogging{Enabled: true},
+ VIPs: []vips.VIP{
+ vips.VIP{
+ ID: 1000,
+ Address: "206.10.10.210",
+ Type: "PUBLIC",
+ Version: "IPV4",
+ },
+ },
+ Nodes: []nodes.Node{
+ nodes.Node{
+ Address: "10.1.1.1",
+ ID: 1041,
+ Port: 80,
+ Status: "ONLINE",
+ Condition: "ENABLED",
+ },
+ nodes.Node{
+ Address: "10.1.1.2",
+ ID: 1411,
+ Port: 80,
+ Status: "ONLINE",
+ Condition: "ENABLED",
+ },
+ },
+ SessionPersistence: sessions.SessionPersistence{Type: "HTTP_COOKIE"},
+ ConnectionThrottle: throttle.ConnectionThrottle{MaxConnections: 100},
+ Cluster: Cluster{Name: "c1.dfw1"},
+ Created: Datetime{Time: toTime(t, ts1)},
+ Updated: Datetime{Time: toTime(t, ts2)},
+ SourceAddrs: SourceAddrs{
+ IPv4Public: "10.12.99.28",
+ IPv4Private: "10.0.0.0",
+ IPv6Public: "2001:4801:79f1:1::1/64",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, lb)
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateLBResponse(t, id1)
+
+ opts := UpdateOpts{
+ Name: "a-new-loadbalancer",
+ Protocol: "TCP",
+ HalfClosed: gophercloud.Enabled,
+ Algorithm: "RANDOM",
+ Port: 8080,
+ Timeout: 100,
+ HTTPSRedirect: gophercloud.Disabled,
+ }
+
+ err := Update(client.ServiceClient(), id1, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListProtocols(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListProtocolsResponse(t)
+
+ count := 0
+
+ err := ListProtocols(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractProtocols(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Protocol{
+ Protocol{Name: "DNS_TCP", Port: 53},
+ Protocol{Name: "DNS_UDP", Port: 53},
+ Protocol{Name: "FTP", Port: 21},
+ Protocol{Name: "HTTP", Port: 80},
+ Protocol{Name: "HTTPS", Port: 443},
+ Protocol{Name: "IMAPS", Port: 993},
+ Protocol{Name: "IMAPv4", Port: 143},
+ }
+
+ th.CheckDeepEquals(t, expected[0:7], actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestListAlgorithms(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListAlgorithmsResponse(t)
+
+ count := 0
+
+ err := ListAlgorithms(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAlgorithms(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Algorithm{
+ Algorithm{Name: "LEAST_CONNECTIONS"},
+ Algorithm{Name: "RANDOM"},
+ Algorithm{Name: "ROUND_ROBIN"},
+ Algorithm{Name: "WEIGHTED_LEAST_CONNECTIONS"},
+ Algorithm{Name: "WEIGHTED_ROUND_ROBIN"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestIsLoggingEnabled(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetLoggingResponse(t, id1)
+
+ res, err := IsLoggingEnabled(client.ServiceClient(), id1)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, res)
+}
+
+func TestEnablingLogging(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockEnableLoggingResponse(t, id1)
+
+ err := EnableLogging(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDisablingLogging(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDisableLoggingResponse(t, id1)
+
+ err := DisableLogging(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetErrorPage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetErrorPageResponse(t, id1)
+
+ content, err := GetErrorPage(client.ServiceClient(), id1).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &ErrorPage{Content: "<html>DEFAULT ERROR PAGE</html>"}
+ th.AssertDeepEquals(t, expected, content)
+}
+
+func TestSetErrorPage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockSetErrorPageResponse(t, id1)
+
+ html := "<html>New error page</html>"
+ content, err := SetErrorPage(client.ServiceClient(), id1, html).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &ErrorPage{Content: html}
+ th.AssertDeepEquals(t, expected, content)
+}
+
+func TestDeleteErrorPage(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteErrorPageResponse(t, id1)
+
+ err := DeleteErrorPage(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetStats(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetStatsResponse(t, id1)
+
+ content, err := GetStats(client.ServiceClient(), id1).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Stats{
+ ConnectTimeout: 10,
+ ConnectError: 20,
+ ConnectFailure: 30,
+ DataTimedOut: 40,
+ KeepAliveTimedOut: 50,
+ MaxConnections: 60,
+ CurrentConnections: 40,
+ SSLConnectTimeout: 10,
+ SSLConnectError: 20,
+ SSLConnectFailure: 30,
+ SSLDataTimedOut: 40,
+ SSLKeepAliveTimedOut: 50,
+ SSLMaxConnections: 60,
+ SSLCurrentConnections: 40,
+ }
+ th.AssertDeepEquals(t, expected, content)
+}
+
+func TestIsCached(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetCachingResponse(t, id1)
+
+ res, err := IsContentCached(client.ServiceClient(), id1)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, res)
+}
+
+func TestEnablingCaching(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockEnableCachingResponse(t, id1)
+
+ err := EnableCaching(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDisablingCaching(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDisableCachingResponse(t, id1)
+
+ err := DisableCaching(client.ServiceClient(), id1).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/lbs/results.go b/rackspace/lb/v1/lbs/results.go
new file mode 100644
index 0000000..bc475a9
--- /dev/null
+++ b/rackspace/lb/v1/lbs/results.go
@@ -0,0 +1,420 @@
+package lbs
+
+import (
+ "reflect"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/acl"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/nodes"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/sessions"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/throttle"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1/vips"
+)
+
+// Protocol represents the network protocol which the load balancer accepts.
+type Protocol struct {
+ // The name of the protocol, e.g. HTTP, LDAP, FTP, etc.
+ Name string
+
+ // The port number for the protocol.
+ Port int
+}
+
+// Algorithm defines how traffic should be directed between back-end nodes.
+type Algorithm struct {
+ // The name of the algorithm, e.g RANDOM, ROUND_ROBIN, etc.
+ Name string
+}
+
+// Status represents the potential state of a load balancer resource.
+type Status string
+
+const (
+ // ACTIVE indicates that the LB is configured properly and ready to serve
+ // traffic to incoming requests via the configured virtual IPs.
+ ACTIVE Status = "ACTIVE"
+
+ // BUILD indicates that the LB is being provisioned for the first time and
+ // configuration is being applied to bring the service online. The service
+ // cannot yet serve incoming requests.
+ BUILD Status = "BUILD"
+
+ // PENDINGUPDATE indicates that the LB is online but configuration changes
+ // are being applied to update the service based on a previous request.
+ PENDINGUPDATE Status = "PENDING_UPDATE"
+
+ // PENDINGDELETE indicates that the LB is online but configuration changes
+ // are being applied to begin deletion of the service based on a previous
+ // request.
+ PENDINGDELETE Status = "PENDING_DELETE"
+
+ // SUSPENDED indicates that the LB has been taken offline and disabled.
+ SUSPENDED Status = "SUSPENDED"
+
+ // ERROR indicates that the system encountered an error when attempting to
+ // configure the load balancer.
+ ERROR Status = "ERROR"
+
+ // DELETED indicates that the LB has been deleted.
+ DELETED Status = "DELETED"
+)
+
+// Datetime represents the structure of a Created or Updated field.
+type Datetime struct {
+ Time time.Time `mapstructure:"-"`
+}
+
+// LoadBalancer represents a load balancer API resource.
+type LoadBalancer struct {
+ // Human-readable name for the load balancer.
+ Name string
+
+ // The unique ID for the load balancer.
+ ID int
+
+ // Represents the service protocol being load balanced. See Protocol type for
+ // a list of accepted values.
+ // See http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/protocols.html
+ // for a full list of supported protocols.
+ Protocol string
+
+ // Defines how traffic should be directed between back-end nodes. The default
+ // algorithm is RANDOM. See Algorithm type for a list of accepted values.
+ Algorithm string
+
+ // The current status of the load balancer.
+ Status Status
+
+ // The number of load balancer nodes.
+ NodeCount int `mapstructure:"nodeCount"`
+
+ // Slice of virtual IPs associated with this load balancer.
+ VIPs []vips.VIP `mapstructure:"virtualIps"`
+
+ // Datetime when the LB was created.
+ Created Datetime
+
+ // Datetime when the LB was created.
+ Updated Datetime
+
+ // Port number for the service you are load balancing.
+ Port int
+
+ // HalfClosed provides the ability for one end of the connection to
+ // terminate its output while still receiving data from the other end. This
+ // is only available on TCP/TCP_CLIENT_FIRST protocols.
+ HalfClosed bool
+
+ // Timeout represents the timeout value between a load balancer and its
+ // nodes. Defaults to 30 seconds with a maximum of 120 seconds.
+ Timeout int
+
+ // The cluster name.
+ Cluster Cluster
+
+ // Nodes shows all the back-end nodes which are associated with the load
+ // balancer. These are the devices which are delivered traffic.
+ Nodes []nodes.Node
+
+ // Current connection logging configuration.
+ ConnectionLogging ConnectionLogging
+
+ // SessionPersistence specifies whether multiple requests from clients are
+ // directed to the same node.
+ SessionPersistence sessions.SessionPersistence
+
+ // ConnectionThrottle specifies a limit on the number of connections per IP
+ // address to help mitigate malicious or abusive traffic to your applications.
+ ConnectionThrottle throttle.ConnectionThrottle
+
+ // The source public and private IP addresses.
+ SourceAddrs SourceAddrs `mapstructure:"sourceAddresses"`
+
+ // Represents the access rules for this particular load balancer. IP addresses
+ // or subnet ranges, depending on their type (ALLOW or DENY), can be permitted
+ // or blocked.
+ AccessList acl.AccessList
+}
+
+// SourceAddrs represents the source public and private IP addresses.
+type SourceAddrs struct {
+ IPv4Public string `json:"ipv4Public" mapstructure:"ipv4Public"`
+ IPv4Private string `json:"ipv4Servicenet" mapstructure:"ipv4Servicenet"`
+ IPv6Public string `json:"ipv6Public" mapstructure:"ipv6Public"`
+ IPv6Private string `json:"ipv6Servicenet" mapstructure:"ipv6Servicenet"`
+}
+
+// ConnectionLogging - temp
+type ConnectionLogging struct {
+ Enabled bool
+}
+
+// Cluster - temp
+type Cluster struct {
+ Name string
+}
+
+// LBPage is the page returned by a pager when traversing over a collection of
+// LBs.
+type LBPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p LBPage) IsEmpty() (bool, error) {
+ is, err := ExtractLBs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractLBs accepts a Page struct, specifically a LBPage struct, and extracts
+// the elements into a slice of LoadBalancer structs. In other words, a generic
+// collection is mapped into a relevant slice.
+func ExtractLBs(page pagination.Page) ([]LoadBalancer, error) {
+ var resp struct {
+ LBs []LoadBalancer `mapstructure:"loadBalancers" json:"loadBalancers"`
+ }
+
+ coll := page.(LBPage).Body
+ err := mapstructure.Decode(coll, &resp)
+
+ s := reflect.ValueOf(coll.(map[string]interface{})["loadBalancers"])
+
+ for i := 0; i < s.Len(); i++ {
+ val := (s.Index(i).Interface()).(map[string]interface{})
+
+ ts, err := extractTS(val, "created")
+ if err != nil {
+ return resp.LBs, err
+ }
+ resp.LBs[i].Created.Time = ts
+
+ ts, err = extractTS(val, "updated")
+ if err != nil {
+ return resp.LBs, err
+ }
+ resp.LBs[i].Updated.Time = ts
+ }
+
+ return resp.LBs, err
+}
+
+func extractTS(body map[string]interface{}, key string) (time.Time, error) {
+ val := body[key].(map[string]interface{})
+ return time.Parse(time.RFC3339, val["time"].(string))
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as a LB, if possible.
+func (r commonResult) Extract() (*LoadBalancer, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ LB LoadBalancer `mapstructure:"loadBalancer"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ json := r.Body.(map[string]interface{})
+ lb := json["loadBalancer"].(map[string]interface{})
+
+ ts, err := extractTS(lb, "created")
+ if err != nil {
+ return nil, err
+ }
+ response.LB.Created.Time = ts
+
+ ts, err = extractTS(lb, "updated")
+ if err != nil {
+ return nil, err
+ }
+ response.LB.Updated.Time = ts
+
+ return &response.LB, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// ProtocolPage is the page returned by a pager when traversing over a
+// collection of LB protocols.
+type ProtocolPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a ProtocolPage struct is empty.
+func (p ProtocolPage) IsEmpty() (bool, error) {
+ is, err := ExtractProtocols(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractProtocols accepts a Page struct, specifically a ProtocolPage struct,
+// and extracts the elements into a slice of Protocol structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractProtocols(page pagination.Page) ([]Protocol, error) {
+ var resp struct {
+ Protocols []Protocol `mapstructure:"protocols" json:"protocols"`
+ }
+ err := mapstructure.Decode(page.(ProtocolPage).Body, &resp)
+ return resp.Protocols, err
+}
+
+// AlgorithmPage is the page returned by a pager when traversing over a
+// collection of LB algorithms.
+type AlgorithmPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AlgorithmPage struct is empty.
+func (p AlgorithmPage) IsEmpty() (bool, error) {
+ is, err := ExtractAlgorithms(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAlgorithms accepts a Page struct, specifically a AlgorithmPage struct,
+// and extracts the elements into a slice of Algorithm structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractAlgorithms(page pagination.Page) ([]Algorithm, error) {
+ var resp struct {
+ Algorithms []Algorithm `mapstructure:"algorithms" json:"algorithms"`
+ }
+ err := mapstructure.Decode(page.(AlgorithmPage).Body, &resp)
+ return resp.Algorithms, err
+}
+
+// ErrorPage represents the HTML file that is shown to an end user who is
+// attempting to access a load balancer node that is offline/unavailable.
+//
+// During provisioning, every load balancer is configured with a default error
+// page that gets displayed when traffic is requested for an offline node.
+//
+// You can add a single custom error page with an HTTP-based protocol to a load
+// balancer. Page updates override existing content. If a custom error page is
+// deleted, or the load balancer is changed to a non-HTTP protocol, the default
+// error page is restored.
+type ErrorPage struct {
+ Content string
+}
+
+// ErrorPageResult represents the result of an error page operation -
+// specifically getting or creating one.
+type ErrorPageResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as an ErrorPage, if possible.
+func (r ErrorPageResult) Extract() (*ErrorPage, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ ErrorPage ErrorPage `mapstructure:"errorpage"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.ErrorPage, err
+}
+
+// Stats represents all the key information about a load balancer's usage.
+type Stats struct {
+ // The number of connections closed by this load balancer because its
+ // ConnectTimeout interval was exceeded.
+ ConnectTimeout int `mapstructure:"connectTimeOut"`
+
+ // The number of transaction or protocol errors for this load balancer.
+ ConnectError int
+
+ // Number of connection failures for this load balancer.
+ ConnectFailure int
+
+ // Number of connections closed by this load balancer because its Timeout
+ // interval was exceeded.
+ DataTimedOut int
+
+ // Number of connections closed by this load balancer because the
+ // 'keepalive_timeout' interval was exceeded.
+ KeepAliveTimedOut int
+
+ // The maximum number of simultaneous TCP connections this load balancer has
+ // processed at any one time.
+ MaxConnections int `mapstructure:"maxConn"`
+
+ // Number of simultaneous connections active at the time of the request.
+ CurrentConnections int `mapstructure:"currentConn"`
+
+ // Number of SSL connections closed by this load balancer because the
+ // ConnectTimeout interval was exceeded.
+ SSLConnectTimeout int `mapstructure:"connectTimeOutSsl"`
+
+ // Number of SSL transaction or protocol erros in this load balancer.
+ SSLConnectError int `mapstructure:"connectErrorSsl"`
+
+ // Number of SSL connection failures in this load balancer.
+ SSLConnectFailure int `mapstructure:"connectFailureSsl"`
+
+ // Number of SSL connections closed by this load balancer because the
+ // Timeout interval was exceeded.
+ SSLDataTimedOut int `mapstructure:"dataTimedOutSsl"`
+
+ // Number of SSL connections closed by this load balancer because the
+ // 'keepalive_timeout' interval was exceeded.
+ SSLKeepAliveTimedOut int `mapstructure:"keepAliveTimedOutSsl"`
+
+ // Maximum number of simultaneous SSL connections this load balancer has
+ // processed a any one time.
+ SSLMaxConnections int `mapstructure:"maxConnSsl"`
+
+ // Number of simultaneous SSL connections active at the time of the request.
+ SSLCurrentConnections int `mapstructure:"currentConnSsl"`
+}
+
+// StatsResult represents the result of a Stats operation.
+type StatsResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any commonResult as a Stats struct, if possible.
+func (r StatsResult) Extract() (*Stats, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ res := &Stats{}
+ err := mapstructure.Decode(r.Body, res)
+ return res, err
+}
diff --git a/rackspace/lb/v1/lbs/urls.go b/rackspace/lb/v1/lbs/urls.go
new file mode 100644
index 0000000..471a86b
--- /dev/null
+++ b/rackspace/lb/v1/lbs/urls.go
@@ -0,0 +1,49 @@
+package lbs
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ protocolsPath = "protocols"
+ algorithmsPath = "algorithms"
+ logPath = "connectionlogging"
+ epPath = "errorpage"
+ stPath = "stats"
+ cachePath = "contentcaching"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id))
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(path)
+}
+
+func protocolsURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(path, protocolsPath)
+}
+
+func algorithmsURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(path, algorithmsPath)
+}
+
+func loggingURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), logPath)
+}
+
+func errorPageURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), epPath)
+}
+
+func statsURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), stPath)
+}
+
+func cacheURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), cachePath)
+}
diff --git a/rackspace/lb/v1/monitors/doc.go b/rackspace/lb/v1/monitors/doc.go
new file mode 100644
index 0000000..2c5be75
--- /dev/null
+++ b/rackspace/lb/v1/monitors/doc.go
@@ -0,0 +1,21 @@
+/*
+Package monitors provides information and interaction with the Health Monitor
+API resource for the Rackspace Cloud Load Balancer service.
+
+The load balancing service includes a health monitoring resource that
+periodically checks your back-end nodes to ensure they are responding correctly.
+If a node does not respond, it is removed from rotation until the health monitor
+determines that the node is functional. In addition to being performed
+periodically, a health check also executes against every new node that is
+added, to ensure that the node is operating properly before allowing it to
+service traffic. Only one health monitor is allowed to be enabled on a load
+balancer at a time.
+
+As part of a good strategy for monitoring connections, secondary nodes should
+also be created which provide failover for effectively routing traffic in case
+the primary node fails. This is an additional feature that ensures that you
+remain up in case your primary node fails.
+
+There are three types of health monitor: CONNECT, HTTP and HTTPS.
+*/
+package monitors
diff --git a/rackspace/lb/v1/monitors/fixtures.go b/rackspace/lb/v1/monitors/fixtures.go
new file mode 100644
index 0000000..a565abc
--- /dev/null
+++ b/rackspace/lb/v1/monitors/fixtures.go
@@ -0,0 +1,87 @@
+package monitors
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/healthmonitor"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "healthMonitor": {
+ "type": "CONNECT",
+ "delay": 10,
+ "timeout": 10,
+ "attemptsBeforeDeactivation": 3
+ }
+}
+ `)
+ })
+}
+
+func mockUpdateConnectResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "healthMonitor": {
+ "type": "CONNECT",
+ "delay": 10,
+ "timeout": 10,
+ "attemptsBeforeDeactivation": 3
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockUpdateHTTPResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "healthMonitor": {
+ "attemptsBeforeDeactivation": 3,
+ "bodyRegex": "{regex}",
+ "delay": 10,
+ "path": "/foo",
+ "statusRegex": "200",
+ "timeout": 10,
+ "type": "HTTPS"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/monitors/requests.go b/rackspace/lb/v1/monitors/requests.go
new file mode 100644
index 0000000..cfc35d2
--- /dev/null
+++ b/rackspace/lb/v1/monitors/requests.go
@@ -0,0 +1,178 @@
+package monitors
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+)
+
+var (
+ errAttemptLimit = errors.New("AttemptLimit field must be an int greater than 1 and less than 10")
+ errDelay = errors.New("Delay field must be an int greater than 1 and less than 10")
+ errTimeout = errors.New("Timeout field must be an int greater than 1 and less than 10")
+)
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package.
+type UpdateOptsBuilder interface {
+ ToMonitorUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateConnectMonitorOpts represents the options needed to update a CONNECT
+// monitor.
+type UpdateConnectMonitorOpts struct {
+ // Required - number of permissible monitor failures before removing a node
+ // from rotation. Must be a number between 1 and 10.
+ AttemptLimit int
+
+ // Required - the minimum number of seconds to wait before executing the
+ // health monitor. Must be a number between 1 and 3600.
+ Delay int
+
+ // Required - maximum number of seconds to wait for a connection to be
+ // established before timing out. Must be a number between 1 and 300.
+ Timeout int
+}
+
+// ToMonitorUpdateMap produces a map for updating CONNECT monitors.
+func (opts UpdateConnectMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+ type m map[string]interface{}
+
+ if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
+ return m{}, errAttemptLimit
+ }
+ if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
+ return m{}, errDelay
+ }
+ if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
+ return m{}, errTimeout
+ }
+
+ return m{"healthMonitor": m{
+ "attemptsBeforeDeactivation": opts.AttemptLimit,
+ "delay": opts.Delay,
+ "timeout": opts.Timeout,
+ "type": CONNECT,
+ }}, nil
+}
+
+// UpdateHTTPMonitorOpts represents the options needed to update a HTTP monitor.
+type UpdateHTTPMonitorOpts struct {
+ // Required - number of permissible monitor failures before removing a node
+ // from rotation. Must be a number between 1 and 10.
+ AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
+
+ // Required - the minimum number of seconds to wait before executing the
+ // health monitor. Must be a number between 1 and 3600.
+ Delay int
+
+ // Required - maximum number of seconds to wait for a connection to be
+ // established before timing out. Must be a number between 1 and 300.
+ Timeout int
+
+ // Required - a regular expression that will be used to evaluate the contents
+ // of the body of the response.
+ BodyRegex string
+
+ // Required - the HTTP path that will be used in the sample request.
+ Path string
+
+ // Required - a regular expression that will be used to evaluate the HTTP
+ // status code returned in the response.
+ StatusRegex string
+
+ // Optional - the name of a host for which the health monitors will check.
+ HostHeader string
+
+ // Required - either HTTP or HTTPS
+ Type Type
+}
+
+// ToMonitorUpdateMap produces a map for updating HTTP(S) monitors.
+func (opts UpdateHTTPMonitorOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+ type m map[string]interface{}
+
+ if !gophercloud.IntWithinRange(opts.AttemptLimit, 1, 10) {
+ return m{}, errAttemptLimit
+ }
+ if !gophercloud.IntWithinRange(opts.Delay, 1, 3600) {
+ return m{}, errDelay
+ }
+ if !gophercloud.IntWithinRange(opts.Timeout, 1, 300) {
+ return m{}, errTimeout
+ }
+ if opts.Type != HTTP && opts.Type != HTTPS {
+ return m{}, errors.New("Type must either by HTTP or HTTPS")
+ }
+ if opts.BodyRegex == "" {
+ return m{}, errors.New("BodyRegex is a required field")
+ }
+ if opts.Path == "" {
+ return m{}, errors.New("Path is a required field")
+ }
+ if opts.StatusRegex == "" {
+ return m{}, errors.New("StatusRegex is a required field")
+ }
+
+ json := m{
+ "attemptsBeforeDeactivation": opts.AttemptLimit,
+ "delay": opts.Delay,
+ "timeout": opts.Timeout,
+ "type": opts.Type,
+ "bodyRegex": opts.BodyRegex,
+ "path": opts.Path,
+ "statusRegex": opts.StatusRegex,
+ }
+
+ if opts.HostHeader != "" {
+ json["hostHeader"] = opts.HostHeader
+ }
+
+ return m{"healthMonitor": json}, nil
+}
+
+// Update is the operation responsible for updating a health monitor.
+func Update(c *gophercloud.ServiceClient, id int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToMonitorUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing details of a health monitor.
+func Get(c *gophercloud.ServiceClient, id int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for deleting a health monitor.
+func Delete(c *gophercloud.ServiceClient, id int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, id), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/monitors/requests_test.go b/rackspace/lb/v1/monitors/requests_test.go
new file mode 100644
index 0000000..76a60db
--- /dev/null
+++ b/rackspace/lb/v1/monitors/requests_test.go
@@ -0,0 +1,75 @@
+package monitors
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestUpdateCONNECT(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateConnectResponse(t, lbID)
+
+ opts := UpdateConnectMonitorOpts{
+ AttemptLimit: 3,
+ Delay: 10,
+ Timeout: 10,
+ }
+
+ err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdateHTTP(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateHTTPResponse(t, lbID)
+
+ opts := UpdateHTTPMonitorOpts{
+ AttemptLimit: 3,
+ Delay: 10,
+ Timeout: 10,
+ BodyRegex: "{regex}",
+ Path: "/foo",
+ StatusRegex: "200",
+ Type: HTTPS,
+ }
+
+ err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ m, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Monitor{
+ Type: CONNECT,
+ Delay: 10,
+ Timeout: 10,
+ AttemptLimit: 3,
+ }
+
+ th.AssertDeepEquals(t, expected, m)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID)
+
+ err := Delete(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/monitors/results.go b/rackspace/lb/v1/monitors/results.go
new file mode 100644
index 0000000..eec556f
--- /dev/null
+++ b/rackspace/lb/v1/monitors/results.go
@@ -0,0 +1,90 @@
+package monitors
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// Type represents the type of Monitor.
+type Type string
+
+// Useful constants.
+const (
+ CONNECT Type = "CONNECT"
+ HTTP Type = "HTTP"
+ HTTPS Type = "HTTPS"
+)
+
+// Monitor represents a health monitor API resource. A monitor comes in three
+// forms: CONNECT, HTTP or HTTPS.
+//
+// A CONNECT monitor establishes a basic connection to each node on its defined
+// port to ensure that the service is listening properly. The connect monitor
+// is the most basic type of health check and does no post-processing or
+// protocol-specific health checks.
+//
+// HTTP and HTTPS health monitors are generally considered more intelligent and
+// powerful than CONNECT. It is capable of processing an HTTP or HTTPS response
+// to determine the condition of a node. It supports the same basic properties
+// as CONNECT and includes additional attributes that are used to evaluate the
+// HTTP response.
+type Monitor struct {
+ // Number of permissible monitor failures before removing a node from
+ // rotation.
+ AttemptLimit int `mapstructure:"attemptsBeforeDeactivation"`
+
+ // The minimum number of seconds to wait before executing the health monitor.
+ Delay int
+
+ // Maximum number of seconds to wait for a connection to be established
+ // before timing out.
+ Timeout int
+
+ // Type of the health monitor.
+ Type Type
+
+ // A regular expression that will be used to evaluate the contents of the
+ // body of the response.
+ BodyRegex string
+
+ // The name of a host for which the health monitors will check.
+ HostHeader string
+
+ // The HTTP path that will be used in the sample request.
+ Path string
+
+ // A regular expression that will be used to evaluate the HTTP status code
+ // returned in the response.
+ StatusRegex string
+}
+
+// UpdateResult represents the result of an Update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// DeleteResult represents the result of an Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// Extract interprets any GetResult as a Monitor.
+func (r GetResult) Extract() (*Monitor, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ M Monitor `mapstructure:"healthMonitor"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.M, err
+}
diff --git a/rackspace/lb/v1/monitors/urls.go b/rackspace/lb/v1/monitors/urls.go
new file mode 100644
index 0000000..0a1e6df
--- /dev/null
+++ b/rackspace/lb/v1/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ monitorPath = "healthmonitor"
+)
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), monitorPath)
+}
diff --git a/rackspace/lb/v1/nodes/doc.go b/rackspace/lb/v1/nodes/doc.go
new file mode 100644
index 0000000..49c4318
--- /dev/null
+++ b/rackspace/lb/v1/nodes/doc.go
@@ -0,0 +1,35 @@
+/*
+Package nodes provides information and interaction with the Node API resource
+for the Rackspace Cloud Load Balancer service.
+
+Nodes are responsible for servicing the requests received through the load
+balancer's virtual IP. A node is usually a virtual machine. By default, the
+load balancer employs a basic health check that ensures the node is listening
+on its defined port. The node is checked at the time of addition and at regular
+intervals as defined by the load balancer's health check configuration. If a
+back-end node is not listening on its port, or does not meet the conditions of
+the defined check, then connections will not be forwarded to the node, and its
+status is changed to OFFLINE. Only nodes that are in an ONLINE status receive
+and can service traffic from the load balancer.
+
+All nodes have an associated status that indicates whether the node is
+ONLINE, OFFLINE, or DRAINING. Only nodes that are in ONLINE status can receive
+and service traffic from the load balancer. The OFFLINE status represents a
+node that cannot accept or service traffic. A node in DRAINING status
+represents a node that stops the traffic manager from sending any additional
+new connections to the node, but honors established sessions. If the traffic
+manager receives a request and session persistence requires that the node is
+used, the traffic manager uses it. The status is determined by the passive or
+active health monitors.
+
+If the WEIGHTED_ROUND_ROBIN load balancer algorithm mode is selected, then the
+caller should assign the relevant weights to the node as part of the weight
+attribute of the node element. When the algorithm of the load balancer is
+changed to WEIGHTED_ROUND_ROBIN and the nodes do not already have an assigned
+weight, the service automatically sets the weight to 1 for all nodes.
+
+One or more secondary nodes can be added to a specified load balancer so that
+if all the primary nodes fail, traffic can be redirected to secondary nodes.
+The type attribute allows configuring the node as either PRIMARY or SECONDARY.
+*/
+package nodes
diff --git a/rackspace/lb/v1/nodes/fixtures.go b/rackspace/lb/v1/nodes/fixtures.go
new file mode 100644
index 0000000..0aea541
--- /dev/null
+++ b/rackspace/lb/v1/nodes/fixtures.go
@@ -0,0 +1,208 @@
+package nodes
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/nodes"
+}
+
+func _nodeURL(lbID, nodeID int) string {
+ return _rootURL(lbID) + "/" + strconv.Itoa(nodeID)
+}
+
+func mockListResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "nodes": [
+ {
+ "id": 410,
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE",
+ "weight": 3,
+ "type": "PRIMARY"
+ },
+ {
+ "id": 411,
+ "address": "10.1.1.2",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE",
+ "weight": 8,
+ "type": "SECONDARY"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "nodes": [
+ {
+ "address": "10.2.2.3",
+ "port": 80,
+ "condition": "ENABLED",
+ "type": "PRIMARY"
+ },
+ {
+ "address": "10.2.2.4",
+ "port": 81,
+ "condition": "ENABLED",
+ "type": "SECONDARY"
+ }
+ ]
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "nodes": [
+ {
+ "address": "10.2.2.3",
+ "id": 185,
+ "port": 80,
+ "status": "ONLINE",
+ "condition": "ENABLED",
+ "weight": 1,
+ "type": "PRIMARY"
+ },
+ {
+ "address": "10.2.2.4",
+ "id": 186,
+ "port": 81,
+ "status": "ONLINE",
+ "condition": "ENABLED",
+ "weight": 1,
+ "type": "SECONDARY"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID, nodeID int) {
+ th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockGetResponse(t *testing.T, lbID, nodeID int) {
+ th.Mux.HandleFunc(_nodeURL(lbID, nodeID), 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, `
+{
+ "node": {
+ "id": 410,
+ "address": "10.1.1.1",
+ "port": 80,
+ "condition": "ENABLED",
+ "status": "ONLINE",
+ "weight": 12,
+ "type": "PRIMARY"
+ }
+}
+ `)
+ })
+}
+
+func mockUpdateResponse(t *testing.T, lbID, nodeID int) {
+ th.Mux.HandleFunc(_nodeURL(lbID, nodeID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "node": {
+ "address": "1.2.3.4",
+ "condition": "DRAINING",
+ "weight": 10,
+ "type": "SECONDARY"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockListEventsResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID)+"/events", 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, `
+{
+ "nodeServiceEvents": [
+ {
+ "detailedMessage": "Node is ok",
+ "nodeId": 373,
+ "id": 7,
+ "type": "UPDATE_NODE",
+ "description": "Node '373' status changed to 'ONLINE' for load balancer '323'",
+ "category": "UPDATE",
+ "severity": "INFO",
+ "relativeUri": "/406271/loadbalancers/323/nodes/373/events",
+ "accountId": 406271,
+ "loadbalancerId": 323,
+ "title": "Node Status Updated",
+ "author": "Rackspace Cloud",
+ "created": "10-30-2012 10:18:23"
+ }
+ ]
+}
+`)
+ })
+}
diff --git a/rackspace/lb/v1/nodes/requests.go b/rackspace/lb/v1/nodes/requests.go
new file mode 100644
index 0000000..bfd0aed
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -0,0 +1,286 @@
+package nodes
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// load balancer nodes. It requires the node ID, its parent load balancer ID,
+// and optional limit integer (passed in either as a pointer or a nil poitner).
+func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager {
+ url := rootURL(client, loadBalancerID)
+ if limit != nil {
+ url += fmt.Sprintf("?limit=%d", limit)
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return NodePage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+ ToNodeCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+ // Required - the IP address or CIDR for this back-end node. It can either be
+ // a private IP (ServiceNet) or a public IP.
+ Address string
+
+ // Optional - the port on which traffic is sent and received.
+ Port int
+
+ // Optional - the condition of the node. See the consts in Results.go.
+ Condition Condition
+
+ // Optional - the type of the node. See the consts in Results.go.
+ Type Type
+
+ // Optional - a pointer to an integer between 0 and 100.
+ Weight *int
+}
+
+func validateWeight(weight *int) error {
+ if weight != nil && (*weight > 100 || *weight < 0) {
+ return errors.New("Weight must be a valid int between 0 and 100")
+ }
+ return nil
+}
+
+// ToNodeCreateMap converts a slice of options into a map that can be used for
+// the JSON.
+func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) {
+ type nodeMap map[string]interface{}
+ nodes := []nodeMap{}
+
+ for k, v := range opts {
+ if v.Address == "" {
+ return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k)
+ }
+ if weightErr := validateWeight(v.Weight); weightErr != nil {
+ return nodeMap{}, weightErr
+ }
+
+ node := make(map[string]interface{})
+ node["address"] = v.Address
+
+ if v.Port > 0 {
+ node["port"] = v.Port
+ }
+ if v.Condition != "" {
+ node["condition"] = v.Condition
+ }
+ if v.Type != "" {
+ node["type"] = v.Type
+ }
+ if v.Weight != nil {
+ node["weight"] = &v.Weight
+ }
+
+ nodes = append(nodes, node)
+ }
+
+ return nodeMap{"nodes": nodes}, nil
+}
+
+// Create is the operation responsible for creating a new node on a load
+// balancer. Since every load balancer exists in both ServiceNet and the public
+// Internet, both private and public IP addresses can be used for nodes.
+//
+// If nodes need time to boot up services before they become operational, you
+// can temporarily prevent traffic from being sent to that node by setting the
+// Condition field to DRAINING. Health checks will still be performed; but once
+// your node is ready, you can update its condition to ENABLED and have it
+// handle traffic.
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToNodeCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ resp, err := perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ pr, err := pagination.PageResultFrom(resp.HttpResponse)
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ return CreateResult{pagination.SinglePageBase(pr)}
+}
+
+// BulkDelete is the operation responsible for batch deleting multiple nodes in
+// a single operation. It accepts a slice of integer IDs and will remove them
+// from the load balancer. The maximum limit is 10 node removals at once.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(nodeIDs) > 10 || len(nodeIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += gophercloud.IDSliceToQueryString("id", nodeIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing details for a single node.
+func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", resourceURL(c, lbID, nodeID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// UpdateOptsBuilder represents a type that can be converted into a JSON-like
+// map structure.
+type UpdateOptsBuilder interface {
+ ToNodeUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represent the options for updating an existing node.
+type UpdateOpts struct {
+ // Optional - the IP address or CIDR for this back-end node. It can either be
+ // a private IP (ServiceNet) or a public IP.
+ Address string
+
+ // Optional - the condition of the node. See the consts in Results.go.
+ Condition Condition
+
+ // Optional - the type of the node. See the consts in Results.go.
+ Type Type
+
+ // Optional - a pointer to an integer between 0 and 100.
+ Weight *int
+}
+
+// ToNodeUpdateMap converts an options struct into a JSON-like map.
+func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) {
+ node := make(map[string]interface{})
+
+ if opts.Address != "" {
+ node["address"] = opts.Address
+ }
+ if opts.Condition != "" {
+ node["condition"] = opts.Condition
+ }
+ if opts.Weight != nil {
+ if weightErr := validateWeight(opts.Weight); weightErr != nil {
+ return node, weightErr
+ }
+ node["weight"] = &opts.Weight
+ }
+ if opts.Type != "" {
+ node["type"] = opts.Type
+ }
+
+ return map[string]interface{}{"node": node}, nil
+}
+
+// Update is the operation responsible for updating an existing node. A node's
+// IP, port, and status are immutable attributes and cannot be modified.
+func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToNodeUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", resourceURL(c, lbID, nodeID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for permanently deleting a node.
+func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, nodeID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
+
+// ListEventsOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListEventsOptsBuilder interface {
+ ToEventsListQuery() (string, error)
+}
+
+// ListEventsOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListEventsOpts struct {
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+}
+
+// ToEventsListQuery formats a ListOpts into a query string.
+func (opts ListEventsOpts) ToEventsListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// ListEvents is the operation responsible for listing all the events
+// associated with the activity between the node and the load balancer. The
+// events report errors found with the node. The detailedMessage provides the
+// detailed reason for the error.
+func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager {
+ url := eventsURL(client, loadBalancerID)
+
+ if opts != nil {
+ query, err := opts.ToEventsListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return NodeEventPage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/rackspace/lb/v1/nodes/requests_test.go b/rackspace/lb/v1/nodes/requests_test.go
new file mode 100644
index 0000000..f888a14
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests_test.go
@@ -0,0 +1,212 @@
+package nodes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ nodeID = 67890
+ nodeID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNodes(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Node{
+ Node{
+ ID: 410,
+ Address: "10.1.1.1",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 3,
+ Type: PRIMARY,
+ },
+ Node{
+ ID: 411,
+ Address: "10.1.1.2",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 8,
+ Type: SECONDARY,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ CreateOpt{
+ Address: "10.2.2.3",
+ Port: 80,
+ Condition: ENABLED,
+ Type: PRIMARY,
+ },
+ CreateOpt{
+ Address: "10.2.2.4",
+ Port: 81,
+ Condition: ENABLED,
+ Type: SECONDARY,
+ },
+ }
+
+ page := Create(client.ServiceClient(), lbID, opts)
+
+ actual, err := page.ExtractNodes()
+ th.AssertNoErr(t, err)
+
+ expected := []Node{
+ Node{
+ ID: 185,
+ Address: "10.2.2.3",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 1,
+ Type: PRIMARY,
+ },
+ Node{
+ ID: 186,
+ Address: "10.2.2.4",
+ Port: 81,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 1,
+ Type: SECONDARY,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{nodeID, nodeID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID, nodeID)
+
+ node, err := Get(client.ServiceClient(), lbID, nodeID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Node{
+ ID: 410,
+ Address: "10.1.1.1",
+ Port: 80,
+ Condition: ENABLED,
+ Status: ONLINE,
+ Weight: 12,
+ Type: PRIMARY,
+ }
+
+ th.AssertDeepEquals(t, expected, node)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateResponse(t, lbID, nodeID)
+
+ opts := UpdateOpts{
+ Address: "1.2.3.4",
+ Weight: gophercloud.IntToPointer(10),
+ Condition: DRAINING,
+ Type: SECONDARY,
+ }
+
+ err := Update(client.ServiceClient(), lbID, nodeID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID, nodeID)
+
+ err := Delete(client.ServiceClient(), lbID, nodeID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListEvents(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListEventsResponse(t, lbID)
+
+ count := 0
+
+ pager := ListEvents(client.ServiceClient(), lbID, ListEventsOpts{})
+
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNodeEvents(page)
+ th.AssertNoErr(t, err)
+
+ expected := []NodeEvent{
+ NodeEvent{
+ DetailedMessage: "Node is ok",
+ NodeID: 373,
+ ID: 7,
+ Type: "UPDATE_NODE",
+ Description: "Node '373' status changed to 'ONLINE' for load balancer '323'",
+ Category: "UPDATE",
+ Severity: "INFO",
+ RelativeURI: "/406271/loadbalancers/323/nodes/373/events",
+ AccountID: 406271,
+ LoadBalancerID: 323,
+ Title: "Node Status Updated",
+ Author: "Rackspace Cloud",
+ Created: "10-30-2012 10:18:23",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
diff --git a/rackspace/lb/v1/nodes/results.go b/rackspace/lb/v1/nodes/results.go
new file mode 100644
index 0000000..916485f
--- /dev/null
+++ b/rackspace/lb/v1/nodes/results.go
@@ -0,0 +1,210 @@
+package nodes
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Node represents a back-end device, usually a virtual machine, that can
+// handle traffic. It is assigned traffic based on its parent load balancer.
+type Node struct {
+ // The IP address or CIDR for this back-end node.
+ Address string
+
+ // The unique ID for this node.
+ ID int
+
+ // The port on which traffic is sent and received.
+ Port int
+
+ // The node's status.
+ Status Status
+
+ // The node's condition.
+ Condition Condition
+
+ // The priority at which this node will receive traffic if a weighted
+ // algorithm is used by its parent load balancer. Ranges from 1 to 100.
+ Weight int
+
+ // Type of node.
+ Type Type
+}
+
+// Type indicates whether the node is of a PRIMARY or SECONDARY nature.
+type Type string
+
+const (
+ // PRIMARY nodes are in the normal rotation to receive traffic from the load
+ // balancer.
+ PRIMARY Type = "PRIMARY"
+
+ // SECONDARY nodes are only in the rotation to receive traffic from the load
+ // balancer when all the primary nodes fail. This provides a failover feature
+ // that automatically routes traffic to the secondary node in the event that
+ // the primary node is disabled or in a failing state. Note that active
+ // health monitoring must be enabled on the load balancer to enable the
+ // failover feature to the secondary node.
+ SECONDARY Type = "SECONDARY"
+)
+
+// Condition represents the condition of a node.
+type Condition string
+
+const (
+ // ENABLED indicates that the node is permitted to accept new connections.
+ ENABLED Condition = "ENABLED"
+
+ // DISABLED indicates that the node is not permitted to accept any new
+ // connections regardless of session persistence configuration. Existing
+ // connections are forcibly terminated.
+ DISABLED Condition = "DISABLED"
+
+ // DRAINING indicates that the node is allowed to service existing
+ // established connections and connections that are being directed to it as a
+ // result of the session persistence configuration.
+ DRAINING Condition = "DRAINING"
+)
+
+// Status indicates whether the node can accept service traffic. If a node is
+// not listening on its port or does not meet the conditions of the defined
+// active health check for the load balancer, then the load balancer does not
+// forward connections, and its status is listed as OFFLINE.
+type Status string
+
+const (
+ // ONLINE indicates that the node is healthy and capable of receiving traffic
+ // from the load balancer.
+ ONLINE Status = "ONLINE"
+
+ // OFFLINE indicates that the node is not in a position to receive service
+ // traffic. It is usually switched into this state when a health check is not
+ // satisfied with the node's response time.
+ OFFLINE Status = "OFFLINE"
+)
+
+// NodePage is the page returned by a pager when traversing over a collection
+// of nodes.
+type NodePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a NodePage struct is empty.
+func (p NodePage) IsEmpty() (bool, error) {
+ is, err := ExtractNodes(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+func commonExtractNodes(body interface{}) ([]Node, error) {
+ var resp struct {
+ Nodes []Node `mapstructure:"nodes" json:"nodes"`
+ }
+
+ err := mapstructure.Decode(body, &resp)
+
+ return resp.Nodes, err
+}
+
+// ExtractNodes accepts a Page struct, specifically a NodePage struct, and
+// extracts the elements into a slice of Node structs. In other words, a
+// generic collection is mapped into a relevant slice.
+func ExtractNodes(page pagination.Page) ([]Node, error) {
+ return commonExtractNodes(page.(NodePage).Body)
+}
+
+// CreateResult represents the result of a create operation. Since multiple
+// nodes can be added in one operation, this result represents multiple nodes
+// and should be treated as a typical pagination Page. Use its ExtractNodes
+// method to get out a slice of Node structs.
+type CreateResult struct {
+ pagination.SinglePageBase
+}
+
+// ExtractNodes extracts a slice of Node structs from a CreateResult.
+func (res CreateResult) ExtractNodes() ([]Node, error) {
+ return commonExtractNodes(res.Body)
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+func (r commonResult) Extract() (*Node, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Node Node `mapstructure:"node"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.Node, err
+}
+
+// NodeEvent represents a service event that occurred between a node and a
+// load balancer.
+type NodeEvent struct {
+ ID int
+ DetailedMessage string
+ NodeID int
+ Type string
+ Description string
+ Category string
+ Severity string
+ RelativeURI string
+ AccountID int
+ LoadBalancerID int
+ Title string
+ Author string
+ Created string
+}
+
+// NodeEventPage is a concrete type which embeds the common SinglePageBase
+// struct, and is used when traversing node event collections.
+type NodeEventPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty is a concrete function which indicates whether an NodeEventPage is
+// empty or not.
+func (r NodeEventPage) IsEmpty() (bool, error) {
+ is, err := ExtractNodeEvents(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractNodeEvents accepts a Page struct, specifically a NodeEventPage
+// struct, and extracts the elements into a slice of NodeEvent structs. In
+// other words, the collection is mapped into a relevant slice.
+func ExtractNodeEvents(page pagination.Page) ([]NodeEvent, error) {
+ var resp struct {
+ Events []NodeEvent `mapstructure:"nodeServiceEvents" json:"nodeServiceEvents"`
+ }
+
+ err := mapstructure.Decode(page.(NodeEventPage).Body, &resp)
+
+ return resp.Events, err
+}
diff --git a/rackspace/lb/v1/nodes/urls.go b/rackspace/lb/v1/nodes/urls.go
new file mode 100644
index 0000000..2cefee2
--- /dev/null
+++ b/rackspace/lb/v1/nodes/urls.go
@@ -0,0 +1,25 @@
+package nodes
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ lbPath = "loadbalancers"
+ nodePath = "nodes"
+ eventPath = "events"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, strconv.Itoa(nodeID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath)
+}
+
+func eventsURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), nodePath, eventPath)
+}
diff --git a/rackspace/lb/v1/sessions/doc.go b/rackspace/lb/v1/sessions/doc.go
new file mode 100644
index 0000000..dcec0a8
--- /dev/null
+++ b/rackspace/lb/v1/sessions/doc.go
@@ -0,0 +1,30 @@
+/*
+Package sessions provides information and interaction with the Session
+Persistence feature of the Rackspace Cloud Load Balancer service.
+
+Session persistence is a feature of the load balancing service that forces
+multiple requests from clients (of the same protocol) to be directed to the
+same node. This is common with many web applications that do not inherently
+share application state between back-end servers.
+
+There are two modes to choose from: HTTP_COOKIE and SOURCE_IP. You can only set
+one of the session persistence modes on a load balancer, and it can only
+support one protocol. If you set HTTP_COOKIE mode for an HTTP load balancer, it
+supports session persistence for HTTP requests only. Likewise, if you set
+SOURCE_IP mode for an HTTPS load balancer, it supports session persistence for
+only HTTPS requests.
+
+To support session persistence for both HTTP and HTTPS requests concurrently,
+choose one of the following options:
+
+- Use two load balancers, one configured for session persistence for HTTP
+requests and the other configured for session persistence for HTTPS requests.
+That way, the load balancers support session persistence for both HTTP and
+HTTPS requests concurrently, with each load balancer supporting one of the
+protocols.
+
+- Use one load balancer, configure it for session persistence for HTTP requests,
+and then enable SSL termination for that load balancer. The load balancer
+supports session persistence for both HTTP and HTTPS requests concurrently.
+*/
+package sessions
diff --git a/rackspace/lb/v1/sessions/fixtures.go b/rackspace/lb/v1/sessions/fixtures.go
new file mode 100644
index 0000000..9596819
--- /dev/null
+++ b/rackspace/lb/v1/sessions/fixtures.go
@@ -0,0 +1,58 @@
+package sessions
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+ return "/loadbalancers/" + strconv.Itoa(id) + "/sessionpersistence"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "sessionPersistence": {
+ "persistenceType": "HTTP_COOKIE"
+ }
+}
+`)
+ })
+}
+
+func mockEnableResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "sessionPersistence": {
+ "persistenceType": "HTTP_COOKIE"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDisableResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/sessions/requests.go b/rackspace/lb/v1/sessions/requests.go
new file mode 100644
index 0000000..9853ad1
--- /dev/null
+++ b/rackspace/lb/v1/sessions/requests.go
@@ -0,0 +1,82 @@
+package sessions
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package.
+type CreateOptsBuilder interface {
+ ToSPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Required - can either be HTTPCOOKIE or SOURCEIP
+ Type Type
+}
+
+// ToSPCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToSPCreateMap() (map[string]interface{}, error) {
+ sp := make(map[string]interface{})
+
+ if opts.Type == "" {
+ return sp, errors.New("Type is a required field")
+ }
+
+ sp["persistenceType"] = opts.Type
+ return map[string]interface{}{"sessionPersistence": sp}, nil
+}
+
+// Enable is the operation responsible for enabling session persistence for a
+// particular load balancer.
+func Enable(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) EnableResult {
+ var res EnableResult
+
+ reqBody, err := opts.ToSPCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing details of the session
+// persistence configuration for a particular load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Disable is the operation responsible for disabling session persistence for a
+// particular load balancer.
+func Disable(c *gophercloud.ServiceClient, lbID int) DisableResult {
+ var res DisableResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/sessions/requests_test.go b/rackspace/lb/v1/sessions/requests_test.go
new file mode 100644
index 0000000..f319e54
--- /dev/null
+++ b/rackspace/lb/v1/sessions/requests_test.go
@@ -0,0 +1,44 @@
+package sessions
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestEnable(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockEnableResponse(t, lbID)
+
+ opts := CreateOpts{Type: HTTPCOOKIE}
+ err := Enable(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ sp, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SessionPersistence{Type: HTTPCOOKIE}
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestDisable(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDisableResponse(t, lbID)
+
+ err := Disable(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/sessions/results.go b/rackspace/lb/v1/sessions/results.go
new file mode 100644
index 0000000..fe90e72
--- /dev/null
+++ b/rackspace/lb/v1/sessions/results.go
@@ -0,0 +1,58 @@
+package sessions
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// Type represents the type of session persistence being used.
+type Type string
+
+const (
+ // HTTPCOOKIE is a session persistence mechanism that inserts an HTTP cookie
+ // and is used to determine the destination back-end node. This is supported
+ // for HTTP load balancing only.
+ HTTPCOOKIE Type = "HTTP_COOKIE"
+
+ // SOURCEIP is a session persistence mechanism that keeps track of the source
+ // IP address that is mapped and is able to determine the destination
+ // back-end node. This is supported for HTTPS pass-through and non-HTTP load
+ // balancing only.
+ SOURCEIP Type = "SOURCE_IP"
+)
+
+// SessionPersistence indicates how a load balancer is using session persistence
+type SessionPersistence struct {
+ Type Type `mapstructure:"persistenceType"`
+}
+
+// EnableResult represents the result of an enable operation.
+type EnableResult struct {
+ gophercloud.ErrResult
+}
+
+// DisableResult represents the result of a disable operation.
+type DisableResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a GetResult as an SP, if possible.
+func (r GetResult) Extract() (*SessionPersistence, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ SP SessionPersistence `mapstructure:"sessionPersistence"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.SP, err
+}
diff --git a/rackspace/lb/v1/sessions/urls.go b/rackspace/lb/v1/sessions/urls.go
new file mode 100644
index 0000000..c4a896d
--- /dev/null
+++ b/rackspace/lb/v1/sessions/urls.go
@@ -0,0 +1,16 @@
+package sessions
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ spPath = "sessionpersistence"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), spPath)
+}
diff --git a/rackspace/lb/v1/ssl/doc.go b/rackspace/lb/v1/ssl/doc.go
new file mode 100644
index 0000000..6a2c174
--- /dev/null
+++ b/rackspace/lb/v1/ssl/doc.go
@@ -0,0 +1,22 @@
+/*
+Package ssl provides information and interaction with the SSL Termination
+feature of the Rackspace Cloud Load Balancer service.
+
+You may only enable and configure SSL termination on load balancers with
+non-secure protocols, such as HTTP, but not HTTPS.
+
+SSL-terminated load balancers decrypt the traffic at the traffic manager and
+pass unencrypted traffic to the back-end node. Because of this, the customer's
+back-end nodes don't know what protocol the client requested. For this reason,
+the X-Forwarded-Proto (XFP) header has been added for identifying the
+originating protocol of an HTTP request as "http" or "https" depending on what
+protocol the client requested.
+
+Not every service returns certificates in the proper order. Please verify that
+your chain of certificates matches that of walking up the chain from the domain
+to the CA root.
+
+If used for HTTP to HTTPS redirection, the LoadBalancer's securePort attribute
+must be set to 443, and its secureTrafficOnly attribute must be true.
+*/
+package ssl
diff --git a/rackspace/lb/v1/ssl/fixtures.go b/rackspace/lb/v1/ssl/fixtures.go
new file mode 100644
index 0000000..1d40100
--- /dev/null
+++ b/rackspace/lb/v1/ssl/fixtures.go
@@ -0,0 +1,195 @@
+package ssl
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+ return "/loadbalancers/" + strconv.Itoa(id) + "/ssltermination"
+}
+
+func _certURL(id, certID int) string {
+ url := _rootURL(id) + "/certificatemappings"
+ if certID > 0 {
+ url += "/" + strconv.Itoa(certID)
+ }
+ return url
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "sslTermination": {
+ "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "enabled": true,
+ "secureTrafficOnly": false,
+ "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ "securePort": 443
+ }
+}
+`)
+ })
+}
+
+func mockUpdateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "sslTermination": {
+ "enabled": true,
+ "securePort": 443,
+ "secureTrafficOnly": false,
+ "privateKey": "foo",
+ "certificate": "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate": "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func mockListCertsResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_certURL(lbID, 0), 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, `
+{
+ "certificateMappings": [
+ {
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com"
+ }
+ },
+ {
+ "certificateMapping": {
+ "id": 124,
+ "hostName": "*.rackspace.com"
+ }
+ }
+ ]
+}
+`)
+ })
+}
+
+func mockAddCertResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_certURL(lbID, 0), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "certificateMapping": {
+ "hostName": "rackspace.com",
+ "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+ })
+}
+
+func mockGetCertResponse(t *testing.T, lbID, certID int) {
+ th.Mux.HandleFunc(_certURL(lbID, certID), 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, `
+{
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+`)
+ })
+}
+
+func mockUpdateCertResponse(t *testing.T, lbID, certID int) {
+ th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "certificateMapping": {
+ "hostName": "rackspace.com",
+ "privateKey":"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "certificateMapping": {
+ "id": 123,
+ "hostName": "rackspace.com",
+ "certificate":"-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ "intermediateCertificate":"-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n"
+ }
+}
+ `)
+ })
+}
+
+func mockDeleteCertResponse(t *testing.T, lbID, certID int) {
+ th.Mux.HandleFunc(_certURL(lbID, certID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusOK)
+ })
+}
diff --git a/rackspace/lb/v1/ssl/requests.go b/rackspace/lb/v1/ssl/requests.go
new file mode 100644
index 0000000..84b2712
--- /dev/null
+++ b/rackspace/lb/v1/ssl/requests.go
@@ -0,0 +1,278 @@
+package ssl
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+var (
+ errPrivateKey = errors.New("PrivateKey is a required field")
+ errCertificate = errors.New("Certificate is a required field")
+ errIntCertificate = errors.New("IntCertificate is a required field")
+)
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package.
+type UpdateOptsBuilder interface {
+ ToSSLUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // Required - consult the SSLTermConfig struct for more info.
+ SecurePort int
+
+ // Required - consult the SSLTermConfig struct for more info.
+ PrivateKey string
+
+ // Required - consult the SSLTermConfig struct for more info.
+ Certificate string
+
+ // Required - consult the SSLTermConfig struct for more info.
+ IntCertificate string
+
+ // Optional - consult the SSLTermConfig struct for more info.
+ Enabled *bool
+
+ // Optional - consult the SSLTermConfig struct for more info.
+ SecureTrafficOnly *bool
+}
+
+// ToSSLUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToSSLUpdateMap() (map[string]interface{}, error) {
+ ssl := make(map[string]interface{})
+
+ if opts.SecurePort == 0 {
+ return ssl, errors.New("SecurePort needs to be an integer greater than 0")
+ }
+ if opts.PrivateKey == "" {
+ return ssl, errPrivateKey
+ }
+ if opts.Certificate == "" {
+ return ssl, errCertificate
+ }
+ if opts.IntCertificate == "" {
+ return ssl, errIntCertificate
+ }
+
+ ssl["securePort"] = opts.SecurePort
+ ssl["privateKey"] = opts.PrivateKey
+ ssl["certificate"] = opts.Certificate
+ ssl["intermediateCertificate"] = opts.IntCertificate
+
+ if opts.Enabled != nil {
+ ssl["enabled"] = &opts.Enabled
+ }
+
+ if opts.SecureTrafficOnly != nil {
+ ssl["secureTrafficOnly"] = &opts.SecureTrafficOnly
+ }
+
+ return map[string]interface{}{"sslTermination": ssl}, nil
+}
+
+// Update is the operation responsible for updating the SSL Termination
+// configuration for a load balancer.
+func Update(c *gophercloud.ServiceClient, lbID int, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToSSLUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing the details of the SSL
+// Termination configuration for a load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for deleting the SSL Termination
+// configuration for a load balancer.
+func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// ListCerts will list all of the certificate mappings associated with a
+// SSL-terminated HTTP load balancer.
+func ListCerts(c *gophercloud.ServiceClient, lbID int) pagination.Pager {
+ url := certURL(c, lbID)
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return CertPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateCertOptsBuilder is the interface options structs have to satisfy in
+// order to be used in the AddCert operation in this package.
+type CreateCertOptsBuilder interface {
+ ToCertCreateMap() (map[string]interface{}, error)
+}
+
+// CreateCertOpts represents the options used when adding a new certificate mapping.
+type CreateCertOpts struct {
+ HostName string
+ PrivateKey string
+ Certificate string
+ IntCertificate string
+}
+
+// ToCertCreateMap will cast an CreateCertOpts struct to a map for JSON serialization.
+func (opts CreateCertOpts) ToCertCreateMap() (map[string]interface{}, error) {
+ cm := make(map[string]interface{})
+
+ if opts.HostName == "" {
+ return cm, errors.New("HostName is a required option")
+ }
+ if opts.PrivateKey == "" {
+ return cm, errPrivateKey
+ }
+ if opts.Certificate == "" {
+ return cm, errCertificate
+ }
+
+ cm["hostName"] = opts.HostName
+ cm["privateKey"] = opts.PrivateKey
+ cm["certificate"] = opts.Certificate
+
+ if opts.IntCertificate != "" {
+ cm["intermediateCertificate"] = opts.IntCertificate
+ }
+
+ return map[string]interface{}{"certificateMapping": cm}, nil
+}
+
+// CreateCert will add a new SSL certificate and allow an SSL-terminated HTTP
+// load balancer to use it. This feature is useful because it allows multiple
+// certificates to be used. The maximum number of certificates that can be
+// stored per LB is 20.
+func CreateCert(c *gophercloud.ServiceClient, lbID int, opts CreateCertOptsBuilder) CreateCertResult {
+ var res CreateCertResult
+
+ reqBody, err := opts.ToCertCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", certURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// GetCert will show the details of an existing SSL certificate.
+func GetCert(c *gophercloud.ServiceClient, lbID, certID int) GetCertResult {
+ var res GetCertResult
+
+ _, res.Err = perigee.Request("GET", certResourceURL(c, lbID, certID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// UpdateCertOptsBuilder is the interface options structs have to satisfy in
+// order to be used in the UpdateCert operation in this package.
+type UpdateCertOptsBuilder interface {
+ ToCertUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateCertOpts represents the options needed to update a SSL certificate.
+type UpdateCertOpts struct {
+ HostName string
+ PrivateKey string
+ Certificate string
+ IntCertificate string
+}
+
+// ToCertUpdateMap will cast an UpdateCertOpts struct into a map for JSON
+// seralization.
+func (opts UpdateCertOpts) ToCertUpdateMap() (map[string]interface{}, error) {
+ cm := make(map[string]interface{})
+
+ if opts.HostName != "" {
+ cm["hostName"] = opts.HostName
+ }
+ if opts.PrivateKey != "" {
+ cm["privateKey"] = opts.PrivateKey
+ }
+ if opts.Certificate != "" {
+ cm["certificate"] = opts.Certificate
+ }
+ if opts.IntCertificate != "" {
+ cm["intermediateCertificate"] = opts.IntCertificate
+ }
+
+ return map[string]interface{}{"certificateMapping": cm}, nil
+}
+
+// UpdateCert is the operation responsible for updating the details of an
+// existing SSL certificate.
+func UpdateCert(c *gophercloud.ServiceClient, lbID, certID int, opts UpdateCertOptsBuilder) UpdateCertResult {
+ var res UpdateCertResult
+
+ reqBody, err := opts.ToCertUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", certResourceURL(c, lbID, certID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// DeleteCert is the operation responsible for permanently removing a SSL
+// certificate.
+func DeleteCert(c *gophercloud.ServiceClient, lbID, certID int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", certResourceURL(c, lbID, certID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/ssl/requests_test.go b/rackspace/lb/v1/ssl/requests_test.go
new file mode 100644
index 0000000..fb14c4a
--- /dev/null
+++ b/rackspace/lb/v1/ssl/requests_test.go
@@ -0,0 +1,167 @@
+package ssl
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ certID = 67890
+)
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ sp, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &SSLTermConfig{
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ Enabled: true,
+ SecureTrafficOnly: false,
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ SecurePort: 443,
+ }
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateResponse(t, lbID)
+
+ opts := UpdateOpts{
+ Enabled: gophercloud.Enabled,
+ SecurePort: 443,
+ SecureTrafficOnly: gophercloud.Disabled,
+ PrivateKey: "foo",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ err := Update(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID)
+
+ err := Delete(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestListCerts(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListCertsResponse(t, lbID)
+
+ count := 0
+
+ err := ListCerts(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractCerts(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Certificate{
+ Certificate{ID: 123, HostName: "rackspace.com"},
+ Certificate{ID: 124, HostName: "*.rackspace.com"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreateCert(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockAddCertResponse(t, lbID)
+
+ opts := CreateCertOpts{
+ HostName: "rackspace.com",
+ PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+
+ cm, err := CreateCert(client.ServiceClient(), lbID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Certificate{
+ ID: 123,
+ HostName: "rackspace.com",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ th.AssertDeepEquals(t, expected, cm)
+}
+
+func TestGetCertMapping(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetCertResponse(t, lbID, certID)
+
+ sp, err := GetCert(client.ServiceClient(), lbID, certID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Certificate{
+ ID: 123,
+ HostName: "rackspace.com",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestUpdateCert(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockUpdateCertResponse(t, lbID, certID)
+
+ opts := UpdateCertOpts{
+ HostName: "rackspace.com",
+ PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAwIudSMpRZx7TS0/AtDVX3DgXwLD9g+XrNaoazlhwhpYALgzJ\nLAbAnOxT6OT0gTpkPus/B7QhW6y6Auf2cdBeW31XoIwPsSoyNhxgErGBxzNARRB9\nlI1HCa1ojFrcULluj4W6rpaOycI5soDBJiJHin/hbZBPZq6vhPCuNP7Ya48Zd/2X\nCQ9ft3XKfmbs1SdrdROIhigse/SGRbMrCorn/vhNIuohr7yOlHG3GcVcUI9k6ZSZ\nBbqF+ZA4ApSF/Q6/cumieEgofhkYbx5fg02s9Jwr4IWnIR2bSHs7UQ6sVgKYzjs7\nPd3Unpa74jFw6/H6shABoO2CIYLotGmQbFgnpwIDAQABAoIBAQCBCQ+PCIclJHNV\ntUzfeCA5ZR4F9JbxHdRTUnxEbOB8UWotckQfTScoAvj4yvdQ42DrCZxj/UOdvFOs\nPufZvlp91bIz1alugWjE+p8n5+2hIaegoTyHoWZKBfxak0myj5KYfHZvKlbmv1ML\nXV4TwEVRfAIG+v87QTY/UUxuF5vR+BpKIbgUJLfPUFFvJUdl84qsJ44pToxaYUd/\nh5YAGC00U4ay1KVSAUnTkkPNZ0lPG/rWU6w6WcTvNRLMd8DzFLTKLOgQfHhbExAF\n+sXPWjWSzbBRP1O7fHqq96QQh4VFiY/7w9W+sDKQyV6Ul17OSXs6aZ4f+lq4rJTI\n1FG96YiBAoGBAO1tiH0h1oWDBYfJB3KJJ6CQQsDGwtHo/DEgznFVP4XwEVbZ98Ha\nBfBCn3sAybbaikyCV1Hwj7kfHMZPDHbrcUSFX7quu/2zPK+wO3lZKXSyu4YsguSa\nRedInN33PpdnlPhLyQdWSuD5sVHJDF6xn22vlyxeILH3ooLg2WOFMPmVAoGBAM+b\nUG/a7iyfpAQKYyuFAsXz6SeFaDY+ZYeX45L112H8Pu+Ie/qzon+bzLB9FIH8GP6+\nQpQgmm/p37U2gD1zChUv7iW6OfQBKk9rWvMpfRF6d7YHquElejhizfTZ+ntBV/VY\ndOYEczxhrdW7keLpatYaaWUy/VboRZmlz/9JGqVLAoGAHfqNmFc0cgk4IowEj7a3\ntTNh6ltub/i+FynwRykfazcDyXaeLPDtfQe8gVh5H8h6W+y9P9BjJVnDVVrX1RAn\nbiJ1EupLPF5sVDapW8ohTOXgfbGTGXBNUUW+4Nv+IDno+mz/RhjkPYHpnM0I7c/5\ntGzOZsC/2hjNgT8I0+MWav0CgYEAuULdJeQVlKalI6HtW2Gn1uRRVJ49H+LQkY6e\nW3+cw2jo9LI0CMWSphNvNrN3wIMp/vHj0fHCP0pSApDvIWbuQXfzKaGko7UCf7rK\nf6GvZRCHkV4IREBAb97j8bMvThxClMNqFfU0rFZyXP+0MOyhFQyertswrgQ6T+Fi\n2mnvKD8CgYAmJHP3NTDRMoMRyAzonJ6nEaGUbAgNmivTaUWMe0+leCvAdwD89gzC\nTKbm3eDUg/6Va3X6ANh3wsfIOe4RXXxcbcFDk9R4zO2M5gfLSjYm5Q87EBZ2hrdj\nM2gLI7dt6thx0J8lR8xRHBEMrVBdgwp0g1gQzo5dAV88/kpkZVps8Q==\n-----END RSA PRIVATE KEY-----\n",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+
+ cm, err := UpdateCert(client.ServiceClient(), lbID, certID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Certificate{
+ ID: 123,
+ HostName: "rackspace.com",
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIEXTCCA0WgAwIBAgIGATTEAjK3MA0GCSqGSIb3DQEBBQUAMIGDMRkwFwYDVQQD\nExBUZXN0IENBIFNUdWIgS2V5MRcwFQYDVQQLEw5QbGF0Zm9ybSBMYmFhczEaMBgG\nA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFDASBgNVBAcTC1NhbiBBbnRvbmlvMQ4w\nDAYDVQQIEwVUZXhhczELMAkGA1UEBhMCVVMwHhcNMTIwMTA5MTk0NjQ1WhcNMTQw\nMTA4MTk0NjQ1WjCBgjELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMRQwEgYD\nVQQHEwtTYW4gQW50b25pbzEaMBgGA1UEChMRUmFja3NwYWNlIEhvc3RpbmcxFzAV\nBgNVBAsTDlBsYXRmb3JtIExiYWFzMRgwFgYDVQQDEw9UZXN0IENsaWVudCBLZXkw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAi51IylFnHtNLT8C0NVfc\nOBfAsP2D5es1qhrOWHCGlgAuDMksBsCc7FPo5PSBOmQ+6z8HtCFbrLoC5/Zx0F5b\nfVegjA+xKjI2HGASsYHHM0BFEH2UjUcJrWiMWtxQuW6Phbqulo7JwjmygMEmIkeK\nf+FtkE9mrq+E8K40/thrjxl3/ZcJD1+3dcp+ZuzVJ2t1E4iGKCx79IZFsysKiuf+\n+E0i6iGvvI6UcbcZxVxQj2TplJkFuoX5kDgClIX9Dr9y6aJ4SCh+GRhvHl+DTaz0\nnCvghachHZtIeztRDqxWApjOOzs93dSelrviMXDr8fqyEAGg7YIhgui0aZBsWCen\nAgMBAAGjgdUwgdIwgbAGA1UdIwSBqDCBpYAUNpx1Pc6cGA7KqEwHMmHBTZMA7lSh\ngYmkgYYwgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVU4IBATAd\nBgNVHQ4EFgQULueOfsjZZOHwJHZwBy6u0swnpccwDQYJKoZIhvcNAQEFBQADggEB\nAFNuqSVUaotUJoWDv4z7Kbi6JFpTjDht5ORw4BdVYlRD4h9DACAFzPrPV2ym/Osp\nhNMdZq6msZku7MdOSQVhdeGWrSNk3M8O9Hg7cVzPNXOF3iNoo3irQ5tURut44xs4\nWw5YWQqS9WyUY5snD8tm7Y1rQTPfhg+678xIq/zWCv/u+FSnfVv1nlhLVQkEeG/Y\ngh1uMaTIpUKTGEjIAGtpGP7wwIcXptR/HyfzhTUSTaWc1Ef7zoKT9LL5z3IV1hC2\njVWz+RwYs98LjMuksJFoHqRfWyYhCIym0jb6GTwaEmpxAjc+d7OLNQdnoEGoUYGP\nYjtfkRYg265ESMA+Kww4Xy8=\n-----END CERTIFICATE-----\n",
+ IntCertificate: "-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzEZMBcGA1UEAxMQVGVz\ndCBDQSBTVHViIEtleTEXMBUGA1UECxMOUGxhdGZvcm0gTGJhYXMxGjAYBgNVBAoT\nEVJhY2tzcGFjZSBIb3N0aW5nMRQwEgYDVQQHEwtTYW4gQW50b25pbzEOMAwGA1UE\nCBMFVGV4YXMxCzAJBgNVBAYTAlVTMB4XDTEyMDEwOTE5NDU0OVoXDTE0MDEwODE5\nNDU0OVowgYMxGTAXBgNVBAMTEFRlc3QgQ0EgU1R1YiBLZXkxFzAVBgNVBAsTDlBs\nYXRmb3JtIExiYWFzMRowGAYDVQQKExFSYWNrc3BhY2UgSG9zdGluZzEUMBIGA1UE\nBxMLU2FuIEFudG9uaW8xDjAMBgNVBAgTBVRleGFzMQswCQYDVQQGEwJVUzCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANNh55lwTVwQvNoEZjq1zGdYz9jA\nXXdjizn8AJhjHLOAallfPtvCfTEgKanhdoyz5FnhQE8HbDAop/KNS1lN2UMvdl5f\nZNLTSjJrNtedqxQwxN/i3bpyBxNVejUH2NjV1mmyj+5CJYwCzWalvI/gLPq/A3as\nO2EQqtf3U8unRgn0zXLRdYxV9MrUzNAmdipPNvNrsVdrCgA42rgF/8KsyRVQfJCX\nfN7PGCfrsC3YaUvhymraWxNnXIzMYTNa9wEeBZLUw8SlEtpa1Zsvui+TPXu3USNZ\nVnWH8Lb6ENlnoX0VBwo62fjOG3JzhNKoJawi3bRqyDdINOvafr7iPrrs/T8CAwEA\nAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNpx1Pc6cGA7KqEwHMmHB\nTZMA7lQwDQYJKoZIhvcNAQEFBQADggEBAMoRgH3iTG3t317viLKoY+lNMHUgHuR7\nb3mn9MidJKyYVewe6hCDIN6WY4fUojmMW9wFJWJIo0hRMNHL3n3tq8HP2j20Mxy8\nacPdfGZJa+jiBw72CrIGdobKaFduIlIEDBA1pNdZIJ+EulrtqrMesnIt92WaypIS\n8JycbIgDMCiyC0ENHEk8UWlC6429c7OZAsplMTbHME/1R4btxjkdfrYZJjdJ2yL2\n8cjZDUDMCPTdW/ycP07Gkq30RB5tACB5aZdaCn2YaKC8FsEdhff4X7xEOfOEHWEq\nSRxADDj8Lx1MT6QpR07hCiDyHfTCtbqzI0iGjX63Oh7xXSa0f+JVTa8=\n-----END CERTIFICATE-----\n",
+ }
+ th.AssertDeepEquals(t, expected, cm)
+}
+
+func TestDeleteCert(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteCertResponse(t, lbID, certID)
+
+ err := DeleteCert(client.ServiceClient(), lbID, certID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/ssl/results.go b/rackspace/lb/v1/ssl/results.go
new file mode 100644
index 0000000..ead9fcd
--- /dev/null
+++ b/rackspace/lb/v1/ssl/results.go
@@ -0,0 +1,148 @@
+package ssl
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SSLTermConfig represents the SSL configuration for a particular load balancer.
+type SSLTermConfig struct {
+ // The port on which the SSL termination load balancer listens for secure
+ // traffic. The value must be unique to the existing LB protocol/port
+ // combination
+ SecurePort int `mapstructure:"securePort"`
+
+ // The private key for the SSL certificate which is validated and verified
+ // against the provided certificates.
+ PrivateKey string `mapstructure:"privatekey"`
+
+ // The certificate used for SSL termination, which is validated and verified
+ // against the key and intermediate certificate if provided.
+ Certificate string
+
+ // The intermediate certificate (for the user). The intermediate certificate
+ // is validated and verified against the key and certificate credentials
+ // provided. A user may only provide this value when accompanied by a
+ // Certificate, PrivateKey, and SecurePort. It may not be added or updated as
+ // a single attribute in a future operation.
+ IntCertificate string `mapstructure:"intermediatecertificate"`
+
+ // Determines if the load balancer is enabled to terminate SSL traffic or not.
+ // If this is set to false, the load balancer retains its specified SSL
+ // attributes but does not terminate SSL traffic.
+ Enabled bool
+
+ // Determines if the load balancer can only accept secure traffic. If set to
+ // true, the load balancer will not accept non-secure traffic.
+ SecureTrafficOnly bool
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a GetResult as a SSLTermConfig struct, if possible.
+func (r GetResult) Extract() (*SSLTermConfig, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ SSL SSLTermConfig `mapstructure:"sslTermination"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.SSL, err
+}
+
+// Certificate represents an SSL certificate associated with an SSL-terminated
+// HTTP load balancer.
+type Certificate struct {
+ ID int
+ HostName string
+ Certificate string
+ IntCertificate string `mapstructure:"intermediateCertificate"`
+}
+
+// CertPage represents a page of certificates.
+type CertPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a CertMappingPage struct is empty.
+func (p CertPage) IsEmpty() (bool, error) {
+ is, err := ExtractCerts(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractCerts accepts a Page struct, specifically a CertPage struct, and
+// extracts the elements into a slice of Cert structs. In other words, a generic
+// collection is mapped into a relevant slice.
+func ExtractCerts(page pagination.Page) ([]Certificate, error) {
+ type NestedMap struct {
+ Cert Certificate `mapstructure:"certificateMapping" json:"certificateMapping"`
+ }
+ var resp struct {
+ Certs []NestedMap `mapstructure:"certificateMappings" json:"certificateMappings"`
+ }
+
+ err := mapstructure.Decode(page.(CertPage).Body, &resp)
+
+ slice := []Certificate{}
+ for _, cert := range resp.Certs {
+ slice = append(slice, cert.Cert)
+ }
+
+ return slice, err
+}
+
+type certResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a result as a CertMapping struct, if possible.
+func (r certResult) Extract() (*Certificate, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Cert Certificate `mapstructure:"certificateMapping"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.Cert, err
+}
+
+// CreateCertResult represents the result of an CreateCert operation.
+type CreateCertResult struct {
+ certResult
+}
+
+// GetCertResult represents the result of a GetCert operation.
+type GetCertResult struct {
+ certResult
+}
+
+// UpdateCertResult represents the result of an UpdateCert operation.
+type UpdateCertResult struct {
+ certResult
+}
diff --git a/rackspace/lb/v1/ssl/urls.go b/rackspace/lb/v1/ssl/urls.go
new file mode 100644
index 0000000..aa814b3
--- /dev/null
+++ b/rackspace/lb/v1/ssl/urls.go
@@ -0,0 +1,25 @@
+package ssl
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ sslPath = "ssltermination"
+ certPath = "certificatemappings"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), sslPath)
+}
+
+func certURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath)
+}
+
+func certResourceURL(c *gophercloud.ServiceClient, id, certID int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), sslPath, certPath, strconv.Itoa(certID))
+}
diff --git a/rackspace/lb/v1/throttle/doc.go b/rackspace/lb/v1/throttle/doc.go
new file mode 100644
index 0000000..1ed605d
--- /dev/null
+++ b/rackspace/lb/v1/throttle/doc.go
@@ -0,0 +1,5 @@
+/*
+Package throttle provides information and interaction with the Connection
+Throttling feature of the Rackspace Cloud Load Balancer service.
+*/
+package throttle
diff --git a/rackspace/lb/v1/throttle/fixtures.go b/rackspace/lb/v1/throttle/fixtures.go
new file mode 100644
index 0000000..40223f6
--- /dev/null
+++ b/rackspace/lb/v1/throttle/fixtures.go
@@ -0,0 +1,61 @@
+package throttle
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(id int) string {
+ return "/loadbalancers/" + strconv.Itoa(id) + "/connectionthrottle"
+}
+
+func mockGetResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "connectionThrottle": {
+ "maxConnections": 100
+ }
+}
+`)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "connectionThrottle": {
+ "maxConnectionRate": 0,
+ "maxConnections": 200,
+ "minConnections": 0,
+ "rateInterval": 0
+ }
+}
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/throttle/requests.go b/rackspace/lb/v1/throttle/requests.go
new file mode 100644
index 0000000..8c2e4be
--- /dev/null
+++ b/rackspace/lb/v1/throttle/requests.go
@@ -0,0 +1,95 @@
+package throttle
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package.
+type CreateOptsBuilder interface {
+ ToCTCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Required - the maximum amount of connections per IP address to allow per LB.
+ MaxConnections int
+
+ // Deprecated as of v1.22.
+ MaxConnectionRate int
+
+ // Deprecated as of v1.22.
+ MinConnections int
+
+ // Deprecated as of v1.22.
+ RateInterval int
+}
+
+// ToCTCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCTCreateMap() (map[string]interface{}, error) {
+ ct := make(map[string]interface{})
+
+ if opts.MaxConnections < 0 || opts.MaxConnections > 100000 {
+ return ct, errors.New("MaxConnections must be an int between 0 and 100000")
+ }
+
+ ct["maxConnections"] = opts.MaxConnections
+ ct["maxConnectionRate"] = opts.MaxConnectionRate
+ ct["minConnections"] = opts.MinConnections
+ ct["rateInterval"] = opts.RateInterval
+
+ return map[string]interface{}{"connectionThrottle": ct}, nil
+}
+
+// Create is the operation responsible for creating or updating the connection
+// throttling configuration for a load balancer.
+func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToCTCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Get is the operation responsible for showing the details of the connection
+// throttling configuration for a load balancer.
+func Get(c *gophercloud.ServiceClient, lbID int) GetResult {
+ var res GetResult
+
+ _, res.Err = perigee.Request("GET", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for deleting the connection throttling
+// configuration for a load balancer.
+func Delete(c *gophercloud.ServiceClient, lbID int) DeleteResult {
+ var res DeleteResult
+
+ _, res.Err = perigee.Request("DELETE", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/throttle/requests_test.go b/rackspace/lb/v1/throttle/requests_test.go
new file mode 100644
index 0000000..6e9703f
--- /dev/null
+++ b/rackspace/lb/v1/throttle/requests_test.go
@@ -0,0 +1,44 @@
+package throttle
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const lbID = 12345
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{MaxConnections: 200}
+ err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockGetResponse(t, lbID)
+
+ sp, err := Get(client.ServiceClient(), lbID).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &ConnectionThrottle{MaxConnections: 100}
+ th.AssertDeepEquals(t, expected, sp)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID)
+
+ err := Delete(client.ServiceClient(), lbID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/throttle/results.go b/rackspace/lb/v1/throttle/results.go
new file mode 100644
index 0000000..db93c6f
--- /dev/null
+++ b/rackspace/lb/v1/throttle/results.go
@@ -0,0 +1,43 @@
+package throttle
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+)
+
+// ConnectionThrottle represents the connection throttle configuration for a
+// particular load balancer.
+type ConnectionThrottle struct {
+ MaxConnections int
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets a GetResult as a SP, if possible.
+func (r GetResult) Extract() (*ConnectionThrottle, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ CT ConnectionThrottle `mapstructure:"connectionThrottle"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+
+ return &response.CT, err
+}
diff --git a/rackspace/lb/v1/throttle/urls.go b/rackspace/lb/v1/throttle/urls.go
new file mode 100644
index 0000000..b77f0ac
--- /dev/null
+++ b/rackspace/lb/v1/throttle/urls.go
@@ -0,0 +1,16 @@
+package throttle
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ ctPath = "connectionthrottle"
+)
+
+func rootURL(c *gophercloud.ServiceClient, id int) string {
+ return c.ServiceURL(path, strconv.Itoa(id), ctPath)
+}
diff --git a/rackspace/lb/v1/vips/doc.go b/rackspace/lb/v1/vips/doc.go
new file mode 100644
index 0000000..5c3846d
--- /dev/null
+++ b/rackspace/lb/v1/vips/doc.go
@@ -0,0 +1,13 @@
+/*
+Package vips provides information and interaction with the Virtual IP API
+resource for the Rackspace Cloud Load Balancer service.
+
+A virtual IP (VIP) makes a load balancer accessible by clients. The load
+balancing service supports either a public VIP, routable on the public Internet,
+or a ServiceNet address, routable only within the region in which the load
+balancer resides.
+
+All load balancers must have at least one virtual IP associated with them at
+all times.
+*/
+package vips
diff --git a/rackspace/lb/v1/vips/fixtures.go b/rackspace/lb/v1/vips/fixtures.go
new file mode 100644
index 0000000..158759f
--- /dev/null
+++ b/rackspace/lb/v1/vips/fixtures.go
@@ -0,0 +1,88 @@
+package vips
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func _rootURL(lbID int) string {
+ return "/loadbalancers/" + strconv.Itoa(lbID) + "/virtualips"
+}
+
+func mockListResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), 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, `
+{
+ "virtualIps": [
+ {
+ "id": 1000,
+ "address": "206.10.10.210",
+ "type": "PUBLIC"
+ }
+ ]
+}
+ `)
+ })
+}
+
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "type":"PUBLIC",
+ "ipVersion":"IPV6"
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "address":"fd24:f480:ce44:91bc:1af2:15ff:0000:0002",
+ "id":9000134,
+ "type":"PUBLIC",
+ "ipVersion":"IPV6"
+}
+ `)
+ })
+}
+
+func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ r.ParseForm()
+
+ for k, v := range ids {
+ fids := r.Form["id"]
+ th.AssertEquals(t, strconv.Itoa(v), fids[k])
+ }
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+func mockDeleteResponse(t *testing.T, lbID, vipID int) {
+ url := _rootURL(lbID) + "/" + strconv.Itoa(vipID)
+ th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/rackspace/lb/v1/vips/requests.go b/rackspace/lb/v1/vips/requests.go
new file mode 100644
index 0000000..42f0c1d
--- /dev/null
+++ b/rackspace/lb/v1/vips/requests.go
@@ -0,0 +1,112 @@
+package vips
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// load balancer virtual IP addresses.
+func List(client *gophercloud.ServiceClient, loadBalancerID int) pagination.Pager {
+ url := rootURL(client, loadBalancerID)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return VIPPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToVIPCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Optional - the ID of an existing virtual IP. By doing this, you are
+ // allowing load balancers to share IPV6 addresses.
+ ID string
+
+ // Optional - the type of address.
+ Type Type
+
+ // Optional - the version of address.
+ Version Version
+}
+
+// ToVIPCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToVIPCreateMap() (map[string]interface{}, error) {
+ lb := make(map[string]interface{})
+
+ if opts.ID != "" {
+ lb["id"] = opts.ID
+ }
+ if opts.Type != "" {
+ lb["type"] = opts.Type
+ }
+ if opts.Version != "" {
+ lb["ipVersion"] = opts.Version
+ }
+
+ return lb, nil
+}
+
+// Create is the operation responsible for assigning a new Virtual IP to an
+// existing load balancer resource. Currently, only version 6 IP addresses may
+// be added.
+func Create(c *gophercloud.ServiceClient, lbID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToVIPCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(c, lbID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// BulkDelete is the operation responsible for batch deleting multiple VIPs in
+// a single operation. It accepts a slice of integer IDs and will remove them
+// from the load balancer. The maximum limit is 10 VIP removals at once.
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, vipIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(vipIDs) > 10 || len(vipIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 VIP IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += gophercloud.IDSliceToQueryString("id", vipIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+// Delete is the operation responsible for permanently deleting a VIP.
+func Delete(c *gophercloud.ServiceClient, lbID, vipID int) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, lbID, vipID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return res
+}
diff --git a/rackspace/lb/v1/vips/requests_test.go b/rackspace/lb/v1/vips/requests_test.go
new file mode 100644
index 0000000..74ac461
--- /dev/null
+++ b/rackspace/lb/v1/vips/requests_test.go
@@ -0,0 +1,87 @@
+package vips
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ vipID = 67890
+ vipID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVIPs(page)
+ th.AssertNoErr(t, err)
+
+ expected := []VIP{
+ VIP{ID: 1000, Address: "206.10.10.210", Type: "PUBLIC"},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ Type: "PUBLIC",
+ Version: "IPV6",
+ }
+
+ vip, err := Create(client.ServiceClient(), lbID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &VIP{
+ Address: "fd24:f480:ce44:91bc:1af2:15ff:0000:0002",
+ ID: 9000134,
+ Type: "PUBLIC",
+ Version: "IPV6",
+ }
+
+ th.CheckDeepEquals(t, expected, vip)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{vipID, vipID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockDeleteResponse(t, lbID, vipID)
+
+ err := Delete(client.ServiceClient(), lbID, vipID).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/vips/results.go b/rackspace/lb/v1/vips/results.go
new file mode 100644
index 0000000..678b2af
--- /dev/null
+++ b/rackspace/lb/v1/vips/results.go
@@ -0,0 +1,89 @@
+package vips
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// VIP represents a Virtual IP API resource.
+type VIP struct {
+ Address string `json:"address,omitempty"`
+ ID int `json:"id,omitempty"`
+ Type Type `json:"type,omitempty"`
+ Version Version `json:"ipVersion,omitempty" mapstructure:"ipVersion"`
+}
+
+// Version represents the version of a VIP.
+type Version string
+
+// Convenient constants to use for type
+const (
+ IPV4 Version = "IPV4"
+ IPV6 Version = "IPV6"
+)
+
+// Type represents the type of a VIP.
+type Type string
+
+const (
+ // PUBLIC indicates a VIP type that is routable on the public Internet.
+ PUBLIC Type = "PUBLIC"
+
+ // PRIVATE indicates a VIP type that is routable only on ServiceNet.
+ PRIVATE Type = "SERVICENET"
+)
+
+// VIPPage is the page returned by a pager when traversing over a collection
+// of VIPs.
+type VIPPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether a VIPPage struct is empty.
+func (p VIPPage) IsEmpty() (bool, error) {
+ is, err := ExtractVIPs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractVIPs accepts a Page struct, specifically a VIPPage struct, and
+// extracts the elements into a slice of VIP structs. In other words, a
+// generic collection is mapped into a relevant slice.
+func ExtractVIPs(page pagination.Page) ([]VIP, error) {
+ var resp struct {
+ VIPs []VIP `mapstructure:"virtualIps" json:"virtualIps"`
+ }
+
+ err := mapstructure.Decode(page.(VIPPage).Body, &resp)
+
+ return resp.VIPs, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+func (r commonResult) Extract() (*VIP, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ resp := &VIP{}
+ err := mapstructure.Decode(r.Body, resp)
+
+ return resp, err
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/vips/urls.go b/rackspace/lb/v1/vips/urls.go
new file mode 100644
index 0000000..28f063a
--- /dev/null
+++ b/rackspace/lb/v1/vips/urls.go
@@ -0,0 +1,20 @@
+package vips
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ lbPath = "loadbalancers"
+ vipPath = "virtualips"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, nodeID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath, strconv.Itoa(nodeID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(lbPath, strconv.Itoa(lbID), vipPath)
+}