Add os-volume_upload_image action to volumeactions (#240)

* Add os-volume_upload_image action to volumeactions

* Code updates to align with style guide
diff --git a/acceptance/openstack/blockstorage/extensions/extensions.go b/acceptance/openstack/blockstorage/extensions/extensions.go
index 05a3e12..3a859c9 100644
--- a/acceptance/openstack/blockstorage/extensions/extensions.go
+++ b/acceptance/openstack/blockstorage/extensions/extensions.go
@@ -7,11 +7,66 @@
 	"testing"
 
 	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/acceptance/tools"
 	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
 	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
 	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
 )
 
+// CreateUploadImage will upload volume it as volume-baked image. An name of new image or err will be
+// returned
+func CreateUploadImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume) (string, error) {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume-backed image uploading in short mode.")
+	}
+
+	imageName := tools.RandomString("ACPTTEST", 16)
+	uploadImageOpts := volumeactions.UploadImageOpts{
+		ImageName: imageName,
+		Force:     true,
+	}
+
+	if err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).ExtractErr(); err != nil {
+		return "", err
+	}
+
+	t.Logf("Uploading volume %s as volume-backed image %s", volume.ID, imageName)
+
+	if err := volumes.WaitForStatus(client, volume.ID, "available", 60); err != nil {
+		return "", err
+	}
+
+	t.Logf("Uploaded volume %s as volume-backed image %s", volume.ID, imageName)
+
+	return imageName, nil
+
+}
+
+// DeleteUploadedImage deletes uploaded image. An error will be returned
+// if the deletion request failed.
+func DeleteUploadedImage(t *testing.T, client *gophercloud.ServiceClient, imageName string) error {
+	if testing.Short() {
+		t.Skip("Skipping test that requires volume-backed image removing in short mode.")
+	}
+
+	t.Logf("Getting image id for image name %s", imageName)
+
+	imageID, err := images.IDFromName(client, imageName)
+	if err != nil {
+		return err
+	}
+
+	t.Logf("Removing image %s", imageID)
+
+	err = images.Delete(client, imageID).ExtractErr()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // CreateVolumeAttach will attach a volume to an instance. An error will be
 // returned if the attachment failed.
 func CreateVolumeAttach(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, server *servers.Server) error {
diff --git a/acceptance/openstack/blockstorage/extensions/volumeactions_test.go b/acceptance/openstack/blockstorage/extensions/volumeactions_test.go
index a088dd6..d15d17a 100644
--- a/acceptance/openstack/blockstorage/extensions/volumeactions_test.go
+++ b/acceptance/openstack/blockstorage/extensions/volumeactions_test.go
@@ -12,6 +12,33 @@
 	compute "github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2"
 )
 
+func TestVolumeActionsUploadImageDestroy(t *testing.T) {
+	blockClient, err := clients.NewBlockStorageV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a blockstorage client: %v", err)
+	}
+	computeClient, err := clients.NewComputeV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create a compute client: %v", err)
+	}
+
+	volume, err := blockstorage.CreateVolume(t, blockClient)
+	if err != nil {
+		t.Fatalf("Unable to create volume: %v", err)
+	}
+	defer blockstorage.DeleteVolume(t, blockClient, volume)
+
+	imageName, err := CreateUploadImage(t, blockClient, volume)
+	if err != nil {
+		t.Fatalf("Unable to upload volume-backed image: %v", err)
+	}
+
+	err = DeleteUploadedImage(t, computeClient, imageName)
+	if err != nil {
+		t.Fatalf("Unable to delete volume-backed image: %v", err)
+	}
+}
+
 func TestVolumeActionsAttachCreateDestroy(t *testing.T) {
 	blockClient, err := clients.NewBlockStorageV2Client()
 	if err != nil {
diff --git a/openstack/blockstorage/extensions/volumeactions/requests.go b/openstack/blockstorage/extensions/volumeactions/requests.go
index 1aff494..e3c7df3 100644
--- a/openstack/blockstorage/extensions/volumeactions/requests.go
+++ b/openstack/blockstorage/extensions/volumeactions/requests.go
@@ -214,3 +214,40 @@
 	})
 	return
 }
