Merge pull request #210 from jamiehannaford/os-network-v2-ext

[wip] Neutron v2 extensions
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..5835048
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,87 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestSnapshots(t *testing.T) {
+
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+	}
+
+	v, err := volumes.Create(client, &volumes.CreateOpts{
+		Name: "gophercloud-test-volume",
+		Size: 1,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Failed to create volume: %v\n", err)
+	}
+
+	err = volumes.WaitForStatus(client, v.ID, "available", 120)
+	if err != nil {
+		t.Fatalf("Failed to create volume: %v\n", err)
+	}
+
+	t.Logf("Created volume: %v\n", v)
+
+	ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+		Name:     "gophercloud-test-snapshot",
+		VolumeID: v.ID,
+	}).Extract()
+	if err != nil {
+		t.Fatalf("Failed to create snapshot: %v\n", err)
+	}
+
+	err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+	if err != nil {
+		t.Fatalf("Failed to create snapshot: %v\n", err)
+	}
+
+	t.Logf("Created snapshot: %+v\n", ss)
+
+	err = snapshots.Delete(client, ss.ID)
+	if err != nil {
+		t.Fatalf("Failed to delete snapshot: %v", err)
+	}
+
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := snapshots.Get(client, ss.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	if err != nil {
+		t.Fatalf("Failed to delete snapshot: %v", err)
+	}
+
+	t.Log("Deleted snapshot\n")
+
+	err = volumes.Delete(client, v.ID)
+	if err != nil {
+		t.Errorf("Failed to delete volume: %v", err)
+	}
+
+	err = gophercloud.WaitFor(120, func() (bool, error) {
+		_, err := volumes.Get(client, v.ID).Extract()
+		if err != nil {
+			return true, nil
+		}
+
+		return false, nil
+	})
+	if err != nil {
+		t.Errorf("Failed to delete volume: %v", err)
+	}
+
+	t.Log("Deleted volume\n")
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..21a47ac
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,88 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack"
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/openstack/utils"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+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.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+		Region: os.Getenv("OS_REGION_NAME"),
+	})
+}
+
+func TestVolumes(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+	}
+
+	cv, err := volumes.Create(client, &volumes.CreateOpts{
+		Size: 1,
+		Name: "gophercloud-test-volume",
+	}).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer func() {
+		err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+		if err != nil {
+			t.Error(err)
+		}
+		err = volumes.Delete(client, cv.ID)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+
+	_, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+		Name: "gophercloud-updated-volume",
+	}).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	v, err := volumes.Get(client, cv.ID).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	fmt.Printf("Got volume: %+v\n", v)
+
+	if v.Name != "gophercloud-updated-volume" {
+		t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name)
+	}
+
+	err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+		vols, err := volumes.ExtractVolumes(page)
+		if len(vols) != 1 {
+			t.Errorf("Expected 1 volume, got %d", len(vols))
+		}
+		return true, err
+	})
+	if err != nil {
+		t.Errorf("Error listing volumes: %v", err)
+	}
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..416e341
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+func TestVolumeTypes(t *testing.T) {
+	client, err := newClient()
+	if err != nil {
+		t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+	}
+
+	vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+		ExtraSpecs: map[string]interface{}{
+			"capabilities": "gpu",
+			"priority":     3,
+		},
+		Name: "gophercloud-test-volumeType",
+	}).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer func() {
+		time.Sleep(10000 * time.Millisecond)
+		err = volumetypes.Delete(client, vt.ID)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+	}()
+	t.Logf("Created volume type: %+v\n", vt)
+
+	vt, err = volumetypes.Get(client, vt.ID).Extract()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	t.Logf("Got volume type: %+v\n", vt)
+
+	err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		volTypes, err := volumetypes.ExtractVolumeTypes(page)
+		if len(volTypes) != 1 {
+			t.Errorf("Expected 1 volume type, got %d", len(volTypes))
+		}
+		t.Logf("Listing volume types: %+v\n", volTypes)
+		return true, err
+	})
+	if err != nil {
+		t.Errorf("Error trying to list volume types: %v", err)
+	}
+}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index 620ef1b..131b089 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -46,10 +46,10 @@
 	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,
+	server, err := servers.Create(client, servers.CreateOpts{
+		Name:      name,
+		FlavorRef: choices.FlavorID,
+		ImageRef:  choices.ImageID,
 	}).Extract()
 	if err != nil {
 		t.Fatalf("Unable to create server: %v", err)
@@ -114,9 +114,7 @@
 
 	t.Logf("Attempting to rename the server to %s.", alternateName)
 
-	updated, err := servers.Update(client, server.ID, map[string]interface{}{
-		"name": alternateName,
-	}).Extract()
+	updated, err := servers.Update(client, server.ID, servers.UpdateOpts{Name: alternateName}).Extract()
 	if err != nil {
 		t.Fatalf("Unable to rename server: %v", err)
 	}
diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go
index d5f9ea6..341acb7 100644
--- a/acceptance/openstack/identity/v3/token_test.go
+++ b/acceptance/openstack/identity/v3/token_test.go
@@ -34,15 +34,10 @@
 	service := openstack.NewIdentityV3(provider)
 
 	// Use the service to create a token.
-	result, err := tokens3.Create(service, ao, nil)
+	token, err := tokens3.Create(service, ao, nil).Extract()
 	if err != nil {
 		t.Fatalf("Unable to get token: %v", err)
 	}
 
-	token, err := result.TokenID()
-	if err != nil {
-		t.Fatalf("Unable to extract token from response: %v", err)
-	}
-
-	t.Logf("Acquired token: %s", token)
+	t.Logf("Acquired token: %s", token.ID)
 }
