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.