Merge pull request #220 from jrperritt/os-blockstorage
OS blockstorage v1
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..5835048
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,87 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestSnapshots(t *testing.T) {
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ v, err := volumes.Create(client, &volumes.CreateOpts{
+ Name: "gophercloud-test-volume",
+ Size: 1,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ err = volumes.WaitForStatus(client, v.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ t.Logf("Created volume: %v\n", v)
+
+ ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+ Name: "gophercloud-test-snapshot",
+ VolumeID: v.ID,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ t.Logf("Created snapshot: %+v\n", ss)
+
+ err = snapshots.Delete(client, ss.ID)
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := snapshots.Get(client, ss.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ t.Log("Deleted snapshot\n")
+
+ err = volumes.Delete(client, v.ID)
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := volumes.Get(client, v.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ t.Log("Deleted volume\n")
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..21a47ac
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,88 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+func TestVolumes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ cv, err := volumes.Create(client, &volumes.CreateOpts{
+ Size: 1,
+ Name: "gophercloud-test-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+ if err != nil {
+ t.Error(err)
+ }
+ err = volumes.Delete(client, cv.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+ Name: "gophercloud-updated-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ v, err := volumes.Get(client, cv.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ fmt.Printf("Got volume: %+v\n", v)
+
+ if v.Name != "gophercloud-updated-volume" {
+ t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name)
+ }
+
+ err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+ vols, err := volumes.ExtractVolumes(page)
+ if len(vols) != 1 {
+ t.Errorf("Expected 1 volume, got %d", len(vols))
+ }
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error listing volumes: %v", err)
+ }
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..416e341
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestVolumeTypes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ "priority": 3,
+ },
+ Name: "gophercloud-test-volumeType",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ time.Sleep(10000 * time.Millisecond)
+ err = volumetypes.Delete(client, vt.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+ t.Logf("Created volume type: %+v\n", vt)
+
+ vt, err = volumetypes.Get(client, vt.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ t.Logf("Got volume type: %+v\n", vt)
+
+ err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ volTypes, err := volumetypes.ExtractVolumeTypes(page)
+ if len(volTypes) != 1 {
+ t.Errorf("Expected 1 volume type, got %d", len(volTypes))
+ }
+ t.Logf("Listing volume types: %+v\n", volTypes)
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error trying to list volume types: %v", err)
+ }
+}
diff --git a/openstack/blockstorage/v1/apiversions/doc.go b/openstack/blockstorage/v1/apiversions/doc.go
new file mode 100644
index 0000000..c3c486f
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/doc.go
@@ -0,0 +1,3 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Cinder service.
+package apiversions
diff --git a/openstack/blockstorage/v1/apiversions/requests.go b/openstack/blockstorage/v1/apiversions/requests.go
new file mode 100644
index 0000000..b3a39f7
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,28 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// ListVersions lists all the Cinder API versions available to end-users.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(c, listURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+ return APIVersionPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, v string) GetResult {
+ var res GetResult
+ _, err := perigee.Request("GET", getURL(client, v), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
diff --git a/openstack/blockstorage/v1/apiversions/requests_test.go b/openstack/blockstorage/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..c135722
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests_test.go
@@ -0,0 +1,156 @@
+package apiversions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "versions": [
+ {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "CURRENT",
+ "updated": "2012-11-21T11:33:21Z",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v2/",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+ }`)
+ })
+
+ count := 0
+
+ List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAPIVersions(page)
+ if err != nil {
+ t.Errorf("Failed to extract API versions: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersion{
+ APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ },
+ APIVersion{
+ ID: "v2.0",
+ Status: "CURRENT",
+ Updated: "2012-11-21T11:33:21Z",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestAPIInfo(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "version": {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.volume+xml;version=1"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"
+ }
+ ],
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ },
+ {
+ "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf",
+ "type": "application/pdf",
+ "rel": "describedby"
+ },
+ {
+ "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl",
+ "type": "application/vnd.sun.wadl+xml",
+ "rel": "describedby"
+ }
+ ]
+ }
+ }`)
+ })
+
+ actual, err := Get(ServiceClient(), "v1").Extract()
+ if err != nil {
+ t.Errorf("Failed to extract version: %v", err)
+ }
+
+ expected := APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ }
+
+ th.AssertEquals(t, actual.ID, expected.ID)
+ th.AssertEquals(t, actual.Status, expected.Status)
+ th.AssertEquals(t, actual.Updated, expected.Updated)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..eeff132
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -0,0 +1,62 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// APIVersion represents an API version for Cinder.
+type APIVersion struct {
+ ID string `json:"id" mapstructure:"id"` // unique identifier
+ Status string `json:"status" mapstructure:"status"` // current status
+ Updated string `json:"updated" mapstructure:"updated"` // date last updated
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+ is, err := ExtractAPIVersions(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+ var resp struct {
+ Versions []APIVersion `mapstructure:"versions"`
+ }
+
+ err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Versions, nil
+}
+
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+func (r GetResult) Extract() (*APIVersion, error) {
+ var resp struct {
+ Version *APIVersion `mapstructure:"version"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Version, nil
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..56f8260
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+ return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("")
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls_test.go b/openstack/blockstorage/v1/apiversions/urls_test.go
new file mode 100644
index 0000000..37e9142
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "v1")
+ expected := endpoint + "v1/"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..40b44d8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,130 @@
+package snapshots
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Snapshot. This object is passed to
+// the snapshots.Create function. For more information about these parameters,
+// see the Snapshot object.
+type CreateOpts struct {
+ Description string // OPTIONAL
+ Force bool // OPTIONAL
+ Metadata map[string]interface{} // OPTIONAL
+ Name string // OPTIONAL
+ VolumeID string // REQUIRED
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To extract
+// the Snapshot object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+ type snapshot struct {
+ Description *string `json:"display_description,omitempty"`
+ Force bool `json:"force,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ VolumeID *string `json:"volume_id,omitempty"`
+ }
+
+ type request struct {
+ Snapshot snapshot `json:"snapshot"`
+ }
+
+ reqBody := request{
+ Snapshot: snapshot{},
+ }
+
+ reqBody.Snapshot.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Snapshot.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Snapshot.VolumeID = gophercloud.MaybeString(opts.VolumeID)
+
+ reqBody.Snapshot.Force = opts.Force
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot object
+// from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOpts hold options for listing Snapshots. It is passed to the
+// snapshots.List function.
+type ListOpts struct {
+ Name string `q:"display_name"`
+ Status string `q:"status"`
+ VolumeID string `q:"volume_id"`
+}
+
+// List returns Snapshots optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOpts contain options for updating an existing Snapshot. This object is
+// passed to the snapshots.Update function. For more information about the
+// parameters, see the Snapshot object.
+type UpdateMetadataOpts struct {
+ Metadata map[string]interface{}
+}
+
+// Update will update the Snapshot with provided information. To extract the updated
+// Snapshot from the response, call the ExtractMetadata method on the UpdateResult.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts *UpdateMetadataOpts) UpdateMetadataResult {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ }
+
+ reqBody := request{}
+
+ reqBody.Metadata = opts.Metadata
+
+ var res UpdateMetadataResult
+
+ _, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..d29cc0d
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -0,0 +1,198 @@
+package snapshots
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "snapshots": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "display_name": "snapshot-001"
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "display_name": "snapshot-002"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSnapshots(page)
+ if err != nil {
+ t.Errorf("Failed to extract snapshots: %v", err)
+ return false, err
+ }
+
+ expected := []Snapshot{
+ Snapshot{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "snapshot-001",
+ },
+ Snapshot{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "snapshot-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "snapshot-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "snapshot-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "snapshot-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestUpdateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+
+ fmt.Fprintf(w, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+ })
+
+ expected := map[string]interface{}{"key": "v1"}
+
+ options := &UpdateMetadataOpts{
+ Metadata: map[string]interface{}{
+ "key": "v1",
+ },
+ }
+ actual, err := UpdateMetadata(ServiceClient(), "123", options).ExtractMetadata()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, actual, expected)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..9509bca
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,98 @@
+package snapshots
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Snapshot contains all the information associated with an OpenStack Snapshot.
+type Snapshot struct {
+ Status string `mapstructure:"status"` // currect status of the Snapshot
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Snapshot is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the Snapshot bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which this Snapshot was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which this Snapshot was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Snapshot, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Snapshots.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumes, err := ExtractSnapshots(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumes) == 0, nil
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
+ var response struct {
+ Snapshots []Snapshot `json:"snapshots"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.Snapshots, err
+}
+
+// UpdateMetadataResult contains the response body and error from an UpdateMetadata request.
+type UpdateMetadataResult struct {
+ commonResult
+}
+
+// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
+func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ m := r.Resp["metadata"].(map[string]interface{})
+
+ return m, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Snapshot object out of the commonResult object.
+func (r commonResult) Extract() (*Snapshot, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Snapshot *Snapshot `json:"snapshot"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("snapshots: Error decoding snapshots.commonResult: %v", err)
+ }
+ return res.Snapshot, nil
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls.go b/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..4d635e8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls.go
@@ -0,0 +1,27 @@
+package snapshots
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("snapshots")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("snapshots", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("snapshots", id, "metadata")
+}
+
+func updateMetadataURL(c *gophercloud.ServiceClient, id string) string {
+ return metadataURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls_test.go b/openstack/blockstorage/v1/snapshots/urls_test.go
new file mode 100644
index 0000000..feacf7f
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls_test.go
@@ -0,0 +1,50 @@
+package snapshots
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateMetadataURL(t *testing.T) {
+ actual := updateMetadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/util.go b/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..b882875
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util.go
@@ -0,0 +1,20 @@
+package snapshots
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..bca27db
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,150 @@
+package volumes
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Volume. This object is passed to
+// the volumes.Create function. For more information about these parameters,
+// see the Volume object.
+type CreateOpts struct {
+ Availability string // OPTIONAL
+ Description string // OPTIONAL
+ Metadata map[string]string // OPTIONAL
+ Name string // OPTIONAL
+ Size int // REQUIRED
+ SnapshotID, SourceVolID, ImageID string // REQUIRED (one of them)
+ VolumeType string // OPTIONAL
+}
+
+// Create will create a new Volume based on the values in CreateOpts. To extract
+// the Volume object from the response, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+
+ type volume struct {
+ Availability *string `json:"availability_zone,omitempty"`
+ Description *string `json:"display_description,omitempty"`
+ ImageID *string `json:"imageRef,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ Size *int `json:"size,omitempty"`
+ SnapshotID *string `json:"snapshot_id,omitempty"`
+ SourceVolID *string `json:"source_volid,omitempty"`
+ VolumeType *string `json:"volume_type,omitempty"`
+ }
+
+ type request struct {
+ Volume volume `json:"volume"`
+ }
+
+ reqBody := request{
+ Volume: volume{},
+ }
+
+ reqBody.Volume.Availability = gophercloud.MaybeString(opts.Availability)
+ reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Volume.ImageID = gophercloud.MaybeString(opts.ImageID)
+ reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Volume.Size = gophercloud.MaybeInt(opts.Size)
+ reqBody.Volume.SnapshotID = gophercloud.MaybeString(opts.SnapshotID)
+ reqBody.Volume.SourceVolID = gophercloud.MaybeString(opts.SourceVolID)
+ reqBody.Volume.VolumeType = gophercloud.MaybeString(opts.VolumeType)
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+ return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// Get retrieves the Volume with the provided ID. To extract the Volume object from
+// the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOpts holds options for listing Volumes. It is passed to the volumes.List
+// function.
+type ListOpts struct {
+ AllTenants bool `q:"all_tenants"` // admin-only option. Set it to true to see all tenant volumes.
+ Metadata map[string]string `q:"metadata"` // List only volumes that contain Metadata.
+ Name string `q:"name"` // List only volumes that have Name as the display name.
+ Status string `q:"status"` // List only volumes that have a status of Status.
+}
+
+// List returns Volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+ }
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+ Name string // OPTIONAL
+ Description string // OPTIONAL
+ Metadata map[string]string // OPTIONAL
+}
+
+// Update will update the Volume with provided information. To extract the updated
+// Volume from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts *UpdateOpts) UpdateResult {
+ type update struct {
+ Description *string `json:"display_description,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ }
+
+ type request struct {
+ Volume update `json:"volume"`
+ }
+
+ reqBody := request{
+ Volume: update{},
+ }
+
+ reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+
+ var res UpdateResult
+
+ _, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..54ff91d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,160 @@
+package volumes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "volumes": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "display_name": "vol-001"
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "display_name": "vol-002"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volumes: %v", err)
+ return false, err
+ }
+
+ expected := []Volume{
+ Volume{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-001",
+ },
+ Volume{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "display_name": "vol-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "vol-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume": {
+ "display_name": "vol-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "display_name": "vol-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..78c863f
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,87 @@
+package volumes
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+ Status string `mapstructure:"status"` // current status of the Volume
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Volume is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the volume bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which the Volume was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which the Volume was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Volume, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volumes.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumes, err := ExtractVolumes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumes) == 0, nil
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(page pagination.Page) ([]Volume, error) {
+ var response struct {
+ Volumes []Volume `json:"volumes"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.Volumes, err
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+ commonResult
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (*Volume, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Volume *Volume `json:"volume"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("volumes: Error decoding volumes.commonResult: %v", err)
+ }
+ return res.Volume, nil
+}
diff --git a/openstack/blockstorage/v1/volumes/urls.go b/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..29629a1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("volumes")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("volumes", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumes/urls_test.go b/openstack/blockstorage/v1/volumes/urls_test.go
new file mode 100644
index 0000000..a95270e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls_test.go
@@ -0,0 +1,44 @@
+package volumes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/volumes/util.go b/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..0e2f16e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util.go
@@ -0,0 +1,20 @@
+package volumes
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..afe650d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,75 @@
+package volumetypes
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOpts are options for creating a volume type.
+type CreateOpts struct {
+ // OPTIONAL. See VolumeType.
+ ExtraSpecs map[string]interface{}
+ // OPTIONAL. See VolumeType.
+ Name string
+}
+
+// Create will create a new volume, optionally wih CreateOpts. To extract the
+// created volume type object, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+ type volumeType struct {
+ ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"`
+ Name *string `json:"name,omitempty"`
+ }
+
+ type request struct {
+ VolumeType volumeType `json:"volume_type"`
+ }
+
+ reqBody := request{
+ VolumeType: volumeType{},
+ }
+
+ reqBody.VolumeType.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.VolumeType.ExtraSpecs = opts.ExtraSpecs
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, err := perigee.Request("GET", getURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, listURL(client), createPage)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests_test.go b/openstack/blockstorage/v1/volumetypes/requests_test.go
new file mode 100644
index 0000000..a9c6512
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -0,0 +1,172 @@
+package volumetypes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "volume_types": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "name": "vol-type-001",
+ "extra_specs": {
+ "capabilities": "gpu"
+ }
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "name": "vol-type-002",
+ "extra_specs": {}
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumeTypes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volume types: %v", err)
+ return false, err
+ }
+
+ expected := []VolumeType{
+ VolumeType{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-type-001",
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ },
+ },
+ VolumeType{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-type-002",
+ ExtraSpecs: map[string]interface{}{},
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "extra_specs": {
+ "serverNumber": "2"
+ }
+ }
+}
+ `)
+ })
+
+ vt, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+ th.AssertEquals(t, vt.Name, "vol-type-001")
+ th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume_type": {
+ "name": "vol-type-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-type-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-type-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..8e5932a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,72 @@
+package volumetypes
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// VolumeType contains all information associated with an OpenStack Volume Type.
+type VolumeType struct {
+ ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata
+ ID string `json:"id" mapstructure:"id"` // unique identifier
+ Name string `json:"name" mapstructure:"name"` // display name
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volume Types.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumeTypes, err := ExtractVolumeTypes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumeTypes) == 0, nil
+}
+
+// ExtractVolumeTypes extracts and returns Volume Types.
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+ var response struct {
+ VolumeTypes []VolumeType `mapstructure:"volume_types"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.VolumeTypes, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Volume Type object out of the commonResult object.
+func (r commonResult) Extract() (*VolumeType, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Volume Type: %v", err)
+ }
+
+ return res.VolumeType, nil
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls.go b/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..cf8367b
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls.go
@@ -0,0 +1,19 @@
+package volumetypes
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("types")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("types", id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls_test.go b/openstack/blockstorage/v1/volumetypes/urls_test.go
new file mode 100644
index 0000000..44016e2
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls_test.go
@@ -0,0 +1,38 @@
+package volumetypes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/client.go b/openstack/client.go
index e14a48b..1b057d0 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -310,3 +310,13 @@
}
return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
}
+
+// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
+func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("volume")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+}
diff --git a/params.go b/params.go
index c492ec0..10aefea 100644
--- a/params.go
+++ b/params.go
@@ -21,6 +21,13 @@
return nil
}
+func MaybeInt(original int) *int {
+ if original != 0 {
+ return &original
+ }
+ return nil
+}
+
var t time.Time
func isZero(v reflect.Value) bool {
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..c424759
--- /dev/null
+++ b/util.go
@@ -0,0 +1,22 @@
+package gophercloud
+
+import (
+ "fmt"
+ "time"
+)
+
+// WaitFor polls a predicate function once per second up to secs times to wait for a certain state to arrive.
+func WaitFor(secs int, predicate func() (bool, error)) error {
+ for i := 0; i < secs; i++ {
+ time.Sleep(1 * time.Second)
+
+ satisfied, err := predicate()
+ if err != nil {
+ return err
+ }
+ if satisfied {
+ return nil
+ }
+ }
+ return fmt.Errorf("Time out in WaitFor.")
+}