Merge pull request #244 from jamiehannaford/rax-block-storage

Rackspace block storage
diff --git a/acceptance/rackspace/blockstorage/v1/common.go b/acceptance/rackspace/blockstorage/v1/common.go
new file mode 100644
index 0000000..e9fdd99
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/common.go
@@ -0,0 +1,38 @@
+// +build acceptance
+
+package v1
+
+import (
+	"os"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/acceptance/tools"
+	"github.com/rackspace/gophercloud/rackspace"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+	opts, err := rackspace.AuthOptionsFromEnv()
+	if err != nil {
+		return nil, err
+	}
+	opts = tools.OnlyRS(opts)
+	region := os.Getenv("RS_REGION")
+
+	provider, err := rackspace.AuthenticatedClient(opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return rackspace.NewBlockStorageV1(provider, gophercloud.EndpointOpts{
+		Region: region,
+	})
+}
+
+func setup(t *testing.T) *gophercloud.ServiceClient {
+	client, err := newClient()
+	th.AssertNoErr(t, err)
+
+	return client
+}
diff --git a/acceptance/rackspace/blockstorage/v1/snapshot_test.go b/acceptance/rackspace/blockstorage/v1/snapshot_test.go
new file mode 100644
index 0000000..be1314b
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/snapshot_test.go
@@ -0,0 +1,82 @@
+// +build acceptance blockstorage snapshots
+
+package v1
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/snapshots"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSnapshots(t *testing.T) {
+	client := setup(t)
+	volID := testVolumeCreate(t, client)
+
+	t.Log("Creating snapshots")
+	s := testSnapshotCreate(t, client, volID)
+	id := s.ID
+
+	t.Log("Listing snapshots")
+	testSnapshotList(t, client)
+
+	t.Logf("Getting snapshot %s", id)
+	testSnapshotGet(t, client, id)
+
+	t.Logf("Updating snapshot %s", id)
+	testSnapshotUpdate(t, client, id)
+
+	t.Logf("Deleting snapshot %s", id)
+	testSnapshotDelete(t, client, id)
+	s.WaitUntilDeleted(client, -1)
+
+	t.Logf("Deleting volume %s", volID)
+	testVolumeDelete(t, client, volID)
+}
+
+func testSnapshotCreate(t *testing.T, client *gophercloud.ServiceClient, volID string) *snapshots.Snapshot {
+	opts := snapshots.CreateOpts{VolumeID: volID, Name: "snapshot-001"}
+	s, err := snapshots.Create(client, opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created snapshot %s", s.ID)
+
+	t.Logf("Waiting for new snapshot to become available...")
+	start := time.Now().Second()
+	s.WaitUntilComplete(client, -1)
+	t.Logf("Snapshot completed after %ds", time.Now().Second()-start)
+
+	return s
+}
+
+func testSnapshotList(t *testing.T, client *gophercloud.ServiceClient) {
+	snapshots.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		sList, err := snapshots.ExtractSnapshots(page)
+		th.AssertNoErr(t, err)
+
+		for _, s := range sList {
+			t.Logf("Snapshot: ID [%s] Name [%s] Volume ID [%s] Progress [%s] Created [%s]",
+				s.ID, s.Name, s.VolumeID, s.Progress, s.CreatedAt)
+		}
+
+		return true, nil
+	})
+}
+
+func testSnapshotGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	_, err := snapshots.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func testSnapshotUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	_, err := snapshots.Update(client, id, snapshots.UpdateOpts{Name: "new_name"}).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func testSnapshotDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := snapshots.Delete(client, id)
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted snapshot %s", id)
+}
diff --git a/acceptance/rackspace/blockstorage/v1/volume_test.go b/acceptance/rackspace/blockstorage/v1/volume_test.go
new file mode 100644
index 0000000..5a52ac7
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/volume_test.go
@@ -0,0 +1,71 @@
+// +build acceptance blockstorage volumes
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVolumes(t *testing.T) {
+	client := setup(t)
+
+	t.Logf("Listing volumes")
+	testVolumeList(t, client)
+
+	t.Logf("Creating volume")
+	volumeID := testVolumeCreate(t, client)
+
+	t.Logf("Getting volume %s", volumeID)
+	testVolumeGet(t, client, volumeID)
+
+	t.Logf("Updating volume %s", volumeID)
+	testVolumeUpdate(t, client, volumeID)
+
+	t.Logf("Deleting volume %s", volumeID)
+	testVolumeDelete(t, client, volumeID)
+}
+
+func testVolumeList(t *testing.T, client *gophercloud.ServiceClient) {
+	volumes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		vList, err := volumes.ExtractVolumes(page)
+		th.AssertNoErr(t, err)
+
+		for _, v := range vList {
+			t.Logf("Volume: ID [%s] Name [%s] Type [%s] Created [%s]", v.ID, v.Name,
+				v.VolumeType, v.CreatedAt)
+		}
+
+		return true, nil
+	})
+}
+
+func testVolumeCreate(t *testing.T, client *gophercloud.ServiceClient) string {
+	vol, err := volumes.Create(client, os.CreateOpts{Size: 75}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size)
+	return vol.ID
+}
+
+func testVolumeGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	vol, err := volumes.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created volume: ID [%s] Size [%s]", vol.ID, vol.Size)
+}
+
+func testVolumeUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	vol, err := volumes.Update(client, id, volumes.UpdateOpts{Name: "new_name"}).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created volume: ID [%s] Name [%s]", vol.ID, vol.Name)
+}
+
+func testVolumeDelete(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	err := volumes.Delete(client, id)
+	th.AssertNoErr(t, err)
+	t.Logf("Deleted volume %s", id)
+}
diff --git a/acceptance/rackspace/blockstorage/v1/volume_type_test.go b/acceptance/rackspace/blockstorage/v1/volume_type_test.go
new file mode 100644
index 0000000..716f2b9
--- /dev/null
+++ b/acceptance/rackspace/blockstorage/v1/volume_type_test.go
@@ -0,0 +1,46 @@
+// +build acceptance blockstorage volumetypes
+
+package v1
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumetypes"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAll(t *testing.T) {
+	client := setup(t)
+
+	t.Logf("Listing volume types")
+	id := testList(t, client)
+
+	t.Logf("Getting volume type %s", id)
+	testGet(t, client, id)
+}
+
+func testList(t *testing.T, client *gophercloud.ServiceClient) string {
+	var lastID string
+
+	volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+		typeList, err := volumetypes.ExtractVolumeTypes(page)
+		th.AssertNoErr(t, err)
+
+		for _, vt := range typeList {
+			t.Logf("Volume type: ID [%s] Name [%s]", vt.ID, vt.Name)
+			lastID = vt.ID
+		}
+
+		return true, nil
+	})
+
+	return lastID
+}
+
+func testGet(t *testing.T, client *gophercloud.ServiceClient, id string) {
+	vt, err := volumetypes.Get(client, id).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Volume: ID [%s] Name [%s]", vt.ID, vt.Name)
+}
diff --git a/acceptance/tools/pkg.go b/acceptance/tools/pkg.go
new file mode 100644
index 0000000..f7eca12
--- /dev/null
+++ b/acceptance/tools/pkg.go
@@ -0,0 +1 @@
+package tools
diff --git a/openstack/blockstorage/v1/snapshots/fixtures.go b/openstack/blockstorage/v1/snapshots/fixtures.go
new file mode 100644
index 0000000..d1461fb
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/fixtures.go
@@ -0,0 +1,114 @@
+package snapshots
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+    {
+      "snapshots": [
+        {
+          "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+          "display_name": "snapshot-001"
+        },
+        {
+          "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+          "display_name": "snapshot-002"
+        }
+      ]
+    }
+    `)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	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", fake.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"
+    }
+}
+      `)
+	})
+}
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "snapshot": {
+        "volume_id": "1234",
+        "display_name": "snapshot-001"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "snapshot": {
+        "volume_id": "1234",
+        "display_name": "snapshot-001",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+    `)
+	})
+}
+
+func MockUpdateMetadataResponse(t *testing.T) {
+	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", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestJSONRequest(t, r, `
+    {
+      "metadata": {
+        "key": "v1"
+      }
+    }
+    `)
+
+		fmt.Fprintf(w, `
+      {
+        "metadata": {
+          "key": "v1"
+        }
+      }
+    `)
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	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", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
index e13a348..8db55d9 100644
--- a/openstack/blockstorage/v1/snapshots/requests_test.go
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -1,8 +1,6 @@
 package snapshots
 
 import (
-	"fmt"
-	"net/http"
 	"testing"
 
 	"github.com/rackspace/gophercloud/pagination"
@@ -14,30 +12,10 @@
 	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", client.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"
-				}
-			]
-		}
-		`)
-	})
+	MockListResponse(t)
 
 	count := 0