+
+// UploadImageOptsBuilder allows extensions to add additional parameters to the
+// UploadImage request.
+type UploadImageOptsBuilder interface {
+	ToVolumeUploadImageMap() (map[string]interface{}, error)
+}
+
+// UploadImageOpts contains options for uploading a Volume to image storage.
+type UploadImageOpts struct {
+	// Container format, may be bare, ofv, ova, etc.
+	ContainerFormat string `json:"container_format,omitempty"`
+	// Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc.
+	DiskFormat string `json:"disk_format,omitempty"`
+	// The name of image that will be stored in glance
+	ImageName string `json:"image_name,omitempty"`
+	// Force image creation, usable if volume attached to instance
+	Force bool `json:"force,omitempty"`
+}
+
+// ToVolumeUploadImageMap assembles a request body based on the contents of a
+// UploadImageOpts.
+func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "os-volume_upload_image")
+}
+
+// UploadImage will upload image base on the values in UploadImageOptsBuilder
+func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) {
+	b, err := opts.ToVolumeUploadImageMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(uploadURL(client, id), b, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+	return
+}
diff --git a/openstack/blockstorage/extensions/volumeactions/results.go b/openstack/blockstorage/extensions/volumeactions/results.go
index b5695b7..634b04d 100644
--- a/openstack/blockstorage/extensions/volumeactions/results.go
+++ b/openstack/blockstorage/extensions/volumeactions/results.go
@@ -17,6 +17,11 @@
 	gophercloud.ErrResult
 }
 
+// UploadImageResult contains the response body and error from a UploadImage request.
+type UploadImageResult struct {
+	gophercloud.ErrResult
+}
+
 // ReserveResult contains the response body and error from a Get request.
 type ReserveResult struct {
 	gophercloud.ErrResult
diff --git a/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go b/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go
index 4c3c0dd..d914097 100644
--- a/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go
+++ b/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go
@@ -74,6 +74,31 @@
 		})
 }
 
+func MockUploadImageResponse(t *testing.T) {
+	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "POST")
+			th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+			th.TestHeader(t, r, "Content-Type", "application/json")
+			th.TestHeader(t, r, "Accept", "application/json")
+			th.TestJSONRequest(t, r, `
+{
+    "os-volume_upload_image": {
+        "container_format": "bare",
+        "force": true,
+        "image_name": "test",
+        "disk_format": "raw"
+    }
+}
+          `)
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusAccepted)
+
+			fmt.Fprintf(w, `{}`)
+		})
+}
+
 func MockReserveResponse(t *testing.T) {
 	th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action",
 		func(w http.ResponseWriter, r *http.Request) {
diff --git a/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go b/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go
index b1f7af7..6132161 100644
--- a/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go
+++ b/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go
@@ -44,6 +44,21 @@
 	th.AssertNoErr(t, err)
 }
 
+func TestUploadImage(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	MockUploadImageResponse(t)
+	options := &volumeactions.UploadImageOpts{
+		ContainerFormat: "bare",
+		DiskFormat:      "raw",
+		ImageName:       "test",
+		Force:           true,
+	}
+
+	err := volumeactions.UploadImage(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
 func TestReserve(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/blockstorage/extensions/volumeactions/urls.go b/openstack/blockstorage/extensions/volumeactions/urls.go
index a172549..5efd2b2 100644
--- a/openstack/blockstorage/extensions/volumeactions/urls.go
+++ b/openstack/blockstorage/extensions/volumeactions/urls.go
@@ -14,6 +14,10 @@
 	return attachURL(c, id)
 }
 
+func uploadURL(c *gophercloud.ServiceClient, id string) string {
+	return attachURL(c, id)
+}
+
 func reserveURL(c *gophercloud.ServiceClient, id string) string {
 	return attachURL(c, id)
 }