Merge remote-tracking branch 'upstream/v0.2.0' into rackspace-compute
Conflicts:
acceptance/README.md
rackspace/client.go
diff --git a/acceptance/README.md b/acceptance/README.md
index df84a46..1e86c9f 100644
--- a/acceptance/README.md
+++ b/acceptance/README.md
@@ -28,12 +28,15 @@
|`OS_AUTH_URL`|The identity URL you need to authenticate|
|`OS_TENANT_NAME`|Your API tenant name|
|`OS_TENANT_ID`|Your API tenant ID|
+|`RS_USERNAME`|Your Rackspace username|
+|`RS_APIKEY`|Your Rackspace API key|
#### General
|Name|Description|
|---|---|
|`OS_REGION_NAME`|The region you want your resources to reside in|
+|`RS_REGION`|Rackspace region you want your resource to reside in|
#### Compute
@@ -42,6 +45,8 @@
|`OS_IMAGE_ID`|The ID of the image your want your server to be based on|
|`OS_FLAVOR_ID`|The ID of the flavor you want your server to be based on|
|`OS_FLAVOR_ID_RESIZE`|The ID of the flavor you want your server to be resized to|
+|`RS_IMAGE_ID`|The ID of the image you want servers to be created with|
+|`RS_FLAVOR_ID`|The ID of the flavor you want your server to be created with|
### 2. Run the test suite
diff --git a/acceptance/rackspace/compute/v2/compute_test.go b/acceptance/rackspace/compute/v2/compute_test.go
new file mode 100644
index 0000000..3419c10
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/compute_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v2
+
+import (
+ "errors"
+ "os"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ // Obtain credentials from the environment.
+ options := gophercloud.AuthOptions{
+ Username: os.Getenv("RS_USERNAME"),
+ APIKey: os.Getenv("RS_APIKEY"),
+ }
+ region := os.Getenv("RS_REGION")
+
+ if options.Username == "" {
+ return nil, errors.New("Please provide a Rackspace username as RS_USERNAME.")
+ }
+ if options.APIKey == "" {
+ return nil, errors.New("Please provide a Rackspace API key as RS_APIKEY.")
+ }
+ if region == "" {
+ return nil, errors.New("Please provide a Rackspace region as RS_REGION.")
+ }
+
+ client, err := rackspace.AuthenticatedClient(options)
+ if err != nil {
+ return nil, err
+ }
+
+ return rackspace.NewComputeV2(client, gophercloud.EndpointOpts{
+ Region: region,
+ })
+}
+
+type serverOpts struct {
+ imageID string
+ flavorID string
+}
+
+func optionsFromEnv() (*serverOpts, error) {
+ options := &serverOpts{
+ imageID: os.Getenv("RS_IMAGE_ID"),
+ flavorID: os.Getenv("RS_FLAVOR_ID"),
+ }
+ if options.imageID == "" {
+ return nil, errors.New("Please provide a valid Rackspace image ID as RS_IMAGE_ID")
+ }
+ if options.flavorID == "" {
+ return nil, errors.New("Please provide a valid Rackspace flavor ID as RS_FLAVOR_ID")
+ }
+ return options, nil
+}
diff --git a/acceptance/rackspace/compute/v2/flavors_test.go b/acceptance/rackspace/compute/v2/flavors_test.go
new file mode 100644
index 0000000..248ab91
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/flavors_test.go
@@ -0,0 +1,61 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/compute/v2/flavors"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListFlavors(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ count := 0
+ err = flavors.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ t.Logf("-- Page %0d --", count)
+
+ fs, err := flavors.ExtractFlavors(page)
+ th.AssertNoErr(t, err)
+
+ for i, flavor := range fs {
+ t.Logf("[%02d] id=[%s]", i, flavor.ID)
+ t.Logf(" name=[%s]", flavor.Name)
+ t.Logf(" disk=[%d]", flavor.Disk)
+ t.Logf(" RAM=[%d]", flavor.RAM)
+ t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor)
+ t.Logf(" swap=[%d]", flavor.Swap)
+ t.Logf(" VCPUs=[%d]", flavor.VCPUs)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ if count == 0 {
+ t.Errorf("No flavors listed!")
+ }
+}
+
+func TestGetFlavor(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ options, err := optionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ flavor, err := flavors.Get(client, options.flavorID).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Requested flavor:")
+ t.Logf(" id=[%s]", flavor.ID)
+ t.Logf(" name=[%s]", flavor.Name)
+ t.Logf(" disk=[%d]", flavor.Disk)
+ t.Logf(" RAM=[%d]", flavor.RAM)
+ t.Logf(" rxtx_factor=[%f]", flavor.RxTxFactor)
+ t.Logf(" swap=[%d]", flavor.Swap)
+ t.Logf(" VCPUs=[%d]", flavor.VCPUs)
+}
diff --git a/acceptance/rackspace/compute/v2/images_test.go b/acceptance/rackspace/compute/v2/images_test.go
new file mode 100644
index 0000000..5e36c2e
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/images_test.go
@@ -0,0 +1,63 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/compute/v2/images"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestListImages(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ count := 0
+ err = images.ListDetail(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ t.Logf("-- Page %02d --", count)
+
+ is, err := images.ExtractImages(page)
+ th.AssertNoErr(t, err)
+
+ for i, image := range is {
+ t.Logf("[%02d] id=[%s]", i, image.ID)
+ t.Logf(" name=[%s]", image.Name)
+ t.Logf(" created=[%s]", image.Created)
+ t.Logf(" updated=[%s]", image.Updated)
+ t.Logf(" min disk=[%d]", image.MinDisk)
+ t.Logf(" min RAM=[%d]", image.MinRAM)
+ t.Logf(" progress=[%d]", image.Progress)
+ t.Logf(" status=[%s]", image.Status)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ if count < 1 {
+ t.Errorf("Expected at least one page of images.")
+ }
+}
+
+func TestGetImage(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ options, err := optionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ image, err := images.Get(client, options.imageID).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Requested image:")
+ t.Logf(" id=[%s]", image.ID)
+ t.Logf(" name=[%s]", image.Name)
+ t.Logf(" created=[%s]", image.Created)
+ t.Logf(" updated=[%s]", image.Updated)
+ t.Logf(" min disk=[%d]", image.MinDisk)
+ t.Logf(" min RAM=[%d]", image.MinRAM)
+ t.Logf(" progress=[%d]", image.Progress)
+ t.Logf(" status=[%s]", image.Status)
+}
diff --git a/acceptance/rackspace/compute/v2/pkg.go b/acceptance/rackspace/compute/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/rackspace/compute/v2/servers_test.go b/acceptance/rackspace/compute/v2/servers_test.go
new file mode 100644
index 0000000..6247c26
--- /dev/null
+++ b/acceptance/rackspace/compute/v2/servers_test.go
@@ -0,0 +1,158 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/compute/v2/servers"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient) *os.Server {
+ options, err := optionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ 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,
+ }).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Creating server.")
+
+ err = servers.WaitForStatus(client, s.ID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+ t.Logf("Server created successfully.")
+
+ return s
+}
+
+func logServer(t *testing.T, server *os.Server, index int) {
+ if index == -1 {
+ t.Logf(" id=[%s]", server.ID)
+ } else {
+ t.Logf("[%02d] id=[%s]", index, server.ID)
+ }
+ t.Logf(" name=[%s]", server.Name)
+ t.Logf(" tenant ID=[%s]", server.TenantID)
+ t.Logf(" user ID=[%s]", server.UserID)
+ t.Logf(" updated=[%s]", server.Updated)
+ t.Logf(" created=[%s]", server.Created)
+ t.Logf(" host ID=[%s]", server.HostID)
+ t.Logf(" access IPv4=[%s]", server.AccessIPv4)
+ t.Logf(" access IPv6=[%s]", server.AccessIPv6)
+ t.Logf(" image=[%v]", server.Image)
+ t.Logf(" flavor=[%v]", server.Flavor)
+ t.Logf(" addresses=[%v]", server.Addresses)
+ t.Logf(" metadata=[%v]", server.Metadata)
+ t.Logf(" links=[%v]", server.Links)
+ t.Logf(" keyname=[%s]", server.KeyName)
+ t.Logf(" admin password=[%s]", server.AdminPass)
+ t.Logf(" status=[%s]", server.Status)
+ t.Logf(" progress=[%d]", server.Progress)
+}
+
+func getServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+ t.Logf("> servers.Get")
+
+ details, err := servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+ logServer(t, details, -1)
+}
+
+func listServers(t *testing.T, client *gophercloud.ServiceClient) {
+ t.Logf("> servers.List")
+
+ count := 0
+ err := servers.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ t.Logf("--- Page %02d ---", count)
+
+ s, err := servers.ExtractServers(page)
+ th.AssertNoErr(t, err)
+ for index, server := range s {
+ logServer(t, &server, index)
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func changeAdminPassword(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+ t.Logf("> servers.ChangeAdminPassword")
+
+ original := server.AdminPass
+
+ t.Logf("Changing server password.")
+ err := servers.ChangeAdminPassword(client, server.ID, tools.MakeNewPassword(original)).Extract()
+ th.AssertNoErr(t, err)
+
+ err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+ t.Logf("Password changed successfully.")
+}
+
+func rebootServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+ t.Logf("> servers.Reboot")
+
+ err := servers.Reboot(client, server.ID, os.HardReboot).Extract()
+ th.AssertNoErr(t, err)
+
+ err = servers.WaitForStatus(client, server.ID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Server successfully rebooted.")
+}
+
+func rebuildServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+ t.Logf("> servers.Rebuild")
+
+ options, err := optionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ opts := os.RebuildOpts{
+ Name: tools.RandomString("RenamedGopher", 16),
+ AdminPass: tools.MakeNewPassword(server.AdminPass),
+ ImageID: options.imageID,
+ }
+ after, err := servers.Rebuild(client, server.ID, opts).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, after.ID, server.ID)
+
+ err = servers.WaitForStatus(client, after.ID, "ACTIVE", 300)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Server successfully rebuilt.")
+ logServer(t, after, -1)
+}
+
+func deleteServer(t *testing.T, client *gophercloud.ServiceClient, server *os.Server) {
+ t.Logf("> servers.Delete")
+
+ err := servers.Delete(client, server.ID)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Server deleted successfully.")
+}
+
+func TestServerOperations(t *testing.T) {
+ client, err := newClient()
+ th.AssertNoErr(t, err)
+
+ server := createServer(t, client)
+ defer deleteServer(t, client, server)
+
+ getServer(t, client, server)
+ listServers(t, client)
+ changeAdminPassword(t, client, server)
+ rebootServer(t, client, server)
+ rebuildServer(t, client, server)
+}
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
index 4771ebb..ffade12 100644
--- a/acceptance/tools/tools.go
+++ b/acceptance/tools/tools.go
@@ -1,4 +1,5 @@
// +build acceptance
+
package tools
import (
diff --git a/openstack/compute/v2/servers/data_test.go b/openstack/compute/v2/servers/data_test.go
deleted file mode 100644
index d3a0ee0..0000000
--- a/openstack/compute/v2/servers/data_test.go
+++ /dev/null
@@ -1,328 +0,0 @@
-package servers
-
-// Recorded responses for the server resource.
-
-const (
- serverListBody = `
- {
- "servers": [
- {
- "status": "ACTIVE",
- "updated": "2014-09-25T13:10:10Z",
- "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
- "OS-EXT-SRV-ATTR:host": "devstack",
- "addresses": {
- "private": [
- {
- "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
- "version": 4,
- "addr": "10.0.0.32",
- "OS-EXT-IPS:type": "fixed"
- }
- ]
- },
- "links": [
- {
- "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
- "rel": "self"
- },
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
- "rel": "bookmark"
- }
- ],
- "key_name": null,
- "image": {
- "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
- "rel": "bookmark"
- }
- ]
- },
- "OS-EXT-STS:task_state": null,
- "OS-EXT-STS:vm_state": "active",
- "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
- "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
- "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
- "flavor": {
- "id": "1",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark"
- }
- ]
- },
- "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
- "security_groups": [
- {
- "name": "default"
- }
- ],
- "OS-SRV-USG:terminated_at": null,
- "OS-EXT-AZ:availability_zone": "nova",
- "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
- "name": "herp",
- "created": "2014-09-25T13:10:02Z",
- "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
- "OS-DCF:diskConfig": "MANUAL",
- "os-extended-volumes:volumes_attached": [],
- "accessIPv4": "",
- "accessIPv6": "",
- "progress": 0,
- "OS-EXT-STS:power_state": 1,
- "config_drive": "",
- "metadata": {}
- },
- {
- "status": "ACTIVE",
- "updated": "2014-09-25T13:04:49Z",
- "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
- "OS-EXT-SRV-ATTR:host": "devstack",
- "addresses": {
- "private": [
- {
- "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
- "version": 4,
- "addr": "10.0.0.31",
- "OS-EXT-IPS:type": "fixed"
- }
- ]
- },
- "links": [
- {
- "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "rel": "self"
- },
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "rel": "bookmark"
- }
- ],
- "key_name": null,
- "image": {
- "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
- "rel": "bookmark"
- }
- ]
- },
- "OS-EXT-STS:task_state": null,
- "OS-EXT-STS:vm_state": "active",
- "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
- "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
- "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
- "flavor": {
- "id": "1",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark"
- }
- ]
- },
- "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "security_groups": [
- {
- "name": "default"
- }
- ],
- "OS-SRV-USG:terminated_at": null,
- "OS-EXT-AZ:availability_zone": "nova",
- "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
- "name": "derp",
- "created": "2014-09-25T13:04:41Z",
- "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
- "OS-DCF:diskConfig": "MANUAL",
- "os-extended-volumes:volumes_attached": [],
- "accessIPv4": "",
- "accessIPv6": "",
- "progress": 0,
- "OS-EXT-STS:power_state": 1,
- "config_drive": "",
- "metadata": {}
- }
- ]
- }
- `
-
- singleServerBody = `
- {
- "server": {
- "status": "ACTIVE",
- "updated": "2014-09-25T13:04:49Z",
- "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
- "OS-EXT-SRV-ATTR:host": "devstack",
- "addresses": {
- "private": [
- {
- "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
- "version": 4,
- "addr": "10.0.0.31",
- "OS-EXT-IPS:type": "fixed"
- }
- ]
- },
- "links": [
- {
- "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "rel": "self"
- },
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "rel": "bookmark"
- }
- ],
- "key_name": null,
- "image": {
- "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
- "rel": "bookmark"
- }
- ]
- },
- "OS-EXT-STS:task_state": null,
- "OS-EXT-STS:vm_state": "active",
- "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
- "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
- "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
- "flavor": {
- "id": "1",
- "links": [
- {
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark"
- }
- ]
- },
- "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "security_groups": [
- {
- "name": "default"
- }
- ],
- "OS-SRV-USG:terminated_at": null,
- "OS-EXT-AZ:availability_zone": "nova",
- "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
- "name": "derp",
- "created": "2014-09-25T13:04:41Z",
- "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
- "OS-DCF:diskConfig": "MANUAL",
- "os-extended-volumes:volumes_attached": [],
- "accessIPv4": "",
- "accessIPv6": "",
- "progress": 0,
- "OS-EXT-STS:power_state": 1,
- "config_drive": "",
- "metadata": {}
- }
- }
- `
-)
-
-var (
- serverHerp = Server{
- Status: "ACTIVE",
- Updated: "2014-09-25T13:10:10Z",
- HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
- Addresses: map[string]interface{}{
- "private": []interface{}{
- map[string]interface{}{
- "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
- "version": float64(4),
- "addr": "10.0.0.32",
- "OS-EXT-IPS:type": "fixed",
- },
- },
- },
- Links: []interface{}{
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
- "rel": "self",
- },
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
- "rel": "bookmark",
- },
- },
- Image: map[string]interface{}{
- "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "links": []interface{}{
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
- "rel": "bookmark",
- },
- },
- },
- Flavor: map[string]interface{}{
- "id": "1",
- "links": []interface{}{
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark",
- },
- },
- },
- ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
- UserID: "9349aff8be7545ac9d2f1d00999a23cd",
- Name: "herp",
- Created: "2014-09-25T13:10:02Z",
- TenantID: "fcad67a6189847c4aecfa3c81a05783b",
- Metadata: map[string]interface{}{},
- }
- serverDerp = Server{
- Status: "ACTIVE",
- Updated: "2014-09-25T13:04:49Z",
- HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
- Addresses: map[string]interface{}{
- "private": []interface{}{
- map[string]interface{}{
- "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
- "version": float64(4),
- "addr": "10.0.0.31",
- "OS-EXT-IPS:type": "fixed",
- },
- },
- },
- Links: []interface{}{
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "rel": "self",
- },
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- "rel": "bookmark",
- },
- },
- Image: map[string]interface{}{
- "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "links": []interface{}{
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
- "rel": "bookmark",
- },
- },
- },
- Flavor: map[string]interface{}{
- "id": "1",
- "links": []interface{}{
- map[string]interface{}{
- "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
- "rel": "bookmark",
- },
- },
- },
- ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
- UserID: "9349aff8be7545ac9d2f1d00999a23cd",
- Name: "derp",
- Created: "2014-09-25T13:04:41Z",
- TenantID: "fcad67a6189847c4aecfa3c81a05783b",
- Metadata: map[string]interface{}{},
- }
-)
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
new file mode 100644
index 0000000..e5f7c4b
--- /dev/null
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -0,0 +1,415 @@
+// +build fixtures
+
+package servers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ServerListBody contains the canned body of a servers.List response.
+const ServerListBody = `
+{
+ "servers": [
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-25T13:10:10Z",
+ "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ "OS-EXT-SRV-ATTR:host": "devstack",
+ "addresses": {
+ "private": [
+ {
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+ "version": 4,
+ "addr": "10.0.0.32",
+ "OS-EXT-IPS:type": "fixed"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "self"
+ },
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "bookmark"
+ }
+ ],
+ "key_name": null,
+ "image": {
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e",
+ "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000",
+ "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "security_groups": [
+ {
+ "name": "default"
+ }
+ ],
+ "OS-SRV-USG:terminated_at": null,
+ "OS-EXT-AZ:availability_zone": "nova",
+ "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+ "name": "herp",
+ "created": "2014-09-25T13:10:02Z",
+ "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+ "OS-DCF:diskConfig": "MANUAL",
+ "os-extended-volumes:volumes_attached": [],
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "progress": 0,
+ "OS-EXT-STS:power_state": 1,
+ "config_drive": "",
+ "metadata": {}
+ },
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-25T13:04:49Z",
+ "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ "OS-EXT-SRV-ATTR:host": "devstack",
+ "addresses": {
+ "private": [
+ {
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+ "version": 4,
+ "addr": "10.0.0.31",
+ "OS-EXT-IPS:type": "fixed"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "self"
+ },
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "bookmark"
+ }
+ ],
+ "key_name": null,
+ "image": {
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+ "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+ "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "security_groups": [
+ {
+ "name": "default"
+ }
+ ],
+ "OS-SRV-USG:terminated_at": null,
+ "OS-EXT-AZ:availability_zone": "nova",
+ "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+ "name": "derp",
+ "created": "2014-09-25T13:04:41Z",
+ "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+ "OS-DCF:diskConfig": "MANUAL",
+ "os-extended-volumes:volumes_attached": [],
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "progress": 0,
+ "OS-EXT-STS:power_state": 1,
+ "config_drive": "",
+ "metadata": {}
+ }
+ ]
+}
+`
+
+// SingleServerBody is the canned body of a Get request on an existing server.
+const SingleServerBody = `
+{
+ "server": {
+ "status": "ACTIVE",
+ "updated": "2014-09-25T13:04:49Z",
+ "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ "OS-EXT-SRV-ATTR:host": "devstack",
+ "addresses": {
+ "private": [
+ {
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+ "version": 4,
+ "addr": "10.0.0.31",
+ "OS-EXT-IPS:type": "fixed"
+ }
+ ]
+ },
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "self"
+ },
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "bookmark"
+ }
+ ],
+ "key_name": null,
+ "image": {
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d",
+ "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000",
+ "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack",
+ "flavor": {
+ "id": "1",
+ "links": [
+ {
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "security_groups": [
+ {
+ "name": "default"
+ }
+ ],
+ "OS-SRV-USG:terminated_at": null,
+ "OS-EXT-AZ:availability_zone": "nova",
+ "user_id": "9349aff8be7545ac9d2f1d00999a23cd",
+ "name": "derp",
+ "created": "2014-09-25T13:04:41Z",
+ "tenant_id": "fcad67a6189847c4aecfa3c81a05783b",
+ "OS-DCF:diskConfig": "MANUAL",
+ "os-extended-volumes:volumes_attached": [],
+ "accessIPv4": "",
+ "accessIPv6": "",
+ "progress": 0,
+ "OS-EXT-STS:power_state": 1,
+ "config_drive": "",
+ "metadata": {}
+ }
+}
+`
+
+var (
+ // ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
+ ServerHerp = Server{
+ Status: "ACTIVE",
+ Updated: "2014-09-25T13:10:10Z",
+ HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b",
+ "version": float64(4),
+ "addr": "10.0.0.32",
+ "OS-EXT-IPS:type": "fixed",
+ },
+ },
+ },
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ "rel": "bookmark",
+ },
+ },
+ Image: map[string]interface{}{
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "1",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark",
+ },
+ },
+ },
+ ID: "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5",
+ UserID: "9349aff8be7545ac9d2f1d00999a23cd",
+ Name: "herp",
+ Created: "2014-09-25T13:10:02Z",
+ TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+ Metadata: map[string]interface{}{},
+ }
+
+ // ServerDerp is a Server struct that should correspond to the second server in ServerListBody.
+ ServerDerp = Server{
+ Status: "ACTIVE",
+ Updated: "2014-09-25T13:04:49Z",
+ HostID: "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362",
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be",
+ "version": float64(4),
+ "addr": "10.0.0.31",
+ "OS-EXT-IPS:type": "fixed",
+ },
+ },
+ },
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ "rel": "bookmark",
+ },
+ },
+ Image: map[string]interface{}{
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "1",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1",
+ "rel": "bookmark",
+ },
+ },
+ },
+ ID: "9e5476bd-a4ec-4653-93d6-72c93aa682ba",
+ UserID: "9349aff8be7545ac9d2f1d00999a23cd",
+ Name: "derp",
+ Created: "2014-09-25T13:04:41Z",
+ TenantID: "fcad67a6189847c4aecfa3c81a05783b",
+ Metadata: map[string]interface{}{},
+ }
+)
+
+// HandleServerCreationSuccessfully sets up the test server to respond to a server creation request
+// with a given response.
+func HandleServerCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "server": {
+ "name": "derp",
+ "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "flavorRef": "1"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// 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) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
+// change request.
+func HandleAdminPasswordChangeSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleRebootSuccessfully sets up the test server to respond to a reboot request with success.
+func HandleRebootSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleRebuildSuccessfully sets up the test server to respond to a rebuild request with success.
+func HandleRebuildSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `
+ {
+ "rebuild": {
+ "name": "new-name",
+ "adminPass": "swordfish",
+ "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ "accessIPv4": "1.2.3.4"
+ }
+ }
+ `)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 0db92f9..5b65d86 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -6,24 +6,24 @@
"testing"
"github.com/rackspace/gophercloud/pagination"
- "github.com/rackspace/gophercloud/testhelper"
- fake "github.com/rackspace/gophercloud/testhelper/client"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
)
func TestListServers(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ 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)
+ fmt.Fprintf(w, ServerListBody)
case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
fmt.Fprintf(w, `{ "servers": [] }`)
default:
@@ -32,7 +32,7 @@
})
pages := 0
- err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ err := List(client.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
pages++
actual, err := ExtractServers(page)
@@ -43,13 +43,13 @@
if len(actual) != 2 {
t.Fatalf("Expected 2 servers, got %d", len(actual))
}
- equalServers(t, serverHerp, actual[0])
- equalServers(t, serverDerp, actual[1])
+ th.CheckDeepEquals(t, ServerHerp, actual[0])
+ th.CheckDeepEquals(t, ServerDerp, actual[1])
return true, nil
})
- testhelper.AssertNoErr(t, err)
+ th.AssertNoErr(t, err)
if pages != 1 {
t.Errorf("Expected 1 page, saw %d", pages)
@@ -57,154 +57,95 @@
}
func TestCreateServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleServerCreationSuccessfully(t, SingleServerBody)
- testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `{
- "server": {
- "name": "derp",
- "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "flavorRef": "1"
- }
- }`)
-
- w.WriteHeader(http.StatusAccepted)
- w.Header().Add("Content-Type", "application/json")
- fmt.Fprintf(w, singleServerBody)
- })
-
- client := fake.ServiceClient()
- actual, err := Create(client, CreateOpts{
+ actual, err := Create(client.ServiceClient(), CreateOpts{
Name: "derp",
ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb",
FlavorRef: "1",
}).Extract()
- if err != nil {
- t.Fatalf("Unexpected Create error: %v", err)
- }
+ th.AssertNoErr(t, err)
- equalServers(t, serverDerp, *actual)
+ th.CheckDeepEquals(t, ServerDerp, *actual)
}
func TestDeleteServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleServerDeletionSuccessfully(t)
- testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "DELETE")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-
- w.WriteHeader(http.StatusNoContent)
- })
-
- client := fake.ServiceClient()
- err := Delete(client, "asdfasdfasdf")
- if err != nil {
- t.Fatalf("Unexpected Delete error: %v", err)
- }
+ err := Delete(client.ServiceClient(), "asdfasdfasdf")
+ th.AssertNoErr(t, err)
}
func TestGetServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "GET")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+ 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)
+ fmt.Fprintf(w, SingleServerBody)
})
- client := fake.ServiceClient()
+ client := client.ServiceClient()
actual, err := Get(client, "1234asdf").Extract()
if err != nil {
t.Fatalf("Unexpected Get error: %v", err)
}
- equalServers(t, serverDerp, *actual)
+ th.CheckDeepEquals(t, ServerDerp, *actual)
}
func TestUpdateServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "PUT")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestHeader(t, r, "Accept", "application/json")
- testhelper.TestHeader(t, r, "Content-Type", "application/json")
- testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+ 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)
+ fmt.Fprintf(w, SingleServerBody)
})
- client := fake.ServiceClient()
+ client := client.ServiceClient()
actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
if err != nil {
t.Fatalf("Unexpected Update error: %v", err)
}
- equalServers(t, serverDerp, *actual)
+ th.CheckDeepEquals(t, ServerDerp, *actual)
}
func TestChangeServerAdminPassword(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleAdminPasswordChangeSuccessfully(t)
- testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
-
- w.WriteHeader(http.StatusAccepted)
- })
-
- res := ChangeAdminPassword(fake.ServiceClient(), "1234asdf", "new-password")
- testhelper.AssertNoErr(t, res.Err)
+ res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+ th.AssertNoErr(t, res.Err)
}
func TestRebootServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleRebootSuccessfully(t)
- testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
-
- w.WriteHeader(http.StatusAccepted)
- })
-
- res := Reboot(fake.ServiceClient(), "1234asdf", SoftReboot)
- testhelper.AssertNoErr(t, res.Err)
+ res := Reboot(client.ServiceClient(), "1234asdf", SoftReboot)
+ th.AssertNoErr(t, res.Err)
}
func TestRebuildServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
-
- testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `
- {
- "rebuild": {
- "name": "new-name",
- "adminPass": "swordfish",
- "imageRef": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
- "accessIPv4": "1.2.3.4"
- }
- }
- `)
-
- w.WriteHeader(http.StatusAccepted)
- w.Header().Add("Content-Type", "application/json")
- fmt.Fprintf(w, singleServerBody)
- })
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleRebuildSuccessfully(t, SingleServerBody)
opts := RebuildOpts{
Name: "new-name",
@@ -213,56 +154,56 @@
AccessIPv4: "1.2.3.4",
}
- actual, err := Rebuild(fake.ServiceClient(), "1234asdf", opts).Extract()
- testhelper.AssertNoErr(t, err)
+ actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract()
+ th.AssertNoErr(t, err)
- equalServers(t, serverDerp, *actual)
+ th.CheckDeepEquals(t, ServerDerp, *actual)
}
func TestResizeServer(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+ th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
w.WriteHeader(http.StatusAccepted)
})
- res := Resize(fake.ServiceClient(), "1234asdf", "2")
- testhelper.AssertNoErr(t, res.Err)
+ res := Resize(client.ServiceClient(), "1234asdf", "2")
+ th.AssertNoErr(t, res.Err)
}
func TestConfirmResize(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+ th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{ "confirmResize": null }`)
w.WriteHeader(http.StatusNoContent)
})
- res := ConfirmResize(fake.ServiceClient(), "1234asdf")
- testhelper.AssertNoErr(t, res.Err)
+ res := ConfirmResize(client.ServiceClient(), "1234asdf")
+ th.AssertNoErr(t, res.Err)
}
func TestRevertResize(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
- testhelper.TestMethod(t, r, "POST")
- testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
- testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
+ th.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{ "revertResize": null }`)
w.WriteHeader(http.StatusAccepted)
})
- res := RevertResize(fake.ServiceClient(), "1234asdf")
- testhelper.AssertNoErr(t, res.Err)
+ res := RevertResize(client.ServiceClient(), "1234asdf")
+ th.AssertNoErr(t, res.Err)
}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index c3e41d7..74a221f 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -101,11 +101,11 @@
Links []interface{}
// KeyName indicates which public key was injected into the server on launch.
- KeyName string `mapstructure:"keyname"`
+ KeyName string `json:"key_name" mapstructure:"key_name"`
// AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place.
// Note that this is the ONLY time this field will be valid.
- AdminPass string `mapstructure:"adminPass"`
+ AdminPass string `json:"adminPass" mapstructure:"adminPass"`
}
// ServerPage abstracts the raw results of making a List() request against the API.
diff --git a/openstack/compute/v2/servers/servers_test.go b/openstack/compute/v2/servers/servers_test.go
deleted file mode 100644
index 590fc8b..0000000
--- a/openstack/compute/v2/servers/servers_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package servers
-
-import (
- "reflect"
- "testing"
-)
-
-// This provides more fine-grained failures when Servers differ, because Server structs are too damn big to compare by eye.
-// FIXME I should absolutely refactor this into a general-purpose thing in testhelper.
-func equalServers(t *testing.T, expected Server, actual Server) {
- if expected.ID != actual.ID {
- t.Errorf("ID differs. expected=[%s], actual=[%s]", expected.ID, actual.ID)
- }
- if expected.TenantID != actual.TenantID {
- t.Errorf("TenantID differs. expected=[%s], actual=[%s]", expected.TenantID, actual.TenantID)
- }
- if expected.UserID != actual.UserID {
- t.Errorf("UserID differs. expected=[%s], actual=[%s]", expected.UserID, actual.UserID)
- }
- if expected.Name != actual.Name {
- t.Errorf("Name differs. expected=[%s], actual=[%s]", expected.Name, actual.Name)
- }
- if expected.Updated != actual.Updated {
- t.Errorf("Updated differs. expected=[%s], actual=[%s]", expected.Updated, actual.Updated)
- }
- if expected.Created != actual.Created {
- t.Errorf("Created differs. expected=[%s], actual=[%s]", expected.Created, actual.Created)
- }
- if expected.HostID != actual.HostID {
- t.Errorf("HostID differs. expected=[%s], actual=[%s]", expected.HostID, actual.HostID)
- }
- if expected.Status != actual.Status {
- t.Errorf("Status differs. expected=[%s], actual=[%s]", expected.Status, actual.Status)
- }
- if expected.Progress != actual.Progress {
- t.Errorf("Progress differs. expected=[%s], actual=[%s]", expected.Progress, actual.Progress)
- }
- if expected.AccessIPv4 != actual.AccessIPv4 {
- t.Errorf("AccessIPv4 differs. expected=[%s], actual=[%s]", expected.AccessIPv4, actual.AccessIPv4)
- }
- if expected.AccessIPv6 != actual.AccessIPv6 {
- t.Errorf("AccessIPv6 differs. expected=[%s], actual=[%s]", expected.AccessIPv6, actual.AccessIPv6)
- }
- if !reflect.DeepEqual(expected.Image, actual.Image) {
- t.Errorf("Image differs. expected=[%s], actual=[%s]", expected.Image, actual.Image)
- }
- if !reflect.DeepEqual(expected.Flavor, actual.Flavor) {
- t.Errorf("Flavor differs. expected=[%s], actual=[%s]", expected.Flavor, actual.Flavor)
- }
- if !reflect.DeepEqual(expected.Addresses, actual.Addresses) {
- t.Errorf("Addresses differ. expected=[%s], actual=[%s]", expected.Addresses, actual.Addresses)
- }
- if !reflect.DeepEqual(expected.Metadata, actual.Metadata) {
- t.Errorf("Metadata differs. expected=[%s], actual=[%s]", expected.Metadata, actual.Metadata)
- }
- if !reflect.DeepEqual(expected.Links, actual.Links) {
- t.Errorf("Links differs. expected=[%s], actual=[%s]", expected.Links, actual.Links)
- }
- if expected.KeyName != actual.KeyName {
- t.Errorf("KeyName differs. expected=[%s], actual=[%s]", expected.KeyName, actual.KeyName)
- }
- if expected.AdminPass != actual.AdminPass {
- t.Errorf("AdminPass differs. expected=[%s], actual=[%s]", expected.AdminPass, actual.AdminPass)
- }
-}
diff --git a/openstack/compute/v2/servers/util.go b/openstack/compute/v2/servers/util.go
new file mode 100644
index 0000000..e6baf74
--- /dev/null
+++ b/openstack/compute/v2/servers/util.go
@@ -0,0 +1,20 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+// WaitForStatus will continually poll a server until it successfully transitions to a specified
+// status. It will do this for at most the number of seconds specified.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/compute/v2/servers/util_test.go b/openstack/compute/v2/servers/util_test.go
new file mode 100644
index 0000000..e192ae3
--- /dev/null
+++ b/openstack/compute/v2/servers/util_test.go
@@ -0,0 +1,38 @@
+package servers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestWaitForStatus(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/servers/4321", func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "server": {
+ "name": "the-server",
+ "id": "4321",
+ "status": "ACTIVE"
+ }
+ }`)
+ })
+
+ err := WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 0)
+ if err == nil {
+ t.Errorf("Expected error: 'Time Out in WaitFor'")
+ }
+
+ err = WaitForStatus(client.ServiceClient(), "4321", "ACTIVE", 3)
+ th.CheckNoErr(t, err)
+}
diff --git a/rackspace/client.go b/rackspace/client.go
index 9bfa4be..cf00dc7 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -114,6 +114,20 @@
}
}
+// NewComputeV2 creates a ServiceClient that may be used to access the v2 compute service.
+func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("compute")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+
+ return &gophercloud.ServiceClient{
+ Provider: client,
+ Endpoint: url,
+ }, nil
+}
+
// NewObjectCDNV1 creates a ServiceClient that may be used with the Rackspace v1 CDN.
func NewObjectCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:object-cdn")
diff --git a/rackspace/compute/v2/flavors/delegate.go b/rackspace/compute/v2/flavors/delegate.go
new file mode 100644
index 0000000..2cf31b5
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate.go
@@ -0,0 +1,46 @@
+package flavors
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts helps control the results returned by the List() function. For example, a flavor with a
+// minDisk field of 10 will not be returned if you specify MinDisk set to 20.
+type ListOpts struct {
+
+ // MinDisk and MinRAM, if provided, elide flavors that do not meet your criteria.
+ MinDisk int `q:"minDisk"`
+ MinRAM int `q:"minRam"`
+
+ // Marker specifies the ID of the last flavor in the previous page.
+ Marker string `q:"marker"`
+
+ // Limit instructs List to refrain from sending excessively large lists of flavors.
+ Limit int `q:"limit"`
+}
+
+// ToFlavorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFlavorListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List enumerates the server images available to your account.
+func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(client, opts)
+}
+
+// Get returns details about a single flavor, identity by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(client, id)
+}
+
+// ExtractFlavors interprets a page of List results as Flavors.
+func ExtractFlavors(page pagination.Page) ([]os.Flavor, error) {
+ return os.ExtractFlavors(page)
+}
diff --git a/rackspace/compute/v2/flavors/delegate_test.go b/rackspace/compute/v2/flavors/delegate_test.go
new file mode 100644
index 0000000..b2a2ea2
--- /dev/null
+++ b/rackspace/compute/v2/flavors/delegate_test.go
@@ -0,0 +1,62 @@
+package flavors
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListFlavors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/flavors/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, ListOutput)
+ case "performance1-2":
+ fmt.Fprintf(w, `{ "flavors": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ count := 0
+ err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := ExtractFlavors(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedFlavorSlice, actual)
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGetFlavor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/flavors/performance1-1", 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")
+ fmt.Fprintf(w, GetOutput)
+ })
+
+ actual, err := Get(client.ServiceClient(), "performance1-1").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &Performance1Flavor, actual)
+}
diff --git a/rackspace/compute/v2/flavors/fixtures.go b/rackspace/compute/v2/flavors/fixtures.go
new file mode 100644
index 0000000..b6dca93
--- /dev/null
+++ b/rackspace/compute/v2/flavors/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+package flavors
+
+import (
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+)
+
+// ListOutput is a sample response of a flavor List request.
+const ListOutput = `
+{
+ "flavors": [
+ {
+ "OS-FLV-EXT-DATA:ephemeral": 0,
+ "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+ "class": "performance1",
+ "disk_io_index": "40",
+ "number_of_data_disks": "0",
+ "policy_class": "performance_flavor",
+ "resize_policy_class": "performance_flavor"
+ },
+ "disk": 20,
+ "id": "performance1-1",
+ "links": [
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
+ "rel": "self"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "1 GB Performance",
+ "ram": 1024,
+ "rxtx_factor": 200,
+ "swap": "",
+ "vcpus": 1
+ },
+ {
+ "OS-FLV-EXT-DATA:ephemeral": 20,
+ "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+ "class": "performance1",
+ "disk_io_index": "40",
+ "number_of_data_disks": "1",
+ "policy_class": "performance_flavor",
+ "resize_policy_class": "performance_flavor"
+ },
+ "disk": 40,
+ "id": "performance1-2",
+ "links": [
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-2",
+ "rel": "self"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-2",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "2 GB Performance",
+ "ram": 2048,
+ "rxtx_factor": 400,
+ "swap": "",
+ "vcpus": 2
+ }
+ ]
+}`
+
+// GetOutput is a sample response from a flavor Get request. Its contents correspond to the
+// Performance1Flavor struct.
+const GetOutput = `
+{
+ "flavor": {
+ "OS-FLV-EXT-DATA:ephemeral": 0,
+ "OS-FLV-WITH-EXT-SPECS:extra_specs": {
+ "class": "performance1",
+ "disk_io_index": "40",
+ "number_of_data_disks": "0",
+ "policy_class": "performance_flavor",
+ "resize_policy_class": "performance_flavor"
+ },
+ "disk": 20,
+ "id": "performance1-1",
+ "links": [
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/v2/864477/flavors/performance1-1",
+ "rel": "self"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/864477/flavors/performance1-1",
+ "rel": "bookmark"
+ }
+ ],
+ "name": "1 GB Performance",
+ "ram": 1024,
+ "rxtx_factor": 200,
+ "swap": "",
+ "vcpus": 1
+ }
+}
+`
+
+// Performance1Flavor is the expected result of parsing GetOutput, or the first element of
+// ListOutput.
+var Performance1Flavor = os.Flavor{
+ ID: "performance1-1",
+ Disk: 20,
+ RAM: 1024,
+ Name: "1 GB Performance",
+ RxTxFactor: 200.0,
+ Swap: 0,
+ VCPUs: 1,
+}
+
+// Performance2Flavor is the second result expected from parsing ListOutput.
+var Performance2Flavor = os.Flavor{
+ ID: "performance1-2",
+ Disk: 40,
+ RAM: 2048,
+ Name: "2 GB Performance",
+ RxTxFactor: 400.0,
+ Swap: 0,
+ VCPUs: 2,
+}
+
+// ExpectedFlavorSlice is the slice of Flavor structs that are expected to be parsed from
+// ListOutput.
+var ExpectedFlavorSlice = []os.Flavor{Performance1Flavor, Performance2Flavor}
diff --git a/rackspace/compute/v2/images/delegate.go b/rackspace/compute/v2/images/delegate.go
new file mode 100644
index 0000000..18e1f31
--- /dev/null
+++ b/rackspace/compute/v2/images/delegate.go
@@ -0,0 +1,22 @@
+package images
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListDetail enumerates the available server images.
+func ListDetail(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.ListDetail(client, opts)
+}
+
+// Get acquires additional detail about a specific image by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(client, id)
+}
+
+// ExtractImages interprets a page as a collection of server images.
+func ExtractImages(page pagination.Page) ([]os.Image, error) {
+ return os.ExtractImages(page)
+}
diff --git a/rackspace/compute/v2/images/delegate_test.go b/rackspace/compute/v2/images/delegate_test.go
new file mode 100644
index 0000000..db0a6e3
--- /dev/null
+++ b/rackspace/compute/v2/images/delegate_test.go
@@ -0,0 +1,62 @@
+package images
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListImageDetails(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/images/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, ListOutput)
+ case "e19a734c-c7e6-443a-830c-242209c4d65d":
+ fmt.Fprintf(w, `{ "images": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ count := 0
+ err := ListDetail(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractImages(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedImageSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGetImageDetails(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/images/e19a734c-c7e6-443a-830c-242209c4d65d", 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")
+ fmt.Fprintf(w, GetOutput)
+ })
+
+ actual, err := Get(client.ServiceClient(), "e19a734c-c7e6-443a-830c-242209c4d65d").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &UbuntuImage, actual)
+}
diff --git a/rackspace/compute/v2/images/fixtures.go b/rackspace/compute/v2/images/fixtures.go
new file mode 100644
index 0000000..c46d196
--- /dev/null
+++ b/rackspace/compute/v2/images/fixtures.go
@@ -0,0 +1,199 @@
+// +build fixtures
+package images
+
+import (
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+)
+
+// ListOutput is an example response from an /images/detail request.
+const ListOutput = `
+{
+ "images": [
+ {
+ "OS-DCF:diskConfig": "MANUAL",
+ "OS-EXT-IMG-SIZE:size": 1.017415075e+09,
+ "created": "2014-10-01T15:49:02Z",
+ "id": "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+ "links": [
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+ "rel": "self"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+ "rel": "bookmark"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/111222/images/30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+ "rel": "alternate",
+ "type": "application/vnd.openstack.image"
+ }
+ ],
+ "metadata": {
+ "auto_disk_config": "disabled",
+ "cache_in_nova": "True",
+ "com.rackspace__1__build_core": "1",
+ "com.rackspace__1__build_managed": "1",
+ "com.rackspace__1__build_rackconnect": "1",
+ "com.rackspace__1__options": "0",
+ "com.rackspace__1__platform_target": "PublicCloud",
+ "com.rackspace__1__release_build_date": "2014-10-01_15-46-08",
+ "com.rackspace__1__release_id": "100",
+ "com.rackspace__1__release_version": "10",
+ "com.rackspace__1__source": "kickstart",
+ "com.rackspace__1__visible_core": "1",
+ "com.rackspace__1__visible_managed": "0",
+ "com.rackspace__1__visible_rackconnect": "0",
+ "image_type": "base",
+ "org.openstack__1__architecture": "x64",
+ "org.openstack__1__os_distro": "org.archlinux",
+ "org.openstack__1__os_version": "2014.8",
+ "os_distro": "arch",
+ "os_type": "linux",
+ "vm_mode": "hvm"
+ },
+ "minDisk": 20,
+ "minRam": 512,
+ "name": "Arch 2014.10 (PVHVM)",
+ "progress": 100,
+ "status": "ACTIVE",
+ "updated": "2014-10-01T19:37:58Z"
+ },
+ {
+ "OS-DCF:diskConfig": "AUTO",
+ "OS-EXT-IMG-SIZE:size": 1.060306463e+09,
+ "created": "2014-10-01T12:58:11Z",
+ "id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+ "links": [
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "self"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "bookmark"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "alternate",
+ "type": "application/vnd.openstack.image"
+ }
+ ],
+ "metadata": {
+ "auto_disk_config": "True",
+ "cache_in_nova": "True",
+ "com.rackspace__1__build_core": "1",
+ "com.rackspace__1__build_managed": "1",
+ "com.rackspace__1__build_rackconnect": "1",
+ "com.rackspace__1__options": "0",
+ "com.rackspace__1__platform_target": "PublicCloud",
+ "com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
+ "com.rackspace__1__release_id": "1007",
+ "com.rackspace__1__release_version": "6",
+ "com.rackspace__1__source": "kickstart",
+ "com.rackspace__1__visible_core": "1",
+ "com.rackspace__1__visible_managed": "1",
+ "com.rackspace__1__visible_rackconnect": "1",
+ "image_type": "base",
+ "org.openstack__1__architecture": "x64",
+ "org.openstack__1__os_distro": "com.ubuntu",
+ "org.openstack__1__os_version": "14.04",
+ "os_distro": "ubuntu",
+ "os_type": "linux",
+ "vm_mode": "xen"
+ },
+ "minDisk": 20,
+ "minRam": 512,
+ "name": "Ubuntu 14.04 LTS (Trusty Tahr)",
+ "progress": 100,
+ "status": "ACTIVE",
+ "updated": "2014-10-01T15:51:44Z"
+ }
+ ]
+}
+`
+
+// GetOutput is an example response from an /images request.
+const GetOutput = `
+{
+ "image": {
+ "OS-DCF:diskConfig": "AUTO",
+ "OS-EXT-IMG-SIZE:size": 1060306463,
+ "created": "2014-10-01T12:58:11Z",
+ "id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+ "links": [
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/v2/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "self"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "bookmark"
+ },
+ {
+ "href": "https://iad.servers.api.rackspacecloud.com/111222/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "alternate",
+ "type": "application/vnd.openstack.image"
+ }
+ ],
+ "metadata": {
+ "auto_disk_config": "True",
+ "cache_in_nova": "True",
+ "com.rackspace__1__build_core": "1",
+ "com.rackspace__1__build_managed": "1",
+ "com.rackspace__1__build_rackconnect": "1",
+ "com.rackspace__1__options": "0",
+ "com.rackspace__1__platform_target": "PublicCloud",
+ "com.rackspace__1__release_build_date": "2014-10-01_12-31-03",
+ "com.rackspace__1__release_id": "1007",
+ "com.rackspace__1__release_version": "6",
+ "com.rackspace__1__source": "kickstart",
+ "com.rackspace__1__visible_core": "1",
+ "com.rackspace__1__visible_managed": "1",
+ "com.rackspace__1__visible_rackconnect": "1",
+ "image_type": "base",
+ "org.openstack__1__architecture": "x64",
+ "org.openstack__1__os_distro": "com.ubuntu",
+ "org.openstack__1__os_version": "14.04",
+ "os_distro": "ubuntu",
+ "os_type": "linux",
+ "vm_mode": "xen"
+ },
+ "minDisk": 20,
+ "minRam": 512,
+ "name": "Ubuntu 14.04 LTS (Trusty Tahr)",
+ "progress": 100,
+ "status": "ACTIVE",
+ "updated": "2014-10-01T15:51:44Z"
+ }
+}
+`
+
+// ArchImage is the first Image structure that should be parsed from ListOutput.
+var ArchImage = os.Image{
+ ID: "30aa010e-080e-4d4b-a7f9-09fc55b07d69",
+ Name: "Arch 2014.10 (PVHVM)",
+ Created: "2014-10-01T15:49:02Z",
+ Updated: "2014-10-01T19:37:58Z",
+ MinDisk: 20,
+ MinRAM: 512,
+ Progress: 100,
+ Status: "ACTIVE",
+}
+
+// UbuntuImage is the second Image structure that should be parsed from ListOutput and
+// the only image that should be extracted from GetOutput.
+var UbuntuImage = os.Image{
+ ID: "e19a734c-c7e6-443a-830c-242209c4d65d",
+ Name: "Ubuntu 14.04 LTS (Trusty Tahr)",
+ Created: "2014-10-01T12:58:11Z",
+ Updated: "2014-10-01T15:51:44Z",
+ MinDisk: 20,
+ MinRAM: 512,
+ Progress: 100,
+ Status: "ACTIVE",
+}
+
+// ExpectedImageSlice is the collection of images that should be parsed from ListOutput,
+// in order.
+var ExpectedImageSlice = []os.Image{ArchImage, UbuntuImage}
diff --git a/rackspace/compute/v2/servers/delegate.go b/rackspace/compute/v2/servers/delegate.go
new file mode 100644
index 0000000..cbf5384
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate.go
@@ -0,0 +1,61 @@
+package servers
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// List makes a request against the API to list servers accessible to you.
+func List(client *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(client, opts)
+}
+
+// Create requests a server to be provisioned to the user in the current tenant.
+func Create(client *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(client, opts)
+}
+
+// Delete requests that a server previously provisioned be removed from your account.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ return os.Delete(client, id)
+}
+
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) os.GetResult {
+ return os.Get(client, id)
+}
+
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) os.ActionResult {
+ return os.ChangeAdminPassword(client, id, newPassword)
+}
+
+// Reboot requests that a given server reboot. Two methods exist for rebooting a server:
+//
+// os.HardReboot (aka PowerCycle) restarts the server instance by physically cutting power to the
+// machine, or if a VM, terminating it at the hypervisor level. It's done. Caput. Full stop. Then,
+// after a brief wait, power is restored or the VM instance restarted.
+//
+// os.SoftReboot (aka OSReboot) simply tells the OS to restart under its own procedures. E.g., in
+// Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to restart the machine.
+func Reboot(client *gophercloud.ServiceClient, id string, how os.RebootMethod) os.ActionResult {
+ return os.Reboot(client, id, how)
+}
+
+// Rebuild will reprovision the server according to the configuration options provided in the
+// RebuildOpts struct.
+func Rebuild(client *gophercloud.ServiceClient, id string, opts os.RebuildOptsBuilder) os.RebuildResult {
+ return os.Rebuild(client, id, opts)
+}
+
+// WaitForStatus will continually poll a server until it successfully transitions to a specified
+// status. It will do this for at most the number of seconds specified.
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return os.WaitForStatus(c, id, status, secs)
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]os.Server, error) {
+ return os.ExtractServers(page)
+}
diff --git a/rackspace/compute/v2/servers/delegate_test.go b/rackspace/compute/v2/servers/delegate_test.go
new file mode 100644
index 0000000..0c331eb
--- /dev/null
+++ b/rackspace/compute/v2/servers/delegate_test.go
@@ -0,0 +1,112 @@
+package servers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ os "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 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")
+ fmt.Fprintf(w, ListOutput)
+ })
+
+ count := 0
+ err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractServers(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedServerSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestCreateServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleServerCreationSuccessfully(t, CreateOutput)
+
+ actual, err := Create(client.ServiceClient(), os.CreateOpts{
+ Name: "derp",
+ ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ FlavorRef: "1",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, &CreatedServer, actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleServerDeletionSuccessfully(t)
+
+ err := Delete(client.ServiceClient(), "asdfasdfasdf")
+ th.AssertNoErr(t, err)
+}
+
+func TestGetServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee", 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")
+ fmt.Fprintf(w, GetOutput)
+ })
+
+ actual, err := Get(client.ServiceClient(), "8c65cb68-0681-4c30-bc88-6b83a8a26aee").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &GophercloudServer, actual)
+}
+
+func TestChangeAdminPassword(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleAdminPasswordChangeSuccessfully(t)
+
+ res := ChangeAdminPassword(client.ServiceClient(), "1234asdf", "new-password")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestReboot(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleRebootSuccessfully(t)
+
+ res := Reboot(client.ServiceClient(), "1234asdf", os.SoftReboot)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestRebuildServer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleRebuildSuccessfully(t, GetOutput)
+
+ opts := os.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",
+ }
+ actual, err := Rebuild(client.ServiceClient(), "1234asdf", opts).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &GophercloudServer, actual)
+}
diff --git a/rackspace/compute/v2/servers/fixtures.go b/rackspace/compute/v2/servers/fixtures.go
new file mode 100644
index 0000000..b22a289
--- /dev/null
+++ b/rackspace/compute/v2/servers/fixtures.go
@@ -0,0 +1,439 @@
+// +build fixtures
+
+package servers
+
+import (
+ os "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+)
+
+// ListOutput is the recorded output of a Rackspace servers.List request.
+const ListOutput = `
+{
+ "servers": [
+ {
+ "OS-DCF:diskConfig": "MANUAL",
+ "OS-EXT-STS:power_state": 1,
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "accessIPv4": "1.2.3.4",
+ "accessIPv6": "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+ "addresses": {
+ "private": [
+ {
+ "addr": "10.208.230.113",
+ "version": 4
+ }
+ ],
+ "public": [
+ {
+ "addr": "2001:4800:7818:101:2000:9b5e:7428:a2d0",
+ "version": 6
+ },
+ {
+ "addr": "104.130.131.164",
+ "version": 4
+ }
+ ]
+ },
+ "created": "2014-09-23T12:34:58Z",
+ "flavor": {
+ "id": "performance1-8",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "hostId": "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
+ "id": "59818cee-bc8c-44eb-8073-673ee65105f7",
+ "image": {
+ "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "key_name": "mykey",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+ "rel": "self"
+ },
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+ "rel": "bookmark"
+ }
+ ],
+ "metadata": {},
+ "name": "devstack",
+ "progress": 100,
+ "status": "ACTIVE",
+ "tenant_id": "111111",
+ "updated": "2014-09-23T12:38:19Z",
+ "user_id": "14ae7bb21d81422694655f3cc30f2930"
+ },
+ {
+ "OS-DCF:diskConfig": "MANUAL",
+ "OS-EXT-STS:power_state": 1,
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "accessIPv4": "1.1.2.3",
+ "accessIPv6": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+ "addresses": {
+ "private": [
+ {
+ "addr": "10.10.20.30",
+ "version": 4
+ }
+ ],
+ "public": [
+ {
+ "addr": "1.1.2.3",
+ "version": 4
+ },
+ {
+ "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+ "version": 6
+ }
+ ]
+ },
+ "created": "2014-07-21T19:32:55Z",
+ "flavor": {
+ "id": "performance1-2",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "hostId": "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
+ "id": "25f1c7f5-e00a-4715-b354-16e24b2f4630",
+ "image": {
+ "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "key_name": "otherkey",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+ "rel": "self"
+ },
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+ "rel": "bookmark"
+ }
+ ],
+ "metadata": {},
+ "name": "peril-dfw",
+ "progress": 100,
+ "status": "ACTIVE",
+ "tenant_id": "111111",
+ "updated": "2014-07-21T19:34:24Z",
+ "user_id": "14ae7bb21d81422694655f3cc30f2930"
+ }
+ ]
+}
+`
+
+// GetOutput is the recorded output of a Rackspace servers.Get request.
+const GetOutput = `
+{
+ "server": {
+ "OS-DCF:diskConfig": "AUTO",
+ "OS-EXT-STS:power_state": 1,
+ "OS-EXT-STS:task_state": null,
+ "OS-EXT-STS:vm_state": "active",
+ "accessIPv4": "1.2.4.8",
+ "accessIPv6": "2001:4800:6666:105:2a0f:c056:f594:7777",
+ "addresses": {
+ "private": [
+ {
+ "addr": "10.20.40.80",
+ "version": 4
+ }
+ ],
+ "public": [
+ {
+ "addr": "1.2.4.8",
+ "version": 4
+ },
+ {
+ "addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
+ "version": 6
+ }
+ ]
+ },
+ "created": "2014-10-21T14:42:16Z",
+ "flavor": {
+ "id": "performance1-1",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "hostId": "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
+ "id": "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+ "image": {
+ "id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "bookmark"
+ }
+ ]
+ },
+ "key_name": null,
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+ "rel": "self"
+ },
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+ "rel": "bookmark"
+ }
+ ],
+ "metadata": {},
+ "name": "Gophercloud-pxpGGuey",
+ "progress": 100,
+ "status": "ACTIVE",
+ "tenant_id": "111111",
+ "updated": "2014-10-21T14:42:57Z",
+ "user_id": "14ae7bb21d81423694655f4dd30f2930"
+ }
+}
+`
+
+// CreateOutput contains a sample of Rackspace's response to a Create call.
+const CreateOutput = `
+{
+ "server": {
+ "OS-DCF:diskConfig": "AUTO",
+ "adminPass": "v7tADqbE5pr9",
+ "id": "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+ "links": [
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+ "rel": "self"
+ },
+ {
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+ "rel": "bookmark"
+ }
+ ]
+ }
+}
+`
+
+// DevstackServer is the expected first result from parsing ListOutput.
+var DevstackServer = os.Server{
+ ID: "59818cee-bc8c-44eb-8073-673ee65105f7",
+ Name: "devstack",
+ TenantID: "111111",
+ UserID: "14ae7bb21d81422694655f3cc30f2930",
+ HostID: "e8951a524bc465b0898aeac7674da6fe1495e253ae1ea17ddb2c2475",
+ Updated: "2014-09-23T12:38:19Z",
+ Created: "2014-09-23T12:34:58Z",
+ AccessIPv4: "1.2.3.4",
+ AccessIPv6: "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+ Progress: 100,
+ Status: "ACTIVE",
+ Image: map[string]interface{}{
+ "id": "255df5fb-e3d4-45a3-9a07-c976debf7c14",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/255df5fb-e3d4-45a3-9a07-c976debf7c14",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "performance1-8",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-8",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "addr": "10.20.30.40",
+ "version": float64(4.0),
+ },
+ },
+ "public": []interface{}{
+ map[string]interface{}{
+ "addr": "1111:4822:7818:121:2000:9b5e:7438:a2d0",
+ "version": float64(6.0),
+ },
+ map[string]interface{}{
+ "addr": "1.2.3.4",
+ "version": float64(4.0),
+ },
+ },
+ },
+ Metadata: map[string]interface{}{},
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59918cee-bd9d-44eb-8173-673ee75105f7",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/59818cee-bc8c-44eb-8073-673ee65105f7",
+ "rel": "bookmark",
+ },
+ },
+ KeyName: "mykey",
+ AdminPass: "",
+}
+
+// PerilServer is the expected second result from parsing ListOutput.
+var PerilServer = os.Server{
+ ID: "25f1c7f5-e00a-4715-b354-16e24b2f4630",
+ Name: "peril-dfw",
+ TenantID: "111111",
+ UserID: "14ae7bb21d81422694655f3cc30f2930",
+ HostID: "f859679906d6b1a38c1bd516b78f4dcc7d5fcf012578fa3ce460716c",
+ Updated: "2014-07-21T19:34:24Z",
+ Created: "2014-07-21T19:32:55Z",
+ AccessIPv4: "1.1.2.3",
+ AccessIPv6: "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+ Progress: 100,
+ Status: "ACTIVE",
+ Image: map[string]interface{}{
+ "id": "bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/bb02b1a3-bc77-4d17-ab5b-421d89850fca",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "performance1-2",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-2",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "addr": "10.10.20.30",
+ "version": float64(4.0),
+ },
+ },
+ "public": []interface{}{
+ map[string]interface{}{
+ "addr": "2222:4444:7817:101:be76:4eff:f0e5:9e02",
+ "version": float64(6.0),
+ },
+ map[string]interface{}{
+ "addr": "1.1.2.3",
+ "version": float64(4.0),
+ },
+ },
+ },
+ Metadata: map[string]interface{}{},
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/25f1c7f5-e00a-4715-b355-16e24b2f4630",
+ "rel": "bookmark",
+ },
+ },
+ KeyName: "otherkey",
+ AdminPass: "",
+}
+
+// GophercloudServer is the expected result from parsing GetOutput.
+var GophercloudServer = os.Server{
+ ID: "8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+ Name: "Gophercloud-pxpGGuey",
+ TenantID: "111111",
+ UserID: "14ae7bb21d81423694655f4dd30f2930",
+ HostID: "430d2ae02de0a7af77012c94778145eccf67e75b1fac0528aa10d4a7",
+ Updated: "2014-10-21T14:42:57Z",
+ Created: "2014-10-21T14:42:16Z",
+ AccessIPv4: "1.2.4.8",
+ AccessIPv6: "2001:4800:6666:105:2a0f:c056:f594:7777",
+ Progress: 100,
+ Status: "ACTIVE",
+ Image: map[string]interface{}{
+ "id": "e19a734c-c7e6-443a-830c-242209c4d65d",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/images/e19a734c-c7e6-443a-830c-242209c4d65d",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Flavor: map[string]interface{}{
+ "id": "performance1-1",
+ "links": []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/flavors/performance1-1",
+ "rel": "bookmark",
+ },
+ },
+ },
+ Addresses: map[string]interface{}{
+ "private": []interface{}{
+ map[string]interface{}{
+ "addr": "10.20.40.80",
+ "version": float64(4.0),
+ },
+ },
+ "public": []interface{}{
+ map[string]interface{}{
+ "addr": "2001:4800:6666:105:2a0f:c056:f594:7777",
+ "version": float64(6.0),
+ },
+ map[string]interface{}{
+ "addr": "1.2.4.8",
+ "version": float64(4.0),
+ },
+ },
+ },
+ Metadata: map[string]interface{}{},
+ Links: []interface{}{
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/v2/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+ "rel": "self",
+ },
+ map[string]interface{}{
+ "href": "https://dfw.servers.api.rackspacecloud.com/111111/servers/8c65cb68-0681-4c30-bc88-6b83a8a26aee",
+ "rel": "bookmark",
+ },
+ },
+ KeyName: "",
+ AdminPass: "",
+}
+
+// CreatedServer is the partial Server struct that can be parsed from CreateOutput.
+var CreatedServer = os.Server{
+ ID: "bb63327b-6a2f-34bc-b0ef-4b6d97ea637e",
+ AdminPass: "v7tADqbE5pr9",
+ Links: []interface{}{},
+}
+
+// ExpectedServerSlice is the collection of servers, in order, that should be parsed from ListOutput.
+var ExpectedServerSlice = []os.Server{DevstackServer, PerilServer}
diff --git a/results.go b/results.go
index f60bc76..19557fb 100644
--- a/results.go
+++ b/results.go
@@ -1,6 +1,9 @@
package gophercloud
-import "net/http"
+import (
+ "encoding/json"
+ "net/http"
+)
// Result acts as a base struct that other results can embed.
type Result struct {
@@ -16,6 +19,15 @@
Err error
}
+// PrettyPrintJSON creates a string containing the full response body as pretty-printed JSON.
+func (r Result) PrettyPrintJSON() string {
+ pretty, err := json.MarshalIndent(r.Body, "", " ")
+ if err != nil {
+ panic(err.Error())
+ }
+ return string(pretty)
+}
+
// RFC3339Milli describes a time format used by API responses.
const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
diff --git a/testhelper/convenience.go b/testhelper/convenience.go
index f6cb371..ca27cad 100644
--- a/testhelper/convenience.go
+++ b/testhelper/convenience.go
@@ -5,11 +5,12 @@
"path/filepath"
"reflect"
"runtime"
+ "strings"
"testing"
)
-func prefix() string {
- _, file, line, _ := runtime.Caller(3)
+func prefix(depth int) string {
+ _, file, line, _ := runtime.Caller(depth)
return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
}
@@ -22,11 +23,182 @@
}
func logFatal(t *testing.T, str string) {
- t.Fatalf("\033[1;31m%s %s\033[0m", prefix(), str)
+ t.Fatalf("\033[1;31m%s %s\033[0m", prefix(3), str)
}
func logError(t *testing.T, str string) {
- t.Errorf("\033[1;31m%s %s\033[0m", prefix(), str)
+ t.Errorf("\033[1;31m%s %s\033[0m", prefix(3), str)
+}
+
+type diffLogger func([]string, interface{}, interface{})
+
+type visit struct {
+ a1 uintptr
+ a2 uintptr
+ typ reflect.Type
+}
+
+// Recursively visits the structures of "expected" and "actual". The diffLogger function will be
+// invoked with each different value encountered, including the reference path that was followed
+// to get there.
+func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) {
+ defer func() {
+ // Fall back to the regular reflect.DeepEquals function.
+ if r := recover(); r != nil {
+ var e, a interface{}
+ if expected.IsValid() {
+ e = expected.Interface()
+ }
+ if actual.IsValid() {
+ a = actual.Interface()
+ }
+
+ if !reflect.DeepEqual(e, a) {
+ logDifference(path, e, a)
+ }
+ }
+ }()
+
+ if !expected.IsValid() && actual.IsValid() {
+ logDifference(path, nil, actual.Interface())
+ return
+ }
+ if expected.IsValid() && !actual.IsValid() {
+ logDifference(path, expected.Interface(), nil)
+ return
+ }
+ if !expected.IsValid() && !actual.IsValid() {
+ return
+ }
+
+ hard := func(k reflect.Kind) bool {
+ switch k {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
+ return true
+ }
+ return false
+ }
+
+ if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) {
+ addr1 := expected.UnsafeAddr()
+ addr2 := actual.UnsafeAddr()
+
+ if addr1 > addr2 {
+ addr1, addr2 = addr2, addr1
+ }
+
+ if addr1 == addr2 {
+ // References are identical. We can short-circuit
+ return
+ }
+
+ typ := expected.Type()
+ v := visit{addr1, addr2, typ}
+ if visited[v] {
+ // Already visited.
+ return
+ }
+
+ // Remember this visit for later.
+ visited[v] = true
+ }
+
+ switch expected.Kind() {
+ case reflect.Array:
+ for i := 0; i < expected.Len(); i++ {
+ hop := append(path, fmt.Sprintf("[%d]", i))
+ deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
+ }
+ return
+ case reflect.Slice:
+ if expected.IsNil() != actual.IsNil() {
+ logDifference(path, expected.Interface(), actual.Interface())
+ return
+ }
+ if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
+ return
+ }
+ for i := 0; i < expected.Len(); i++ {
+ hop := append(path, fmt.Sprintf("[%d]", i))
+ deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
+ }
+ return
+ case reflect.Interface:
+ if expected.IsNil() != actual.IsNil() {
+ logDifference(path, expected.Interface(), actual.Interface())
+ return
+ }
+ deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
+ return
+ case reflect.Ptr:
+ deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
+ return
+ case reflect.Struct:
+ for i, n := 0, expected.NumField(); i < n; i++ {
+ field := expected.Type().Field(i)
+ hop := append(path, "."+field.Name)
+ deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference)
+ }
+ return
+ case reflect.Map:
+ if expected.IsNil() != actual.IsNil() {
+ logDifference(path, expected.Interface(), actual.Interface())
+ return
+ }
+ if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
+ return
+ }
+
+ var keys []reflect.Value
+ if expected.Len() >= actual.Len() {
+ keys = expected.MapKeys()
+ } else {
+ keys = actual.MapKeys()
+ }
+
+ for _, k := range keys {
+ expectedValue := expected.MapIndex(k)
+ actualValue := expected.MapIndex(k)
+
+ if !expectedValue.IsValid() {
+ logDifference(path, nil, actual.Interface())
+ return
+ }
+ if !actualValue.IsValid() {
+ logDifference(path, expected.Interface(), nil)
+ return
+ }
+
+ hop := append(path, fmt.Sprintf("[%v]", k))
+ deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference)
+ }
+ return
+ case reflect.Func:
+ if expected.IsNil() != actual.IsNil() {
+ logDifference(path, expected.Interface(), actual.Interface())
+ }
+ return
+ default:
+ if expected.Interface() != actual.Interface() {
+ logDifference(path, expected.Interface(), actual.Interface())
+ }
+ }
+}
+
+func deepDiff(expected, actual interface{}, logDifference diffLogger) {
+ if expected == nil || actual == nil {
+ logDifference([]string{}, expected, actual)
+ return
+ }
+
+ expectedValue := reflect.ValueOf(expected)
+ actualValue := reflect.ValueOf(actual)
+
+ if expectedValue.Type() != actualValue.Type() {
+ logDifference([]string{}, expected, actual)
+ return
+ }
+ deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference)
}
// AssertEquals compares two arbitrary values and performs a comparison. If the
@@ -47,16 +219,33 @@
// AssertDeepEquals - like Equals - performs a comparison - but on more complex
// structures that requires deeper inspection
func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
- if !reflect.DeepEqual(expected, actual) {
- logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
+ pre := prefix(2)
+
+ differed := false
+ deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
+ differed = true
+ t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m",
+ pre,
+ strings.Join(path, ""),
+ green(expected),
+ yellow(actual))
+ })
+ if differed {
+ logFatal(t, "The structures were different.")
}
}
// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error
func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
- if !reflect.DeepEqual(expected, actual) {
- logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
- }
+ pre := prefix(2)
+
+ deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
+ t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m",
+ pre,
+ strings.Join(path, ""),
+ green(expected),
+ yellow(actual))
+ })
}
// AssertNoErr is a convenience function for checking whether an error value is