Merge pull request #346 from haklop/fix-pool-members

Add a missing omitempty on the CreateOpts struct of LBaaS members
diff --git a/.travis.yml b/.travis.yml
index cf4f8ca..946f98c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,8 @@
 go:
   - 1.1
   - 1.2
+  - 1.3
+  - 1.4
   - tip
 script: script/cibuild
 after_success:
@@ -12,3 +14,4 @@
   - go get github.com/mattn/goveralls
   - export PATH=$PATH:$HOME/gopath/bin/
   - goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
+sudo: false
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/UPGRADING.md b/UPGRADING.md
index da3758b..a702cfc 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -238,7 +238,7 @@
   imageList, err := images.ExtractImages(page)
 
   for _, i := range imageList {
-    // "i" will be a images.Image
+    // "i" will be an images.Image
   }
 })
 ```
@@ -284,7 +284,7 @@
   imageList, err := images.ExtractImages(page)
 
   for _, i := range imageList {
-    // "i" will be a images.Image
+    // "i" will be an images.Image
   }
 })
 ```
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
index 9132ee5..7741aa9 100644
--- a/acceptance/openstack/blockstorage/v1/snapshots_test.go
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -13,9 +13,9 @@
 
 func TestSnapshots(t *testing.T) {
 
-	client, err := newClient()
+	client, err := newClient(t)
 	th.AssertNoErr(t, err)
-	
+
 	v, err := volumes.Create(client, &volumes.CreateOpts{
 		Name: "gophercloud-test-volume",
 		Size: 1,
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
index 99da39a..7760427 100644
--- a/acceptance/openstack/blockstorage/v1/volumes_test.go
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -13,7 +13,7 @@
 	th "github.com/rackspace/gophercloud/testhelper"
 )
 
-func newClient() (*gophercloud.ServiceClient, error) {
+func newClient(t *testing.T) (*gophercloud.ServiceClient, error) {
 	ao, err := openstack.AuthOptionsFromEnv()
 	th.AssertNoErr(t, err)
 
@@ -26,7 +26,7 @@
 }
 
 func TestVolumes(t *testing.T) {
-	client, err := newClient()
+	client, err := newClient(t)
 	th.AssertNoErr(t, err)
 
 	cv, err := volumes.Create(client, &volumes.CreateOpts{
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
index 5adcd81..000bc01 100644
--- a/acceptance/openstack/blockstorage/v1/volumetypes_test.go
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -12,7 +12,7 @@
 )
 
 func TestVolumeTypes(t *testing.T) {
-	client, err := newClient()
+	client, err := newClient(t)
 	th.AssertNoErr(t, err)
 
 	vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
index d08abe6..add0e5f 100644
--- a/acceptance/openstack/compute/v2/bootfromvolume_test.go
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -5,10 +5,10 @@
 import (
 	"testing"
 
+	"github.com/rackspace/gophercloud/acceptance/tools"
 	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
 	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
 	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/smashwilson/gophercloud/acceptance/tools"
 )
 
 func TestBootFromVolume(t *testing.T) {
@@ -37,14 +37,19 @@
 
 	serverCreateOpts := servers.CreateOpts{
 		Name:      name,
-		FlavorRef: "3",
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
 	}
 	server, err := bootfromvolume.Create(client, bootfromvolume.CreateOptsExt{
 		serverCreateOpts,
 		bd,
 	}).Extract()
 	th.AssertNoErr(t, err)
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
 	t.Logf("Created server: %+v\n", server)
-	//defer deleteServer(t, client, server)
+	defer servers.Delete(client, server.ID)
 	t.Logf("Deleting server [%s]...", name)
 }
diff --git a/acceptance/openstack/compute/v2/compute_test.go b/acceptance/openstack/compute/v2/compute_test.go
index 46eb9ff..33e49fe 100644
--- a/acceptance/openstack/compute/v2/compute_test.go
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance common
 
 package v2
 
diff --git a/acceptance/openstack/compute/v2/keypairs_test.go b/acceptance/openstack/compute/v2/keypairs_test.go
new file mode 100644
index 0000000..3e12d6b
--- /dev/null
+++ b/acceptance/openstack/compute/v2/keypairs_test.go
@@ -0,0 +1,74 @@
+// +build acceptance
+
+package v2
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"testing"
+
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+
+	"code.google.com/p/go.crypto/ssh"
+)
+
+const keyName = "gophercloud_test_key_pair"
+
+func TestCreateServerWithKeyPair(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	if testing.Short() {
+		t.Skip("Skipping test that requires server creation in short mode.")
+	}
+
+	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	publicKey := privateKey.PublicKey
+	pub, err := ssh.NewPublicKey(&publicKey)
+	th.AssertNoErr(t, err)
+	pubBytes := ssh.MarshalAuthorizedKey(pub)
+	pk := string(pubBytes)
+
+	kp, err := keypairs.Create(client, keypairs.CreateOpts{
+		Name:      keyName,
+		PublicKey: pk,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created key pair: %s\n", kp)
+
+	choices, err := ComputeChoicesFromEnv()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("Gophercloud-", 8)
+	t.Logf("Creating server [%s] with key pair.", name)
+
+	serverCreateOpts := servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
+	}
+
+	server, err := servers.Create(client, keypairs.CreateOptsExt{
+		serverCreateOpts,
+		keyName,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	defer servers.Delete(client, server.ID)
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+
+	server, err = servers.Get(client, server.ID).Extract()
+	t.Logf("Created server: %+v\n", server)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, server.KeyName, keyName)
+
+	t.Logf("Deleting key pair [%s]...", kp.Name)
+	err = keypairs.Delete(client, keyName).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleting server [%s]...", name)
+}
diff --git a/acceptance/openstack/compute/v2/secdefrules_test.go b/acceptance/openstack/compute/v2/secdefrules_test.go
new file mode 100644
index 0000000..78b0798
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secdefrules_test.go
@@ -0,0 +1,72 @@
+// +build acceptance compute defsecrules
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	dsr "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/defsecrules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecDefRules(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	id := createDefRule(t, client)
+
+	listDefRules(t, client)
+
+	getDefRule(t, client, id)
+
+	deleteDefRule(t, client, id)
+}
+
+func createDefRule(t *testing.T, client *gophercloud.ServiceClient) string {
+	opts := dsr.CreateOpts{
+		FromPort:   tools.RandomInt(80, 89),
+		ToPort:     tools.RandomInt(90, 99),
+		IPProtocol: "TCP",
+		CIDR:       "0.0.0.0/0",
+	}
+
+	rule, err := dsr.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created default rule %s", rule.ID)
+
+	return rule.ID
+}
+
+func listDefRules(t *testing.T, client *gophercloud.ServiceClient) {
+	err := dsr.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		drList, err := dsr.ExtractDefaultRules(page)
+		th.AssertNoErr(t, err)
+
+		for _, dr := range drList {
+			t.Logf("Listing default rule %s: Name [%s] From Port [%s] To Port [%s] Protocol [%s]",
+				dr.ID, dr.FromPort, dr.ToPort, dr.IPProtocol)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	rule, err := dsr.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting rule %s: %#v", id, rule)
+}
+
+func deleteDefRule(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := dsr.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted rule %s", id)
+}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
new file mode 100644
index 0000000..4f50739
--- /dev/null
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -0,0 +1,177 @@
+// +build acceptance compute secgroups
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecGroups(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	serverID, needsDeletion := findServer(t, client)
+
+	groupID := createSecGroup(t, client)
+
+	listSecGroups(t, client)
+
+	newName := tools.RandomString("secgroup_", 5)
+	updateSecGroup(t, client, groupID, newName)
+
+	getSecGroup(t, client, groupID)
+
+	addRemoveRules(t, client, groupID)
+
+	addServerToSecGroup(t, client, serverID, newName)
+
+	removeServerFromSecGroup(t, client, serverID, newName)
+
+	if needsDeletion {
+		servers.Delete(client, serverID)
+	}
+
+	deleteSecGroup(t, client, groupID)
+}
+
+func createSecGroup(t *testing.T, client *gophercloud.ServiceClient) string {
+	opts := secgroups.CreateOpts{
+		Name:        tools.RandomString("secgroup_", 5),
+		Description: "something",
+	}
+
+	group, err := secgroups.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Created secgroup %s %s", group.ID, group.Name)
+
+	return group.ID
+}
+
+func listSecGroups(t *testing.T, client *gophercloud.ServiceClient) {
+	err := secgroups.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		secGrpList, err := secgroups.ExtractSecurityGroups(page)
+		th.AssertNoErr(t, err)
+
+		for _, sg := range secGrpList {
+			t.Logf("Listing secgroup %s: Name [%s] Desc [%s] TenantID [%s]", sg.ID,
+				sg.Name, sg.Description, sg.TenantID)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func updateSecGroup(t *testing.T, client *gophercloud.ServiceClient, id, newName string) {
+	opts := secgroups.UpdateOpts{
+		Name:        newName,
+		Description: tools.RandomString("dec_", 10),
+	}
+	group, err := secgroups.Update(client, id, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Updated %s's name to %s", group.ID, group.Name)
+}
+
+func getSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	group, err := secgroups.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Getting %s: %#v", id, group)
+}
+
+func addRemoveRules(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	opts := secgroups.CreateRuleOpts{
+		ParentGroupID: id,
+		FromPort:      22,
+		ToPort:        22,
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := secgroups.CreateRule(client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Adding rule %s to group %s", rule.ID, id)
+
+	err = secgroups.DeleteRule(client, rule.ID).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted rule %s from group %s", rule.ID, id)
+}
+
+func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) {
+	var serverID string
+	var needsDeletion bool
+
+	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 {
+			serverID = s.ID
+			needsDeletion = false
+
+			t.Logf("Found an existing server: ID [%s]", serverID)
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if serverID == "" {
+		t.Log("No server found, creating one")
+
+		choices, err := ComputeChoicesFromEnv()
+		th.AssertNoErr(t, err)
+
+		opts := &servers.CreateOpts{
+			Name:      tools.RandomString("secgroup_test_", 5),
+			ImageRef:  choices.ImageID,
+			FlavorRef: choices.FlavorID,
+		}
+
+		s, err := servers.Create(client, opts).Extract()
+		th.AssertNoErr(t, err)
+		serverID = s.ID
+
+		t.Logf("Created server %s, waiting for it to build", s.ID)
+		err = servers.WaitForStatus(client, serverID, "ACTIVE", 300)
+		th.AssertNoErr(t, err)
+
+		needsDeletion = true
+	}
+
+	return serverID, needsDeletion
+}
+
+func addServerToSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) {
+	err := secgroups.AddServerToGroup(client, serverID, groupName).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Adding group %s to server %s", groupName, serverID)
+}
+
+func removeServerFromSecGroup(t *testing.T, client *gophercloud.ServiceClient, serverID, groupName string) {
+	err := secgroups.RemoveServerFromGroup(client, serverID, groupName).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Removing group %s from server %s", groupName, serverID)
+}
+
+func deleteSecGroup(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := secgroups.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Deleted group %s", id)
+}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index e223c18..d52a9d3 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -12,6 +12,7 @@
 	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
 	"github.com/rackspace/gophercloud/openstack/networking/v2/networks"
 	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
 )
 
 func TestListServers(t *testing.T) {
@@ -94,6 +95,8 @@
 	name := tools.RandomString("ACPTTEST", 16)
 	t.Logf("Attempting to create server: %s\n", name)
 
+	pwd := tools.MakeNewPassword("")
+
 	server, err := servers.Create(client, servers.CreateOpts{
 		Name:      name,
 		FlavorRef: choices.FlavorID,
@@ -101,11 +104,14 @@
 		Networks: []servers.Network{
 			servers.Network{UUID: network.ID},
 		},
+		AdminPass: pwd,
 	}).Extract()
 	if err != nil {
 		t.Fatalf("Unable to create server: %v", err)
 	}
 
+	th.AssertEquals(t, pwd, server.AdminPass)
+
 	return server, err
 }
 
@@ -391,3 +397,54 @@
 		t.Fatal(err)
 	}
 }
+
+func TestServerMetadata(t *testing.T) {
+	t.Parallel()
+
+	choices, err := ComputeChoicesFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{
+		"foo":  "bar",
+		"this": "that",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("UpdateMetadata result: %+v\n", metadata)
+
+	err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr()
+	th.AssertNoErr(t, err)
+
+	metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{
+		"foo": "baz",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("CreateMetadatum result: %+v\n", metadata)
+
+	metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadatum result: %+v\n", metadata)
+	th.AssertEquals(t, "baz", metadata["foo"])
+
+	metadata, err = servers.Metadata(client, server.ID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Metadata result: %+v\n", metadata)
+
+	metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("ResetMetadata result: %+v\n", metadata)
+	th.AssertDeepEquals(t, map[string]string{}, metadata)
+}
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
index 2b4e062..d1fa1e3 100644
--- a/acceptance/openstack/identity/v2/extension_test.go
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
index feae233..96bf1fd 100644
--- a/acceptance/openstack/identity/v2/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/role_test.go b/acceptance/openstack/identity/v2/role_test.go
new file mode 100644
index 0000000..ba243fe
--- /dev/null
+++ b/acceptance/openstack/identity/v2/role_test.go
@@ -0,0 +1,58 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+	client := authenticatedClient(t)
+
+	tenantID := findTenant(t, client)
+	userID := createUser(t, client, tenantID)
+	roleID := listRoles(t, client)
+
+	addUserRole(t, client, tenantID, userID, roleID)
+
+	deleteUserRole(t, client, tenantID, userID, roleID)
+
+	deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+	var roleID string
+
+	err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		roleList, err := roles.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		for _, role := range roleList {
+			t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+			roleID = role.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+	err := roles.AddUserRole(client, tenantID, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID, roleID string) {
+	err := roles.DeleteUserRole(client, tenantID, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
index 2054598..578fc48 100644
--- a/acceptance/openstack/identity/v2/tenant_test.go
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
index 0632a48..d903140 100644
--- a/acceptance/openstack/identity/v2/token_test.go
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -1,4 +1,4 @@
-// +build acceptance
+// +build acceptance identity
 
 package v2
 
diff --git a/acceptance/openstack/identity/v2/user_test.go b/acceptance/openstack/identity/v2/user_test.go
new file mode 100644
index 0000000..fe73d19
--- /dev/null
+++ b/acceptance/openstack/identity/v2/user_test.go
@@ -0,0 +1,127 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+	"github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+	client := authenticatedClient(t)
+
+	tenantID := findTenant(t, client)
+
+	userID := createUser(t, client, tenantID)
+
+	listUsers(t, client)
+
+	getUser(t, client, userID)
+
+	updateUser(t, client, userID)
+
+	listUserRoles(t, client, tenantID, userID)
+
+	deleteUser(t, client, userID)
+}
+
+func findTenant(t *testing.T, client *gophercloud.ServiceClient) string {
+	var tenantID string
+	err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		tenantList, err := tenants.ExtractTenants(page)
+		th.AssertNoErr(t, err)
+
+		for _, t := range tenantList {
+			tenantID = t.ID
+			break
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	return tenantID
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient, tenantID string) string {
+	t.Log("Creating user")
+
+	opts := users.CreateOpts{
+		Name:     tools.RandomString("user_", 5),
+		Enabled:  users.Disabled,
+		TenantID: tenantID,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created user %s on tenant %s", user.ID, tenantID)
+
+	return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+	err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		userList, err := users.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		for _, user := range userList {
+			t.Logf("Listing user: ID [%s] Name [%s] Email [%s] Enabled? [%s]",
+				user.ID, user.Name, user.Email, strconv.FormatBool(user.Enabled))
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	_, err := users.Get(client, userID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	opts := users.UpdateOpts{Name: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+	user, err := users.Update(client, userID, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated user %s: Name [%s] Email [%s]", userID, user.Name, user.Email)
+}
+
+func listUserRoles(t *testing.T, client *gophercloud.ServiceClient, tenantID, userID string) {
+	count := 0
+	err := users.ListRoles(client, tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+
+		roleList, err := users.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		t.Logf("Listing roles for user %s", userID)
+
+		for _, r := range roleList {
+			t.Logf("- %s (%s)", r.Name, r.ID)
+		}
+
+		return true, nil
+	})
+
+	if count == 0 {
+		t.Logf("No roles for user %s", userID)
+	}
+
+	th.AssertNoErr(t, err)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	res := users.Delete(client, userID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted user %s", userID)
+}
diff --git a/acceptance/openstack/networking/v2/port_test.go b/acceptance/openstack/networking/v2/port_test.go
index 7f22dbd..03e8e27 100644
--- a/acceptance/openstack/networking/v2/port_test.go
+++ b/acceptance/openstack/networking/v2/port_test.go
@@ -82,7 +82,7 @@
 		th.AssertNoErr(t, err)
 
 		for _, p := range portList {
-			t.Logf("Port: ID [%s] Name [%s] Status [%d] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
+			t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
 				p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups)
 		}
 
diff --git a/acceptance/rackspace/cdn/v1/base_test.go b/acceptance/rackspace/cdn/v1/base_test.go
new file mode 100644
index 0000000..135f5b3
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/base_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/base"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBaseOps(t *testing.T) {
+	client := newClient(t)
+	t.Log("Retrieving Home Document")
+	testHomeDocumentGet(t, client)
+
+	t.Log("Pinging root URL")
+	testPing(t, client)
+}
+
+func testHomeDocumentGet(t *testing.T, client *gophercloud.ServiceClient) {
+	hd, err := base.Get(client).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved home document: %+v", *hd)
+}
+
+func testPing(t *testing.T, client *gophercloud.ServiceClient) {
+	err := base.Ping(client).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully pinged root URL")
+}
diff --git a/acceptance/rackspace/cdn/v1/common.go b/acceptance/rackspace/cdn/v1/common.go
new file mode 100644
index 0000000..2333ca7
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/common.go
@@ -0,0 +1,23 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+	ao, err := rackspace.AuthOptionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	client, err := rackspace.AuthenticatedClient(ao)
+	th.AssertNoErr(t, err)
+
+	c, err := rackspace.NewCDNV1(client, gophercloud.EndpointOpts{})
+	th.AssertNoErr(t, err)
+	return c
+}
diff --git a/acceptance/rackspace/cdn/v1/flavor_test.go b/acceptance/rackspace/cdn/v1/flavor_test.go
new file mode 100644
index 0000000..f26cff0
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/flavor_test.go
@@ -0,0 +1,47 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/flavors"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestFlavor(t *testing.T) {
+	client := newClient(t)
+
+	t.Log("Listing Flavors")
+	id := testFlavorsList(t, client)
+
+	t.Log("Retrieving Flavor")
+	testFlavorGet(t, client, id)
+}
+
+func testFlavorsList(t *testing.T, client *gophercloud.ServiceClient) string {
+	var id string
+	err := flavors.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		flavorList, err := os.ExtractFlavors(page)
+		th.AssertNoErr(t, err)
+
+		for _, flavor := range flavorList {
+			t.Logf("Listing flavor: ID [%s] Providers [%+v]", flavor.ID, flavor.Providers)
+			id = flavor.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	return id
+}
+
+func testFlavorGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	flavor, err := flavors.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved Flavor: %+v", *flavor)
+}
diff --git a/acceptance/rackspace/cdn/v1/service_test.go b/acceptance/rackspace/cdn/v1/service_test.go
new file mode 100644
index 0000000..c19c241
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/service_test.go
@@ -0,0 +1,93 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/services"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestService(t *testing.T) {
+	client := newClient(t)
+
+	t.Log("Creating Service")
+	loc := testServiceCreate(t, client, "test-site-1")
+	t.Logf("Created service at location: %s", loc)
+
+	defer testServiceDelete(t, client, loc)
+
+	t.Log("Updating Service")
+	testServiceUpdate(t, client, loc)
+
+	t.Log("Retrieving Service")
+	testServiceGet(t, client, loc)
+
+	t.Log("Listing Services")
+	testServiceList(t, client)
+}
+
+func testServiceCreate(t *testing.T, client *gophercloud.ServiceClient, name string) string {
+	createOpts := os.CreateOpts{
+		Name: name,
+		Domains: []os.Domain{
+			os.Domain{
+				Domain: "www." + name + ".com",
+			},
+		},
+		Origins: []os.Origin{
+			os.Origin{
+				Origin: name + ".com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		FlavorID: "cdn",
+	}
+	l, err := services.Create(client, createOpts).Extract()
+	th.AssertNoErr(t, err)
+	return l
+}
+
+func testServiceGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	s, err := services.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Retrieved service: %+v", *s)
+}
+
+func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	opts := os.UpdateOpts{
+		os.Append{
+			Value: os.Domain{Domain: "newDomain.com", Protocol: "http"},
+		},
+	}
+
+	loc, err := services.Update(client, id, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully updated service at location: %s", loc)
+}
+
+func testServiceList(t *testing.T, client *gophercloud.ServiceClient) {
+	err := services.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		serviceList, err := os.ExtractServices(page)
+		th.AssertNoErr(t, err)
+
+		for _, service := range serviceList {
+			t.Logf("Listing service: %+v", service)
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func testServiceDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := services.Delete(client, id).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Successfully deleted service (%s)", id)
+}
diff --git a/acceptance/rackspace/cdn/v1/serviceasset_test.go b/acceptance/rackspace/cdn/v1/serviceasset_test.go
new file mode 100644
index 0000000..c32bf25
--- /dev/null
+++ b/acceptance/rackspace/cdn/v1/serviceasset_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	osServiceAssets "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+	"github.com/rackspace/gophercloud/rackspace/cdn/v1/serviceassets"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestServiceAsset(t *testing.T) {
+	client := newClient(t)
+
+	t.Log("Creating Service")
+	loc := testServiceCreate(t, client, "test-site-2")
+	t.Logf("Created service at location: %s", loc)
+
+	t.Log("Deleting Service Assets")
+	testServiceAssetDelete(t, client, loc)
+}
+
+func testServiceAssetDelete(t *testing.T, client *gophercloud.ServiceClient, url string) {
+	deleteOpts := osServiceAssets.DeleteOpts{
+		All: true,
+	}
+	err := serviceassets.Delete(client, url, deleteOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Log("Successfully deleted all Service Assets")
+}
diff --git a/acceptance/rackspace/compute/v2/bootfromvolume_test.go b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
index 010bf42..d7e6aa7 100644
--- a/acceptance/rackspace/compute/v2/bootfromvolume_test.go
+++ b/acceptance/rackspace/compute/v2/bootfromvolume_test.go
@@ -5,11 +5,11 @@
 import (
 	"testing"
 
+	"github.com/rackspace/gophercloud/acceptance/tools"
 	osBFV "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
 	"github.com/rackspace/gophercloud/rackspace/compute/v2/bootfromvolume"
 	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
 	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/smashwilson/gophercloud/acceptance/tools"
 )
 
 func TestBootFromVolume(t *testing.T) {
@@ -41,6 +41,9 @@
 	}).Extract()
 	th.AssertNoErr(t, err)
 	t.Logf("Created server: %+v\n", server)
-	//defer deleteServer(t, client, server)
-	t.Logf("Deleting server [%s]...", name)
+	defer deleteServer(t, client, server)
+
+	getServer(t, client, server)
+
+	listServers(t, client)
 }
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
index 511f0a9..81c8599 100644
--- a/acceptance/rackspace/compute/v2/servers_test.go
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -39,11 +39,14 @@
 
 	name := tools.RandomString("Gophercloud-", 8)
 
+	pwd := tools.MakeNewPassword("")
+
 	opts := &servers.CreateOpts{
 		Name:       name,
 		ImageRef:   options.imageID,
 		FlavorRef:  options.flavorID,
 		DiskConfig: diskconfig.Manual,
+		AdminPass:  pwd,
 	}
 
 	if keyName != "" {
@@ -59,6 +62,8 @@
 	th.AssertNoErr(t, err)
 	t.Logf("Server created successfully.")
 
+	th.CheckEquals(t, pwd, s.AdminPass)
+
 	return s
 }
 
@@ -120,7 +125,7 @@
 	original := server.AdminPass
 
 	t.Logf("Changing server password.")
-	err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract()
+	err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).ExtractErr()
 	th.AssertNoErr(t, err)
 
 	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
@@ -131,7 +136,7 @@
 func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
 	t.Logf("> servers.Reboot")
 
-	err := servers.Reboot(client, server.ID, os.HardReboot).Extract()
+	err := servers.Reboot(client, server.ID, os.HardReboot).ExtractErr()
 	th.AssertNoErr(t, err)
 
 	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
diff --git a/acceptance/rackspace/identity/v2/pkg.go b/acceptance/rackspace/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/identity/v2/role_test.go b/acceptance/rackspace/identity/v2/role_test.go
new file mode 100644
index 0000000..efaeb75
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/role_test.go
@@ -0,0 +1,59 @@
+// +build acceptance identity roles
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/roles"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestRoles(t *testing.T) {
+	client := authenticatedClient(t)
+
+	userID := createUser(t, client)
+	roleID := listRoles(t, client)
+
+	addUserRole(t, client, userID, roleID)
+
+	deleteUserRole(t, client, userID, roleID)
+
+	deleteUser(t, client, userID)
+}
+
+func listRoles(t *testing.T, client *gophercloud.ServiceClient) string {
+	var roleID string
+
+	err := roles.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		roleList, err := os.ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		for _, role := range roleList {
+			t.Logf("Listing role: ID [%s] Name [%s]", role.ID, role.Name)
+			roleID = role.ID
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	return roleID
+}
+
+func addUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+	err := roles.AddUserRole(client, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Added role %s to user %s", roleID, userID)
+}
+
+func deleteUserRole(t *testing.T, client *gophercloud.ServiceClient, userID, roleID string) {
+	err := roles.DeleteUserRole(client, userID, roleID).ExtractErr()
+	th.AssertNoErr(t, err)
+	t.Logf("Removed role %s from user %s", roleID, userID)
+}
diff --git a/acceptance/rackspace/identity/v2/user_test.go b/acceptance/rackspace/identity/v2/user_test.go
new file mode 100644
index 0000000..28c0c83
--- /dev/null
+++ b/acceptance/rackspace/identity/v2/user_test.go
@@ -0,0 +1,93 @@
+// +build acceptance identity
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/identity/v2/users"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestUsers(t *testing.T) {
+	client := authenticatedClient(t)
+
+	userID := createUser(t, client)
+
+	listUsers(t, client)
+
+	getUser(t, client, userID)
+
+	updateUser(t, client, userID)
+
+	resetApiKey(t, client, userID)
+
+	deleteUser(t, client, userID)
+}
+
+func createUser(t *testing.T, client *gophercloud.ServiceClient) string {
+	t.Log("Creating user")
+
+	opts := users.CreateOpts{
+		Username: tools.RandomString("user_", 5),
+		Enabled:  os.Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := users.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created user %s", user.ID)
+
+	return user.ID
+}
+
+func listUsers(t *testing.T, client *gophercloud.ServiceClient) {
+	err := users.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		userList, err := os.ExtractUsers(page)
+		th.AssertNoErr(t, err)
+
+		for _, user := range userList {
+			t.Logf("Listing user: ID [%s] Username [%s] Email [%s] Enabled? [%s]",
+				user.ID, user.Username, user.Email, strconv.FormatBool(user.Enabled))
+		}
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
+
+func getUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	_, err := users.Get(client, userID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting user %s", userID)
+}
+
+func updateUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	opts := users.UpdateOpts{Username: tools.RandomString("new_name", 5), Email: "new@foo.com"}
+	user, err := users.Update(client, userID, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated user %s: Username [%s] Email [%s]", userID, user.Username, user.Email)
+}
+
+func deleteUser(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	res := users.Delete(client, userID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted user %s", userID)
+}
+
+func resetApiKey(t *testing.T, client *gophercloud.ServiceClient, userID string) {
+	key, err := users.ResetAPIKey(client, userID).Extract()
+	th.AssertNoErr(t, err)
+
+	if key.APIKey == "" {
+		t.Fatal("failed to reset API key for user")
+	}
+
+	t.Logf("Reset API key for user %s to %s", key.Username, key.APIKey)
+}
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/acceptance/rackspace/networking/v2/common.go b/acceptance/rackspace/networking/v2/common.go
new file mode 100644
index 0000000..8170418
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/common.go
@@ -0,0 +1,39 @@
+package v2
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var Client *gophercloud.ServiceClient
+
+func NewClient() (*gophercloud.ServiceClient, error) {
+	opts, err := rackspace.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+
+	provider, err := rackspace.AuthenticatedClient(opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewNetworkV2(provider, gophercloud.EndpointOpts{
+		Name:   "cloudNetworks",
+		Region: os.Getenv("RS_REGION"),
+	})
+}
+
+func Setup(t *testing.T) {
+	client, err := NewClient()
+	th.AssertNoErr(t, err)
+	Client = client
+}
+
+func Teardown() {
+	Client = nil
+}
diff --git a/acceptance/rackspace/networking/v2/network_test.go b/acceptance/rackspace/networking/v2/network_test.go
new file mode 100644
index 0000000..3862123
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/network_test.go
@@ -0,0 +1,65 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"strconv"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Create a network
+	n, err := networks.Create(Client, os.CreateOpts{Name: "sample_network", AdminStateUp: os.Up}).Extract()
+	th.AssertNoErr(t, err)
+	defer networks.Delete(Client, n.ID)
+	th.AssertEquals(t, "sample_network", n.Name)
+	th.AssertEquals(t, true, n.AdminStateUp)
+	networkID := n.ID
+
+	// List networks
+	pager := networks.List(Client, os.ListOpts{Limit: 2})
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		networkList, err := os.ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+
+		for _, n := range networkList {
+			t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+				n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+
+	// Get a network
+	if networkID == "" {
+		t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+	}
+	n, err = networks.Get(Client, networkID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "ACTIVE", n.Status)
+	th.AssertDeepEquals(t, []string{}, n.Subnets)
+	th.AssertEquals(t, "sample_network", n.Name)
+	th.AssertEquals(t, true, n.AdminStateUp)
+	th.AssertEquals(t, false, n.Shared)
+	th.AssertEquals(t, networkID, n.ID)
+
+	// Update network
+	n, err = networks.Update(Client, networkID, os.UpdateOpts{Name: "new_network_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "new_network_name", n.Name)
+
+	// Delete network
+	res := networks.Delete(Client, networkID)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/rackspace/networking/v2/port_test.go b/acceptance/rackspace/networking/v2/port_test.go
new file mode 100644
index 0000000..3c42bb2
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/port_test.go
@@ -0,0 +1,116 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	osPorts "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/ports"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/subnets"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPortCRUD(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Setup network
+	t.Log("Setting up network")
+	networkID, err := createNetwork()
+	th.AssertNoErr(t, err)
+	defer networks.Delete(Client, networkID)
+
+	// Setup subnet
+	t.Logf("Setting up subnet on network %s", networkID)
+	subnetID, err := createSubnet(networkID)
+	th.AssertNoErr(t, err)
+	defer subnets.Delete(Client, subnetID)
+
+	// Create port
+	t.Logf("Create port based on subnet %s", subnetID)
+	portID := createPort(t, networkID, subnetID)
+
+	// List ports
+	t.Logf("Listing all ports")
+	listPorts(t)
+
+	// Get port
+	if portID == "" {
+		t.Fatalf("In order to retrieve a port, the portID must be set")
+	}
+	p, err := ports.Get(Client, portID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, portID, p.ID)
+
+	// Update port
+	p, err = ports.Update(Client, portID, osPorts.UpdateOpts{Name: "new_port_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "new_port_name", p.Name)
+
+	// Delete port
+	res := ports.Delete(Client, portID)
+	th.AssertNoErr(t, res.Err)
+}
+
+func createPort(t *testing.T, networkID, subnetID string) string {
+	enable := true
+	opts := osPorts.CreateOpts{
+		NetworkID:    networkID,
+		Name:         "my_port",
+		AdminStateUp: &enable,
+		FixedIPs:     []osPorts.IP{osPorts.IP{SubnetID: subnetID}},
+	}
+	p, err := ports.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, networkID, p.NetworkID)
+	th.AssertEquals(t, "my_port", p.Name)
+	th.AssertEquals(t, true, p.AdminStateUp)
+
+	return p.ID
+}
+
+func listPorts(t *testing.T) {
+	count := 0
+	pager := ports.List(Client, osPorts.ListOpts{})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- Page ---")
+
+		portList, err := osPorts.ExtractPorts(page)
+		th.AssertNoErr(t, err)
+
+		for _, p := range portList {
+			t.Logf("Port: ID [%s] Name [%s] Status [%s] MAC addr [%s] Fixed IPs [%#v] Security groups [%#v]",
+				p.ID, p.Name, p.Status, p.MACAddress, p.FixedIPs, p.SecurityGroups)
+		}
+
+		return true, nil
+	})
+
+	th.CheckNoErr(t, err)
+
+	if count == 0 {
+		t.Logf("No pages were iterated over when listing ports")
+	}
+}
+
+func createNetwork() (string, error) {
+	res, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract()
+	return res.ID, err
+}
+
+func createSubnet(networkID string) (string, error) {
+	s, err := subnets.Create(Client, osSubnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  osSubnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: osSubnets.Down,
+	}).Extract()
+	return s.ID, err
+}
diff --git a/acceptance/rackspace/networking/v2/subnet_test.go b/acceptance/rackspace/networking/v2/subnet_test.go
new file mode 100644
index 0000000..c401432
--- /dev/null
+++ b/acceptance/rackspace/networking/v2/subnet_test.go
@@ -0,0 +1,84 @@
+// +build acceptance networking
+
+package v2
+
+import (
+	"testing"
+
+	osNetworks "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	osSubnets "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/networks"
+	"github.com/rackspace/gophercloud/rackspace/networking/v2/subnets"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListSubnets(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	pager := subnets.List(Client, osSubnets.ListOpts{Limit: 2})
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("--- Page ---")
+
+		subnetList, err := osSubnets.ExtractSubnets(page)
+		th.AssertNoErr(t, err)
+
+		for _, s := range subnetList {
+			t.Logf("Subnet: ID [%s] Name [%s] IP Version [%d] CIDR [%s] GatewayIP [%s]",
+				s.ID, s.Name, s.IPVersion, s.CIDR, s.GatewayIP)
+		}
+
+		return true, nil
+	})
+	th.CheckNoErr(t, err)
+}
+
+func TestSubnetCRUD(t *testing.T) {
+	Setup(t)
+	defer Teardown()
+
+	// Setup network
+	t.Log("Setting up network")
+	n, err := networks.Create(Client, osNetworks.CreateOpts{Name: "tmp_network", AdminStateUp: osNetworks.Up}).Extract()
+	th.AssertNoErr(t, err)
+	networkID := n.ID
+	defer networks.Delete(Client, networkID)
+
+	// Create subnet
+	t.Log("Create subnet")
+	enable := false
+	opts := osSubnets.CreateOpts{
+		NetworkID:  networkID,
+		CIDR:       "192.168.199.0/24",
+		IPVersion:  osSubnets.IPv4,
+		Name:       "my_subnet",
+		EnableDHCP: &enable,
+	}
+	s, err := subnets.Create(Client, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, networkID, s.NetworkID)
+	th.AssertEquals(t, "192.168.199.0/24", s.CIDR)
+	th.AssertEquals(t, 4, s.IPVersion)
+	th.AssertEquals(t, "my_subnet", s.Name)
+	th.AssertEquals(t, false, s.EnableDHCP)
+	subnetID := s.ID
+
+	// Get subnet
+	t.Log("Getting subnet")
+	s, err = subnets.Get(Client, subnetID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, subnetID, s.ID)
+
+	// Update subnet
+	t.Log("Update subnet")
+	s, err = subnets.Update(Client, subnetID, osSubnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "new_subnet_name", s.Name)
+
+	// Delete subnet
+	t.Log("Delete subnet")
+	res := subnets.Delete(Client, subnetID)
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
index 61b1d7a..35679b7 100644
--- a/acceptance/tools/tools.go
+++ b/acceptance/tools/tools.go
@@ -1,10 +1,11 @@
-// +build acceptance
+// +build acceptance common
 
 package tools
 
 import (
 	"crypto/rand"
 	"errors"
+	mrand "math/rand"
 	"os"
 	"time"
 
@@ -72,6 +73,12 @@
 	return prefix + string(bytes)
 }
 
+// RandomInt will return a random integer between a specified range.
+func RandomInt(min, max int) int {
+	mrand.Seed(time.Now().Unix())
+	return mrand.Intn(max-min) + min
+}
+
 // Elide returns the first bit of its input string with a suffix of "..." if it's longer than
 // a comfortable 40 characters.
 func Elide(value string) string {
diff --git a/auth_options.go b/auth_options.go
index bc0ef65..19ce5d4 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -1,20 +1,28 @@
 package gophercloud
 
-// AuthOptions allows anyone calling Authenticate to supply the required access
-// credentials. Its fields are the union of those recognized by each identity
-// implementation and provider.
+/*
+AuthOptions stores information needed to authenticate to an OpenStack cluster.
+You can populate one manually, or use a provider's AuthOptionsFromEnv() function
+to read relevant information from the standard environment variables. Pass one
+to a provider's AuthenticatedClient function to authenticate and obtain a
+ProviderClient representing an active session on that provider.
+
+Its fields are the union of those recognized by each identity implementation and
+provider.
+*/
 type AuthOptions struct {
 	// IdentityEndpoint specifies the HTTP endpoint that is required to work with
-	// the Identity API of the appropriate version. Required by the identity
-	// services, but often populated by a provider Client.
+	// the Identity API of the appropriate version. While it's ultimately needed by
+	// all of the identity services, it will often be populated by a provider-level
+	// function.
 	IdentityEndpoint string
 
 	// Username is required if using Identity V2 API. Consult with your provider's
 	// control panel to discover your account's username. In Identity V3, either
-	// UserID or a combination of Username and DomainID or DomainName.
+	// UserID or a combination of Username and DomainID or DomainName are needed.
 	Username, UserID string
 
-	// Exactly one of Password or ApiKey is required for the Identity V2 and V3
+	// Exactly one of Password or APIKey is required for the Identity V2 and V3
 	// APIs. Consult with your provider's control panel to discover your account's
 	// preferred method of authentication.
 	Password, APIKey string
@@ -25,7 +33,7 @@
 
 	// The TenantID and TenantName fields are optional for the Identity V2 API.
 	// Some providers allow you to specify a TenantName instead of the TenantId.
-	// Some require both.  Your provider's authentication policies will determine
+	// Some require both. Your provider's authentication policies will determine
 	// how these fields influence authentication.
 	TenantID, TenantName string
 
@@ -34,5 +42,7 @@
 	// re-authenticate automatically if/when your token expires.  If you set it to
 	// false, it will not cache these settings, but re-authentication will not be
 	// possible.  This setting defaults to false.
+	//
+	// This setting is speculative and is currently not respected!
 	AllowReauth bool
 }
diff --git a/auth_results.go b/auth_results.go
index 1a1faa5..856a233 100644
--- a/auth_results.go
+++ b/auth_results.go
@@ -2,10 +2,9 @@
 
 import "time"
 
-// AuthResults encapsulates the raw results from an authentication request. As OpenStack allows
-// extensions to influence the structure returned in ways that Gophercloud cannot predict at
-// compile-time, you should use type-safe accessors to work with the data represented by this type,
-// such as ServiceCatalog and TokenID.
+// AuthResults [deprecated] is a leftover type from the v0.x days. It was
+// intended to describe common functionality among identity service results, but
+// is not actually used anywhere.
 type AuthResults interface {
 	// TokenID returns the token's ID value from the authentication response.
 	TokenID() (string, error)
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..fb81a9d
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,67 @@
+/*
+Package gophercloud provides a multi-vendor interface to OpenStack-compatible
+clouds. The library has a three-level hierarchy: providers, services, and
+resources.
+
+Provider structs represent the service providers that offer and manage a
+collection of services. Examples of providers include: OpenStack, Rackspace,
+HP. These are defined like so:
+
+  opts := gophercloud.AuthOptions{
+    IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
+    Username: "{username}",
+    Password: "{password}",
+    TenantID: "{tenant_id}",
+  }
+
+  provider, err := openstack.AuthenticatedClient(opts)
+
+Service structs are specific to a provider and handle all of the logic and
+operations for a particular OpenStack service. Examples of services include:
+Compute, Object Storage, Block Storage. In order to define one, you need to
+pass in the parent provider, like so:
+
+  opts := gophercloud.EndpointOpts{Region: "RegionOne"}
+
+  client := openstack.NewComputeV2(provider, opts)
+
+Resource structs are the domain models that services make use of in order
+to work with and represent the state of API resources:
+
+  server, err := servers.Get(client, "{serverId}").Extract()
+
+Intermediate Result structs are returned for API operations, which allow
+generic access to the HTTP headers, response body, and any errors associated
+with the network transaction. To turn a result into a usable resource struct,
+you must call the Extract method which is chained to the response, or an
+Extract function from an applicable extension:
+
+  result := servers.Get(client, "{serverId}")
+
+  // Attempt to extract the disk configuration from the OS-DCF disk config
+  // extension:
+  config, err := diskconfig.ExtractGet(result)
+
+All requests that enumerate a collection return a Pager struct that is used to
+iterate through the results one page at a time. Use the EachPage method on that
+Pager to handle each successive Page in a closure, then use the appropriate
+extraction method from that request's package to interpret that Page as a slice
+of results:
+
+  err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) {
+    s, err := servers.ExtractServers(page)
+    if err != nil {
+      return false, err
+    }
+
+    // Handle the []servers.Server slice.
+
+    // Return "false" or an error to prematurely stop fetching new pages.
+    return true, nil
+  })
+
+This top-level package contains utility functions and data types that are used
+throughout the provider and service packages. Of particular note for end users
+are the AuthOptions and EndpointOpts structs.
+*/
+package gophercloud
diff --git a/endpoint_search.go b/endpoint_search.go
index b6f6b48..5189431 100644
--- a/endpoint_search.go
+++ b/endpoint_search.go
@@ -3,58 +3,85 @@
 import "errors"
 
 var (
-	// ErrServiceNotFound is returned when no service matches the EndpointOpts.
+	// ErrServiceNotFound is returned when no service in a service catalog matches
+	// the provided EndpointOpts. This is generally returned by provider service
+	// factory methods like "NewComputeV2()" and can mean that a service is not
+	// enabled for your account.
 	ErrServiceNotFound = errors.New("No suitable service could be found in the service catalog.")
 
-	// ErrEndpointNotFound is returned when no available endpoints match the provided EndpointOpts.
+	// ErrEndpointNotFound is returned when no available endpoints match the
+	// provided EndpointOpts. This is also generally returned by provider service
+	// factory methods, and usually indicates that a region was specified
+	// incorrectly.
 	ErrEndpointNotFound = errors.New("No suitable endpoint could be found in the service catalog.")
 )
 
-// Availability indicates whether a specific service endpoint is accessible.
-// Identity v2 lists these as different kinds of URLs ("adminURL",
-// "internalURL", and "publicURL"), while v3 lists them as "Interfaces".
+// Availability indicates to whom a specific service endpoint is accessible:
+// the internet at large, internal networks only, or only to administrators.
+// Different identity services use different terminology for these. Identity v2
+// lists them as different kinds of URLs within the service catalog ("adminURL",
+// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an
+// endpoint's response.
 type Availability string
 
 const (
-	// AvailabilityAdmin makes an endpoint only available to administrators.
+	// AvailabilityAdmin indicates that an endpoint is only available to
+	// administrators.
 	AvailabilityAdmin Availability = "admin"
 
-	// AvailabilityPublic makes an endpoint available to everyone.
+	// AvailabilityPublic indicates that an endpoint is available to everyone on
+	// the internet.
 	AvailabilityPublic Availability = "public"
 
-	// AvailabilityInternal makes an endpoint only available within the cluster.
+	// AvailabilityInternal indicates that an endpoint is only available within
+	// the cluster's internal network.
 	AvailabilityInternal Availability = "internal"
 )
 
-// EndpointOpts contains options for finding an endpoint for an Openstack client.
+// EndpointOpts specifies search criteria used by queries against an
+// OpenStack service catalog. The options must contain enough information to
+// unambiguously identify one, and only one, endpoint within the catalog.
+//
+// Usually, these are passed to service client factory functions in a provider
+// package, like "rackspace.NewComputeV2()".
 type EndpointOpts struct {
-	// Type is the service type for the client (e.g., "compute", "object-store").
-	// Required.
+	// Type [required] is the service type for the client (e.g., "compute",
+	// "object-store"). Generally, this will be supplied by the service client
+	// function, but a user-given value will be honored if provided.
 	Type string
 
-	// Name is the service name for the client (e.g., "nova") as it appears in
-	// the service catalog. Services can have the same Type but a different Name,
-	// which is why both Type and Name are sometimes needed. Optional.
+	// Name [optional] is the service name for the client (e.g., "nova") as it
+	// appears in the service catalog. Services can have the same Type but a
+	// different Name, which is why both Type and Name are sometimes needed.
 	Name string
 
-	// Region is the geographic region in which the service resides. Required only
-	// for services that span multiple regions.
+	// Region [required] is the geographic region in which the endpoint resides,
+	// generally specifying which datacenter should house your resources.
+	// Required only for services that span multiple regions.
 	Region string
 
-	// Availability is the visibility of the endpoint to be returned. Valid types
-	// are: AvailabilityPublic, AvailabilityInternal, or AvailabilityAdmin.
-	// Availability is not required, and defaults to AvailabilityPublic.
-	// Not all providers or services offer all Availability options.
+	// Availability [optional] is the visibility of the endpoint to be returned.
+	// Valid types include the constants AvailabilityPublic, AvailabilityInternal,
+	// or AvailabilityAdmin from this package.
+	//
+	// Availability is not required, and defaults to AvailabilityPublic. Not all
+	// providers or services offer all Availability options.
 	Availability Availability
 }
 
-// EndpointLocator is a function that describes how to locate a single endpoint
-// from a service catalog for a specific ProviderClient. It should be set
-// during ProviderClient authentication and used to discover related ServiceClients.
+/*
+EndpointLocator is an internal function to be used by provider implementations.
+
+It provides an implementation that locates a single endpoint from a service
+catalog for a specific ProviderClient based on user-provided EndpointOpts. The
+provider then uses it to discover related ServiceClients.
+*/
 type EndpointLocator func(EndpointOpts) (string, error)
 
-// ApplyDefaults sets EndpointOpts fields if not already set. Currently,
-// EndpointOpts.Availability defaults to the public endpoint.
+// ApplyDefaults is an internal method to be used by provider implementations.
+//
+// It sets EndpointOpts fields if not already set, including a default type.
+// Currently, EndpointOpts.Availability defaults to the public endpoint.
 func (eo *EndpointOpts) ApplyDefaults(t string) {
 	if eo.Type == "" {
 		eo.Type = t
diff --git a/openstack/blockstorage/v1/snapshots/util_test.go b/openstack/blockstorage/v1/snapshots/util_test.go
deleted file mode 100644
index a4c4c82..0000000
--- a/openstack/blockstorage/v1/snapshots/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package snapshots
-
-import (
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/snapshots/1234", func(w http.ResponseWriter, r *http.Request) {
-		time.Sleep(2 * time.Second)
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"snapshot": {
-				"display_name": "snapshot-001",
-				"id": "1234",
-				"status":"available"
-			}
-		}`)
-	})
-
-	err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
-	if err == nil {
-		t.Errorf("Expected error: 'Time Out in WaitFor'")
-	}
-
-	err = WaitForStatus(client.ServiceClient(), "1234", "available", 3)
-	th.CheckNoErr(t, err)
-}
diff --git a/openstack/blockstorage/v1/volumes/util_test.go b/openstack/blockstorage/v1/volumes/util_test.go
deleted file mode 100644
index 24ef3b6..0000000
--- a/openstack/blockstorage/v1/volumes/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package volumes
-
-import (
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/volumes/1234", func(w http.ResponseWriter, r *http.Request) {
-		time.Sleep(3 * time.Second)
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"volume": {
-				"display_name": "vol-001",
-				"id": "1234",
-				"status":"available"
-			}
-		}`)
-	})
-
-	err := WaitForStatus(client.ServiceClient(), "1234", "available", 0)
-	if err == nil {
-		t.Errorf("Expected error: 'Time Out in WaitFor'")
-	}
-
-	err = WaitForStatus(client.ServiceClient(), "1234", "available", 6)
-	th.CheckNoErr(t, err)
-}
diff --git a/openstack/cdn/v1/base/doc.go b/openstack/cdn/v1/base/doc.go
new file mode 100644
index 0000000..f78d4f7
--- /dev/null
+++ b/openstack/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the OpenStack CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/openstack/cdn/v1/base/fixtures.go b/openstack/cdn/v1/base/fixtures.go
new file mode 100644
index 0000000..19b5ece
--- /dev/null
+++ b/openstack/cdn/v1/base/fixtures.go
@@ -0,0 +1,53 @@
+package base
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleGetSuccessfully creates an HTTP handler at `/` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+    {
+        "resources": {
+            "rel/cdn": {
+                "href-template": "services{?marker,limit}",
+                "href-vars": {
+                    "marker": "param/marker",
+                    "limit": "param/limit"
+                },
+                "hints": {
+                    "allow": [
+                        "GET"
+                    ],
+                    "formats": {
+                        "application/json": {}
+                    }
+                }
+            }
+        }
+    }
+    `)
+
+	})
+}
+
+// HandlePingSuccessfully creates an HTTP handler at `/ping` on the test handler
+// mux that responds with a `Ping` response.
+func HandlePingSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/cdn/v1/base/requests.go b/openstack/cdn/v1/base/requests.go
new file mode 100644
index 0000000..9d8632c
--- /dev/null
+++ b/openstack/cdn/v1/base/requests.go
@@ -0,0 +1,30 @@
+package base
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	"github.com/racker/perigee"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) PingResult {
+	var res PingResult
+	_, res.Err = perigee.Request("GET", pingURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+		OmitAccept:  true,
+	})
+	return res
+}
diff --git a/openstack/cdn/v1/base/requests_test.go b/openstack/cdn/v1/base/requests_test.go
new file mode 100644
index 0000000..a8d95f9
--- /dev/null
+++ b/openstack/cdn/v1/base/requests_test.go
@@ -0,0 +1,43 @@
+package base
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(fake.ServiceClient()).Extract()
+	th.CheckNoErr(t, err)
+
+	expected := HomeDocument{
+		"rel/cdn": map[string]interface{}{
+        "href-template": "services{?marker,limit}",
+        "href-vars": map[string]interface{}{
+            "marker": "param/marker",
+            "limit": "param/limit",
+        },
+        "hints": map[string]interface{}{
+            "allow": []string{"GET"},
+            "formats": map[string]interface{}{
+                "application/json": map[string]interface{}{},
+            },
+        },
+    },
+	}
+	th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePingSuccessfully(t)
+
+	err := Ping(fake.ServiceClient()).ExtractErr()
+	th.CheckNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/base/results.go b/openstack/cdn/v1/base/results.go
new file mode 100644
index 0000000..bef1da8
--- /dev/null
+++ b/openstack/cdn/v1/base/results.go
@@ -0,0 +1,35 @@
+package base
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+)
+
+// HomeDocument is a resource that contains all the resources for the CDN API.
+type HomeDocument map[string]interface{}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a home document resource.
+func (r GetResult) Extract() (*HomeDocument, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	submap, ok := r.Body.(map[string]interface{})["resources"]
+	if !ok {
+		return nil, errors.New("Unexpected HomeDocument structure")
+	}
+	casted := HomeDocument(submap.(map[string]interface{}))
+
+	return &casted, nil
+}
+
+// PingResult represents the result of a Ping operation.
+type PingResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/base/urls.go b/openstack/cdn/v1/base/urls.go
new file mode 100644
index 0000000..a95e18b
--- /dev/null
+++ b/openstack/cdn/v1/base/urls.go
@@ -0,0 +1,11 @@
+package base
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL()
+}
+
+func pingURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("ping")
+}
diff --git a/openstack/cdn/v1/flavors/doc.go b/openstack/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..d406698
--- /dev/null
+++ b/openstack/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/openstack/cdn/v1/flavors/fixtures.go b/openstack/cdn/v1/flavors/fixtures.go
new file mode 100644
index 0000000..f413b6b
--- /dev/null
+++ b/openstack/cdn/v1/flavors/fixtures.go
@@ -0,0 +1,82 @@
+package flavors
+
+import (
+  "fmt"
+  "net/http"
+  "testing"
+
+  th "github.com/rackspace/gophercloud/testhelper"
+  fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNFlavorsSuccessfully creates an HTTP handler at `/flavors` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNFlavorsSuccessfully(t *testing.T) {
+  th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) {
+    th.TestMethod(t, r, "GET")
+    th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+    w.Header().Set("Content-Type", "application/json")
+    w.WriteHeader(http.StatusOK)
+    fmt.Fprintf(w, `
+      {
+        "flavors": [
+            {
+                "id": "europe",
+                "providers": [
+                    {
+                        "provider": "Fastly",
+                        "links": [
+                            {
+                                "href": "http://www.fastly.com",
+                                "rel": "provider_url"
+                            }
+                        ]
+                    }
+                ],
+                "links": [
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+                        "rel": "self"
+                    }
+                ]
+            }
+        ]
+    }
+    `)
+  })
+}
+
+// HandleGetCDNFlavorSuccessfully creates an HTTP handler at `/flavors/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNFlavorSuccessfully(t *testing.T) {
+  th.Mux.HandleFunc("/flavors/asia", func(w http.ResponseWriter, r *http.Request) {
+    th.TestMethod(t, r, "GET")
+    th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+    w.Header().Set("Content-Type", "application/json")
+    w.WriteHeader(http.StatusOK)
+    fmt.Fprintf(w, `
+      {
+          "id" : "asia",
+          "providers" : [
+              {
+                  "provider" : "ChinaCache",
+                  "links": [
+                      {
+                          "href": "http://www.chinacache.com",
+                          "rel": "provider_url"
+                      }
+                  ]
+              }
+          ],
+          "links": [
+              {
+                  "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+                  "rel": "self"
+              }
+          ]
+      }
+    `)
+  })
+}
diff --git a/openstack/cdn/v1/flavors/requests.go b/openstack/cdn/v1/flavors/requests.go
new file mode 100644
index 0000000..88ac891
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests.go
@@ -0,0 +1,27 @@
+package flavors
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(c)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return FlavorPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(c, url, createPage)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
diff --git a/openstack/cdn/v1/flavors/requests_test.go b/openstack/cdn/v1/flavors/requests_test.go
new file mode 100644
index 0000000..7ddf1b1
--- /dev/null
+++ b/openstack/cdn/v1/flavors/requests_test.go
@@ -0,0 +1,90 @@
+package flavors
+
+import (
+  "testing"
+
+  "github.com/rackspace/gophercloud"
+  "github.com/rackspace/gophercloud/pagination"
+  th "github.com/rackspace/gophercloud/testhelper"
+  fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+  th.SetupHTTP()
+  defer th.TeardownHTTP()
+
+  HandleListCDNFlavorsSuccessfully(t)
+
+  count := 0
+
+  err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+    count++
+    actual, err := ExtractFlavors(page)
+    if err != nil {
+      t.Errorf("Failed to extract flavors: %v", err)
+      return false, err
+    }
+
+    expected := []Flavor{
+      Flavor{
+        ID:   "europe",
+        Providers: []Provider{
+          Provider{
+            Provider: "Fastly",
+            Links: []gophercloud.Link{
+              gophercloud.Link{
+                Href: "http://www.fastly.com",
+                Rel: "provider_url",
+              },
+            },
+          },
+        },
+        Links: []gophercloud.Link{
+          gophercloud.Link{
+            Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+            Rel:  "self",
+          },
+        },
+      },
+    }
+
+    th.CheckDeepEquals(t, expected, actual)
+
+    return true, nil
+  })
+  th.AssertNoErr(t, err)
+  th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+  th.SetupHTTP()
+  defer th.TeardownHTTP()
+
+  HandleGetCDNFlavorSuccessfully(t)
+
+  expected := &Flavor{
+    ID:   "asia",
+    Providers: []Provider{
+      Provider{
+        Provider: "ChinaCache",
+        Links: []gophercloud.Link{
+          gophercloud.Link{
+            Href: "http://www.chinacache.com",
+            Rel:  "provider_url",
+          },
+        },
+      },
+    },
+    Links: []gophercloud.Link{
+      gophercloud.Link{
+        Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+        Rel:  "self",
+      },
+    },
+  }
+
+
+  actual, err := Get(fake.ServiceClient(), "asia").Extract()
+  th.AssertNoErr(t, err)
+  th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/cdn/v1/flavors/results.go b/openstack/cdn/v1/flavors/results.go
new file mode 100644
index 0000000..8cab48b
--- /dev/null
+++ b/openstack/cdn/v1/flavors/results.go
@@ -0,0 +1,71 @@
+package flavors
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Provider represents a provider for a particular flavor.
+type Provider struct {
+	// Specifies the name of the provider. The name must not exceed 64 bytes in
+	// length and is limited to unicode, digits, underscores, and hyphens.
+	Provider string `mapstructure:"provider"`
+	// Specifies a list with an href where rel is provider_url.
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// Flavor represents a mapping configuration to a CDN provider.
+type Flavor struct {
+	// Specifies the name of the flavor. The name must not exceed 64 bytes in
+	// length and is limited to unicode, digits, underscores, and hyphens.
+	ID string `mapstructure:"id"`
+	// Specifies the list of providers mapped to this flavor.
+	Providers []Provider `mapstructure:"providers"`
+	// Specifies the self-navigating JSON document paths.
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// FlavorPage is the page returned by a pager when traversing over a
+// collection of CDN flavors.
+type FlavorPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a FlavorPage contains no Flavors.
+func (r FlavorPage) IsEmpty() (bool, error) {
+	flavors, err := ExtractFlavors(r)
+	if err != nil {
+		return true, err
+	}
+	return len(flavors) == 0, nil
+}
+
+// ExtractFlavors extracts and returns Flavors. It is used while iterating over
+// a flavors.List call.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+	var response struct {
+		Flavors []Flavor `json:"flavors"`
+	}
+
+	err := mapstructure.Decode(page.(FlavorPage).Body, &response)
+	return response.Flavors, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a flavor from a GetResult.
+func (r GetResult) Extract() (*Flavor, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res Flavor
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res, err
+}
diff --git a/openstack/cdn/v1/flavors/urls.go b/openstack/cdn/v1/flavors/urls.go
new file mode 100644
index 0000000..6eb38d2
--- /dev/null
+++ b/openstack/cdn/v1/flavors/urls.go
@@ -0,0 +1,11 @@
+package flavors
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("flavors")
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("flavors", id)
+}
diff --git a/openstack/cdn/v1/serviceassets/doc.go b/openstack/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..ceecaa5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the OpenStack CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/openstack/cdn/v1/serviceassets/fixtures.go b/openstack/cdn/v1/serviceassets/fixtures.go
new file mode 100644
index 0000000..38e7fc5
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/fixtures.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+  "net/http"
+  "testing"
+
+  th "github.com/rackspace/gophercloud/testhelper"
+  fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleDeleteCDNAssetSuccessfully creates an HTTP handler at `/services/{id}/assets` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNAssetSuccessfully(t *testing.T) {
+  th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0/assets", 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/openstack/cdn/v1/serviceassets/requests.go b/openstack/cdn/v1/serviceassets/requests.go
new file mode 100644
index 0000000..5248ef2
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests.go
@@ -0,0 +1,52 @@
+package serviceassets
+
+import (
+	"strings"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the Delete
+// request.
+type DeleteOptsBuilder interface {
+	ToCDNAssetDeleteParams() (string, error)
+}
+
+// DeleteOpts is a structure that holds options for deleting CDN service assets.
+type DeleteOpts struct {
+	// If all is set to true, specifies that the delete occurs against all of the
+	// assets for the service.
+	All bool `q:"all"`
+	// Specifies the relative URL of the asset to be deleted.
+	URL string `q:"url"`
+}
+
+// ToCDNAssetDeleteParams formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToCDNAssetDeleteParams() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// Delete accepts a unique service ID or URL and deletes the CDN service asset associated with
+// it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string, opts DeleteOptsBuilder) DeleteResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = deleteURL(c, idOrURL)
+	}
+
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", url, perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+	return res
+}
diff --git a/openstack/cdn/v1/serviceassets/requests_test.go b/openstack/cdn/v1/serviceassets/requests_test.go
new file mode 100644
index 0000000..32896ee
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/requests_test.go
@@ -0,0 +1,18 @@
+package serviceassets
+
+import (
+  "testing"
+
+  th "github.com/rackspace/gophercloud/testhelper"
+  fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+  th.SetupHTTP()
+  defer th.TeardownHTTP()
+
+  HandleDeleteCDNAssetSuccessfully(t)
+
+  err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+  th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/serviceassets/results.go b/openstack/cdn/v1/serviceassets/results.go
new file mode 100644
index 0000000..1d8734b
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/results.go
@@ -0,0 +1,8 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/serviceassets/urls.go b/openstack/cdn/v1/serviceassets/urls.go
new file mode 100644
index 0000000..cb0aea8
--- /dev/null
+++ b/openstack/cdn/v1/serviceassets/urls.go
@@ -0,0 +1,7 @@
+package serviceassets
+
+import "github.com/rackspace/gophercloud"
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("services", id, "assets")
+}
diff --git a/openstack/cdn/v1/services/doc.go b/openstack/cdn/v1/services/doc.go
new file mode 100644
index 0000000..41f7c60
--- /dev/null
+++ b/openstack/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the OpenStack CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/openstack/cdn/v1/services/errors.go b/openstack/cdn/v1/services/errors.go
new file mode 100644
index 0000000..359584c
--- /dev/null
+++ b/openstack/cdn/v1/services/errors.go
@@ -0,0 +1,7 @@
+package services
+
+import "fmt"
+
+func no(str string) error {
+	return fmt.Errorf("Required parameter %s not provided", str)
+}
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
new file mode 100644
index 0000000..d9bc9f2
--- /dev/null
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -0,0 +1,372 @@
+package services
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// HandleListCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `List` response.
+func HandleListCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+    {
+        "links": [
+            {
+                "rel": "next",
+                "href": "https://www.poppycdn.io/v1.0/services?marker=96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0&limit=20"
+            }
+        ],
+        "services": [
+            {
+                "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+                "name": "mywebsite.com",
+                "domains": [
+                    {
+                        "domain": "www.mywebsite.com"
+                    }
+                ],
+                "origins": [
+                    {
+                        "origin": "mywebsite.com",
+                        "port": 80,
+                        "ssl": false
+                    }
+                ],
+                "caching": [
+                    {
+                        "name": "default",
+                        "ttl": 3600
+                    },
+                    {
+                        "name": "home",
+                        "ttl": 17200,
+                        "rules": [
+                            {
+                                "name": "index",
+                                "request_url": "/index.htm"
+                            }
+                        ]
+                    },
+                    {
+                        "name": "images",
+                        "ttl": 12800,
+                        "rules": [
+                            {
+                                "name": "images",
+                                "request_url": "*.png"
+                            }
+                        ]
+                    }
+                ],
+                "restrictions": [
+                    {
+                        "name": "website only",
+                        "rules": [
+                            {
+                                "name": "mywebsite.com",
+                                "referrer": "www.mywebsite.com"
+                            }
+                        ]
+                    }
+                ],
+                "flavor_id": "asia",
+                "status": "deployed",
+                "errors" : [],
+                "links": [
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "mywebsite.com.cdn123.poppycdn.net",
+                        "rel": "access_url"
+                    },
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/flavors/asia",
+                        "rel": "flavor"
+                    }
+                ]
+            },
+            {
+                "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+                "name": "myothersite.com",
+                "domains": [
+                    {
+                        "domain": "www.myothersite.com"
+                    }
+                ],
+                "origins": [
+                    {
+                        "origin": "44.33.22.11",
+                        "port": 80,
+                        "ssl": false
+                    },
+                    {
+                        "origin": "77.66.55.44",
+                        "port": 80,
+                        "ssl": false,
+                        "rules": [
+                            {
+                                "name": "videos",
+                                "request_url": "^/videos/*.m3u"
+                            }
+                        ]
+                    }
+                ],
+                "caching": [
+                    {
+                        "name": "default",
+                        "ttl": 3600
+                    }
+                ],
+                "restrictions": [
+                    {}
+                ],
+                "flavor_id": "europe",
+                "status": "deployed",
+                "links": [
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+                        "rel": "self"
+                    },
+                    {
+                        "href": "myothersite.com.poppycdn.net",
+                        "rel": "access_url"
+                    },
+                    {
+                        "href": "https://www.poppycdn.io/v1.0/flavors/europe",
+                        "rel": "flavor"
+                    }
+                ]
+            }
+        ]
+    }
+    `)
+		case "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1":
+			fmt.Fprintf(w, `{
+        "services": []
+    }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+}
+
+// HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
+// that responds with a `Create` response.
+func HandleCreateCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services", 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, `
+      {
+        "name": "mywebsite.com",
+        "domains": [
+            {
+                "domain": "www.mywebsite.com"
+            },
+            {
+                "domain": "blog.mywebsite.com"
+            }
+        ],
+        "origins": [
+            {
+                "origin": "mywebsite.com",
+                "port": 80,
+                "ssl": false
+            }
+        ],
+        "restrictions": [
+                         {
+                         "name": "website only",
+                         "rules": [
+                                   {
+                                   "name": "mywebsite.com",
+                                   "referrer": "www.mywebsite.com"
+                    }
+                ]
+            }
+        ],
+        "caching": [
+            {
+                "name": "default",
+                "ttl": 3600
+            }
+        ],
+
+        "flavor_id": "cdn"
+      }
+   `)
+		w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Get` response.
+func HandleGetCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+    {
+        "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+        "name": "mywebsite.com",
+        "domains": [
+            {
+                "domain": "www.mywebsite.com",
+                "protocol": "http"
+            }
+        ],
+        "origins": [
+            {
+                "origin": "mywebsite.com",
+                "port": 80,
+                "ssl": false
+            }
+        ],
+        "caching": [
+            {
+                "name": "default",
+                "ttl": 3600
+            },
+            {
+                "name": "home",
+                "ttl": 17200,
+                "rules": [
+                    {
+                        "name": "index",
+                        "request_url": "/index.htm"
+                    }
+                ]
+            },
+            {
+                "name": "images",
+                "ttl": 12800,
+                "rules": [
+                    {
+                        "name": "images",
+                        "request_url": "*.png"
+                    }
+                ]
+            }
+        ],
+        "restrictions": [
+            {
+                "name": "website only",
+                "rules": [
+                    {
+                        "name": "mywebsite.com",
+                        "referrer": "www.mywebsite.com"
+                    }
+                ]
+            }
+        ],
+        "flavor_id": "cdn",
+        "status": "deployed",
+        "errors" : [],
+        "links": [
+            {
+                "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+                "rel": "self"
+            },
+            {
+                "href": "blog.mywebsite.com.cdn1.raxcdn.com",
+                "rel": "access_url"
+            },
+            {
+                "href": "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+                "rel": "flavor"
+            }
+        ]
+    }
+    `)
+	})
+}
+
+// HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Update` response.
+func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `
+      [
+				{
+					"op": "add",
+					"path": "/domains/-",
+					"value": {"domain": "appended.mocksite4.com"}
+				},
+				{
+					"op": "add",
+					"path": "/domains/4",
+					"value": {"domain": "inserted.mocksite4.com"}
+				},
+				{
+					"op": "add",
+					"path": "/domains",
+					"value": [
+						{"domain": "bulkadded1.mocksite4.com"},
+						{"domain": "bulkadded2.mocksite4.com"}
+					]
+				},
+				{
+					"op": "replace",
+					"path": "/origins/2",
+					"value": {"origin": "44.33.22.11", "port": 80, "ssl": false}
+				},
+				{
+					"op": "replace",
+					"path": "/origins",
+					"value": [
+						{"origin": "44.33.22.11", "port": 80, "ssl": false},
+						{"origin": "55.44.33.22", "port": 443, "ssl": true}
+					]
+				},
+				{
+					"op": "remove",
+					"path": "/caching/8"
+				},
+				{
+					"op": "remove",
+					"path": "/caching"
+				},
+				{
+					"op": "replace",
+					"path": "/name",
+					"value": "differentServiceName"
+				}
+    ]
+   `)
+		w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
+// that responds with a `Delete` response.
+func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", 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/openstack/cdn/v1/services/requests.go b/openstack/cdn/v1/services/requests.go
new file mode 100644
index 0000000..ad1e1da
--- /dev/null
+++ b/openstack/cdn/v1/services/requests.go
@@ -0,0 +1,391 @@
+package services
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToCDNServiceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+	Marker string `q:"marker"`
+	Limit  int    `q:"limit"`
+}
+
+// ToCDNServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToCDNServiceListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(c)
+	if opts != nil {
+		query, err := opts.ToCDNServiceListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPage := func(r pagination.PageResult) pagination.Page {
+		p := ServicePage{pagination.MarkerPageBase{PageResult: r}}
+		p.MarkerPageBase.Owner = p
+		return p
+	}
+
+	pager := pagination.NewPager(c, url, createPage)
+	return pager
+}
+
+// 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 {
+	ToCDNServiceCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+	// REQUIRED. Specifies the name of the service. The minimum length for name is
+	// 3. The maximum length is 256.
+	Name string
+	// REQUIRED. Specifies a list of domains used by users to access their website.
+	Domains []Domain
+	// REQUIRED. Specifies a list of origin domains or IP addresses where the
+	// original assets are stored.
+	Origins []Origin
+	// REQUIRED. Specifies the CDN provider flavor ID to use. For a list of
+	// flavors, see the operation to list the available flavors. The minimum
+	// length for flavor_id is 1. The maximum length is 256.
+	FlavorID string
+	// OPTIONAL. Specifies the TTL rules for the assets under this service. Supports wildcards for fine-grained control.
+	Caching []CacheRule
+	// OPTIONAL. Specifies the restrictions that define who can access assets (content from the CDN cache).
+	Restrictions []Restriction
+}
+
+// ToCDNServiceCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToCDNServiceCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return nil, no("Name")
+	}
+	s["name"] = opts.Name
+
+	if opts.Domains == nil {
+		return nil, no("Domains")
+	}
+	for _, domain := range opts.Domains {
+		if domain.Domain == "" {
+			return nil, no("Domains[].Domain")
+		}
+	}
+	s["domains"] = opts.Domains
+
+	if opts.Origins == nil {
+		return nil, no("Origins")
+	}
+	for _, origin := range opts.Origins {
+		if origin.Origin == "" {
+			return nil, no("Origins[].Origin")
+		}
+		if origin.Rules == nil && len(opts.Origins) > 1 {
+			return nil, no("Origins[].Rules")
+		}
+		for _, rule := range origin.Rules {
+			if rule.Name == "" {
+				return nil, no("Origins[].Rules[].Name")
+			}
+			if rule.RequestURL == "" {
+				return nil, no("Origins[].Rules[].RequestURL")
+			}
+		}
+	}
+	s["origins"] = opts.Origins
+
+	if opts.FlavorID == "" {
+		return nil, no("FlavorID")
+	}
+	s["flavor_id"] = opts.FlavorID
+
+	if opts.Caching != nil {
+		for _, cache := range opts.Caching {
+			if cache.Name == "" {
+				return nil, no("Caching[].Name")
+			}
+			if cache.Rules != nil {
+				for _, rule := range cache.Rules {
+					if rule.Name == "" {
+						return nil, no("Caching[].Rules[].Name")
+					}
+					if rule.RequestURL == "" {
+						return nil, no("Caching[].Rules[].RequestURL")
+					}
+				}
+			}
+		}
+		s["caching"] = opts.Caching
+	}
+
+	if opts.Restrictions != nil {
+		for _, restriction := range opts.Restrictions {
+			if restriction.Name == "" {
+				return nil, no("Restrictions[].Name")
+			}
+			if restriction.Rules != nil {
+				for _, rule := range restriction.Rules {
+					if rule.Name == "" {
+						return nil, no("Restrictions[].Rules[].Name")
+					}
+				}
+			}
+		}
+		s["restrictions"] = opts.Restrictions
+	}
+
+	return s, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToCDNServiceCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	resp, err := perigee.Request("POST", createURL(c), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		OkCodes:     []int{202},
+	})
+	res.Header = resp.HttpResponse.Header
+	res.Err = err
+	return res
+}
+
+// Get retrieves a specific service based on its URL or its unique ID. For
+// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Get(c *gophercloud.ServiceClient, idOrURL string) GetResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = getURL(c, idOrURL)
+	}
+
+	var res GetResult
+	_, res.Err = perigee.Request("GET", url, perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		Results:     &res.Body,
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// Path is a JSON pointer location that indicates which service parameter is being added, replaced,
+// or removed.
+type Path struct {
+	baseElement string
+}
+
+func (p Path) renderRoot() string {
+	return "/" + p.baseElement
+}
+
+func (p Path) renderDash() string {
+	return fmt.Sprintf("/%s/-", p.baseElement)
+}
+
+func (p Path) renderIndex(index int64) string {
+	return fmt.Sprintf("/%s/%d", p.baseElement, index)
+}
+
+var (
+	// PathDomains indicates that an update operation is to be performed on a Domain.
+	PathDomains = Path{baseElement: "domains"}
+
+	// PathOrigins indicates that an update operation is to be performed on an Origin.
+	PathOrigins = Path{baseElement: "origins"}
+
+	// PathCaching indicates that an update operation is to be performed on a CacheRule.
+	PathCaching = Path{baseElement: "caching"}
+)
+
+type value interface {
+	toPatchValue() interface{}
+	appropriatePath() Path
+	renderRootOr(func(p Path) string) string
+}
+
+// Patch represents a single update to an existing Service. Multiple updates to a service can be
+// submitted at the same time.
+type Patch interface {
+	ToCDNServiceUpdateMap() map[string]interface{}
+}
+
+// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to
+// a Service at a fixed index. Use an Append instead to append the new value to the end of its
+// collection. Pass it to the Update function as part of the Patch slice.
+type Insertion struct {
+	Index int64
+	Value value
+}
+
+// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the
+// Update call.
+func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }),
+		"value": i.Value.toPatchValue(),
+	}
+}
+
+// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a
+// Service at the end of its respective collection. Use an Insertion instead to insert the value
+// at a fixed index within the collection. Pass this to the Update function as part of its
+// Patch slice.
+type Append struct {
+	Value value
+}
+
+// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the
+// Update call.
+func (a Append) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  a.Value.renderRootOr(func(p Path) string { return p.renderDash() }),
+		"value": a.Value.toPatchValue(),
+	}
+}
+
+// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule)
+// in-place by index. Pass it to the Update function as part of the Patch slice.
+type Replacement struct {
+	Value value
+	Index int64
+}
+
+// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the
+// Update call.
+func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }),
+		"value": r.Value.toPatchValue(),
+	}
+}
+
+// NameReplacement specifically updates the Service name. Pass it to the Update function as part
+// of the Patch slice.
+type NameReplacement struct {
+	NewName string
+}
+
+// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the
+// Update call.
+func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/name",
+		"value": r.NewName,
+	}
+}
+
+// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or
+// CacheRule) by index. Pass it to the Update function as part of the Patch slice.
+type Removal struct {
+	Path  Path
+	Index int64
+	All   bool
+}
+
+// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the
+// Update call.
+func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} {
+	result := map[string]interface{}{"op": "remove"}
+	if r.All {
+		result["path"] = r.Path.renderRoot()
+	} else {
+		result["path"] = r.Path.renderIndex(r.Index)
+	}
+	return result
+}
+
+type UpdateOpts []Patch
+
+// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and
+// updates an existing CDN service using the values provided. idOrURL can be either the service's
+// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOpts) UpdateResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = updateURL(c, idOrURL)
+	}
+
+	reqBody := make([]map[string]interface{}, len(opts))
+	for i, patch := range opts {
+		reqBody[i] = patch.ToCDNServiceUpdateMap()
+	}
+
+	resp, err := perigee.Request("PATCH", url, perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		OkCodes:     []int{202},
+	})
+	var result UpdateResult
+	result.Header = resp.HttpResponse.Header
+	result.Err = err
+	return result
+}
+
+// Delete accepts a service's ID or its URL and deletes the CDN service
+// associated with it. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+// are valid options for idOrURL.
+func Delete(c *gophercloud.ServiceClient, idOrURL string) DeleteResult {
+	var url string
+	if strings.Contains(idOrURL, "/") {
+		url = idOrURL
+	} else {
+		url = deleteURL(c, idOrURL)
+	}
+
+	var res DeleteResult
+	_, res.Err = perigee.Request("DELETE", url, perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+	return res
+}
diff --git a/openstack/cdn/v1/services/requests_test.go b/openstack/cdn/v1/services/requests_test.go
new file mode 100644
index 0000000..59e826f
--- /dev/null
+++ b/openstack/cdn/v1/services/requests_test.go
@@ -0,0 +1,358 @@
+package services
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleListCDNServiceSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractServices(page)
+		if err != nil {
+			t.Errorf("Failed to extract services: %v", err)
+			return false, err
+		}
+
+		expected := []Service{
+			Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Name: "mywebsite.com",
+				Domains: []Domain{
+					Domain{
+						Domain: "www.mywebsite.com",
+					},
+				},
+				Origins: []Origin{
+					Origin{
+						Origin: "mywebsite.com",
+						Port:   80,
+						SSL:    false,
+					},
+				},
+				Caching: []CacheRule{
+					CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+					CacheRule{
+						Name: "home",
+						TTL:  17200,
+						Rules: []TTLRule{
+							TTLRule{
+								Name:       "index",
+								RequestURL: "/index.htm",
+							},
+						},
+					},
+					CacheRule{
+						Name: "images",
+						TTL:  12800,
+						Rules: []TTLRule{
+							TTLRule{
+								Name:       "images",
+								RequestURL: "*.png",
+							},
+						},
+					},
+				},
+				Restrictions: []Restriction{
+					Restriction{
+						Name: "website only",
+						Rules: []RestrictionRule{
+							RestrictionRule{
+								Name:     "mywebsite.com",
+								Referrer: "www.mywebsite.com",
+							},
+						},
+					},
+				},
+				FlavorID: "asia",
+				Status:   "deployed",
+				Errors:   []Error{},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "mywebsite.com.cdn123.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+						Rel:  "flavor",
+					},
+				},
+			},
+			Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+				Name: "myothersite.com",
+				Domains: []Domain{
+					Domain{
+						Domain: "www.myothersite.com",
+					},
+				},
+				Origins: []Origin{
+					Origin{
+						Origin: "44.33.22.11",
+						Port:   80,
+						SSL:    false,
+					},
+					Origin{
+						Origin: "77.66.55.44",
+						Port:   80,
+						SSL:    false,
+						Rules: []OriginRule{
+							OriginRule{
+								Name:       "videos",
+								RequestURL: "^/videos/*.m3u",
+							},
+						},
+					},
+				},
+				Caching: []CacheRule{
+					CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+				},
+				Restrictions: []Restriction{},
+				FlavorID:     "europe",
+				Status:       "deployed",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "myothersite.com.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "flavor",
+					},
+				},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleCreateCDNServiceSuccessfully(t)
+
+	createOpts := CreateOpts{
+		Name: "mywebsite.com",
+		Domains: []Domain{
+			Domain{
+				Domain: "www.mywebsite.com",
+			},
+			Domain{
+				Domain: "blog.mywebsite.com",
+			},
+		},
+		Origins: []Origin{
+			Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Restrictions: []Restriction{
+			Restriction{
+				Name: "website only",
+				Rules: []RestrictionRule{
+					RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		Caching: []CacheRule{
+			CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+		},
+		FlavorID: "cdn",
+	}
+
+	expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleGetCDNServiceSuccessfully(t)
+
+	expected := &Service{
+		ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+		Name: "mywebsite.com",
+		Domains: []Domain{
+			Domain{
+				Domain:   "www.mywebsite.com",
+				Protocol: "http",
+			},
+		},
+		Origins: []Origin{
+			Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Caching: []CacheRule{
+			CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+			CacheRule{
+				Name: "home",
+				TTL:  17200,
+				Rules: []TTLRule{
+					TTLRule{
+						Name:       "index",
+						RequestURL: "/index.htm",
+					},
+				},
+			},
+			CacheRule{
+				Name: "images",
+				TTL:  12800,
+				Rules: []TTLRule{
+					TTLRule{
+						Name:       "images",
+						RequestURL: "*.png",
+					},
+				},
+			},
+		},
+		Restrictions: []Restriction{
+			Restriction{
+				Name: "website only",
+				Rules: []RestrictionRule{
+					RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		FlavorID: "cdn",
+		Status:   "deployed",
+		Errors:   []Error{},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+				Rel:  "access_url",
+			},
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+				Rel:  "flavor",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestSuccessfulUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleUpdateCDNServiceSuccessfully(t)
+
+	expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	ops := UpdateOpts{
+		// Append a single Domain
+		Append{Value: Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		Insertion{
+			Index: 4,
+			Value: Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		Append{
+			Value: DomainList{
+				Domain{Domain: "bulkadded1.mocksite4.com"},
+				Domain{Domain: "bulkadded2.mocksite4.com"},
+			},
+		},
+		// Replace a single Origin
+		Replacement{
+			Index: 2,
+			Value: Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		Replacement{
+			Index: 0, // Ignored
+			Value: OriginList{
+				Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+				Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
+			},
+		},
+		// Remove a single CacheRule
+		Removal{
+			Index: 8,
+			Path:  PathCaching,
+		},
+		// Bulk removal
+		Removal{
+			All:  true,
+			Path: PathCaching,
+		},
+		// Service name replacement
+		NameReplacement{
+			NewName: "differentServiceName",
+		},
+	}
+
+	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleDeleteCDNServiceSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
new file mode 100644
index 0000000..33406c4
--- /dev/null
+++ b/openstack/cdn/v1/services/results.go
@@ -0,0 +1,316 @@
+package services
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Domain represents a domain used by users to access their website.
+type Domain struct {
+	// Specifies the domain used to access the assets on their website, for which
+	// a CNAME is given to the CDN provider.
+	Domain string `mapstructure:"domain" json:"domain"`
+	// Specifies the protocol used to access the assets on this domain. Only "http"
+	// or "https" are currently allowed. The default is "http".
+	Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
+}
+
+func (d Domain) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["domain"] = d.Domain
+	if d.Protocol != "" {
+		r["protocol"] = d.Protocol
+	}
+	return r
+}
+
+func (d Domain) appropriatePath() Path {
+	return PathDomains
+}
+
+func (d Domain) renderRootOr(render func(p Path) string) string {
+	return render(d.appropriatePath())
+}
+
+// DomainList provides a useful way to perform bulk operations in a single Patch.
+type DomainList []Domain
+
+func (list DomainList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, domain := range list {
+		r[i] = domain.toPatchValue()
+	}
+	return r
+}
+
+func (list DomainList) appropriatePath() Path {
+	return PathDomains
+}
+
+func (list DomainList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
+// OriginRule represents a rule that defines when an origin should be accessed.
+type OriginRule struct {
+	// Specifies the name of this rule.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the request URL this rule should match for this origin to be used. Regex is supported.
+	RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// Origin specifies a list of origin domains or IP addresses where the original assets are stored.
+type Origin struct {
+	// Specifies the URL or IP address to pull origin content from.
+	Origin string `mapstructure:"origin" json:"origin"`
+	// Specifies the port used to access the origin. The default is port 80.
+	Port int `mapstructure:"port" json:"port,omitempty"`
+	// Specifies whether or not to use HTTPS to access the origin. The default
+	// is false.
+	SSL bool `mapstructure:"ssl" json:"ssl"`
+	// Specifies a collection of rules that define the conditions when this origin
+	// should be accessed. If there is more than one origin, the rules parameter is required.
+	Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+func (o Origin) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["origin"] = o.Origin
+	r["port"] = o.Port
+	r["ssl"] = o.SSL
+	if len(o.Rules) > 0 {
+		r["rules"] = make([]map[string]interface{}, len(o.Rules))
+		for index, rule := range o.Rules {
+			submap := r["rules"].([]map[string]interface{})[index]
+			submap["name"] = rule.Name
+			submap["request_url"] = rule.RequestURL
+		}
+	}
+	return r
+}
+
+func (o Origin) appropriatePath() Path {
+	return PathOrigins
+}
+
+func (o Origin) renderRootOr(render func(p Path) string) string {
+	return render(o.appropriatePath())
+}
+
+// OriginList provides a useful way to perform bulk operations in a single Patch.
+type OriginList []Origin
+
+func (list OriginList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, origin := range list {
+		r[i] = origin.toPatchValue()
+	}
+	return r
+}
+
+func (list OriginList) appropriatePath() Path {
+	return PathOrigins
+}
+
+func (list OriginList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
+// TTLRule specifies a rule that determines if a TTL should be applied to an asset.
+type TTLRule struct {
+	// Specifies the name of this rule.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the request URL this rule should match for this TTL to be used. Regex is supported.
+	RequestURL string `mapstructure:"request_url" json:"request_url"`
+}
+
+// CacheRule specifies the TTL rules for the assets under this service.
+type CacheRule struct {
+	// Specifies the name of this caching rule. Note: 'default' is a reserved name used for the default TTL setting.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the TTL to apply.
+	TTL int `mapstructure:"ttl" json:"ttl"`
+	// Specifies a collection of rules that determine if this TTL should be applied to an asset.
+	Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
+}
+
+func (c CacheRule) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["name"] = c.Name
+	r["ttl"] = c.TTL
+	r["rules"] = make([]map[string]interface{}, len(c.Rules))
+	for index, rule := range c.Rules {
+		submap := r["rules"].([]map[string]interface{})[index]
+		submap["name"] = rule.Name
+		submap["request_url"] = rule.RequestURL
+	}
+	return r
+}
+
+func (c CacheRule) appropriatePath() Path {
+	return PathCaching
+}
+
+func (c CacheRule) renderRootOr(render func(p Path) string) string {
+	return render(c.appropriatePath())
+}
+
+// CacheRuleList provides a useful way to perform bulk operations in a single Patch.
+type CacheRuleList []CacheRule
+
+func (list CacheRuleList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, rule := range list {
+		r[i] = rule.toPatchValue()
+	}
+	return r
+}
+
+func (list CacheRuleList) appropriatePath() Path {
+	return PathCaching
+}
+
+func (list CacheRuleList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
+// RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
+type RestrictionRule struct {
+	// Specifies the name of this rule.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies the http host that requests must come from.
+	Referrer string `mapstructure:"referrer" json:"referrer,omitempty"`
+}
+
+// Restriction specifies a restriction that defines who can access assets (content from the CDN cache).
+type Restriction struct {
+	// Specifies the name of this restriction.
+	Name string `mapstructure:"name" json:"name"`
+	// Specifies a collection of rules that determine if this TTL should be applied to an asset.
+	Rules []RestrictionRule `mapstructure:"rules" json:"rules"`
+}
+
+// Error specifies an error that occurred during the previous service action.
+type Error struct {
+	// Specifies an error message detailing why there is an error.
+	Message string `mapstructure:"message"`
+}
+
+// Service represents a CDN service resource.
+type Service struct {
+	// Specifies the service ID that represents distributed content. The value is
+	// a UUID, such as 96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0, that is generated by the server.
+	ID string `mapstructure:"id"`
+	// Specifies the name of the service.
+	Name string `mapstructure:"name"`
+	// Specifies a list of domains used by users to access their website.
+	Domains []Domain `mapstructure:"domains"`
+	// Specifies a list of origin domains or IP addresses where the original assets are stored.
+	Origins []Origin `mapstructure:"origins"`
+	// Specifies the TTL rules for the assets under this service. Supports wildcards for fine grained control.
+	Caching []CacheRule `mapstructure:"caching"`
+	// Specifies the restrictions that define who can access assets (content from the CDN cache).
+	Restrictions []Restriction `mapstructure:"restrictions" json:"restrictions,omitempty"`
+	// Specifies the CDN provider flavor ID to use. For a list of flavors, see the operation to list the available flavors.
+	FlavorID string `mapstructure:"flavor_id"`
+	// Specifies the current status of the service.
+	Status string `mapstructure:"status"`
+	// Specifies the list of errors that occurred during the previous service action.
+	Errors []Error `mapstructure:"errors"`
+	// Specifies the self-navigating JSON document paths.
+	Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// ServicePage is the page returned by a pager when traversing over a
+// collection of CDN services.
+type ServicePage struct {
+	pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no services.
+func (r ServicePage) IsEmpty() (bool, error) {
+	services, err := ExtractServices(r)
+	if err != nil {
+		return true, err
+	}
+	return len(services) == 0, nil
+}
+
+// LastMarker returns the last service in a ListResult.
+func (r ServicePage) LastMarker() (string, error) {
+	services, err := ExtractServices(r)
+	if err != nil {
+		return "", err
+	}
+	if len(services) == 0 {
+		return "", nil
+	}
+	return (services[len(services)-1]).ID, nil
+}
+
+// ExtractServices is a function that takes a ListResult and returns the services' information.
+func ExtractServices(page pagination.Page) ([]Service, error) {
+	var response struct {
+		Services []Service `mapstructure:"services"`
+	}
+
+	err := mapstructure.Decode(page.(ServicePage).Body, &response)
+	return response.Services, err
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that extracts the location of a newly created service.
+func (r CreateResult) Extract() (string, error) {
+	if r.Err != nil {
+		return "", r.Err
+	}
+	if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+		return l[0], nil
+	}
+	return "", nil
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that extracts a service from a GetResult.
+func (r GetResult) Extract() (*Service, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res Service
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return &res, err
+}
+
+// UpdateResult represents the result of a Update operation.
+type UpdateResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that extracts the location of an updated service.
+func (r UpdateResult) Extract() (string, error) {
+	if r.Err != nil {
+		return "", r.Err
+	}
+	if l, ok := r.Header["Location"]; ok && len(l) > 0 {
+		return l[0], nil
+	}
+	return "", nil
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/cdn/v1/services/urls.go b/openstack/cdn/v1/services/urls.go
new file mode 100644
index 0000000..d953d4c
--- /dev/null
+++ b/openstack/cdn/v1/services/urls.go
@@ -0,0 +1,23 @@
+package services
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("services")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("services", id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
diff --git a/openstack/client.go b/openstack/client.go
index 99b3d46..9c12dca 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -203,3 +203,14 @@
 	}
 	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
 }
+
+// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("cdn")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/doc.go b/openstack/compute/v2/extensions/defsecrules/doc.go
new file mode 100644
index 0000000..2571a1a
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/doc.go
@@ -0,0 +1 @@
+package defsecrules
diff --git a/openstack/compute/v2/extensions/defsecrules/fixtures.go b/openstack/compute/v2/extensions/defsecrules/fixtures.go
new file mode 100644
index 0000000..c28e492
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/fixtures.go
@@ -0,0 +1,108 @@
+package defsecrules
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const rootPath = "/os-security-group-default-rules"
+
+func mockListRulesResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, 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, `
+{
+  "security_group_default_rules": [
+    {
+      "from_port": 80,
+      "id": "{ruleID}",
+      "ip_protocol": "TCP",
+      "ip_range": {
+        "cidr": "10.10.10.0/24"
+      },
+      "to_port": 80
+    }
+  ]
+}
+      `)
+	})
+}
+
+func mockCreateRuleResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, 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, `
+{
+  "security_group_default_rule": {
+    "ip_protocol": "TCP",
+    "from_port": 80,
+    "to_port": 80,
+    "cidr": "10.10.12.0/24"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_default_rule": {
+    "from_port": 80,
+    "id": "{ruleID}",
+    "ip_protocol": "TCP",
+    "ip_range": {
+      "cidr": "10.10.12.0/24"
+    },
+    "to_port": 80
+  }
+}
+`)
+	})
+}
+
+func mockGetRuleResponse(t *testing.T, ruleID string) {
+	url := rootPath + "/" + ruleID
+	th.Mux.HandleFunc(url, 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, `
+{
+  "security_group_default_rule": {
+    "id": "{ruleID}",
+    "from_port": 80,
+    "to_port": 80,
+    "ip_protocol": "TCP",
+    "ip_range": {
+      "cidr": "10.10.12.0/24"
+    }
+  }
+}
+			`)
+	})
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+	url := rootPath + "/" + ruleID
+	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.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests.go b/openstack/compute/v2/extensions/defsecrules/requests.go
new file mode 100644
index 0000000..7d19741
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests.go
@@ -0,0 +1,111 @@
+package defsecrules
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List will return a collection of default rules.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return DefaultRulePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// CreateOpts represents the configuration for adding a new default rule.
+type CreateOpts struct {
+	// Required - the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+
+	// Required - the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+
+	// Required - the protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol"`
+
+	// ONLY required if FromGroupID is blank. This represents the IP range that
+	// will be the source of network traffic to your security group. Use
+	// 0.0.0.0/0 to allow all IP addresses.
+	CIDR string `json:"cidr,omitempty"`
+}
+
+// CreateOptsBuilder builds the create rule options into a serializable format.
+type CreateOptsBuilder interface {
+	ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// ToRuleCreateMap builds the create rule options into a serializable format.
+func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	rule := make(map[string]interface{})
+
+	if opts.FromPort == 0 {
+		return rule, errors.New("A FromPort must be set")
+	}
+	if opts.ToPort == 0 {
+		return rule, errors.New("A ToPort must be set")
+	}
+	if opts.IPProtocol == "" {
+		return rule, errors.New("A IPProtocol must be set")
+	}
+	if opts.CIDR == "" {
+		return rule, errors.New("A CIDR must be set")
+	}
+
+	rule["from_port"] = opts.FromPort
+	rule["to_port"] = opts.ToPort
+	rule["ip_protocol"] = opts.IPProtocol
+	rule["cidr"] = opts.CIDR
+
+	return map[string]interface{}{"security_group_default_rule": rule}, nil
+}
+
+// Create is the operation responsible for creating a new default rule.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var result CreateResult
+
+	reqBody, err := opts.ToRuleCreateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     &reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Get will return details for a particular default rule.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+
+	_, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Delete will permanently delete a default rule from the project.
+func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+
+	_, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests_test.go b/openstack/compute/v2/extensions/defsecrules/requests_test.go
new file mode 100644
index 0000000..d4ebe87
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/requests_test.go
@@ -0,0 +1,100 @@
+package defsecrules
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const ruleID = "{ruleID}"
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListRulesResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractDefaultRules(page)
+		th.AssertNoErr(t, err)
+
+		expected := []DefaultRule{
+			DefaultRule{
+				FromPort:   80,
+				ID:         ruleID,
+				IPProtocol: "TCP",
+				IPRange:    secgroups.IPRange{CIDR: "10.10.10.0/24"},
+				ToPort:     80,
+			},
+		}
+
+		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()
+
+	mockCreateRuleResponse(t)
+
+	opts := CreateOpts{
+		IPProtocol: "TCP",
+		FromPort:   80,
+		ToPort:     80,
+		CIDR:       "10.10.12.0/24",
+	}
+
+	group, err := Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &DefaultRule{
+		ID:         ruleID,
+		FromPort:   80,
+		ToPort:     80,
+		IPProtocol: "TCP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetRuleResponse(t, ruleID)
+
+	group, err := Get(client.ServiceClient(), ruleID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &DefaultRule{
+		ID:         ruleID,
+		FromPort:   80,
+		ToPort:     80,
+		IPProtocol: "TCP",
+		IPRange:    secgroups.IPRange{CIDR: "10.10.12.0/24"},
+	}
+
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteRuleResponse(t, ruleID)
+
+	err := Delete(client.ServiceClient(), ruleID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/results.go b/openstack/compute/v2/extensions/defsecrules/results.go
new file mode 100644
index 0000000..e588d3e
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/results.go
@@ -0,0 +1,69 @@
+package defsecrules
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// DefaultRule represents a default rule - which is identical to a
+// normal security rule.
+type DefaultRule secgroups.Rule
+
+// DefaultRulePage is a single page of a DefaultRule collection.
+type DefaultRulePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of default rules contains any results.
+func (page DefaultRulePage) IsEmpty() (bool, error) {
+	users, err := ExtractDefaultRules(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractDefaultRules returns a slice of DefaultRules contained in a single
+// page of results.
+func ExtractDefaultRules(page pagination.Page) ([]DefaultRule, error) {
+	casted := page.(DefaultRulePage).Body
+	var response struct {
+		Rules []DefaultRule `mapstructure:"security_group_default_rules"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.Rules, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// Extract will extract a DefaultRule struct from most responses.
+func (r commonResult) Extract() (*DefaultRule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Rule DefaultRule `mapstructure:"security_group_default_rule"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &response)
+
+	return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/defsecrules/urls.go b/openstack/compute/v2/extensions/defsecrules/urls.go
new file mode 100644
index 0000000..cc928ab
--- /dev/null
+++ b/openstack/compute/v2/extensions/defsecrules/urls.go
@@ -0,0 +1,13 @@
+package defsecrules
+
+import "github.com/rackspace/gophercloud"
+
+const rulepath = "os-security-group-default-rules"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rulepath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rulepath)
+}
diff --git a/openstack/compute/v2/extensions/keypairs/requests.go b/openstack/compute/v2/extensions/keypairs/requests.go
index 7d1a2ac..7b372a3 100644
--- a/openstack/compute/v2/extensions/keypairs/requests.go
+++ b/openstack/compute/v2/extensions/keypairs/requests.go
@@ -5,9 +5,34 @@
 
 	"github.com/racker/perigee"
 	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+// CreateOptsExt adds a KeyPair option to the base CreateOpts.
+type CreateOptsExt struct {
+	servers.CreateOptsBuilder
+	KeyName string `json:"key_name,omitempty"`
+}
+
+// ToServerCreateMap adds the key_name and, optionally, key_data options to
+// the base server creation options.
+func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
+	base, err := opts.CreateOptsBuilder.ToServerCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if opts.KeyName == "" {
+		return base, nil
+	}
+
+	serverMap := base["server"].(map[string]interface{})
+	serverMap["key_name"] = opts.KeyName
+
+	return base, nil
+}
+
 // List returns a Pager that allows you to iterate over a collection of KeyPairs.
 func List(client *gophercloud.ServiceClient) pagination.Pager {
 	return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
@@ -21,7 +46,7 @@
 	ToKeyPairCreateMap() (map[string]interface{}, error)
 }
 
-// CreateOpts species keypair creation or import parameters.
+// CreateOpts specifies keypair creation or import parameters.
 type CreateOpts struct {
 	// Name [required] is a friendly name to refer to this KeyPair in other services.
 	Name string
diff --git a/openstack/compute/v2/extensions/secgroups/doc.go b/openstack/compute/v2/extensions/secgroups/doc.go
new file mode 100644
index 0000000..702f32c
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/doc.go
@@ -0,0 +1 @@
+package secgroups
diff --git a/openstack/compute/v2/extensions/secgroups/fixtures.go b/openstack/compute/v2/extensions/secgroups/fixtures.go
new file mode 100644
index 0000000..ca76f68
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/fixtures.go
@@ -0,0 +1,265 @@
+package secgroups
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const rootPath = "/os-security-groups"
+
+const listGroupsJSON = `
+{
+  "security_groups": [
+    {
+      "description": "default",
+      "id": "{groupID}",
+      "name": "default",
+      "rules": [],
+      "tenant_id": "openstack"
+    }
+  ]
+}
+`
+
+func mockListGroupsResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, 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, listGroupsJSON)
+	})
+}
+
+func mockListGroupsByServerResponse(t *testing.T, serverID string) {
+	url := fmt.Sprintf("%s/servers/%s%s", rootPath, serverID, rootPath)
+	th.Mux.HandleFunc(url, 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, listGroupsJSON)
+	})
+}
+
+func mockCreateGroupResponse(t *testing.T) {
+	th.Mux.HandleFunc(rootPath, 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, `
+{
+  "security_group": {
+    "name": "test",
+    "description": "something"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group": {
+    "description": "something",
+    "id": "{groupID}",
+    "name": "test",
+    "rules": [],
+    "tenant_id": "openstack"
+  }
+}
+`)
+	})
+}
+
+func mockUpdateGroupResponse(t *testing.T, groupID string) {
+	url := fmt.Sprintf("%s/%s", rootPath, groupID)
+	th.Mux.HandleFunc(url, 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, `
+{
+  "security_group": {
+    "name": "new_name",
+		"description": "new_desc"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group": {
+    "description": "something",
+    "id": "{groupID}",
+    "name": "new_name",
+    "rules": [],
+    "tenant_id": "openstack"
+  }
+}
+`)
+	})
+}
+
+func mockGetGroupsResponse(t *testing.T, groupID string) {
+	url := fmt.Sprintf("%s/%s", rootPath, groupID)
+	th.Mux.HandleFunc(url, 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, `
+{
+  "security_group": {
+    "description": "default",
+    "id": "{groupID}",
+    "name": "default",
+    "rules": [
+      {
+        "from_port": 80,
+        "group": {
+          "tenant_id": "openstack",
+          "name": "default"
+        },
+        "ip_protocol": "TCP",
+        "to_port": 85,
+        "parent_group_id": "{groupID}",
+        "ip_range": {
+						"cidr": "0.0.0.0"
+				},
+        "id": "{ruleID}"
+      }
+    ],
+    "tenant_id": "openstack"
+  }
+}
+			`)
+	})
+}
+
+func mockGetNumericIDGroupResponse(t *testing.T, groupID int) {
+	url := fmt.Sprintf("%s/%d", rootPath, groupID)
+	th.Mux.HandleFunc(url, 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, `
+{
+	"security_group": {
+		"id": 12345
+	}
+}
+			`)
+	})
+}
+
+func mockDeleteGroupResponse(t *testing.T, groupID string) {
+	url := fmt.Sprintf("%s/%s", rootPath, groupID)
+	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.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockAddRuleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/os-security-group-rules", 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, `
+{
+  "security_group_rule": {
+    "from_port": 22,
+    "ip_protocol": "TCP",
+    "to_port": 22,
+    "parent_group_id": "{groupID}",
+    "cidr": "0.0.0.0/0"
+  }
+}	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "security_group_rule": {
+    "from_port": 22,
+    "group": {},
+    "ip_protocol": "TCP",
+    "to_port": 22,
+    "parent_group_id": "{groupID}",
+    "ip_range": {
+      "cidr": "0.0.0.0/0"
+    },
+    "id": "{ruleID}"
+  }
+}`)
+	})
+}
+
+func mockDeleteRuleResponse(t *testing.T, ruleID string) {
+	url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
+	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.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockAddServerToGroupResponse(t *testing.T, serverID string) {
+	url := fmt.Sprintf("/servers/%s/action", serverID)
+	th.Mux.HandleFunc(url, 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, `
+{
+  "addSecurityGroup": {
+    "name": "test"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) {
+	url := fmt.Sprintf("/servers/%s/action", serverID)
+	th.Mux.HandleFunc(url, 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, `
+{
+  "removeSecurityGroup": {
+    "name": "test"
+  }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
new file mode 100644
index 0000000..09503d7
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -0,0 +1,298 @@
+package secgroups
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return SecurityGroupPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, createPage)
+}
+
+// List will return a collection of all the security groups for a particular
+// tenant.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return commonList(client, rootURL(client))
+}
+
+// ListByServer will return a collection of all the security groups which are
+// associated with a particular server.
+func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
+	return commonList(client, listByServerURL(client, serverID))
+}
+
+// GroupOpts is the underlying struct responsible for creating or updating
+// security groups. It therefore represents the mutable attributes of a
+// security group.
+type GroupOpts struct {
+	// Required - the name of your security group.
+	Name string `json:"name"`
+
+	// Required - the description of your security group.
+	Description string `json:"description"`
+}
+
+// CreateOpts is the struct responsible for creating a security group.
+type CreateOpts GroupOpts
+
+// CreateOptsBuilder builds the create options into a serializable format.
+type CreateOptsBuilder interface {
+	ToSecGroupCreateMap() (map[string]interface{}, error)
+}
+
+var (
+	errName = errors.New("Name is a required field")
+	errDesc = errors.New("Description is a required field")
+)
+
+// ToSecGroupCreateMap builds the create options into a serializable format.
+func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) {
+	sg := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return sg, errName
+	}
+	if opts.Description == "" {
+		return sg, errDesc
+	}
+
+	sg["name"] = opts.Name
+	sg["description"] = opts.Description
+
+	return map[string]interface{}{"security_group": sg}, nil
+}
+
+// Create will create a new security group.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var result CreateResult
+
+	reqBody, err := opts.ToSecGroupCreateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     &reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// UpdateOpts is the struct responsible for updating an existing security group.
+type UpdateOpts GroupOpts
+
+// UpdateOptsBuilder builds the update options into a serializable format.
+type UpdateOptsBuilder interface {
+	ToSecGroupUpdateMap() (map[string]interface{}, error)
+}
+
+// ToSecGroupUpdateMap builds the update options into a serializable format.
+func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) {
+	sg := make(map[string]interface{})
+
+	if opts.Name == "" {
+		return sg, errName
+	}
+	if opts.Description == "" {
+		return sg, errDesc
+	}
+
+	sg["name"] = opts.Name
+	sg["description"] = opts.Description
+
+	return map[string]interface{}{"security_group": sg}, nil
+}
+
+// Update will modify the mutable properties of a security group, notably its
+// name and description.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	reqBody, err := opts.ToSecGroupUpdateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = perigee.Request("PUT", resourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     &reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Get will return details for a particular security group.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+
+	_, result.Err = perigee.Request("GET", resourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Delete will permanently delete a security group from the project.
+func Delete(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+
+	_, result.Err = perigee.Request("DELETE", resourceURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return result
+}
+
+// CreateRuleOpts represents the configuration for adding a new rule to an
+// existing security group.
+type CreateRuleOpts struct {
+	// Required - the ID of the group that this rule will be added to.
+	ParentGroupID string `json:"parent_group_id"`
+
+	// Required - the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+
+	// Required - the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+
+	// Required - the protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol"`
+
+	// ONLY required if FromGroupID is blank. This represents the IP range that
+	// will be the source of network traffic to your security group. Use
+	// 0.0.0.0/0 to allow all IP addresses.
+	CIDR string `json:"cidr,omitempty"`
+
+	// ONLY required if CIDR is blank. This value represents the ID of a group
+	// that forwards traffic to the parent group. So, instead of accepting
+	// network traffic from an entire IP range, you can instead refine the
+	// inbound source by an existing security group.
+	FromGroupID string `json:"group_id,omitempty"`
+}
+
+// CreateRuleOptsBuilder builds the create rule options into a serializable format.
+type CreateRuleOptsBuilder interface {
+	ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// ToRuleCreateMap builds the create rule options into a serializable format.
+func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	rule := make(map[string]interface{})
+
+	if opts.ParentGroupID == "" {
+		return rule, errors.New("A ParentGroupID must be set")
+	}
+	if opts.FromPort == 0 {
+		return rule, errors.New("A FromPort must be set")
+	}
+	if opts.ToPort == 0 {
+		return rule, errors.New("A ToPort must be set")
+	}
+	if opts.IPProtocol == "" {
+		return rule, errors.New("A IPProtocol must be set")
+	}
+	if opts.CIDR == "" && opts.FromGroupID == "" {
+		return rule, errors.New("A CIDR or FromGroupID must be set")
+	}
+
+	rule["parent_group_id"] = opts.ParentGroupID
+	rule["from_port"] = opts.FromPort
+	rule["to_port"] = opts.ToPort
+	rule["ip_protocol"] = opts.IPProtocol
+
+	if opts.CIDR != "" {
+		rule["cidr"] = opts.CIDR
+	}
+	if opts.FromGroupID != "" {
+		rule["from_group_id"] = opts.FromGroupID
+	}
+
+	return map[string]interface{}{"security_group_rule": rule}, nil
+}
+
+// CreateRule will add a new rule to an existing security group (whose ID is
+// specified in CreateRuleOpts). You have the option of controlling inbound
+// traffic from either an IP range (CIDR) or from another security group.
+func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) CreateRuleResult {
+	var result CreateRuleResult
+
+	reqBody, err := opts.ToRuleCreateMap()
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = perigee.Request("POST", rootRuleURL(client), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     &reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// DeleteRule will permanently delete a rule from a security group.
+func DeleteRule(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+
+	_, result.Err = perigee.Request("DELETE", resourceRuleURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return result
+}
+
+func actionMap(prefix, groupName string) map[string]map[string]string {
+	return map[string]map[string]string{
+		prefix + "SecurityGroup": map[string]string{"name": groupName},
+	}
+}
+
+// AddServerToGroup will associate a server and a security group, enforcing the
+// rules of the group on the server.
+func AddServerToGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+
+	_, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     actionMap("add", groupName),
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return result
+}
+
+// RemoveServerFromGroup will disassociate a server from a security group.
+func RemoveServerFromGroup(client *gophercloud.ServiceClient, serverID, groupName string) gophercloud.ErrResult {
+	var result gophercloud.ErrResult
+
+	_, result.Err = perigee.Request("POST", serverActionURL(client, serverID), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     actionMap("remove", groupName),
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+
+	return result
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests_test.go b/openstack/compute/v2/extensions/secgroups/requests_test.go
new file mode 100644
index 0000000..4e21d5d
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/requests_test.go
@@ -0,0 +1,248 @@
+package secgroups
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	serverID = "{serverID}"
+	groupID  = "{groupID}"
+	ruleID   = "{ruleID}"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListGroupsResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSecurityGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []SecurityGroup{
+			SecurityGroup{
+				ID:          groupID,
+				Description: "default",
+				Name:        "default",
+				Rules:       []Rule{},
+				TenantID:    "openstack",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestListByServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListGroupsByServerResponse(t, serverID)
+
+	count := 0
+
+	err := ListByServer(client.ServiceClient(), serverID).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSecurityGroups(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []SecurityGroup{
+			SecurityGroup{
+				ID:          groupID,
+				Description: "default",
+				Name:        "default",
+				Rules:       []Rule{},
+				TenantID:    "openstack",
+			},
+		}
+
+		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()
+
+	mockCreateGroupResponse(t)
+
+	opts := CreateOpts{
+		Name:        "test",
+		Description: "something",
+	}
+
+	group, err := Create(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{
+		ID:          groupID,
+		Name:        "test",
+		Description: "something",
+		TenantID:    "openstack",
+		Rules:       []Rule{},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateGroupResponse(t, groupID)
+
+	opts := UpdateOpts{
+		Name:        "new_name",
+		Description: "new_desc",
+	}
+
+	group, err := Update(client.ServiceClient(), groupID, opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{
+		ID:          groupID,
+		Name:        "new_name",
+		Description: "something",
+		TenantID:    "openstack",
+		Rules:       []Rule{},
+	}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetGroupsResponse(t, groupID)
+
+	group, err := Get(client.ServiceClient(), groupID).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{
+		ID:          groupID,
+		Description: "default",
+		Name:        "default",
+		TenantID:    "openstack",
+		Rules: []Rule{
+			Rule{
+				FromPort:      80,
+				ToPort:        85,
+				IPProtocol:    "TCP",
+				IPRange:       IPRange{CIDR: "0.0.0.0"},
+				Group:         Group{TenantID: "openstack", Name: "default"},
+				ParentGroupID: groupID,
+				ID:            ruleID,
+			},
+		},
+	}
+
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestGetNumericID(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	numericGroupID := 12345
+
+	mockGetNumericIDGroupResponse(t, numericGroupID)
+
+	group, err := Get(client.ServiceClient(), "12345").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &SecurityGroup{ID: "12345"}
+	th.AssertDeepEquals(t, expected, group)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteGroupResponse(t, groupID)
+
+	err := Delete(client.ServiceClient(), groupID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAddRule(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddRuleResponse(t)
+
+	opts := CreateRuleOpts{
+		ParentGroupID: groupID,
+		FromPort:      22,
+		ToPort:        22,
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := CreateRule(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Rule{
+		FromPort:      22,
+		ToPort:        22,
+		Group:         Group{},
+		IPProtocol:    "TCP",
+		ParentGroupID: groupID,
+		IPRange:       IPRange{CIDR: "0.0.0.0/0"},
+		ID:            ruleID,
+	}
+
+	th.AssertDeepEquals(t, expected, rule)
+}
+
+func TestDeleteRule(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteRuleResponse(t, ruleID)
+
+	err := DeleteRule(client.ServiceClient(), ruleID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAddServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddServerToGroupResponse(t, serverID)
+
+	err := AddServerToGroup(client.ServiceClient(), serverID, "test").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestRemoveServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockRemoveServerFromGroupResponse(t, serverID)
+
+	err := RemoveServerFromGroup(client.ServiceClient(), serverID, "test").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go
new file mode 100644
index 0000000..478c5dc
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/results.go
@@ -0,0 +1,147 @@
+package secgroups
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// SecurityGroup represents a security group.
+type SecurityGroup struct {
+	// The unique ID of the group. If Neutron is installed, this ID will be
+	// represented as a string UUID; if Neutron is not installed, it will be a
+	// numeric ID. For the sake of consistency, we always cast it to a string.
+	ID string
+
+	// The human-readable name of the group, which needs to be unique.
+	Name string
+
+	// The human-readable description of the group.
+	Description string
+
+	// The rules which determine how this security group operates.
+	Rules []Rule
+
+	// The ID of the tenant to which this security group belongs.
+	TenantID string `mapstructure:"tenant_id"`
+}
+
+// Rule represents a security group rule, a policy which determines how a
+// security group operates and what inbound traffic it allows in.
+type Rule struct {
+	// The unique ID. If Neutron is installed, this ID will be
+	// represented as a string UUID; if Neutron is not installed, it will be a
+	// numeric ID. For the sake of consistency, we always cast it to a string.
+	ID string
+
+	// The lower bound of the port range which this security group should open up
+	FromPort int `mapstructure:"from_port"`
+
+	// The upper bound of the port range which this security group should open up
+	ToPort int `mapstructure:"to_port"`
+
+	// The IP protocol (e.g. TCP) which the security group accepts
+	IPProtocol string `mapstructure:"ip_protocol"`
+
+	// The CIDR IP range whose traffic can be received
+	IPRange IPRange `mapstructure:"ip_range"`
+
+	// The security group ID to which this rule belongs
+	ParentGroupID string `mapstructure:"parent_group_id"`
+
+	// Not documented.
+	Group Group
+}
+
+// IPRange represents the IP range whose traffic will be accepted by the
+// security group.
+type IPRange struct {
+	CIDR string
+}
+
+// Group represents a group.
+type Group struct {
+	TenantID string `mapstructure:"tenant_id"`
+	Name     string
+}
+
+// SecurityGroupPage is a single page of a SecurityGroup collection.
+type SecurityGroupPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Security Groups contains any results.
+func (page SecurityGroupPage) IsEmpty() (bool, error) {
+	users, err := ExtractSecurityGroups(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractSecurityGroups returns a slice of SecurityGroups contained in a single page of results.
+func ExtractSecurityGroups(page pagination.Page) ([]SecurityGroup, error) {
+	casted := page.(SecurityGroupPage).Body
+	var response struct {
+		SecurityGroups []SecurityGroup `mapstructure:"security_groups"`
+	}
+
+	err := mapstructure.WeakDecode(casted, &response)
+
+	return response.SecurityGroups, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// Extract will extract a SecurityGroup struct from most responses.
+func (r commonResult) Extract() (*SecurityGroup, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		SecurityGroup SecurityGroup `mapstructure:"security_group"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &response)
+
+	return &response.SecurityGroup, err
+}
+
+// CreateRuleResult represents the result when adding rules to a security group.
+type CreateRuleResult struct {
+	gophercloud.Result
+}
+
+// Extract will extract a Rule struct from a CreateRuleResult.
+func (r CreateRuleResult) Extract() (*Rule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Rule Rule `mapstructure:"security_group_rule"`
+	}
+
+	err := mapstructure.WeakDecode(r.Body, &response)
+
+	return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/secgroups/urls.go b/openstack/compute/v2/extensions/secgroups/urls.go
new file mode 100644
index 0000000..f4760b6
--- /dev/null
+++ b/openstack/compute/v2/extensions/secgroups/urls.go
@@ -0,0 +1,32 @@
+package secgroups
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	secgrouppath = "os-security-groups"
+	rulepath     = "os-security-group-rules"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(secgrouppath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(secgrouppath)
+}
+
+func listByServerURL(c *gophercloud.ServiceClient, serverID string) string {
+	return c.ServiceURL(secgrouppath, "servers", serverID, secgrouppath)
+}
+
+func rootRuleURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rulepath)
+}
+
+func resourceRuleURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rulepath, id)
+}
+
+func serverActionURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("servers", id, "action")
+}
diff --git a/openstack/compute/v2/extensions/startstop/doc.go b/openstack/compute/v2/extensions/startstop/doc.go
new file mode 100644
index 0000000..d2729f8
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/doc.go
@@ -0,0 +1,5 @@
+/*
+Package startstop provides functionality to start and stop servers that have
+been provisioned by the OpenStack Compute service.
+*/
+package startstop
diff --git a/openstack/compute/v2/extensions/startstop/fixtures.go b/openstack/compute/v2/extensions/startstop/fixtures.go
new file mode 100644
index 0000000..670828a
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/fixtures.go
@@ -0,0 +1,27 @@
+package startstop
+
+import (
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockStartServerResponse(t *testing.T, id string) {
+	th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-start": null}`)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+func mockStopServerResponse(t *testing.T, id string) {
+	th.Mux.HandleFunc("/servers/"+id+"/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, `{"os-stop": null}`)
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/compute/v2/extensions/startstop/requests.go b/openstack/compute/v2/extensions/startstop/requests.go
new file mode 100644
index 0000000..99c91b0
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests.go
@@ -0,0 +1,40 @@
+package startstop
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+)
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "action")
+}
+
+// Start is the operation responsible for starting a Compute server.
+func Start(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+
+	reqBody := map[string]interface{}{"os-start": nil}
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     reqBody,
+		OkCodes:     []int{202},
+	})
+
+	return res
+}
+
+// Stop is the operation responsible for stopping a Compute server.
+func Stop(client *gophercloud.ServiceClient, id string) gophercloud.ErrResult {
+	var res gophercloud.ErrResult
+
+	reqBody := map[string]interface{}{"os-stop": nil}
+
+	_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		ReqBody:     reqBody,
+		OkCodes:     []int{202},
+	})
+
+	return res
+}
diff --git a/openstack/compute/v2/extensions/startstop/requests_test.go b/openstack/compute/v2/extensions/startstop/requests_test.go
new file mode 100644
index 0000000..97a121b
--- /dev/null
+++ b/openstack/compute/v2/extensions/startstop/requests_test.go
@@ -0,0 +1,30 @@
+package startstop
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const serverID = "{serverId}"
+
+func TestStart(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockStartServerResponse(t, serverID)
+
+	err := Start(client.ServiceClient(), serverID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestStop(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockStopServerResponse(t, serverID)
+
+	err := Stop(client.ServiceClient(), serverID).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index e872b07..0164605 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -457,3 +457,103 @@
 		fmt.Fprintf(w, response)
 	})
 }
+
+// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request.
+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" }`))
+	})
+}
+
+// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request.
+func HandleMetadatumGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+	})
+}
+
+// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request.
+func HandleMetadatumCreateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"meta": {
+				"foo": "bar"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+	})
+}
+
+// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request.
+func HandleMetadatumDeleteSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
+func HandleMetadataGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+	})
+}
+
+// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request.
+func HandleMetadataResetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+				"metadata": {
+					"foo": "bar",
+					"this": "that"
+				}
+			}`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+	})
+}
+
+// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request.
+func HandleMetadataUpdateSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/1234asdf/metadata", 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, `{
+				"metadata": {
+					"foo": "baz",
+					"this": "those"
+				}
+			}`)
+
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", "application/json")
+		w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`))
+	})
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 95a4188..81a4fca 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -2,6 +2,7 @@
 
 import (
 	"encoding/base64"
+	"errors"
 	"fmt"
 
 	"github.com/racker/perigee"
@@ -131,6 +132,10 @@
 
 	// ConfigDrive [optional] enables metadata injection through a configuration drive.
 	ConfigDrive bool
+
+	// AdminPass [optional] sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the response.
+	AdminPass string
 }
 
 // ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