+
 	List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
 		count++
 		actual, err := ExtractSnapshots(page)
@@ -71,21 +49,7 @@
 	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", client.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"
-    }
-}
-			`)
-	})
+	MockGetResponse(t)
 
 	v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
 	th.AssertNoErr(t, err)
@@ -98,35 +62,9 @@
 	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", client.TokenID)
-		th.TestHeader(t, r, "Content-Type", "application/json")
-		th.TestHeader(t, r, "Accept", "application/json")
-		th.TestJSONRequest(t, r, `
-{
-    "snapshot": {
-				"volume_id": "1234",
-        "display_name": "snapshot-001"
-    }
-}
-			`)
+	MockCreateResponse(t)
 
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusCreated)
-
-		fmt.Fprintf(w, `
-{
-    "snapshot": {
-				"volume_id": "1234",
-        "display_name": "snapshot-001",
-        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
-    }
-}
-		`)
-	})
-
-	options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
+	options := CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
 	n, err := Create(client.ServiceClient(), options).Extract()
 	th.AssertNoErr(t, err)
 
@@ -139,26 +77,7 @@
 	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", client.TokenID)
-		th.TestHeader(t, r, "Content-Type", "application/json")
-		th.TestJSONRequest(t, r, `
-		{
-			"metadata": {
-				"key": "v1"
-			}
-		}
-		`)
-
-		fmt.Fprintf(w, `
-			{
-				"metadata": {
-					"key": "v1"
-				}
-			}
-		`)
-	})
+	MockUpdateMetadataResponse(t)
 
 	expected := map[string]interface{}{"key": "v1"}
 
@@ -167,6 +86,7 @@
 			"key": "v1",
 		},
 	}
+
 	actual, err := UpdateMetadata(client.ServiceClient(), "123", options).ExtractMetadata()
 
 	th.AssertNoErr(t, err)
@@ -177,11 +97,7 @@
 	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", client.TokenID)
-		w.WriteHeader(http.StatusNoContent)
-	})
+	MockDeleteResponse(t)
 
 	err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
 	th.AssertNoErr(t, err)
diff --git a/openstack/blockstorage/v1/volumes/fixtures.go b/openstack/blockstorage/v1/volumes/fixtures.go
new file mode 100644
index 0000000..a01ad05
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/fixtures.go
@@ -0,0 +1,105 @@
+package volumes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+  {
+    "volumes": [
+      {
+        "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+        "display_name": "vol-001"
+      },
+      {
+        "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+        "display_name": "vol-002"
+      }
+    ]
+  }
+  `)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	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", fake.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"
+    }
+}
+      `)
+	})
+}
+
+func MockCreateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "volume": {
+        "size": 75
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "volume": {
+        "size": 4,
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+    }
+}
+    `)
+	})
+}
+
+func MockDeleteResponse(t *testing.T) {
+	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", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func MockUpdateResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
+    {
+      "volume": {
+        "display_name": "vol-002",
+        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+        }
+    }
+    `)
+	})
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
index 473a4e7..11b950e 100644
--- a/openstack/blockstorage/v1/volumes/requests_test.go
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -1,8 +1,6 @@
 package volumes
 
 import (
-	"fmt"
-	"net/http"
 	"testing"
 
 	"github.com/rackspace/gophercloud/pagination"
@@ -14,30 +12,10 @@
 	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", client.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"
-				}
-			]
-		}
-		`)
-	})
+	MockListResponse(t)
 
 	count := 0
