Merge pull request #421 from ZettaIO/serveraction-createimage

server action to create a server image / snapshot
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index e47bc0e..4339a16 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -651,3 +651,14 @@
 			}`)
 	})
 }
+
+// HandleCreateServerImageSuccessfully sets up the test server to respond to a TestCreateServerImage request.
+func HandleCreateServerImageSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/serverimage/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Location", "https://0.0.0.0/images/xxxx-xxxxx-xxxxx-xxxx")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index e0950e4..aa8c1a8 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -14,7 +14,6 @@
 type ListOptsBuilder interface {
 	ToServerListQuery() (string, error)
 }
-
 // ListOpts allows the filtering and sorting of paginated collections through
 // the API. Filtering is achieved by passing in struct field values that map to
 // the server attributes you want to see returned. Marker and Limit are used
@@ -701,3 +700,46 @@
 	}
 	return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), createPageFn)
 }
+
+type CreateImageOpts struct {
+	// Name [required] of the image/snapshot
+	Name string
+	// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the created image.
+	Metadata map[string]string
+}
+
+type CreateImageOptsBuilder interface {
+	ToServerCreateImageMap() (map[string]interface{}, error)
+}
+
+// ToServerCreateImageMap formats a CreateImageOpts structure into a request body.
+func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) {
+	var err error
+	img := make(map[string]interface{})
+	if opts.Name == "" {
+		return nil, fmt.Errorf("Cannot create a server image without a name")
+	}
+	img["name"] = opts.Name
+	if opts.Metadata != nil {
+		img["metadata"] = opts.Metadata
+	}
+	createImage := make(map[string]interface{})
+	createImage["createImage"] = img
+	return createImage, err
+}
+
+// CreateImage makes a request against the nova API to schedule an image to be created of the server
+func CreateImage(client *gophercloud.ServiceClient, serverId string, opts CreateImageOptsBuilder) CreateImageResult {
+	var res CreateImageResult
+	reqBody, err := opts.ToServerCreateImageMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+	response, err := client.Post(actionURL(client, serverId), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	res.Err = err
+	res.Header = response.Header
+	return res	
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 62b89e0..1f39fe1 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -325,3 +325,12 @@
 	th.AssertNoErr(t, err)
 	th.CheckEquals(t, 1, pages)
 }
+
+func TestCreateServerImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateServerImageSuccessfully(t)
+
+	_, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index e2be6ba..f278709 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -2,6 +2,9 @@
 
 import (
 	"reflect"
+	"fmt"
+	"path"
+	"net/url"
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/rackspace/gophercloud"
@@ -74,6 +77,28 @@
 	ActionResult
 }
 
+// CreateImageResult represents the result of an image creation operation
+type CreateImageResult struct {
+	gophercloud.Result
+}
+
+// ExtractImageID gets the ID of the newly created server image from the header
+func (res CreateImageResult) ExtractImageID() (string, error) {
+	if res.Err != nil {
+		return "", res.Err
+	}
+	// Get the image id from the header
+	u, err := url.ParseRequestURI(res.Header.Get("Location"))
+	if err != nil {
+		return "", fmt.Errorf("Failed to parse the image id: %s", err.Error())
+	}
+	imageId := path.Base(u.Path)
+	if imageId == "." || imageId == "/" {
+		return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u)
+	}
+	return imageId, nil
+}
+
 // Extract interprets any RescueResult as an AdminPass, if possible.
 func (r RescueResult) Extract() (string, error) {
 	if r.Err != nil {