diff --git a/openstack/blockstorage/v1/apiversions/doc.go b/openstack/blockstorage/v1/apiversions/doc.go
new file mode 100644
index 0000000..c3c486f
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/doc.go
@@ -0,0 +1,3 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Cinder service.
+package apiversions
diff --git a/openstack/blockstorage/v1/apiversions/requests.go b/openstack/blockstorage/v1/apiversions/requests.go
new file mode 100644
index 0000000..b3a39f7
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,28 @@
+package apiversions
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// ListVersions lists all the Cinder API versions available to end-users.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(c, listURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+		return APIVersionPage{pagination.SinglePageBase(r)}
+	})
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, v string) GetResult {
+	var res GetResult
+	_, err := perigee.Request("GET", getURL(client, v), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		Results:     &res.Resp,
+	})
+	res.Err = err
+	return res
+}
diff --git a/openstack/blockstorage/v1/apiversions/requests_test.go b/openstack/blockstorage/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..c135722
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests_test.go
@@ -0,0 +1,156 @@
+package apiversions
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{
+			TokenID: TokenID,
+		},
+		Endpoint: th.Endpoint(),
+	}
+}
+
+func TestListVersions(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `{
+			"versions": [
+				{
+					"status": "CURRENT",
+					"updated": "2012-01-04T11:33:21Z",
+					"id": "v1.0",
+					"links": [
+						{
+							"href": "http://23.253.228.211:8776/v1/",
+							"rel": "self"
+						}
+					]
+			    },
+				{
+					"status": "CURRENT",
+					"updated": "2012-11-21T11:33:21Z",
+					"id": "v2.0",
+					"links": [
+						{
+							"href": "http://23.253.228.211:8776/v2/",
+							"rel": "self"
+						}
+					]
+				}
+			]
+		}`)
+	})
+
+	count := 0
+
+	List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractAPIVersions(page)
+		if err != nil {
+			t.Errorf("Failed to extract API versions: %v", err)
+			return false, err
+		}
+
+		expected := []APIVersion{
+			APIVersion{
+				ID:      "v1.0",
+				Status:  "CURRENT",
+				Updated: "2012-01-04T11:33:21Z",
+			},
+			APIVersion{
+				ID:      "v2.0",
+				Status:  "CURRENT",
+				Updated: "2012-11-21T11:33:21Z",
+			},
+		}
+
+		th.AssertDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestAPIInfo(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `{
+			"version": {
+				"status": "CURRENT",
+				"updated": "2012-01-04T11:33:21Z",
+				"media-types": [
+					{
+						"base": "application/xml",
+						"type": "application/vnd.openstack.volume+xml;version=1"
+					},
+					{
+						"base": "application/json",
+						"type": "application/vnd.openstack.volume+json;version=1"
+					}
+				],
+				"id": "v1.0",
+				"links": [
+					{
+						"href": "http://23.253.228.211:8776/v1/",
+						"rel": "self"
+					},
+					{
+						"href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf",
+						"type": "application/pdf",
+						"rel": "describedby"
+					},
+					{
+						"href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl",
+						"type": "application/vnd.sun.wadl+xml",
+						"rel": "describedby"
+					}
+				]
+			}
+		}`)
+	})
+
+	actual, err := Get(ServiceClient(), "v1").Extract()
+	if err != nil {
+		t.Errorf("Failed to extract version: %v", err)
+	}
+
+	expected := APIVersion{
+		ID:      "v1.0",
+		Status:  "CURRENT",
+		Updated: "2012-01-04T11:33:21Z",
+	}
+
+	th.AssertEquals(t, actual.ID, expected.ID)
+	th.AssertEquals(t, actual.Status, expected.Status)
+	th.AssertEquals(t, actual.Updated, expected.Updated)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..eeff132
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -0,0 +1,62 @@
+package apiversions
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// APIVersion represents an API version for Cinder.
+type APIVersion struct {
+	ID      string `json:"id" mapstructure:"id"`           // unique identifier
+	Status  string `json:"status" mapstructure:"status"`   // current status
+	Updated string `json:"updated" mapstructure:"updated"` // date last updated
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+	is, err := ExtractAPIVersions(r)
+	if err != nil {
+		return true, err
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+	var resp struct {
+		Versions []APIVersion `mapstructure:"versions"`
+	}
+
+	err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Versions, nil
+}
+
+type GetResult struct {
+	gophercloud.CommonResult
+}
+
+func (r GetResult) Extract() (*APIVersion, error) {
+	var resp struct {
+		Version *APIVersion `mapstructure:"version"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Version, nil
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..56f8260
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+	return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("")
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls_test.go b/openstack/blockstorage/v1/apiversions/urls_test.go
new file mode 100644
index 0000000..37e9142
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "v1")
+	expected := endpoint + "v1/"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..40b44d8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,130 @@
+package snapshots
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Snapshot. This object is passed to
+// the snapshots.Create function. For more information about these parameters,
+// see the Snapshot object.
+type CreateOpts struct {
+	Description string                 // OPTIONAL
+	Force       bool                   // OPTIONAL
+	Metadata    map[string]interface{} // OPTIONAL
+	Name        string                 // OPTIONAL
+	VolumeID    string                 // REQUIRED
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To extract
+// the Snapshot object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+	type snapshot struct {
+		Description *string                `json:"display_description,omitempty"`
+		Force       bool                   `json:"force,omitempty"`
+		Metadata    map[string]interface{} `json:"metadata,omitempty"`
+		Name        *string                `json:"display_name,omitempty"`
+		VolumeID    *string                `json:"volume_id,omitempty"`
+	}
+
+	type request struct {
+		Snapshot snapshot `json:"snapshot"`
+	}
+
+	reqBody := request{
+		Snapshot: snapshot{},
+	}
+
+	reqBody.Snapshot.Description = gophercloud.MaybeString(opts.Description)
+	reqBody.Snapshot.Name = gophercloud.MaybeString(opts.Name)
+	reqBody.Snapshot.VolumeID = gophercloud.MaybeString(opts.VolumeID)
+
+	reqBody.Snapshot.Force = opts.Force
+
+	var res CreateResult
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+		ReqBody:     &reqBody,
+		Results:     &res.Resp,
+	})
+	return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{202, 204},
+	})
+	return err
+}
+
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot object
+// from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+		Results:     &res.Resp,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// ListOpts hold options for listing Snapshots. It is passed to the
+// snapshots.List function.
+type ListOpts struct {
+	Name     string `q:"display_name"`
+	Status   string `q:"status"`
+	VolumeID string `q:"volume_id"`
+}
+
+// List returns Snapshots optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query.String()
+	}
+
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return ListResult{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOpts contain options for updating an existing Snapshot. This object is
+// passed to the snapshots.Update function. For more information about the
+// parameters, see the Snapshot object.
+type UpdateMetadataOpts struct {
+	Metadata map[string]interface{}
+}
+
+// Update will update the Snapshot with provided information. To extract the updated
+// Snapshot from the response, call the ExtractMetadata method on the UpdateResult.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts *UpdateMetadataOpts) UpdateMetadataResult {
+	type request struct {
+		Metadata map[string]interface{} `json:"metadata,omitempty"`
+	}
+
+	reqBody := request{}
+
+	reqBody.Metadata = opts.Metadata
+
+	var res UpdateMetadataResult
+
+	_, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		ReqBody:     &reqBody,
+		Results:     &res.Resp,
+	})
+	return res
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..d29cc0d
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -0,0 +1,198 @@
+package snapshots
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{
+			TokenID: TokenID,
+		},
+		Endpoint: th.Endpoint(),
+	}
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+		{
+			"snapshots": [
+				{
+					"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+					"display_name": "snapshot-001"
+				},
+				{
+					"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+					"display_name": "snapshot-002"
+				}
+			]
+		}
+		`)
+	})
+
+	client := ServiceClient()
+	count := 0
+
+	List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractSnapshots(page)
+		if err != nil {
+			t.Errorf("Failed to extract snapshots: %v", err)
+			return false, err
+		}
+
+		expected := []Snapshot{
+			Snapshot{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "snapshot-001",
+			},
+			Snapshot{
+				ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name: "snapshot-002",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "snapshot": {
+        "display_name": "snapshot-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+			`)
+	})
+
+	v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "snapshot-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "snapshot": {
+        "display_name": "snapshot-001"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "snapshot": {
+        "display_name": "snapshot-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+		`)
+	})
+
+	options := &CreateOpts{Name: "snapshot-001"}
+	n, err := Create(ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "snapshot-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestUpdateMetadata(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `
+		{
+			"metadata": {
+				"key": "v1"
+			}
+		}
+		`)
+
+		fmt.Fprintf(w, `
+			{
+				"metadata": {
+					"key": "v1"
+				}
+			}
+		`)
+	})
+
+	expected := map[string]interface{}{"key": "v1"}
+
+	options := &UpdateMetadataOpts{
+		Metadata: map[string]interface{}{
+			"key": "v1",
+		},
+	}
+	actual, err := UpdateMetadata(ServiceClient(), "123", options).ExtractMetadata()
+
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, actual, expected)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..9509bca
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,98 @@
+package snapshots
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Snapshot contains all the information associated with an OpenStack Snapshot.
+type Snapshot struct {
+	Status           string            `mapstructure:"status"`              // currect status of the Snapshot
+	Name             string            `mapstructure:"display_name"`        // display name
+	Attachments      []string          `mapstructure:"attachments"`         // instances onto which the Snapshot is attached
+	AvailabilityZone string            `mapstructure:"availability_zone"`   // logical group
+	Bootable         string            `mapstructure:"bootable"`            // is the Snapshot bootable
+	CreatedAt        string            `mapstructure:"created_at"`          // date created
+	Description      string            `mapstructure:"display_discription"` // display description
+	VolumeType       string            `mapstructure:"volume_type"`         // see VolumeType object for more information
+	SnapshotID       string            `mapstructure:"snapshot_id"`         // ID of the Snapshot from which this Snapshot was created
+	SourceVolID      string            `mapstructure:"source_volid"`        // ID of the Volume from which this Snapshot was created
+	Metadata         map[string]string `mapstructure:"metadata"`            // user-defined key-value pairs
+	ID               string            `mapstructure:"id"`                  // unique identifier
+	Size             int               `mapstructure:"size"`                // size of the Snapshot, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Snapshots.
+func (r ListResult) IsEmpty() (bool, error) {
+	volumes, err := ExtractSnapshots(r)
+	if err != nil {
+		return true, err
+	}
+	return len(volumes) == 0, nil
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
+	var response struct {
+		Snapshots []Snapshot `json:"snapshots"`
+	}
+
+	err := mapstructure.Decode(page.(ListResult).Body, &response)
+	return response.Snapshots, err
+}
+
+// UpdateMetadataResult contains the response body and error from an UpdateMetadata request.
+type UpdateMetadataResult struct {
+	commonResult
+}
+
+// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
+func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	m := r.Resp["metadata"].(map[string]interface{})
+
+	return m, nil
+}
+
+type commonResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract will get the Snapshot object out of the commonResult object.
+func (r commonResult) Extract() (*Snapshot, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Snapshot *Snapshot `json:"snapshot"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &res)
+	if err != nil {
+		return nil, fmt.Errorf("snapshots: Error decoding snapshots.commonResult: %v", err)
+	}
+	return res.Snapshot, nil
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls.go b/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..4d635e8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls.go
@@ -0,0 +1,27 @@
+package snapshots
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("snapshots")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("snapshots", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return deleteURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("snapshots", id, "metadata")
+}
+
+func updateMetadataURL(c *gophercloud.ServiceClient, id string) string {
+	return metadataURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls_test.go b/openstack/blockstorage/v1/snapshots/urls_test.go
new file mode 100644
index 0000000..feacf7f
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls_test.go
@@ -0,0 +1,50 @@
+package snapshots
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "snapshots"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "snapshots"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+	actual := metadataURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo/metadata"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateMetadataURL(t *testing.T) {
+	actual := updateMetadataURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo/metadata"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/util.go b/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..b882875
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util.go
@@ -0,0 +1,20 @@
+package snapshots
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..bca27db
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,150 @@
+package volumes
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Volume. This object is passed to
+// the volumes.Create function. For more information about these parameters,
+// see the Volume object.
+type CreateOpts struct {
+	Availability                     string            // OPTIONAL
+	Description                      string            // OPTIONAL
+	Metadata                         map[string]string // OPTIONAL
+	Name                             string            // OPTIONAL
+	Size                             int               // REQUIRED
+	SnapshotID, SourceVolID, ImageID string            // REQUIRED (one of them)
+	VolumeType                       string            // OPTIONAL
+}
+
+// Create will create a new Volume based on the values in CreateOpts. To extract
+// the Volume object from the response, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+
+	type volume struct {
+		Availability *string           `json:"availability_zone,omitempty"`
+		Description  *string           `json:"display_description,omitempty"`
+		ImageID      *string           `json:"imageRef,omitempty"`
+		Metadata     map[string]string `json:"metadata,omitempty"`
+		Name         *string           `json:"display_name,omitempty"`
+		Size         *int              `json:"size,omitempty"`
+		SnapshotID   *string           `json:"snapshot_id,omitempty"`
+		SourceVolID  *string           `json:"source_volid,omitempty"`
+		VolumeType   *string           `json:"volume_type,omitempty"`
+	}
+
+	type request struct {
+		Volume volume `json:"volume"`
+	}
+
+	reqBody := request{
+		Volume: volume{},
+	}
+
+	reqBody.Volume.Availability = gophercloud.MaybeString(opts.Availability)
+	reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+	reqBody.Volume.ImageID = gophercloud.MaybeString(opts.ImageID)
+	reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+	reqBody.Volume.Size = gophercloud.MaybeInt(opts.Size)
+	reqBody.Volume.SnapshotID = gophercloud.MaybeString(opts.SnapshotID)
+	reqBody.Volume.SourceVolID = gophercloud.MaybeString(opts.SourceVolID)
+	reqBody.Volume.VolumeType = gophercloud.MaybeString(opts.VolumeType)
+
+	var res CreateResult
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Resp,
+		OkCodes:     []int{200, 201},
+	})
+	return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{202, 204},
+	})
+	return err
+}
+
+// Get retrieves the Volume with the provided ID. To extract the Volume object from
+// the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+		Results:     &res.Resp,
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+	return res
+}
+
+// ListOpts holds options for listing Volumes. It is passed to the volumes.List
+// function.
+type ListOpts struct {
+	AllTenants bool              `q:"all_tenants"` // admin-only option. Set it to true to see all tenant volumes.
+	Metadata   map[string]string `q:"metadata"`    // List only volumes that contain Metadata.
+	Name       string            `q:"name"`        // List only volumes that have Name as the display name.
+	Status     string            `q:"status"`      // List only volumes that have a status of Status.
+}
+
+// List returns Volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := gophercloud.BuildQueryString(opts)
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query.String()
+	}
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return ListResult{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+	Name        string            // OPTIONAL
+	Description string            // OPTIONAL
+	Metadata    map[string]string // OPTIONAL
+}
+
+// Update will update the Volume with provided information. To extract the updated
+// Volume from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts *UpdateOpts) UpdateResult {
+	type update struct {
+		Description *string           `json:"display_description,omitempty"`
+		Metadata    map[string]string `json:"metadata,omitempty"`
+		Name        *string           `json:"display_name,omitempty"`
+	}
+
+	type request struct {
+		Volume update `json:"volume"`
+	}
+
+	reqBody := request{
+		Volume: update{},
+	}
+
+	reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+	reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+
+	var res UpdateResult
+
+	_, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		ReqBody:     &reqBody,
+		Results:     &res.Resp,
+	})
+	return res
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..54ff91d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,160 @@
+package volumes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{
+			TokenID: TokenID,
+		},
+		Endpoint: th.Endpoint(),
+	}
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+		{
+			"volumes": [
+				{
+					"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+					"display_name": "vol-001"
+				},
+				{
+					"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+					"display_name": "vol-002"
+				}
+			]
+		}
+		`)
+	})
+
+	client := ServiceClient()
+	count := 0
+
+	List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volumes: %v", err)
+			return false, err
+		}
+
+		expected := []Volume{
+			Volume{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-001",
+			},
+			Volume{
+				ID:   "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name: "vol-002",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "volume": {
+        "display_name": "vol-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+			`)
+	})
+
+	v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, v.Name, "vol-001")
+	th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "volume": {
+        "display_name": "vol-001"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "volume": {
+        "display_name": "vol-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+		`)
+	})
+
+	options := &CreateOpts{Name: "vol-001"}
+	n, err := Create(ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "vol-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..78c863f
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,87 @@
+package volumes
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+	Status           string            `mapstructure:"status"`              // current status of the Volume
+	Name             string            `mapstructure:"display_name"`        // display name
+	Attachments      []string          `mapstructure:"attachments"`         // instances onto which the Volume is attached
+	AvailabilityZone string            `mapstructure:"availability_zone"`   // logical group
+	Bootable         string            `mapstructure:"bootable"`            // is the volume bootable
+	CreatedAt        string            `mapstructure:"created_at"`          // date created
+	Description      string            `mapstructure:"display_discription"` // display description
+	VolumeType       string            `mapstructure:"volume_type"`         // see VolumeType object for more information
+	SnapshotID       string            `mapstructure:"snapshot_id"`         // ID of the Snapshot from which the Volume was created
+	SourceVolID      string            `mapstructure:"source_volid"`        // ID of the Volume from which the Volume was created
+	Metadata         map[string]string `mapstructure:"metadata"`            // user-defined key-value pairs
+	ID               string            `mapstructure:"id"`                  // unique identifier
+	Size             int               `mapstructure:"size"`                // size of the Volume, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// ListResult is a pagination.pager that is returned from a call to the List function.
+type ListResult struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volumes.
+func (r ListResult) IsEmpty() (bool, error) {
+	volumes, err := ExtractVolumes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(volumes) == 0, nil
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(page pagination.Page) ([]Volume, error) {
+	var response struct {
+		Volumes []Volume `json:"volumes"`
+	}
+
+	err := mapstructure.Decode(page.(ListResult).Body, &response)
+	return response.Volumes, err
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+	commonResult
+}
+
+type commonResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (*Volume, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Volume *Volume `json:"volume"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &res)
+	if err != nil {
+		return nil, fmt.Errorf("volumes: Error decoding volumes.commonResult: %v", err)
+	}
+	return res.Volume, nil
+}
diff --git a/openstack/blockstorage/v1/volumes/urls.go b/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..29629a1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("volumes")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return createURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("volumes", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return deleteURL(c, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return deleteURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumes/urls_test.go b/openstack/blockstorage/v1/volumes/urls_test.go
new file mode 100644
index 0000000..a95270e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls_test.go
@@ -0,0 +1,44 @@
+package volumes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "volumes"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "volumes"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "volumes/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "volumes/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "volumes/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/volumes/util.go b/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..0e2f16e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util.go
@@ -0,0 +1,20 @@
+package volumes
+
+import (
+	"github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+	return gophercloud.WaitFor(secs, func() (bool, error) {
+		current, err := Get(c, id).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		if current.Status == status {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..afe650d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,75 @@
+package volumetypes
+
+import (
+	"github.com/racker/perigee"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOpts are options for creating a volume type.
+type CreateOpts struct {
+	// OPTIONAL. See VolumeType.
+	ExtraSpecs map[string]interface{}
+	// OPTIONAL. See VolumeType.
+	Name string
+}
+
+// Create will create a new volume, optionally wih CreateOpts. To extract the
+// created volume type object, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+	type volumeType struct {
+		ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"`
+		Name       *string                `json:"name,omitempty"`
+	}
+
+	type request struct {
+		VolumeType volumeType `json:"volume_type"`
+	}
+
+	reqBody := request{
+		VolumeType: volumeType{},
+	}
+
+	reqBody.VolumeType.Name = gophercloud.MaybeString(opts.Name)
+	reqBody.VolumeType.ExtraSpecs = opts.ExtraSpecs
+
+	var res CreateResult
+	_, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200, 201},
+		ReqBody:     &reqBody,
+		Results:     &res.Resp,
+	})
+	return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	_, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{202},
+	})
+	return err
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, err := perigee.Request("GET", getURL(client, id), perigee.Options{
+		MoreHeaders: client.Provider.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+		Results:     &res.Resp,
+	})
+	res.Err = err
+	return res
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+		return ListResult{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, listURL(client), createPage)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests_test.go b/openstack/blockstorage/v1/volumetypes/requests_test.go
new file mode 100644
index 0000000..a9c6512
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -0,0 +1,172 @@
+package volumetypes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{
+			TokenID: TokenID,
+		},
+		Endpoint: th.Endpoint(),
+	}
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+		{
+			"volume_types": [
+				{
+					"id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+					"name": "vol-type-001",
+					"extra_specs": {
+						"capabilities": "gpu"
+						}
+				},
+				{
+					"id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+					"name": "vol-type-002",
+					"extra_specs": {}
+				}
+			]
+		}
+		`)
+	})
+
+	client := ServiceClient()
+	count := 0
+
+	List(client).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractVolumeTypes(page)
+		if err != nil {
+			t.Errorf("Failed to extract volume types: %v", err)
+			return false, err
+		}
+
+		expected := []VolumeType{
+			VolumeType{
+				ID:   "289da7f8-6440-407c-9fb4-7db01ec49164",
+				Name: "vol-type-001",
+				ExtraSpecs: map[string]interface{}{
+					"capabilities": "gpu",
+				},
+			},
+			VolumeType{
+				ID:         "96c3bda7-c82a-4f50-be73-ca7621794835",
+				Name:       "vol-type-002",
+				ExtraSpecs: map[string]interface{}{},
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+{
+    "volume_type": {
+        "name": "vol-type-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+		"extra_specs": {
+			"serverNumber": "2"
+		}
+    }
+}
+			`)
+	})
+
+	vt, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+	th.AssertEquals(t, vt.Name, "vol-type-001")
+	th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "volume_type": {
+        "name": "vol-type-001"
+    }
+}
+			`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "volume_type": {
+        "name": "vol-type-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+		`)
+	})
+
+	options := &CreateOpts{Name: "vol-type-001"}
+	n, err := Create(ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Name, "vol-type-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..8e5932a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,72 @@
+package volumetypes
+
+import (
+	"fmt"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// VolumeType contains all information associated with an OpenStack Volume Type.
+type VolumeType struct {
+	ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata
+	ID         string                 `json:"id" mapstructure:"id"`                   // unique identifier
+	Name       string                 `json:"name" mapstructure:"name"`               // display name
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+	commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volume Types.
+func (r ListResult) IsEmpty() (bool, error) {
+	volumeTypes, err := ExtractVolumeTypes(r)
+	if err != nil {
+		return true, err
+	}
+	return len(volumeTypes) == 0, nil
+}
+
+// ExtractVolumeTypes extracts and returns Volume Types.
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+	var response struct {
+		VolumeTypes []VolumeType `mapstructure:"volume_types"`
+	}
+
+	err := mapstructure.Decode(page.(ListResult).Body, &response)
+	return response.VolumeTypes, err
+}
+
+type commonResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract will get the Volume Type object out of the commonResult object.
+func (r commonResult) Extract() (*VolumeType, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &res)
+	if err != nil {
+		return nil, fmt.Errorf("Error decoding Volume Type: %v", err)
+	}
+
+	return res.VolumeType, nil
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls.go b/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..cf8367b
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls.go
@@ -0,0 +1,19 @@
+package volumetypes
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("types")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+	return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("types", id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+	return getURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls_test.go b/openstack/blockstorage/v1/volumetypes/urls_test.go
new file mode 100644
index 0000000..44016e2
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls_test.go
@@ -0,0 +1,38 @@
+package volumetypes
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient())
+	expected := endpoint + "types"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient())
+	expected := endpoint + "types"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "foo")
+	expected := endpoint + "types/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "foo")
+	expected := endpoint + "types/foo"
+	th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/client.go b/openstack/client.go
index eeeb809..1b057d0 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -175,15 +175,12 @@
 		v3Client.Endpoint = endpoint
 	}
 
