Merge pull request #53 from rackspace/change-password

Change password
diff --git a/acceptance/04-create-server.go b/acceptance/04-create-server.go
index 9d29b0e..06c541b 100644
--- a/acceptance/04-create-server.go
+++ b/acceptance/04-create-server.go
@@ -22,46 +22,6 @@
 	flag.Parse()
 }
 
-func aSuitableImage(api gophercloud.CloudServersProvider) string {
-	images, err := api.ListImages()
-	if err != nil {
-		panic(err)
-	}
-
-	// TODO(sfalvo):
-	// Works for Rackspace, might not work for your provider!
-	// Need to figure out why ListImages() provides 0 values for
-	// Ram and Disk fields.
-	//
-	// Until then, just return Ubuntu 12.04 LTS.
-	for i := 0; i < len(images); i++ {
-		if images[i].Id == "6a668bb8-fb5d-407a-9a89-6f957bced767" {
-			return images[i].Id
-		}
-	}
-	panic("Image 6a668bb8-fb5d-407a-9a89-6f957bced767 (Ubuntu 12.04 LTS) not found.")
-}
-
-func aSuitableFlavor(api gophercloud.CloudServersProvider) string {
-	flavors, err := api.ListFlavors()
-	if err != nil {
-		panic(err)
-	}
-
-	// TODO(sfalvo):
-	// Works for Rackspace, might not work for your provider!
-	// Need to figure out why ListFlavors() provides 0 values for
-	// Ram and Disk fields.
-	//
-	// Until then, just return Ubuntu 12.04 LTS.
-	for i := 0; i < len(flavors); i++ {
-		if flavors[i].Id == "2" {
-			return flavors[i].Id
-		}
-	}
-	panic("Flavor 2 (512MB 1-core 20GB machine) not found.")
-}
-
 func main() {
 	configure()
 
@@ -86,20 +46,7 @@
 		panic(err)
 	}
 
-	if *imageRef == "" {
-		*imageRef = aSuitableImage(servers)
-	}
-
-	if *flavorRef == "" {
-		*flavorRef = aSuitableFlavor(servers)
-	}
-
-	_, err = servers.CreateServer(gophercloud.NewServer{
-		Name:      *serverName,
-		ImageRef:  *imageRef,
-		FlavorRef: *flavorRef,
-		AdminPass: *adminPass,
-	})
+	_, err = createServer(servers, *imageRef, *flavorRef, *serverName, *adminPass)
 	if err != nil {
 		panic(err)
 	}
diff --git a/acceptance/07-change-admin-password.go b/acceptance/07-change-admin-password.go
new file mode 100644
index 0000000..2ce7bbe
--- /dev/null
+++ b/acceptance/07-change-admin-password.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"github.com/rackspace/gophercloud"
+	"time"
+)
+
+var quiet = flag.Bool("quiet", false, "Quiet mode, for acceptance testing.  $? still indicates errors though.")
+var serverId = flag.String("i", "", "ID of server whose admin password is to be changed.")
+var newPass = flag.String("p", "", "New password for the server.")
+
+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)
+	}
+
+	// If user doesn't explicitly provide a server ID, create one dynamically.
+	if *serverId == "" {
+		var err error
+		*serverId, err = createServer(api, "", "", "", "")
+		if err != nil {
+			panic(err)
+		}
+
+		// Wait for server to finish provisioning.
+		for {
+			s, err := api.ServerById(*serverId)
+			if err != nil {
+				panic(err)
+			}
+			if s.Status == "ACTIVE" {
+				break
+			}
+			time.Sleep(10 * time.Second)
+		}
+	}
+
+	// If no password is provided, create one dynamically.
+	if *newPass == "" {
+		*newPass = randomString("", 16)
+	}
+
+	err = api.SetAdminPassword(*serverId, *newPass)
+	if err != nil {
+		panic(err)
+	}
+
+	if !*quiet {
+		fmt.Println("Password change request submitted.")
+	}
+}
diff --git a/acceptance/07-delete-server.go b/acceptance/99-delete-server.go
similarity index 100%
rename from acceptance/07-delete-server.go
rename to acceptance/99-delete-server.go
diff --git a/acceptance/libargs.go b/acceptance/libargs.go
index 1802591..405938a 100644
--- a/acceptance/libargs.go
+++ b/acceptance/libargs.go
@@ -4,6 +4,7 @@
 	"fmt"
 	"os"
 	"crypto/rand"
+	"github.com/rackspace/gophercloud"
 )
 
 // getCredentials will verify existence of needed credential information