+
 	List(client.ServiceClient(), &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
 		count++
 		actual, err := ExtractVolumes(page)
@@ -71,21 +49,7 @@
 	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", client.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"
-    }
-}
-			`)
-	})
+	MockGetResponse(t)
 
 	v, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
 	th.AssertNoErr(t, err)
@@ -98,33 +62,9 @@
 	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", client.TokenID)
-		th.TestHeader(t, r, "Content-Type", "application/json")
-		th.TestHeader(t, r, "Accept", "application/json")
-		th.TestJSONRequest(t, r, `
-{
-    "volume": {
-        "size": 4
-    }
-}
-			`)
+	MockCreateResponse(t)
 
-		w.Header().Add("Content-Type", "application/json")
-		w.WriteHeader(http.StatusCreated)
-
-		fmt.Fprintf(w, `
-{
-    "volume": {
-        "size": 4,
-        "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
-    }
-}
-		`)
-	})
-
-	options := &CreateOpts{Size: 4}
+	options := &CreateOpts{Size: 75}
 	n, err := Create(client.ServiceClient(), options).Extract()
 	th.AssertNoErr(t, err)
 
@@ -136,11 +76,7 @@
 	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", client.TokenID)
-		w.WriteHeader(http.StatusNoContent)
-	})
+	MockDeleteResponse(t)
 
 	err := Delete(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
 	th.AssertNoErr(t, err)
@@ -150,21 +86,9 @@
 	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, "PUT")
-		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
-		w.WriteHeader(http.StatusOK)
-		fmt.Fprintf(w, `
-		{
-			"volume": {
-				"display_name": "vol-002",
-				"id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
-		    }
-		}
-		`)
-	})
+	MockUpdateResponse(t)
 
-	options := &UpdateOpts{Name: "vol-002"}
+	options := UpdateOpts{Name: "vol-002"}
 	v, err := Update(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
 	th.AssertNoErr(t, err)
 	th.CheckEquals(t, "vol-002", v.Name)
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
index 7215daf..ca322d1 100644
--- a/openstack/blockstorage/v1/volumes/results.go
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -9,19 +9,44 @@
 
 // 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
+	// Current status of the volume.
+	Status string `mapstructure:"status"`
+
+	// Human-readable display name for the volume.
+	Name string `mapstructure:"display_name"`
+
+	// Instances onto which the volume is attached.
+	Attachments []string `mapstructure:"attachments"`
+
+	// This parameter is no longer used.
+	AvailabilityZone string `mapstructure:"availability_zone"`
+
+	// Indicates whether this is a bootable volume.
+	Bootable string `mapstructure:"bootable"`
+
+	// The date when this volume was created.
+	CreatedAt string `mapstructure:"created_at"`
+
+	// Human-readable description for the volume.
+	Description string `mapstructure:"display_discription"`
+
+	// The type of volume to create, either SATA or SSD.
+	VolumeType string `mapstructure:"volume_type"`
+
+	// The ID of the snapshot from which the volume was created
+	SnapshotID string `mapstructure:"snapshot_id"`
+
+	// The ID of another block storage volume from which the current volume was created
+	SourceVolID string `mapstructure:"source_volid"`
+
+	// Arbitrary key-value pairs defined by the user.
+	Metadata map[string]string `mapstructure:"metadata"`
+
+	// Unique identifier for the volume.
+	ID string `mapstructure:"id"`
+
+	// Size of the volume in GB.
+	Size int `mapstructure:"size"`
 }
 
 // CreateResult contains the response body and error from a Create request.
diff --git a/openstack/blockstorage/v1/volumetypes/fixtures.go b/openstack/blockstorage/v1/volumetypes/fixtures.go
new file mode 100644
index 0000000..e3326ea
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/fixtures.go
@@ -0,0 +1,60 @@
+package volumetypes
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+    {
+      "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": {}
+        }
+      ]
+    }
+    `)
+	})
+}
+
+func MockGetResponse(t *testing.T) {
+	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", fake.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"
+    }
+    }
+}
+      `)
+	})
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests_test.go b/openstack/blockstorage/v1/volumetypes/requests_test.go
index 412e335..8b11786 100644
--- a/openstack/blockstorage/v1/volumetypes/requests_test.go
+++ b/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -14,34 +14,10 @@
 	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", client.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": {}
-				}
-			]
-		}
-		`)
-	})
+	MockListResponse(t)
 
 	count := 0