-	result, err := tokens3.Create(v3Client, options, nil)
+	token, err := tokens3.Create(v3Client, options, nil).Extract()
 	if err != nil {
 		return err
 	}
+	client.TokenID = token.ID
 
-	client.TokenID, err = result.TokenID()
-	if err != nil {
-		return err
-	}
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
 		return v3endpointLocator(v3Client, opts)
 	}
@@ -313,3 +310,13 @@
 	}
 	return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
 }
+
+// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
+func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+	eo.ApplyDefaults("volume")
+	url, err := client.EndpointLocator(eo)
+	if err != nil {
+		return nil, err
+	}
+	return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 6c72a0a..20ca52e 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -1,6 +1,7 @@
 package servers
 
 import (
+	"encoding/base64"
 	"fmt"
 
 	"github.com/racker/perigee"
@@ -17,14 +18,120 @@
 	return pagination.NewPager(client, detailURL(client), createPage)
 }
 
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+// The CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+	ToServerCreateMap() map[string]interface{}
+}
+
+// Network is used within CreateOpts to control a new server's network attachments.
+type Network struct {
+	// UUID of a nova-network to attach to the newly provisioned server.
+	// Required unless Port is provided.
+	UUID string
+
+	// Port of a neutron network to attach to the newly provisioned server.
+	// Required unless UUID is provided.
+	Port string
+
+	// FixedIP [optional] specifies a fixed IPv4 address to be used on this network.
+	FixedIP string
+}
+
+// CreateOpts specifies server creation parameters.
+type CreateOpts struct {
+	// Name [required] is the name to assign to the newly launched server.
+	Name string
+
+	// ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+	// Optional if using the boot-from-volume extension.
+	ImageRef string
+
+	// FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+	FlavorRef string
+
+	// SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+	SecurityGroups []string
+
+	// UserData [optional] contains configuration information or scripts to use upon launch.
+	// Create will base64-encode it for you.
+	UserData []byte
+
+	// AvailabilityZone [optional] in which to launch the server.
+	AvailabilityZone string
+
+	// Networks [optional] dictates how this server will be attached to available networks.
+	// By default, the server will be attached to all isolated networks for the tenant.
+	Networks []Network
+
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+	Metadata map[string]string
+
+	// Personality [optional] includes the path and contents of a file to inject into the server at launch.
+	// The maximum size of the file is 255 bytes (decoded).
+	Personality []byte
+
+	// ConfigDrive [optional] enables metadata injection through a configuration drive.
+	ConfigDrive bool
+}
+
+// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToServerCreateMap() map[string]interface{} {
+	server := make(map[string]interface{})
+
+	server["name"] = opts.Name
+	server["imageRef"] = opts.ImageRef
+	server["flavorRef"] = opts.FlavorRef
+
+	if opts.UserData != nil {
+		encoded := base64.StdEncoding.EncodeToString(opts.UserData)
+		server["user_data"] = &encoded
+	}
+	if opts.Personality != nil {
+		encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+		server["personality"] = &encoded
+	}
+	if opts.ConfigDrive {
+		server["config_drive"] = "true"
+	}
+	if opts.AvailabilityZone != "" {
+		server["availability_zone"] = opts.AvailabilityZone
+	}
+	if opts.Metadata != nil {
+		server["metadata"] = opts.Metadata
+	}
+
+	if len(opts.SecurityGroups) > 0 {
+		securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
+		for i, groupName := range opts.SecurityGroups {
+			securityGroups[i] = map[string]interface{}{"name": groupName}
+		}
+	}
+	if len(opts.Networks) > 0 {
+		networks := make([]map[string]interface{}, len(opts.Networks))
+		for i, net := range opts.Networks {
+			networks[i] = make(map[string]interface{})
+			if net.UUID != "" {
+				networks[i]["uuid"] = net.UUID
+			}
+			if net.Port != "" {
+				networks[i]["port"] = net.Port
+			}
+			if net.FixedIP != "" {
+				networks[i]["fixed_ip"] = net.FixedIP
+			}
+		}
+	}
+
+	return map[string]interface{}{"server": server}
+}
+
 // Create requests a server to be provisioned to the user in the current tenant.