@@ -158,12 +163,16 @@
 	if opts.Metadata != nil {
 		server["metadata"] = opts.Metadata
 	}
+	if opts.AdminPass != "" {
+		server["adminPass"] = opts.AdminPass
+	}
 
 	if len(opts.SecurityGroups) > 0 {
 		securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
 		for i, groupName := range opts.SecurityGroups {
 			securityGroups[i] = map[string]interface{}{"name": groupName}
 		}
+		server["security_groups"] = securityGroups
 	}
 
 	if len(opts.Networks) > 0 {
@@ -221,6 +230,7 @@
 	_, result.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
 		Results:     &result.Body,
 		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 203},
 	})
 	return result
 }
@@ -475,7 +485,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{}{
@@ -536,3 +546,182 @@
 
 	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
+}
+
+// ToServerRescueMap 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
+}
+
+// ResetMetadataOptsBuilder allows extensions to add additional parameters to the
+// Reset request.
+type ResetMetadataOptsBuilder interface {
+	ToMetadataResetMap() (map[string]interface{}, error)
+}
+
+// MetadataOpts is a map that contains key-value pairs.
+type MetadataOpts map[string]string
+
+// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
+	return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
+	return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ResetMetadata will create multiple new key-value pairs for the given server ID.
+// Note: Using this operation will erase any already-existing metadata and create
+// the new metadata provided. To keep any already-existing metadata, use the
+// UpdateMetadatas or UpdateMetadata function.
+func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult {
+	var res ResetMetadataResult
+	metadata, err := opts.ToMetadataResetMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	_, res.Err = perigee.Request("PUT", metadataURL(client, id), perigee.Options{
+		ReqBody:     metadata,
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return res
+}
+
+// Metadata requests all the metadata for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult {
+	var res GetMetadataResult
+	_, res.Err = perigee.Request("GET", metadataURL(client, id), perigee.Options{
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return res
+}
+
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type UpdateMetadataOptsBuilder interface {
+	ToMetadataUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID.
+// This operation does not affect already-existing metadata that is not specified
+// by opts.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
+	var res UpdateMetadataResult
+	metadata, err := opts.ToMetadataUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	_, res.Err = perigee.Request("POST", metadataURL(client, id), perigee.Options{
+		ReqBody:     metadata,
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return res
+}
+
+// MetadatumOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type MetadatumOptsBuilder interface {
+	ToMetadatumCreateMap() (map[string]interface{}, string, error)
+}
+
+// MetadatumOpts is a map of length one that contains a key-value pair.
+type MetadatumOpts map[string]string
+
+// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts.
+func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
+	if len(opts) != 1 {
+		return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.")
+	}
+	metadatum := map[string]interface{}{"meta": opts}
+	var key string
+	for k := range metadatum["meta"].(MetadatumOpts) {
+		key = k
+	}
+	return metadatum, key, nil
+}
+
+// CreateMetadatum will create or update the key-value pair with the given key for the given server ID.
+func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult {
+	var res CreateMetadatumResult
+	metadatum, key, err := opts.ToMetadatumCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("PUT", metadatumURL(client, id, key), perigee.Options{
+		ReqBody:     metadatum,
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return res
+}
+
+// Metadatum requests the key-value pair with the given key for the given server ID.
+func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult {
+	var res GetMetadatumResult
+	_, res.Err = perigee.Request("GET", metadatumURL(client, id, key), perigee.Options{
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return res
+}
+
+// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
+func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
+	var res DeleteMetadatumResult
+	_, res.Err = perigee.Request("DELETE", metadatumURL(client, id, key), perigee.Options{
+		Results:     &res.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+	})
+	return res
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 392e2d8..017e793 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -174,3 +174,93 @@
 	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)
+}
+
+func TestGetMetadatum(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadatumGetSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar"}
+	actual, err := Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateMetadatum(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadatumCreateSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar"}
+	actual, err := CreateMetadatum(client.ServiceClient(), "1234asdf", MetadatumOpts{"foo": "bar"}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteMetadatum(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadatumDeleteSuccessfully(t)
+
+	err := DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestGetMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadataGetSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar", "this": "that"}
+	actual, err := Metadata(client.ServiceClient(), "1234asdf").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResetMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadataResetSuccessfully(t)
+
+	expected := map[string]string{"foo": "bar", "this": "that"}
+	actual, err := ResetMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+		"foo":  "bar",
+		"this": "that",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleMetadataUpdateSuccessfully(t)
+
+	expected := map[string]string{"foo": "baz", "this": "those"}
+	actual, err := UpdateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+		"foo":  "baz",
+		"this": "those",
+	}).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index 53946ba..d63d7c8 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -1,6 +1,8 @@
 package servers
 
 import (
+	"reflect"
+
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
@@ -20,8 +22,21 @@
 		Server Server `mapstructure:"server"`
 	}
 
-	err := mapstructure.Decode(r.Body, &response)
-	return &response.Server, err
+	config := &mapstructure.DecoderConfig{
+		DecodeHook: toMapFromString,
+		Result:     &response,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	err = decoder.Decode(r.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	return &response.Server, nil
 }
 
 // CreateResult temporarily contains the response from a Create call.
@@ -39,7 +54,7 @@
 	serverResult
 }
 
-// DeleteResult temporarily contains the response from an Delete call.
+// DeleteResult temporarily contains the response from a Delete call.
 type DeleteResult struct {
 	gophercloud.ErrResult
 }
@@ -54,6 +69,25 @@
 	gophercloud.ErrResult
 }
 
+// RescueResult represents the result of a server rescue operation
+type RescueResult struct {
+	ActionResult
+}
+
+// Extract interprets any RescueResult as an AdminPass, if possible.
+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.
@@ -145,6 +179,93 @@
 	var response struct {
 		Servers []Server `mapstructure:"servers"`
 	}
-	err := mapstructure.Decode(casted, &response)
+
+	config := &mapstructure.DecoderConfig{
+		DecodeHook: toMapFromString,
+		Result:     &response,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	err = decoder.Decode(casted)
+
+	//err := mapstructure.Decode(casted, &response)
 	return response.Servers, err
 }
+
+// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
+type MetadataResult struct {
+	gophercloud.Result
+}
+
+// GetMetadataResult temporarily contains the response from a metadata Get call.
+type GetMetadataResult struct {
+	MetadataResult
+}
+
+// ResetMetadataResult temporarily contains the response from a metadata Reset call.
+type ResetMetadataResult struct {
+	MetadataResult
+}
+
+// UpdateMetadataResult temporarily contains the response from a metadata Update call.
+type UpdateMetadataResult struct {
+	MetadataResult
+}
+
+// MetadatumResult contains the result of a call for individual a single key-value pair.
+type MetadatumResult struct {
+	gophercloud.Result
+}
+
+// GetMetadatumResult temporarily contains the response from a metadatum Get call.
+type GetMetadatumResult struct {
+	MetadatumResult
+}
+
+// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
+type CreateMetadatumResult struct {
+	MetadatumResult
+}
+
+// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
+type DeleteMetadatumResult struct {
+	gophercloud.ErrResult
+}
+
+// Extract interprets any MetadataResult as a Metadata, if possible.
+func (r MetadataResult) Extract() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Metadata map[string]string `mapstructure:"metadata"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return response.Metadata, err
+}
+
+// Extract interprets any MetadatumResult as a Metadatum, if possible.
+func (r MetadatumResult) Extract() (map[string]string, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Metadatum map[string]string `mapstructure:"meta"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+	return response.Metadatum, err
+}
+
+func toMapFromString(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) {
+	if (from == reflect.String) && (to == reflect.Map) {
+		return map[string]interface{}{}, nil
+	}
+	return data, nil
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 57587ab..4bc6586 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -29,3 +29,11 @@
 func actionURL(client *gophercloud.ServiceClient, id string) string {
 	return client.ServiceURL("servers", id, "action")
 }
+
+func metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
+	return client.ServiceURL("servers", id, "metadata", key)
+}
+
+func metadataURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "metadata")
+}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
index cc895c9..17a1d28 100644
--- a/openstack/compute/v2/servers/urls_test.go
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -54,3 +54,15 @@
 	expected := endpoint + "servers/foo/action"
 	th.CheckEquals(t, expected, actual)
 }
+
+func TestMetadatumURL(t *testing.T) {
+	actual := metadatumURL(endpointClient(), "foo", "bar")
+	expected := endpoint + "servers/foo/metadata/bar"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+	actual := metadataURL(endpointClient(), "foo")
+	expected := endpoint + "servers/foo/metadata"
+	th.CheckEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/util_test.go b/openstack/compute/v2/servers/util_test.go
deleted file mode 100644
index e192ae3..0000000
--- a/openstack/compute/v2/servers/util_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package servers
-
-import (
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	th "github.com/rackspace/gophercloud/testhelper"
-	"github.com/rackspace/gophercloud/testhelper/client"
-)
-
-func TestWaitForStatus(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	th.Mux.HandleFunc("/servers/4321", func(w http.ResponseWriter, r *http.Request) {
-		time.Sleep(2 * time.Second)
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"server": {
-				"name": "the-server",
-				"id": "4321",
-				"status": "ACTIVE"
-			}
-		}`)
-	})
-
-	err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0)
-	if err == nil {
-		t.Errorf("Expected error: 'Time Out in WaitFor'")
-	}
-
-	err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3)
-	th.CheckNoErr(t, err)
-}
diff --git a/openstack/identity/v2/extensions/admin/roles/docs.go b/openstack/identity/v2/extensions/admin/roles/docs.go
new file mode 100644
index 0000000..8954178
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/docs.go
@@ -0,0 +1,16 @@
+// Package roles provides functionality to interact with and control roles on
+// the API.
+//
+// A role represents a personality that a user can assume when performing a
+// specific set of operations. If a role includes a set of rights and
+// privileges, a user assuming that role inherits those rights and privileges.
+//
+// When a token is generated, the list of roles that user can assume is returned
+// back to them. Services that are being called by that user determine how they
+// interpret the set of roles a user has and to which operations or resources
+// each role grants access.
+//
+// It is up to individual services such as Compute or Image to assign meaning
+// to these roles. As far as the Identity service is concerned, a role is an
+// arbitrary name assigned by the user.
+package roles
diff --git a/openstack/identity/v2/extensions/admin/roles/fixtures.go b/openstack/identity/v2/extensions/admin/roles/fixtures.go
new file mode 100644
index 0000000..8256f0f
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/fixtures.go
@@ -0,0 +1,48 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/OS-KSADM/roles", 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, `
+{
+    "roles": [
+        {
+            "id": "123",
+            "name": "compute:admin",
+            "description": "Nova Administrator"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_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.StatusNoContent)
+	})
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/requests.go b/openstack/identity/v2/extensions/admin/roles/requests.go
new file mode 100644
index 0000000..152031a
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests.go
@@ -0,0 +1,44 @@
+package roles
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("PUT", userRoleURL(client, tenantID, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+	})
+
+	return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("DELETE", userRoleURL(client, tenantID, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/requests_test.go b/openstack/identity/v2/extensions/admin/roles/requests_test.go
new file mode 100644
index 0000000..7bfeea4
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/requests_test.go
@@ -0,0 +1,64 @@
+package roles
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []Role{
+			Role{
+				ID:          "123",
+				Name:        "compute:admin",
+				Description: "Nova Administrator",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := AddUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := DeleteUserRole(client.ServiceClient(), "{tenant_id}", "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/results.go b/openstack/identity/v2/extensions/admin/roles/results.go
new file mode 100644
index 0000000..ebb3aa5
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/results.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Role represents an API role resource.
+type Role struct {
+	// The unique ID for the role.
+	ID string
+
+	// The human-readable name of the role.
+	Name string
+
+	// The description of the role.
+	Description string
+
+	// The associated service for this role.
+	ServiceID string
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+	casted := page.(RolePage).Body
+	var response struct {
+		Roles []Role `mapstructure:"roles"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Roles, err
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/identity/v2/extensions/admin/roles/urls.go b/openstack/identity/v2/extensions/admin/roles/urls.go
new file mode 100644
index 0000000..61b3155
--- /dev/null
+++ b/openstack/identity/v2/extensions/admin/roles/urls.go
@@ -0,0 +1,21 @@
+package roles
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	ExtPath  = "OS-KSADM"
+	RolePath = "roles"
+	UserPath = "users"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(ExtPath, RolePath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(ExtPath, RolePath)
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string {
+	return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID)
+}
diff --git a/openstack/identity/v2/users/doc.go b/openstack/identity/v2/users/doc.go
new file mode 100644
index 0000000..82abcb9
--- /dev/null
+++ b/openstack/identity/v2/users/doc.go
@@ -0,0 +1 @@
+package users
diff --git a/openstack/identity/v2/users/fixtures.go b/openstack/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..8941868
--- /dev/null
+++ b/openstack/identity/v2/users/fixtures.go
@@ -0,0 +1,163 @@
+package users
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+    "users":[
+        {
+            "id": "u1000",
+						"name": "John Smith",
+            "username": "jqsmith",
+            "email": "john.smith@example.org",
+            "enabled": true,
+						"tenant_id": "12345"
+        },
+        {
+            "id": "u1001",
+						"name": "Jane Smith",
+            "username": "jqsmith",
+            "email": "jane.smith@example.org",
+            "enabled": true,
+						"tenant_id": "12345"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func mockCreateUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+    "user": {
+		    "name": "new_user",
+		    "tenant_id": "12345",
+				"enabled": false,
+				"email": "new_user@foo.com"
+    }
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "user": {
+        "name": "new_user",
+        "tenant_id": "12345",
+        "enabled": false,
+        "email": "new_user@foo.com",
+        "id": "c39e3de9be2d4c779f1dfd6abacc176d"
+    }
+}
+`)
+	})
+}
+
+func mockGetUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/new_user", 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, `
+{
+		"user": {
+				"name": "new_user",
+				"tenant_id": "12345",
+				"enabled": false,
+				"email": "new_user@foo.com",
+				"id": "c39e3de9be2d4c779f1dfd6abacc176d"
+		}
+}
+`)
+	})
+}
+
+func mockUpdateUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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, `
+{
+    "user": {
+		    "name": "new_name",
+		    "enabled": true,
+		    "email": "new_email@foo.com"
+    }
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"user": {
+				"name": "new_name",
+				"tenant_id": "12345",
+				"enabled": true,
+				"email": "new_email@foo.com",
+				"id": "c39e3de9be2d4c779f1dfd6abacc176d"
+		}
+}
+`)
+	})
+}
+
+func mockDeleteUserResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func mockListRolesResponse(t *testing.T) {
+	th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", 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, `
+{
+    "roles": [
+        {
+            "id": "9fe2ff9ee4384b1894a90878d3e92bab",
+            "name": "foo_role"
+        },
+        {
+            "id": "1ea3d56793574b668e85960fbf651e13",
+            "name": "admin"
+        }
+    ]
+}
+	`)
+	})
+}
diff --git a/openstack/identity/v2/users/requests.go b/openstack/identity/v2/users/requests.go
new file mode 100644
index 0000000..4ce395f
--- /dev/null
+++ b/openstack/identity/v2/users/requests.go
@@ -0,0 +1,180 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return UserPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, rootURL(client), createPage)
+}
+
+// EnabledState represents whether the user is enabled or not.
+type EnabledState *bool
+
+// Useful variables to use when creating or updating users.
+var (
+	iTrue  = true
+	iFalse = false
+
+	Enabled  EnabledState = &iTrue
+	Disabled EnabledState = &iFalse
+)
+
+// CommonOpts are the parameters that are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+	// Either a name or username is required. When provided, the value must be
+	// unique or a 409 conflict error will be returned. If you provide a name but
+	// omit a username, the latter will be set to the former; and vice versa.
+	Name, Username string
+
+	// The ID of the tenant to which you want to assign this user.
+	TenantID string
+
+	// Indicates whether this user is enabled or not.
+	Enabled EnabledState
+
+	// The email address of this user.
+	Email string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+type CreateOptsBuilder interface {
+	ToUserCreateMap() (map[string]interface{}, error)
+}
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+
+	if opts.Name == "" && opts.Username == "" {
+		return m, errors.New("Either a Name or Username must be provided")
+	}
+
+	if opts.Name != "" {
+		m["name"] = opts.Name
+	}
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.TenantID != "" {
+		m["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToUserCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = perigee.Request("POST", rootURL(client), perigee.Options{
+		Results:     &res.Body,
+		ReqBody:     reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+	})
+
+	return res
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+
+	_, result.Err = perigee.Request("GET", ResourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if opts.Name != "" {
+		m["name"] = opts.Name
+	}
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.TenantID != "" {
+		m["tenant_id"] = opts.TenantID
+	}
+
+	return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	_, result.Err = perigee.Request("PUT", ResourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     opts.ToUserUpdateMap(),
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
+	var result DeleteResult
+
+	_, result.Err = perigee.Request("DELETE", ResourceURL(client, id), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
+
+func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager {
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return RolePage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage)
+}
diff --git a/openstack/identity/v2/users/requests_test.go b/openstack/identity/v2/users/requests_test.go
new file mode 100644
index 0000000..04f8371
--- /dev/null
+++ b/openstack/identity/v2/users/requests_test.go
@@ -0,0 +1,165 @@
+package users
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListUserResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractUsers(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []User{
+			User{
+				ID:       "u1000",
+				Name:     "John Smith",
+				Username: "jqsmith",
+				Email:    "john.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+			User{
+				ID:       "u1001",
+				Name:     "Jane Smith",
+				Username: "jqsmith",
+				Email:    "jane.smith@example.org",
+				Enabled:  true,
+				TenantID: "12345",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateUserResponse(t)
+
+	opts := CreateOpts{
+		Name:     "new_user",
+		TenantID: "12345",
+		Enabled:  Disabled,
+		Email:    "new_user@foo.com",
+	}
+
+	user, err := Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_user",
+		ID:       "c39e3de9be2d4c779f1dfd6abacc176d",
+		Email:    "new_user@foo.com",
+		Enabled:  false,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetUserResponse(t)
+
+	user, err := Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_user",
+		ID:       "c39e3de9be2d4c779f1dfd6abacc176d",
+		Email:    "new_user@foo.com",
+		Enabled:  false,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateUserResponse(t)
+
+	id := "c39e3de9be2d4c779f1dfd6abacc176d"
+	opts := UpdateOpts{
+		Name:    "new_name",
+		Enabled: Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	expected := &User{
+		Name:     "new_name",
+		ID:       id,
+		Email:    "new_email@foo.com",
+		Enabled:  true,
+		TenantID: "12345",
+	}
+
+	th.AssertDeepEquals(t, expected, user)
+}
+
+func TestDeleteUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteUserResponse(t)
+
+	res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestListingUserRoles(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListRolesResponse(t)
+
+	tenantID := "1d8b6120dcc640fda4fc9194ffc80273"
+	userID := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+	err := ListRoles(client.ServiceClient(), tenantID, userID).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractRoles(page)
+		th.AssertNoErr(t, err)
+
+		expected := []Role{
+			Role{ID: "9fe2ff9ee4384b1894a90878d3e92bab", Name: "foo_role"},
+			Role{ID: "1ea3d56793574b668e85960fbf651e13", Name: "admin"},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v2/users/results.go b/openstack/identity/v2/users/results.go
new file mode 100644
index 0000000..f531d5d
--- /dev/null
+++ b/openstack/identity/v2/users/results.go
@@ -0,0 +1,128 @@
+package users
+
+import (
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+	// The UUID for this user.
+	ID string
+
+	// The human name for this user.
+	Name string
+
+	// The username for this user.
+	Username string
+
+	// Indicates whether the user is enabled (true) or disabled (false).
+	Enabled bool
+
+	// The email address for this user.
+	Email string
+
+	// The ID of the tenant to which this user belongs.
+	TenantID string `mapstructure:"tenant_id"`
+}
+
+// Role assigns specific responsibilities to users, allowing them to accomplish
+// certain API operations whilst scoped to a service.
+type Role struct {
+	// UUID of the role
+	ID string
+
+	// Name of the role
+	Name string
+}
+
+// UserPage is a single page of a User collection.
+type UserPage struct {
+	pagination.SinglePageBase
+}
+
+// RolePage is a single page of a user Role collection.
+type RolePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page UserPage) IsEmpty() (bool, error) {
+	users, err := ExtractUsers(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractUsers returns a slice of Tenants contained in a single page of results.
+func ExtractUsers(page pagination.Page) ([]User, error) {
+	casted := page.(UserPage).Body
+	var response struct {
+		Users []User `mapstructure:"users"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Users, err
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page RolePage) IsEmpty() (bool, error) {
+	users, err := ExtractRoles(page)
+	if err != nil {
+		return false, err
+	}
+	return len(users) == 0, nil
+}
+
+// ExtractRoles returns a slice of Roles contained in a single page of results.
+func ExtractRoles(page pagination.Page) ([]Role, error) {
+	casted := page.(RolePage).Body
+	var response struct {
+		Roles []Role `mapstructure:"roles"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	return response.Roles, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as a User, if possible.
+func (r commonResult) Extract() (*User, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		User User `mapstructure:"user"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.User, err
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a Delete operation
+type DeleteResult struct {
+	commonResult
+}
diff --git a/openstack/identity/v2/users/urls.go b/openstack/identity/v2/users/urls.go
new file mode 100644
index 0000000..7ec4385
--- /dev/null
+++ b/openstack/identity/v2/users/urls.go
@@ -0,0 +1,21 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	tenantPath = "tenants"
+	userPath   = "users"
+	rolePath   = "roles"
+)
+
+func ResourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(userPath, id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(userPath)
+}
+
+func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string {
+	return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath)
+}
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/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index eaa7136..dedbb25 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -188,7 +188,7 @@
 	}
 
 	// Send request to API
-	_, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
+	_, res.Err = perigee.Request("PUT", updateURL(c, networkID), perigee.Options{
 		MoreHeaders: c.AuthenticatedHeaders(),
 		ReqBody:     &reqBody,
 		Results:     &res.Body,
diff --git a/openstack/networking/v2/networks/urls.go b/openstack/networking/v2/networks/urls.go
index 33c2387..a9eecc5 100644
--- a/openstack/networking/v2/networks/urls.go
+++ b/openstack/networking/v2/networks/urls.go
@@ -22,6 +22,10 @@
 	return rootURL(c)
 }
 
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return resourceURL(c, id)
+}
+
 func deleteURL(c *gophercloud.ServiceClient, id string) string {
 	return resourceURL(c, id)
 }
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 3399907..06d273e 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -164,7 +164,6 @@
 		ReqBody:     &reqBody,
 		Results:     &res.Body,
 		OkCodes:     []int{201},
-		DumpReqJson: true,
 	})
 
 	return res
diff --git a/openstack/objectstorage/v1/objects/fixtures.go b/openstack/objectstorage/v1/objects/fixtures.go
index d951160..ec61637 100644
--- a/openstack/objectstorage/v1/objects/fixtures.go
+++ b/openstack/objectstorage/v1/objects/fixtures.go
@@ -105,13 +105,31 @@
 	})
 }
 
-// HandleCreateObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux that
-// responds with a `Create` response.
-func HandleCreateObjectSuccessfully(t *testing.T) {
+// HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux
+// that responds with a `Create` response. A Content-Type of "text/plain" is expected.
+func HandleCreateTextObjectSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "text/plain")
+		th.TestHeader(t, r, "Accept", "application/json")
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
+// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server-
+// side content-type detection will be triggered properly.
+func HandleCreateTypelessObjectSuccessfully(t *testing.T) {
 	th.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "PUT")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		th.TestHeader(t, r, "Accept", "application/json")
+
+		if contentType, present := r.Header["Content-Type"]; present {
+			t.Errorf("Expected Content-Type header to be omitted, but was %#v", contentType)
+		}
+
 		w.WriteHeader(http.StatusCreated)
 	})
 }
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index 9778de3..0fdb041 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -18,6 +18,10 @@
 
 // ListOpts is a structure that holds parameters for listing objects.
 type ListOpts struct {
+	// Full is a true/false value that represents the amount of object information
+	// returned. If Full is set to true, then the content-type, number of bytes, hash
+	// date last modified, and name are returned. If set to false or not set, then
+	// only the object names are returned.
 	Full      bool
 	Limit     int    `q:"limit"`
 	Marker    string `q:"marker"`
@@ -88,7 +92,7 @@
 
 // ToObjectDownloadParams formats a DownloadOpts into a query string and map of
 // headers.
-func (opts ListOpts) ToObjectDownloadParams() (map[string]string, string, error) {
+func (opts DownloadOpts) ToObjectDownloadParams() (map[string]string, string, error) {
 	q, err := gophercloud.BuildQueryString(opts)
 	if err != nil {
 		return nil, "", err
@@ -125,7 +129,7 @@
 
 	resp, err := perigee.Request("GET", url, perigee.Options{
 		MoreHeaders: h,
-		OkCodes:     []int{200},
+		OkCodes:     []int{200, 304},
 	})
 
 	res.Body = resp.HttpResponse.Body
@@ -146,7 +150,7 @@
 	Metadata           map[string]string
 	ContentDisposition string `h:"Content-Disposition"`
 	ContentEncoding    string `h:"Content-Encoding"`
-	ContentLength      int    `h:"Content-Length"`
+	ContentLength      int64  `h:"Content-Length"`
 	ContentType        string `h:"Content-Type"`
 	CopyFrom           string `h:"X-Copy-From"`
 	DeleteAfter        int    `h:"X-Delete-After"`
@@ -201,14 +205,20 @@
 		url += query
 	}
 
-	contentType := h["Content-Type"]
-
-	resp, err := perigee.Request("PUT", url, perigee.Options{
-		ContentType: contentType,
+	popts := perigee.Options{
 		ReqBody:     content,
 		MoreHeaders: h,
 		OkCodes:     []int{201, 202},
-	})
+	}
+
+	if contentType, explicit := h["Content-Type"]; explicit {
+		popts.ContentType = contentType
+		delete(h, "Content-Type")
+	} else {
+		popts.OmitContentType = true
+	}
+
+	resp, err := perigee.Request("PUT", url, popts)
 	res.Header = resp.HttpResponse.Header
 	res.Err = err
 	return res
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
index c3c28a7..6be3534 100644
--- a/openstack/objectstorage/v1/objects/requests_test.go
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -83,14 +83,24 @@
 func TestCreateObject(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
-	HandleCreateObjectSuccessfully(t)
+	HandleCreateTextObjectSuccessfully(t)
 
 	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
-	options := &CreateOpts{ContentType: "application/json"}
+	options := &CreateOpts{ContentType: "text/plain"}
 	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
 	th.AssertNoErr(t, res.Err)
 }
 
+func TestCreateObjectWithoutContentType(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateTypelessObjectSuccessfully(t)
+
+	content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
+	th.AssertNoErr(t, res.Err)
+}
+
 func TestCopyObject(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go
index 102d94c..b51b840 100644
--- a/openstack/objectstorage/v1/objects/results.go
+++ b/openstack/objectstorage/v1/objects/results.go
@@ -14,11 +14,22 @@
 
 // Object is a structure that holds information related to a storage object.
 type Object struct {
-	Bytes        int    `json:"bytes" mapstructure:"bytes"`
-	ContentType  string `json:"content_type" mapstructure:"content_type"`
-	Hash         string `json:"hash" mapstructure:"hash"`
+	// Bytes is the total number of bytes that comprise the object.
+	Bytes int64 `json:"bytes" mapstructure:"bytes"`
+
+	// ContentType is the content type of the object.
+	ContentType string `json:"content_type" mapstructure:"content_type"`
+
+	// Hash represents the MD5 checksum value of the object's content.
+	Hash string `json:"hash" mapstructure:"hash"`
+
+	// LastModified is the RFC3339Milli time the object was last modified, represented
+	// as a string. For any given object (obj), this value may be parsed to a time.Time:
+	// lastModified, err := time.Parse(gophercloud.RFC3339Milli, obj.LastModified)
 	LastModified string `json:"last_modified" mapstructure:"last_modified"`
-	Name         string `json:"name" mapstructure:"name"`
+
+	// Name is the unique name for the object.
+	Name string `json:"name" mapstructure:"name"`
 }
 
 // ObjectPage is a single page of objects that is returned from a call to the
diff --git a/package.go b/package.go
deleted file mode 100644
index e8c2e82..0000000
--- a/package.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-Package gophercloud provides a multi-vendor interface to OpenStack-compatible
-clouds. The library has a three-level hierarchy: providers, services, and
-resources.
-
-Provider structs represent the service providers that offer and manage a
-collection of services. Examples of providers include: OpenStack, Rackspace,
-HP. These are defined like so:
-
-  opts := gophercloud.AuthOptions{
-    IdentityEndpoint: "https://my-openstack.com:5000/v2.0",
-    Username: "{username}",
-    Password: "{password}",
-    TenantID: "{tenant_id}",
-  }
-
-  provider, err := openstack.AuthenticatedClient(opts)
-
-Service structs are specific to a provider and handle all of the logic and
-operations for a particular OpenStack service. Examples of services include:
-Compute, Object Storage, Block Storage. In order to define one, you need to
-pass in the parent provider, like so:
-
-  opts := gophercloud.EndpointOpts{Region: "RegionOne"}
-
-  client := openstack.NewComputeV2(provider, opts)
-
-Resource structs are the domain models that services make use of in order
-to work with and represent the state of API resources:
-
-  server, err := servers.Get(client, "{serverId}").Extract()
-
-Another convention is to return Result structs for API operations, which allow
-you to access the HTTP headers, response body, and associated errors with the
-network transaction. To get a resource struct, you then call the Extract
-method which is chained to the response.
-*/
-package gophercloud
diff --git a/params.go b/params.go
index 5fe3c2c..948783b 100644
--- a/params.go
+++ b/params.go
@@ -9,11 +9,35 @@
 	"time"
 )
 
-// MaybeString takes a string that might be a zero-value, and either returns a
-// pointer to its address or a nil value (i.e. empty pointer). This is useful
-// for converting zero values in options structs when the end-user hasn't
-// defined values. Those zero values need to be nil in order for the JSON
-// serialization to ignore them.
+// 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.
+
+It takes a string that might be a zero value and returns either a pointer to its
+address or nil. This is useful for allowing users to conveniently omit values
+from an options struct by leaving them zeroed, but still pass nil to the JSON
+serializer so they'll be omitted from the request body.
+*/
 func MaybeString(original string) *string {
 	if original != "" {
 		return &original
@@ -21,8 +45,14 @@
 	return nil
 }
 
-// MaybeInt takes an int that might be a zero-value, and either returns a
-// pointer to its address or a nil value (i.e. empty pointer).
+/*
+MaybeInt is an internal function to be used by request methods in individual
+resource packages.
+
+Like MaybeString, it accepts an int that may or may not be a zero value, and
+returns either a pointer to its address or nil. It's intended to hint that the
+JSON serializer should omit its field.
+*/
 func MaybeInt(original int) *int {
 	if original != 0 {
 		return &original
@@ -61,19 +91,26 @@
 }
 
 /*
-BuildQueryString accepts a generic structure and parses it URL struct. It
-converts field names into query names based on "q" tags. So for example, this
-type:
+BuildQueryString is an internal function to be used by request methods in
+individual resource packages.
 
-	struct {
+It accepts a tagged structure and expands it into a URL struct. Field names are
+converted into query parameters based on a "q" tag. For example:
+
+	type struct Something {
 	   Bar string `q:"x_bar"`
 	   Baz int    `q:"lorem_ipsum"`
-	}{
-	   Bar: "XXX",
-	   Baz: "YYY",
 	}
 
-will be converted into ?x_bar=XXX&lorem_ipsum=YYYY
+	instance := Something{
+	   Bar: "AAA",
+	   Baz: "BBB",
+	}
+
+will be converted into "?x_bar=AAA&lorem_ipsum=BBB".
+
+The struct's fields may be strings, integers, or boolean values. Fields left at
+their type's zero value will be omitted from the query.
 */
 func BuildQueryString(opts interface{}) (*url.URL, error) {
 	optsValue := reflect.ValueOf(opts)
@@ -86,7 +123,8 @@
 		optsType = optsType.Elem()
 	}
 
-	var optsSlice []string
+	params := url.Values{}
+
 	if optsValue.Kind() == reflect.Struct {
 		for i := 0; i < optsValue.NumField(); i++ {
 			v := optsValue.Field(i)
@@ -101,11 +139,11 @@
 				if !isZero(v) {
 					switch v.Kind() {
 					case reflect.String:
-						optsSlice = append(optsSlice, tags[0]+"="+v.String())
+						params.Add(tags[0], v.String())
 					case reflect.Int:
-						optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatInt(v.Int(), 10))
+						params.Add(tags[0], strconv.FormatInt(v.Int(), 10))
 					case reflect.Bool:
-						optsSlice = append(optsSlice, tags[0]+"="+strconv.FormatBool(v.Bool()))
+						params.Add(tags[0], strconv.FormatBool(v.Bool()))
 					}
 				} else {
 					// Otherwise, the field is not set.
@@ -115,26 +153,42 @@
 					}
 				}
 			}
+		}
 
-		}
-		// URL encode the string for safety.
-		s := strings.Join(optsSlice, "&")
-		if s != "" {
-			s = "?" + s
-		}
-		u, err := url.Parse(s)
-		if err != nil {
-			return nil, err
-		}
-		return u, nil
+		return &url.URL{RawQuery: params.Encode()}, nil
 	}
 	// Return an error if the underlying type of 'opts' isn't a struct.
 	return nil, fmt.Errorf("Options type is not a struct.")
 }
 