+
 	List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
 		count++
 		actual, err := ExtractVolumeTypes(page)
@@ -79,24 +55,7 @@
 	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", client.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"
-		}
-    }
-}
-			`)
-	})
+	MockGetResponse(t)
 
 	vt, err := Get(client.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
 	th.AssertNoErr(t, err)
diff --git a/rackspace/blockstorage/v1/snapshots/delegate.go b/rackspace/blockstorage/v1/snapshots/delegate.go
new file mode 100644
index 0000000..3ae2438
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/delegate.go
@@ -0,0 +1,134 @@
+package snapshots
+
+import (
+	"errors"
+
+	"github.com/racker/perigee"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+)
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL("snapshots", id)
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToSnapshotCreateMap() (map[string]interface{}, error)
+}
+
+// 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 {
+	// REQUIRED
+	VolumeID string
+	// OPTIONAL
+	Description string
+	// OPTIONAL
+	Force bool
+	// OPTIONAL
+	Name string
+}
+
+// ToSnapshotCreateMap assembles a request body based on the contents of a
+// CreateOpts.
+func (opts CreateOpts) ToSnapshotCreateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.VolumeID == "" {
+		return nil, errors.New("Required CreateOpts field 'VolumeID' not set.")
+	}
+
+	s["volume_id"] = opts.VolumeID
+
+	if opts.Description != "" {
+		s["display_description"] = opts.Description
+	}
+	if opts.Name != "" {
+		s["display_name"] = opts.Name
+	}
+	if opts.Force {
+		s["force"] = opts.Force
+	}
+
+	return map[string]interface{}{"snapshot": s}, nil
+}
+
+// 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 CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	return os.Delete(client, id)
+}
+
+// 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 {
+	return GetResult{os.Get(client, id)}
+}
+
+// List returns Snapshots.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client, os.ListOpts{})
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToSnapshotUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+	Name        string
+	Description string
+}
+
+// ToSnapshotUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToSnapshotUpdateMap() (map[string]interface{}, error) {
+	s := make(map[string]interface{})
+
+	if opts.Name != "" {
+		s["display_name"] = opts.Name
+	}
+	if opts.Description != "" {
+		s["display_description"] = opts.Description
+	}
+
+	return map[string]interface{}{"snapshot": s}, nil
+}
+
+// Update accepts a UpdateOpts struct and updates an existing snapshot using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, snapshotID string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToSnapshotUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = perigee.Request("PUT", updateURL(c, snapshotID), perigee.Options{
+		MoreHeaders: c.AuthenticatedHeaders(),
+		ReqBody:     &reqBody,
+		Results:     &res.Body,
+		OkCodes:     []int{200, 201},
+	})
+
+	return res
+}
diff --git a/rackspace/blockstorage/v1/snapshots/delegate_test.go b/rackspace/blockstorage/v1/snapshots/delegate_test.go
new file mode 100644
index 0000000..fad7636
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/delegate_test.go
@@ -0,0 +1,97 @@
+package snapshots
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const endpoint = "http://localhost:57909/v1/12345"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "foo")
+	expected := endpoint + "snapshots/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockListResponse(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).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
+	})
+
+	th.AssertEquals(t, 1, count)
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockGetResponse(t)
+
+	v, err := Get(fake.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()
+
+	os.MockCreateResponse(t)
+
+	options := &CreateOpts{VolumeID: "1234", Name: "snapshot-001"}
+	n, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.VolumeID, "1234")
+	th.AssertEquals(t, n.Name, "snapshot-001")
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockDeleteResponse(t)
+
+	err := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
diff --git a/rackspace/blockstorage/v1/snapshots/results.go b/rackspace/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..0fab282
--- /dev/null
+++ b/rackspace/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,149 @@
+package snapshots
+
+import (
+	"github.com/racker/perigee"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Status is the type used to represent a snapshot's status
+type Status string
+
+// Constants to use for supported statuses
+const (
+	Creating    Status = "CREATING"
+	Available   Status = "AVAILABLE"
+	Deleting    Status = "DELETING"
+	Error       Status = "ERROR"
+	DeleteError Status = "ERROR_DELETING"
+)
+
+// Snapshot is the Rackspace representation of an external block storage device.
+type Snapshot struct {
+	// The timestamp when this snapshot was created.
+	CreatedAt string `mapstructure:"created_at"`
+
+	// The human-readable description for this snapshot.
+	Description string `mapstructure:"display_description"`
+
+	// The human-readable name for this snapshot.
+	Name string `mapstructure:"display_name"`
+
+	// The UUID for this snapshot.
+	ID string `mapstructure:"id"`
+
+	// The random metadata associated with this snapshot. Note: unlike standard
+	// OpenStack snapshots, this cannot actually be set.
+	Metadata map[string]string `mapstructure:"metadata"`
+
+	// Indicates the current progress of the snapshot's backup procedure.
+	Progress string `mapstructure:"os-extended-snapshot-attributes:progress"`
+
+	// The project ID.
+	ProjectID string `mapstructure:"os-extended-snapshot-attributes:project_id"`
+
+	// The size of the volume which this snapshot backs up.
+	Size int `mapstructure:"size"`
+
+	// The status of the snapshot.
+	Status Status `mapstructure:"status"`
+
+	// The ID of the volume which this snapshot seeks to back up.
+	VolumeID string `mapstructure:"volume_id"`
+}
+
+// CreateResult represents the result of a create operation
+type CreateResult struct {
+	os.CreateResult
+}
+
+// GetResult represents the result of a get operation
+type GetResult struct {
+	os.GetResult
+}
+
+// UpdateResult represents the result of an update operation
+type UpdateResult struct {
+	gophercloud.Result
+}
+
+func commonExtract(resp interface{}, err error) (*Snapshot, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		Snapshot *Snapshot `json:"snapshot"`
+	}
+
+	err = mapstructure.Decode(resp, &respStruct)
+
+	return respStruct.Snapshot, err
+}
+
+// Extract will get the Snapshot object out of the GetResult object.
+func (r GetResult) Extract() (*Snapshot, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the CreateResult object.
+func (r CreateResult) Extract() (*Snapshot, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Snapshot object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*Snapshot, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// 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.(os.ListResult).Body, &response)
+	return response.Snapshots, err
+}
+
+// WaitUntilComplete will continually poll a snapshot until it successfully
+// transitions to a specified state. It will do this for at most the number of
+// seconds specified.
+func (snapshot Snapshot) WaitUntilComplete(c *gophercloud.ServiceClient, timeout int) error {
+	return gophercloud.WaitFor(timeout, func() (bool, error) {
+		// Poll resource
+		current, err := Get(c, snapshot.ID).Extract()
+		if err != nil {
+			return false, err
+		}
+
+		// Has it been built yet?
+		if current.Progress == "100%" {
+			return true, nil
+		}
+
+		return false, nil
+	})
+}
+
+// WaitUntilDeleted will continually poll a snapshot until it has been
+// successfully deleted, i.e. returns a 404 status.
+func (snapshot Snapshot) WaitUntilDeleted(c *gophercloud.ServiceClient, timeout int) error {
+	return gophercloud.WaitFor(timeout, func() (bool, error) {
+		// Poll resource
+		_, err := Get(c, snapshot.ID).Extract()
+
+		// Check for a 404
+		if casted, ok := err.(*perigee.UnexpectedResponseCodeError); ok && casted.Actual == 404 {
+			return true, nil
+		} else if err != nil {
+			return false, err
+		}
+
+		return false, nil
+	})
+}
diff --git a/rackspace/blockstorage/v1/volumes/delegate.go b/rackspace/blockstorage/v1/volumes/delegate.go
new file mode 100644
index 0000000..4f14454
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/delegate.go
@@ -0,0 +1,75 @@
+package volumes
+
+import (
+	"fmt"
+
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type CreateOpts struct {
+	os.CreateOpts
+}
+
+func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
+	if opts.Size < 75 || opts.Size > 1024 {
+		return nil, fmt.Errorf("Size field must be between 75 and 1024")
+	}
+
+	return opts.CreateOpts.ToVolumeCreateMap()
+}
+
+// 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 os.CreateOptsBuilder) CreateResult {
+	return CreateResult{os.Create(client, opts)}
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+	return os.Delete(client, id)
+}
+
+// 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 {
+	return GetResult{os.Get(client, id)}
+}
+
+// List returns volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client, os.ListOpts{})
+}
+
+// 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 {
+	// OPTIONAL
+	Name string
+	// OPTIONAL
+	Description string
+}
+
+// ToVolumeUpdateMap assembles a request body based on the contents of an
+// UpdateOpts.
+func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
+	v := make(map[string]interface{})
+
+	if opts.Description != "" {
+		v["display_description"] = opts.Description
+	}
+	if opts.Name != "" {
+		v["display_name"] = opts.Name
+	}
+
+	return map[string]interface{}{"volume": v}, nil
+}
+
+// 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 os.UpdateOptsBuilder) UpdateResult {
+	return UpdateResult{os.Update(client, id, opts)}
+}
diff --git a/rackspace/blockstorage/v1/volumes/delegate_test.go b/rackspace/blockstorage/v1/volumes/delegate_test.go
new file mode 100644
index 0000000..2383c54
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/delegate_test.go
@@ -0,0 +1,106 @@
+package volumes
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockListResponse(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).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
+	})
+
+	th.AssertEquals(t, 1, count)
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockGetResponse(t)
+
+	v, err := Get(fake.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()
+
+	os.MockCreateResponse(t)
+
+	n, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 75}}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, n.Size, 4)
+	th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestSizeRange(t *testing.T) {
+	_, err := Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 1}}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+
+	_, err = Create(fake.ServiceClient(), CreateOpts{os.CreateOpts{Size: 2000}}).Extract()
+	if err == nil {
+		t.Fatalf("Expected error, got none")
+	}
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockDeleteResponse(t)
+
+	err := Delete(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockUpdateResponse(t)
+
+	options := &UpdateOpts{Name: "vol-002"}
+	v, err := Update(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22", options).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, "vol-002", v.Name)
+}
diff --git a/rackspace/blockstorage/v1/volumes/results.go b/rackspace/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..c7c2cc4
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumes/results.go
@@ -0,0 +1,66 @@
+package volumes
+
+import (
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+	"github.com/rackspace/gophercloud/pagination"
+
+	"github.com/mitchellh/mapstructure"
+)
+
+// Volume wraps an Openstack volume
+type Volume os.Volume
+
+// CreateResult represents the result of a create operation
+type CreateResult struct {
+	os.CreateResult
+}
+
+// GetResult represents the result of a get operation
+type GetResult struct {
+	os.GetResult
+}
+
+// UpdateResult represents the result of an update operation
+type UpdateResult struct {
+	os.UpdateResult
+}
+
+func commonExtract(resp interface{}, err error) (*Volume, error) {
+	if err != nil {
+		return nil, err
+	}
+
+	var respStruct struct {
+		Volume *Volume `json:"volume"`
+	}
+
+	err = mapstructure.Decode(resp, &respStruct)
+
+	return respStruct.Volume, err
+}
+
+// Extract will get the Volume object out of the GetResult object.
+func (r GetResult) Extract() (*Volume, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Volume object out of the CreateResult object.
+func (r CreateResult) Extract() (*Volume, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// Extract will get the Volume object out of the UpdateResult object.
+func (r UpdateResult) Extract() (*Volume, error) {
+	return commonExtract(r.Body, r.Err)
+}
+
+// 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.(os.ListResult).Body, &response)
+
+	return response.Volumes, err
+}
diff --git a/rackspace/blockstorage/v1/volumetypes/delegate.go b/rackspace/blockstorage/v1/volumetypes/delegate.go
new file mode 100644
index 0000000..c96b3e4
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/delegate.go
@@ -0,0 +1,18 @@
+package volumetypes
+
+import (
+	"github.com/rackspace/gophercloud"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return os.List(client)
+}
+
+// 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 {
+	return GetResult{os.Get(client, id)}
+}
diff --git a/rackspace/blockstorage/v1/volumetypes/delegate_test.go b/rackspace/blockstorage/v1/volumetypes/delegate_test.go
new file mode 100644
index 0000000..6e65c90
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/delegate_test.go
@@ -0,0 +1,64 @@
+package volumetypes
+
+import (
+	"testing"
+
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockListResponse(t)
+
+	count := 0
+
+	err := List(fake.ServiceClient()).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
+	})
+
+	th.AssertEquals(t, 1, count)
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	os.MockGetResponse(t)
+
+	vt, err := Get(fake.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")
+}
diff --git a/rackspace/blockstorage/v1/volumetypes/results.go b/rackspace/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..39c8d6f
--- /dev/null
+++ b/rackspace/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,37 @@
+package volumetypes
+
+import (
+	"github.com/mitchellh/mapstructure"
+	os "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type VolumeType os.VolumeType
+
+type GetResult struct {
+	os.GetResult
+}
+
+// Extract will get the Volume Type struct out of the response.
+func (r GetResult) 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.Body, &res)
+
+	return res.VolumeType, err
+}
+
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+	var response struct {
+		VolumeTypes []VolumeType `mapstructure:"volume_types"`
+	}
+
+	err := mapstructure.Decode(page.(os.ListResult).Body, &response)
+	return response.VolumeTypes, err
+}
diff --git a/rackspace/client.go b/rackspace/client.go
index 86a78a1..5f739a8 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -142,3 +142,15 @@
 func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
 	return os.NewObjectStorageV1(client, eo)
 }
+
+// NewBlockStorageV1 creates a ServiceClient that can be used to access the
+// Rackspace Cloud Block Storage v1 API.
+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{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/util.go b/util.go
index c66af89..6f62944 100644
--- a/util.go
+++ b/util.go
@@ -1,16 +1,25 @@
 package gophercloud
 
 import (
-	"fmt"
+	"errors"
 	"strings"
 	"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++ {
+// WaitFor polls a predicate function once per second up to secs times to wait
+// for a certain state to arrive.
+func WaitFor(timeout int, predicate func() (bool, error)) error {
+	start := time.Now().Second()
+	for {
+		// Force a 1s sleep
 		time.Sleep(1 * time.Second)
 
+		// If a timeout is set, and that's been exceeded, shut it down
+		if timeout >= 0 && time.Now().Second()-start >= timeout {
+			return errors.New("A timeout occurred")
+		}
+
+		// Execute the function
 		satisfied, err := predicate()
 		if err != nil {
 			return err
@@ -19,7 +28,6 @@
 			return nil
 		}
 	}
-	return fmt.Errorf("Time out in WaitFor.")
 }
 
 // NormalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.