-func Create(client *gophercloud.ServiceClient, opts map[string]interface{}) CreateResult {
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var result CreateResult
 	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
-		Results: &result.Resp,
-		ReqBody: map[string]interface{}{
-			"server": opts,
-		},
+		Results:     &result.Resp,
+		ReqBody:     opts.ToServerCreateMap(),
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{202},
 	})
@@ -50,14 +157,46 @@
 	return result
 }
 
+// UpdateOptsLike allows extentions to add additional attributes to the Update request.
+type UpdateOptsLike interface {
+	ToServerUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts struct {
+	// Name [optional] changes the displayed name of the server.
+	// The server host name will *not* change.
+	// Server names are not constrained to be unique, even within the same tenant.
+	Name string
+
+	// AccessIPv4 [optional] provides a new IPv4 address for the instance.
+	AccessIPv4 string
+
+	// AccessIPv6 [optional] provides a new IPv6 address for the instance.
+	AccessIPv6 string
+}
+
+// ToServerUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} {
+	server := make(map[string]string)
+	if opts.Name != "" {
+		server["name"] = opts.Name
+	}
+	if opts.AccessIPv4 != "" {
+		server["accessIPv4"] = opts.AccessIPv4
+	}
+	if opts.AccessIPv6 != "" {
+		server["accessIPv6"] = opts.AccessIPv6
+	}
+	return map[string]interface{}{"server": server}
+}
+
 // Update requests that various attributes of the indicated server be changed.
