server metadata operations and tests
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
+}