Merge pull request #54 from rackspace/resize-server

Add resize server action.
diff --git a/acceptance/09-resize-server.go b/acceptance/09-resize-server.go
new file mode 100644
index 0000000..558b2cf
--- /dev/null
+++ b/acceptance/09-resize-server.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+	"fmt"
+	"flag"
+	"github.com/rackspace/gophercloud"
+	"time"
+)
+
+var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
+
+func main() {
+	provider, username, password := getCredentials()
+	flag.Parse()
+
+	acc, err := gophercloud.Authenticate(
+		provider,
+		gophercloud.AuthOptions{
+			Username: username,
+			Password: password,
+		},
+	)
+	if err != nil {
+		panic(err)
+	}
+
+	api, err := gophercloud.ServersApi(acc, gophercloud.ApiCriteria{
+		Name:      "cloudServersOpenStack",
+		Region:    "DFW",
+		VersionId: "2",
+		UrlChoice: gophercloud.PublicURL,
+	})
+	if err != nil {
+		panic(err)
+	}
+
+	// These tests are going to take some time to complete.
+	// So, we'll do two tests at the same time to help amortize test time.
+	done := make(chan bool)
+	go resizeRejectTest(api, done)
+	go resizeAcceptTest(api, done)
+	_ = <- done
+	_ = <- done
+
+	if !*quiet {
+		fmt.Println("Done.")
+	}
+}
+
+func resizeRejectTest(api gophercloud.CloudServersProvider, done chan bool) {
+	withServer(api, func(id string) {
+		newFlavorId := findAlternativeFlavor()
+		err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "")
+		if err != nil {
+			panic(err)
+		}
+
+		waitForVerifyResize(api, id)
+
+		err = api.RevertResize(id)
+		if err != nil {
+			panic(err)
+		}
+	})
+	done <- true
+}
+
+func resizeAcceptTest(api gophercloud.CloudServersProvider, done chan bool) {
+	withServer(api, func(id string) {
+		newFlavorId := findAlternativeFlavor()
+		err := api.ResizeServer(id, randomString("ACPTTEST", 24), newFlavorId, "")
+		if err != nil {
+			panic(err)
+		}
+
+		waitForVerifyResize(api, id)
+
+		err = api.ConfirmResize(id)
+		if err != nil {
+			panic(err)
+		}
+	})
+	done <- true
+}
+
+func waitForVerifyResize(api gophercloud.CloudServersProvider, id string) {
+	for {
+		s, err := api.ServerById(id)
+		if err != nil {
+			panic(err)
+		}
+		if s.Status == "VERIFY_RESIZE" {
+			break
+		}
+		time.Sleep(10 * time.Second)
+	}
+}
+
+func withServer(api gophercloud.CloudServersProvider, f func(string)) {
+	id, err := createServer(api, "", "", "", "")
+	if err != nil {
+		panic(err)
+	}
+
+	for {
+		s, err := api.ServerById(id)
+		if err != nil {
+			panic(err)
+		}
+		if s.Status == "ACTIVE" {
+			break
+		}
+		time.Sleep(10 * time.Second)
+	}
+
+	f(id)
+
+	err = api.DeleteServerById(id)
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/acceptance/libargs.go b/acceptance/libargs.go
index 405938a..67858f1 100644
--- a/acceptance/libargs.go
+++ b/acceptance/libargs.go
@@ -122,3 +122,9 @@
 
 	return newServer.Id, nil
 }
+
+// findAlternativeFlavor locates a flavor to resize a server to.  It is guaranteed to be different
+// than what aSuitableFlavor() returns.  If none could be found, this function will panic.
+func findAlternativeFlavor() string {
+	return "3"  // 1GB image, up from 512MB image
+}
\ No newline at end of file
diff --git a/interfaces.go b/interfaces.go
index 089b6dd..de1f7c8 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -32,6 +32,9 @@
 	CreateServer(ns NewServer) (*NewServer, error)
 	DeleteServerById(id string) error
 	SetAdminPassword(id string, pw string) error
+	ResizeServer(id, newName, newFlavor, newDiskConfig string) error
+	RevertResize(id string) error
+	ConfirmResize(id string) error
 
   // Images
 
diff --git a/servers.go b/servers.go
index ee38e9f..78ad350 100644
--- a/servers.go
+++ b/servers.go
@@ -131,6 +131,62 @@
 	return err
 }
 
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) ResizeServer(id, newName, newFlavor, newDiskConfig string) error {
+	err := gsp.context.WithReauth(gsp.access, func() error {
+        url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
+        rr := ResizeRequest{
+                Name:       newName,
+                FlavorRef:  newFlavor,
+                DiskConfig: newDiskConfig,
+        }
+        return perigee.Post(url, perigee.Options{
+                ReqBody: &struct {
+                        Resize ResizeRequest `json:"resize"`
+                }{rr},
+                OkCodes: []int{202},
+                MoreHeaders: map[string]string{
+                        "X-Auth-Token": gsp.access.AuthToken(),
+                },
+        })
+	})
+	return err
+}
+
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) RevertResize(id string) error {
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
+		return perigee.Post(url, perigee.Options{
+			ReqBody: &struct {
+				RevertResize *int `json:"revertResize"`
+			}{nil},
+			OkCodes: []int{202},
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+		})
+	})
+	return err
+}
+
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) ConfirmResize(id string) error {
+	err := gsp.context.WithReauth(gsp.access, func() error {
+		url := fmt.Sprintf("%s/servers/%s/action", gsp.endpoint, id)
+		return perigee.Post(url, perigee.Options{
+			ReqBody: &struct {
+				ConfirmResize *int `json:"confirmResize"`
+			}{nil},
+			OkCodes: []int{204},
+			MoreHeaders: map[string]string{
+				"X-Auth-Token": gsp.access.AuthToken(),
+			},
+		})
+	})
+	return err
+}
+
 // RaxBandwidth provides measurement of server bandwidth consumed over a given audit interval.
 type RaxBandwidth struct {
 	AuditPeriodEnd    string `json:"audit_period_end"`
@@ -313,3 +369,12 @@
 	Links           []Link          `json:"links,omitempty"`
 	OsDcfDiskConfig string          `json:"OS-DCF:diskConfig,omitempty"`
 }
+
+// ResizeRequest structures are used internally to encode to JSON the parameters required to resize a server instance.
+// Client applications will not use this structure (no API accepts an instance of this structure).
+// See the Region method ResizeServer() for more details on how to resize a server.
+type ResizeRequest struct {
+        Name       string `json:"name,omitempty"`
+        FlavorRef  string `json:"flavorRef"`
+        DiskConfig string `json:"OS-DCF:diskConfig,omitempty"`
+}