-func Update(client *gophercloud.ServiceClient, id string, opts map[string]interface{}) UpdateResult {
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsLike) UpdateResult {
 	var result UpdateResult
 	_, result.Err = perigee.Request("PUT", serverURL(client, id), perigee.Options{
-		Results: &result.Resp,
-		ReqBody: map[string]interface{}{
-			"server": opts,
-		},
+		Results:     &result.Resp,
+		ReqBody:     opts.ToServerUpdateMap(),
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 	})
 	return result
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index fa1a2f5..01c4cdb 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -88,10 +88,10 @@
 	})
 
 	client := serviceClient()
-	actual, err := Create(client, map[string]interface{}{
-		"name":      "derp",
-		"imageRef":  "f90f6034-2570-4974-8351-6b49732ef2eb",
-		"flavorRef": "1",
+	actual, err := Create(client, CreateOpts{
+		Name:      "derp",
+		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
+		FlavorRef: "1",
 	}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Create error: %v", err)
@@ -154,9 +154,7 @@
 	})
 
 	client := serviceClient()
-	actual, err := Update(client, "1234asdf", map[string]interface{}{
-		"name": "new-name",
-	}).Extract()
+	actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Update error: %v", err)
 	}
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
index 186d0fc..eb52573 100644
--- a/openstack/identity/v3/endpoints/requests.go
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -9,14 +9,6 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
-// maybeString returns nil for empty strings and nil for empty.
-func maybeString(original string) *string {
-	if original != "" {
-		return &original
-	}
-	return nil
-}
-
 // EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
 type EndpointOpts struct {
 	Availability gophercloud.Availability
@@ -28,7 +20,7 @@
 
 // Create inserts a new Endpoint into the service catalog.
 // Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
-func Create(client *gophercloud.ServiceClient, opts EndpointOpts) (*Endpoint, error) {
+func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
 	// Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
 	type endpoint struct {
 		Interface string  `json:"interface"`
@@ -42,22 +34,18 @@
 		Endpoint endpoint `json:"endpoint"`
 	}
 
-	type response struct {
-		Endpoint Endpoint `json:"endpoint"`
-	}
-
 	// Ensure that EndpointOpts is fully populated.
 	if opts.Availability == "" {
-		return nil, ErrAvailabilityRequired
+		return createErr(ErrAvailabilityRequired)
 	}
 	if opts.Name == "" {
-		return nil, ErrNameRequired
+		return createErr(ErrNameRequired)
 	}
 	if opts.URL == "" {
-		return nil, ErrURLRequired
+		return createErr(ErrURLRequired)
 	}
 	if opts.ServiceID == "" {
-		return nil, ErrServiceIDRequired
+		return createErr(ErrServiceIDRequired)
 	}
 
 	// Populate the request body.
@@ -69,20 +57,16 @@
 			ServiceID: opts.ServiceID,
 		},
 	}
-	reqBody.Endpoint.Region = maybeString(opts.Region)
+	reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
 
-	var respBody response
-	_, err := perigee.Request("POST", getListURL(client), perigee.Options{
+	var result CreateResult
+	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		ReqBody:     &reqBody,
-		Results:     &respBody,
+		Results:     &result.Resp,
 		OkCodes:     []int{201},
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return &respBody.Endpoint, nil
+	return result
 }
 
 // ListOpts allows finer control over the the endpoints returned by a List call.
@@ -114,13 +98,13 @@
 		return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	u := getListURL(client) + utils.BuildQuery(q)
+	u := listURL(client) + utils.BuildQuery(q)
 	return pagination.NewPager(client, u, createPage)
 }
 
 // Update changes an existing endpoint with new data.
 // All fields are optional in the provided EndpointOpts.
