Merge pull request #329 from jrperritt/servers-metadata
Server Metadata Operations
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index be1fe7a..d52a9d3 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -397,3 +397,54 @@
t.Fatal(err)
}
}
+
+func TestServerMetadata(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ th.AssertNoErr(t, 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.UpdateMetadata(client, server.ID, servers.MetadataOpts{
+ "foo": "bar",
+ "this": "that",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("UpdateMetadata result: %+v\n", metadata)
+
+ err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr()
+ th.AssertNoErr(t, err)
+
+ metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{
+ "foo": "baz",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("CreateMetadatum result: %+v\n", metadata)
+
+ metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Metadatum result: %+v\n", metadata)
+ th.AssertEquals(t, "baz", metadata["foo"])
+
+ metadata, err = servers.Metadata(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Metadata result: %+v\n", metadata)
+
+ metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("ResetMetadata 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..0164605 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" }`))
})
}
+
+// HandleMetadatumGetSuccessfully sets up the test server to respond to a metadatum Get request.
+func HandleMetadatumGetSuccessfully(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"}}`))
+ })
+}
+
+// HandleMetadatumCreateSuccessfully sets up the test server to respond to a metadatum Create request.
+func HandleMetadatumCreateSuccessfully(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"}}`))
+ })
+}
+
+// HandleMetadatumDeleteSuccessfully sets up the test server to respond to a metadatum Delete request.
+func HandleMetadatumDeleteSuccessfully(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)
+ })
+}
+
+// HandleMetadataGetSuccessfully sets up the test server to respond to a metadata Get request.
+func HandleMetadataGetSuccessfully(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"}}`))
+ })
+}
+
+// HandleMetadataResetSuccessfully sets up the test server to respond to a metadata Create request.
+func HandleMetadataResetSuccessfully(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"}}`))
+ })
+}
+
+// HandleMetadataUpdateSuccessfully sets up the test server to respond to a metadata Update request.
+func HandleMetadataUpdateSuccessfully(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..79d7998 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
}
+
+// ResetMetadataOptsBuilder allows extensions to add additional parameters to the
+// Reset request.
+type ResetMetadataOptsBuilder interface {
+ ToMetadataResetMap() (map[string]interface{}, error)
+}
+
+// MetadataOpts is a map that contains key-value pairs.
+type MetadataOpts map[string]string
+
+// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
+ return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts.
+func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
+ return map[string]interface{}{"metadata": opts}, nil
+}
+
+// ResetMetadata 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 ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) ResetMetadataResult {
+ var res ResetMetadataResult
+ metadata, err := opts.ToMetadataResetMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ _, res.Err = perigee.Request("PUT", metadataURL(client, id), perigee.Options{
+ ReqBody: metadata,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// Metadata requests all the metadata for the given server ID.
+func Metadata(client *gophercloud.ServiceClient, id string) GetMetadataResult {
+ var res GetMetadataResult
+ _, res.Err = perigee.Request("GET", metadataURL(client, id), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type UpdateMetadataOptsBuilder interface {
+ ToMetadataUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateMetadata 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 UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) UpdateMetadataResult {
+ var res UpdateMetadataResult
+ metadata, err := opts.ToMetadataUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+ _, res.Err = perigee.Request("POST", metadataURL(client, id), perigee.Options{
+ ReqBody: metadata,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// MetadatumOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type MetadatumOptsBuilder interface {
+ ToMetadatumCreateMap() (map[string]interface{}, string, error)
+}
+
+// MetadatumOpts is a map of length one that contains a key-value pair.
+type MetadatumOpts map[string]string
+
+// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts.
+func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
+ if len(opts) != 1 {
+ return nil, "", errors.New("CreateMetadatum operation must have 1 and only 1 key-value pair.")
+ }
+ metadatum := map[string]interface{}{"meta": opts}
+ var key string
+ for k := range metadatum["meta"].(MetadatumOpts) {
+ key = k
+ }
+ return metadatum, key, nil
+}
+
+// CreateMetadatum will create or update the key-value pair with the given key for the given server ID.
+func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) CreateMetadatumResult {
+ var res CreateMetadatumResult
+ metadatum, key, err := opts.ToMetadatumCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("PUT", metadatumURL(client, id, key), perigee.Options{
+ ReqBody: metadatum,
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// Metadatum requests the key-value pair with the given key for the given server ID.
+func Metadatum(client *gophercloud.ServiceClient, id, key string) GetMetadatumResult {
+ var res GetMetadatumResult
+ _, res.Err = perigee.Request("GET", metadatumURL(client, id, key), perigee.Options{
+ Results: &res.Body,
+ MoreHeaders: client.AuthenticatedHeaders(),
+ })
+ return res
+}
+
+// DeleteMetadatum will delete the key-value pair with the given key for the given server ID.
+func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) DeleteMetadatumResult {
+ var res DeleteMetadatumResult
+ _, res.Err = perigee.Request("DELETE", metadatumURL(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..017e793 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 TestGetMetadatum(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatumGetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar"}
+ actual, err := Metadatum(client.ServiceClient(), "1234asdf", "foo").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestCreateMetadatum(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatumCreateSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar"}
+ actual, err := CreateMetadatum(client.ServiceClient(), "1234asdf", MetadatumOpts{"foo": "bar"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestDeleteMetadatum(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadatumDeleteSuccessfully(t)
+
+ err := DeleteMetadatum(client.ServiceClient(), "1234asdf", "foo").ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataGetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar", "this": "that"}
+ actual, err := Metadata(client.ServiceClient(), "1234asdf").Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResetMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataResetSuccessfully(t)
+
+ expected := map[string]string{"foo": "bar", "this": "that"}
+ actual, err := ResetMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+ "foo": "bar",
+ "this": "that",
+ }).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleMetadataUpdateSuccessfully(t)
+
+ expected := map[string]string{"foo": "baz", "this": "those"}
+ actual, err := UpdateMetadata(client.ServiceClient(), "1234asdf", MetadataOpts{
+ "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..3a145f8 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
}
+
+// MetadataResult contains the result of a call for (potentially) multiple key-value pairs.
+type MetadataResult struct {
+ gophercloud.Result
+}
+
+// GetMetadataResult temporarily contains the response from a metadata Get call.
+type GetMetadataResult struct {
+ MetadataResult
+}
+
+// ResetMetadataResult temporarily contains the response from a metadata Reset call.
+type ResetMetadataResult struct {
+ MetadataResult
+}
+
+// UpdateMetadataResult temporarily contains the response from a metadata Update call.
+type UpdateMetadataResult struct {
+ MetadataResult
+}
+
+// MetadatumResult contains the result of a call for individual a single key-value pair.
+type MetadatumResult struct {
+ gophercloud.Result
+}
+
+// GetMetadatumResult temporarily contains the response from a metadatum Get call.
+type GetMetadatumResult struct {
+ MetadatumResult
+}
+
+// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
+type CreateMetadatumResult struct {
+ MetadatumResult
+}
+
+// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
+type DeleteMetadatumResult struct {
+ gophercloud.ErrResult
+}
+
+// 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:"metadata"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Metadata, err
+}
+
+// Extract interprets any MetadatumResult as a Metadatum, if possible.
+func (r MetadatumResult) Extract() (map[string]string, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Metadatum map[string]string `mapstructure:"meta"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ return response.Metadatum, err
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 57587ab..4bc6586 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 metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
+ return client.ServiceURL("servers", id, "metadata", key)
+}
+
+func metadataURL(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..17a1d28 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 TestMetadatumURL(t *testing.T) {
+ actual := metadatumURL(endpointClient(), "foo", "bar")
+ expected := endpoint + "servers/foo/metadata/bar"
+ th.CheckEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo")
+ expected := endpoint + "servers/foo/metadata"
+ th.CheckEquals(t, expected, actual)
+}