@@ -37,4 +38,87 @@
         bytes[i] = alphanum[b % byte(len(alphanum))]
     }
     return prefix + string(bytes)
-}
\ No newline at end of file
+}
+
+// aSuitableImage finds a minimal image for use in dynamically creating servers.
+// If none can be found, this function will panic.
+func aSuitableImage(api gophercloud.CloudServersProvider) string {
+	images, err := api.ListImages()
+	if err != nil {
+		panic(err)
+	}
+
+	// TODO(sfalvo):
+	// Works for Rackspace, might not work for your provider!
+	// Need to figure out why ListImages() provides 0 values for
+	// Ram and Disk fields.
+	//
+	// Until then, just return Ubuntu 12.04 LTS.
+	for i := 0; i < len(images); i++ {
+		if images[i].Id == "23b564c9-c3e6-49f9-bc68-86c7a9ab5018" {
+			return images[i].Id
+		}
+	}
+	panic("Image 23b564c9-c3e6-49f9-bc68-86c7a9ab5018 (Ubuntu 12.04 LTS) not found.")
+}
+
+// aSuitableFlavor finds the minimum flavor capable of running the test image
+// chosen by aSuitableImage.  If none can be found, this function will panic.
+func aSuitableFlavor(api gophercloud.CloudServersProvider) string {
+	flavors, err := api.ListFlavors()
+	if err != nil {
+		panic(err)
+	}
+
+	// TODO(sfalvo):
+	// Works for Rackspace, might not work for your provider!
+	// Need to figure out why ListFlavors() provides 0 values for
+	// Ram and Disk fields.
+	//
+	// Until then, just return Ubuntu 12.04 LTS.
+	for i := 0; i < len(flavors); i++ {
+		if flavors[i].Id == "2" {
+			return flavors[i].Id
+		}
+	}
+	panic("Flavor 2 (512MB 1-core 20GB machine) not found.")
+}
+
+// createServer creates a new server in a manner compatible with acceptance testing.
+// In particular, it ensures that the name of the server always starts with "ACPTTEST--",
+// which the delete servers acceptance test relies on to identify servers to delete.
+// Passing in empty image and flavor references will force the use of reasonable defaults.
+// An empty name string will result in a dynamically created name prefixed with "ACPTTEST--".
+// A blank admin password will cause a password to be automatically generated; however,
+// at present no means of recovering this password exists, as no acceptance tests yet require
+// this data.
+func createServer(servers gophercloud.CloudServersProvider, imageRef, flavorRef, name, adminPass string) (string, error) {
+	if imageRef == "" {
+		imageRef = aSuitableImage(servers)
+	}
+
+	if flavorRef == "" {
+		flavorRef = aSuitableFlavor(servers)
+	}
+
+	if len(name) < 1 {
+		name = randomString("ACPTTEST", 16)
+	}
+
+	if (len(name) < 8) || (name[0:8] != "ACPTTEST") {
+		name = fmt.Sprintf("ACPTTEST--%s", name)
+	}
+
+	newServer, err := servers.CreateServer(gophercloud.NewServer{
+		Name:      name,
+		ImageRef:  imageRef,
+		FlavorRef: flavorRef,
+		AdminPass: adminPass,
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	return newServer.Id, nil
+}
diff --git a/interfaces.go b/interfaces.go
index 4342553..089b6dd 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -27,10 +27,11 @@
   // Servers
 
 	ListServers() ([]Server, error)
-  ListServersLinksOnly() ([]Server, error)
+	ListServersLinksOnly() ([]Server, error)
 	ServerById(id string) (*Server, error)
 	CreateServer(ns NewServer) (*NewServer, error)
 	DeleteServerById(id string) error
+	SetAdminPassword(id string, pw string) error
 
   // Images
 
diff --git a/servers.go b/servers.go
index 4f7cc1e..ee38e9f 100644
--- a/servers.go
+++ b/servers.go
@@ -5,6 +5,7 @@
 
 import (
 	"github.com/racker/perigee"
+	"fmt"
 )
 
 // genericServersProvider structures provide the implementation for generic OpenStack-compatible
@@ -107,6 +108,29 @@
 	return err
 }
 
+// See the CloudServersProvider interface for details.
+func (gsp *genericServersProvider) SetAdminPassword(id, pw 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 {
+				ChangePassword struct {
+					AdminPass string `json:"adminPass"`
+				} `json:"changePassword"`
+			}{
+				struct {
+					AdminPass string `json:"adminPass"`
+				}{pw},
+			},
+			OkCodes: []int{202},
+			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"`