-func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) (*Endpoint, error) {
+func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
 	type endpoint struct {
 		Interface *string `json:"interface,omitempty"`
 		Name      *string `json:"name,omitempty"`
@@ -133,34 +117,26 @@
 		Endpoint endpoint `json:"endpoint"`
 	}
 
-	type response struct {
-		Endpoint Endpoint `json:"endpoint"`
-	}
-
 	reqBody := request{Endpoint: endpoint{}}
-	reqBody.Endpoint.Interface = maybeString(string(opts.Availability))
-	reqBody.Endpoint.Name = maybeString(opts.Name)
-	reqBody.Endpoint.Region = maybeString(opts.Region)
-	reqBody.Endpoint.URL = maybeString(opts.URL)
-	reqBody.Endpoint.ServiceID = maybeString(opts.ServiceID)
+	reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
+	reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
+	reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+	reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
+	reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
 
-	var respBody response
-	_, err := perigee.Request("PATCH", getEndpointURL(client, endpointID), perigee.Options{
+	var result UpdateResult
+	_, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		ReqBody:     &reqBody,
-		Results:     &respBody,
+		Results:     &result.Resp,
 		OkCodes:     []int{200},
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return &respBody.Endpoint, nil
+	return result
 }
 
 // Delete removes an endpoint from the service catalog.
 func Delete(client *gophercloud.ServiceClient, endpointID string) error {
-	_, err := perigee.Request("DELETE", getEndpointURL(client, endpointID), perigee.Options{
+	_, err := perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
diff --git a/openstack/identity/v3/endpoints/requests_test.go b/openstack/identity/v3/endpoints/requests_test.go
index 241d175..c30bd55 100644
--- a/openstack/identity/v3/endpoints/requests_test.go
+++ b/openstack/identity/v3/endpoints/requests_test.go
@@ -59,13 +59,13 @@
 
 	client := serviceClient()
 
-	result, err := Create(client, EndpointOpts{
+	actual, err := Create(client, EndpointOpts{
 		Availability: gophercloud.AvailabilityPublic,
 		Name:         "the-endiest-of-points",
 		Region:       "underground",
 		URL:          "https://1.2.3.4:9000/",
 		ServiceID:    "asdfasdfasdfasdf",
-	})
+	}).Extract()
 	if err != nil {
 		t.Fatalf("Unable to create an endpoint: %v", err)
 	}
@@ -79,8 +79,8 @@
 		URL:          "https://1.2.3.4:9000/",
 	}
 
-	if !reflect.DeepEqual(result, expected) {
-		t.Errorf("Expected %#v, was %#v", expected, result)
+	if !reflect.DeepEqual(actual, expected) {
+		t.Errorf("Expected %#v, was %#v", expected, actual)
 	}
 }
 
@@ -205,7 +205,7 @@
 	actual, err := Update(client, "12", EndpointOpts{
 		Name:   "renamed",
 		Region: "somewhere-else",
-	})
+	}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected error from Update: %v", err)
 	}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
index 8da90f3..2dd2357 100644
--- a/openstack/identity/v3/endpoints/results.go
+++ b/openstack/identity/v3/endpoints/results.go
@@ -1,11 +1,51 @@
 package endpoints
 
 import (
+	"fmt"
+
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+type commonResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Endpoint, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Endpoint `json:"endpoint"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &res)
+	if err != nil {
+		return nil, fmt.Errorf("Error decoding Endpoint: %v", err)
+	}
+
+	return &res.Endpoint, nil
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// createErr quickly wraps an error in a CreateResult.
+func createErr(err error) CreateResult {
+	return CreateResult{commonResult{gophercloud.CommonResult{Err: err}}}
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+	commonResult
+}
+
 // Endpoint describes the entry point for another service's API.
 type Endpoint struct {
 	ID           string                   `mapstructure:"id" json:"id"`
diff --git a/openstack/identity/v3/endpoints/urls.go b/openstack/identity/v3/endpoints/urls.go
index 011cc01..547d7b1 100644
--- a/openstack/identity/v3/endpoints/urls.go
+++ b/openstack/identity/v3/endpoints/urls.go
@@ -2,10 +2,10 @@
 
 import "github.com/rackspace/gophercloud"
 
-func getListURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("endpoints")
 }
 
-func getEndpointURL(client *gophercloud.ServiceClient, endpointID string) string {
+func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
 	return client.ServiceURL("endpoints", endpointID)
 }
diff --git a/openstack/identity/v3/endpoints/urls_test.go b/openstack/identity/v3/endpoints/urls_test.go
index fe1fb4a..0b183b7 100644
--- a/openstack/identity/v3/endpoints/urls_test.go
+++ b/openstack/identity/v3/endpoints/urls_test.go
@@ -8,7 +8,7 @@
 
 func TestGetListURL(t *testing.T) {
 	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
-	url := getListURL(&client)
+	url := listURL(&client)
 	if url != "http://localhost:5000/v3/endpoints" {
 		t.Errorf("Unexpected list URL generated: [%s]", url)
 	}
@@ -16,7 +16,7 @@
 
 func TestGetEndpointURL(t *testing.T) {
 	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
-	url := getEndpointURL(&client, "1234")
+	url := endpointURL(&client, "1234")
 	if url != "http://localhost:5000/v3/endpoints/1234" {
 		t.Errorf("Unexpected service URL generated: [%s]", url)
 	}
diff --git a/openstack/identity/v3/services/requests.go b/openstack/identity/v3/services/requests.go
index 405a9a6..7816aca 100644
--- a/openstack/identity/v3/services/requests.go
+++ b/openstack/identity/v3/services/requests.go
@@ -14,25 +14,21 @@
 }
 
 // Create adds a new service of the requested type to the catalog.
-func Create(client *gophercloud.ServiceClient, serviceType string) (*Service, error) {
+func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
 	type request struct {
 		Type string `json:"type"`
 	}
 
 	req := request{Type: serviceType}
-	var resp response
 
-	_, err := perigee.Request("POST", getListURL(client), perigee.Options{
+	var result CreateResult
+	_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		ReqBody:     &req,
-		Results:     &resp,
+		Results:     &result.Resp,
 		OkCodes:     []int{201},
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return &resp.Service, nil
+	return result
 }
 
 // ListOpts allows you to query the List method.
@@ -54,7 +50,7 @@
 	if opts.PerPage != 0 {
 		q["perPage"] = strconv.Itoa(opts.PerPage)
 	}
-	u := getListURL(client) + utils.BuildQuery(q)
+	u := listURL(client) + utils.BuildQuery(q)
 
 	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
 		return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
@@ -64,45 +60,38 @@
 }
 
 // Get returns additional information about a service, given its ID.
-func Get(client *gophercloud.ServiceClient, serviceID string) (*Service, error) {
-	var resp response
-	_, err := perigee.Request("GET", getServiceURL(client, serviceID), perigee.Options{
+func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
+	var result GetResult
+	_, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
-		Results:     &resp,
+		Results:     &result.Resp,
 		OkCodes:     []int{200},
 	})
-	if err != nil {
-		return nil, err
-	}
-	return &resp.Service, nil
+	return result
 }
 
-// Update changes the service type of an existing service.s
-func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) (*Service, error) {
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
 	type request struct {
 		Type string `json:"type"`
 	}
 
 	req := request{Type: serviceType}
 
-	var resp response
-	_, err := perigee.Request("PATCH", getServiceURL(client, serviceID), perigee.Options{
+	var result UpdateResult
+	_, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		ReqBody:     &req,
-		Results:     &resp,
+		Results:     &result.Resp,
 		OkCodes:     []int{200},
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return &resp.Service, nil
+	return result
 }
 
 // Delete removes an existing service.
 // It either deletes all associated endpoints, or fails until all endpoints are deleted.
 func Delete(client *gophercloud.ServiceClient, serviceID string) error {
-	_, err := perigee.Request("DELETE", getServiceURL(client, serviceID), perigee.Options{
+	_, err := perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{
 		MoreHeaders: client.Provider.AuthenticatedHeaders(),
 		OkCodes:     []int{204},
 	})
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
index 804f034..a3d345b 100644
--- a/openstack/identity/v3/services/requests_test.go
+++ b/openstack/identity/v3/services/requests_test.go
@@ -45,7 +45,7 @@
 
 	client := serviceClient()
 
-	result, err := Create(client, "compute")
+	result, err := Create(client, "compute").Extract()
 	if err != nil {
 		t.Fatalf("Unexpected error from Create: %v", err)
 	}
@@ -161,7 +161,7 @@
 
 	client := serviceClient()
 
-	result, err := Get(client, "12345")
+	result, err := Get(client, "12345").Extract()
 	if err != nil {
 		t.Fatalf("Error fetching service information: %v", err)
 	}
@@ -202,13 +202,13 @@
 
 	client := serviceClient()
 
-	result, err := Update(client, "12345", "lasermagic")
+	result, err := Update(client, "12345", "lasermagic").Extract()
 	if err != nil {
 		t.Fatalf("Unable to update service: %v", err)
 	}
 
 	if result.ID != "12345" {
-
+		t.Fatalf("Expected ID 12345, was %s", result.ID)
 	}
 }
 
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
index cccea8e..b4e7bd2 100644
--- a/openstack/identity/v3/services/results.go
+++ b/openstack/identity/v3/services/results.go
@@ -1,11 +1,52 @@
 package services
 
 import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 
 	"github.com/mitchellh/mapstructure"
 )
 
+type commonResult struct {
+	gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Service, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Service `json:"service"`
+	}
+
+	err := mapstructure.Decode(r.Resp, &res)
+	if err != nil {
+		return nil, fmt.Errorf("Error decoding Service: %v", err)
+	}
+
+	return &res.Service, nil
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+	commonResult
+}
+
 // Service is the result of a list or information query.
 type Service struct {
 	Description *string `json:"description,omitempty"`
diff --git a/openstack/identity/v3/services/urls.go b/openstack/identity/v3/services/urls.go
index 3556238..85443a4 100644
--- a/openstack/identity/v3/services/urls.go
+++ b/openstack/identity/v3/services/urls.go
@@ -2,10 +2,10 @@
 
 import "github.com/rackspace/gophercloud"
 
-func getListURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("services")
 }
 
-func getServiceURL(client *gophercloud.ServiceClient, serviceID string) string {
+func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
 	return client.ServiceURL("services", serviceID)
 }
diff --git a/openstack/identity/v3/services/urls_test.go b/openstack/identity/v3/services/urls_test.go
index deded69..5a31b32 100644
--- a/openstack/identity/v3/services/urls_test.go
+++ b/openstack/identity/v3/services/urls_test.go
@@ -6,17 +6,17 @@
 	"github.com/rackspace/gophercloud"
 )
 
-func TestGetListURL(t *testing.T) {
+func TestListURL(t *testing.T) {
 	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
-	url := getListURL(&client)
+	url := listURL(&client)
 	if url != "http://localhost:5000/v3/services" {
 		t.Errorf("Unexpected list URL generated: [%s]", url)
 	}
 }
 
-func TestGetServiceURL(t *testing.T) {
+func TestServiceURL(t *testing.T) {
 	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
-	url := getServiceURL(&client, "1234")
+	url := serviceURL(&client, "1234")
 	if url != "http://localhost:5000/v3/services/1234" {
 		t.Errorf("Unexpected service URL generated: [%s]", url)
 	}
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index ab4ae05..c8587b6 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -20,7 +20,7 @@
 }
 
 // Create authenticates and either generates a new token, or changes the Scope of an existing token.
-func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) (gophercloud.AuthResults, error) {
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
 	type domainReq struct {
 		ID   *string `json:"id,omitempty"`
 		Name *string `json:"name,omitempty"`
@@ -73,13 +73,13 @@
 
 	// Test first for unrecognized arguments.
 	if options.APIKey != "" {
-		return nil, ErrAPIKeyProvided
+		return createErr(ErrAPIKeyProvided)
 	}
 	if options.TenantID != "" {
-		return nil, ErrTenantIDProvided
+		return createErr(ErrTenantIDProvided)
 	}
 	if options.TenantName != "" {
-		return nil, ErrTenantNameProvided
+		return createErr(ErrTenantNameProvided)
 	}
 
 	if options.Password == "" {
@@ -87,16 +87,16 @@
 			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
 			// parameters.
 			if options.Username != "" {
-				return nil, ErrUsernameWithToken
+				return createErr(ErrUsernameWithToken)
 			}
 			if options.UserID != "" {
-				return nil, ErrUserIDWithToken
+				return createErr(ErrUserIDWithToken)
 			}
 			if options.DomainID != "" {
-				return nil, ErrDomainIDWithToken
+				return createErr(ErrDomainIDWithToken)
 			}
 			if options.DomainName != "" {
-				return nil, ErrDomainNameWithToken
+				return createErr(ErrDomainNameWithToken)
 			}
 
 			// Configure the request for Token authentication.
@@ -106,7 +106,7 @@
 			}
 		} else {
 			// If no password or token ID are available, authentication can't continue.
-			return nil, ErrMissingPassword
+			return createErr(ErrMissingPassword)
 		}
 	} else {
 		// Password authentication.
@@ -114,23 +114,23 @@
 
 		// At least one of Username and UserID must be specified.
 		if options.Username == "" && options.UserID == "" {
-			return nil, ErrUsernameOrUserID
+			return createErr(ErrUsernameOrUserID)
 		}
 
 		if options.Username != "" {
 			// If Username is provided, UserID may not be provided.
 			if options.UserID != "" {
-				return nil, ErrUsernameOrUserID
+				return createErr(ErrUsernameOrUserID)
 			}
 
 			// Either DomainID or DomainName must also be specified.
 			if options.DomainID == "" && options.DomainName == "" {
-				return nil, ErrDomainIDOrDomainName
+				return createErr(ErrDomainIDOrDomainName)
 			}
 
 			if options.DomainID != "" {
 				if options.DomainName != "" {
-					return nil, ErrDomainIDOrDomainName
+					return createErr(ErrDomainIDOrDomainName)
 				}
 
 				// Configure the request for Username and Password authentication with a DomainID.
@@ -158,10 +158,10 @@
 		if options.UserID != "" {
 			// If UserID is specified, neither DomainID nor DomainName may be.
 			if options.DomainID != "" {
-				return nil, ErrDomainIDWithUserID
+				return createErr(ErrDomainIDWithUserID)
 			}
 			if options.DomainName != "" {
-				return nil, ErrDomainNameWithUserID
+				return createErr(ErrDomainNameWithUserID)
 			}
 
 			// Configure the request for UserID and Password authentication.
@@ -177,10 +177,10 @@
 			// ProjectName provided: either DomainID or DomainName must also be supplied.
 			// ProjectID may not be supplied.
 			if scope.DomainID == "" && scope.DomainName == "" {
-				return nil, ErrScopeDomainIDOrDomainName
+				return createErr(ErrScopeDomainIDOrDomainName)
 			}
 			if scope.ProjectID != "" {
-				return nil, ErrScopeProjectIDOrProjectName
+				return createErr(ErrScopeProjectIDOrProjectName)
 			}
 
 			if scope.DomainID != "" {
@@ -205,10 +205,10 @@
 		} else if scope.ProjectID != "" {
 			// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
 			if scope.DomainID != "" {
-				return nil, ErrScopeProjectIDAlone
+				return createErr(ErrScopeProjectIDAlone)
 			}
 			if scope.DomainName != "" {
-				return nil, ErrScopeProjectIDAlone
+				return createErr(ErrScopeProjectIDAlone)
 			}
 
 			// ProjectID
@@ -218,7 +218,7 @@
 		} else if scope.DomainID != "" {
 			// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
 			if scope.DomainName != "" {
-				return nil, ErrScopeDomainIDOrDomainName
+				return createErr(ErrScopeDomainIDOrDomainName)
 			}
 
 			// DomainID
@@ -226,51 +226,45 @@
 				Domain: &domainReq{ID: &scope.DomainID},
 			}
 		} else if scope.DomainName != "" {
-			return nil, ErrScopeDomainName
+			return createErr(ErrScopeDomainName)
 		} else {
-			return nil, ErrScopeEmpty
+			return createErr(ErrScopeEmpty)
 		}
 	}
 
-	var result TokenCreateResult
-	response, err := perigee.Request("POST", getTokenURL(c), perigee.Options{
+	var result CreateResult
+	var response *perigee.Response
+	response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
 		ReqBody: &req,
-		Results: &result.response,
+		Results: &result.Resp,
 		OkCodes: []int{201},
 	})
-	if err != nil {
-		return nil, err
+	if result.Err != nil {
+		return result
 	}
-
-	// Extract the token ID from the response, if present.
-	result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
-	return &result, nil
+	result.header = response.HttpResponse.Header
+	return result
 }
 
 // Get validates and retrieves information about another token.
-func Get(c *gophercloud.ServiceClient, token string) (*TokenCreateResult, error) {
-	var result TokenCreateResult
-
-	response, err := perigee.Request("GET", getTokenURL(c), perigee.Options{
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+	var result GetResult
+	var response *perigee.Response
+	response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
 		MoreHeaders: subjectTokenHeaders(c, token),
-		Results:     &result.response,
+		Results:     &result.Resp,
 		OkCodes:     []int{200, 203},
 	})
-
-	if err != nil {
-		return nil, err
+	if result.Err != nil {
+		return result
 	}
-
-	// Extract the token ID from the response, if present.
-	result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
-	return &result, nil
+	result.header = response.HttpResponse.Header
+	return result
 }
 
 // Validate determines if a specified token is valid or not.
 func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
-	response, err := perigee.Request("HEAD", getTokenURL(c), perigee.Options{
+	response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
 		MoreHeaders: subjectTokenHeaders(c, token),
 		OkCodes:     []int{204, 404},
 	})
@@ -283,7 +277,7 @@
 
 // Revoke immediately makes specified token invalid.
 func Revoke(c *gophercloud.ServiceClient, token string) error {
-	_, err := perigee.Request("DELETE", getTokenURL(c), perigee.Options{
+	_, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
 		MoreHeaders: subjectTokenHeaders(c, token),
 		OkCodes:     []int{204},
 	})
diff --git a/openstack/identity/v3/tokens/requests_test.go b/openstack/identity/v3/tokens/requests_test.go
index d0813ac..367c73c 100644
--- a/openstack/identity/v3/tokens/requests_test.go
+++ b/openstack/identity/v3/tokens/requests_test.go
@@ -29,10 +29,14 @@
 		testhelper.TestJSONRequest(t, r, requestJSON)
 
 		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, `{}`)
+		fmt.Fprintf(w, `{
+			"token": {
+				"expires_at": "2014-10-02T13:45:00.000000Z"
+			}
+		}`)
 	})
 
-	_, err := Create(&client, options, scope)
+	_, err := Create(&client, options, scope).Extract()
 	if err != nil {
 		t.Errorf("Create returned an error: %v", err)
 	}
@@ -50,7 +54,7 @@
 		client.Provider.TokenID = "abcdef123456"
 	}
 
-	_, err := Create(&client, options, scope)
+	_, err := Create(&client, options, scope).Extract()
 	if err == nil {
 		t.Errorf("Create did NOT return an error")
 	}
@@ -250,18 +254,21 @@
 		w.Header().Add("X-Subject-Token", "aaa111")
 
 		w.WriteHeader(http.StatusCreated)
-		fmt.Fprintf(w, `{}`)
+		fmt.Fprintf(w, `{
+			"token": {
+				"expires_at": "2014-10-02T13:45:00.000000Z"
+			}
+		}`)
 	})
 
 	options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
-	result, err := Create(&client, options, nil)
+	token, err := Create(&client, options, nil).Extract()
 	if err != nil {
-		t.Errorf("Create returned an error: %v", err)
+		t.Fatalf("Create returned an error: %v", err)
 	}
 
-	token, _ := result.TokenID()
-	if token != "aaa111" {
-		t.Errorf("Expected token to be aaa111, but was %s", token)
+	if token.ID != "aaa111" {
+		t.Errorf("Expected token to be aaa111, but was %s", token.ID)
 	}
 }
 
@@ -413,19 +420,14 @@
 		`)
 	})
 
