diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go index 7b928e9e..f6c7c052 100644 --- a/acceptance/openstack/compute/v2/servers_test.go +++ b/acceptance/openstack/compute/v2/servers_test.go @@ -107,6 +107,12 @@ func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *Comp servers.Network{UUID: network.ID}, }, AdminPass: pwd, + Personality: servers.Personality{ + &servers.File{ + Path: "/etc/test", + Contents: []byte("hello world"), + }, + }, }).Extract() if err != nil { t.Fatalf("Unable to create server: %v", err) diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go index aa8c1a87..af77546e 100644 --- a/openstack/compute/v2/servers/requests.go +++ b/openstack/compute/v2/servers/requests.go @@ -2,6 +2,7 @@ package servers import ( "encoding/base64" + "encoding/json" "errors" "fmt" @@ -14,6 +15,7 @@ import ( type ListOptsBuilder interface { ToServerListQuery() (string, error) } + // ListOpts allows the filtering and sorting of paginated collections through // the API. Filtering is achieved by passing in struct field values that map to // the server attributes you want to see returned. Marker and Limit are used @@ -95,6 +97,31 @@ type Network struct { FixedIP string } +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested, +// json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file + Path string + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + // CreateOpts specifies server creation parameters. type CreateOpts struct { // Name [required] is the name to assign to the newly launched server. @@ -124,9 +151,9 @@ type CreateOpts struct { // 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 + // Personality [optional] includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality // ConfigDrive [optional] enables metadata injection through a configuration drive. ConfigDrive bool @@ -154,10 +181,6 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { encoded := base64.StdEncoding.EncodeToString(opts.UserData) server["user_data"] = &encoded } - if opts.Personality != nil { - encoded := base64.StdEncoding.EncodeToString(opts.Personality) - server["personality"] = &encoded - } if opts.ConfigDrive { server["config_drive"] = "true" } @@ -202,6 +225,10 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { server["networks"] = networks } + if len(opts.Personality) > 0 { + server["personality"] = opts.Personality + } + return map[string]interface{}{"server": server}, nil } @@ -391,9 +418,9 @@ type RebuildOpts struct { // 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 + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality } // ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON @@ -429,9 +456,8 @@ func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { server["metadata"] = opts.Metadata } - if opts.Personality != nil { - encoded := base64.StdEncoding.EncodeToString(opts.Personality) - server["personality"] = &encoded + if len(opts.Personality) > 0 { + server["personality"] = opts.Personality } return map[string]interface{}{"rebuild": server}, nil @@ -741,5 +767,5 @@ func CreateImage(client *gophercloud.ServiceClient, serverId string, opts Create }) res.Err = err res.Header = response.Header - return res + return res } diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go index 1f39fe14..88cb54dd 100644 --- a/openstack/compute/v2/servers/requests_test.go +++ b/openstack/compute/v2/servers/requests_test.go @@ -1,6 +1,8 @@ package servers import ( + "encoding/base64" + "encoding/json" "net/http" "testing" @@ -334,3 +336,38 @@ func TestCreateServerImage(t *testing.T) { _, err := CreateImage(client.ServiceClient(), "serverimage", CreateImageOpts{Name: "test"}).ExtractImageID() th.AssertNoErr(t, err) } + +func TestMarshalPersonality(t *testing.T) { + name := "/etc/test" + contents := []byte("asdfasdf") + + personality := Personality{ + &File{ + Path: name, + Contents: contents, + }, + } + + data, err := json.Marshal(personality) + if err != nil { + t.Fatal(err) + } + + var actual []map[string]string + err = json.Unmarshal(data, &actual) + if err != nil { + t.Fatal(err) + } + + if len(actual) != 1 { + t.Fatal("expected personality length 1") + } + + if actual[0]["path"] != name { + t.Fatal("file path incorrect") + } + + if actual[0]["contents"] != base64.StdEncoding.EncodeToString(contents) { + t.Fatal("file contents incorrect") + } +} diff --git a/rackspace/compute/v2/servers/requests.go b/rackspace/compute/v2/servers/requests.go index 809183ec..1ebb8971 100644 --- a/rackspace/compute/v2/servers/requests.go +++ b/rackspace/compute/v2/servers/requests.go @@ -36,9 +36,9 @@ type CreateOpts struct { // 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 + // Personality [optional] includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality os.Personality // ConfigDrive [optional] enables metadata injection through a configuration drive. ConfigDrive bool @@ -130,9 +130,9 @@ type RebuildOpts struct { // 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 + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality os.Personality // Rackspace-specific stuff begins here.