Merge pull request #413 from doubledutch/personality
Server personality
diff --git a/acceptance/openstack/compute/v2/servergroup_test.go b/acceptance/openstack/compute/v2/servergroup_test.go
index 80015e1..945854e 100644
--- a/acceptance/openstack/compute/v2/servergroup_test.go
+++ b/acceptance/openstack/compute/v2/servergroup_test.go
@@ -3,10 +3,15 @@
package v2
import (
+ "fmt"
"testing"
"github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ th "github.com/rackspace/gophercloud/testhelper"
)
func createServerGroup(t *testing.T, computeClient *gophercloud.ServiceClient) (*servergroups.ServerGroup, error) {
@@ -36,7 +41,53 @@
return nil
}
+func createServerInGroup(t *testing.T, computeClient *gophercloud.ServiceClient, choices *ComputeChoices, serverGroup *servergroups.ServerGroup) (*servers.Server, error) {
+ if testing.Short() {
+ t.Skip("Skipping test that requires server creation in short mode.")
+ }
+
+ name := tools.RandomString("ACPTTEST", 16)
+ t.Logf("Attempting to create server: %s\n", name)
+
+ pwd := tools.MakeNewPassword("")
+
+ serverCreateOpts := servers.CreateOpts{
+ Name: name,
+ FlavorRef: choices.FlavorID,
+ ImageRef: choices.ImageID,
+ AdminPass: pwd,
+ }
+ server, err := servers.Create(computeClient, schedulerhints.CreateOptsExt{
+ serverCreateOpts,
+ schedulerhints.SchedulerHints{
+ Group: serverGroup.ID,
+ },
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+
+ th.AssertEquals(t, pwd, server.AdminPass)
+
+ return server, err
+}
+
+func verifySchedulerWorked(t *testing.T, firstServer, secondServer *servers.Server) error {
+ t.Logf("First server hostID: %v", firstServer.HostID)
+ t.Logf("Second server hostID: %v", secondServer.HostID)
+ if firstServer.HostID == secondServer.HostID {
+ return nil
+ }
+
+ return fmt.Errorf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID)
+}
+
func TestServerGroups(t *testing.T) {
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
computeClient, err := newClient()
if err != nil {
t.Fatalf("Unable to create a compute client: %v", err)
@@ -48,11 +99,45 @@
}
defer func() {
servergroups.Delete(computeClient, sg.ID)
- t.Logf("ServerGroup deleted.")
+ t.Logf("Server Group deleted.")
}()
err = getServerGroup(t, computeClient, sg.ID)
if err != nil {
t.Fatalf("Unable to get server group: %v", err)
}
+
+ firstServer, err := createServerInGroup(t, computeClient, choices, sg)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer func() {
+ servers.Delete(computeClient, firstServer.ID)
+ t.Logf("Server deleted.")
+ }()
+
+ if err = waitForStatus(computeClient, firstServer, "ACTIVE"); err != nil {
+ t.Fatalf("Unable to wait for server: %v", err)
+ }
+
+ firstServer, err = servers.Get(computeClient, firstServer.ID).Extract()
+
+ secondServer, err := createServerInGroup(t, computeClient, choices, sg)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer func() {
+ servers.Delete(computeClient, secondServer.ID)
+ t.Logf("Server deleted.")
+ }()
+
+ if err = waitForStatus(computeClient, secondServer, "ACTIVE"); err != nil {
+ t.Fatalf("Unable to wait for server: %v", err)
+ }
+
+ secondServer, err = servers.Get(computeClient, secondServer.ID).Extract()
+
+ if err = verifySchedulerWorked(t, firstServer, secondServer); err != nil {
+ t.Fatalf("Scheduling did not work: %v", err)
+ }
}
diff --git a/openstack/compute/v2/extensions/schedulerhints/doc.go b/openstack/compute/v2/extensions/schedulerhints/doc.go
new file mode 100644
index 0000000..0bd4566
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/doc.go
@@ -0,0 +1,3 @@
+// Package schedulerhints enables instances to provide the OpenStack scheduler
+// hints about where they should be placed in the cloud.
+package schedulerhints
diff --git a/openstack/compute/v2/extensions/schedulerhints/requests.go b/openstack/compute/v2/extensions/schedulerhints/requests.go
new file mode 100644
index 0000000..567eef4
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/requests.go
@@ -0,0 +1,134 @@
+package schedulerhints
+
+import (
+ "fmt"
+ "net"
+ "regexp"
+ "strings"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// SchedulerHints represents a set of scheduling hints that are passed to the
+// OpenStack scheduler
+type SchedulerHints struct {
+ // Group specifies a Server Group to place the instance in.
+ Group string
+
+ // DifferentHost will place the instance on a compute node that does not
+ // host the given instances.
+ DifferentHost []string
+
+ // SameHost will place the instance on a compute node that hosts the given
+ // instances.
+ SameHost []string
+
+ // Query is a conditional statement that results in compute nodes able to
+ // host the instance.
+ Query []interface{}
+
+ // TargetCell specifies a cell name where the instance will be placed.
+ TargetCell string
+
+ // BuildNearHostIP specifies a subnet of compute nodes to host the instance.
+ BuildNearHostIP string
+}
+
+// SchedulerHintsBuilder builds the scheduler hints into a serializable format.
+type SchedulerHintsBuilder interface {
+ ToServerSchedulerHintsMap() (map[string]interface{}, error)
+}
+
+// ToServerSchedulerHintsMap builds the scheduler hints into a serializable format.
+func (opts SchedulerHints) ToServerSchedulerHintsMap() (map[string]interface{}, error) {
+ sh := make(map[string]interface{})
+
+ uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$")
+
+ if opts.Group != "" {
+ if !uuidRegex.MatchString(opts.Group) {
+ return nil, fmt.Errorf("Group must be a UUID")
+ }
+ sh["group"] = opts.Group
+ }
+
+ if len(opts.DifferentHost) > 0 {
+ for _, diffHost := range opts.DifferentHost {
+ if !uuidRegex.MatchString(diffHost) {
+ return nil, fmt.Errorf("The hosts in DifferentHost must be in UUID format.")
+ }
+ }
+ sh["different_host"] = opts.DifferentHost
+ }
+
+ if len(opts.SameHost) > 0 {
+ for _, sameHost := range opts.SameHost {
+ if !uuidRegex.MatchString(sameHost) {
+ return nil, fmt.Errorf("The hosts in SameHost must be in UUID format.")
+ }
+ }
+ sh["same_host"] = opts.SameHost
+ }
+
+ /* Query can be something simple like:
+ [">=", "$free_ram_mb", 1024]
+
+ Or more complex like:
+ ['and',
+ ['>=', '$free_ram_mb', 1024],
+ ['>=', '$free_disk_mb', 200 * 1024]
+ ]
+
+ Because of the possible complexity, just make sure the length is a minimum of 3.
+ */
+ if len(opts.Query) > 0 {
+ if len(opts.Query) < 3 {
+ return nil, fmt.Errorf("Query must be a conditional statement in the format of [op,variable,value]")
+ }
+ sh["query"] = opts.Query
+ }
+
+ if opts.TargetCell != "" {
+ sh["target_cell"] = opts.TargetCell
+ }
+
+ if opts.BuildNearHostIP != "" {
+ if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil {
+ return nil, fmt.Errorf("BuildNearHostIP must be a valid subnet in the form 192.168.1.1/24")
+ }
+ ipParts := strings.Split(opts.BuildNearHostIP, "/")
+ sh["build_near_host_ip"] = ipParts[0]
+ sh["cidr"] = "/" + ipParts[1]
+ }
+
+ return sh, nil
+}
+
+// CreateOptsExt adds a SchedulerHints option to the base CreateOpts.
+type CreateOptsExt struct {
+ servers.CreateOptsBuilder
+
+ // SchedulerHints provides a set of hints to the scheduler.
+ SchedulerHints SchedulerHintsBuilder
+}
+
+// ToServerCreateMap adds the SchedulerHints option to the base server creation options.
+func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
+ base, err := opts.CreateOptsBuilder.ToServerCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ schedulerHints, err := opts.SchedulerHints.ToServerSchedulerHintsMap()
+ if err != nil {
+ return nil, err
+ }
+
+ if len(schedulerHints) == 0 {
+ return base, nil
+ }
+
+ base["os:scheduler_hints"] = schedulerHints
+
+ return base, nil
+}
diff --git a/openstack/compute/v2/extensions/schedulerhints/requests_test.go b/openstack/compute/v2/extensions/schedulerhints/requests_test.go
new file mode 100644
index 0000000..491a455
--- /dev/null
+++ b/openstack/compute/v2/extensions/schedulerhints/requests_test.go
@@ -0,0 +1,126 @@
+package schedulerhints
+
+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",
+ }
+
+ schedulerHints := SchedulerHints{
+ Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+ DifferentHost: []string{
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287",
+ },
+ SameHost: []string{
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287",
+ },
+ Query: []interface{}{">=", "$free_ram_mb", "1024"},
+ TargetCell: "foobar",
+ BuildNearHostIP: "192.168.1.1/24",
+ }
+
+ ext := CreateOptsExt{
+ CreateOptsBuilder: base,
+ SchedulerHints: schedulerHints,
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "asdfasdfasdf",
+ "flavorRef": "performance1-1"
+ },
+ "os:scheduler_hints": {
+ "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+ "different_host": [
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287"
+ ],
+ "same_host": [
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287"
+ ],
+ "query": [
+ ">=", "$free_ram_mb", "1024"
+ ],
+ "target_cell": "foobar",
+ "build_near_host_ip": "192.168.1.1",
+ "cidr": "/24"
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}
+
+func TestCreateOptsWithComplexQuery(t *testing.T) {
+ base := servers.CreateOpts{
+ Name: "createdserver",
+ ImageRef: "asdfasdfasdf",
+ FlavorRef: "performance1-1",
+ }
+
+ schedulerHints := SchedulerHints{
+ Group: "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+ DifferentHost: []string{
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287",
+ },
+ SameHost: []string{
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287",
+ },
+ Query: []interface{}{"and", []string{">=", "$free_ram_mb", "1024"}, []string{">=", "$free_disk_mb", "204800"}},
+ TargetCell: "foobar",
+ BuildNearHostIP: "192.168.1.1/24",
+ }
+
+ ext := CreateOptsExt{
+ CreateOptsBuilder: base,
+ SchedulerHints: schedulerHints,
+ }
+
+ expected := `
+ {
+ "server": {
+ "name": "createdserver",
+ "imageRef": "asdfasdfasdf",
+ "flavorRef": "performance1-1"
+ },
+ "os:scheduler_hints": {
+ "group": "101aed42-22d9-4a3e-9ba1-21103b0d1aba",
+ "different_host": [
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287"
+ ],
+ "same_host": [
+ "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
+ "8c19174f-4220-44f0-824a-cd1eeef10287"
+ ],
+ "query": [
+ "and",
+ [">=", "$free_ram_mb", "1024"],
+ [">=", "$free_disk_mb", "204800"]
+ ],
+ "target_cell": "foobar",
+ "build_near_host_ip": "192.168.1.1",
+ "cidr": "/24"
+ }
+ }
+ `
+ actual, err := ext.ToServerCreateMap()
+ th.AssertNoErr(t, err)
+ th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/objectstorage/v1/objects/requests.go b/openstack/objectstorage/v1/objects/requests.go
index 7eedde2..62c006a 100644
--- a/openstack/objectstorage/v1/objects/requests.go
+++ b/openstack/objectstorage/v1/objects/requests.go
@@ -188,7 +188,7 @@
}
// Create is a function that creates a new object or replaces an existing object.
-func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOptsBuilder) CreateResult {
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts CreateOptsBuilder) CreateResult {
var res CreateResult
url := createURL(c, containerName, objectName)
diff --git a/openstack/objectstorage/v1/objects/requests_test.go b/openstack/objectstorage/v1/objects/requests_test.go
index 6be3534..941e59e 100644
--- a/openstack/objectstorage/v1/objects/requests_test.go
+++ b/openstack/objectstorage/v1/objects/requests_test.go
@@ -3,6 +3,7 @@
import (
"bytes"
"io"
+ "strings"
"testing"
"github.com/rackspace/gophercloud/pagination"
@@ -85,7 +86,7 @@
defer th.TeardownHTTP()
HandleCreateTextObjectSuccessfully(t)
- content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+ content := strings.NewReader("Did gyre and gimble in the wabe")
options := &CreateOpts{ContentType: "text/plain"}
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
th.AssertNoErr(t, res.Err)
@@ -96,7 +97,7 @@
defer th.TeardownHTTP()
HandleCreateTypelessObjectSuccessfully(t)
- content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
+ content := strings.NewReader("The sky was the color of television, tuned to a dead channel.")
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &CreateOpts{})
th.AssertNoErr(t, res.Err)
}
diff --git a/provider_client.go b/provider_client.go
index 0dff2cf..e5b75b2 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -85,9 +85,9 @@
// content type of the request will default to "application/json" unless overridden by MoreHeaders.
// It's an error to specify both a JSONBody and a RawBody.
JSONBody interface{}
- // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
+ // RawBody contains an io.ReadSeeker that will be consumed by the request directly. No content-type
// will be set unless one is provided explicitly by MoreHeaders.
- RawBody io.Reader
+ RawBody io.ReadSeeker
// JSONResponse, if provided, will be populated with the contents of the response body parsed as
// JSON.
@@ -124,11 +124,11 @@
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
- var body io.Reader
+ var body io.ReadSeeker
var contentType *string
// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
- // io.Reader as-is. Default the content-type to application/json.
+ // io.ReadSeeker as-is. Default the content-type to application/json.
if options.JSONBody != nil {
if options.RawBody != nil {
panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
@@ -189,6 +189,7 @@
if err != nil {
return nil, fmt.Errorf("Error trying to re-authenticate: %s", err)
}
+ options.RawBody.Seek(0, 0)
resp, err = client.Request(method, url, options)
if err != nil {
return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
@@ -260,7 +261,7 @@
opts = &RequestOpts{}
}
- if v, ok := (JSONBody).(io.Reader); ok {
+ if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
@@ -278,7 +279,7 @@
opts = &RequestOpts{}
}
- if v, ok := (JSONBody).(io.Reader); ok {
+ if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
diff --git a/rackspace/objectstorage/v1/objects/delegate.go b/rackspace/objectstorage/v1/objects/delegate.go
index 028d66a..94c820b 100644
--- a/rackspace/objectstorage/v1/objects/delegate.go
+++ b/rackspace/objectstorage/v1/objects/delegate.go
@@ -33,7 +33,7 @@
}
// Create is a function that creates a new object or replaces an existing object.
-func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts os.CreateOptsBuilder) os.CreateResult {
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.ReadSeeker, opts os.CreateOptsBuilder) os.CreateResult {
return os.Create(c, containerName, objectName, content, opts)
}
diff --git a/rackspace/objectstorage/v1/objects/delegate_test.go b/rackspace/objectstorage/v1/objects/delegate_test.go
index 8ab8029..3431056 100644
--- a/rackspace/objectstorage/v1/objects/delegate_test.go
+++ b/rackspace/objectstorage/v1/objects/delegate_test.go
@@ -1,7 +1,7 @@
package objects
import (
- "bytes"
+ "strings"
"testing"
os "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects"
@@ -68,7 +68,7 @@
defer th.TeardownHTTP()
os.HandleCreateTextObjectSuccessfully(t)
- content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+ content := strings.NewReader("Did gyre and gimble in the wabe")
options := &os.CreateOpts{ContentType: "text/plain"}
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, options)
th.AssertNoErr(t, res.Err)
@@ -79,7 +79,7 @@
defer th.TeardownHTTP()
os.HandleCreateTypelessObjectSuccessfully(t)
- content := bytes.NewBufferString("The sky was the color of television, tuned to a dead channel.")
+ content := strings.NewReader("The sky was the color of television, tuned to a dead channel.")
res := Create(fake.ServiceClient(), "testContainer", "testObject", content, &os.CreateOpts{})
th.AssertNoErr(t, res.Err)
}