merging in changes from v0.2.0 branch
diff --git a/acceptance/openstack/compute_test.go b/acceptance/openstack/compute_test.go
index 8d5fe28..d7c0eba 100644
--- a/acceptance/openstack/compute_test.go
+++ b/acceptance/openstack/compute_test.go
@@ -186,3 +186,158 @@
return
}
}
+
+func TestActionChangeAdminPassword(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = changeAdminPassword(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionReboot(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = servers.Reboot(ts.client, ts.createdServer.Id, "aldhjflaskhjf")
+ if err == nil {
+ t.Fatal("Expected the SDK to provide an ArgumentError here")
+ }
+
+ err = rebootServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionRebuild(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = rebuildServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeConfirm(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = resizeServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = confirmResize(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeRevert(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = resizeServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = revertResize(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/acceptance/openstack/tools_test.go b/acceptance/openstack/tools_test.go
index d27ab0a..042ab34 100644
--- a/acceptance/openstack/tools_test.go
+++ b/acceptance/openstack/tools_test.go
@@ -31,6 +31,7 @@
updatedServer *servers.Server
serverName string
alternateName string
+ flavorIdResize string
}
func setupForList(service string) (*testState, error) {
@@ -80,6 +81,15 @@
return ts, fmt.Errorf("Expected OS_FLAVOR_ID environment variable to be set")
}
+ ts.flavorIdResize = os.Getenv("OS_FLAVOR_ID_RESIZE")
+ if ts.flavorIdResize == "" {
+ return ts, fmt.Errorf("Expected OS_FLAVOR_ID_RESIZE environment variable to be set")
+ }
+
+ if ts.flavorIdResize == ts.flavorId {
+ return ts, fmt.Errorf("OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE cannot be the same")
+ }
+
ts.region = os.Getenv("OS_REGION_NAME")
if ts.region == "" {
ts.region = ts.eps[0].Region
@@ -169,13 +179,13 @@
}
if ts.gottenServer.Status == s {
- fmt.Printf("Server created after %d seconds (approximately)\n", 300-timeout)
+ fmt.Printf("Server reached state %s after %d seconds (approximately)\n", s, 300-timeout)
break
}
}
if err == errTimeout {
- fmt.Printf("I'm not waiting around.\n")
+ fmt.Printf("Time out -- I'm not waiting around.\n")
err = nil
}
@@ -229,6 +239,116 @@
return err
}
+func makeNewPassword(oldPass string) string {
+ fmt.Println("Current password: "+oldPass)
+ randomPassword := randomString("", 16)
+ for randomPassword == oldPass {
+ randomPassword = randomString("", 16)
+ }
+ fmt.Println(" New password: "+randomPassword)
+ return randomPassword
+}
+
+func changeAdminPassword(ts *testState) error {
+ randomPassword := makeNewPassword(ts.createdServer.AdminPass)
+
+ err := servers.ChangeAdminPassword(ts.client, ts.createdServer.Id, randomPassword)
+ if err != nil {
+ return err
+ }
+
+ err = waitForStatus(ts, "PASSWORD")
+ if err != nil {
+ return err
+ }
+
+ return waitForStatus(ts, "ACTIVE")
+}
+
+func rebootServer(ts *testState) error {
+ fmt.Println("Attempting reboot of server "+ts.createdServer.Id)
+ err := servers.Reboot(ts.client, ts.createdServer.Id, servers.OSReboot)
+ if err != nil {
+ return err
+ }
+
+ err = waitForStatus(ts, "REBOOT")
+ if err != nil {
+ return err
+ }
+
+ return waitForStatus(ts, "ACTIVE")
+}
+
+func rebuildServer(ts *testState) error {
+ fmt.Println("Attempting to rebuild server "+ts.createdServer.Id)
+
+ newPassword := makeNewPassword(ts.createdServer.AdminPass)
+ newName := randomString("ACPTTEST", 16)
+ sr, err := servers.Rebuild(ts.client, ts.createdServer.Id, newName, newPassword, ts.imageId, nil)
+ if err != nil {
+ return err
+ }
+
+ s, err := servers.GetServer(sr)
+ if err != nil {
+ return err
+ }
+ if s.Id != ts.createdServer.Id {
+ return fmt.Errorf("Expected rebuilt server ID of %s; got %s", ts.createdServer.Id, s.Id)
+ }
+
+ err = waitForStatus(ts, "REBUILD")
+ if err != nil {
+ return err
+ }
+
+ return waitForStatus(ts, "ACTIVE")
+}
+
+func resizeServer(ts *testState) error {
+ fmt.Println("Attempting to resize server "+ts.createdServer.Id)
+
+ err := servers.Resize(ts.client, ts.createdServer.Id, ts.flavorIdResize)
+ if err != nil {
+ return err
+ }
+
+ err = waitForStatus(ts, "RESIZE")
+ if err != nil {
+ return err
+ }
+
+ return waitForStatus(ts, "VERIFY_RESIZE")
+}
+
+func confirmResize(ts *testState) error {
+ fmt.Println("Attempting to confirm resize for server "+ts.createdServer.Id)
+
+ err := servers.ConfirmResize(ts.client, ts.createdServer.Id)
+ if err != nil {
+ return err
+ }
+
+ return waitForStatus(ts, "ACTIVE")
+}
+
+func revertResize(ts *testState) error {
+ fmt.Println("Attempting to revert resize for server "+ts.createdServer.Id)
+
+ err := servers.RevertResize(ts.client, ts.createdServer.Id)
+ if err != nil {
+ return err
+ }
+
+ err = waitForStatus(ts, "REVERT_RESIZE")
+ if err != nil {
+ return err
+ }
+
+ return waitForStatus(ts, "ACTIVE")
+}
+
// randomString generates a string of given length, but random content.
// All content will be within the ASCII graphic character set.
// (Implementation from Even Shaw's contribution on
diff --git a/openstack/compute/images/images.go b/openstack/compute/images/images.go
index cc08589..c881092 100644
--- a/openstack/compute/images/images.go
+++ b/openstack/compute/images/images.go
@@ -44,3 +44,9 @@
}
return images, nil
}
+
+func GetImage(ir ImageResults) (Image, error) {
+ image := Image{}
+ err := mapstructure.Decode(ir, &image)
+ return image, err
+}
diff --git a/openstack/compute/images/images_test.go b/openstack/compute/images/images_test.go
new file mode 100644
index 0000000..05a8550
--- /dev/null
+++ b/openstack/compute/images/images_test.go
@@ -0,0 +1,43 @@
+package images
+
+import (
+ "testing"
+ "encoding/json"
+)
+
+const (
+ // This example was taken from: http://docs.openstack.org/api/openstack-compute/2/content/Rebuild_Server-d1e3538.html
+
+ simpleImageJson = `{
+ "id": "52415800-8b69-11e0-9b19-734f6f006e54",
+ "name": "CentOS 5.2",
+ "links": [{
+ "rel": "self",
+ "href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
+ },{
+ "rel": "bookmark",
+ "href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
+ }]
+ }`
+)
+
+func TestGetImage(t *testing.T) {
+ var simpleImageMap map[string]interface{}
+ err := json.Unmarshal([]byte(simpleImageJson), &simpleImageMap)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ image, err := GetImage(simpleImageMap)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if image.Id != "52415800-8b69-11e0-9b19-734f6f006e54" {
+ t.Fatal("I expected an image ID of 52415800-8b69-11e0-9b19-734f6f006e54; got "+image.Id)
+ }
+
+ if image.Name != "CentOS 5.2" {
+ t.Fatal("I expected an image name of CentOS 5.2; got "+image.Name)
+ }
+}
diff --git a/openstack/compute/images/requests.go b/openstack/compute/images/requests.go
index ff875c9..79783c4 100644
--- a/openstack/compute/images/requests.go
+++ b/openstack/compute/images/requests.go
@@ -8,6 +8,7 @@
var ErrNotImplemented = fmt.Errorf("Images functionality not implemented.")
type ListResults map[string]interface{}
+type ImageResults map[string]interface{}
func List(c *Client) (ListResults, error) {
var lr ListResults
diff --git a/openstack/compute/servers/client.go b/openstack/compute/servers/client.go
index 3d51dd8..3ce6965 100644
--- a/openstack/compute/servers/client.go
+++ b/openstack/compute/servers/client.go
@@ -42,6 +42,10 @@
return c.getDeleteUrl(id)
}
+func (c *Client) getActionUrl(id string) string {
+ return fmt.Sprintf("%s/servers/%s/action", c.endpoint, id)
+}
+
func (c *Client) getListHeaders() (map[string]string, error) {
t, err := c.getAuthToken()
if err != nil {
@@ -69,6 +73,10 @@
return c.getListHeaders()
}
+func (c *Client) getActionHeaders() (map[string]string, error) {
+ return c.getListHeaders()
+}
+
func (c *Client) getAuthToken() (string, error) {
var err error
diff --git a/openstack/compute/servers/requests.go b/openstack/compute/servers/requests.go
index 28abd1c..2a004f6 100644
--- a/openstack/compute/servers/requests.go
+++ b/openstack/compute/servers/requests.go
@@ -2,6 +2,7 @@
import (
"github.com/racker/perigee"
+ "fmt"
)
// ListResult abstracts the raw results of making a List() request against the
@@ -10,6 +11,12 @@
// provided through separate, type-safe accessors or methods.
type ListResult map[string]interface{}
+// ServerResult abstracts a single server description,
+// as returned by the OpenStack provider.
+// As OpenStack extensions may freely alter the response bodies of the
+// structures returned to the client,
+// you may only safely access the data provided through
+// separate, type-safe accessors or methods.
type ServerResult map[string]interface{}
// List makes a request against the API to list servers accessible to you.
@@ -96,3 +103,217 @@
})
return sr, err
}
+
+// ChangeAdminPassword alters the administrator or root password for a specified
+// server.
+func ChangeAdminPassword(c *Client, id, newPassword string) error {
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct{C map[string]string `json:"changePassword"`}{
+ map[string]string{"adminPass": newPassword},
+ },
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// ErrArgument errors occur when an argument supplied to a package function
+// fails to fall within acceptable values. For example, the Reboot() function
+// expects the "how" parameter to be one of HardReboot or SoftReboot. These
+// constants are (currently) strings, leading someone to wonder if they can pass
+// other string values instead, perhaps in an effort to break the API of their
+// provider. Reboot() returns this error in this situation.
+//
+// Function identifies which function was called/which function is generating
+// the error.
+// Argument identifies which formal argument was responsible for producing the
+// error.
+// Value provides the value as it was passed into the function.
+type ErrArgument struct {
+ Function, Argument string
+ Value interface{}
+}
+
+// Error yields a useful diagnostic for debugging purposes.
+func (e *ErrArgument) Error() string {
+ return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value)
+}
+
+func (e *ErrArgument) String() string {
+ return e.Error()
+}
+
+// These constants determine how a server should be rebooted.
+// See the Reboot() function for further details.
+const (
+ SoftReboot = "SOFT"
+ HardReboot = "HARD"
+ OSReboot = SoftReboot
+ PowerCycle = HardReboot
+)
+
+// Reboot requests that a given server reboot.
+// Two methods exist for rebooting a server:
+//
+// HardReboot (aka PowerCycle) -- restarts the server instance by physically
+// cutting power to the machine, or if a VM, terminating it at the hypervisor
+// level. It's done. Caput. Full stop. Then, after a brief while, power is
+// restored or the VM instance restarted.
+//
+// SoftReboot (aka OSReboot). This approach simply tells the OS to restart
+// under its own procedures. E.g., in Linux, asking it to enter runlevel 6,
+// or executing "sudo shutdown -r now", or by wasking Windows to restart the
+// machine.
+func Reboot(c *Client, id, how string) error {
+ if (how != SoftReboot) && (how != HardReboot) {
+ return &ErrArgument{
+ Function: "Reboot",
+ Argument: "how",
+ Value: how,
+ }
+ }
+
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct{C map[string]string `json:"reboot"`}{
+ map[string]string{"type": how},
+ },
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// Rebuild requests that the Openstack provider reprovision the
+// server. The rebuild will need to know the server's name and
+// new image reference or ID. In addition, and unlike building
+// a server with Create(), you must provide an administrator
+// password.
+//
+// Additional options may be specified with the additional map.
+// This function treats a nil map the same as an empty map.
+//
+// Rebuild returns a server result as though you had called
+// GetDetail() on the server's ID. The information, however,
+// refers to the new server, not the old.
+func Rebuild(c *Client, id, name, password, imageRef string, additional map[string]interface{}) (ServerResult, error) {
+ var sr ServerResult
+
+ if id == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "id",
+ Value: "",
+ }
+ }
+
+ if name == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "name",
+ Value: "",
+ }
+ }
+
+ if password == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "password",
+ Value: "",
+ }
+ }
+
+ if imageRef == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "imageRef",
+ Value: "",
+ }
+ }
+
+ if additional == nil {
+ additional = make(map[string]interface{}, 0)
+ }
+
+ additional["name"] = name
+ additional["imageRef"] = imageRef
+ additional["adminPass"] = password
+
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return sr, err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct{R map[string]interface{} `json:"rebuild"`}{
+ additional,
+ },
+ Results: &sr,
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return sr, err
+}
+
+// Resize instructs the provider to change the flavor of the server.
+// Note that this implies rebuilding it. Unfortunately, one cannot pass rebuild parameters to the resize function.
+// When the resize completes, the server will be in RESIZE_VERIFY state.
+// While in this state, you can explore the use of the new server's configuration.
+// If you like it, call ConfirmResize() to commit the resize permanently.
+// Otherwise, call RevertResize() to restore the old configuration.
+func Resize(c *Client, id, flavorRef string) error {
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct{R map[string]interface{} `json:"resize"`}{
+ map[string]interface{}{"flavorRef": flavorRef},
+ },
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// ConfirmResize confirms a previous resize operation on a server.
+// See Resize() for more details.
+func ConfirmResize(c *Client, id string) error {
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: map[string]interface{}{"confirmResize": nil},
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// RevertResize cancels a previous resize operation on a server.
+// See Resize() for more details.
+func RevertResize(c *Client, id string) error {
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: map[string]interface{}{"revertResize": nil},
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
diff --git a/openstack/compute/servers/servers.go b/openstack/compute/servers/servers.go
index e3bdbe3..28d66d0 100644
--- a/openstack/compute/servers/servers.go
+++ b/openstack/compute/servers/servers.go
@@ -36,6 +36,9 @@
// Metadata includes a list of all user-specified key-value pairs attached to the server.
//
// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+//
+// AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place.
+// Note that this is the ONLY time this field will be valid.
type Server struct {
Id string
TenantId string `mapstructure:tenant_id`
@@ -53,6 +56,7 @@
Addresses map[string]interface{}
Metadata map[string]interface{}
Links []interface{}
+ AdminPass string `mapstructure:adminPass`
}
// GetServers interprets the result of a List() call, producing a slice of Server entities.