Merge pull request #256 from smashwilson/disk-config
Extension: Disk Config
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
index c3465cd..af4bbe0 100644
--- a/acceptance/rackspace/compute/v2/servers_test.go
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -7,14 +7,30 @@
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+ oskey "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"
os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/compute/v2/keypairs"
"github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
th "github.com/rackspace/gophercloud/testhelper"
)
-func createServer(t *testing.T, client *gophercloud.ServiceClient) *os.Server {
- if testing.Short(){
+func createServerKeyPair(t *testing.T, client *gophercloud.ServiceClient) *oskey.KeyPair {
+ name := tools.RandomString("importedkey-", 8)
+ pubkey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDlIQ3r+zd97kb9Hzmujd3V6pbO53eb3Go4q2E8iqVGWQfZTrFdL9KACJnqJIm9HmncfRkUTxE37hqeGCCv8uD+ZPmPiZG2E60OX1mGDjbbzAyReRwYWXgXHopggZTLak5k4mwZYaxwaufbVBDRn847e01lZnaXaszEToLM37NLw+uz29sl3TwYy2R0RGHPwPc160aWmdLjSyd1Nd4c9pvvOP/EoEuBjIC6NJJwg2Rvg9sjjx9jYj0QUgc8CqKLN25oMZ69kNJzlFylKRUoeeVr89txlR59yehJWk6Uw6lYFTdJmcmQOFVAJ12RMmS1hLWCM8UzAgtw+EDa0eqBxBDl smash@winter"
+
+ k, err := keypairs.Create(client, oskey.CreateOpts{
+ Name: name,
+ PublicKey: pubkey,
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ return k
+}
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient, keyName string) *os.Server {
+ if testing.Short() {
t.Skip("Skipping test that requires server creation in short mode.")
}
@@ -23,10 +39,12 @@
name := tools.RandomString("Gophercloud-", 8)
t.Logf("Creating server [%s].", name)
- s, err := servers.Create(client, &os.CreateOpts{
- Name: name,
- ImageRef: options.imageID,
- FlavorRef: options.flavorID,
+ s, err := servers.Create(client, &servers.CreateOpts{
+ Name: name,
+ ImageRef: options.imageID,
+ FlavorRef: options.flavorID,
+ KeyPair: keyName,
+ DiskConfig: diskconfig.Manual,
}).Extract()
th.AssertNoErr(t, err)
t.Logf("Creating server.")
@@ -122,10 +140,11 @@
options, err := optionsFromEnv()
th.AssertNoErr(t, err)
- opts := os.RebuildOpts{
- Name: tools.RandomString("RenamedGopher", 16),
- AdminPass: tools.MakeNewPassword(server.AdminPass),
- ImageID: options.imageID,
+ opts := servers.RebuildOpts{
+ Name: tools.RandomString("RenamedGopher", 16),
+ AdminPass: tools.MakeNewPassword(server.AdminPass),
+ ImageID: options.imageID,
+ DiskConfig: diskconfig.Manual,
}
after, err := servers.Rebuild(client, server.ID, opts).Extract()
th.AssertNoErr(t, err)
@@ -147,11 +166,23 @@
t.Logf("Server deleted successfully.")
}
+func deleteServerKeyPair(t *testing.T, client *gophercloud.ServiceClient, k *oskey.KeyPair) {
+ t.Logf("> keypairs.Delete")
+
+ err := keypairs.Delete(client, k.Name).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Keypair deleted successfully.")
+}
+
func TestServerOperations(t *testing.T) {
client, err := newClient()
th.AssertNoErr(t, err)
- server := createServer(t, client)
+ kp := createServerKeyPair(t, client)
+ defer deleteServerKeyPair(t, client, kp)
+
+ server := createServer(t, client, kp.Name)
defer deleteServer(t, client, server)
getServer(t, client, server)
diff --git a/openstack/compute/v2/extensions/diskconfig/requests.go b/openstack/compute/v2/extensions/diskconfig/requests.go
new file mode 100644
index 0000000..06a922a
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/requests.go
@@ -0,0 +1,107 @@
+package diskconfig
+
+import (
+ "errors"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// DiskConfig represents one of the two possible settings for the DiskConfig option when creating,
+// rebuilding, or resizing servers: Auto or Manual.
+type DiskConfig string
+
+const (
+ // Auto builds a server with a single partition the size of the target flavor disk and
+ // automatically adjusts the filesystem to fit the entire partition. Auto may only be used with
+ // images and servers that use a single EXT3 partition.
+ Auto DiskConfig = "AUTO"
+
+ // Manual builds a server using whatever partition scheme and filesystem are present in the source
+ // image. If the target flavor disk is larger, the remaining space is left unpartitioned. This
+ // enables images to have non-EXT3 filesystems, multiple partitions, and so on, and enables you
+ // to manage the disk configuration. It also results in slightly shorter boot times.
+ Manual DiskConfig = "MANUAL"
+)
+
+// ErrInvalidDiskConfig is returned if an invalid string is specified for a DiskConfig option.
+var ErrInvalidDiskConfig = errors.New("DiskConfig must be either diskconfig.Auto or diskconfig.Manual.")
+
+// Validate ensures that a DiskConfig contains an appropriate value.
+func (config DiskConfig) validate() error {
+ switch config {
+ case Auto, Manual:
+ return nil
+ default:
+ return ErrInvalidDiskConfig
+ }
+}
+
+// CreateOptsExt adds a DiskConfig option to the base CreateOpts.
+type CreateOptsExt struct {
+ servers.CreateOptsBuilder
+
+ // DiskConfig [optional] controls how the created server's disk is partitioned.
+ DiskConfig DiskConfig
+}
+
+// ToServerCreateMap adds the diskconfig option to the base server creation options.
+func (opts CreateOptsExt) ToServerCreateMap() map[string]interface{} {
+ base := opts.CreateOptsBuilder.ToServerCreateMap()
+
+ serverMap := base["server"].(map[string]interface{})
+ serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig)
+
+ return base
+}
+
+// RebuildOptsExt adds a DiskConfig option to the base RebuildOpts.
+type RebuildOptsExt struct {
+ servers.RebuildOptsBuilder
+
+ // DiskConfig [optional] controls how the rebuilt server's disk is partitioned.
+ DiskConfig DiskConfig
+}
+
+// ToServerRebuildMap adds the diskconfig option to the base server rebuild options.
+func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) {
+ err := opts.DiskConfig.validate()
+ if err != nil {
+ return nil, err
+ }
+
+ base, err := opts.RebuildOptsBuilder.ToServerRebuildMap()
+ if err != nil {
+ return nil, err
+ }
+
+ serverMap := base["rebuild"].(map[string]interface{})
+ serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig)
+
+ return base, nil
+}
+
+// ResizeOptsExt adds a DiskConfig option to the base server resize options.
+type ResizeOptsExt struct {
+ servers.ResizeOptsBuilder
+
+ // DiskConfig [optional] controls how the resized server's disk is partitioned.
+ DiskConfig DiskConfig
+}
+
+// ToServerResizeMap adds the diskconfig option to the base server creation options.
+func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) {
+ err := opts.DiskConfig.validate()
+ if err != nil {
+ return nil, err
+ }
+
+ base, err := opts.ResizeOptsBuilder.ToServerResizeMap()
+ if err != nil {
+ return nil, err
+ }
+
+ serverMap := base["resize"].(map[string]interface{})
+ serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig)
+
+ return base, nil
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/requests_test.go b/openstack/compute/v2/extensions/diskconfig/requests_test.go
new file mode 100644
index 0000000..1f4f626
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/requests_test.go
@@ -0,0 +1,85 @@
+package diskconfig
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ ImageRef: "asdfasdfasdf",
+ FlavorRef: "performance1-1",
+ }
+
+ ext := CreateOptsExt{
+ CreateOptsBuilder: base,
+ DiskConfig: Manual,
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "asdfasdfasdf",
+ "flavorRef": "performance1-1",
+ "OS-DCF:diskConfig": "MANUAL"
+ }
+ }
+ `
+ th.CheckJSONEquals(t, expected, ext.ToServerCreateMap())
+}
+
+func TestRebuildOpts(t *testing.T) {
+ base := servers.RebuildOpts{
+ Name: "rebuiltserver",
+ AdminPass: "swordfish",
+ ImageID: "asdfasdfasdf",
+ }
+
+ ext := RebuildOptsExt{
+ RebuildOptsBuilder: base,
+ DiskConfig: Auto,
+ }
+
+ actual, err := ext.ToServerRebuildMap()
+ th.AssertNoErr(t, err)
+
+ expected := `
+ {
+ "rebuild": {
+ "name": "rebuiltserver",
+ "imageRef": "asdfasdfasdf",
+ "adminPass": "swordfish",
+ "OS-DCF:diskConfig": "AUTO"
+ }
+ }
+ `
+ th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestResizeOpts(t *testing.T) {
+ base := servers.ResizeOpts{
+ FlavorRef: "performance1-8",
+ }
+
+ ext := ResizeOptsExt{
+ ResizeOptsBuilder: base,
+ DiskConfig: Auto,
+ }
+
+ actual, err := ext.ToServerResizeMap()
+ th.AssertNoErr(t, err)
+
+ expected := `
+ {
+ "resize": {
+ "flavorRef": "performance1-8",
+ "OS-DCF:diskConfig": "AUTO"
+ }
+ }
+ `
+ th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/results.go b/openstack/compute/v2/extensions/diskconfig/results.go
new file mode 100644
index 0000000..10ec2da
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/results.go
@@ -0,0 +1,60 @@
+package diskconfig
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func commonExtract(result gophercloud.Result) (*DiskConfig, error) {
+ var resp struct {
+ Server struct {
+ DiskConfig string `mapstructure:"OS-DCF:diskConfig"`
+ } `mapstructure:"server"`
+ }
+
+ err := mapstructure.Decode(result.Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ config := DiskConfig(resp.Server.DiskConfig)
+ return &config, nil
+}
+
+// ExtractGet returns the disk configuration from a servers.Get call.
+func ExtractGet(result servers.GetResult) (*DiskConfig, error) {
+ return commonExtract(result.Result)
+}
+
+// ExtractUpdate returns the disk configuration from a servers.Update call.
+func ExtractUpdate(result servers.UpdateResult) (*DiskConfig, error) {
+ return commonExtract(result.Result)
+}
+
+// ExtractRebuild returns the disk configuration from a servers.Rebuild call.
+func ExtractRebuild(result servers.RebuildResult) (*DiskConfig, error) {
+ return commonExtract(result.Result)
+}
+
+// ExtractDiskConfig returns the DiskConfig setting for a specific server acquired from an
+// servers.ExtractServers call, while iterating through a Pager.
+func ExtractDiskConfig(page pagination.Page, index int) (*DiskConfig, error) {
+ casted := page.(servers.ServerPage).Body
+
+ type server struct {
+ DiskConfig string `mapstructure:"OS-DCF:diskConfig"`
+ }
+ var response struct {
+ Servers []server `mapstructure:"servers"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ config := DiskConfig(response.Servers[index].DiskConfig)
+ return &config, nil
+}
diff --git a/openstack/compute/v2/extensions/diskconfig/results_test.go b/openstack/compute/v2/extensions/diskconfig/results_test.go
new file mode 100644
index 0000000..dd8d2b7
--- /dev/null
+++ b/openstack/compute/v2/extensions/diskconfig/results_test.go
@@ -0,0 +1,68 @@
+package diskconfig
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestExtractGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ servers.HandleServerGetSuccessfully(t)
+
+ config, err := ExtractGet(servers.Get(client.ServiceClient(), "1234asdf"))
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, Manual, *config)
+}
+
+func TestExtractUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ servers.HandleServerUpdateSuccessfully(t)
+
+ r := servers.Update(client.ServiceClient(), "1234asdf", servers.UpdateOpts{
+ Name: "new-name",
+ })
+ config, err := ExtractUpdate(r)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, Manual, *config)
+}
+
+func TestExtractRebuild(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ servers.HandleRebuildSuccessfully(t, servers.SingleServerBody)
+
+ r := servers.Rebuild(client.ServiceClient(), "1234asdf", servers.RebuildOpts{
+ Name: "new-name",
+ AdminPass: "swordfish",
+ ImageID: "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ AccessIPv4: "1.2.3.4",
+ })
+ config, err := ExtractRebuild(r)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, Manual, *config)
+}
+
+func TestExtractList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ servers.HandleServerListSuccessfully(t)
+
+ pages := 0
+ err := servers.List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ config, err := ExtractDiskConfig(page, 0)
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, Manual, *config)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, pages, 1)
+}
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index e5f7c4b..e872b07 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -359,6 +359,26 @@
})
}
+// HandleServerListSuccessfully sets up the test server to respond to a server List request.
+func HandleServerListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, ServerListBody)
+ case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
+ fmt.Fprintf(w, `{ "servers": [] }`)
+ default:
+ t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
// HandleServerDeletionSuccessfully sets up the test server to respond to a server deletion request.
func HandleServerDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
@@ -369,6 +389,30 @@
})
}
+// HandleServerGetSuccessfully sets up the test server to respond to a server Get request.
+func HandleServerGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SingleServerBody)
+ })
+}
+
+// HandleServerUpdateSuccessfully sets up the test server to respond to a server Update request.
+func HandleServerUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+
+ fmt.Fprintf(w, SingleServerBody)
+ })
+}
+
// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
// change request.
func HandleAdminPasswordChangeSuccessfully(t *testing.T) {
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 632ba28..c6eca11 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -456,6 +456,28 @@
return result
}
+// ResizeOptsBuilder is an interface that allows extensions to override the default structure of
+// a Resize request.
+type ResizeOptsBuilder interface {
+ ToServerResizeMap() (map[string]interface{}, error)
+}
+
+// ResizeOpts represents the configuration options used to control a Resize operation.
+type ResizeOpts struct {
+ // FlavorRef is the ID of the flavor you wish your server to become.
+ FlavorRef string
+}
+
+// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body to the
+// Resize request.
+func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
+ resize := map[string]interface{}{
+ "flavorRef": opts.FlavorRef,
+ }
+
+ return map[string]interface{}{"resize": resize}, nil
+}
+
// 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.
@@ -463,15 +485,16 @@
// 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(client *gophercloud.ServiceClient, id, flavorRef string) ActionResult {
+func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult {
var res ActionResult
+ reqBody, err := opts.ToServerResizeMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
_, res.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
- ReqBody: struct {
- R map[string]interface{} `json:"resize"`
- }{
- map[string]interface{}{"flavorRef": flavorRef},
- },
+ ReqBody: reqBody,
MoreHeaders: client.AuthenticatedHeaders(),
OkCodes: []int{202},
})
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 5b65d86..23fe781 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -1,7 +1,6 @@
package servers
import (
- "fmt"
"net/http"
"testing"
@@ -13,23 +12,7 @@
func TestListServers(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
-
- w.Header().Add("Content-Type", "application/json")
- r.ParseForm()
- marker := r.Form.Get("marker")
- switch marker {
- case "":
- fmt.Fprintf(w, ServerListBody)
- case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
- fmt.Fprintf(w, `{ "servers": [] }`)
- default:
- t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
- }
- })
+ HandleServerListSuccessfully(t)
pages := 0
err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
@@ -83,14 +66,7 @@
func TestGetServer(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
- th.TestHeader(t, r, "Accept", "application/json")
-
- fmt.Fprintf(w, SingleServerBody)
- })
+ HandleServerGetSuccessfully(t)
client := client.ServiceClient()
actual, err := Get(client, "1234asdf").Extract()
@@ -104,16 +80,7 @@
func TestUpdateServer(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
-
- th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
- th.TestMethod(t, r, "PUT")
- th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
- th.TestHeader(t, r, "Accept", "application/json")
- th.TestHeader(t, r, "Content-Type", "application/json")
- th.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
-
- fmt.Fprintf(w, SingleServerBody)
- })
+ HandleServerUpdateSuccessfully(t)
client := client.ServiceClient()
actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
@@ -172,7 +139,7 @@
w.WriteHeader(http.StatusAccepted)
})
- res := Resize(client.ServiceClient(), "1234asdf", "2")
+ res := Resize(client.ServiceClient(), "1234asdf", ResizeOpts{FlavorRef: "2"})
th.AssertNoErr(t, res.Err)
}
diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go
new file mode 100644
index 0000000..b83a893
--- /dev/null
+++ b/rackspace/compute/v2/servers/requests.go
@@ -0,0 +1,138 @@
+package servers
+
+import (
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// CreateOpts specifies all of the options that Rackspace accepts in its Create request, including
+// the union of all extensions that Rackspace supports.
+type CreateOpts struct {
+ // Name [required] is the name to assign to the newly launched server.
+ Name string
+
+ // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+ // Optional if using the boot-from-volume extension.
+ ImageRef string
+
+ // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+ FlavorRef string
+
+ // SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+ SecurityGroups []string
+
+ // UserData [optional] contains configuration information or scripts to use upon launch.
+ // Create will base64-encode it for you.
+ UserData []byte
+
+ // AvailabilityZone [optional] in which to launch the server.
+ AvailabilityZone string
+
+ // Networks [optional] dictates how this server will be attached to available networks.
+ // By default, the server will be attached to all isolated networks for the tenant.
+ Networks []os.Network
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+
+ // ConfigDrive [optional] enables metadata injection through a configuration drive.
+ ConfigDrive bool
+
+ // Rackspace-specific extensions begin here.
+
+ // KeyPair [optional] specifies the name of the SSH KeyPair to be injected into the newly launched
+ // server. See the "keypairs" extension in OpenStack compute v2.
+ KeyPair string
+
+ // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig"
+ // extension in OpenStack compute v2.
+ DiskConfig diskconfig.DiskConfig
+}
+
+// ToServerCreateMap constructs a request body using all of the OpenStack extensions that are
+// active on Rackspace.
+func (opts CreateOpts) ToServerCreateMap() map[string]interface{} {
+ base := os.CreateOpts{
+ Name: opts.Name,
+ ImageRef: opts.ImageRef,
+ FlavorRef: opts.FlavorRef,
+ SecurityGroups: opts.SecurityGroups,
+ UserData: opts.UserData,
+ AvailabilityZone: opts.AvailabilityZone,
+ Networks: opts.Networks,
+ Metadata: opts.Metadata,
+ Personality: opts.Personality,
+ ConfigDrive: opts.ConfigDrive,
+ }
+
+ drive := diskconfig.CreateOptsExt{
+ CreateOptsBuilder: base,
+ DiskConfig: opts.DiskConfig,
+ }
+
+ result := drive.ToServerCreateMap()
+
+ // key_name doesn't actually come from the extension (or at least isn't documented there) so
+ // we need to add it manually.
+ serverMap := result["server"].(map[string]interface{})
+ serverMap["key_name"] = opts.KeyPair
+
+ return result
+}
+
+// RebuildOpts represents all of the configuration options used in a server rebuild operation that
+// are supported by Rackspace.
+type RebuildOpts struct {
+ // Required. The ID of the image you want your server to be provisioned on
+ ImageID string
+
+ // Name to set the server to
+ Name string
+
+ // Required. The server's admin password
+ AdminPass string
+
+ // AccessIPv4 [optional] provides a new IPv4 address for the instance.
+ AccessIPv4 string
+
+ // AccessIPv6 [optional] provides a new IPv6 address for the instance.
+ AccessIPv6 string
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+
+ // Rackspace-specific stuff begins here.
+
+ // DiskConfig [optional] controls how the created server's disk is partitioned. See the "diskconfig"
+ // extension in OpenStack compute v2.
+ DiskConfig diskconfig.DiskConfig
+}
+
+// ToServerRebuildMap constructs a request body using all of the OpenStack extensions that are
+// active on Rackspace.
+func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
+ base := os.RebuildOpts{
+ ImageID: opts.ImageID,
+ Name: opts.Name,
+ AdminPass: opts.AdminPass,
+ AccessIPv4: opts.AccessIPv4,
+ AccessIPv6: opts.AccessIPv6,
+ Metadata: opts.Metadata,
+ Personality: opts.Personality,
+ }
+
+ drive := diskconfig.RebuildOptsExt{
+ RebuildOptsBuilder: base,
+ DiskConfig: opts.DiskConfig,
+ }
+
+ return drive.ToServerRebuildMap()
+}
diff --git a/rackspace/compute/v2/servers/requests_test.go b/rackspace/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..ac7058f
--- /dev/null
+++ b/rackspace/compute/v2/servers/requests_test.go
@@ -0,0 +1,55 @@
+package servers
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestCreateOpts(t *testing.T) {
+ opts := CreateOpts{
+ Name: "createdserver",
+ ImageRef: "image-id",
+ FlavorRef: "flavor-id",
+ KeyPair: "mykey",
+ DiskConfig: diskconfig.Manual,
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "image-id",
+ "flavorRef": "flavor-id",
+ "key_name": "mykey",
+ "OS-DCF:diskConfig": "MANUAL"
+ }
+ }
+ `
+ th.CheckJSONEquals(t, expected, opts.ToServerCreateMap())
+}
+
+func TestRebuildOpts(t *testing.T) {
+ opts := RebuildOpts{
+ Name: "rebuiltserver",
+ AdminPass: "swordfish",
+ ImageID: "asdfasdfasdf",
+ DiskConfig: diskconfig.Auto,
+ }
+
+ actual, err := opts.ToServerRebuildMap()
+ th.AssertNoErr(t, err)
+
+ expected := `
+ {
+ "rebuild": {
+ "name": "rebuiltserver",
+ "imageRef": "asdfasdfasdf",
+ "adminPass": "swordfish",
+ "OS-DCF:diskConfig": "AUTO"
+ }
+ }
+ `
+ th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/testhelper/convenience.go b/testhelper/convenience.go
index ca27cad..adb77e5 100644
--- a/testhelper/convenience.go
+++ b/testhelper/convenience.go
@@ -1,6 +1,7 @@
package testhelper
import (
+ "encoding/json"
"fmt"
"path/filepath"
"reflect"
@@ -9,25 +10,32 @@
"testing"
)
+const (
+ logBodyFmt = "\033[1;31m%s %s\033[0m"
+ greenCode = "\033[0m\033[1;32m"
+ yellowCode = "\033[0m\033[1;33m"
+ resetCode = "\033[0m\033[1;31m"
+)
+
func prefix(depth int) string {
_, file, line, _ := runtime.Caller(depth)
return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
}
func green(str interface{}) string {
- return fmt.Sprintf("\033[0m\033[1;32m%#v\033[0m\033[1;31m", str)
+ return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode)
}
func yellow(str interface{}) string {
- return fmt.Sprintf("\033[0m\033[1;33m%#v\033[0m\033[1;31m", str)
+ return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode)
}
func logFatal(t *testing.T, str string) {
- t.Fatalf("\033[1;31m%s %s\033[0m", prefix(3), str)
+ t.Fatalf(logBodyFmt, prefix(3), str)
}
func logError(t *testing.T, str string) {
- t.Errorf("\033[1;31m%s %s\033[0m", prefix(3), str)
+ t.Errorf(logBodyFmt, prefix(3), str)
}
type diffLogger func([]string, interface{}, interface{})
@@ -248,6 +256,58 @@
})
}
+// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and
+// CheckJSONEquals.
+func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool {
+ var parsedExpected interface{}
+ err := json.Unmarshal([]byte(expectedJSON), &parsedExpected)
+ if err != nil {
+ t.Errorf("Unable to parse expected value as JSON: %v", err)
+ return false
+ }
+
+ if !reflect.DeepEqual(parsedExpected, actual) {
+ prettyExpected, err := json.MarshalIndent(parsedExpected, "", " ")
+ if err != nil {
+ t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON)
+ } else {
+ // We can't use green() here because %#v prints prettyExpected as a byte array literal, which
+ // is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason.
+ t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode)
+ }
+
+ prettyActual, err := json.MarshalIndent(actual, "", " ")
+ if err != nil {
+ t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual)
+ } else {
+ // We can't use yellow() for the same reason.
+ t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode)
+ }
+
+ return false
+ }
+ return true
+}
+
+// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that
+// both are consistent. If they aren't, the expected and actual structures are pretty-printed and
+// shown for comparison.
+//
+// This is useful for comparing structures that are built as nested map[string]interface{} values,
+// which are a pain to construct as literals.
+func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) {
+ if !isJSONEquals(t, expectedJSON, actual) {
+ logFatal(t, "The generated JSON structure differed.")
+ }
+}
+
+// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal.
+func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) {
+ if !isJSONEquals(t, expectedJSON, actual) {
+ logError(t, "The generated JSON structure differed.")
+ }
+}
+
// AssertNoErr is a convenience function for checking whether an error value is
// an actual error
func AssertNoErr(t *testing.T, e error) {
diff --git a/testhelper/http_responses.go b/testhelper/http_responses.go
index 481a833..e1f1f9a 100644
--- a/testhelper/http_responses.go
+++ b/testhelper/http_responses.go
@@ -81,33 +81,11 @@
t.Errorf("Unable to read request body: %v", err)
}
- var expectedJSON interface{}
- err = json.Unmarshal([]byte(expected), &expectedJSON)
- if err != nil {
- t.Errorf("Unable to parse expected value as JSON: %v", err)
- }
-
var actualJSON interface{}
err = json.Unmarshal(b, &actualJSON)
if err != nil {
t.Errorf("Unable to parse request body as JSON: %v", err)
}
- if !reflect.DeepEqual(expectedJSON, actualJSON) {
- prettyExpected, err := json.MarshalIndent(expectedJSON, "", " ")
- if err != nil {
- t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expected)
- } else {
- t.Logf("Expected JSON:\n%s", prettyExpected)
- }
-
- prettyActual, err := json.MarshalIndent(actualJSON, "", " ")
- if err != nil {
- t.Logf("Unable to pretty-print actual JSON: %v\n%s", err, b)
- } else {
- t.Logf("Actual JSON:\n%s", prettyActual)
- }
-
- t.Errorf("Response body did not contain the correct JSON.")
- }
+ CheckJSONEquals(t, expected, actualJSON)
}