-	result, err := Get(&client, "abcdef12345")
+	token, err := Get(&client, "abcdef12345").Extract()
 	if err != nil {
 		t.Errorf("Info returned an error: %v", err)
 	}
 
-	expires, err := result.ExpiresAt()
-	if err != nil {
-		t.Errorf("Error extracting token expiration time: %v", err)
-	}
-
 	expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
-	if expires != expected {
-		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), expires.Format(time.UnixDate))
+	if token.ExpiresAt != expected {
+		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
 	}
 }
 
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index 8e0f018..c970c67 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -1,46 +1,81 @@
 package tokens
 
 import (
+	"net/http"
 	"time"
 
 	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
 )
 
 // RFC3339Milli describes the time format used by identity API responses.
 const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
 
-// TokenCreateResult contains the document structure returned from a Create call.
-type TokenCreateResult struct {
-	response map[string]interface{}
-	tokenID  string
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+	gophercloud.CommonResult
+
+	// header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+	header http.Header
 }
 
-// TokenID retrieves a token generated by a Create call from an token creation response.
-func (r *TokenCreateResult) TokenID() (string, error) {
-	return r.tokenID, nil
-}
-
-// ExpiresAt retrieves the token expiration time.
-func (r *TokenCreateResult) ExpiresAt() (time.Time, error) {
-	type tokenResp struct {
-		ExpiresAt string `mapstructure:"expires_at"`
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+	if r.Err != nil {
+		return nil, r.Err
 	}
 
-	type response struct {
-		Token tokenResp `mapstructure:"token"`
+	var response struct {
+		Token struct {
+			ExpiresAt string `mapstructure:"expires_at"`
+		} `mapstructure:"token"`
 	}
 
-	var resp response
-	err := mapstructure.Decode(r.response, &resp)
+	var token Token
+
+	// Parse the token itself from the stored headers.
+	token.ID = r.header.Get("X-Subject-Token")
+
+	err := mapstructure.Decode(r.Resp, &response)
 	if err != nil {
-		return time.Time{}, err
+		return nil, err
 	}
 
 	// Attempt to parse the timestamp.
-	ts, err := time.Parse(RFC3339Milli, resp.Token.ExpiresAt)
+	token.ExpiresAt, err = time.Parse(RFC3339Milli, response.Token.ExpiresAt)
 	if err != nil {
-		return time.Time{}, err
+		return nil, err
 	}
 
-	return ts, nil
+	return &token, nil
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+	commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+	return CreateResult{
+		commonResult: commonResult{
+			CommonResult: gophercloud.CommonResult{Err: err},
+			header:       nil,
+		},
+	}
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+	// ID is the issued token.
+	ID string
+
+	// ExpiresAt is the timestamp at which this token will no longer be accepted.
+	ExpiresAt time.Time
 }
diff --git a/openstack/identity/v3/tokens/results_test.go b/openstack/identity/v3/tokens/results_test.go
deleted file mode 100644
index 669db61..0000000
--- a/openstack/identity/v3/tokens/results_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package tokens
-
-import (
-	"testing"
-	"time"
-)
-
-func TestTokenID(t *testing.T) {
-	result := TokenCreateResult{tokenID: "1234"}
-
-	token, _ := result.TokenID()
-	if token != "1234" {
-		t.Errorf("Expected tokenID of 1234, got %s", token)
-	}
-}
-
-func TestExpiresAt(t *testing.T) {
-	resp := map[string]interface{}{
-		"token": map[string]string{
-			"expires_at": "2013-02-02T18:30:59.000000Z",
-		},
-	}
-
-	result := TokenCreateResult{
-		tokenID:  "1234",
-		response: resp,
-	}
-
-	expected, _ := time.Parse(time.UnixDate, "Sat Feb 2 18:30:59 UTC 2013")
-	actual, err := result.ExpiresAt()
-	if err != nil {
-		t.Errorf("Error extraction expiration time: %v", err)
-	}
-	if actual != expected {
-		t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), actual.Format(time.UnixDate))
-	}
-}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
index 5b47c02..360b60a 100644
--- a/openstack/identity/v3/tokens/urls.go
+++ b/openstack/identity/v3/tokens/urls.go
@@ -2,6 +2,6 @@
 
 import "github.com/rackspace/gophercloud"
 
