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 {