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)
+}