Merge remote-tracking branch 'upstream/v0.2.0' into rackspace-compute

Conflicts:
	acceptance/README.md
	rackspace/client.go
diff --git a/acceptance/README.md b/acceptance/README.md
index df84a46..1e86c9f 100644
--- a/acceptance/README.md
+++ b/acceptance/README.md
@@ -28,12 +28,15 @@
 |`OS_AUTH_URL`|The identity URL you need to authenticate|
 |`OS_TENANT_NAME`|Your API tenant name|
 |`OS_TENANT_ID`|Your API tenant ID|
+|`RS_USERNAME`|Your Rackspace username|
+|`RS_APIKEY`|Your Rackspace API key|
 
 #### General
 
 |Name|Description|
 |---|---|
 |`OS_REGION_NAME`|The region you want your resources to reside in|
+|`RS_REGION`|Rackspace region you want your resource to reside in|
 
 #### Compute
 
@@ -42,6 +45,8 @@
 |`OS_IMAGE_ID`|The ID of the image your want your server to be based on|
 |`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on|
 |`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to|
+|`RS_IMAGE_ID`|The ID of the image you want servers to be created with|
+|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with|
 
 ### 2. Run the test suite
 
diff --git a/acceptance/rackspace/compute/v2/compute_test.go b/acceptance/rackspace/compute/v2/compute_test.go
new file mode 100644
index 0000000..3419c10
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/compute_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v2
+
+import (
+	"errors"
+	"os"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/rackspace"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	// Obtain credentials from the environment.
+	options := gophercloud.AuthOptions{
+		Username: os.Getenv("RS_USERNAME"),
+		APIKey:   os.Getenv("RS_APIKEY"),
+	}
+	region := os.Getenv("RS_REGION")
+
+	if options.Username == "" {
+		return nil, errors.New("Please provide a Rackspace username as RS_USERNAME.")
+	}
+	if options.APIKey == "" {
+		return nil, errors.New("Please provide a Rackspace API key as RS_APIKEY.")
+	}
+	if region == "" {
+		return nil, errors.New("Please provide a Rackspace region as RS_REGION.")
+	}
+
+	client, err := rackspace.AuthenticatedClient(options)
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewComputeV2(client, gophercloud.EndpointOpts{
+		Region: region,
+	})
+}
+
+type serverOpts struct {
+	imageID  string
+	flavorID string
+}
+
+func optionsFromEnv() (*serverOpts, error) {
+	options := &serverOpts{
+		imageID:  os.Getenv("RS_IMAGE_ID"),
+		flavorID: os.Getenv("RS_FLAVOR_ID"),
+	}
+	if options.imageID == "" {
+		return nil, errors.New("Please provide a valid Rackspace image ID as RS_IMAGE_ID")
+	}
+	if options.flavorID == "" {
+		return nil, errors.New("Please provide a valid Rackspace flavor ID as RS_FLAVOR_ID")
+	}
+	return options, nil
+}
diff --git a/acceptance/rackspace/compute/v2/flavors_test.go b/acceptance/rackspace/compute/v2/flavors_test.go
new file mode 100644
index 0000000..248ab91
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/flavors_test.go
@@ -0,0 +1,61 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/flavors"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListFlavors(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	count := 0
+	err = flavors.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("-- Page %0d --", count)
+
+		fs, err := flavors.ExtractFlavors(page)
+		th.AssertNoErr(t, err)
+
+		for i, flavor := range fs {
+			t.Logf("[%02d]      id=[%s]", i, flavor.ID)
+			t.Logf("        name=[%s]", flavor.Name)
+			t.Logf("        disk=[%d]", flavor.Disk)
+			t.Logf("         RAM=[%d]", flavor.RAM)
+			t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor)
+			t.Logf("        swap=[%d]", flavor.Swap)
+			t.Logf("       VCPUs=[%d]", flavor.VCPUs)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count == 0 {
+		t.Errorf("No flavors listed!")
+	}
+}
+
+func TestGetFlavor(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	flavor, err := flavors.Get(client, options.flavorID).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Requested flavor:")
+	t.Logf("          id=[%s]", flavor.ID)
+	t.Logf("        name=[%s]", flavor.Name)
+	t.Logf("        disk=[%d]", flavor.Disk)
+	t.Logf("         RAM=[%d]", flavor.RAM)
+	t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor)
+	t.Logf("        swap=[%d]", flavor.Swap)
+	t.Logf("       VCPUs=[%d]", flavor.VCPUs)
+}
diff --git a/acceptance/rackspace/compute/v2/images_test.go b/acceptance/rackspace/compute/v2/images_test.go
new file mode 100644
index 0000000..5e36c2e
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/images_test.go
@@ -0,0 +1,63 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/images"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListImages(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	count := 0
+	err = images.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("-- Page %02d --", count)
+
+		is, err := images.ExtractImages(page)
+		th.AssertNoErr(t, err)
+
+		for i, image := range is {
+			t.Logf("[%02d]   id=[%s]", i, image.ID)
+			t.Logf("     name=[%s]", image.Name)
+			t.Logf("  created=[%s]", image.Created)
+			t.Logf("  updated=[%s]", image.Updated)
+			t.Logf(" min disk=[%d]", image.MinDisk)
+			t.Logf("  min RAM=[%d]", image.MinRAM)
+			t.Logf(" progress=[%d]", image.Progress)
+			t.Logf("   status=[%s]", image.Status)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	if count < 1 {
+		t.Errorf("Expected at least one page of images.")
+	}
+}
+
+func TestGetImage(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	image, err := images.Get(client, options.imageID).Extract()
+	th.AssertNoErr(t, err)
+
+	t.Logf("Requested image:")
+	t.Logf("       id=[%s]", image.ID)
+	t.Logf("     name=[%s]", image.Name)
+	t.Logf("  created=[%s]", image.Created)
+	t.Logf("  updated=[%s]", image.Updated)
+	t.Logf(" min disk=[%d]", image.MinDisk)
+	t.Logf("  min RAM=[%d]", image.MinRAM)
+	t.Logf(" progress=[%d]", image.Progress)
+	t.Logf("   status=[%s]", image.Status)
+}
diff --git a/acceptance/rackspace/compute/v2/pkg.go b/acceptance/rackspace/compute/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
new file mode 100644
index 0000000..6247c26
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -0,0 +1,158 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient) *os.Server {
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	name := tools.RandomString("Gophercloud-", 8)
+	t.Logf("Creating server [%s].", name)
+	s, err := servers.Create(client, &os.CreateOpts{
+		Name:      name,
+		ImageRef:  options.imageID,
+		FlavorRef: options.flavorID,
+	}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Creating server.")
+
+	err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+	t.Logf("Server created successfully.")
+
+	return s
+}
+
+func logServer(t *testing.T, server *os.Server, index int) {
+	if index == -1 {
+		t.Logf("             id=[%s]", server.ID)
+	} else {
+		t.Logf("[%02d]             id=[%s]", index, server.ID)
+	}
+	t.Logf("           name=[%s]", server.Name)
+	t.Logf("      tenant ID=[%s]", server.TenantID)
+	t.Logf("        user ID=[%s]", server.UserID)
+	t.Logf("        updated=[%s]", server.Updated)
+	t.Logf("        created=[%s]", server.Created)
+	t.Logf("        host ID=[%s]", server.HostID)
+	t.Logf("    access IPv4=[%s]", server.AccessIPv4)
+	t.Logf("    access IPv6=[%s]", server.AccessIPv6)
+	t.Logf("          image=[%v]", server.Image)
+	t.Logf("         flavor=[%v]", server.Flavor)
+	t.Logf("      addresses=[%v]", server.Addresses)
+	t.Logf("       metadata=[%v]", server.Metadata)
+	t.Logf("          links=[%v]", server.Links)
+	t.Logf("        keyname=[%s]", server.KeyName)
+	t.Logf(" admin password=[%s]", server.AdminPass)
+	t.Logf("         status=[%s]", server.Status)
+	t.Logf("       progress=[%d]", server.Progress)
+}
+
+func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Get")
+
+	details, err := servers.Get(client, server.ID).Extract()
+	th.AssertNoErr(t, err)
+	logServer(t, details, -1)
+}
+
+func listServers(t *testing.T, client *gophercloud.ServiceClient) {
+	t.Logf("> servers.List")
+
+	count := 0
+	err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		t.Logf("--- Page %02d ---", count)
+
+		s, err := servers.ExtractServers(page)
+		th.AssertNoErr(t, err)
+		for index, server := range s {
+			logServer(t, &server, index)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.ChangeAdminPassword")
+
+	original := server.AdminPass
+
+	t.Logf("Changing server password.")
+	err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract()
+	th.AssertNoErr(t, err)
+
+	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+	t.Logf("Password changed successfully.")
+}
+
+func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Reboot")
+
+	err := servers.Reboot(client, server.ID, os.HardReboot).Extract()
+	th.AssertNoErr(t, err)
+
+	err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Server successfully rebooted.")
+}
+
+func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Rebuild")
+
+	options, err := optionsFromEnv()
+	th.AssertNoErr(t, err)
+
+	opts := os.RebuildOpts{
+		Name:      tools.RandomString("RenamedGopher", 16),
+		AdminPass: tools.MakeNewPassword(server.AdminPass),
+		ImageID:   options.imageID,
+	}
+	after, err := servers.Rebuild(client, server.ID, opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, after.ID, server.ID)
+
+	err = servers.WaitForStatus(client, after.ID, "ACTIVE", 300)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Server successfully rebuilt.")
+	logServer(t, after, -1)
+}
+
+func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+	t.Logf("> servers.Delete")
+
+	err := servers.Delete(client, server.ID)
+	th.AssertNoErr(t, err)
+
+	t.Logf("Server deleted successfully.")
+}
+
+func TestServerOperations(t *testing.T) {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	server := createServer(t, client)
+	defer deleteServer(t, client, server)
+
+	getServer(t, client, server)
+	listServers(t, client)
+	changeAdminPassword(t, client, server)
+	rebootServer(t, client, server)
+	rebuildServer(t, client, server)
+}
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
index 4771ebb..ffade12 100644
--- a/acceptance/tools/tools.go
+++ b/acceptance/tools/tools.go
@@ -1,4 +1,5 @@
 // +build acceptance