-// BuildHeaders accepts a generic structure and parses it into a string map. It
-// converts field names into header names based on "h" tags, and field values
-// into header values by a simple one-to-one mapping.
+/*
+BuildHeaders is an internal function to be used by request methods in
+individual resource packages.
+
+It accepts an arbitrary tagged structure and produces a string map that's
+suitable for use as the HTTP headers of an outgoing request. Field names are
+mapped to header names based in "h" tags.
+
+  type struct Something {
+    Bar string `h:"x_bar"`
+    Baz int    `h:"lorem_ipsum"`
+  }
+
+  instance := Something{
+    Bar: "AAA",
+    Baz: "BBB",
+  }
+
+will be converted into:
+
+  map[string]string{
+    "x_bar": "AAA",
+    "lorem_ipsum": "BBB",
+  }
+
+Untagged fields and fields left at their zero values are skipped. Integers,
+booleans and string values are supported.
+*/
 func BuildHeaders(opts interface{}) (map[string]string, error) {
 	optsValue := reflect.ValueOf(opts)
 	if optsValue.Kind() == reflect.Ptr {
@@ -182,3 +236,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/params_test.go b/params_test.go
index 9f1d3bd..4a2c9fe 100644
--- a/params_test.go
+++ b/params_test.go
@@ -43,7 +43,7 @@
 		R: "red",
 		C: true,
 	}
-	expected := &url.URL{RawQuery: "j=2&r=red&c=true"}
+	expected := &url.URL{RawQuery: "c=true&j=2&r=red"}
 	actual, err := BuildQueryString(&opts)
 	if err != nil {
 		t.Errorf("Error building query string: %v", err)
@@ -138,5 +138,18 @@
 	expected = false
 	actual = isZero(testStructValue)
 	th.CheckEquals(t, expected, actual)
+}
 
+func TestQueriesAreEscaped(t *testing.T) {
+	type foo struct {
+		Name  string `q:"something"`
+		Shape string `q:"else"`
+	}
+
+	expected := &url.URL{RawQuery: "else=Triangl+e&something=blah%2B%3F%21%21foo"}
+
+	actual, err := BuildQueryString(foo{Name: "blah+?!!foo", Shape: "Triangl e"})
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, expected, actual)
 }
