server metadata operations and tests
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index be1fe7a..a3e5879 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -397,3 +397,56 @@
t.Fatal(err)
}
}
+
+func TestServerMetadata(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ metadata, err := servers.UpdateMetadatas(client, server.ID, servers.MetadatasOpts{
+ "foo": "bar",
+ "this": "that",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("UpdateMetadatas result: %+v\n", metadata)
+
+ err = servers.DeleteMetadata(client, server.ID, "foo").ExtractErr()
+ th.AssertNoErr(t, err)
+
+ metadata, err = servers.CreateMetadata(client, server.ID, servers.MetadataOpts{
+ "foo": "baz",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("CreateMetadata result: %+v\n", metadata)
+
+ metadata, err = servers.Metadata(client, server.ID, "foo").Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Metadata result: %+v\n", metadata)
+ th.AssertEquals(t, "baz", metadata["foo"])
+
+ metadata, err = servers.Metadatas(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Metadatas result: %+v\n", metadata)
+
+ metadata, err = servers.CreateMetadatas(client, server.ID, servers.MetadatasOpts{}).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("CreateMetadatas result: %+v\n", metadata)
+ th.AssertDeepEquals(t, map[string]string{}, metadata)
+}
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index 9ec3def..ed03b5c 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -458,6 +458,7 @@
})
}
+// HandleServerRescueSuccessfully sets up the test server to respond to a server Rescue request.
func HandleServerRescueSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
@@ -468,3 +469,91 @@
w.Write([]byte(`{ "adminPass": "1234567890" }`))
})
}
+
+// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
+func HandleMetadataGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+ })
+}
+
+// HandleMetadataCreateSuccessfully sets up the test server to respond to a server creation request.
+func HandleMetadataCreateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "meta": {
+ "foo": "bar"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "meta": {"foo":"bar"}}`))
+ })
+}
+
+// HandleMetadataDeleteSuccessfully sets up the test server to respond to a metadata Delete request.
+func HandleMetadataDeleteSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata/foo", 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)
+ })
+}
+
+// HandleMetadatasGetSuccessfully sets up the test server to respond to a metadatas Get request.
+func HandleMetadatasGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+ })
+}
+
+// HandleMetadatasCreateSuccessfully sets up the test server to respond to a metadatas Create request.
+func HandleMetadatasCreateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "metadata": {
+ "foo": "bar",
+ "this": "that"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "metadata": {"foo":"bar", "this":"that"}}`))
+ })
+}
+
+// HandleMetadatasUpdateSuccessfully sets up the test server to respond to a metadatas Update request.
+func HandleMetadatasUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "metadata": {
+ "foo": "baz",
+ "this": "those"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "application/json")
+ w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`))
+ })
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index ed35b33..7023bcf 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -2,6 +2,7 @@
import (
"encoding/base64"
+ "errors"
"fmt"
"github.com/racker/perigee"
@@ -559,7 +560,7 @@
AdminPass string
}
-// ToRescueResizeMap formats a RescueOpts as a map that can be used as a JSON
+// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON
// request body for the Rescue request.
func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) {
server := make(map[string]interface{})
@@ -592,3 +593,134 @@
return result
}
+
+// CreateMetadatasOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateMetadatasOptsBuilder interface {
+ ToMetadatasCreateMap() (map[string]interface{}, error)
+}
+
+// MetadatasOpts is a map that contains key-value pairs.
+type MetadatasOpts map[string]string
+
+// ToMetadatasCreateMap assembles a body for a Create request based on the contents of a MetadatasOpts.
+func (opts MetadatasOpts) ToMetadatasCreateMap() (map[string]interface{}, error) {
+ return map[string]interface{}{"metadata": opts}, nil
+}
+
+// UpdateMetadatasOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type UpdateMetadatasOptsBuilder interface {
+ ToMetadatasUpdateMap() (map[string]interface{}, error)
+}
+
+// ToMetadatasUpdateMap assembles a body for an Update request based on the contents of a MetadatasOpts.
+func (opts MetadatasOpts) ToMetadatasUpdateMap() (map[string]interface{}, error) {
+ return map[string]interface{}{"metadata": opts}, nil
+}
+
+// CreateMetadatas will create multiple new key-value pairs for the given server ID.
+// Note: Using this operation will erase any already-existing metadata and create
+// the new metadata provided. To keep any already-existing metadata, use the
+// UpdateMetadatas or UpdateMetadata function.
+func CreateMetadatas(client *gophercloud.ServiceClient, id string, opts CreateMetadatasOptsBuilder) CreateMetadatasResult {
+ var res CreateMetadatasResult
+ metadatas, err := opts.ToMetadatasCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ _, res.Err = perigee.Request("PUT", metadatasURL(client, id), perigee.Options{
+ ReqBody: metadatas,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// Metadatas requests all the metadata for the given server ID.
+func Metadatas(client *gophercloud.ServiceClient, id string) GetMetadatasResult {
+ var res GetMetadatasResult
+ _, res.Err = perigee.Request("GET", metadatasURL(client, id), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// UpdateMetadatas updates (or creates) all the metadata specified by opts for the given server ID.
+// This operation does not affect already-existing metadata that is not specified
+// by opts.
+func UpdateMetadatas(client *gophercloud.ServiceClient, id string, opts UpdateMetadatasOptsBuilder) UpdateMetadatasResult {
+ var res UpdateMetadatasResult
+ metadatas, err := opts.ToMetadatasUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ _, res.Err = perigee.Request("POST", metadatasURL(client, id), perigee.Options{
+ ReqBody: metadatas,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// MetadataOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type MetadataOptsBuilder interface {
+ ToMetadataCreateMap() (map[string]interface{}, string, error)
+}
+
+// MetadataOpts is a map of length one that contains a key-value pair.
+type MetadataOpts map[string]string
+
+// ToMetadataCreateMap assembles a body for a Create request based on the contents of a MetadatasOpts.
+func (opts MetadataOpts) ToMetadataCreateMap() (map[string]interface{}, string, error) {
+ if len(opts) != 1 {
+ return nil, "", errors.New("CreateMetadata operation must have 1 and only 1 key-value pair.")
+ }
+ metadata := map[string]interface{}{"meta": opts}
+ var key string
+ for k := range metadata["meta"].(MetadataOpts) {
+ key = k
+ }
+ return metadata, key, nil
+}
+
+// CreateMetadata will create or update the key-value pair with the given key for the given server ID.
+func CreateMetadata(client *gophercloud.ServiceClient, id string, opts MetadataOptsBuilder) CreateMetadataResult {
+ var res CreateMetadataResult
+ metadata, key, err := opts.ToMetadataCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", metadataURL(client, id, key), perigee.Options{
+ ReqBody: metadata,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// Metadata requests the key-value pair with the given key for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id, key string) GetMetadataResult {
+ var res GetMetadataResult
+ _, res.Err = perigee.Request("GET", metadataURL(client, id, key), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// DeleteMetadata will delete the key-value pair with the given key for the given server ID.
+func DeleteMetadata(client *gophercloud.ServiceClient, id, key string) DeleteMetadataResult {
+ var res DeleteMetadataResult
+ _, res.Err = perigee.Request("DELETE", metadataURL(client, id, key), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 9639702..34b0c57 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -188,3 +188,79 @@
adminPass, _ := res.Extract()
th.AssertEquals(t, "1234567890", adminPass)
}
+
+func TestGetMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataGetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar"}
+ actual, err := Metadata(client.ServiceClient(), "1234asdf", "foo").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataCreateSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar"}
+ actual, err := CreateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{"foo": "bar"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataDeleteSuccessfully(t)
+
+ err := DeleteMetadata(client.ServiceClient(), "1234asdf", "foo").ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetMetadatas(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatasGetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar", "this": "that"}
+ actual, err := Metadatas(client.ServiceClient(), "1234asdf").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateMetadatas(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatasCreateSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar", "this": "that"}
+ actual, err := CreateMetadatas(client.ServiceClient(), "1234asdf", MetadatasOpts{
+ "foo": "bar",
+ "this": "that",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateMetadatas(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatasUpdateSuccessfully(t)
+
+ expected := map[string]string{"foo": "baz", "this": "those"}
+ actual, err := UpdateMetadatas(client.ServiceClient(), "1234asdf", MetadatasOpts{
+ "foo": "baz",
+ "this": "those",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index fec5345..10c003a 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -39,7 +39,7 @@
serverResult
}
-// DeleteResult temporarily contains the response from an Delete call.
+// DeleteResult temporarily contains the response from a Delete call.
type DeleteResult struct {
gophercloud.ErrResult
}
@@ -59,6 +59,7 @@
ActionResult
}
+// Extract interprets any RescueResult as an AdminPass, if possible.
func (r RescueResult) Extract() (string, error) {
if r.Err != nil {
return "", r.Err
@@ -166,3 +167,71 @@
err := mapstructure.Decode(casted, &response)
return response.Servers, err
}
+
+// MetadatasResult contains the result of a call for (potentially) multiple key-value pairs.
+type MetadatasResult struct {
+ gophercloud.Result
+}
+
+// GetMetadatasResult temporarily contains the response from a metadatas Get call.
+type GetMetadatasResult struct {
+ MetadatasResult
+}
+
+// CreateMetadatasResult temporarily contains the response from a metadatas Create call.
+type CreateMetadatasResult struct {
+ MetadatasResult
+}
+
+// UpdateMetadatasResult temporarily contains the response from a metadatas Update call.
+type UpdateMetadatasResult struct {
+ MetadatasResult
+}
+
+// MetadataResult contains the result of a call for individual a single key-value pair.
+type MetadataResult struct {
+ gophercloud.Result
+}
+
+// GetMetadataResult temporarily contains the response from a metadata Get call.
+type GetMetadataResult struct {
+ MetadataResult
+}
+
+// CreateMetadataResult temporarily contains the response from a metadata Create call.
+type CreateMetadataResult struct {
+ MetadataResult
+}
+
+// DeleteMetadataResult temporarily contains the response from a metadata Delete call.
+type DeleteMetadataResult struct {
+ gophercloud.ErrResult
+}
+
+// Extract interprets any MetadatasResult as a Metadatas, if possible.
+func (r MetadatasResult) Extract() (map[string]string, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Metadatas map[string]string `mapstructure:"metadata"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Metadatas, err
+}
+
+// Extract interprets any MetadataResult as a Metadata, if possible.
+func (r MetadataResult) Extract() (map[string]string, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Metadata map[string]string `mapstructure:"meta"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Metadata, err
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 57587ab..a5e6576 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -29,3 +29,11 @@
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
+
+func metadataURL(client *gophercloud.ServiceClient, id, key string) string {
+ return client.ServiceURL("servers", id, "metadata", key)
+}
+
+func metadatasURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "metadata")
+}
diff --git a/openstack/compute/v2/servers/urls_test.go b/openstack/compute/v2/servers/urls_test.go
index cc895c9..1cdb210 100644
--- a/openstack/compute/v2/servers/urls_test.go
+++ b/openstack/compute/v2/servers/urls_test.go
@@ -54,3 +54,15 @@
expected := endpoint + "servers/foo/action"
th.CheckEquals(t, expected, actual)
}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "servers/foo/metadata/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestMetadatasURL(t *testing.T) {
+ actual := metadatasURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/metadata"
+ th.CheckEquals(t, expected, actual)
+}