+
 package tools
 
 import (
diff --git a/openstack/compute/v2/servers/data_test.go b/openstack/compute/v2/servers/data_test.go
deleted file mode 100644
index d3a0ee0..0000000
--- a/openstack/compute/v2/servers/data_test.go
+++ /dev/null
@@ -1,328 +0,0 @@
-package servers
-
-// Recorded responses for the server resource.
-
-const (
-	serverListBody = `
-      {
-        "servers": [
-          {
-            "status": "ACTIVE",
-            "updated": "2014-09-25T13:10:10Z",
-            "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
-            "OS-EXT-SRV-ATTR:host": "devstack",
-            "addresses": {
-              "private": [
-                {
-                  "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
-                  "version": 4,
-                  "addr": "10.0.0.32",
-                  "OS-EXT-IPS:type": "fixed"
-                }
-              ]
-            },
-            "links": [
-              {
-                "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
-                "rel": "self"
-              },
-              {
-                "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
-                "rel": "bookmark"
-              }
-            ],
-            "key_name": null,
-            "image": {
-              "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
-              "links": [
-                {
-                  "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
-                  "rel": "bookmark"
-                }
-              ]
-            },
-            "OS-EXT-STS:task_state": null,
-            "OS-EXT-STS:vm_state": "active",
-            "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
-            "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
-            "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
-            "flavor": {
-              "id": "1",
-              "links": [
-                {
-                  "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
-                  "rel": "bookmark"
-                }
-              ]
-            },
-            "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
-            "security_groups": [
-              {
-                "name": "default"
-              }
-            ],
-            "OS-SRV-USG:terminated_at": null,
-            "OS-EXT-AZ:availability_zone": "nova",
-            "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
-            "name": "herp",
-            "created": "2014-09-25T13:10:02Z",
-            "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
-            "OS-DCF:diskConfig": "MANUAL",
-            "os-extended-volumes:volumes_attached": [],
-            "accessIPv4": "",
-            "accessIPv6": "",
-            "progress": 0,
-            "OS-EXT-STS:power_state": 1,
-            "config_drive": "",
-            "metadata": {}
-          },
-          {
-            "status": "ACTIVE",
-            "updated": "2014-09-25T13:04:49Z",
-            "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
-            "OS-EXT-SRV-ATTR:host": "devstack",
-            "addresses": {
-              "private": [
-                {
-                  "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
-                  "version": 4,
-                  "addr": "10.0.0.31",
-                  "OS-EXT-IPS:type": "fixed"
-                }
-              ]
-            },
-            "links": [
-              {
-                "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-                "rel": "self"
-              },
-              {
-                "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-                "rel": "bookmark"
-              }
-            ],
-            "key_name": null,
-            "image": {
-              "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
-              "links": [
-                {
-                  "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
-                  "rel": "bookmark"
-                }
-              ]
-            },
-            "OS-EXT-STS:task_state": null,
-            "OS-EXT-STS:vm_state": "active",
-            "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
-            "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
-            "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
-            "flavor": {
-              "id": "1",
-              "links": [
-                {
-                  "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
-                  "rel": "bookmark"
-                }
-              ]
-            },
-            "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-            "security_groups": [
-              {
-                "name": "default"
-              }
-            ],
-            "OS-SRV-USG:terminated_at": null,
-            "OS-EXT-AZ:availability_zone": "nova",
-            "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
-            "name": "derp",
-            "created": "2014-09-25T13:04:41Z",
-            "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
-            "OS-DCF:diskConfig": "MANUAL",
-            "os-extended-volumes:volumes_attached": [],
-            "accessIPv4": "",
-            "accessIPv6": "",
-            "progress": 0,
-            "OS-EXT-STS:power_state": 1,
-            "config_drive": "",
-            "metadata": {}
-          }
-        ]
-      }
-    `
-
-	singleServerBody = `
-    {
-      "server": {
-        "status": "ACTIVE",
-        "updated": "2014-09-25T13:04:49Z",
-        "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
-        "OS-EXT-SRV-ATTR:host": "devstack",
-        "addresses": {
-          "private": [
-            {
-              "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
-              "version": 4,
-              "addr": "10.0.0.31",
-              "OS-EXT-IPS:type": "fixed"
-            }
-          ]
-        },
-        "links": [
-          {
-            "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-            "rel": "self"
-          },
-          {
-            "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-            "rel": "bookmark"
-          }
-        ],
-        "key_name": null,
-        "image": {
-          "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
-          "links": [
-            {
-              "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
-              "rel": "bookmark"
-            }
-          ]
-        },
-        "OS-EXT-STS:task_state": null,
-        "OS-EXT-STS:vm_state": "active",
-        "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
-        "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
-        "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
-        "flavor": {
-          "id": "1",
-          "links": [
-            {
-              "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
-              "rel": "bookmark"
-            }
-          ]
-        },
-        "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-        "security_groups": [
-          {
-            "name": "default"
-          }
-        ],
-        "OS-SRV-USG:terminated_at": null,
-        "OS-EXT-AZ:availability_zone": "nova",
-        "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
-        "name": "derp",
-        "created": "2014-09-25T13:04:41Z",
-        "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
-        "OS-DCF:diskConfig": "MANUAL",
-        "os-extended-volumes:volumes_attached": [],
-        "accessIPv4": "",
-        "accessIPv6": "",
-        "progress": 0,
-        "OS-EXT-STS:power_state": 1,
-        "config_drive": "",
-        "metadata": {}
-      }
-    }
-    `
-)
-
-var (
-	serverHerp = Server{
-		Status:  "ACTIVE",
-		Updated: "2014-09-25T13:10:10Z",
-		HostID:  "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
-		Addresses: map[string]interface{}{
-			"private": []interface{}{
-				map[string]interface{}{
-					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
-					"version":                 float64(4),
-					"addr":                    "10.0.0.32",
-					"OS-EXT-IPS:type":         "fixed",
-				},
-			},
-		},
-		Links: []interface{}{
-			map[string]interface{}{
-				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
-				"rel":  "self",
-			},
-			map[string]interface{}{
-				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
-				"rel":  "bookmark",
-			},
-		},
-		Image: map[string]interface{}{
-			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
-			"links": []interface{}{
-				map[string]interface{}{
-					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
-					"rel":  "bookmark",
-				},
-			},
-		},
-		Flavor: map[string]interface{}{
-			"id": "1",
-			"links": []interface{}{
-				map[string]interface{}{
-					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
-					"rel":  "bookmark",
-				},
-			},
-		},
-		ID:       "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
-		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
-		Name:     "herp",
-		Created:  "2014-09-25T13:10:02Z",
-		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
-		Metadata: map[string]interface{}{},
-	}
-	serverDerp = Server{
-		Status:  "ACTIVE",
-		Updated: "2014-09-25T13:04:49Z",
-		HostID:  "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
-		Addresses: map[string]interface{}{
-			"private": []interface{}{
-				map[string]interface{}{
-					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
-					"version":                 float64(4),
-					"addr":                    "10.0.0.31",
-					"OS-EXT-IPS:type":         "fixed",
-				},
-			},
-		},
-		Links: []interface{}{
-			map[string]interface{}{
-				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-				"rel":  "self",
-			},
-			map[string]interface{}{
-				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-				"rel":  "bookmark",
-			},
-		},
-		Image: map[string]interface{}{
-			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
-			"links": []interface{}{
-				map[string]interface{}{
-					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
-					"rel":  "bookmark",
-				},
-			},
-		},
-		Flavor: map[string]interface{}{
-			"id": "1",
-			"links": []interface{}{
-				map[string]interface{}{
-					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
-					"rel":  "bookmark",
-				},
-			},
-		},
-		ID:       "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
-		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
-		Name:     "derp",
-		Created:  "2014-09-25T13:04:41Z",
-		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
-		Metadata: map[string]interface{}{},
-	}
-)
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
new file mode 100644
index 0000000..e5f7c4b
--- /dev/null
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -0,0 +1,415 @@
+// +build fixtures
+
+package servers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ServerListBody contains the canned body of a servers.List response.
+const ServerListBody = `
+{
+	"servers": [
+		{
+			"status": "ACTIVE",
+			"updated": "2014-09-25T13:10:10Z",
+			"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+			"OS-EXT-SRV-ATTR:host": "devstack",
+			"addresses": {
+				"private": [
+					{
+						"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+						"version": 4,
+						"addr": "10.0.0.32",
+						"OS-EXT-IPS:type": "fixed"
+					}
+				]
+			},
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+					"rel": "self"
+				},
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+					"rel": "bookmark"
+				}
+			],
+			"key_name": null,
+			"image": {
+				"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
+			"OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
+			"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+			"flavor": {
+				"id": "1",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+			"security_groups": [
+				{
+					"name": "default"
+				}
+			],
+			"OS-SRV-USG:terminated_at": null,
+			"OS-EXT-AZ:availability_zone": "nova",
+			"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+			"name": "herp",
+			"created": "2014-09-25T13:10:02Z",
+			"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+			"OS-DCF:diskConfig": "MANUAL",
+			"os-extended-volumes:volumes_attached": [],
+			"accessIPv4": "",
+			"accessIPv6": "",
+			"progress": 0,
+			"OS-EXT-STS:power_state": 1,
+			"config_drive": "",
+			"metadata": {}
+		},
+		{
+			"status": "ACTIVE",
+			"updated": "2014-09-25T13:04:49Z",
+			"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+			"OS-EXT-SRV-ATTR:host": "devstack",
+			"addresses": {
+				"private": [
+					{
+						"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+						"version": 4,
+						"addr": "10.0.0.31",
+						"OS-EXT-IPS:type": "fixed"
+					}
+				]
+			},
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+					"rel": "self"
+				},
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+					"rel": "bookmark"
+				}
+			],
+			"key_name": null,
+			"image": {
+				"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+			"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+			"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+			"flavor": {
+				"id": "1",
+				"links": [
+					{
+						"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+			"security_groups": [
+				{
+					"name": "default"
+				}
+			],
+			"OS-SRV-USG:terminated_at": null,
+			"OS-EXT-AZ:availability_zone": "nova",
+			"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+			"name": "derp",
+			"created": "2014-09-25T13:04:41Z",
+			"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+			"OS-DCF:diskConfig": "MANUAL",
+			"os-extended-volumes:volumes_attached": [],
+			"accessIPv4": "",
+			"accessIPv6": "",
+			"progress": 0,
+			"OS-EXT-STS:power_state": 1,
+			"config_drive": "",
+			"metadata": {}
+		}
+	]
+}
+`
+
+// SingleServerBody is the canned body of a Get request on an existing server.
+const SingleServerBody = `
+{
+	"server": {
+		"status": "ACTIVE",
+		"updated": "2014-09-25T13:04:49Z",
+		"hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+		"OS-EXT-SRV-ATTR:host": "devstack",
+		"addresses": {
+			"private": [
+				{
+					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+					"version": 4,
+					"addr": "10.0.0.31",
+					"OS-EXT-IPS:type": "fixed"
+				}
+			]
+		},
+		"links": [
+			{
+				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel": "self"
+			},
+			{
+				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel": "bookmark"
+			}
+		],
+		"key_name": null,
+		"image": {
+			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"OS-EXT-STS:task_state": null,
+		"OS-EXT-STS:vm_state": "active",
+		"OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+		"OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+		"OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+		"flavor": {
+			"id": "1",
+			"links": [
+				{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+		"security_groups": [
+			{
+				"name": "default"
+			}
+		],
+		"OS-SRV-USG:terminated_at": null,
+		"OS-EXT-AZ:availability_zone": "nova",
+		"user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+		"name": "derp",
+		"created": "2014-09-25T13:04:41Z",
+		"tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+		"OS-DCF:diskConfig": "MANUAL",
+		"os-extended-volumes:volumes_attached": [],
+		"accessIPv4": "",
+		"accessIPv6": "",
+		"progress": 0,
+		"OS-EXT-STS:power_state": 1,
+		"config_drive": "",
+		"metadata": {}
+	}
+}
+`
+
+var (
+	// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
+	ServerHerp = Server{
+		Status:  "ACTIVE",
+		Updated: "2014-09-25T13:10:10Z",
+		HostID:  "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+		Addresses: map[string]interface{}{
+			"private": []interface{}{
+				map[string]interface{}{
+					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+					"version":                 float64(4),
+					"addr":                    "10.0.0.32",
+					"OS-EXT-IPS:type":         "fixed",
+				},
+			},
+		},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+				"rel":  "bookmark",
+			},
+		},
+		Image: map[string]interface{}{
+			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		Flavor: map[string]interface{}{
+			"id": "1",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		ID:       "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
+		Name:     "herp",
+		Created:  "2014-09-25T13:10:02Z",
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]interface{}{},
+	}
+
+	// ServerDerp is a Server struct that should correspond to the second server in ServerListBody.
+	ServerDerp = Server{
+		Status:  "ACTIVE",
+		Updated: "2014-09-25T13:04:49Z",
+		HostID:  "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+		Addresses: map[string]interface{}{
+			"private": []interface{}{
+				map[string]interface{}{
+					"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+					"version":                 float64(4),
+					"addr":                    "10.0.0.31",
+					"OS-EXT-IPS:type":         "fixed",
+				},
+			},
+		},
+		Links: []interface{}{
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel":  "self",
+			},
+			map[string]interface{}{
+				"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+				"rel":  "bookmark",
+			},
+		},
+		Image: map[string]interface{}{
+			"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		Flavor: map[string]interface{}{
+			"id": "1",
+			"links": []interface{}{
+				map[string]interface{}{
+					"href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+					"rel":  "bookmark",
+				},
+			},
+		},
+		ID:       "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+		UserID:   "9349aff8be7545ac9d2f1d00999a23cd",
+		Name:     "derp",
+		Created:  "2014-09-25T13:04:41Z",
+		TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+		Metadata: map[string]interface{}{},
+	}
+)
+
+// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request
+// with a given response.
+func HandleServerCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/servers", 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, `{
+			"server": {
+				"name": "derp",
+				"imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
+				"flavorRef": "1"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request.
+func HandleServerDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf", 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)
+	})
+}
+
+// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
+// change request.
+func HandleAdminPasswordChangeSuccessfully(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, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success.
+func HandleRebootSuccessfully(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, `{ "reboot": { "type": "SOFT" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
+// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success.
+func HandleRebuildSuccessfully(t *testing.T, response string) {
+	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, `
+			{
+				"rebuild": {
+					"name": "new-name",
+					"adminPass": "swordfish",
+					"imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+					"accessIPv4": "1.2.3.4"
+				}
+			}
+		`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 0db92f9..5b65d86 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -6,24 +6,24 @@
 	"testing"
 
 	"github.com/rackspace/gophercloud/pagination"
-	"github.com/rackspace/gophercloud/testhelper"
-	fake "github.com/rackspace/gophercloud/testhelper/client"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
 )
 
 func TestListServers(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+	th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
 		r.ParseForm()
 		marker := r.Form.Get("marker")
 		switch marker {
 		case "":
-			fmt.Fprintf(w, serverListBody)
+			fmt.Fprintf(w, ServerListBody)
 		case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
 			fmt.Fprintf(w, `{ "servers": [] }`)
 		default:
@@ -32,7 +32,7 @@
 	})
 
 	pages := 0
-	err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+	err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
 		pages++
 
 		actual, err := ExtractServers(page)
@@ -43,13 +43,13 @@
 		if len(actual) != 2 {
 			t.Fatalf("Expected 2 servers, got %d", len(actual))
 		}
-		equalServers(t, serverHerp, actual[0])
-		equalServers(t, serverDerp, actual[1])
+		th.CheckDeepEquals(t, ServerHerp, actual[0])
+		th.CheckDeepEquals(t, ServerDerp, actual[1])
 
 		return true, nil
 	})
 
-	testhelper.AssertNoErr(t, err)
+	th.AssertNoErr(t, err)
 
 	if pages != 1 {
 		t.Errorf("Expected 1 page, saw %d", pages)
@@ -57,154 +57,95 @@
 }
 
 func TestCreateServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerCreationSuccessfully(t, SingleServerBody)
 
-	testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `{
-			"server": {
-				"name": "derp",
-				"imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
-				"flavorRef": "1"
-			}
-		}`)
-
-		w.WriteHeader(http.StatusAccepted)
-		w.Header().Add("Content-Type", "application/json")
-		fmt.Fprintf(w, singleServerBody)
-	})
-
-	client := fake.ServiceClient()
-	actual, err := Create(client, CreateOpts{
+	actual, err := Create(client.ServiceClient(), CreateOpts{
 		Name:      "derp",
 		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
 		FlavorRef: "1",
 	}).Extract()
-	if err != nil {
-		t.Fatalf("Unexpected Create error: %v", err)
-	}
+	th.AssertNoErr(t, err)
 
-	equalServers(t, serverDerp, *actual)
+	th.CheckDeepEquals(t, ServerDerp, *actual)
 }
 
 func TestDeleteServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleServerDeletionSuccessfully(t)
 
-	testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "DELETE")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	client := fake.ServiceClient()
-	err := Delete(client, "asdfasdfasdf")
-	if err != nil {
-		t.Fatalf("Unexpected Delete error: %v", err)
-	}
+	err := Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, err)
 }
 
 func TestGetServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
+	th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
 
-		fmt.Fprintf(w, singleServerBody)
+		fmt.Fprintf(w, SingleServerBody)
 	})
 
-	client := fake.ServiceClient()
+	client := client.ServiceClient()
 	actual, err := Get(client, "1234asdf").Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Get error: %v", err)
 	}
 
-	equalServers(t, serverDerp, *actual)
+	th.CheckDeepEquals(t, ServerDerp, *actual)
 }
 
 func TestUpdateServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "PUT")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestHeader(t, r, "Accept", "application/json")
-		testhelper.TestHeader(t, r, "Content-Type", "application/json")
-		testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+	th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
 
-		fmt.Fprintf(w, singleServerBody)
+		fmt.Fprintf(w, SingleServerBody)
 	})
 
-	client := fake.ServiceClient()
+	client := client.ServiceClient()
 	actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Update error: %v", err)
 	}
 
-	equalServers(t, serverDerp, *actual)
+	th.CheckDeepEquals(t, ServerDerp, *actual)
 }
 
 func TestChangeServerAdminPassword(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAdminPasswordChangeSuccessfully(t)
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
-
-		w.WriteHeader(http.StatusAccepted)
-	})
-
-	res := ChangeAdminPassword(fake.ServiceClient(), "1234asdf", "new-password")
-	testhelper.AssertNoErr(t, res.Err)
+	res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+	th.AssertNoErr(t, res.Err)
 }
 
 func TestRebootServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRebootSuccessfully(t)
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
-
-		w.WriteHeader(http.StatusAccepted)
-	})
-
-	res := Reboot(fake.ServiceClient(), "1234asdf", SoftReboot)
-	testhelper.AssertNoErr(t, res.Err)
+	res := Reboot(client.ServiceClient(), "1234asdf", SoftReboot)
+	th.AssertNoErr(t, res.Err)
 }
 
 func TestRebuildServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
-
-	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `
-			{
-				"rebuild": {
-					"name": "new-name",
-					"adminPass": "swordfish",
-					"imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
-					"accessIPv4": "1.2.3.4"
-				}
-			}
-		`)
-
-		w.WriteHeader(http.StatusAccepted)
-		w.Header().Add("Content-Type", "application/json")
-		fmt.Fprintf(w, singleServerBody)
-	})
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleRebuildSuccessfully(t, SingleServerBody)
 
 	opts := RebuildOpts{
 		Name:       "new-name",
@@ -213,56 +154,56 @@
 		AccessIPv4: "1.2.3.4",
 	}
 
-	actual, err := Rebuild(fake.ServiceClient(), "1234asdf", opts).Extract()
-	testhelper.AssertNoErr(t, err)
+	actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract()
+	th.AssertNoErr(t, err)
 
-	equalServers(t, serverDerp, *actual)
+	th.CheckDeepEquals(t, ServerDerp, *actual)
 }
 
 func TestResizeServer(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+	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, `{ "resize": { "flavorRef": "2" } }`)
 
 		w.WriteHeader(http.StatusAccepted)
 	})
 
-	res := Resize(fake.ServiceClient(), "1234asdf", "2")
-	testhelper.AssertNoErr(t, res.Err)
+	res := Resize(client.ServiceClient(), "1234asdf", "2")
+	th.AssertNoErr(t, res.Err)
 }
 
 func TestConfirmResize(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+	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, `{ "confirmResize": null }`)
 
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	res := ConfirmResize(fake.ServiceClient(), "1234asdf")
-	testhelper.AssertNoErr(t, res.Err)
+	res := ConfirmResize(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
 }
 
 func TestRevertResize(t *testing.T) {
-	testhelper.SetupHTTP()
-	defer testhelper.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
-		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-		testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
+	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, `{ "revertResize": null }`)
 
 		w.WriteHeader(http.StatusAccepted)
 	})
 
-	res := RevertResize(fake.ServiceClient(), "1234asdf")
-	testhelper.AssertNoErr(t, res.Err)
+	res := RevertResize(client.ServiceClient(), "1234asdf")
+	th.AssertNoErr(t, res.Err)
 }
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index c3e41d7..74a221f 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -101,11 +101,11 @@
 	Links []interface{}
 
 	// KeyName indicates which public key was injected into the server on launch.
-	KeyName string `mapstructure:"keyname"`
+	KeyName string `json:"key_name" mapstructure:"key_name"`
 
 	// AdminPass will generally be empty ("").  However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place.
 	// Note that this is the ONLY time this field will be valid.
-	AdminPass string `mapstructure:"adminPass"`
+	AdminPass string `json:"adminPass" mapstructure:"adminPass"`
 }
 
 // ServerPage abstracts the raw results of making a List() request against the API.
diff --git a/openstack/compute/v2/servers/servers_test.go b/openstack/compute/v2/servers/servers_test.go
deleted file mode 100644
index 590fc8b..0000000
--- a/openstack/compute/v2/servers/servers_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package servers
-
-import (
-	"reflect"
-	"testing"
-)
-
-// This provides more fine-grained failures when Servers differ, because Server structs are too damn big to compare by eye.
-// FIXME I should absolutely refactor this into a general-purpose thing in testhelper.
-func equalServers(t *testing.T, expected Server, actual Server) {
-	if expected.ID != actual.ID {
-		t.Errorf("ID differs. expected=[%s], actual=[%s]", expected.ID, actual.ID)
-	}
-	if expected.TenantID != actual.TenantID {
-		t.Errorf("TenantID differs. expected=[%s], actual=[%s]", expected.TenantID, actual.TenantID)
-	}
-	if expected.UserID != actual.UserID {
-		t.Errorf("UserID differs. expected=[%s], actual=[%s]", expected.UserID, actual.UserID)
-	}
-	if expected.Name != actual.Name {
-		t.Errorf("Name differs. expected=[%s], actual=[%s]", expected.Name, actual.Name)
-	}
-	if expected.Updated != actual.Updated {
-		t.Errorf("Updated differs. expected=[%s], actual=[%s]", expected.Updated, actual.Updated)
-	}
-	if expected.Created != actual.Created {
-		t.Errorf("Created differs. expected=[%s], actual=[%s]", expected.Created, actual.Created)
-	}
-	if expected.HostID != actual.HostID {
-		t.Errorf("HostID differs. expected=[%s], actual=[%s]", expected.HostID, actual.HostID)
-	}
-	if expected.Status != actual.Status {
-		t.Errorf("Status differs. expected=[%s], actual=[%s]", expected.Status, actual.Status)
-	}
-	if expected.Progress != actual.Progress {
-		t.Errorf("Progress differs. expected=[%s], actual=[%s]", expected.Progress, actual.Progress)
-	}
-	if expected.AccessIPv4 != actual.AccessIPv4 {
-		t.Errorf("AccessIPv4 differs. expected=[%s], actual=[%s]", expected.AccessIPv4, actual.AccessIPv4)
-	}
-	if expected.AccessIPv6 != actual.AccessIPv6 {
-		t.Errorf("AccessIPv6 differs. expected=[%s], actual=[%s]", expected.AccessIPv6, actual.AccessIPv6)
-	}
-	if !reflect.DeepEqual(expected.Image, actual.Image) {
-		t.Errorf("Image differs. expected=[%s], actual=[%s]", expected.Image, actual.Image)
-	}
-	if !reflect.DeepEqual(expected.Flavor, actual.Flavor) {
-		t.Errorf("Flavor differs. expected=[%s], actual=[%s]", expected.Flavor, actual.Flavor)
-	}
-	if !reflect.DeepEqual(expected.Addresses, actual.Addresses) {
-		t.Errorf("Addresses differ. expected=[%s], actual=[%s]", expected.Addresses, actual.Addresses)
-	}
-	if !reflect.DeepEqual(expected.Metadata, actual.Metadata) {
-		t.Errorf("Metadata differs. expected=[%s], actual=[%s]", expected.Metadata, actual.Metadata)
-	}
-	if !reflect.DeepEqual(expected.Links, actual.Links) {
-		t.Errorf("Links differs. expected=[%s], actual=[%s]", expected.Links, actual.Links)
-	}
-	if expected.KeyName != actual.KeyName {
-		t.Errorf("KeyName differs. expected=[%s], actual=[%s]", expected.KeyName, actual.KeyName)
-	}
-	if expected.AdminPass != actual.AdminPass {
-		t.Errorf("AdminPass differs. expected=[%s], actual=[%s]", expected.AdminPass, actual.AdminPass)
-	}
-}
diff --git a/openstack/compute/v2/servers/util.go b/openstack/compute/v2/servers/util.go
new file mode 100644
index 0000000..e6baf74
--- /dev/null
+++ b/openstack/compute/v2/servers/util.go
@@ -0,0 +1,20 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+// WaitForStatus will continually poll a server until it successfully transitions to a specified
+// status. It will do this for at most the number of seconds specified.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/openstack/compute/v2/servers/util_test.go b/openstack/compute/v2/servers/util_test.go
new file mode 100644
index 0000000..e192ae3
--- /dev/null
+++ b/openstack/compute/v2/servers/util_test.go
@@ -0,0 +1,38 @@
+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/rackspace/client.go b/rackspace/client.go
index 9bfa4be..cf00dc7 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -114,6 +114,20 @@
 	}
 }
 
+// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service.
+func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("compute")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+
+	return &gophercloud.ServiceClient{
+		Provider: client,
+		Endpoint: url,
+	}, nil
+}
+
 // NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN.
 func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
 	eo.ApplyDefaults("rax:object-cdn")
diff --git a/rackspace/compute/v2/flavors/delegate.go b/rackspace/compute/v2/flavors/delegate.go
new file mode 100644
index 0000000..2cf31b5
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate.go
@@ -0,0 +1,46 @@
+package flavors
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts helps control the results returned by the List() function. For example, a flavor with a
+// minDisk field of 10 will not be returned if you specify MinDisk set to 20.
+type ListOpts struct {
+
+	// MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria.
+	MinDisk int `q:"minDisk"`
+	MinRAM  int `q:"minRam"`
+
+	// Marker specifies the ID of the last flavor in the previous page.
+	Marker string `q:"marker"`
+
+	// Limit instructs List to refrain from sending excessively large lists of flavors.
+	Limit int `q:"limit"`
+}
+
+// ToFlavorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List enumerates the server images available to your account.
+func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(client, opts)
+}
+
+// Get returns details about a single flavor, identity by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(client, id)
+}
+
+// ExtractFlavors interprets a page of List results as Flavors.
+func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) {
+	return os.ExtractFlavors(page)
+}
diff --git a/rackspace/compute/v2/flavors/delegate_test.go b/rackspace/compute/v2/flavors/delegate_test.go
new file mode 100644
index 0000000..b2a2ea2
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate_test.go
@@ -0,0 +1,62 @@
+package flavors
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, ListOutput)
+		case "performance1-2":
+			fmt.Fprintf(w, `{ "flavors": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	count := 0
+	err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		actual, err := ExtractFlavors(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedFlavorSlice, actual)
+
+		count++
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGetFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors/performance1-1", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+
+	actual, err := Get(client.ServiceClient(), "performance1-1").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &Performance1Flavor, actual)
+}
diff --git a/rackspace/compute/v2/flavors/fixtures.go b/rackspace/compute/v2/flavors/fixtures.go
new file mode 100644
index 0000000..b6dca93
--- /dev/null
+++ b/rackspace/compute/v2/flavors/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+package flavors
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+)
+
+// ListOutput is a sample response of a flavor List request.
+const ListOutput = `
+{
+  "flavors": [
+    {
+      "OS-FLV-EXT-DATA:ephemeral": 0,
+      "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+        "class": "performance1",
+        "disk_io_index": "40",
+        "number_of_data_disks": "0",
+        "policy_class": "performance_flavor",
+        "resize_policy_class": "performance_flavor"
+      },
+      "disk": 20,
+      "id": "performance1-1",
+      "links": [
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
+          "rel": "self"
+        },
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "1 GB Performance",
+      "ram": 1024,
+      "rxtx_factor": 200,
+      "swap": "",
+      "vcpus": 1
+    },
+    {
+      "OS-FLV-EXT-DATA:ephemeral": 20,
+      "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+        "class": "performance1",
+        "disk_io_index": "40",
+        "number_of_data_disks": "1",
+        "policy_class": "performance_flavor",
+        "resize_policy_class": "performance_flavor"
+      },
+      "disk": 40,
+      "id": "performance1-2",
+      "links": [
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2",
+          "rel": "self"
+        },
+        {
+          "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2",
+          "rel": "bookmark"
+        }
+      ],
+      "name": "2 GB Performance",
+      "ram": 2048,
+      "rxtx_factor": 400,
+      "swap": "",
+      "vcpus": 2
+    }
+  ]
+}`
+
+// GetOutput is a sample response from a flavor Get request. Its contents correspond to the
+// Performance1Flavor struct.
+const GetOutput = `
+{
+  "flavor": {
+    "OS-FLV-EXT-DATA:ephemeral": 0,
+    "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+      "class": "performance1",
+      "disk_io_index": "40",
+      "number_of_data_disks": "0",
+      "policy_class": "performance_flavor",
+      "resize_policy_class": "performance_flavor"
+    },
+    "disk": 20,
+    "id": "performance1-1",
+    "links": [
+      {
+        "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
+        "rel": "self"
+      },
+      {
+        "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
+        "rel": "bookmark"
+      }
+    ],
+    "name": "1 GB Performance",
+    "ram": 1024,
+    "rxtx_factor": 200,
+    "swap": "",
+    "vcpus": 1
+  }
+}
+`
+
+// Performance1Flavor is the expected result of parsing GetOutput, or the first element of
+// ListOutput.
+var Performance1Flavor = os.Flavor{
+	ID:         "performance1-1",
+	Disk:       20,
+	RAM:        1024,
+	Name:       "1 GB Performance",
+	RxTxFactor: 200.0,
+	Swap:       0,
+	VCPUs:      1,
+}
+
+// Performance2Flavor is the second result expected from parsing ListOutput.
+var Performance2Flavor = os.Flavor{
+	ID:         "performance1-2",
+	Disk:       40,
+	RAM:        2048,
+	Name:       "2 GB Performance",
+	RxTxFactor: 400.0,
+	Swap:       0,
+	VCPUs:      2,
+}
+
+// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from
+// ListOutput.
+var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor}
diff --git a/rackspace/compute/v2/images/delegate.go b/rackspace/compute/v2/images/delegate.go
new file mode 100644
index 0000000..18e1f31
--- /dev/null
+++ b/rackspace/compute/v2/images/delegate.go
@@ -0,0 +1,22 @@
+package images
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ListDetail enumerates the available server images.
+func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.ListDetail(client, opts)
+}
+
+// Get acquires additional detail about a specific image by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(client, id)
+}
+
+// ExtractImages interprets a page as a collection of server images.
+func ExtractImages(page pagination.Page) ([]os.Image, error) {
+	return os.ExtractImages(page)
+}
diff --git a/rackspace/compute/v2/images/delegate_test.go b/rackspace/compute/v2/images/delegate_test.go
new file mode 100644
index 0000000..db0a6e3
--- /dev/null
+++ b/rackspace/compute/v2/images/delegate_test.go
@@ -0,0 +1,62 @@
+package images
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListImageDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, ListOutput)
+		case "e19a734c-c7e6-443a-830c-242209c4d65d":
+			fmt.Fprintf(w, `{ "images": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	count := 0
+	err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractImages(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedImageSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGetImageDetails(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/images/e19a734c-c7e6-443a-830c-242209c4d65d", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+
+	actual, err := Get(client.ServiceClient(), "e19a734c-c7e6-443a-830c-242209c4d65d").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &UbuntuImage, actual)
+}
diff --git a/rackspace/compute/v2/images/fixtures.go b/rackspace/compute/v2/images/fixtures.go
new file mode 100644
index 0000000..c46d196
--- /dev/null
+++ b/rackspace/compute/v2/images/fixtures.go
@@ -0,0 +1,199 @@
+// +build fixtures
+package images
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+)
+
+// ListOutput is an example response from an /images/detail request.
+const ListOutput = `
+{
+	"images": [
+		{
+			"OS-DCF:diskConfig": "MANUAL",
+			"OS-EXT-IMG-SIZE:size": 1.017415075e+09,
+			"created": "2014-10-01T15:49:02Z",
+			"id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+			"links": [
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+					"rel": "self"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+					"rel": "bookmark"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+					"rel": "alternate",
+					"type": "application/vnd.openstack.image"
+				}
+			],
+			"metadata": {
+				"auto_disk_config": "disabled",
+				"cache_in_nova": "True",
+				"com.rackspace__1__build_core": "1",
+				"com.rackspace__1__build_managed": "1",
+				"com.rackspace__1__build_rackconnect": "1",
+				"com.rackspace__1__options": "0",
+				"com.rackspace__1__platform_target": "PublicCloud",
+				"com.rackspace__1__release_build_date": "2014-10-01_15-46-08",
+				"com.rackspace__1__release_id": "100",
+				"com.rackspace__1__release_version": "10",
+				"com.rackspace__1__source": "kickstart",
+				"com.rackspace__1__visible_core": "1",
+				"com.rackspace__1__visible_managed": "0",
+				"com.rackspace__1__visible_rackconnect": "0",
+				"image_type": "base",
+				"org.openstack__1__architecture": "x64",
+				"org.openstack__1__os_distro": "org.archlinux",
+				"org.openstack__1__os_version": "2014.8",
+				"os_distro": "arch",
+				"os_type": "linux",
+				"vm_mode": "hvm"
+			},
+			"minDisk": 20,
+			"minRam": 512,
+			"name": "Arch 2014.10 (PVHVM)",
+			"progress": 100,
+			"status": "ACTIVE",
+			"updated": "2014-10-01T19:37:58Z"
+		},
+		{
+			"OS-DCF:diskConfig": "AUTO",
+			"OS-EXT-IMG-SIZE:size": 1.060306463e+09,
+			"created": "2014-10-01T12:58:11Z",
+			"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+			"links": [
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "self"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "bookmark"
+				},
+				{
+					"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "alternate",
+					"type": "application/vnd.openstack.image"
+				}
+			],
+			"metadata": {
+				"auto_disk_config": "True",
+				"cache_in_nova": "True",
+				"com.rackspace__1__build_core": "1",
+				"com.rackspace__1__build_managed": "1",
+				"com.rackspace__1__build_rackconnect": "1",
+				"com.rackspace__1__options": "0",
+				"com.rackspace__1__platform_target": "PublicCloud",
+				"com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
+				"com.rackspace__1__release_id": "1007",
+				"com.rackspace__1__release_version": "6",
+				"com.rackspace__1__source": "kickstart",
+				"com.rackspace__1__visible_core": "1",
+				"com.rackspace__1__visible_managed": "1",
+				"com.rackspace__1__visible_rackconnect": "1",
+				"image_type": "base",
+				"org.openstack__1__architecture": "x64",
+				"org.openstack__1__os_distro": "com.ubuntu",
+				"org.openstack__1__os_version": "14.04",
+				"os_distro": "ubuntu",
+				"os_type": "linux",
+				"vm_mode": "xen"
+			},
+			"minDisk": 20,
+			"minRam": 512,
+			"name": "Ubuntu 14.04 LTS (Trusty Tahr)",
+			"progress": 100,
+			"status": "ACTIVE",
+			"updated": "2014-10-01T15:51:44Z"
+		}
+	]
+}
+`
+
+// GetOutput is an example response from an /images request.
+const GetOutput = `
+{
+	"image": {
+		"OS-DCF:diskConfig": "AUTO",
+		"OS-EXT-IMG-SIZE:size": 1060306463,
+		"created": "2014-10-01T12:58:11Z",
+		"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+		"links": [
+			{
+				"href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel": "self"
+			},
+			{
+				"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel": "bookmark"
+			},
+			{
+				"href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel": "alternate",
+				"type": "application/vnd.openstack.image"
+			}
+		],
+		"metadata": {
+			"auto_disk_config": "True",
+			"cache_in_nova": "True",
+			"com.rackspace__1__build_core": "1",
+			"com.rackspace__1__build_managed": "1",
+			"com.rackspace__1__build_rackconnect": "1",
+			"com.rackspace__1__options": "0",
+			"com.rackspace__1__platform_target": "PublicCloud",
+			"com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
+			"com.rackspace__1__release_id": "1007",
+			"com.rackspace__1__release_version": "6",
+			"com.rackspace__1__source": "kickstart",
+			"com.rackspace__1__visible_core": "1",
+			"com.rackspace__1__visible_managed": "1",
+			"com.rackspace__1__visible_rackconnect": "1",
+			"image_type": "base",
+			"org.openstack__1__architecture": "x64",
+			"org.openstack__1__os_distro": "com.ubuntu",
+			"org.openstack__1__os_version": "14.04",
+			"os_distro": "ubuntu",
+			"os_type": "linux",
+			"vm_mode": "xen"
+		},
+		"minDisk": 20,
+		"minRam": 512,
+		"name": "Ubuntu 14.04 LTS (Trusty Tahr)",
+		"progress": 100,
+		"status": "ACTIVE",
+		"updated": "2014-10-01T15:51:44Z"
+	}
+}
+`
+
+// ArchImage is the first Image structure that should be parsed from ListOutput.
+var ArchImage = os.Image{
+	ID:       "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+	Name:     "Arch 2014.10 (PVHVM)",
+	Created:  "2014-10-01T15:49:02Z",
+	Updated:  "2014-10-01T19:37:58Z",
+	MinDisk:  20,
+	MinRAM:   512,
+	Progress: 100,
+	Status:   "ACTIVE",
+}
+
+// UbuntuImage is the second Image structure that should be parsed from ListOutput and
+// the only image that should be extracted from GetOutput.
+var UbuntuImage = os.Image{
+	ID:       "e19a734c-c7e6-443a-830c-242209c4d65d",
+	Name:     "Ubuntu 14.04 LTS (Trusty Tahr)",
+	Created:  "2014-10-01T12:58:11Z",
+	Updated:  "2014-10-01T15:51:44Z",
+	MinDisk:  20,
+	MinRAM:   512,
+	Progress: 100,
+	Status:   "ACTIVE",
+}
+
+// ExpectedImageSlice is the collection of images that should be parsed from ListOutput,
+// in order.
+var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage}
diff --git a/rackspace/compute/v2/servers/delegate.go b/rackspace/compute/v2/servers/delegate.go
new file mode 100644
index 0000000..cbf5384
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate.go
@@ -0,0 +1,61 @@
+package servers
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List makes a request against the API to list servers accessible to you.
+func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+	return os.List(client, opts)
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+	return os.Create(client, opts)
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	return os.Delete(client, id)
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+	return os.Get(client, id)
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult {
+	return os.ChangeAdminPassword(client, id, newPassword)
+}
+
+// Reboot requests that a given server reboot. Two methods exist for rebooting a server:
+//
+// os.HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the
+// machine, or if a VM, terminating it at the hypervisor level. It's done. Caput. Full stop. Then,
+// after a brief wait, power is restored or the VM instance restarted.
+//
+// os.SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. E.g., in
+// Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) os.ActionResult {
+	return os.Reboot(client, id, how)
+}
+
+// Rebuild will reprovision the server according to the configuration options provided in the
+// RebuildOpts struct.
+func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult {
+	return os.Rebuild(client, id, opts)
+}
+
+// WaitForStatus will continually poll a server until it successfully transitions to a specified
+// status. It will do this for at most the number of seconds specified.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return os.WaitForStatus(c, id, status, secs)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]os.Server, error) {
+	return os.ExtractServers(page)
+}
diff --git a/rackspace/compute/v2/servers/delegate_test.go b/rackspace/compute/v2/servers/delegate_test.go
new file mode 100644
index 0000000..0c331eb
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate_test.go
@@ -0,0 +1,112 @@
+package servers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListServers(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, ListOutput)
+	})
+
+	count := 0
+	err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractServers(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedServerSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestCreateServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleServerCreationSuccessfully(t, CreateOutput)
+
+	actual, err := Create(client.ServiceClient(), os.CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, &CreatedServer, actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleServerDeletionSuccessfully(t)
+
+	err := Delete(client.ServiceClient(), "asdfasdfasdf")
+	th.AssertNoErr(t, err)
+}
+
+func TestGetServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+
+	actual, err := Get(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &GophercloudServer, actual)
+}
+
+func TestChangeAdminPassword(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleAdminPasswordChangeSuccessfully(t)
+
+	res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestReboot(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleRebootSuccessfully(t)
+
+	res := Reboot(client.ServiceClient(), "1234asdf", os.SoftReboot)
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestRebuildServer(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleRebuildSuccessfully(t, GetOutput)
+
+	opts := os.RebuildOpts{
+		Name:       "new-name",
+		AdminPass:  "swordfish",
+		ImageID:    "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+		AccessIPv4: "1.2.3.4",
+	}
+	actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &GophercloudServer, actual)
+}
diff --git a/rackspace/compute/v2/servers/fixtures.go b/rackspace/compute/v2/servers/fixtures.go
new file mode 100644
index 0000000..b22a289
--- /dev/null
+++ b/rackspace/compute/v2/servers/fixtures.go
@@ -0,0 +1,439 @@
+// +build fixtures
+
+package servers
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// ListOutput is the recorded output of a Rackspace servers.List request.
+const ListOutput = `
+{
+	"servers": [
+		{
+			"OS-DCF:diskConfig": "MANUAL",
+			"OS-EXT-STS:power_state": 1,
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"accessIPv4": "1.2.3.4",
+			"accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+			"addresses": {
+				"private": [
+					{
+						"addr": "10.208.230.113",
+						"version": 4
+					}
+				],
+				"public": [
+					{
+						"addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0",
+						"version": 6
+					},
+					{
+						"addr": "104.130.131.164",
+						"version": 4
+					}
+				]
+			},
+			"created": "2014-09-23T12:34:58Z",
+			"flavor": {
+				"id": "performance1-8",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
+			"id": "59818cee-bc8c-44eb-8073-673ee65105f7",
+			"image": {
+				"id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"key_name": "mykey",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+					"rel": "self"
+				},
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+					"rel": "bookmark"
+				}
+			],
+			"metadata": {},
+			"name": "devstack",
+			"progress": 100,
+			"status": "ACTIVE",
+			"tenant_id": "111111",
+			"updated": "2014-09-23T12:38:19Z",
+			"user_id": "14ae7bb21d81422694655f3cc30f2930"
+		},
+		{
+			"OS-DCF:diskConfig": "MANUAL",
+			"OS-EXT-STS:power_state": 1,
+			"OS-EXT-STS:task_state": null,
+			"OS-EXT-STS:vm_state": "active",
+			"accessIPv4": "1.1.2.3",
+			"accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+			"addresses": {
+				"private": [
+					{
+						"addr": "10.10.20.30",
+						"version": 4
+					}
+				],
+				"public": [
+					{
+						"addr": "1.1.2.3",
+						"version": 4
+					},
+					{
+						"addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+						"version": 6
+					}
+				]
+			},
+			"created": "2014-07-21T19:32:55Z",
+			"flavor": {
+				"id": "performance1-2",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
+			"id": "25f1c7f5-e00a-4715-b354-16e24b2f4630",
+			"image": {
+				"id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+				"links": [
+					{
+						"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+						"rel": "bookmark"
+					}
+				]
+			},
+			"key_name": "otherkey",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+					"rel": "self"
+				},
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+					"rel": "bookmark"
+				}
+			],
+			"metadata": {},
+			"name": "peril-dfw",
+			"progress": 100,
+			"status": "ACTIVE",
+			"tenant_id": "111111",
+			"updated": "2014-07-21T19:34:24Z",
+			"user_id": "14ae7bb21d81422694655f3cc30f2930"
+		}
+	]
+}
+`
+
+// GetOutput is the recorded output of a Rackspace servers.Get request.
+const GetOutput = `
+{
+	"server": {
+		"OS-DCF:diskConfig": "AUTO",
+		"OS-EXT-STS:power_state": 1,
+		"OS-EXT-STS:task_state": null,
+		"OS-EXT-STS:vm_state": "active",
+		"accessIPv4": "1.2.4.8",
+		"accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777",
+		"addresses": {
+			"private": [
+				{
+					"addr": "10.20.40.80",
+					"version": 4
+				}
+			],
+			"public": [
+				{
+					"addr": "1.2.4.8",
+					"version": 4
+				},
+				{
+					"addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
+					"version": 6
+				}
+			]
+		},
+		"created": "2014-10-21T14:42:16Z",
+		"flavor": {
+			"id": "performance1-1",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
+		"id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+		"image": {
+			"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+			"links": [
+				{
+					"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+					"rel": "bookmark"
+				}
+			]
+		},
+		"key_name": null,
+		"links": [
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+				"rel": "self"
+			},
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+				"rel": "bookmark"
+			}
+		],
+		"metadata": {},
+		"name": "Gophercloud-pxpGGuey",
+		"progress": 100,
+		"status": "ACTIVE",
+		"tenant_id": "111111",
+		"updated": "2014-10-21T14:42:57Z",
+		"user_id": "14ae7bb21d81423694655f4dd30f2930"
+	}
+}
+`
+
+// CreateOutput contains a sample of Rackspace's response to a Create call.
+const CreateOutput = `
+{
+	"server": {
+		"OS-DCF:diskConfig": "AUTO",
+		"adminPass": "v7tADqbE5pr9",
+		"id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+		"links": [
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+				"rel": "self"
+			},
+			{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+				"rel": "bookmark"
+			}
+		]
+	}
+}
+`
+
+// DevstackServer is the expected first result from parsing ListOutput.
+var DevstackServer = os.Server{
+	ID:         "59818cee-bc8c-44eb-8073-673ee65105f7",
+	Name:       "devstack",
+	TenantID:   "111111",
+	UserID:     "14ae7bb21d81422694655f3cc30f2930",
+	HostID:     "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
+	Updated:    "2014-09-23T12:38:19Z",
+	Created:    "2014-09-23T12:34:58Z",
+	AccessIPv4: "1.2.3.4",
+	AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+	Progress:   100,
+	Status:     "ACTIVE",
+	Image: map[string]interface{}{
+		"id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Flavor: map[string]interface{}{
+		"id": "performance1-8",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Addresses: map[string]interface{}{
+		"private": []interface{}{
+			map[string]interface{}{
+				"addr":    "10.20.30.40",
+				"version": float64(4.0),
+			},
+		},
+		"public": []interface{}{
+			map[string]interface{}{
+				"addr":    "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+				"version": float64(6.0),
+			},
+			map[string]interface{}{
+				"addr":    "1.2.3.4",
+				"version": float64(4.0),
+			},
+		},
+	},
+	Metadata: map[string]interface{}{},
+	Links: []interface{}{
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7",
+			"rel":  "self",
+		},
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+			"rel":  "bookmark",
+		},
+	},
+	KeyName:   "mykey",
+	AdminPass: "",
+}
+
+// PerilServer is the expected second result from parsing ListOutput.
+var PerilServer = os.Server{
+	ID:         "25f1c7f5-e00a-4715-b354-16e24b2f4630",
+	Name:       "peril-dfw",
+	TenantID:   "111111",
+	UserID:     "14ae7bb21d81422694655f3cc30f2930",
+	HostID:     "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
+	Updated:    "2014-07-21T19:34:24Z",
+	Created:    "2014-07-21T19:32:55Z",
+	AccessIPv4: "1.1.2.3",
+	AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+	Progress:   100,
+	Status:     "ACTIVE",
+	Image: map[string]interface{}{
+		"id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Flavor: map[string]interface{}{
+		"id": "performance1-2",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Addresses: map[string]interface{}{
+		"private": []interface{}{
+			map[string]interface{}{
+				"addr":    "10.10.20.30",
+				"version": float64(4.0),
+			},
+		},
+		"public": []interface{}{
+			map[string]interface{}{
+				"addr":    "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+				"version": float64(6.0),
+			},
+			map[string]interface{}{
+				"addr":    "1.1.2.3",
+				"version": float64(4.0),
+			},
+		},
+	},
+	Metadata: map[string]interface{}{},
+	Links: []interface{}{
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+			"rel":  "self",
+		},
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+			"rel":  "bookmark",
+		},
+	},
+	KeyName:   "otherkey",
+	AdminPass: "",
+}
+
+// GophercloudServer is the expected result from parsing GetOutput.
+var GophercloudServer = os.Server{
+	ID:         "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+	Name:       "Gophercloud-pxpGGuey",
+	TenantID:   "111111",
+	UserID:     "14ae7bb21d81423694655f4dd30f2930",
+	HostID:     "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
+	Updated:    "2014-10-21T14:42:57Z",
+	Created:    "2014-10-21T14:42:16Z",
+	AccessIPv4: "1.2.4.8",
+	AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777",
+	Progress:   100,
+	Status:     "ACTIVE",
+	Image: map[string]interface{}{
+		"id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Flavor: map[string]interface{}{
+		"id": "performance1-1",
+		"links": []interface{}{
+			map[string]interface{}{
+				"href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
+				"rel":  "bookmark",
+			},
+		},
+	},
+	Addresses: map[string]interface{}{
+		"private": []interface{}{
+			map[string]interface{}{
+				"addr":    "10.20.40.80",
+				"version": float64(4.0),
+			},
+		},
+		"public": []interface{}{
+			map[string]interface{}{
+				"addr":    "2001:4800:6666:105:2a0f:c056:f594:7777",
+				"version": float64(6.0),
+			},
+			map[string]interface{}{
+				"addr":    "1.2.4.8",
+				"version": float64(4.0),
+			},
+		},
+	},
+	Metadata: map[string]interface{}{},
+	Links: []interface{}{
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+			"rel":  "self",
+		},
+		map[string]interface{}{
+			"href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+			"rel":  "bookmark",
+		},
+	},
+	KeyName:   "",
+	AdminPass: "",
+}
+
+// CreatedServer is the partial Server struct that can be parsed from CreateOutput.
+var CreatedServer = os.Server{
+	ID:        "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+	AdminPass: "v7tADqbE5pr9",
+	Links:     []interface{}{},
+}
+
+// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput.
+var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer}
diff --git a/results.go b/results.go
index f60bc76..19557fb 100644
--- a/results.go
+++ b/results.go
@@ -1,6 +1,9 @@
 package gophercloud
 
-import "net/http"
+import (
+	"encoding/json"
+	"net/http"
+)
 
 // Result acts as a base struct that other results can embed.
 type Result struct {
@@ -16,6 +19,15 @@
 	Err error
 }
 
+// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON.
+func (r Result) PrettyPrintJSON() string {
+	pretty, err := json.MarshalIndent(r.Body, "", "  ")
+	if err != nil {
+		panic(err.Error())
+	}
+	return string(pretty)
+}
+
 // RFC3339Milli describes a time format used by API responses.
 const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
 
diff --git a/testhelper/convenience.go b/testhelper/convenience.go
index f6cb371..ca27cad 100644
--- a/testhelper/convenience.go
+++ b/testhelper/convenience.go
@@ -5,11 +5,12 @@
 	"path/filepath"
 	"reflect"
 	"runtime"
+	"strings"
 	"testing"
 )
 
-func prefix() string {
-	_, file, line, _ := runtime.Caller(3)
+func prefix(depth int) string {
+	_, file, line, _ := runtime.Caller(depth)
 	return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
 }
 
@@ -22,11 +23,182 @@
 }
 
 func logFatal(t *testing.T, str string) {
-	t.Fatalf("\033[1;31m%s %s\033[0m", prefix(), str)
+	t.Fatalf("\033[1;31m%s %s\033[0m", prefix(3), str)
 }
 
 func logError(t *testing.T, str string) {
-	t.Errorf("\033[1;31m%s %s\033[0m", prefix(), str)
+	t.Errorf("\033[1;31m%s %s\033[0m", prefix(3), str)
+}
+
+type diffLogger func([]string, interface{}, interface{})
+
+type visit struct {
+	a1  uintptr
+	a2  uintptr
+	typ reflect.Type
+}
+
+// Recursively visits the structures of "expected" and "actual". The diffLogger function will be
+// invoked with each different value encountered, including the reference path that was followed
+// to get there.
+func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) {
+	defer func() {
+		// Fall back to the regular reflect.DeepEquals function.
+		if r := recover(); r != nil {
+			var e, a interface{}
+			if expected.IsValid() {
+				e = expected.Interface()
+			}
+			if actual.IsValid() {
+				a = actual.Interface()
+			}
+
+			if !reflect.DeepEqual(e, a) {
+				logDifference(path, e, a)
+			}
+		}
+	}()
+
+	if !expected.IsValid() && actual.IsValid() {
+		logDifference(path, nil, actual.Interface())
+		return
+	}
+	if expected.IsValid() && !actual.IsValid() {
+		logDifference(path, expected.Interface(), nil)
+		return
+	}
+	if !expected.IsValid() && !actual.IsValid() {
+		return
+	}
+
+	hard := func(k reflect.Kind) bool {
+		switch k {
+		case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
+			return true
+		}
+		return false
+	}
+
+	if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) {
+		addr1 := expected.UnsafeAddr()
+		addr2 := actual.UnsafeAddr()
+
+		if addr1 > addr2 {
+			addr1, addr2 = addr2, addr1
+		}
+
+		if addr1 == addr2 {
+			// References are identical. We can short-circuit
+			return
+		}
+
+		typ := expected.Type()
+		v := visit{addr1, addr2, typ}
+		if visited[v] {
+			// Already visited.
+			return
+		}
+
+		// Remember this visit for later.
+		visited[v] = true
+	}
+
+	switch expected.Kind() {
+	case reflect.Array:
+		for i := 0; i < expected.Len(); i++ {
+			hop := append(path, fmt.Sprintf("[%d]", i))
+			deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
+		}
+		return
+	case reflect.Slice:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+			return
+		}
+		if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
+			return
+		}
+		for i := 0; i < expected.Len(); i++ {
+			hop := append(path, fmt.Sprintf("[%d]", i))
+			deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
+		}
+		return
+	case reflect.Interface:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+			return
+		}
+		deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
+		return
+	case reflect.Ptr:
+		deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
+		return
+	case reflect.Struct:
+		for i, n := 0, expected.NumField(); i < n; i++ {
+			field := expected.Type().Field(i)
+			hop := append(path, "."+field.Name)
+			deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference)
+		}
+		return
+	case reflect.Map:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+			return
+		}
+		if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
+			return
+		}
+
+		var keys []reflect.Value
+		if expected.Len() >= actual.Len() {
+			keys = expected.MapKeys()
+		} else {
+			keys = actual.MapKeys()
+		}
+
+		for _, k := range keys {
+			expectedValue := expected.MapIndex(k)
+			actualValue := expected.MapIndex(k)
+
+			if !expectedValue.IsValid() {
+				logDifference(path, nil, actual.Interface())
+				return
+			}
+			if !actualValue.IsValid() {
+				logDifference(path, expected.Interface(), nil)
+				return
+			}
+
+			hop := append(path, fmt.Sprintf("[%v]", k))
+			deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference)
+		}
+		return
+	case reflect.Func:
+		if expected.IsNil() != actual.IsNil() {
+			logDifference(path, expected.Interface(), actual.Interface())
+		}
+		return
+	default:
+		if expected.Interface() != actual.Interface() {
+			logDifference(path, expected.Interface(), actual.Interface())
+		}
+	}
+}
+
+func deepDiff(expected, actual interface{}, logDifference diffLogger) {
+	if expected == nil || actual == nil {
+		logDifference([]string{}, expected, actual)
+		return
+	}
+
+	expectedValue := reflect.ValueOf(expected)
+	actualValue := reflect.ValueOf(actual)
+
+	if expectedValue.Type() != actualValue.Type() {
+		logDifference([]string{}, expected, actual)
+		return
+	}
+	deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference)
 }
 
 // AssertEquals compares two arbitrary values and performs a comparison. If the
@@ -47,16 +219,33 @@
 // AssertDeepEquals - like Equals - performs a comparison - but on more complex
 // structures that requires deeper inspection
 func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
-	if !reflect.DeepEqual(expected, actual) {
-		logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+	pre := prefix(2)
+
+	differed := false
+	deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
+		differed = true
+		t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m",
+			pre,
+			strings.Join(path, ""),
+			green(expected),
+			yellow(actual))
+	})
+	if differed {
+		logFatal(t, "The structures were different.")
 	}
 }
 
 // CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error
 func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
-	if !reflect.DeepEqual(expected, actual) {
-		logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
-	}
+	pre := prefix(2)
+
+	deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
+		t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m",
+			pre,
+			strings.Join(path, ""),
+			green(expected),
+			yellow(actual))
+	})
 }
 
 // AssertNoErr is a convenience function for checking whether an error value is