diff --git a/rackspace/cdn/v1/base/delegate.go b/rackspace/cdn/v1/base/delegate.go
new file mode 100644
index 0000000..5af7e07
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate.go
@@ -0,0 +1,18 @@
+package base
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+)
+
+// Get retrieves the home document, allowing the user to discover the
+// entire API.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+	return os.Get(c)
+}
+
+// Ping retrieves a ping to the server.
+func Ping(c *gophercloud.ServiceClient) os.PingResult {
+	return os.Ping(c)
+}
diff --git a/rackspace/cdn/v1/base/delegate_test.go b/rackspace/cdn/v1/base/delegate_test.go
new file mode 100644
index 0000000..3c05801
--- /dev/null
+++ b/rackspace/cdn/v1/base/delegate_test.go
@@ -0,0 +1,44 @@
+package base
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/base"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetHomeDocument(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleGetSuccessfully(t)
+
+	actual, err := Get(fake.ServiceClient()).Extract()
+	th.CheckNoErr(t, err)
+
+	expected := os.HomeDocument{
+		"rel/cdn": map[string]interface{}{
+			"href-template": "services{?marker,limit}",
+			"href-vars": map[string]interface{}{
+				"marker": "param/marker",
+				"limit": "param/limit",
+			},
+			"hints": map[string]interface{}{
+				"allow": []string{"GET"},
+				"formats": map[string]interface{}{
+					"application/json": map[string]interface{}{},
+				},
+			},
+		},
+	}
+	th.CheckDeepEquals(t, expected, *actual)
+}
+
+func TestPing(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandlePingSuccessfully(t)
+
+	err := Ping(fake.ServiceClient()).ExtractErr()
+	th.CheckNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/base/doc.go b/rackspace/cdn/v1/base/doc.go
new file mode 100644
index 0000000..5582306
--- /dev/null
+++ b/rackspace/cdn/v1/base/doc.go
@@ -0,0 +1,4 @@
+// Package base provides information and interaction with the base API
+// resource in the Rackspace CDN service. This API resource allows for
+// retrieving the Home Document and pinging the root URL.
+package base
diff --git a/rackspace/cdn/v1/flavors/delegate.go b/rackspace/cdn/v1/flavors/delegate.go
new file mode 100644
index 0000000..7152fa2
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate.go
@@ -0,0 +1,18 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a single page of CDN flavors.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(c)
+}
+
+// Get retrieves a specific flavor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(c, id)
+}
diff --git a/rackspace/cdn/v1/flavors/delegate_test.go b/rackspace/cdn/v1/flavors/delegate_test.go
new file mode 100644
index 0000000..d6d299d
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/delegate_test.go
@@ -0,0 +1,90 @@
+package flavors
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleListCDNFlavorsSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractFlavors(page)
+		if err != nil {
+			t.Errorf("Failed to extract flavors: %v", err)
+			return false, err
+		}
+
+		expected := []os.Flavor{
+			os.Flavor{
+				ID: "europe",
+				Providers: []os.Provider{
+					os.Provider{
+						Provider: "Fastly",
+						Links: []gophercloud.Link{
+							gophercloud.Link{
+								Href: "http://www.fastly.com",
+								Rel:  "provider_url",
+							},
+						},
+					},
+				},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "self",
+					},
+				},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleGetCDNFlavorSuccessfully(t)
+
+	expected := &os.Flavor{
+		ID: "asia",
+		Providers: []os.Provider{
+			os.Provider{
+				Provider: "ChinaCache",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "http://www.chinacache.com",
+						Rel:  "provider_url",
+					},
+				},
+			},
+		},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+				Rel:  "self",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "asia").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/cdn/v1/flavors/doc.go b/rackspace/cdn/v1/flavors/doc.go
new file mode 100644
index 0000000..4ad966e
--- /dev/null
+++ b/rackspace/cdn/v1/flavors/doc.go
@@ -0,0 +1,6 @@
+// Package flavors provides information and interaction with the flavors API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing flavors and retrieving a specific flavor.
+//
+// A flavor is a mapping configuration to a CDN provider.
+package flavors
diff --git a/rackspace/cdn/v1/serviceassets/delegate.go b/rackspace/cdn/v1/serviceassets/delegate.go
new file mode 100644
index 0000000..07c93a8
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate.go
@@ -0,0 +1,13 @@
+package serviceassets
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+)
+
+// Delete accepts a unique ID and deletes the CDN service asset associated with
+// it.
+func Delete(c *gophercloud.ServiceClient, id string, opts os.DeleteOptsBuilder) os.DeleteResult {
+	return os.Delete(c, id, opts)
+}
diff --git a/rackspace/cdn/v1/serviceassets/delegate_test.go b/rackspace/cdn/v1/serviceassets/delegate_test.go
new file mode 100644
index 0000000..328e168
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/delegate_test.go
@@ -0,0 +1,19 @@
+package serviceassets
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/serviceassets"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleDeleteCDNAssetSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", nil).ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/serviceassets/doc.go b/rackspace/cdn/v1/serviceassets/doc.go
new file mode 100644
index 0000000..46b3d50
--- /dev/null
+++ b/rackspace/cdn/v1/serviceassets/doc.go
@@ -0,0 +1,7 @@
+// Package serviceassets provides information and interaction with the
+// serviceassets API resource in the Rackspace CDN service. This API resource
+// allows for deleting cached assets.
+//
+// A service distributes assets across the network. Service assets let you
+// interrogate properties about these assets and perform certain actions on them.
+package serviceassets
diff --git a/rackspace/cdn/v1/services/delegate.go b/rackspace/cdn/v1/services/delegate.go
new file mode 100644
index 0000000..e3f1459
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate.go
@@ -0,0 +1,37 @@
+package services
+
+import (
+	"github.com/rackspace/gophercloud"
+
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// CDN services. It accepts a ListOpts struct, which allows for pagination via
+// marker and limit.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Create accepts a CreateOpts struct and creates a new CDN service using the
+// values provided.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Get retrieves a specific service based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(c, id)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing CDN service using
+// the values provided.
+func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult {
+	return os.Update(c, id, patches)
+}
+
+// Delete accepts a unique ID and deletes the CDN service associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(c, id)
+}
diff --git a/rackspace/cdn/v1/services/delegate_test.go b/rackspace/cdn/v1/services/delegate_test.go
new file mode 100644
index 0000000..6c48365
--- /dev/null
+++ b/rackspace/cdn/v1/services/delegate_test.go
@@ -0,0 +1,359 @@
+package services
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/cdn/v1/services"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleListCDNServiceSuccessfully(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient(), &os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractServices(page)
+		if err != nil {
+			t.Errorf("Failed to extract services: %v", err)
+			return false, err
+		}
+
+		expected := []os.Service{
+			os.Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Name: "mywebsite.com",
+				Domains: []os.Domain{
+					os.Domain{
+						Domain: "www.mywebsite.com",
+					},
+				},
+				Origins: []os.Origin{
+					os.Origin{
+						Origin: "mywebsite.com",
+						Port:   80,
+						SSL:    false,
+					},
+				},
+				Caching: []os.CacheRule{
+					os.CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+					os.CacheRule{
+						Name: "home",
+						TTL:  17200,
+						Rules: []os.TTLRule{
+							os.TTLRule{
+								Name:       "index",
+								RequestURL: "/index.htm",
+							},
+						},
+					},
+					os.CacheRule{
+						Name: "images",
+						TTL:  12800,
+						Rules: []os.TTLRule{
+							os.TTLRule{
+								Name:       "images",
+								RequestURL: "*.png",
+							},
+						},
+					},
+				},
+				Restrictions: []os.Restriction{
+					os.Restriction{
+						Name: "website only",
+						Rules: []os.RestrictionRule{
+							os.RestrictionRule{
+								Name:     "mywebsite.com",
+								Referrer: "www.mywebsite.com",
+							},
+						},
+					},
+				},
+				FlavorID: "asia",
+				Status:   "deployed",
+				Errors:   []os.Error{},
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "mywebsite.com.cdn123.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/asia",
+						Rel:  "flavor",
+					},
+				},
+			},
+			os.Service{
+				ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+				Name: "myothersite.com",
+				Domains: []os.Domain{
+					os.Domain{
+						Domain: "www.myothersite.com",
+					},
+				},
+				Origins: []os.Origin{
+					os.Origin{
+						Origin: "44.33.22.11",
+						Port:   80,
+						SSL:    false,
+					},
+					os.Origin{
+						Origin: "77.66.55.44",
+						Port:   80,
+						SSL:    false,
+						Rules: []os.OriginRule{
+							os.OriginRule{
+								Name:       "videos",
+								RequestURL: "^/videos/*.m3u",
+							},
+						},
+					},
+				},
+				Caching: []os.CacheRule{
+					os.CacheRule{
+						Name: "default",
+						TTL:  3600,
+					},
+				},
+				Restrictions: []os.Restriction{},
+				FlavorID:     "europe",
+				Status:       "deployed",
+				Links: []gophercloud.Link{
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
+						Rel:  "self",
+					},
+					gophercloud.Link{
+						Href: "myothersite.com.poppycdn.net",
+						Rel:  "access_url",
+					},
+					gophercloud.Link{
+						Href: "https://www.poppycdn.io/v1.0/flavors/europe",
+						Rel:  "flavor",
+					},
+				},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleCreateCDNServiceSuccessfully(t)
+
+	createOpts := os.CreateOpts{
+		Name: "mywebsite.com",
+		Domains: []os.Domain{
+			os.Domain{
+				Domain: "www.mywebsite.com",
+			},
+			os.Domain{
+				Domain: "blog.mywebsite.com",
+			},
+		},
+		Origins: []os.Origin{
+			os.Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Restrictions: []os.Restriction{
+			os.Restriction{
+				Name: "website only",
+				Rules: []os.RestrictionRule{
+					os.RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		Caching: []os.CacheRule{
+			os.CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+		},
+		FlavorID: "cdn",
+	}
+
+	expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleGetCDNServiceSuccessfully(t)
+
+	expected := &os.Service{
+		ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+		Name: "mywebsite.com",
+		Domains: []os.Domain{
+			os.Domain{
+				Domain:   "www.mywebsite.com",
+				Protocol: "http",
+			},
+		},
+		Origins: []os.Origin{
+			os.Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Caching: []os.CacheRule{
+			os.CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+			os.CacheRule{
+				Name: "home",
+				TTL:  17200,
+				Rules: []os.TTLRule{
+					os.TTLRule{
+						Name:       "index",
+						RequestURL: "/index.htm",
+					},
+				},
+			},
+			os.CacheRule{
+				Name: "images",
+				TTL:  12800,
+				Rules: []os.TTLRule{
+					os.TTLRule{
+						Name:       "images",
+						RequestURL: "*.png",
+					},
+				},
+			},
+		},
+		Restrictions: []os.Restriction{
+			os.Restriction{
+				Name: "website only",
+				Rules: []os.RestrictionRule{
+					os.RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		FlavorID: "cdn",
+		Status:   "deployed",
+		Errors:   []os.Error{},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+				Rel:  "access_url",
+			},
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+				Rel:  "flavor",
+			},
+		},
+	}
+
+	actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestSuccessfulUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleUpdateCDNServiceSuccessfully(t)
+
+	expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	ops := []os.Patch{
+		// Append a single Domain
+		os.Append{Value: os.Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		os.Insertion{
+			Index: 4,
+			Value: os.Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		os.Append{
+			Value: os.DomainList{
+				os.Domain{Domain: "bulkadded1.mocksite4.com"},
+				os.Domain{Domain: "bulkadded2.mocksite4.com"},
+			},
+		},
+		// Replace a single Origin
+		os.Replacement{
+			Index: 2,
+			Value: os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		os.Replacement{
+			Index: 0, // Ignored
+			Value: os.OriginList{
+				os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+				os.Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
+			},
+		},
+		// Remove a single CacheRule
+		os.Removal{
+			Index: 8,
+			Path:  os.PathCaching,
+		},
+		// Bulk removal
+		os.Removal{
+			All:  true,
+			Path: os.PathCaching,
+		},
+		// Service name replacement
+		os.NameReplacement{
+			NewName: "differentServiceName",
+		},
+	}
+
+	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.HandleDeleteCDNServiceSuccessfully(t)
+
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/cdn/v1/services/doc.go b/rackspace/cdn/v1/services/doc.go
new file mode 100644
index 0000000..ee6e2a5
--- /dev/null
+++ b/rackspace/cdn/v1/services/doc.go
@@ -0,0 +1,7 @@
+// Package services provides information and interaction with the services API
+// resource in the Rackspace CDN service. This API resource allows for
+// listing, creating, updating, retrieving, and deleting services.
+//
+// A service represents an application that has its content cached to the edge
+// nodes.
+package services
diff --git a/rackspace/client.go b/rackspace/client.go
index 5f739a8..45199a4 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -154,3 +154,36 @@
 
 	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
+}
+
+// NewNetworkV2 creates a ServiceClient that can be used to access the Rackspace
+// Networking v2 API.
+func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("network")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
+
+// NewCDNV1 creates a ServiceClient that may be used to access the Rackspace v1
+// CDN service.
+func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("rax:cdn")
+	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/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go
index 884b9cb..809183e 100644
--- a/rackspace/compute/v2/servers/requests.go
+++ b/rackspace/compute/v2/servers/requests.go
@@ -43,6 +43,10 @@
 	// ConfigDrive [optional] enables metadata injection through a configuration drive.
 	ConfigDrive bool
 
+	// AdminPass [optional] sets the root user password. If not set, a randomly-generated
+	// password will be created and returned in the response.
+	AdminPass string
+
 	// Rackspace-specific extensions begin here.
 
 	// KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched
@@ -72,6 +76,7 @@
 		Metadata:         opts.Metadata,
 		Personality:      opts.Personality,
 		ConfigDrive:      opts.ConfigDrive,
+		AdminPass:        opts.AdminPass,
 	}
 
 	drive := diskconfig.CreateOptsExt{
diff --git a/rackspace/identity/v2/roles/delegate.go b/rackspace/identity/v2/roles/delegate.go
new file mode 100644
index 0000000..a6c01e4
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate.go
@@ -0,0 +1,53 @@
+package roles
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+)
+
+// List is the operation responsible for listing all available global roles
+// that a user can adopt.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// AddUserRole is the operation responsible for assigning a particular role to
+// a user. This is confined to the scope of the user's tenant - so the tenant
+// ID is a required argument.
+func AddUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("PUT", userRoleURL(client, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+	})
+
+	return result
+}
+
+// DeleteUserRole is the operation responsible for deleting a particular role
+// from a user. This is confined to the scope of the user's tenant - so the
+// tenant ID is a required argument.
+func DeleteUserRole(client *gophercloud.ServiceClient, userID, roleID string) UserRoleResult {
+	var result UserRoleResult
+
+	_, result.Err = perigee.Request("DELETE", userRoleURL(client, userID, roleID), perigee.Options{
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{204},
+	})
+
+	return result
+}
+
+// UserRoleResult represents the result of either an AddUserRole or
+// a DeleteUserRole operation.
+type UserRoleResult struct {
+	gophercloud.ErrResult
+}
+
+func userRoleURL(c *gophercloud.ServiceClient, userID, roleID string) string {
+	return c.ServiceURL(os.UserPath, userID, os.RolePath, os.ExtPath, roleID)
+}
diff --git a/rackspace/identity/v2/roles/delegate_test.go b/rackspace/identity/v2/roles/delegate_test.go
new file mode 100644
index 0000000..fcee97d
--- /dev/null
+++ b/rackspace/identity/v2/roles/delegate_test.go
@@ -0,0 +1,66 @@
+package roles
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/extensions/admin/roles"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListRoleResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractRoles(page)
+		if err != nil {
+			t.Errorf("Failed to extract users: %v", err)
+			return false, err
+		}
+
+		expected := []os.Role{
+			os.Role{
+				ID:          "123",
+				Name:        "compute:admin",
+				Description: "Nova Administrator",
+				ServiceID:   "cke5372ebabeeabb70a0e702a4626977x4406e5",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestAddUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockAddUserRoleResponse(t)
+
+	err := AddUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDeleteUserRole(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockDeleteUserRoleResponse(t)
+
+	err := DeleteUserRole(client.ServiceClient(), "{user_id}", "{role_id}").ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/identity/v2/roles/fixtures.go b/rackspace/identity/v2/roles/fixtures.go
new file mode 100644
index 0000000..5f22d0f
--- /dev/null
+++ b/rackspace/identity/v2/roles/fixtures.go
@@ -0,0 +1,49 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/OS-KSADM/roles", 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, `
+{
+    "roles": [
+        {
+            "id": "123",
+            "name": "compute:admin",
+            "description": "Nova Administrator",
+            "serviceId": "cke5372ebabeeabb70a0e702a4626977x4406e5"
+        }
+    ]
+}
+  `)
+	})
+}
+
+func MockAddUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusCreated)
+	})
+}
+
+func MockDeleteUserRoleResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users/{user_id}/roles/OS-KSADM/{role_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.StatusNoContent)
+	})
+}
diff --git a/rackspace/identity/v2/users/delegate.go b/rackspace/identity/v2/users/delegate.go
new file mode 100644
index 0000000..ae2acde
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate.go
@@ -0,0 +1,145 @@
+package users
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a pager that allows traversal over a collection of users.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// CommonOpts are the options which are shared between CreateOpts and
+// UpdateOpts
+type CommonOpts struct {
+	// Required. The username to assign to the user. When provided, the username
+	// must:
+	// - start with an alphabetical (A-Za-z) character
+	// - have a minimum length of 1 character
+	//
+	// The username may contain upper and lowercase characters, as well as any of
+	// the following special character: . - @ _
+	Username string
+
+	// Required. Email address for the user account.
+	Email string
+
+	// Required. Indicates whether the user can authenticate after the user
+	// account is created. If no value is specified, the default value is true.
+	Enabled os.EnabledState
+
+	// Optional. The password to assign to the user. If provided, the password
+	// must:
+	// - start with an alphabetical (A-Za-z) character
+	// - have a minimum length of 8 characters
+	// - contain at least one uppercase character, one lowercase character, and
+	//   one numeric character.
+	//
+	// The password may contain any of the following special characters: . - @ _
+	Password string
+}
+
+// CreateOpts represents the options needed when creating new users.
+type CreateOpts CommonOpts
+
+// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
+	m := make(map[string]interface{})
+
+	if opts.Username == "" {
+		return m, errors.New("Username is a required field")
+	}
+	if opts.Enabled == nil {
+		return m, errors.New("Enabled is a required field")
+	}
+	if opts.Email == "" {
+		return m, errors.New("Email is a required field")
+	}
+
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = opts.Enabled
+	}
+	if opts.Password != "" {
+		m["OS-KSADM:password"] = opts.Password
+	}
+
+	return map[string]interface{}{"user": m}, nil
+}
+
+// Create is the operation responsible for creating new users.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Get requests details on a single user, either by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	return GetResult{os.Get(client, id)}
+}
+
+// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
+type UpdateOptsBuilder interface {
+	ToUserUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts CommonOpts
+
+// ToUserUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if opts.Username != "" {
+		m["username"] = opts.Username
+	}
+	if opts.Enabled != nil {
+		m["enabled"] = &opts.Enabled
+	}
+	if opts.Email != "" {
+		m["email"] = opts.Email
+	}
+
+	return map[string]interface{}{"user": m}
+}
+
+// Update is the operation responsible for updating exist users by their UUID.
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	_, result.Err = perigee.Request("POST", os.ResourceURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     opts.ToUserUpdateMap(),
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
+
+// Delete is the operation responsible for permanently deleting an API user.
+func Delete(client *gophercloud.ServiceClient, id string) os.DeleteResult {
+	return os.Delete(client, id)
+}
+
+// ResetAPIKey resets the User's API key.
+func ResetAPIKey(client *gophercloud.ServiceClient, id string) ResetAPIKeyResult {
+	var result ResetAPIKeyResult
+
+	_, result.Err = perigee.Request("POST", resetAPIKeyURL(client, id), perigee.Options{
+		Results:     &result.Body,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
diff --git a/rackspace/identity/v2/users/delegate_test.go b/rackspace/identity/v2/users/delegate_test.go
new file mode 100644
index 0000000..62faf0c
--- /dev/null
+++ b/rackspace/identity/v2/users/delegate_test.go
@@ -0,0 +1,111 @@
+package users
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockListResponse(t)
+
+	count := 0
+
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		users, err := os.ExtractUsers(page)
+
+		th.AssertNoErr(t, err)
+		th.AssertEquals(t, "u1000", users[0].ID)
+		th.AssertEquals(t, "u1001", users[1].ID)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 1, count)
+}
+
+func TestCreateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateUser(t)
+
+	opts := CreateOpts{
+		Username: "new_user",
+		Enabled:  os.Disabled,
+		Email:    "new_user@foo.com",
+		Password: "foo",
+	}
+
+	user, err := Create(client.ServiceClient(), opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "123456", user.ID)
+	th.AssertEquals(t, "5830280", user.DomainID)
+	th.AssertEquals(t, "DFW", user.DefaultRegion)
+}
+
+func TestGetUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockGetUser(t)
+
+	user, err := Get(client.ServiceClient(), "new_user").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, true, user.Enabled)
+	th.AssertEquals(t, true, user.MultiFactorEnabled)
+}
+
+func TestUpdateUser(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockUpdateUser(t)
+
+	id := "c39e3de9be2d4c779f1dfd6abacc176d"
+
+	opts := UpdateOpts{
+		Enabled: os.Enabled,
+		Email:   "new_email@foo.com",
+	}
+
+	user, err := Update(client.ServiceClient(), id, opts).Extract()
+
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, true, user.Enabled)
+	th.AssertEquals(t, "new_email@foo.com", user.Email)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockDeleteUser(t)
+
+	res := Delete(client.ServiceClient(), "c39e3de9be2d4c779f1dfd6abacc176d")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestResetAPIKey(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockResetAPIKey(t)
+
+	apiKey, err := ResetAPIKey(client.ServiceClient(), "99").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, "joesmith", apiKey.Username)
+	th.AssertEquals(t, "mooH1eiLahd5ahYood7r", apiKey.APIKey)
+}
diff --git a/rackspace/identity/v2/users/fixtures.go b/rackspace/identity/v2/users/fixtures.go
new file mode 100644
index 0000000..973f39e
--- /dev/null
+++ b/rackspace/identity/v2/users/fixtures.go
@@ -0,0 +1,154 @@
+package users
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func mockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+		"users":[
+				{
+						"id": "u1000",
+						"username": "jqsmith",
+						"email": "john.smith@example.org",
+						"enabled": true
+				},
+				{
+						"id": "u1001",
+						"username": "jqsmith",
+						"email": "jane.smith@example.org",
+						"enabled": true
+				}
+		]
+}
+	`)
+	})
+}
+
+func mockCreateUser(t *testing.T) {
+	th.Mux.HandleFunc("/users", 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, `
+{
+    "user": {
+        "username": "new_user",
+        "enabled": false,
+        "email": "new_user@foo.com",
+				"OS-KSADM:password": "foo"
+    }
+}
+  `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+  "user": {
+    "RAX-AUTH:defaultRegion": "DFW",
+    "RAX-AUTH:domainId": "5830280",
+    "id": "123456",
+    "username": "new_user",
+    "email": "new_user@foo.com",
+    "enabled": false
+  }
+}
+`)
+	})
+}
+
+func mockGetUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/new_user", 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, `
+{
+	"user": {
+		"RAX-AUTH:defaultRegion": "DFW",
+		"RAX-AUTH:domainId": "5830280",
+		"RAX-AUTH:multiFactorEnabled": "true",
+		"id": "c39e3de9be2d4c779f1dfd6abacc176d",
+		"username": "jqsmith",
+		"email": "john.smith@example.org",
+		"enabled": true
+	}
+}
+`)
+	})
+}
+
+func mockUpdateUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", 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, `
+{
+	"user": {
+		"email": "new_email@foo.com",
+		"enabled": true
+	}
+}
+`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"user": {
+		"RAX-AUTH:defaultRegion": "DFW",
+		"RAX-AUTH:domainId": "5830280",
+		"RAX-AUTH:multiFactorEnabled": "true",
+		"id": "123456",
+		"username": "jqsmith",
+		"email": "new_email@foo.com",
+		"enabled": true
+	}
+}
+`)
+	})
+}
+
+func mockDeleteUser(t *testing.T) {
+	th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func mockResetAPIKey(t *testing.T) {
+	th.Mux.HandleFunc("/users/99/OS-KSADM/credentials/RAX-KSKEY:apiKeyCredentials/RAX-AUTH/reset", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "RAX-KSKEY:apiKeyCredentials": {
+        "username": "joesmith",
+        "apiKey": "mooH1eiLahd5ahYood7r"
+    }
+}`)
+	})
+}
diff --git a/rackspace/identity/v2/users/results.go b/rackspace/identity/v2/users/results.go
new file mode 100644
index 0000000..6936ecb
--- /dev/null
+++ b/rackspace/identity/v2/users/results.go
@@ -0,0 +1,129 @@
+package users
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/identity/v2/users"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// User represents a user resource that exists on the API.
+type User struct {
+	// The UUID for this user.
+	ID string
+
+	// The human name for this user.
+	Name string
+
+	// The username for this user.
+	Username string
+
+	// Indicates whether the user is enabled (true) or disabled (false).
+	Enabled bool
+
+	// The email address for this user.
+	Email string
+
+	// The ID of the tenant to which this user belongs.
+	TenantID string `mapstructure:"tenant_id"`
+
+	// Specifies the default region for the user account. This value is inherited
+	// from the user administrator when the account is created.
+	DefaultRegion string `mapstructure:"RAX-AUTH:defaultRegion"`
+
+	// Identifies the domain that contains the user account. This value is
+	// inherited from the user administrator when the account is created.
+	DomainID string `mapstructure:"RAX-AUTH:domainId"`
+
+	// The password value that the user needs for authentication. If the Add user
+	// request included a password value, this attribute is not included in the
+	// response.
+	Password string `mapstructure:"OS-KSADM:password"`
+
+	// Indicates whether the user has enabled multi-factor authentication.
+	MultiFactorEnabled bool `mapstructure:"RAX-AUTH:multiFactorEnabled"`
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+	os.CreateResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+	os.GetResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+	os.UpdateResult
+}
+
+func commonExtract(resp interface{}, err error) (*User, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		User *User `json:"user"`
+	}
+
+	// Since the API returns a string instead of a bool, we need to hack the JSON
+	json := resp.(map[string]interface{})
+	user := json["user"].(map[string]interface{})
+	if s, ok := user["RAX-AUTH:multiFactorEnabled"].(string); ok && s != "" {
+		if b, err := strconv.ParseBool(s); err == nil {
+			user["RAX-AUTH:multiFactorEnabled"] = b
+		}
+	}
+
+	err = mapstructure.Decode(json, &respStruct)
+
+	return respStruct.User, err
+}
+
+// Extract will get the Snapshot object out of the GetResult object.
+func (r GetResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the CreateResult object.
+func (r CreateResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*User, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// ResetAPIKeyResult represents the server response to the ResetAPIKey method.
+type ResetAPIKeyResult struct {
+	gophercloud.Result
+}
+
+// ResetAPIKeyValue represents an API Key that has been reset.
+type ResetAPIKeyValue struct {
+	// The Username for this API Key reset.
+	Username string `mapstructure:"username"`
+
+	// The new API Key for this user.
+	APIKey string `mapstructure:"apiKey"`
+}
+
+// Extract will get the Error or ResetAPIKeyValue object out of the ResetAPIKeyResult object.
+func (r ResetAPIKeyResult) Extract() (*ResetAPIKeyValue, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		ResetAPIKeyValue ResetAPIKeyValue `mapstructure:"RAX-KSKEY:apiKeyCredentials"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.ResetAPIKeyValue, err
+}
diff --git a/rackspace/identity/v2/users/urls.go b/rackspace/identity/v2/users/urls.go
new file mode 100644
index 0000000..bc1aaef
--- /dev/null
+++ b/rackspace/identity/v2/users/urls.go
@@ -0,0 +1,7 @@
+package users
+
+import "github.com/rackspace/gophercloud"
+
+func resetAPIKeyURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("users", id, "OS-KSADM", "credentials", "RAX-KSKEY:apiKeyCredentials", "RAX-AUTH", "reset")
+}
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..98f3962
--- /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 an 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 at 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..7c85945
--- /dev/null
+++ b/rackspace/lb/v1/nodes/fixtures.go
@@ -0,0 +1,207 @@
+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": {
+    "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..77894aa
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -0,0 +1,279 @@
+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 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.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..003d347
--- /dev/null
+++ b/rackspace/lb/v1/nodes/requests_test.go
@@ -0,0 +1,211 @@
+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{
+		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)
+}
diff --git a/rackspace/networking/v2/common/common_tests.go b/rackspace/networking/v2/common/common_tests.go
new file mode 100644
index 0000000..129cd63
--- /dev/null
+++ b/rackspace/networking/v2/common/common_tests.go
@@ -0,0 +1,12 @@
+package common
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return client.ServiceClient()
+}
diff --git a/rackspace/networking/v2/networks/delegate.go b/rackspace/networking/v2/networks/delegate.go
new file mode 100644
index 0000000..dcb0855
--- /dev/null
+++ b/rackspace/networking/v2/networks/delegate.go
@@ -0,0 +1,41 @@
+package networks
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// networks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retrieves a specific network based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+	return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. This operation does not actually require a request body, i.e. the
+// CreateOpts struct argument can be empty.
+//
+// The tenant ID that is contained in the URI is the tenant that creates the
+// network. An admin user, however, has the option of specifying another tenant
+// ID in the CreateOpts struct.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing network using the
+// values provided. For more information, see the Create function.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the network associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+	return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/networks/delegate_test.go b/rackspace/networking/v2/networks/delegate_test.go
new file mode 100644
index 0000000..f51c732
--- /dev/null
+++ b/rackspace/networking/v2/networks/delegate_test.go
@@ -0,0 +1,276 @@
+package networks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+	"github.com/rackspace/gophercloud/pagination"
+	fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks", 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, `
+{
+    "networks": [
+        {
+            "status": "ACTIVE",
+            "subnets": [
+                "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+            ],
+            "name": "private-network",
+            "admin_state_up": true,
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "shared": true,
+            "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+        },
+        {
+            "status": "ACTIVE",
+            "subnets": [
+                "08eae331-0402-425a-923c-34f7cfe39c1b"
+            ],
+            "name": "private",
+            "admin_state_up": true,
+            "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+            "shared": true,
+            "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324"
+        }
+    ]
+}
+      `)
+	})
+
+	client := fake.ServiceClient()
+	count := 0
+
+	List(client, os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractNetworks(page)
+		if err != nil {
+			t.Errorf("Failed to extract networks: %v", err)
+			return false, err
+		}
+
+		expected := []os.Network{
+			os.Network{
+				Status:       "ACTIVE",
+				Subnets:      []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+				Name:         "private-network",
+				AdminStateUp: true,
+				TenantID:     "4fd44f30292945e481c7b8a0c8908869",
+				Shared:       true,
+				ID:           "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+			},
+			os.Network{
+				Status:       "ACTIVE",
+				Subnets:      []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+				Name:         "private",
+				AdminStateUp: true,
+				TenantID:     "26a7980765d0414dbc1fc1f88cdb7e6e",
+				Shared:       true,
+				ID:           "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", 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, `
+{
+		"network": {
+				"status": "ACTIVE",
+				"subnets": [
+						"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+				],
+				"name": "private-network",
+				"admin_state_up": true,
+				"tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+				"shared": true,
+				"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+		}
+}
+			`)
+	})
+
+	n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.Subnets, []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"})
+	th.AssertEquals(t, n.Name, "private-network")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertEquals(t, n.Shared, true)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"network": {
+				"name": "sample_network",
+				"admin_state_up": true
+		}
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+		"network": {
+				"status": "ACTIVE",
+				"subnets": [],
+				"name": "net1",
+				"admin_state_up": true,
+				"tenant_id": "9bacb3c5d39d41a79512987f338cf177",
+				"shared": false,
+				"id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+		}
+}
+		`)
+	})
+
+	iTrue := true
+	options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertDeepEquals(t, n.Subnets, []string{})
+	th.AssertEquals(t, n.Name, "net1")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.TenantID, "9bacb3c5d39d41a79512987f338cf177")
+	th.AssertEquals(t, n.Shared, false)
+	th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestCreateWithOptionalFields(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+	"network": {
+			"name": "sample_network",
+			"admin_state_up": true,
+			"shared": true,
+			"tenant_id": "12345"
+	}
+}
+		`)
+
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	iTrue := true
+	options := os.CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"}
+	_, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+		"network": {
+				"name": "new_network_name",
+				"admin_state_up": false,
+				"shared": true
+		}
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+		"network": {
+				"status": "ACTIVE",
+				"subnets": [],
+				"name": "new_network_name",
+				"admin_state_up": false,
+				"tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+				"shared": true,
+				"id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+		}
+}
+		`)
+	})
+
+	iTrue := true
+	options := os.UpdateOpts{Name: "new_network_name", AdminStateUp: os.Down, Shared: &iTrue}
+	n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "new_network_name")
+	th.AssertEquals(t, n.AdminStateUp, false)
+	th.AssertEquals(t, n.Shared, true)
+	th.AssertEquals(t, n.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/networking/v2/ports/delegate.go b/rackspace/networking/v2/ports/delegate.go
new file mode 100644
index 0000000..091b99e
--- /dev/null
+++ b/rackspace/networking/v2/ports/delegate.go
@@ -0,0 +1,40 @@
+package ports
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// ports. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those ports that are owned by the tenant
+// who submits the request, unless the request is submitted by a user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retrieves a specific port based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+	return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new network using the values
+// provided. You must remember to provide a NetworkID value.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing port using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the port associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+	return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/ports/delegate_test.go b/rackspace/networking/v2/ports/delegate_test.go
new file mode 100644
index 0000000..f53ff59
--- /dev/null
+++ b/rackspace/networking/v2/ports/delegate_test.go
@@ -0,0 +1,322 @@
+package ports
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+	"github.com/rackspace/gophercloud/pagination"
+	fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports", 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, `
+{
+    "ports": [
+        {
+            "status": "ACTIVE",
+            "binding:host_id": "devstack",
+            "name": "",
+            "admin_state_up": true,
+            "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+            "tenant_id": "",
+            "device_owner": "network:router_gateway",
+            "mac_address": "fa:16:3e:58:42:ed",
+            "fixed_ips": [
+                {
+                    "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062",
+                    "ip_address": "172.24.4.2"
+                }
+            ],
+            "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+            "security_groups": [],
+            "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractPorts(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []os.Port{
+			os.Port{
+				Status:       "ACTIVE",
+				Name:         "",
+				AdminStateUp: true,
+				NetworkID:    "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+				TenantID:     "",
+				DeviceOwner:  "network:router_gateway",
+				MACAddress:   "fa:16:3e:58:42:ed",
+				FixedIPs: []os.IP{
+					os.IP{
+						SubnetID:  "008ba151-0b8c-4a67-98b5-0d2b87666062",
+						IPAddress: "172.24.4.2",
+					},
+				},
+				ID:             "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+				SecurityGroups: []string{},
+				DeviceID:       "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", 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, `
+{
+    "port": {
+        "status": "ACTIVE",
+        "name": "",
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "7e02058126cc4950b75f9970368ba177",
+        "device_owner": "network:router_interface",
+        "mac_address": "fa:16:3e:23:fd:d7",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.1"
+            }
+        ],
+        "id": "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2",
+        "security_groups": [],
+        "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e"
+    }
+}
+      `)
+	})
+
+	n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertEquals(t, n.Name, "")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+	th.AssertEquals(t, n.TenantID, "7e02058126cc4950b75f9970368ba177")
+	th.AssertEquals(t, n.DeviceOwner, "network:router_interface")
+	th.AssertEquals(t, n.MACAddress, "fa:16:3e:23:fd:d7")
+	th.AssertDeepEquals(t, n.FixedIPs, []os.IP{
+		os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.1"},
+	})
+	th.AssertEquals(t, n.ID, "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2")
+	th.AssertDeepEquals(t, n.SecurityGroups, []string{})
+	th.AssertEquals(t, n.Status, "ACTIVE")
+	th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "port": {
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "name": "private-port",
+        "admin_state_up": true,
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.2"
+            }
+        ],
+        "security_groups": ["foo"]
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "private-port",
+        "allowed_address_pairs": [],
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+        "device_owner": "",
+        "mac_address": "fa:16:3e:c9:cb:f0",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.2"
+            }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+    `)
+	})
+
+	asu := true
+	options := os.CreateOpts{
+		Name:         "private-port",
+		AdminStateUp: &asu,
+		NetworkID:    "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+		FixedIPs: []os.IP{
+			os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+		},
+		SecurityGroups: []string{"foo"},
+	}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Status, "DOWN")
+	th.AssertEquals(t, n.Name, "private-port")
+	th.AssertEquals(t, n.AdminStateUp, true)
+	th.AssertEquals(t, n.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+	th.AssertEquals(t, n.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+	th.AssertEquals(t, n.DeviceOwner, "")
+	th.AssertEquals(t, n.MACAddress, "fa:16:3e:c9:cb:f0")
+	th.AssertDeepEquals(t, n.FixedIPs, []os.IP{
+		os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+	})
+	th.AssertEquals(t, n.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+	th.AssertDeepEquals(t, n.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), os.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "port": {
+        "name": "new_port_name",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.3"
+            }
+        ],
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ]
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "port": {
+        "status": "DOWN",
+        "name": "new_port_name",
+        "admin_state_up": true,
+        "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+        "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+        "device_owner": "",
+        "mac_address": "fa:16:3e:c9:cb:f0",
+        "fixed_ips": [
+            {
+                "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+                "ip_address": "10.0.0.3"
+            }
+        ],
+        "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+        "security_groups": [
+            "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+        ],
+        "device_id": ""
+    }
+}
+    `)
+	})
+
+	options := os.UpdateOpts{
+		Name: "new_port_name",
+		FixedIPs: []os.IP{
+			os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+		},
+		SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
+	}
+
+	s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "new_port_name")
+	th.AssertDeepEquals(t, s.FixedIPs, []os.IP{
+		os.IP{SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+	})
+	th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/networking/v2/subnets/delegate.go b/rackspace/networking/v2/subnets/delegate.go
new file mode 100644
index 0000000..a7fb7bb
--- /dev/null
+++ b/rackspace/networking/v2/subnets/delegate.go
@@ -0,0 +1,40 @@
+package subnets
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager which allows you to iterate over a collection of
+// subnets. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those subnets that are owned by the tenant
+// who submits the request, unless the request is submitted by a user with
+// administrative rights.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(c, opts)
+}
+
+// Get retrieves a specific subnet based on its unique ID.
+func Get(c *gophercloud.ServiceClient, networkID string) os.GetResult {
+	return os.Get(c, networkID)
+}
+
+// Create accepts a CreateOpts struct and creates a new subnet using the values
+// provided. You must remember to provide a valid NetworkID, CIDR and IP version.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(c, opts)
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnet using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, networkID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+	return os.Update(c, networkID, opts)
+}
+
+// Delete accepts a unique ID and deletes the subnet associated with it.
+func Delete(c *gophercloud.ServiceClient, networkID string) os.DeleteResult {
+	return os.Delete(c, networkID)
+}
diff --git a/rackspace/networking/v2/subnets/delegate_test.go b/rackspace/networking/v2/subnets/delegate_test.go
new file mode 100644
index 0000000..fafc6fb
--- /dev/null
+++ b/rackspace/networking/v2/subnets/delegate_test.go
@@ -0,0 +1,363 @@
+package subnets
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+	"github.com/rackspace/gophercloud/pagination"
+	fake "github.com/rackspace/gophercloud/rackspace/networking/v2/common"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets", 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, `
+{
+    "subnets": [
+        {
+            "name": "private-subnet",
+            "enable_dhcp": true,
+            "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+            "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+            "dns_nameservers": [],
+            "allocation_pools": [
+                {
+                    "start": "10.0.0.2",
+                    "end": "10.0.0.254"
+                }
+            ],
+            "host_routes": [],
+            "ip_version": 4,
+            "gateway_ip": "10.0.0.1",
+            "cidr": "10.0.0.0/24",
+            "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+        },
+        {
+            "name": "my_subnet",
+            "enable_dhcp": true,
+            "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+            "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+            "dns_nameservers": [],
+            "allocation_pools": [
+                {
+                    "start": "192.0.0.2",
+                    "end": "192.255.255.254"
+                }
+            ],
+            "host_routes": [],
+            "ip_version": 4,
+            "gateway_ip": "192.0.0.1",
+            "cidr": "192.0.0.0/8",
+            "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), os.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := os.ExtractSubnets(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []os.Subnet{
+			os.Subnet{
+				Name:           "private-subnet",
+				EnableDHCP:     true,
+				NetworkID:      "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+				TenantID:       "26a7980765d0414dbc1fc1f88cdb7e6e",
+				DNSNameservers: []string{},
+				AllocationPools: []os.AllocationPool{
+					os.AllocationPool{
+						Start: "10.0.0.2",
+						End:   "10.0.0.254",
+					},
+				},
+				HostRoutes: []os.HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "10.0.0.1",
+				CIDR:       "10.0.0.0/24",
+				ID:         "08eae331-0402-425a-923c-34f7cfe39c1b",
+			},
+			os.Subnet{
+				Name:           "my_subnet",
+				EnableDHCP:     true,
+				NetworkID:      "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+				TenantID:       "4fd44f30292945e481c7b8a0c8908869",
+				DNSNameservers: []string{},
+				AllocationPools: []os.AllocationPool{
+					os.AllocationPool{
+						Start: "192.0.0.2",
+						End:   "192.255.255.254",
+					},
+				},
+				HostRoutes: []os.HostRoute{},
+				IPVersion:  4,
+				GatewayIP:  "192.0.0.1",
+				CIDR:       "192.0.0.0/8",
+				ID:         "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", 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, `
+{
+    "subnet": {
+        "name": "my_subnet",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "dns_nameservers": [],
+        "allocation_pools": [
+            {
+                "start": "192.0.0.2",
+                "end": "192.255.255.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "192.0.0.1",
+        "cidr": "192.0.0.0/8",
+        "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+    }
+}
+      `)
+	})
+
+	s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "my_subnet")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+	th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{
+		os.AllocationPool{
+			Start: "192.0.0.2",
+			End:   "192.255.255.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "192.0.0.1")
+	th.AssertEquals(t, s.CIDR, "192.0.0.0/8")
+	th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet": {
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "ip_version": 4,
+        "cidr": "192.168.199.0/24",
+        "dns_nameservers": ["foo"],
+        "allocation_pools": [
+            {
+                "start": "192.168.199.2",
+                "end": "192.168.199.254"
+            }
+        ],
+        "host_routes": [{"destination":"","nexthop": "bar"}]
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "",
+        "enable_dhcp": true,
+        "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+        "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+        "dns_nameservers": [],
+        "allocation_pools": [
+            {
+                "start": "192.168.199.2",
+                "end": "192.168.199.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "192.168.199.1",
+        "cidr": "192.168.199.0/24",
+        "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126"
+    }
+}
+    `)
+	})
+
+	opts := os.CreateOpts{
+		NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+		IPVersion: 4,
+		CIDR:      "192.168.199.0/24",
+		AllocationPools: []os.AllocationPool{
+			os.AllocationPool{
+				Start: "192.168.199.2",
+				End:   "192.168.199.254",
+			},
+		},
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []os.HostRoute{
+			os.HostRoute{NextHop: "bar"},
+		},
+	}
+	s, err := Create(fake.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "")
+	th.AssertEquals(t, s.EnableDHCP, true)
+	th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+	th.AssertDeepEquals(t, s.DNSNameservers, []string{})
+	th.AssertDeepEquals(t, s.AllocationPools, []os.AllocationPool{
+		os.AllocationPool{
+			Start: "192.168.199.2",
+			End:   "192.168.199.254",
+		},
+	})
+	th.AssertDeepEquals(t, s.HostRoutes, []os.HostRoute{})
+	th.AssertEquals(t, s.IPVersion, 4)
+	th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
+	th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+	th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+	res := Create(fake.ServiceClient(), os.CreateOpts{})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo"})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	res = Create(fake.ServiceClient(), os.CreateOpts{NetworkID: "foo", CIDR: "bar", IPVersion: 40})
+	if res.Err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "subnet": {
+        "name": "my_new_subnet",
+        "dns_nameservers": ["foo"],
+        "host_routes": [{"destination":"","nexthop": "bar"}]
+    }
+}
+    `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "subnet": {
+        "name": "my_new_subnet",
+        "enable_dhcp": true,
+        "network_id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+        "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+        "dns_nameservers": [],
+        "allocation_pools": [
+            {
+                "start": "10.0.0.2",
+                "end": "10.0.0.254"
+            }
+        ],
+        "host_routes": [],
+        "ip_version": 4,
+        "gateway_ip": "10.0.0.1",
+        "cidr": "10.0.0.0/24",
+        "id": "08eae331-0402-425a-923c-34f7cfe39c1b"
+    }
+}
+  `)
+	})
+
+	opts := os.UpdateOpts{
+		Name:           "my_new_subnet",
+		DNSNameservers: []string{"foo"},
+		HostRoutes: []os.HostRoute{
+			os.HostRoute{NextHop: "bar"},
+		},
+	}
+	s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, s.Name, "my_new_subnet")
+	th.AssertEquals(t, s.ID, "08eae331-0402-425a-923c-34f7cfe39c1b")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/rackspace/objectstorage/v1/objects/delegate_test.go b/rackspace/objectstorage/v1/objects/delegate_test.go
index 08831ec..8ab8029 100644
--- a/rackspace/objectstorage/v1/objects/delegate_test.go
+++ b/rackspace/objectstorage/v1/objects/delegate_test.go
@@ -66,14 +66,24 @@
 func TestCreateObject(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
-	os.HandleCreateObjectSuccessfully(t)
+	os.HandleCreateTextObjectSuccessfully(t)
 
 	content := bytes.NewBufferString("Did gyre and gimble in the wabe")
-	options := &os.CreateOpts{ContentType: "application/json"}
+	options := &os.CreateOpts{ContentType: "text/plain"}
 	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
 	th.AssertNoErr(t, res.Err)
 }
 
+func TestCreateObjectWithoutContentType(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleCreateTypelessObjectSuccessfully(t)
+
+	content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
+	res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &os.CreateOpts{})
+	th.AssertNoErr(t, res.Err)
+}
+
 func TestCopyObject(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/results.go b/results.go
index f480bc7..3fd5029 100644
--- a/results.go
+++ b/results.go
@@ -5,21 +5,37 @@
 	"net/http"
 )
 
-// Result acts as a base struct that other results can embed.
+/*
+Result is an internal type to be used by individual resource packages, but its
+methods will be available on a wide variety of user-facing embedding types.
+
+It acts as a base struct that other Result types, returned from request
+functions, can embed for convenience. All Results capture basic information
+from the HTTP transaction that was performed, including the response body,
+HTTP headers, and any errors that happened.
+
+Generally, each Result type will have an Extract method that can be used to
+further interpret the result's payload in a specific context. Extensions or
+providers can then provide additional extraction functions to pull out
+provider- or extension-specific information as well.
+*/
 type Result struct {
-	// Body is the payload of the HTTP response from the server. In most cases, this will be the
-	// deserialized JSON structure.
+	// Body is the payload of the HTTP response from the server. In most cases,
+	// this will be the deserialized JSON structure.
 	Body interface{}
 
 	// Header contains the HTTP header structure from the original response.
 	Header http.Header
 
-	// Err is an error that occurred during the operation. It's deferred until extraction to make
-	// it easier to chain operations.
+	// Err is an error that occurred during the operation. It's deferred until
+	// extraction to make it easier to chain the Extract call.
 	Err error
 }
 
-// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON.
+// PrettyPrintJSON creates a string containing the full response body as
+// pretty-printed JSON. It's useful for capturing test fixtures and for
+// debugging extraction bugs. If you include its output in an issue related to
+// a buggy extraction function, we will all love you forever.
 func (r Result) PrettyPrintJSON() string {
 	pretty, err := json.MarshalIndent(r.Body, "", "  ")
 	if err != nil {
@@ -28,44 +44,67 @@
 	return string(pretty)
 }
 
-// ErrResult represents results that only contain a potential error and
-// nothing else. Usually if the operation executed successfully, the Err field
-// will be nil; otherwise it will be stocked with a relevant error.
+// ErrResult is an internal type to be used by individual resource packages, but
+// its methods will be available on a wide variety of user-facing embedding
+// types.
+//
+// It represents results that only contain a potential error and
+// nothing else. Usually, if the operation executed successfully, the Err field
+// will be nil; otherwise it will be stocked with a relevant error. Use the
+// ExtractErr method
+// to cleanly pull it out.
 type ErrResult struct {
 	Result
 }
 
-// ExtractErr is a function that extracts error information from a result.
+// ExtractErr is a function that extracts error information, or nil, from a result.
 func (r ErrResult) ExtractErr() error {
 	return r.Err
 }
 
-// HeaderResult represents a result that only contains an `error` (possibly nil)
-// and an http.Header. This is used, for example, by the `objectstorage` packages
-// in `openstack`, because most of the operations don't return response bodies.
+/*
+HeaderResult is an internal type to be used by individual resource packages, but
+its methods will be available on a wide variety of user-facing embedding types.
+
+It represents a result that only contains an error (possibly nil) and an
+http.Header. This is used, for example, by the objectstorage packages in
+openstack, because most of the operations don't return response bodies, but do
+have relevant information in headers.
+*/
 type HeaderResult struct {
 	Result
 }
 
 // ExtractHeader will return the http.Header and error from the HeaderResult.
-// Usage: header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
+//
+//   header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
 func (hr HeaderResult) ExtractHeader() (http.Header, error) {
 	return hr.Header, hr.Err
 }
 
-// RFC3339Milli describes a time format used by API responses.
+// RFC3339Milli describes a common time format used by some API responses.
 const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
 
-// Link represents a structure that enables paginated collections how to
-// traverse backward or forward. The "Rel" field is usually either "next".
+/*
+Link is an internal type to be used in packages of collection resources that are
+paginated in a certain way.
+
+It's a response substructure common to many paginated collection results that is
+used to point to related pages. Usually, the one we care about is the one with
+Rel field set to "next".
+*/
 type Link struct {
 	Href string `mapstructure:"href"`
 	Rel  string `mapstructure:"rel"`
 }
 
-// ExtractNextURL attempts to extract the next URL from a JSON structure. It
-// follows the common convention of nesting back and next URLs in a "links"
-// JSON array.
+/*
+ExtractNextURL is an internal function useful for packages of collection
+resources that are paginated in a certain way.
+
+It attempts attempts to extract the "next" URL from slice of Link structs, or
+"" if no such URL is present.
+*/
 func ExtractNextURL(links []Link) (string, error) {
 	var url string
 
diff --git a/util.go b/util.go
index 101fd39..fbd9fe9 100644
--- a/util.go
+++ b/util.go
@@ -7,7 +7,9 @@
 )
 
 // WaitFor polls a predicate function, once per second, up to a timeout limit.
-// It usually does this to wait for the resource to transition to a certain state.
+// It usually does this to wait for a resource to transition to a certain state.
+// Resource packages will wrap this in a more convenient function that's
+// specific to a certain resource, but it can also be useful on its own.
 func WaitFor(timeout int, predicate func() (bool, error)) error {
 	start := time.Now().Second()
 	for {
@@ -30,7 +32,10 @@
 	}
 }
 
-// NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
+// NormalizeURL is an internal function to be used by provider clients.
+//
+// It ensures that each endpoint URL has a closing `/`, as expected by
+// ServiceClient's methods.
 func NormalizeURL(url string) string {
 	if !strings.HasSuffix(url, "/") {
 		return url + "/"