-func getTokenURL(c *gophercloud.ServiceClient) string {
+func tokenURL(c *gophercloud.ServiceClient) string {
 	return c.ServiceURL("auth", "tokens")
 }
diff --git a/openstack/identity/v3/tokens/urls_test.go b/openstack/identity/v3/tokens/urls_test.go
index 5ff8bc6..549c398 100644
--- a/openstack/identity/v3/tokens/urls_test.go
+++ b/openstack/identity/v3/tokens/urls_test.go
@@ -14,7 +14,7 @@
 	client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
 
 	expected := testhelper.Endpoint() + "auth/tokens"
-	actual := getTokenURL(&client)
+	actual := tokenURL(&client)
 	if actual != expected {
 		t.Errorf("Expected URL %s, but was %s", expected, actual)
 	}
diff --git a/params.go b/params.go
index c492ec0..10aefea 100644
--- a/params.go
+++ b/params.go
@@ -21,6 +21,13 @@
 	return nil
 }
 
+func MaybeInt(original int) *int {
+	if original != 0 {
+		return &original
+	}
+	return nil
+}
+
 var t time.Time
 
 func isZero(v reflect.Value) bool {
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..c424759
--- /dev/null
+++ b/util.go
@@ -0,0 +1,22 @@
+package gophercloud
+
+import (
+	"fmt"
+	"time"
+)
+
+// WaitFor polls a predicate function once per second up to secs times to wait for a certain state to arrive.
+func WaitFor(secs int, predicate func() (bool, error)) error {
+	for i := 0; i < secs; i++ {
+		time.Sleep(1 * time.Second)
+
+		satisfied, err := predicate()
+		if err != nil {
+			return err
+		}
+		if satisfied {
+			return nil
+		}
+	}
+	return fmt.Errorf("Time out in WaitFor.")
+}