Merge pull request #205 from smashwilson/paginate-compute

Update compute service
diff --git a/acceptance/openstack/compute/compute_test.go b/acceptance/openstack/compute/compute_test.go
deleted file mode 100644
index 2310858..0000000
--- a/acceptance/openstack/compute/compute_test.go
+++ /dev/null
@@ -1,370 +0,0 @@
-// +build acceptance
-
-package compute
-
-import (
-	"fmt"
-	"os"
-	"testing"
-
-	"github.com/rackspace/gophercloud/acceptance/tools"
-	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
-	"github.com/rackspace/gophercloud/openstack/compute/v2/images"
-	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-)
-
-var service = "compute"
-
-func TestListServers(t *testing.T) {
-	ts, err := tools.SetupForList(service)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	fmt.Fprintln(ts.W, "ID\tRegion\tName\tStatus\tIPv4\tIPv6\t")
-
-	region := os.Getenv("OS_REGION_NAME")
-	n := 0
-	for _, ep := range ts.EPs {
-		if (region != "") && (region != ep.Region) {
-			continue
-		}
-
-		client := servers.NewClient(ep.PublicURL, ts.A, ts.O)
-
-		listResults, err := servers.List(client)
-		if err != nil {
-			t.Error(err)
-			return
-		}
-
-		svrs, err := servers.GetServers(listResults)
-		if err != nil {
-			t.Error(err)
-			return
-		}
-
-		n = n + len(svrs)
-
-		for _, s := range svrs {
-			fmt.Fprintf(ts.W, "%s\t%s\t%s\t%s\t%s\t%s\t\n", s.Id, s.Name, ep.Region, s.Status, s.AccessIPv4, s.AccessIPv6)
-		}
-	}
-	ts.W.Flush()
-	fmt.Printf("--------\n%d servers listed.\n", n)
-}
-
-func TestListImages(t *testing.T) {
-	ts, err := tools.SetupForList(service)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	fmt.Fprintln(ts.W, "ID\tRegion\tName\tStatus\tCreated\t")
-
-	region := os.Getenv("OS_REGION_NAME")
-	n := 0
-	for _, ep := range ts.EPs {
-		if (region != "") && (region != ep.Region) {
-			continue
-		}
-
-		client := images.NewClient(ep.PublicURL, ts.A, ts.O)
-
-		listResults, err := images.List(client)
-		if err != nil {
-			t.Error(err)
-			return
-		}
-
-		imgs, err := images.GetImages(listResults)
-		if err != nil {
-			t.Error(err)
-			return
-		}
-
-		n = n + len(imgs)
-
-		for _, i := range imgs {
-			fmt.Fprintf(ts.W, "%s\t%s\t%s\t%s\t%s\t\n", i.Id, ep.Region, i.Name, i.Status, i.Created)
-		}
-	}
-	ts.W.Flush()
-	fmt.Printf("--------\n%d images listed.\n", n)
-}
-
-func TestListFlavors(t *testing.T) {
-	ts, err := tools.SetupForList(service)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	fmt.Fprintln(ts.W, "ID\tRegion\tName\tRAM\tDisk\tVCPUs\t")
-
-	region := os.Getenv("OS_REGION_NAME")
-	n := 0
-	for _, ep := range ts.EPs {
-		if (region != "") && (region != ep.Region) {
-			continue
-		}
-
-		client := flavors.NewClient(ep.PublicURL, ts.A, ts.O)
-
-		listResults, err := flavors.List(client, flavors.ListFilterOptions{})
-		if err != nil {
-			t.Error(err)
-			return
-		}
-
-		flavs, err := flavors.GetFlavors(listResults)
-		if err != nil {
-			t.Error(err)
-			return
-		}
-
-		n = n + len(flavs)
-
-		for _, f := range flavs {
-			fmt.Fprintf(ts.W, "%s\t%s\t%s\t%d\t%d\t%d\t\n", f.Id, ep.Region, f.Name, f.Ram, f.Disk, f.VCpus)
-		}
-	}
-	ts.W.Flush()
-	fmt.Printf("--------\n%d flavors listed.\n", n)
-}
-
-func TestGetFlavor(t *testing.T) {
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	region := os.Getenv("OS_REGION_NAME")
-	for _, ep := range ts.EPs {
-		if (region != "") && (region != ep.Region) {
-			continue
-		}
-		client := flavors.NewClient(ep.PublicURL, ts.A, ts.O)
-
-		getResults, err := flavors.Get(client, ts.FlavorId)
-		if err != nil {
-			t.Fatal(err)
-		}
-		flav, err := flavors.GetFlavor(getResults)
-		if err != nil {
-			t.Fatal(err)
-		}
-		fmt.Printf("%#v\n", flav)
-	}
-}
-
-func TestCreateDestroyServer(t *testing.T) {
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	// We put this in a defer so that it gets executed even in the face of errors or panics.
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Error(err)
-	}
-}
-
-func TestUpdateServer(t *testing.T) {
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	err = tools.ChangeServerName(ts)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-}
-
-func TestActionChangeAdminPassword(t *testing.T) {
-	t.Parallel()
-
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.ChangeAdminPassword(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestActionReboot(t *testing.T) {
-	t.Parallel()
-
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = servers.Reboot(ts.Client, ts.CreatedServer.Id, "aldhjflaskhjf")
-	if err == nil {
-		t.Fatal("Expected the SDK to provide an ArgumentError here")
-	}
-
-	err = tools.RebootServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestActionRebuild(t *testing.T) {
-	t.Parallel()
-
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.RebuildServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestActionResizeConfirm(t *testing.T) {
-	t.Parallel()
-
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.ResizeServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.ConfirmResize(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestActionResizeRevert(t *testing.T) {
-	t.Parallel()
-
-	ts, err := tools.SetupForCRUD()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.CreateServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	defer func() {
-		servers.Delete(ts.Client, ts.CreatedServer.Id)
-	}()
-
-	err = tools.WaitForStatus(ts, "ACTIVE")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.ResizeServer(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = tools.RevertResize(ts)
-	if err != nil {
-		t.Fatal(err)
-	}
-}
diff --git a/acceptance/openstack/compute/pkg.go b/acceptance/openstack/compute/pkg.go
deleted file mode 100644
index a887fe9..0000000
--- a/acceptance/openstack/compute/pkg.go
+++ /dev/null
@@ -1 +0,0 @@
-package compute
diff --git a/acceptance/openstack/compute/tools_test.go b/acceptance/openstack/compute/tools_test.go
deleted file mode 100644
index 1acb824..0000000
--- a/acceptance/openstack/compute/tools_test.go
+++ /dev/null
@@ -1,367 +0,0 @@
-// +build acceptance
-
-package compute
-
-import (
-	"crypto/rand"
-	"fmt"
-	"os"
-	"text/tabwriter"
-	"time"
-
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-	"github.com/rackspace/gophercloud/openstack/utils"
-)
-
-var errTimeout = fmt.Errorf("Timeout.")
-
-type testState struct {
-	o              gophercloud.AuthOptions
-	a              identity.AuthResults
-	sc             *identity.ServiceCatalog
-	eps            []identity.Endpoint
-	w              *tabwriter.Writer
-	imageId        string
-	flavorId       string
-	region         string
-	ep             string
-	client         *servers.Client
-	createdServer  *servers.Server
-	gottenServer   *servers.Server
-	updatedServer  *servers.Server
-	serverName     string
-	alternateName  string
-	flavorIdResize string
-}
-
-func SetupForList(service string) (*testState, error) {
-	var err error
-
-	ts := new(testState)
-
-	ts.o, err = utils.AuthOptions()
-	if err != nil {
-		return ts, err
-	}
-
-	client := &gophercloud.ServiceClient{Endpoint: ts.o.IdentityEndpoint}
-	ts.a, err = identity.Authenticate(client, ts.o)
-	if err != nil {
-		return ts, err
-	}
-
-	ts.sc, err = identity.GetServiceCatalog(ts.a)
-	if err != nil {
-		return ts, err
-	}
-
-	ts.eps, err = FindAllEndpoints(ts.sc, service)
-	if err != nil {
-		return ts, err
-	}
-
-	ts.w = new(tabwriter.Writer)
-	ts.w.Init(os.Stdout, 2, 8, 2, ' ', 0)
-
-	return ts, nil
-}
-
-func SetupForCRUD() (*testState, error) {
-	ts, err := SetupForList("compute")
-	if err != nil {
-		return ts, err
-	}
-
-	ts.imageId = os.Getenv("OS_IMAGE_ID")
-	if ts.imageId == "" {
-		return ts, fmt.Errorf("Expected OS_IMAGE_ID environment variable to be set")
-	}
-
-	ts.flavorId = os.Getenv("OS_FLAVOR_ID")
-	if ts.flavorId == "" {
-		return ts, fmt.Errorf("Expected OS_FLAVOR_ID environment variable to be set")
-	}
-
-	ts.flavorIdResize = os.Getenv("OS_FLAVOR_ID_RESIZE")
-	if ts.flavorIdResize == "" {
-		return ts, fmt.Errorf("Expected OS_FLAVOR_ID_RESIZE environment variable to be set")
-	}
-
-	if ts.flavorIdResize == ts.flavorId {
-		return ts, fmt.Errorf("OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE cannot be the same")
-	}
-
-	ts.region = os.Getenv("OS_REGION_NAME")
-	if ts.region == "" {
-		ts.region = ts.eps[0].Region
-	}
-
-	ts.ep, err = FindEndpointForRegion(ts.eps, ts.region)
-	if err != nil {
-		return ts, err
-	}
-
-	return ts, err
-}
-
-func FindAllEndpoints(sc *identity.ServiceCatalog, service string) ([]identity.Endpoint, error) {
-	ces, err := sc.CatalogEntries()
-	if err != nil {
-		return nil, err
-	}
-
-	for _, ce := range ces {
-		if ce.Type == service {
-			return ce.Endpoints, nil
-		}
-	}
-
-	return nil, fmt.Errorf(service + " endpoint not found.")
-}
-
-func FindEndpointForRegion(eps []identity.Endpoint, r string) (string, error) {
-	for _, ep := range eps {
-		if ep.Region == r {
-			return ep.PublicURL, nil
-		}
-	}
-	return "", fmt.Errorf("Unknown region %s", r)
-}
-
-func CountDown(ts *testState, timeout int) (bool, int, error) {
-	if timeout < 1 {
-		return false, 0, errTimeout
-	}
-	time.Sleep(1 * time.Second)
-	timeout--
-
-	gr, err := servers.GetDetail(ts.client, ts.createdServer.Id)
-	if err != nil {
-		return false, timeout, err
-	}
-
-	ts.gottenServer, err = servers.GetServer(gr)
-	if err != nil {
-		return false, timeout, err
-	}
-
-	return true, timeout, nil
-}
-
-func CreateServer(ts *testState) error {
-	ts.serverName = RandomString("ACPTTEST", 16)
-	fmt.Printf("Attempting to create server: %s\n", ts.serverName)
-
-	ts.client = servers.NewClient(ts.ep, ts.a, ts.o)
-
-	cr, err := servers.Create(ts.client, map[string]interface{}{
-		"flavorRef": ts.flavorId,
-		"imageRef":  ts.imageId,
-		"name":      ts.serverName,
-	})
-	if err != nil {
-		return err
-	}
-
-	ts.createdServer, err = servers.GetServer(cr)
-	return err
-}
-
-func WaitForStatus(ts *testState, s string) error {
-	var (
-		inProgress bool
-		timeout    int
-		err        error
-	)
-
-	for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
-		if ts.gottenServer.Id != ts.createdServer.Id {
-			return fmt.Errorf("created server id (%s) != gotten server id (%s)", ts.createdServer.Id, ts.gottenServer.Id)
-		}
-
-		if ts.gottenServer.Status == s {
-			fmt.Printf("Server reached state %s after %d seconds (approximately)\n", s, 300-timeout)
-			break
-		}
-	}
-
-	if err == errTimeout {
-		fmt.Printf("Time out -- I'm not waiting around.\n")
-		err = nil
-	}
-
-	return err
-}
-
-func ChangeServerName(ts *testState) error {
-	var (
-		inProgress bool
-		timeout    int
-	)
-
-	ts.alternateName = RandomString("ACPTTEST", 16)
-	for ts.alternateName == ts.serverName {
-		ts.alternateName = RandomString("ACPTTEST", 16)
-	}
-	fmt.Println("Attempting to change server name")
-
-	ur, err := servers.Update(ts.client, ts.createdServer.Id, map[string]interface{}{
-		"name": ts.alternateName,
-	})
-	if err != nil {
-		return err
-	}
-
-	ts.updatedServer, err = servers.GetServer(ur)
-	if err != nil {
-		return err
-	}
-
-	if ts.updatedServer.Id != ts.createdServer.Id {
-		return fmt.Errorf("Expected updated and created server to share the same ID")
-	}
-
-	for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
-		if ts.gottenServer.Id != ts.updatedServer.Id {
-			return fmt.Errorf("Updated server ID (%s) != gotten server ID (%s)", ts.updatedServer.Id, ts.gottenServer.Id)
-		}
-
-		if ts.gottenServer.Name == ts.alternateName {
-			fmt.Printf("Server updated after %d seconds (approximately)\n", 300-timeout)
-			break
-		}
-	}
-
-	if err == errTimeout {
-		fmt.Printf("I'm not waiting around.\n")
-		err = nil
-	}
-
-	return err
-}
-
-func MakeNewPassword(oldPass string) string {
-	fmt.Println("Current password: " + oldPass)
-	randomPassword := RandomString("", 16)
-	for randomPassword == oldPass {
-		randomPassword = RandomString("", 16)
-	}
-	fmt.Println("    New password: " + randomPassword)
-	return randomPassword
-}
-
-func ChangeAdminPassword(ts *testState) error {
-	randomPassword := MakeNewPassword(ts.createdServer.AdminPass)
-
-	err := servers.ChangeAdminPassword(ts.client, ts.createdServer.Id, randomPassword)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "PASSWORD")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebootServer(ts *testState) error {
-	fmt.Println("Attempting reboot of server " + ts.createdServer.Id)
-	err := servers.Reboot(ts.client, ts.createdServer.Id, servers.OSReboot)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "REBOOT")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebuildServer(ts *testState) error {
-	fmt.Println("Attempting to rebuild server " + ts.createdServer.Id)
-
-	newPassword := MakeNewPassword(ts.createdServer.AdminPass)
-	newName := RandomString("ACPTTEST", 16)
-	sr, err := servers.Rebuild(ts.client, ts.createdServer.Id, newName, newPassword, ts.imageId, nil)
-	if err != nil {
-		return err
-	}
-
-	s, err := servers.GetServer(sr)
-	if err != nil {
-		return err
-	}
-	if s.Id != ts.createdServer.Id {
-		return fmt.Errorf("Expected rebuilt server ID of %s; got %s", ts.createdServer.Id, s.Id)
-	}
-
-	err = WaitForStatus(ts, "REBUILD")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func ResizeServer(ts *testState) error {
-	fmt.Println("Attempting to resize server " + ts.createdServer.Id)
-
-	err := servers.Resize(ts.client, ts.createdServer.Id, ts.flavorIdResize)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "RESIZE")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "VERIFY_RESIZE")
-}
-
-func ConfirmResize(ts *testState) error {
-	fmt.Println("Attempting to confirm resize for server " + ts.createdServer.Id)
-
-	err := servers.ConfirmResize(ts.client, ts.createdServer.Id)
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func RevertResize(ts *testState) error {
-	fmt.Println("Attempting to revert resize for server " + ts.createdServer.Id)
-
-	err := servers.RevertResize(ts.client, ts.createdServer.Id)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "REVERT_RESIZE")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-// randomString generates a string of given length, but random content.
-// All content will be within the ASCII graphic character set.
-// (Implementation from Even Shaw's contribution on
-// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
-func RandomString(prefix string, n int) string {
-	const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-	var bytes = make([]byte, n)
-	rand.Read(bytes)
-	for i, b := range bytes {
-		bytes[i] = alphanum[b%byte(len(alphanum))]
-	}
-	return prefix + string(bytes)
-}
diff --git a/acceptance/openstack/compute/v2/compute_test.go b/acceptance/openstack/compute/v2/compute_test.go
new file mode 100644
index 0000000..15b5163
--- /dev/null
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -0,0 +1,98 @@
+// +build acceptance
+
+package v2
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/openstack/utils"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	ao, err := utils.AuthOptions()
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := openstack.AuthenticatedClient(ao)
+	if err != nil {
+		return nil, err
+	}
+
+	return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func waitForStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error {
+	return tools.WaitFor(func() (bool, error) {
+		latest, err := servers.Get(client, server.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if latest.Status == status {
+			// Success!
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
+
+// ComputeChoices contains image and flavor selections for use by the acceptance tests.
+type ComputeChoices struct {
+	// ImageID contains the ID of a valid image.
+	ImageID string
+
+	// FlavorID contains the ID of a valid flavor.
+	FlavorID string
+
+	// FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct
+	// from FlavorID.
+	FlavorIDResize string
+}
+
+// ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables.
+// If any required state is missing, an `error` will be returned that enumerates the missing properties.
+func ComputeChoicesFromEnv() (*ComputeChoices, error) {
+	imageID := os.Getenv("OS_IMAGE_ID")
+	flavorID := os.Getenv("OS_FLAVOR_ID")
+	flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE")
+
+	missing := make([]string, 0, 3)
+	if imageID == "" {
+		missing = append(missing, "OS_IMAGE_ID")
+	}
+	if flavorID == "" {
+		missing = append(missing, "OS_FLAVOR_ID")
+	}
+	if flavorIDResize == "" {
+		missing = append(missing, "OS_FLAVOR_ID_RESIZE")
+	}
+
+	notDistinct := ""
+	if flavorID == flavorIDResize {
+		notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct."
+	}
+
+	if len(missing) > 0 || notDistinct != "" {
+		text := "You're missing some important setup:\n"
+		if len(missing) > 0 {
+			text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n"
+		}
+		if notDistinct != "" {
+			text += " * " + notDistinct + "\n"
+		}
+
+		return nil, fmt.Errorf(text)
+	}
+
+	return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize}, nil
+}
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
new file mode 100644
index 0000000..ca810bc
--- /dev/null
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -0,0 +1,57 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListFlavors(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+	pager := flavors.List(client, flavors.ListFilterOptions{})
+	count, pages := 0, 0
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		t.Logf("---")
+		pages++
+		flavors, err := flavors.ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, f := range flavors {
+			t.Logf("%s\t%s\t%d\t%d\t%d", f.ID, f.Name, f.RAM, f.Disk, f.VCPUs)
+		}
+
+		return true, nil
+	})
+
+	t.Logf("--------\n%d flavors listed on %d pages.", count, pages)
+}
+
+func TestGetFlavor(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	flavor, err := flavors.Get(client, choices.FlavorID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get flavor information: %v", err)
+	}
+
+	t.Logf("Flavor: %#v", flavor)
+}
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
new file mode 100644
index 0000000..7fca3ec
--- /dev/null
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/openstack/compute/v2/images"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListImages(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute: client: %v", err)
+	}
+
+	t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+	pager := images.List(client)
+	count, pages := 0, 0
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		images, err := images.ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, i := range images {
+			t.Logf("%s\t%s\t%s\t%s", i.ID, i.Name, i.Status, i.Created)
+		}
+
+		return true, nil
+	})
+
+	t.Logf("--------\n%d images listed on %d pages.", count, pages)
+}
diff --git a/acceptance/openstack/compute/v2/pkg.go b/acceptance/openstack/compute/v2/pkg.go
new file mode 100644
index 0000000..bb158c3
--- /dev/null
+++ b/acceptance/openstack/compute/v2/pkg.go
@@ -0,0 +1,3 @@
+// The v2 package contains acceptance tests for the Openstack Compute V2 service.
+
+package v2
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
new file mode 100644
index 0000000..620ef1b
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -0,0 +1,340 @@
+// +build acceptance
+
+package v2
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListServers(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6")
+
+	pager := servers.List(client)
+	count, pages := 0, 0
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+		t.Logf("---")
+
+		servers, err := servers.ExtractServers(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, s := range servers {
+			t.Logf("%s\t%s\t%s\t%s\t%s\t\n", s.ID, s.Name, s.Status, s.AccessIPv4, s.AccessIPv6)
+			count++
+		}
+
+		return true, nil
+	})
+
+	fmt.Printf("--------\n%d servers listed on %d pages.\n", count, pages)
+}
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) {
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	server, err := servers.Create(client, map[string]interface{}{
+		"flavorRef": choices.FlavorID,
+		"imageRef":  choices.ImageID,
+		"name":      name,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+
+	return server, err
+}
+
+func TestCreateDestroyServer(t *testing.T) {
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create server: %s\n", name)
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatalf("Unable to create server: %v", err)
+	}
+	defer func() {
+		servers.Delete(client, server.ID)
+		t.Logf("Server deleted.")
+	}()
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatalf("Unable to wait for server: %v", err)
+	}
+}
+
+func TestUpdateServer(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	alternateName := tools.RandomString("ACPTTEST", 16)
+	for alternateName == server.Name {
+		alternateName = tools.RandomString("ACPTTEST", 16)
+	}
+
+	t.Logf("Attempting to rename the server to %s.", alternateName)
+
+	updated, err := servers.Update(client, server.ID, map[string]interface{}{
+		"name": alternateName,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unable to rename server: %v", err)
+	}
+
+	if updated.ID != server.ID {
+		t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID)
+	}
+
+	err = tools.WaitFor(func() (bool, error) {
+		latest, err := servers.Get(client, updated.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		return latest.Name == alternateName, nil
+	})
+}
+
+func TestActionChangeAdminPassword(t *testing.T) {
+	t.Parallel()
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	randomPassword := tools.MakeNewPassword(server.AdminPass)
+	err = servers.ChangeAdminPassword(client, server.ID, randomPassword)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "PASSWORD"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionReboot(t *testing.T) {
+	t.Parallel()
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	err = servers.Reboot(client, server.ID, "aldhjflaskhjf")
+	if err == nil {
+		t.Fatal("Expected the SDK to provide an ArgumentError here")
+	}
+
+	t.Logf("Attempting reboot of server %s", server.ID)
+	err = servers.Reboot(client, server.ID, servers.OSReboot)
+	if err != nil {
+		t.Fatalf("Unable to reboot server: %v", err)
+	}
+
+	if err = waitForStatus(client, server, "REBOOT"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionRebuild(t *testing.T) {
+	t.Parallel()
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("Attempting to rebuild server %s", server.ID)
+
+	newPassword := tools.MakeNewPassword(server.AdminPass)
+	newName := tools.RandomString("ACPTTEST", 16)
+	rebuilt, err := servers.Rebuild(client, server.ID, newName, newPassword, choices.ImageID, nil).Extract()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if rebuilt.ID != server.ID {
+		t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID)
+	}
+
+	if err = waitForStatus(client, rebuilt, "REBUILD"); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, rebuilt, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func resizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server, choices *ComputeChoices) {
+	if err := waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+
+	t.Logf("Attempting to resize server [%s]", server.ID)
+
+	if err := servers.Resize(client, server.ID, choices.FlavorIDResize); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := waitForStatus(client, server, "VERIFY_RESIZE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionResizeConfirm(t *testing.T) {
+	t.Parallel()
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+	resizeServer(t, client, server, choices)
+
+	t.Logf("Attempting to confirm resize for server %s", server.ID)
+
+	if err = servers.ConfirmResize(client, server.ID); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestActionResizeRevert(t *testing.T) {
+	t.Parallel()
+
+	choices, err := ComputeChoicesFromEnv()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	server, err := createServer(t, client, choices)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer servers.Delete(client, server.ID)
+	resizeServer(t, client, server, choices)
+
+	t.Logf("Attempting to revert resize for server %s", server.ID)
+
+	if err := servers.RevertResize(client, server.ID); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/network_test.go b/acceptance/openstack/networking/v2/network_test.go
index 9f18597..f00edba 100644
--- a/acceptance/openstack/networking/v2/network_test.go
+++ b/acceptance/openstack/networking/v2/network_test.go
@@ -18,6 +18,7 @@
 	// Create a network
 	n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
 	th.AssertNoErr(t, err)
+	defer networks.Delete(Client, n.ID)
 	th.AssertEquals(t, n.Name, "sample_network")
 	th.AssertEquals(t, n.AdminStateUp, true)
 	networkID := n.ID
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
index 396241c..4771ebb 100644
--- a/acceptance/tools/tools.go
+++ b/acceptance/tools/tools.go
@@ -1,358 +1,41 @@
 // +build acceptance
-
 package tools
 
 import (
 	"crypto/rand"
-	"fmt"
-	"os"
-	"text/tabwriter"
+	"errors"
 	"time"
-
-	"github.com/rackspace/gophercloud"
-	"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-	"github.com/rackspace/gophercloud/openstack/utils"
 )
 
-var errTimeout = fmt.Errorf("Timeout.")
+// ErrTimeout is returned if WaitFor takes longer than 300 second to happen.
+var ErrTimeout = errors.New("Timed out")
 
-type testState struct {
-	O              gophercloud.AuthOptions
-	A              identity.AuthResults
-	SC             *identity.ServiceCatalog
-	EPs            []identity.Endpoint
-	W              *tabwriter.Writer
-	ImageId        string
-	FlavorId       string
-	Region         string
-	EP             string
-	Client         *servers.Client
-	CreatedServer  *servers.Server
-	GottenServer   *servers.Server
-	UpdatedServer  *servers.Server
-	ServerName     string
-	AlternateName  string
-	FlavorIdResize string
-}
+// WaitFor polls a predicate function once per second to wait for a certain state to arrive.
+func WaitFor(predicate func() (bool, error)) error {
+	for i := 0; i < 300; i++ {
+		time.Sleep(1 * time.Second)
 
-func SetupForList(service string) (*testState, error) {
-	var err error
-
-	ts := new(testState)
-
-	ts.O, err = utils.AuthOptions()
-	if err != nil {
-		return ts, err
-	}
-
-	client := &gophercloud.ServiceClient{Endpoint: ts.O.IdentityEndpoint + "/"}
-	ts.A, err = identity.Authenticate(client, ts.O)
-	if err != nil {
-		return ts, err
-	}
-
-	ts.SC, err = identity.GetServiceCatalog(ts.A)
-	if err != nil {
-		return ts, err
-	}
-
-	ts.EPs, err = FindAllEndpoints(ts.SC, service)
-	if err != nil {
-		return ts, err
-	}
-
-	ts.W = new(tabwriter.Writer)
-	ts.W.Init(os.Stdout, 2, 8, 2, ' ', 0)
-
-	return ts, nil
-}
-
-func SetupForCRUD() (*testState, error) {
-	ts, err := SetupForList("compute")
-	if err != nil {
-		return ts, err
-	}
-
-	ts.ImageId = os.Getenv("OS_IMAGE_ID")
-	if ts.ImageId == "" {
-		return ts, fmt.Errorf("Expected OS_IMAGE_ID environment variable to be set")
-	}
-
-	ts.FlavorId = os.Getenv("OS_FLAVOR_ID")
-	if ts.FlavorId == "" {
-		return ts, fmt.Errorf("Expected OS_FLAVOR_ID environment variable to be set")
-	}
-
-	ts.FlavorIdResize = os.Getenv("OS_FLAVOR_ID_RESIZE")
-	if ts.FlavorIdResize == "" {
-		return ts, fmt.Errorf("Expected OS_FLAVOR_ID_RESIZE environment variable to be set")
-	}
-
-	if ts.FlavorIdResize == ts.FlavorId {
-		return ts, fmt.Errorf("OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE cannot be the same")
-	}
-
-	ts.Region = os.Getenv("OS_REGION_NAME")
-	if ts.Region == "" {
-		ts.Region = ts.EPs[0].Region
-	}
-
-	ts.EP, err = FindEndpointForRegion(ts.EPs, ts.Region)
-	if err != nil {
-		return ts, err
-	}
-
-	return ts, err
-}
-
-func FindAllEndpoints(sc *identity.ServiceCatalog, service string) ([]identity.Endpoint, error) {
-	ces, err := sc.CatalogEntries()
-	if err != nil {
-		return nil, err
-	}
-
-	for _, ce := range ces {
-		if ce.Type == service {
-			return ce.Endpoints, nil
+		satisfied, err := predicate()
+		if err != nil {
+			return err
+		}
+		if satisfied {
+			return nil
 		}
 	}
-
-	return nil, fmt.Errorf(service + " endpoint not found.")
+	return ErrTimeout
 }
 
-func FindEndpointForRegion(eps []identity.Endpoint, r string) (string, error) {
-	for _, ep := range eps {
-		if ep.Region == r {
-			return ep.PublicURL, nil
-		}
-	}
-	return "", fmt.Errorf("Unknown region %s", r)
-}
-
-func CountDown(ts *testState, timeout int) (bool, int, error) {
-	if timeout < 1 {
-		return false, 0, errTimeout
-	}
-	time.Sleep(1 * time.Second)
-	timeout--
-
-	gr, err := servers.GetDetail(ts.Client, ts.CreatedServer.Id)
-	if err != nil {
-		return false, timeout, err
-	}
-
-	ts.GottenServer, err = servers.GetServer(gr)
-	if err != nil {
-		return false, timeout, err
-	}
-
-	return true, timeout, nil
-}
-
-func CreateServer(ts *testState) error {
-	ts.ServerName = RandomString("ACPTTEST", 16)
-	fmt.Printf("Attempting to create server: %s\n", ts.ServerName)
-
-	ts.Client = servers.NewClient(ts.EP, ts.A, ts.O)
-
-	cr, err := servers.Create(ts.Client, map[string]interface{}{
-		"flavorRef": ts.FlavorId,
-		"imageRef":  ts.ImageId,
-		"name":      ts.ServerName,
-	})
-	if err != nil {
-		return err
-	}
-
-	ts.CreatedServer, err = servers.GetServer(cr)
-	return err
-}
-
-func WaitForStatus(ts *testState, s string) error {
-	var (
-		inProgress bool
-		timeout    int
-		err        error
-	)
-
-	for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
-		if ts.GottenServer.Id != ts.CreatedServer.Id {
-			return fmt.Errorf("created server id (%s) != gotten server id (%s)", ts.CreatedServer.Id, ts.GottenServer.Id)
-		}
-
-		if ts.GottenServer.Status == s {
-			fmt.Printf("Server reached state %s after %d seconds (approximately)\n", s, 300-timeout)
-			break
-		}
-	}
-
-	if err == errTimeout {
-		fmt.Printf("Time out -- I'm not waiting around.\n")
-		err = nil
-	}
-
-	return err
-}
-
-func ChangeServerName(ts *testState) error {
-	var (
-		inProgress bool
-		timeout    int
-	)
-
-	ts.AlternateName = RandomString("ACPTTEST", 16)
-	for ts.AlternateName == ts.ServerName {
-		ts.AlternateName = RandomString("ACPTTEST", 16)
-	}
-	fmt.Println("Attempting to change server name")
-
-	ur, err := servers.Update(ts.Client, ts.CreatedServer.Id, map[string]interface{}{
-		"name": ts.AlternateName,
-	})
-	if err != nil {
-		return err
-	}
-
-	ts.UpdatedServer, err = servers.GetServer(ur)
-	if err != nil {
-		return err
-	}
-
-	if ts.UpdatedServer.Id != ts.CreatedServer.Id {
-		return fmt.Errorf("Expected updated and created server to share the same ID")
-	}
-
-	for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
-		if ts.GottenServer.Id != ts.UpdatedServer.Id {
-			return fmt.Errorf("Updated server ID (%s) != gotten server ID (%s)", ts.UpdatedServer.Id, ts.GottenServer.Id)
-		}
-
-		if ts.GottenServer.Name == ts.AlternateName {
-			fmt.Printf("Server updated after %d seconds (approximately)\n", 300-timeout)
-			break
-		}
-	}
-
-	if err == errTimeout {
-		fmt.Printf("I'm not waiting around.\n")
-		err = nil
-	}
-
-	return err
-}
-
+// MakeNewPassword generates a new string that's guaranteed to be different than the given one.
 func MakeNewPassword(oldPass string) string {
-	fmt.Println("Current password: " + oldPass)
 	randomPassword := RandomString("", 16)
 	for randomPassword == oldPass {
 		randomPassword = RandomString("", 16)
 	}
-	fmt.Println("    New password: " + randomPassword)
 	return randomPassword
 }
 
-func ChangeAdminPassword(ts *testState) error {
-	randomPassword := MakeNewPassword(ts.CreatedServer.AdminPass)
-
-	err := servers.ChangeAdminPassword(ts.Client, ts.CreatedServer.Id, randomPassword)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "PASSWORD")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebootServer(ts *testState) error {
-	fmt.Println("Attempting reboot of server " + ts.CreatedServer.Id)
-	err := servers.Reboot(ts.Client, ts.CreatedServer.Id, servers.OSReboot)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "REBOOT")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebuildServer(ts *testState) error {
-	fmt.Println("Attempting to rebuild server " + ts.CreatedServer.Id)
-
-	newPassword := MakeNewPassword(ts.CreatedServer.AdminPass)
-	newName := RandomString("ACPTTEST", 16)
-	sr, err := servers.Rebuild(ts.Client, ts.CreatedServer.Id, newName, newPassword, ts.ImageId, nil)
-	if err != nil {
-		return err
-	}
-
-	s, err := servers.GetServer(sr)
-	if err != nil {
-		return err
-	}
-	if s.Id != ts.CreatedServer.Id {
-		return fmt.Errorf("Expected rebuilt server ID of %s; got %s", ts.CreatedServer.Id, s.Id)
-	}
-
-	err = WaitForStatus(ts, "REBUILD")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func ResizeServer(ts *testState) error {
-	fmt.Println("Attempting to resize server " + ts.CreatedServer.Id)
-
-	err := servers.Resize(ts.Client, ts.CreatedServer.Id, ts.FlavorIdResize)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "RESIZE")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "VERIFY_RESIZE")
-}
-
-func ConfirmResize(ts *testState) error {
-	fmt.Println("Attempting to confirm resize for server " + ts.CreatedServer.Id)
-
-	err := servers.ConfirmResize(ts.Client, ts.CreatedServer.Id)
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-func RevertResize(ts *testState) error {
-	fmt.Println("Attempting to revert resize for server " + ts.CreatedServer.Id)
-
-	err := servers.RevertResize(ts.Client, ts.CreatedServer.Id)
-	if err != nil {
-		return err
-	}
-
-	err = WaitForStatus(ts, "REVERT_RESIZE")
-	if err != nil {
-		return err
-	}
-
-	return WaitForStatus(ts, "ACTIVE")
-}
-
-// randomString generates a string of given length, but random content.
+// RandomString generates a string of given length, but random content.
 // All content will be within the ASCII graphic character set.
 // (Implementation from Even Shaw's contribution on
 // http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
diff --git a/openstack/client.go b/openstack/client.go
index a73f301..eeeb809 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -294,6 +294,17 @@
 	return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
 }
 
+// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
+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
+}
+
+// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
 func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
 	eo.ApplyDefaults("network")
 	url, err := client.EndpointLocator(eo)
diff --git a/openstack/compute/v2/flavors/client.go b/openstack/compute/v2/flavors/client.go
deleted file mode 100644
index edeec66..0000000
--- a/openstack/compute/v2/flavors/client.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package flavors
-
-import (
-	"fmt"
-	"net/url"
-	"strconv"
-
-	"github.com/rackspace/gophercloud"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-type Client struct {
-	endpoint  string
-	authority identity.AuthResults
-	options   gophercloud.AuthOptions
-}
-
-func NewClient(e string, a identity.AuthResults, ao gophercloud.AuthOptions) *Client {
-	return &Client{
-		endpoint:  e,
-		authority: a,
-		options:   ao,
-	}
-}
-
-func (c *Client) getListUrl(lfo ListFilterOptions) string {
-	v := url.Values{}
-	if lfo.ChangesSince != "" {
-		v.Set("changes-since", lfo.ChangesSince)
-	}
-	if lfo.MinDisk != 0 {
-		v.Set("minDisk", strconv.Itoa(lfo.MinDisk))
-	}
-	if lfo.MinRam != 0 {
-		v.Set("minRam", strconv.Itoa(lfo.MinRam))
-	}
-	if lfo.Marker != "" {
-		v.Set("marker", lfo.Marker)
-	}
-	if lfo.Limit != 0 {
-		v.Set("limit", strconv.Itoa(lfo.Limit))
-	}
-	tail := ""
-	if len(v) > 0 {
-		tail = fmt.Sprintf("?%s", v.Encode())
-	}
-	return fmt.Sprintf("%s/flavors/detail%s", c.endpoint, tail)
-}
-
-func (c *Client) getGetUrl(id string) string {
-	return fmt.Sprintf("%s/flavors/%s", c.endpoint, id)
-}
-
-func (c *Client) getListHeaders() (map[string]string, error) {
-	t, err := identity.GetToken(c.authority)
-	if err != nil {
-		return map[string]string{}, err
-	}
-
-	return map[string]string{
-		"X-Auth-Token": t.ID,
-	}, nil
-}
diff --git a/openstack/compute/v2/flavors/flavors.go b/openstack/compute/v2/flavors/flavors.go
deleted file mode 100644
index 146bcc4..0000000
--- a/openstack/compute/v2/flavors/flavors.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package flavors
-
-import (
-	"github.com/mitchellh/mapstructure"
-	"reflect"
-)
-
-// Flavor records represent (virtual) hardware configurations for server resources in a region.
-//
-// The Id field contains the flavor's unique identifier.
-// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
-//
-// The Disk and Ram fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
-//
-// The Name field provides a human-readable moniker for the flavor.
-//
-// Swap indicates how much space is reserved for swap.
-// If not provided, this field will be set to 0.
-//
-// VCpus indicates how many (virtual) CPUs are available for this flavor.
-type Flavor struct {
-	Disk       int
-	Id         string
-	Name       string
-	Ram        int
-	RxTxFactor float64 `mapstructure:"rxtx_factor"`
-	Swap       int
-	VCpus      int
-}
-
-func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
-	if (from == reflect.String) && (to == reflect.Int) {
-		return 0, nil
-	}
-	return v, nil
-}
-
-// GetFlavors provides access to the list of flavors returned by the List function.
-func GetFlavors(lr ListResults) ([]Flavor, error) {
-	fa, ok := lr["flavors"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	fms := fa.([]interface{})
-
-	flavors := make([]Flavor, len(fms))
-	for i, fm := range fms {
-		flavorObj := fm.(map[string]interface{})
-		cfg := &mapstructure.DecoderConfig{
-			DecodeHook: defaulter,
-			Result:     &flavors[i],
-		}
-		decoder, err := mapstructure.NewDecoder(cfg)
-		if err != nil {
-			return flavors, err
-		}
-		err = decoder.Decode(flavorObj)
-		if err != nil {
-			return flavors, err
-		}
-	}
-	return flavors, nil
-}
-
-// GetFlavor provides access to the individual flavor returned by the Get function.
-func GetFlavor(gr GetResults) (*Flavor, error) {
-	f, ok := gr["flavor"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-
-	flav := new(Flavor)
-	cfg := &mapstructure.DecoderConfig{
-		DecodeHook: defaulter,
-		Result: flav,
-	}
-	decoder, err := mapstructure.NewDecoder(cfg)
-	if err != nil {
-		return flav, err
-	}
-	err = decoder.Decode(f)
-	return flav, err
-}
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index 3758206..47eb172 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -1,58 +1,48 @@
 package flavors
 
 import (
-	"fmt"
 	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
 )
 
-var ErrNotImplemented = fmt.Errorf("Flavors functionality not implemented.")
-
-type ListResults map[string]interface{}
-type GetResults map[string]interface{}
-
 // ListFilterOptions helps control the results returned by the List() function.
-// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
-// MinDisk and MinRam, if provided, elides flavors which do not meet your criteria.
 // For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
-// Marker and Limit control paging.
-// Limit instructs List to refrain from sending excessively large lists of flavors.
-// Marker instructs List where to start listing from.
 // Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
 type ListFilterOptions struct {
+
+	// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
 	ChangesSince string
-	MinDisk, MinRam int
+
+	// MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
+	MinDisk, MinRAM int
+
+	// Marker and Limit control paging.
+	// Marker instructs List where to start listing from.
 	Marker string
+
+	// Limit instructs List to refrain from sending excessively large lists of flavors.
 	Limit int
 }
 
 // List instructs OpenStack to provide a list of flavors.
 // You may provide criteria by which List curtails its results for easier processing.
 // See ListFilterOptions for more details.
-func List(c *Client, lfo ListFilterOptions) (ListResults, error) {
-	var lr ListResults
-
-	h, err := c.getListHeaders()
-	if err != nil {
-		return nil, err
+func List(client *gophercloud.ServiceClient, lfo ListFilterOptions) pagination.Pager {
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return FlavorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	err = perigee.Get(c.getListUrl(lfo), perigee.Options{
-		Results:     &lr,
-		MoreHeaders: h,
-	})
-	return lr, err
+	return pagination.NewPager(client, listURL(client, lfo), createPage)
 }
 
 // Get instructs OpenStack to provide details on a single flavor, identified by its ID.
-func Get(c *Client, id string) (GetResults, error) {
-	var gr GetResults
-	h, err := c.getListHeaders()	// same for Get Flavor API
-	if err != nil {
-		return gr, err
-	}
-	err = perigee.Get(c.getGetUrl(id), perigee.Options{
-		Results: &gr,
-		MoreHeaders: h,
+// Use ExtractFlavor to convert its result into a Flavor.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var gr GetResult
+	gr.Err = perigee.Get(flavorURL(client, id), perigee.Options{
+		Results:     &gr.Resp,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 	})
-	return gr, err
+	return gr
 }
diff --git a/openstack/compute/v2/flavors/requests_test.go b/openstack/compute/v2/flavors/requests_test.go
new file mode 100644
index 0000000..e1b6b4f
--- /dev/null
+++ b/openstack/compute/v2/flavors/requests_test.go
@@ -0,0 +1,138 @@
+package flavors
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "blerb"
+
+func serviceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+		Endpoint: testhelper.Endpoint(),
+	}
+}
+
+func TestListFlavors(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+					{
+						"flavors": [
+							{
+								"id": "1",
+								"name": "m1.tiny",
+								"disk": 1,
+								"ram": 512,
+								"vcpus": 1
+							},
+							{
+								"id": "2",
+								"name": "m2.small",
+								"disk": 10,
+								"ram": 1024,
+								"vcpus": 2
+							}
+						],
+						"flavors_links": [
+							{
+								"href": "%s/flavors/detail?marker=2",
+								"rel": "next"
+							}
+						]
+					}
+				`, testhelper.Server.URL)
+		case "2":
+			fmt.Fprintf(w, `{ "flavors": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	pages := 0
+	err := List(client, ListFilterOptions{}).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractFlavors(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Flavor{
+			Flavor{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1},
+			Flavor{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Expected %#v, but was %#v", expected, actual)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if pages != 1 {
+		t.Errorf("Expected one page, got %d", pages)
+	}
+}
+
+func TestGetFlavor(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"flavor": {
+					"id": "1",
+					"name": "m1.tiny",
+					"disk": 1,
+					"ram": 512,
+					"vcpus": 1,
+					"rxtx_factor": 1
+				}
+			}
+		`)
+	})
+
+	client := serviceClient()
+	actual, err := Get(client, "12345").Extract()
+	if err != nil {
+		t.Fatalf("Unable to get flavor: %v", err)
+	}
+
+	expected := &Flavor{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       1,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1,
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but was %#v", expected, actual)
+	}
+}
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
new file mode 100644
index 0000000..68c8f58
--- /dev/null
+++ b/openstack/compute/v2/flavors/results.go
@@ -0,0 +1,136 @@
+package flavors
+
+import (
+	"errors"
+	"reflect"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure.
+var ErrCannotInterpet = errors.New("Unable to interpret a response body.")
+
+// GetResult temporarily holds the reponse from a Get call.
+type GetResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract provides access to the individual Flavor returned by the Get function.
+func (gr GetResult) Extract() (*Flavor, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+
+	var result struct {
+		Flavor Flavor `mapstructure:"flavor"`
+	}
+
+	cfg := &mapstructure.DecoderConfig{
+		DecodeHook: defaulter,
+		Result:     &result,
+	}
+	decoder, err := mapstructure.NewDecoder(cfg)
+	if err != nil {
+		return nil, err
+	}
+	err = decoder.Decode(gr.Resp)
+	return &result.Flavor, err
+}
+
+// Flavor records represent (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+	// The Id field contains the flavor's unique identifier.
+	// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
+	ID string `mapstructure:"id"`
+
+	// The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
+	Disk int `mapstructure:"disk"`
+	RAM  int `mapstructure:"ram"`
+
+	// The Name field provides a human-readable moniker for the flavor.
+	Name string `mapstructure:"name"`
+
+	RxTxFactor float64 `mapstructure:"rxtx_factor"`
+
+	// Swap indicates how much space is reserved for swap.
+	// If not provided, this field will be set to 0.
+	Swap int `mapstructure:"swap"`
+
+	// VCPUs indicates how many (virtual) CPUs are available for this flavor.
+	VCPUs int `mapstructure:"vcpus"`
+}
+
+// FlavorPage contains a single page of the response from a List call.
+type FlavorPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty determines if a page contains any results.
+func (p FlavorPage) IsEmpty() (bool, error) {
+	flavors, err := ExtractFlavors(p)
+	if err != nil {
+		return true, err
+	}
+	return len(flavors) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (p FlavorPage) NextPageURL() (string, error) {
+	type link struct {
+		Href string `mapstructure:"href"`
+		Rel  string `mapstructure:"rel"`
+	}
+	type resp struct {
+		Links []link `mapstructure:"flavors_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	var url string
+	for _, l := range r.Links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
+
+func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
+	if (from == reflect.String) && (to == reflect.Int) {
+		return 0, nil
+	}
+	return v, nil
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+	casted := page.(FlavorPage).Body
+	var container struct {
+		Flavors []Flavor `mapstructure:"flavors"`
+	}
+
+	cfg := &mapstructure.DecoderConfig{
+		DecodeHook: defaulter,
+		Result:     &container,
+	}
+	decoder, err := mapstructure.NewDecoder(cfg)
+	if err != nil {
+		return container.Flavors, err
+	}
+	err = decoder.Decode(casted)
+	if err != nil {
+		return container.Flavors, err
+	}
+
+	return container.Flavors, nil
+}
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
new file mode 100644
index 0000000..9e5b562
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls.go
@@ -0,0 +1,37 @@
+package flavors
+
+import (
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func listURL(client *gophercloud.ServiceClient, lfo ListFilterOptions) string {
+	v := url.Values{}
+	if lfo.ChangesSince != "" {
+		v.Set("changes-since", lfo.ChangesSince)
+	}
+	if lfo.MinDisk != 0 {
+		v.Set("minDisk", strconv.Itoa(lfo.MinDisk))
+	}
+	if lfo.MinRAM != 0 {
+		v.Set("minRam", strconv.Itoa(lfo.MinRAM))
+	}
+	if lfo.Marker != "" {
+		v.Set("marker", lfo.Marker)
+	}
+	if lfo.Limit != 0 {
+		v.Set("limit", strconv.Itoa(lfo.Limit))
+	}
+	tail := ""
+	if len(v) > 0 {
+		tail = fmt.Sprintf("?%s", v.Encode())
+	}
+	return client.ServiceURL("flavors", "detail") + tail
+}
+
+func flavorURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("flavors", id)
+}
diff --git a/openstack/compute/v2/images/client.go b/openstack/compute/v2/images/client.go
deleted file mode 100644
index 6322b9c..0000000
--- a/openstack/compute/v2/images/client.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package images
-
-import (
-	"github.com/rackspace/gophercloud"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-type Client struct {
-	endpoint  string
-	authority identity.AuthResults
-	options   gophercloud.AuthOptions
-}
-
-func NewClient(e string, a identity.AuthResults, ao gophercloud.AuthOptions) *Client {
-	return &Client{
-		endpoint:  e,
-		authority: a,
-		options:   ao,
-	}
-}
-
-func (c *Client) getListUrl() string {
-	return c.endpoint + "/images/detail"
-}
-
-func (c *Client) getListHeaders() (map[string]string, error) {
-	t, err := identity.GetToken(c.authority)
-	if err != nil {
-		return map[string]string{}, err
-	}
-
-	return map[string]string{
-		"X-Auth-Token": t.ID,
-	}, nil
-}
diff --git a/openstack/compute/v2/images/images.go b/openstack/compute/v2/images/images.go
deleted file mode 100644
index c881092..0000000
--- a/openstack/compute/v2/images/images.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package images
-
-import "github.com/mitchellh/mapstructure"
-
-// Image is used for JSON (un)marshalling.
-// It provides a description of an OS image.
-//
-// The Id field contains the image's unique identifier.
-// For example, this identifier will be useful for specifying which operating system to install on a new server instance.
-//
-// The MinDisk and MinRam fields specify the minimum resources a server must provide to be able to install the image.
-//
-// The Name field provides a human-readable moniker for the OS image.
-//
-// The Progress and Status fields indicate image-creation status.
-// Any usable image will have 100% progress.
-//
-// The Updated field indicates the last time this image was changed.
-type Image struct {
-	Created  string
-	Id       string
-	MinDisk  int
-	MinRam   int
-	Name     string
-	Progress int
-	Status   string
-	Updated  string
-}
-
-func GetImages(lr ListResults) ([]Image, error) {
-	ia, ok := lr["images"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	ims := ia.([]interface{})
-
-	images := make([]Image, len(ims))
-	for i, im := range ims {
-		imageObj := im.(map[string]interface{})
-		err := mapstructure.Decode(imageObj, &images[i])
-		if err != nil {
-			return images, err
-		}
-	}
-	return images, nil
-}
-
-func GetImage(ir ImageResults) (Image, error) {
-	image := Image{}
-	err := mapstructure.Decode(ir, &image)
-	return image, err
-}
diff --git a/openstack/compute/v2/images/images_test.go b/openstack/compute/v2/images/images_test.go
deleted file mode 100644
index ee3b79e..0000000
--- a/openstack/compute/v2/images/images_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package images
-
-import (
-	"encoding/json"
-	"testing"
-)
-
-const (
-	// This example was taken from: http://docs.openstack.org/api/openstack-compute/2/content/Rebuild_Server-d1e3538.html
-
-	simpleImageJson = `{
-		"id": "52415800-8b69-11e0-9b19-734f6f006e54",
-		"name": "CentOS 5.2",
-		"links": [{
-			"rel": "self",
-			"href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
-		},{
-			"rel": "bookmark",
-			"href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
-		}]
-	}`
-)
-
-func TestGetImage(t *testing.T) {
-	var simpleImageMap map[string]interface{}
-	err := json.Unmarshal([]byte(simpleImageJson), &simpleImageMap)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	image, err := GetImage(simpleImageMap)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if image.Id != "52415800-8b69-11e0-9b19-734f6f006e54" {
-		t.Fatal("I expected an image ID of 52415800-8b69-11e0-9b19-734f6f006e54; got " + image.Id)
-	}
-
-	if image.Name != "CentOS 5.2" {
-		t.Fatal("I expected an image name of CentOS 5.2; got " + image.Name)
-	}
-}
diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go
index 79783c4..a887cc6 100644
--- a/openstack/compute/v2/images/requests.go
+++ b/openstack/compute/v2/images/requests.go
@@ -1,26 +1,28 @@
 package images
 
 import (
-	"fmt"
 	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
 )
 
-var ErrNotImplemented = fmt.Errorf("Images functionality not implemented.")
-
-type ListResults map[string]interface{}
-type ImageResults map[string]interface{}
-
-func List(c *Client) (ListResults, error) {
-	var lr ListResults
-
-	h, err := c.getListHeaders()
-	if err != nil {
-		return nil, err
+// List enumerates the available images.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return ImagePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	err = perigee.Get(c.getListUrl(), perigee.Options{
-		Results:     &lr,
-		MoreHeaders: h,
+	return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// Get acquires additional detail about a specific image by ID.
+// Use ExtractImage() to intepret the result as an openstack Image.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = perigee.Request("GET", imageURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		Results:     &result.Resp,
+		OkCodes:     []int{200},
 	})
-	return lr, err
+	return result
 }
diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go
new file mode 100644
index 0000000..396c21f
--- /dev/null
+++ b/openstack/compute/v2/images/requests_test.go
@@ -0,0 +1,168 @@
+package images
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "aaaaaa"
+
+func serviceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+		Endpoint: testhelper.Endpoint(),
+	}
+}
+
+func TestListImages(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, `
+				{
+					"images": [
+						{
+							"status": "ACTIVE",
+							"updated": "2014-09-23T12:54:56Z",
+							"id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+							"OS-EXT-IMG-SIZE:size": 476704768,
+							"name": "F17-x86_64-cfntools",
+							"created": "2014-09-23T12:54:52Z",
+							"minDisk": 0,
+							"progress": 100,
+							"minRam": 0,
+							"metadata": {}
+						},
+						{
+							"status": "ACTIVE",
+							"updated": "2014-09-23T12:51:43Z",
+							"id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+							"OS-EXT-IMG-SIZE:size": 13167616,
+							"name": "cirros-0.3.2-x86_64-disk",
+							"created": "2014-09-23T12:51:42Z",
+							"minDisk": 0,
+							"progress": 100,
+							"minRam": 0,
+							"metadata": {}
+						}
+					]
+				}
+			`)
+		case "2":
+			fmt.Fprintf(w, `{ "images": [] }`)
+		default:
+			t.Fatalf("Unexpected marker: [%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	pages := 0
+	err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractImages(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []Image{
+			Image{
+				ID:       "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+				Name:     "F17-x86_64-cfntools",
+				Created:  "2014-09-23T12:54:52Z",
+				Updated:  "2014-09-23T12:54:56Z",
+				MinDisk:  0,
+				MinRAM:   0,
+				Progress: 100,
+				Status:   "ACTIVE",
+			},
+			Image{
+				ID:       "f90f6034-2570-4974-8351-6b49732ef2eb",
+				Name:     "cirros-0.3.2-x86_64-disk",
+				Created:  "2014-09-23T12:51:42Z",
+				Updated:  "2014-09-23T12:51:43Z",
+				MinDisk:  0,
+				MinRAM:   0,
+				Progress: 100,
+				Status:   "ACTIVE",
+			},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual)
+		}
+
+		return false, nil
+	})
+
+	if err != nil {
+		t.Fatalf("EachPage error: %v", err)
+	}
+	if pages != 1 {
+		t.Errorf("Expected one page, got %d", pages)
+	}
+}
+
+func TestGetImage(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"image": {
+					"status": "ACTIVE",
+					"updated": "2014-09-23T12:54:56Z",
+					"id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+					"OS-EXT-IMG-SIZE:size": 476704768,
+					"name": "F17-x86_64-cfntools",
+					"created": "2014-09-23T12:54:52Z",
+					"minDisk": 0,
+					"progress": 100,
+					"minRam": 0,
+					"metadata": {}
+				}
+			}
+		`)
+	})
+
+	client := serviceClient()
+	actual, err := Get(client, "12345678").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Get: %v", err)
+	}
+
+	expected := &Image{
+		Status:   "ACTIVE",
+		Updated:  "2014-09-23T12:54:56Z",
+		ID:       "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+		Name:     "F17-x86_64-cfntools",
+		Created:  "2014-09-23T12:54:52Z",
+		MinDisk:  0,
+		Progress: 100,
+		MinRAM:   0,
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but got %#v", expected, actual)
+	}
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..f93c90c
--- /dev/null
+++ b/openstack/compute/v2/images/results.go
@@ -0,0 +1,104 @@
+package images
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily stores a Get response.
+type GetResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult as an Image.
+func (gr GetResult) Extract() (*Image, error) {
+	if gr.Err != nil {
+		return nil, gr.Err
+	}
+
+	var decoded struct {
+		Image Image `mapstructure:"image"`
+	}
+
+	err := mapstructure.Decode(gr.Resp, &decoded)
+	return &decoded.Image, err
+}
+
+// Image is used for JSON (un)marshalling.
+// It provides a description of an OS image.
+type Image struct {
+	// ID contains the image's unique identifier.
+	ID string
+
+	Created string
+
+	// MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
+	MinDisk int
+	MinRAM  int
+
+	// Name provides a human-readable moniker for the OS image.
+	Name string
+
+	// The Progress and Status fields indicate image-creation status.
+	// Any usable image will have 100% progress.
+	Progress int
+	Status   string
+
+	Updated string
+}
+
+// ImagePage contains a single page of results from a List operation.
+// Use ExtractImages to convert it into a slice of usable structs.
+type ImagePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Image results.
+func (page ImagePage) IsEmpty() (bool, error) {
+	images, err := ExtractImages(page)
+	if err != nil {
+		return true, err
+	}
+	return len(images) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ImagePage) NextPageURL() (string, error) {
+	type link struct {
+		Href string `mapstructure:"href"`
+		Rel  string `mapstructure:"rel"`
+	}
+	type resp struct {
+		Links []link `mapstructure:"images_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	var url string
+	for _, l := range r.Links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
+
+// ExtractImages converts a page of List results into a slice of usable Image structs.
+func ExtractImages(page pagination.Page) ([]Image, error) {
+	casted := page.(ImagePage).Body
+	var results struct {
+		Images []Image `mapstructure:"images"`
+	}
+
+	err := mapstructure.Decode(casted, &results)
+	return results.Images, err
+}
diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go
new file mode 100644
index 0000000..4ae2269
--- /dev/null
+++ b/openstack/compute/v2/images/urls.go
@@ -0,0 +1,11 @@
+package images
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("images", "detail")
+}
+
+func imageURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("images", id)
+}
diff --git a/openstack/compute/v2/servers/client.go b/openstack/compute/v2/servers/client.go
deleted file mode 100644
index 8c79f94..0000000
--- a/openstack/compute/v2/servers/client.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package servers
-
-import (
-	"fmt"
-
-	"github.com/rackspace/gophercloud"
-	identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-// Client abstracts the connection information needed to make API requests for OpenStack compute endpoints.
-type Client struct {
-	endpoint  string
-	authority identity.AuthResults
-	options   gophercloud.AuthOptions
-	token     *identity.Token
-}
-
-// NewClient creates a new Client structure to use when issuing requests to the server.
-func NewClient(e string, a identity.AuthResults, o gophercloud.AuthOptions) *Client {
-	return &Client{
-		endpoint:  e,
-		authority: a,
-		options:   o,
-	}
-}
-
-func (c *Client) getListUrl() string {
-	return fmt.Sprintf("%s/servers/detail", c.endpoint)
-}
-
-func (c *Client) getCreateUrl() string {
-	return fmt.Sprintf("%s/servers", c.endpoint)
-}
-
-func (c *Client) getDeleteUrl(id string) string {
-	return fmt.Sprintf("%s/servers/%s", c.endpoint, id)
-}
-
-func (c *Client) getDetailUrl(id string) string {
-	return c.getDeleteUrl(id)
-}
-
-func (c *Client) getUpdateUrl(id string) string {
-	return c.getDeleteUrl(id)
-}
-
-func (c *Client) getActionUrl(id string) string {
-	return fmt.Sprintf("%s/servers/%s/action", c.endpoint, id)
-}
-
-func (c *Client) getListHeaders() (map[string]string, error) {
-	t, err := c.getAuthToken()
-	if err != nil {
-		return map[string]string{}, err
-	}
-
-	return map[string]string{
-		"X-Auth-Token": t,
-	}, nil
-}
-
-func (c *Client) getCreateHeaders() (map[string]string, error) {
-	return c.getListHeaders()
-}
-
-func (c *Client) getDeleteHeaders() (map[string]string, error) {
-	return c.getListHeaders()
-}
-
-func (c *Client) getDetailHeaders() (map[string]string, error) {
-	return c.getListHeaders()
-}
-
-func (c *Client) getUpdateHeaders() (map[string]string, error) {
-	return c.getListHeaders()
-}
-
-func (c *Client) getActionHeaders() (map[string]string, error) {
-	return c.getListHeaders()
-}
-
-func (c *Client) getAuthToken() (string, error) {
-	var err error
-
-	if c.token == nil {
-		c.token, err = identity.GetToken(c.authority)
-		if err != nil {
-			return "", err
-		}
-	}
-
-	return c.token.ID, err
-}
diff --git a/openstack/compute/v2/servers/data_test.go b/openstack/compute/v2/servers/data_test.go
new file mode 100644
index 0000000..d3a0ee0
--- /dev/null
+++ b/openstack/compute/v2/servers/data_test.go
@@ -0,0 +1,328 @@
+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/requests.go b/openstack/compute/v2/servers/requests.go
index 95818da..6c72a0a 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -2,123 +2,80 @@
 
 import (
 	"fmt"
+
 	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
 )
 
-// ListResult abstracts the raw results of making a List() request against the
-// API.  As OpenStack extensions may freely alter the response bodies of
-// structures returned to the client, you may only safely access the data
-// provided through separate, type-safe accessors or methods.
-type ListResult map[string]interface{}
-
-// ServerResult abstracts a single server description,
-// as returned by the OpenStack provider.
-// As OpenStack extensions may freely alter the response bodies of the
-// structures returned to the client,
-// you may only safely access the data provided through
-// separate, type-safe accessors or methods.
-type ServerResult map[string]interface{}
-
 // List makes a request against the API to list servers accessible to you.
-func List(c *Client) (ListResult, error) {
-	var lr ListResult
-
-	h, err := c.getListHeaders()
-	if err != nil {
-		return nil, err
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	err = perigee.Get(c.getListUrl(), perigee.Options{
-		Results:     &lr,
-		MoreHeaders: h,
-	})
-	return lr, err
+	return pagination.NewPager(client, detailURL(client), createPage)
 }
 
 // Create requests a server to be provisioned to the user in the current tenant.
-func Create(c *Client, opts map[string]interface{}) (ServerResult, error) {
-	var sr ServerResult
-
-	h, err := c.getCreateHeaders()
-	if err != nil {
-		return nil, err
-	}
-
-	err = perigee.Post(c.getCreateUrl(), perigee.Options{
-		Results: &sr,
+func Create(client *gophercloud.ServiceClient, opts map[string]interface{}) CreateResult {
+	var result CreateResult
+	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+		Results: &result.Resp,
 		ReqBody: map[string]interface{}{
 			"server": opts,
 		},
-		MoreHeaders: h,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
-	return sr, err
+	return result
 }
 
 // Delete requests that a server previously provisioned be removed from your account.
-func Delete(c *Client, id string) error {
-	h, err := c.getDeleteHeaders()
-	if err != nil {
-		return err
-	}
-
-	err = perigee.Delete(c.getDeleteUrl(id), perigee.Options{
-		MoreHeaders: h,
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", serverURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
 	return err
 }
 
-// GetDetail requests details on a single server, by ID.
-func GetDetail(c *Client, id string) (ServerResult, error) {
-	var sr ServerResult
-
-	h, err := c.getDetailHeaders()
-	if err != nil {
-		return nil, err
-	}
-
-	err = perigee.Get(c.getDetailUrl(id), perigee.Options{
-		Results:     &sr,
-		MoreHeaders: h,
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var result GetResult
+	_, result.Err = perigee.Request("GET", serverURL(client, id), perigee.Options{
+		Results:     &result.Resp,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 	})
-	return sr, err
+	return result
 }
 
 // Update requests that various attributes of the indicated server be changed.
-func Update(c *Client, id string, opts map[string]interface{}) (ServerResult, error) {
-	var sr ServerResult
-
-	h, err := c.getUpdateHeaders()
-	if err != nil {
-		return nil, err
-	}
-
-	err = perigee.Put(c.getUpdateUrl(id), perigee.Options{
-		Results: &sr,
+func Update(client *gophercloud.ServiceClient, id string, opts map[string]interface{}) UpdateResult {
+	var result UpdateResult
+	_, result.Err = perigee.Request("PUT", serverURL(client, id), perigee.Options{
+		Results: &result.Resp,
 		ReqBody: map[string]interface{}{
 			"server": opts,
 		},
-		MoreHeaders: h,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 	})
-	return sr, err
+	return result
 }
 
-// ChangeAdminPassword alters the administrator or root password for a specified
-// server.
-func ChangeAdminPassword(c *Client, id, newPassword string) error {
-	h, err := c.getActionHeaders()
-	if err != nil {
-		return err
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) error {
+	var req struct {
+		ChangePassword struct {
+			AdminPass string `json:"adminPass"`
+		} `json:"changePassword"`
 	}
 
-	err = perigee.Post(c.getActionUrl(id), perigee.Options{
-		ReqBody: struct {
-			C map[string]string `json:"changePassword"`
-		}{
-			map[string]string{"adminPass": newPassword},
-		},
-		MoreHeaders: h,
+	req.ChangePassword.AdminPass = newPassword
+
+	_, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+		ReqBody:     req,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
 	return err
@@ -150,28 +107,29 @@
 	return e.Error()
 }
 
+// RebootMethod describes the mechanisms by which a server reboot can be requested.
+type RebootMethod string
+
 // These constants determine how a server should be rebooted.
 // See the Reboot() function for further details.
 const (
-	SoftReboot = "SOFT"
-	HardReboot = "HARD"
-	OSReboot   = SoftReboot
-	PowerCycle = HardReboot
+	SoftReboot RebootMethod = "SOFT"
+	HardReboot RebootMethod = "HARD"
+	OSReboot                = SoftReboot
+	PowerCycle              = HardReboot
 )
 
 // Reboot requests that a given server reboot.
 // Two methods exist for rebooting a server:
 //
-// 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 while, power is
-// restored or the VM instance restarted.
+// 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 while, power is restored or the VM instance restarted.
 //
-// SoftReboot (aka OSReboot).  This approach 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 wasking Windows to restart the
-// machine.
-func Reboot(c *Client, id, how string) error {
+// 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 RebootMethod) error {
 	if (how != SoftReboot) && (how != HardReboot) {
 		return &ErrArgument{
 			Function: "Reboot",
@@ -180,68 +138,64 @@
 		}
 	}
 
-	h, err := c.getActionHeaders()
-	if err != nil {
-		return err
-	}
-
-	err = perigee.Post(c.getActionUrl(id), perigee.Options{
+	_, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
 		ReqBody: struct {
 			C map[string]string `json:"reboot"`
 		}{
-			map[string]string{"type": how},
+			map[string]string{"type": string(how)},
 		},
-		MoreHeaders: h,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
 	return err
 }
 
-// Rebuild requests that the Openstack provider reprovision the
-// server.  The rebuild will need to know the server's name and
-// new image reference or ID.  In addition, and unlike building
-// a server with Create(), you must provide an administrator
-// password.
+// Rebuild requests that the Openstack provider reprovision the server.
+// The rebuild will need to know the server's name and new image reference or ID.
+// In addition, and unlike building a server with Create(), you must provide an administrator password.
 //
 // Additional options may be specified with the additional map.
 // This function treats a nil map the same as an empty map.
 //
-// Rebuild returns a server result as though you had called
-// GetDetail() on the server's ID.  The information, however,
-// refers to the new server, not the old.
-func Rebuild(c *Client, id, name, password, imageRef string, additional map[string]interface{}) (ServerResult, error) {
-	var sr ServerResult
+// Rebuild returns a server result as though you had called GetDetail() on the server's ID.
+// The information, however, refers to the new server, not the old.
+func Rebuild(client *gophercloud.ServiceClient, id, name, password, imageRef string, additional map[string]interface{}) RebuildResult {
+	var result RebuildResult
 
 	if id == "" {
-		return sr, &ErrArgument{
+		result.Err = &ErrArgument{
 			Function: "Rebuild",
 			Argument: "id",
 			Value:    "",
 		}
+		return result
 	}
 
 	if name == "" {
-		return sr, &ErrArgument{
+		result.Err = &ErrArgument{
 			Function: "Rebuild",
 			Argument: "name",
 			Value:    "",
 		}
+		return result
 	}
 
 	if password == "" {
-		return sr, &ErrArgument{
+		result.Err = &ErrArgument{
 			Function: "Rebuild",
 			Argument: "password",
 			Value:    "",
 		}
+		return result
 	}
 
 	if imageRef == "" {
-		return sr, &ErrArgument{
+		result.Err = &ErrArgument{
 			Function: "Rebuild",
 			Argument: "imageRef",
 			Value:    "",
 		}
+		return result
 	}
 
 	if additional == nil {
@@ -252,43 +206,34 @@
 	additional["imageRef"] = imageRef
 	additional["adminPass"] = password
 
-	h, err := c.getActionHeaders()
-	if err != nil {
-		return sr, err
-	}
-
-	err = perigee.Post(c.getActionUrl(id), perigee.Options{
+	_, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
 		ReqBody: struct {
 			R map[string]interface{} `json:"rebuild"`
 		}{
 			additional,
 		},
-		Results:     &sr,
-		MoreHeaders: h,
+		Results:     &result.Resp,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
-	return sr, err
+	return result
 }
 
 // Resize instructs the provider to change the flavor of the server.
-// Note that this implies rebuilding it.  Unfortunately, one cannot pass rebuild parameters to the resize function.
+// Note that this implies rebuilding it.
+// Unfortunately, one cannot pass rebuild parameters to the resize function.
 // When the resize completes, the server will be in RESIZE_VERIFY state.
 // While in this state, you can explore the use of the new server's configuration.
 // If you like it, call ConfirmResize() to commit the resize permanently.
 // Otherwise, call RevertResize() to restore the old configuration.
-func Resize(c *Client, id, flavorRef string) error {
-	h, err := c.getActionHeaders()
-	if err != nil {
-		return err
-	}
-
-	err = perigee.Post(c.getActionUrl(id), perigee.Options{
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) error {
+	_, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
 		ReqBody: struct {
 			R map[string]interface{} `json:"resize"`
 		}{
 			map[string]interface{}{"flavorRef": flavorRef},
 		},
-		MoreHeaders: h,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
 	return err
@@ -296,15 +241,10 @@
 
 // ConfirmResize confirms a previous resize operation on a server.
 // See Resize() for more details.
-func ConfirmResize(c *Client, id string) error {
-	h, err := c.getActionHeaders()
-	if err != nil {
-		return err
-	}
-
-	err = perigee.Post(c.getActionUrl(id), perigee.Options{
+func ConfirmResize(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
 		ReqBody:     map[string]interface{}{"confirmResize": nil},
-		MoreHeaders: h,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
 	return err
@@ -312,15 +252,10 @@
 
 // RevertResize cancels a previous resize operation on a server.
 // See Resize() for more details.
-func RevertResize(c *Client, id string) error {
-	h, err := c.getActionHeaders()
-	if err != nil {
-		return err
-	}
-
-	err = perigee.Post(c.getActionUrl(id), perigee.Options{
+func RevertResize(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
 		ReqBody:     map[string]interface{}{"revertResize": nil},
-		MoreHeaders: h,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
 	return err
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..fa1a2f5
--- /dev/null
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -0,0 +1,296 @@
+package servers
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "bzbzbzbzbz"
+
+func serviceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+		Endpoint: testhelper.Endpoint(),
+	}
+}
+
+func TestListServers(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.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", tokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		r.ParseForm()
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			fmt.Fprintf(w, serverListBody)
+		case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
+			fmt.Fprintf(w, `{ "servers": [] }`)
+		default:
+			t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
+		}
+	})
+
+	client := serviceClient()
+	pages := 0
+	err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractServers(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 servers, got %d", len(actual))
+		}
+		equalServers(t, serverHerp, actual[0])
+		equalServers(t, serverDerp, actual[1])
+
+		return true, nil
+	})
+
+	if err != nil {
+		t.Fatalf("Unexpected error from EachPage: %v", err)
+	}
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestCreateServer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "POST")
+		testhelper.TestHeader(t, r, "X-Auth-Token", 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 := serviceClient()
+	actual, err := Create(client, map[string]interface{}{
+		"name":      "derp",
+		"imageRef":  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		"flavorRef": "1",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Create error: %v", err)
+	}
+
+	equalServers(t, serverDerp, *actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "DELETE")
+		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	err := Delete(client, "asdfasdfasdf")
+	if err != nil {
+		t.Fatalf("Unexpected Delete error: %v", err)
+	}
+}
+
+func TestGetServer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.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", tokenID)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+
+		fmt.Fprintf(w, singleServerBody)
+	})
+
+	client := serviceClient()
+	actual, err := Get(client, "1234asdf").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	equalServers(t, serverDerp, *actual)
+}
+
+func TestUpdateServer(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.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", tokenID)
+		testhelper.TestHeader(t, r, "Accept", "application/json")
+		testhelper.TestHeader(t, r, "Content-Type", "application/json")
+		testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+
+		fmt.Fprintf(w, singleServerBody)
+	})
+
+	client := serviceClient()
+	actual, err := Update(client, "1234asdf", map[string]interface{}{
+		"name": "new-name",
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Update error: %v", err)
+	}
+
+	equalServers(t, serverDerp, *actual)
+}
+
+func TestChangeServerAdminPassword(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", tokenID)
+		testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	client := serviceClient()
+	err := ChangeAdminPassword(client, "1234asdf", "new-password")
+	if err != nil {
+		t.Errorf("Unexpected ChangeAdminPassword error: %v", err)
+	}
+}
+
+func TestRebootServer(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", tokenID)
+		testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	client := serviceClient()
+	err := Reboot(client, "1234asdf", SoftReboot)
+	if err != nil {
+		t.Errorf("Unexpected Reboot error: %v", 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", 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)
+	})
+
+	client := serviceClient()
+	actual, err := Rebuild(client,
+		"1234asdf", "new-name", "swordfish",
+		"http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+		map[string]interface{}{"accessIPv4": "1.2.3.4"},
+	).Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Rebuild error: %v", err)
+	}
+
+	equalServers(t, serverDerp, *actual)
+}
+
+func TestResizeServer(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", tokenID)
+		testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	client := serviceClient()
+	err := Resize(client, "1234asdf", "2")
+	if err != nil {
+		t.Errorf("Unexpected Reboot error: %v", err)
+	}
+}
+
+func TestConfirmResize(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", tokenID)
+		testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	client := serviceClient()
+	err := ConfirmResize(client, "1234asdf")
+	if err != nil {
+		t.Errorf("Unexpected ConfirmResize error: %v", err)
+	}
+}
+
+func TestRevertResize(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", tokenID)
+		testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	client := serviceClient()
+	err := RevertResize(client, "1234asdf")
+	if err != nil {
+		t.Errorf("Unexpected RevertResize error: %v", err)
+	}
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..dd2e651
--- /dev/null
+++ b/openstack/compute/v2/servers/results.go
@@ -0,0 +1,155 @@
+package servers
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type serverResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract interprets any serverResult as a Server, if possible.
+func (r serverResult) Extract() (*Server, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Server Server `mapstructure:"server"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &response)
+	return &response.Server, err
+}
+
+// CreateResult temporarily contains the response from a Create call.
+type CreateResult struct {
+	serverResult
+}
+
+// GetResult temporarily contains the response from a Get call.
+type GetResult struct {
+	serverResult
+}
+
+// UpdateResult temporarily contains the response from an Update call.
+type UpdateResult struct {
+	serverResult
+}
+
+// RebuildResult temporarily contains the response from a Rebuild call.
+type RebuildResult struct {
+	serverResult
+}
+
+// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
+type Server struct {
+	// ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
+	ID string
+
+	// TenantID identifies the tenant owning this server resource.
+	TenantID string `mapstructure:"tenant_id"`
+
+	// UserID uniquely identifies the user account owning the tenant.
+	UserID string `mapstructure:"user_id"`
+
+	// Name contains the human-readable name for the server.
+	Name string
+
+	// Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
+	Updated string
+	Created string
+
+	HostID string
+
+	// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
+	Status string
+
+	// Progress ranges from 0..100.
+	// A request made against the server completes only once Progress reaches 100.
+	Progress int
+
+	// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
+	AccessIPv4 string
+	AccessIPv6 string
+
+	// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
+	Image map[string]interface{}
+
+	// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
+	Flavor map[string]interface{}
+
+	// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
+	Addresses map[string]interface{}
+
+	// Metadata includes a list of all user-specified key-value pairs attached to the server.
+	Metadata map[string]interface{}
+
+	// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+	Links []interface{}
+
+	// KeyName indicates which public key was injected into the server on launch.
+	KeyName string `mapstructure:"keyname"`
+
+	// 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"`
+}
+
+// ServerPage abstracts the raw results of making a List() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractServers call.
+type ServerPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (page ServerPage) IsEmpty() (bool, error) {
+	servers, err := ExtractServers(page)
+	if err != nil {
+		return true, err
+	}
+	return len(servers) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ServerPage) NextPageURL() (string, error) {
+	type link struct {
+		Href string `mapstructure:"href"`
+		Rel  string `mapstructure:"rel"`
+	}
+	type resp struct {
+		Links []link `mapstructure:"servers_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(page.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	var url string
+	for _, l := range r.Links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]Server, error) {
+	casted := page.(ServerPage).Body
+
+	var response struct {
+		Servers []Server `mapstructure:"servers"`
+	}
+	err := mapstructure.Decode(casted, &response)
+	return response.Servers, err
+}
diff --git a/openstack/compute/v2/servers/servers.go b/openstack/compute/v2/servers/servers.go
deleted file mode 100644
index 28d66d0..0000000
--- a/openstack/compute/v2/servers/servers.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package servers
-
-import (
-	"fmt"
-	"github.com/mitchellh/mapstructure"
-)
-
-// ErrNotImplemented indicates a failure to discover a feature of the response from the API.
-// E.g., a missing server field, a missing extension, etc.
-var ErrNotImplemented = fmt.Errorf("Compute Servers feature not implemented.")
-
-// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
-//
-// Id uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
-//
-// TenantId identifies the tenant owning this server resource.
-//
-// UserId uniquely identifies the user account owning the tenant.
-//
-// Name contains the human-readable name for the server.
-//
-// Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
-//
-// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
-//
-// Progress ranges from 0..100.  A request made against the server completes only once Progress reaches 100.
-//
-// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
-//
-// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
-//
-// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
-//
-// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
-//
-// Metadata includes a list of all user-specified key-value pairs attached to the server.
-//
-// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
-//
-// 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.
-type Server struct {
-	Id         string
-	TenantId   string `mapstructure:tenant_id`
-	UserId     string `mapstructure:user_id`
-	Name       string
-	Updated    string
-	Created    string
-	HostId     string
-	Status     string
-	Progress   int
-	AccessIPv4 string
-	AccessIPv6 string
-	Image      map[string]interface{}
-	Flavor     map[string]interface{}
-	Addresses  map[string]interface{}
-	Metadata   map[string]interface{}
-	Links      []interface{}
-	AdminPass  string `mapstructure:adminPass`
-}
-
-// GetServers interprets the result of a List() call, producing a slice of Server entities.
-func GetServers(lr ListResult) ([]Server, error) {
-	sa, ok := lr["servers"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	serversArray := sa.([]interface{})
-
-	servers := make([]Server, len(serversArray))
-	for i, so := range serversArray {
-		serverObj := so.(map[string]interface{})
-		err := mapstructure.Decode(serverObj, &servers[i])
-		if err != nil {
-			return servers, err
-		}
-	}
-
-	return servers, nil
-}
-
-// GetServer interprets the result of a call expected to return data on a single server.
-func GetServer(sr ServerResult) (*Server, error) {
-	so, ok := sr["server"]
-	if !ok {
-		return nil, ErrNotImplemented
-	}
-	serverObj := so.(map[string]interface{})
-	s := new(Server)
-	err := mapstructure.Decode(serverObj, s)
-	return s, err
-}
diff --git a/openstack/compute/v2/servers/servers_test.go b/openstack/compute/v2/servers/servers_test.go
index b4a094c..590fc8b 100644
--- a/openstack/compute/v2/servers/servers_test.go
+++ b/openstack/compute/v2/servers/servers_test.go
@@ -1,188 +1,65 @@
 package servers
 
 import (
-	"encoding/json"
+	"reflect"
 	"testing"
 )
 
-// Taken from: http://docs.openstack.org/api/openstack-compute/2/content/List_Servers-d1e2078.html
-const goodListServersResult = `{
-"servers": [
-{
-"id": "52415800-8b69-11e0-9b19-734f6af67565",
-"tenant_id": "1234",
-"user_id": "5678",
-"name": "sample-server",
-"updated": "2010-10-10T12:00:00Z",
-"created": "2010-08-10T12:00:00Z",
-"hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
-"status": "BUILD",
-"progress": 60,
-"accessIPv4" : "67.23.10.132",
-"accessIPv6" : "::babe:67.23.10.132",
-"image" : {
-"id": "52415800-8b69-11e0-9b19-734f6f006e54",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
-}
-]
-},
-"flavor" : {
-"id": "52415800-8b69-11e0-9b19-734f216543fd",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-}
-]
-},
-"addresses": {
-"public" : [
-{
-"version": 4,
-"addr": "67.23.10.132"
-},
-{
-"version": 6,
-"addr": "::babe:67.23.10.132"
-},
-{
-"version": 4,
-"addr": "67.23.10.131"
-},
-{
-"version": 6,
-"addr": "::babe:4317:0A83"
-}
-],
-"private" : [
-{
-"version": 4,
-"addr": "10.176.42.16"
-},
-{
-"version": 6,
-"addr": "::babe:10.176.42.16"
-}
-]
-},
-"metadata": {
-"Server Label": "Web Head 1",
-"Image Version": "2.1"
-},
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/servers/52415800-8b69-11e0-9b19-734f6af67565"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/servers/52415800-8b69-11e0-9b19-734f6af67565"
-}
-]
-},
-{
-"id": "52415800-8b69-11e0-9b19-734f1f1350e5",
-"user_id": "5678",
-"name": "sample-server2",
-"tenant_id": "1234",
-"updated": "2010-10-10T12:00:00Z",
-"created": "2010-08-10T12:00:00Z",
-"hostId": "9e107d9d372bb6826bd81d3542a419d6",
-"status": "ACTIVE",
-"accessIPv4" : "67.23.10.133",
-"accessIPv6" : "::babe:67.23.10.133",
-"image" : {
-"id": "52415800-8b69-11e0-9b19-734f5736d2a2",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2"
-}
-]
-},
-"flavor" : {
-"id": "52415800-8b69-11e0-9b19-734f216543fd",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-}
-]
-},
-"addresses": {
-"public" : [
-{
-"version": 4,
-"addr": "67.23.10.133"
-},
-{
-"version": 6,
-"addr": "::babe:67.23.10.133"
-}
-],
-"private" : [
-{
-"version": 4,
-"addr": "10.176.42.17"
-},
-{
-"version": 6,
-"addr": "::babe:10.176.42.17"
-}
-]
-},
-"metadata": {
-"Server Label": "DB 1"
-},
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/servers/52415800-8b69-11e0-9b19-734f1f1350e5"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/servers/52415800-8b69-11e0-9b19-734f1f1350e5"
-}
-]
-}
-]
-}`
-
-func TestGetServer(t *testing.T) {
-	var listResults map[string]interface{}
-	err := json.Unmarshal([]byte(goodListServersResult), &listResults)
-	if err != nil {
-		t.Error(err)
-		return
+// 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)
 	}
-
-	svrs, err := GetServers(listResults)
-	if err != nil {
-		t.Error(err)
-		return
+	if expected.TenantID != actual.TenantID {
+		t.Errorf("TenantID differs. expected=[%s], actual=[%s]", expected.TenantID, actual.TenantID)
 	}
-
-	if len(svrs) != 2 {
-		t.Errorf("Expected 2 servers; got %d", len(svrs))
-		return
+	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/urls.go b/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..52be73e
--- /dev/null
+++ b/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,19 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("servers")
+}
+
+func detailURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("servers", "detail")
+}
+
+func serverURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id)
+}
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "action")
+}
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
index 71be65d..186d0fc 100644
--- a/openstack/identity/v3/endpoints/requests.go
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -111,7 +111,7 @@
 	}
 
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
-		return EndpointPage{pagination.LinkedPageBase(r)}
+		return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
 	u := getListURL(client) + utils.BuildQuery(q)
diff --git a/openstack/identity/v3/services/requests.go b/openstack/identity/v3/services/requests.go
index fbd6a7c..405a9a6 100644
--- a/openstack/identity/v3/services/requests.go
+++ b/openstack/identity/v3/services/requests.go
@@ -57,7 +57,7 @@
 	u := getListURL(client) + utils.BuildQuery(q)
 
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
-		return ServicePage{pagination.LinkedPageBase(r)}
+		return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
 	return pagination.NewPager(client, u, createPage)
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index 0d74eb5..7fed58e 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -73,7 +73,7 @@
 
 	u := listURL(c) + utils.BuildQuery(q)
 	return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
-		return NetworkPage{pagination.LinkedPageBase(r)}
+		return NetworkPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	})
 }
 
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 3b7e605..0c4ae74 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -97,7 +97,7 @@
 
 	u := listURL(c) + utils.BuildQuery(q)
 	return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
-		return PortPage{pagination.LinkedPageBase(r)}
+		return PortPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	})
 }
 
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 9cbf5ab..591af24 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -78,7 +78,7 @@
 
 	u := listURL(c) + utils.BuildQuery(q)
 	return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
-		return SubnetPage{pagination.LinkedPageBase(r)}
+		return SubnetPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	})
 }
 
diff --git a/pagination/linked.go b/pagination/linked.go
index fc88a55..0376edb 100644
--- a/pagination/linked.go
+++ b/pagination/linked.go
@@ -1,29 +1,63 @@
 package pagination
 
-import "github.com/mitchellh/mapstructure"
+import "fmt"
 
 // LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result.
-type LinkedPageBase LastHTTPResponse
+type LinkedPageBase struct {
+	LastHTTPResponse
+
+	// LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer.
+	// If any link along the path is missing, an empty URL will be returned.
+	// If any link results in an unexpected value type, an error will be returned.
+	// When left as "nil", []string{"links", "next"} will be used as a default.
+	LinkPath []string
+}
 
 // NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
 // It assumes that the links are available in a "links" element of the top-level response object.
 // If this is not the case, override NextPageURL on your result type.
 func (current LinkedPageBase) NextPageURL() (string, error) {
-	type response struct {
-		Links struct {
-			Next *string `mapstructure:"next,omitempty"`
-		} `mapstructure:"links"`
+	var path []string
+	var key string
+
+	if current.LinkPath == nil {
+		path = []string{"links", "next"}
+	} else {
+		path = current.LinkPath
 	}
 
-	var r response
-	err := mapstructure.Decode(current.Body, &r)
-	if err != nil {
-		return "", err
+	submap, ok := current.Body.(map[string]interface{})
+	if !ok {
+		return "", fmt.Errorf("Expected an object, but was %#v", current.Body)
 	}
 
-	if r.Links.Next == nil {
-		return "", nil
-	}
+	for {
+		key, path = path[0], path[1:len(path)]
 
-	return *r.Links.Next, nil
+		value, ok := submap[key]
+		if !ok {
+			return "", nil
+		}
+
+		fmt.Printf("key = %#v, path = %#v, value = %#v\n", key, path, value)
+
+		if len(path) > 0 {
+			submap, ok = value.(map[string]interface{})
+			if !ok {
+				return "", fmt.Errorf("Expected an object, but was %#v", value)
+			}
+		} else {
+			if value == nil {
+				// Actual null element.
+				return "", nil
+			}
+
+			url, ok := value.(string)
+			if !ok {
+				return "", fmt.Errorf("Expected a string, but was %#v", value)
+			}
+
+			return url, nil
+		}
+	}
 }
diff --git a/pagination/linked_test.go b/pagination/linked_test.go
index 2093755..2621f98 100644
--- a/pagination/linked_test.go
+++ b/pagination/linked_test.go
@@ -58,7 +58,7 @@
 	client := createClient()
 
 	createPage := func(r LastHTTPResponse) Page {
-		return LinkedPageResult{LinkedPageBase(r)}
+		return LinkedPageResult{LinkedPageBase{LastHTTPResponse: r}}
 	}
 
 	return NewPager(client, testhelper.Server.URL+"/page1", createPage)