From 5f7cf183f41ec2535352b295719419ba50a31177 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 08:35:24 -0400 Subject: [PATCH 01/16] Use an options struct for Resize. --- openstack/compute/v2/servers/requests.go | 35 +++++++++++++++---- openstack/compute/v2/servers/requests_test.go | 2 +- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go index 632ba28b..26a85251 100644 --- a/openstack/compute/v2/servers/requests.go +++ b/openstack/compute/v2/servers/requests.go @@ -456,6 +456,28 @@ func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuild 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 @@ func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuild // 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 ResizeOpts) 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 5b65d865..f3b8a7fe 100644 --- a/openstack/compute/v2/servers/requests_test.go +++ b/openstack/compute/v2/servers/requests_test.go @@ -172,7 +172,7 @@ func TestResizeServer(t *testing.T) { w.WriteHeader(http.StatusAccepted) }) - res := Resize(client.ServiceClient(), "1234asdf", "2") + res := Resize(client.ServiceClient(), "1234asdf", ResizeOpts{FlavorRef: "2"}) th.AssertNoErr(t, res.Err) } From 6935a9bd73b91c62eecaa5366ddcbbb7ba2d1fe1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 09:41:22 -0400 Subject: [PATCH 02/16] Extended create, rebuild and resize opts. --- .../v2/extensions/diskconfig/requests.go | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 openstack/compute/v2/extensions/diskconfig/requests.go diff --git a/openstack/compute/v2/extensions/diskconfig/requests.go b/openstack/compute/v2/extensions/diskconfig/requests.go new file mode 100644 index 00000000..e1ab48f9 --- /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 +} From 3315cf9113d1871fba96892550212bbd471d3acc Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 10:27:35 -0400 Subject: [PATCH 03/16] *JSONEquals() testhelper methods. These are helpful for testing .ToXyzMap() methods, in particular. --- testhelper/convenience.go | 68 +++++++++++++++++++++++++++++++++--- testhelper/http_responses.go | 24 +------------ 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/testhelper/convenience.go b/testhelper/convenience.go index ca27cad1..adb77e5a 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 @@ import ( "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 @@ func CheckDeepEquals(t *testing.T, expected, actual interface{}) { }) } +// 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 481a8330..e1f1f9ac 100644 --- a/testhelper/http_responses.go +++ b/testhelper/http_responses.go @@ -81,33 +81,11 @@ func TestJSONRequest(t *testing.T, r *http.Request, expected string) { 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) } From 5f14f54f385b0b96f1158aad333684d8b235d595 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 10:28:14 -0400 Subject: [PATCH 04/16] It's "diskConfig", camelCase. --- openstack/compute/v2/extensions/diskconfig/requests.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openstack/compute/v2/extensions/diskconfig/requests.go b/openstack/compute/v2/extensions/diskconfig/requests.go index e1ab48f9..06a922ac 100644 --- a/openstack/compute/v2/extensions/diskconfig/requests.go +++ b/openstack/compute/v2/extensions/diskconfig/requests.go @@ -49,7 +49,7 @@ func (opts CreateOptsExt) ToServerCreateMap() map[string]interface{} { base := opts.CreateOptsBuilder.ToServerCreateMap() serverMap := base["server"].(map[string]interface{}) - serverMap["OS-DCF:diskconfig"] = string(opts.DiskConfig) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) return base } @@ -75,7 +75,7 @@ func (opts RebuildOptsExt) ToServerRebuildMap() (map[string]interface{}, error) } serverMap := base["rebuild"].(map[string]interface{}) - serverMap["OS-DCF:diskconfig"] = string(opts.DiskConfig) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) return base, nil } @@ -101,7 +101,7 @@ func (opts ResizeOptsExt) ToServerResizeMap() (map[string]interface{}, error) { } serverMap := base["resize"].(map[string]interface{}) - serverMap["OS-DCF:diskconfig"] = string(opts.DiskConfig) + serverMap["OS-DCF:diskConfig"] = string(opts.DiskConfig) return base, nil } From 5b50549363f52d87c99c918b1ba7889baf4936e2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 10:28:22 -0400 Subject: [PATCH 05/16] Test request body generation. --- .../v2/extensions/diskconfig/requests_test.go | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 openstack/compute/v2/extensions/diskconfig/requests_test.go 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 00000000..6446ed65 --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -0,0 +1,61 @@ +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: "createdserver", + AdminPass: "swordfish", + ImageID: "asdfasdfasdf", + } + + ext := RebuildOptsExt{ + RebuildOptsBuilder: base, + DiskConfig: Auto, + } + + actual, err := ext.ToServerRebuildMap() + th.AssertNoErr(t, err) + + expected := ` + { + "rebuild": { + "name": "createdserver", + "imageRef": "asdfasdfasdf", + "adminPass": "swordfish", + "OS-DCF:diskConfig": "AUTO" + } + } + ` + th.CheckJSONEquals(t, expected, actual) +} From 80387a06323c0576bd30ac081613c412e83d57a3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 10:38:41 -0400 Subject: [PATCH 06/16] Extended ResizeOpts test. --- .../v2/extensions/diskconfig/requests_test.go | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openstack/compute/v2/extensions/diskconfig/requests_test.go b/openstack/compute/v2/extensions/diskconfig/requests_test.go index 6446ed65..38d3c7bc 100644 --- a/openstack/compute/v2/extensions/diskconfig/requests_test.go +++ b/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -59,3 +59,27 @@ func TestRebuildOpts(t *testing.T) { ` 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) +} From 189c95c33266d6c6d849d4ac380b82d21a08e5c9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 11:41:35 -0400 Subject: [PATCH 07/16] Move Get and Update mocks to fixtures. --- openstack/compute/v2/servers/fixtures.go | 24 +++++++++++++++++++ openstack/compute/v2/servers/requests_test.go | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go index e5f7c4b1..a5fca690 100644 --- a/openstack/compute/v2/servers/fixtures.go +++ b/openstack/compute/v2/servers/fixtures.go @@ -369,6 +369,30 @@ func HandleServerDeletionSuccessfully(t *testing.T) { }) } +// 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_test.go b/openstack/compute/v2/servers/requests_test.go index f3b8a7fe..4ca6f010 100644 --- a/openstack/compute/v2/servers/requests_test.go +++ b/openstack/compute/v2/servers/requests_test.go @@ -83,14 +83,7 @@ func TestDeleteServer(t *testing.T) { 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 +97,7 @@ func TestGetServer(t *testing.T) { 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() From 0e5b92a3d3419a9cc7dc2902c71eb7d48ce611e1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 11:42:04 -0400 Subject: [PATCH 08/16] diskconfig.Extract* functions. --- .../v2/extensions/diskconfig/results.go | 60 +++++++++++++++++++ .../v2/extensions/diskconfig/results_test.go | 48 +++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 openstack/compute/v2/extensions/diskconfig/results.go create mode 100644 openstack/compute/v2/extensions/diskconfig/results_test.go diff --git a/openstack/compute/v2/extensions/diskconfig/results.go b/openstack/compute/v2/extensions/diskconfig/results.go new file mode 100644 index 00000000..05a29a89 --- /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-CDF: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 00000000..adbd031e --- /dev/null +++ b/openstack/compute/v2/extensions/diskconfig/results_test.go @@ -0,0 +1,48 @@ +package diskconfig + +import ( + "testing" + + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + 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) +} From a70510a4bf5715ed1843c5ff954199dd60837e1a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 11:54:03 -0400 Subject: [PATCH 09/16] Move HandleServerListSuccessfully() to fixtures, too. --- openstack/compute/v2/servers/fixtures.go | 20 +++++++++++++++++++ openstack/compute/v2/servers/requests_test.go | 19 +----------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go index a5fca690..e872b071 100644 --- a/openstack/compute/v2/servers/fixtures.go +++ b/openstack/compute/v2/servers/fixtures.go @@ -359,6 +359,26 @@ func HandleServerCreationSuccessfully(t *testing.T, response string) { }) } +// 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) { diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go index 4ca6f010..23fe7813 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 @@ import ( 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) { From 3883af2a7ef9840fb0df66c7870e7b0a53498d26 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 11:57:00 -0400 Subject: [PATCH 10/16] Unit test for the Extract function. --- .../v2/extensions/diskconfig/results.go | 2 +- .../v2/extensions/diskconfig/results_test.go | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/extensions/diskconfig/results.go b/openstack/compute/v2/extensions/diskconfig/results.go index 05a29a89..10ec2daf 100644 --- a/openstack/compute/v2/extensions/diskconfig/results.go +++ b/openstack/compute/v2/extensions/diskconfig/results.go @@ -44,7 +44,7 @@ func ExtractDiskConfig(page pagination.Page, index int) (*DiskConfig, error) { casted := page.(servers.ServerPage).Body type server struct { - DiskConfig string `mapstructure:"OS-CDF:diskConfig"` + DiskConfig string `mapstructure:"OS-DCF:diskConfig"` } var response struct { Servers []server `mapstructure:"servers"` diff --git a/openstack/compute/v2/extensions/diskconfig/results_test.go b/openstack/compute/v2/extensions/diskconfig/results_test.go index adbd031e..dd8d2b7d 100644 --- a/openstack/compute/v2/extensions/diskconfig/results_test.go +++ b/openstack/compute/v2/extensions/diskconfig/results_test.go @@ -4,6 +4,7 @@ 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" ) @@ -46,3 +47,22 @@ func TestExtractRebuild(t *testing.T) { 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) +} From ae0ca65057dbf5659bd185f41399af05f2686904 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 12:30:12 -0400 Subject: [PATCH 11/16] rackspace.CreateOpts unifies Rackspace opts. --- rackspace/compute/v2/servers/requests.go | 85 +++++++++++++++++++ rackspace/compute/v2/servers/requests_test.go | 31 +++++++ 2 files changed, 116 insertions(+) create mode 100644 rackspace/compute/v2/servers/requests.go create mode 100644 rackspace/compute/v2/servers/requests_test.go diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go new file mode 100644 index 00000000..8e55a6a4 --- /dev/null +++ b/rackspace/compute/v2/servers/requests.go @@ -0,0 +1,85 @@ +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 +} diff --git a/rackspace/compute/v2/servers/requests_test.go b/rackspace/compute/v2/servers/requests_test.go new file mode 100644 index 00000000..999718ba --- /dev/null +++ b/rackspace/compute/v2/servers/requests_test.go @@ -0,0 +1,31 @@ +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()) +} From 9c24f6b10d989c2e5c5c758c96f6bf0ac2c0fe21 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 12:41:19 -0400 Subject: [PATCH 12/16] Use keys and diskConfig in RS acceptance tests. --- .../rackspace/compute/v2/servers_test.go | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go index c3465cd4..7973f757 100644 --- a/acceptance/rackspace/compute/v2/servers_test.go +++ b/acceptance/rackspace/compute/v2/servers_test.go @@ -7,14 +7,30 @@ import ( "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 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient) *os.Server { 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.") @@ -147,11 +165,23 @@ func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Se 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) From 237aad666c58454fed5c5ac8e9d94e3cbea0fcc8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 12:49:13 -0400 Subject: [PATCH 13/16] Unit testing grammar :lipstick: --- openstack/compute/v2/extensions/diskconfig/requests_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/extensions/diskconfig/requests_test.go b/openstack/compute/v2/extensions/diskconfig/requests_test.go index 38d3c7bc..1f4f6268 100644 --- a/openstack/compute/v2/extensions/diskconfig/requests_test.go +++ b/openstack/compute/v2/extensions/diskconfig/requests_test.go @@ -34,7 +34,7 @@ func TestCreateOpts(t *testing.T) { func TestRebuildOpts(t *testing.T) { base := servers.RebuildOpts{ - Name: "createdserver", + Name: "rebuiltserver", AdminPass: "swordfish", ImageID: "asdfasdfasdf", } @@ -50,7 +50,7 @@ func TestRebuildOpts(t *testing.T) { expected := ` { "rebuild": { - "name": "createdserver", + "name": "rebuiltserver", "imageRef": "asdfasdfasdf", "adminPass": "swordfish", "OS-DCF:diskConfig": "AUTO" From d7814a3fecd732bfcce6c1561c96deb8c95d6253 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 12:49:25 -0400 Subject: [PATCH 14/16] Do the same for Rackspace RebuildOpts. --- rackspace/compute/v2/servers/requests.go | 53 +++++++++++++++++++ rackspace/compute/v2/servers/requests_test.go | 24 +++++++++ 2 files changed, 77 insertions(+) diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go index 8e55a6a4..b83a8937 100644 --- a/rackspace/compute/v2/servers/requests.go +++ b/rackspace/compute/v2/servers/requests.go @@ -83,3 +83,56 @@ func (opts CreateOpts) ToServerCreateMap() map[string]interface{} { 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 index 999718ba..ac7058f3 100644 --- a/rackspace/compute/v2/servers/requests_test.go +++ b/rackspace/compute/v2/servers/requests_test.go @@ -29,3 +29,27 @@ func TestCreateOpts(t *testing.T) { ` 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) +} From 5d686720865927a632a0ef91e451524a10639acb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 12:51:30 -0400 Subject: [PATCH 15/16] Use Rackspace rebuild options. --- acceptance/rackspace/compute/v2/servers_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go index 7973f757..af4bbe07 100644 --- a/acceptance/rackspace/compute/v2/servers_test.go +++ b/acceptance/rackspace/compute/v2/servers_test.go @@ -140,10 +140,11 @@ func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.S 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) From 9e87a92bddfb53ffa05b89436b0a43848137e12a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 23 Oct 2014 14:29:22 -0400 Subject: [PATCH 16/16] Use ResizeOptsBuilder, not ResizeOpts. --- openstack/compute/v2/servers/requests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go index 26a85251..c6eca11a 100644 --- a/openstack/compute/v2/servers/requests.go +++ b/openstack/compute/v2/servers/requests.go @@ -485,7 +485,7 @@ func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { // 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 string, opts ResizeOpts) ActionResult { +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) ActionResult { var res ActionResult reqBody, err := opts.ToServerResizeMap() if err != nil {