Merge pull request #205 from smashwilson/paginate-compute
Update compute service
diff --git a/acceptance/openstack/compute/compute_test.go b/acceptance/openstack/compute/compute_test.go
deleted file mode 100644
index 2310858..0000000
--- a/acceptance/openstack/compute/compute_test.go
+++ /dev/null
@@ -1,370 +0,0 @@
-// +build acceptance
-
-package compute
-
-import (
- "fmt"
- "os"
- "testing"
-
- "github.com/rackspace/gophercloud/acceptance/tools"
- "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
- "github.com/rackspace/gophercloud/openstack/compute/v2/images"
- "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
-)
-
-var service = "compute"
-
-func TestListServers(t *testing.T) {
- ts, err := tools.SetupForList(service)
- if err != nil {
- t.Error(err)
- return
- }
-
- fmt.Fprintln(ts.W, "ID\tRegion\tName\tStatus\tIPv4\tIPv6\t")
-
- region := os.Getenv("OS_REGION_NAME")
- n := 0
- for _, ep := range ts.EPs {
- if (region != "") && (region != ep.Region) {
- continue
- }
-
- client := servers.NewClient(ep.PublicURL, ts.A, ts.O)
-
- listResults, err := servers.List(client)
- if err != nil {
- t.Error(err)
- return
- }
-
- svrs, err := servers.GetServers(listResults)
- if err != nil {
- t.Error(err)
- return
- }
-
- n = n + len(svrs)
-
- for _, s := range svrs {
- fmt.Fprintf(ts.W, "%s\t%s\t%s\t%s\t%s\t%s\t\n", s.Id, s.Name, ep.Region, s.Status, s.AccessIPv4, s.AccessIPv6)
- }
- }
- ts.W.Flush()
- fmt.Printf("--------\n%d servers listed.\n", n)
-}
-
-func TestListImages(t *testing.T) {
- ts, err := tools.SetupForList(service)
- if err != nil {
- t.Error(err)
- return
- }
-
- fmt.Fprintln(ts.W, "ID\tRegion\tName\tStatus\tCreated\t")
-
- region := os.Getenv("OS_REGION_NAME")
- n := 0
- for _, ep := range ts.EPs {
- if (region != "") && (region != ep.Region) {
- continue
- }
-
- client := images.NewClient(ep.PublicURL, ts.A, ts.O)
-
- listResults, err := images.List(client)
- if err != nil {
- t.Error(err)
- return
- }
-
- imgs, err := images.GetImages(listResults)
- if err != nil {
- t.Error(err)
- return
- }
-
- n = n + len(imgs)
-
- for _, i := range imgs {
- fmt.Fprintf(ts.W, "%s\t%s\t%s\t%s\t%s\t\n", i.Id, ep.Region, i.Name, i.Status, i.Created)
- }
- }
- ts.W.Flush()
- fmt.Printf("--------\n%d images listed.\n", n)
-}
-
-func TestListFlavors(t *testing.T) {
- ts, err := tools.SetupForList(service)
- if err != nil {
- t.Error(err)
- return
- }
-
- fmt.Fprintln(ts.W, "ID\tRegion\tName\tRAM\tDisk\tVCPUs\t")
-
- region := os.Getenv("OS_REGION_NAME")
- n := 0
- for _, ep := range ts.EPs {
- if (region != "") && (region != ep.Region) {
- continue
- }
-
- client := flavors.NewClient(ep.PublicURL, ts.A, ts.O)
-
- listResults, err := flavors.List(client, flavors.ListFilterOptions{})
- if err != nil {
- t.Error(err)
- return
- }
-
- flavs, err := flavors.GetFlavors(listResults)
- if err != nil {
- t.Error(err)
- return
- }
-
- n = n + len(flavs)
-
- for _, f := range flavs {
- fmt.Fprintf(ts.W, "%s\t%s\t%s\t%d\t%d\t%d\t\n", f.Id, ep.Region, f.Name, f.Ram, f.Disk, f.VCpus)
- }
- }
- ts.W.Flush()
- fmt.Printf("--------\n%d flavors listed.\n", n)
-}
-
-func TestGetFlavor(t *testing.T) {
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Fatal(err)
- }
-
- region := os.Getenv("OS_REGION_NAME")
- for _, ep := range ts.EPs {
- if (region != "") && (region != ep.Region) {
- continue
- }
- client := flavors.NewClient(ep.PublicURL, ts.A, ts.O)
-
- getResults, err := flavors.Get(client, ts.FlavorId)
- if err != nil {
- t.Fatal(err)
- }
- flav, err := flavors.GetFlavor(getResults)
- if err != nil {
- t.Fatal(err)
- }
- fmt.Printf("%#v\n", flav)
- }
-}
-
-func TestCreateDestroyServer(t *testing.T) {
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Error(err)
- return
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Error(err)
- return
- }
-
- // We put this in a defer so that it gets executed even in the face of errors or panics.
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Error(err)
- }
-}
-
-func TestUpdateServer(t *testing.T) {
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Error(err)
- return
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Error(err)
- return
- }
-
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Error(err)
- return
- }
-
- err = tools.ChangeServerName(ts)
- if err != nil {
- t.Error(err)
- return
- }
-}
-
-func TestActionChangeAdminPassword(t *testing.T) {
- t.Parallel()
-
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.ChangeAdminPassword(ts)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestActionReboot(t *testing.T) {
- t.Parallel()
-
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
-
- err = servers.Reboot(ts.Client, ts.CreatedServer.Id, "aldhjflaskhjf")
- if err == nil {
- t.Fatal("Expected the SDK to provide an ArgumentError here")
- }
-
- err = tools.RebootServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestActionRebuild(t *testing.T) {
- t.Parallel()
-
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.RebuildServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestActionResizeConfirm(t *testing.T) {
- t.Parallel()
-
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.ResizeServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.ConfirmResize(ts)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestActionResizeRevert(t *testing.T) {
- t.Parallel()
-
- ts, err := tools.SetupForCRUD()
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.CreateServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- defer func() {
- servers.Delete(ts.Client, ts.CreatedServer.Id)
- }()
-
- err = tools.WaitForStatus(ts, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.ResizeServer(ts)
- if err != nil {
- t.Fatal(err)
- }
-
- err = tools.RevertResize(ts)
- if err != nil {
- t.Fatal(err)
- }
-}
diff --git a/acceptance/openstack/compute/pkg.go b/acceptance/openstack/compute/pkg.go
deleted file mode 100644
index a887fe9..0000000
--- a/acceptance/openstack/compute/pkg.go
+++ /dev/null
@@ -1 +0,0 @@
-package compute
diff --git a/acceptance/openstack/compute/tools_test.go b/acceptance/openstack/compute/tools_test.go
deleted file mode 100644
index 1acb824..0000000
--- a/acceptance/openstack/compute/tools_test.go
+++ /dev/null
@@ -1,367 +0,0 @@
-// +build acceptance
-
-package compute
-
-import (
- "crypto/rand"
- "fmt"
- "os"
- "text/tabwriter"
- "time"
-
- "github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
- "github.com/rackspace/gophercloud/openstack/utils"
-)
-
-var errTimeout = fmt.Errorf("Timeout.")
-
-type testState struct {
- o gophercloud.AuthOptions
- a identity.AuthResults
- sc *identity.ServiceCatalog
- eps []identity.Endpoint
- w *tabwriter.Writer
- imageId string
- flavorId string
- region string
- ep string
- client *servers.Client
- createdServer *servers.Server
- gottenServer *servers.Server
- updatedServer *servers.Server
- serverName string
- alternateName string
- flavorIdResize string
-}
-
-func SetupForList(service string) (*testState, error) {
- var err error
-
- ts := new(testState)
-
- ts.o, err = utils.AuthOptions()
- if err != nil {
- return ts, err
- }
-
- client := &gophercloud.ServiceClient{Endpoint: ts.o.IdentityEndpoint}
- ts.a, err = identity.Authenticate(client, ts.o)
- if err != nil {
- return ts, err
- }
-
- ts.sc, err = identity.GetServiceCatalog(ts.a)
- if err != nil {
- return ts, err
- }
-
- ts.eps, err = FindAllEndpoints(ts.sc, service)
- if err != nil {
- return ts, err
- }
-
- ts.w = new(tabwriter.Writer)
- ts.w.Init(os.Stdout, 2, 8, 2, ' ', 0)
-
- return ts, nil
-}
-
-func SetupForCRUD() (*testState, error) {
- ts, err := SetupForList("compute")
- if err != nil {
- return ts, err
- }
-
- ts.imageId = os.Getenv("OS_IMAGE_ID")
- if ts.imageId == "" {
- return ts, fmt.Errorf("Expected OS_IMAGE_ID environment variable to be set")
- }
-
- ts.flavorId = os.Getenv("OS_FLAVOR_ID")
- if ts.flavorId == "" {
- return ts, fmt.Errorf("Expected OS_FLAVOR_ID environment variable to be set")
- }
-
- ts.flavorIdResize = os.Getenv("OS_FLAVOR_ID_RESIZE")
- if ts.flavorIdResize == "" {
- return ts, fmt.Errorf("Expected OS_FLAVOR_ID_RESIZE environment variable to be set")
- }
-
- if ts.flavorIdResize == ts.flavorId {
- return ts, fmt.Errorf("OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE cannot be the same")
- }
-
- ts.region = os.Getenv("OS_REGION_NAME")
- if ts.region == "" {
- ts.region = ts.eps[0].Region
- }
-
- ts.ep, err = FindEndpointForRegion(ts.eps, ts.region)
- if err != nil {
- return ts, err
- }
-
- return ts, err
-}
-
-func FindAllEndpoints(sc *identity.ServiceCatalog, service string) ([]identity.Endpoint, error) {
- ces, err := sc.CatalogEntries()
- if err != nil {
- return nil, err
- }
-
- for _, ce := range ces {
- if ce.Type == service {
- return ce.Endpoints, nil
- }
- }
-
- return nil, fmt.Errorf(service + " endpoint not found.")
-}
-
-func FindEndpointForRegion(eps []identity.Endpoint, r string) (string, error) {
- for _, ep := range eps {
- if ep.Region == r {
- return ep.PublicURL, nil
- }
- }
- return "", fmt.Errorf("Unknown region %s", r)
-}
-
-func CountDown(ts *testState, timeout int) (bool, int, error) {
- if timeout < 1 {
- return false, 0, errTimeout
- }
- time.Sleep(1 * time.Second)
- timeout--
-
- gr, err := servers.GetDetail(ts.client, ts.createdServer.Id)
- if err != nil {
- return false, timeout, err
- }
-
- ts.gottenServer, err = servers.GetServer(gr)
- if err != nil {
- return false, timeout, err
- }
-
- return true, timeout, nil
-}
-
-func CreateServer(ts *testState) error {
- ts.serverName = RandomString("ACPTTEST", 16)
- fmt.Printf("Attempting to create server: %s\n", ts.serverName)
-
- ts.client = servers.NewClient(ts.ep, ts.a, ts.o)
-
- cr, err := servers.Create(ts.client, map[string]interface{}{
- "flavorRef": ts.flavorId,
- "imageRef": ts.imageId,
- "name": ts.serverName,
- })
- if err != nil {
- return err
- }
-
- ts.createdServer, err = servers.GetServer(cr)
- return err
-}
-
-func WaitForStatus(ts *testState, s string) error {
- var (
- inProgress bool
- timeout int
- err error
- )
-
- for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
- if ts.gottenServer.Id != ts.createdServer.Id {
- return fmt.Errorf("created server id (%s) != gotten server id (%s)", ts.createdServer.Id, ts.gottenServer.Id)
- }
-
- if ts.gottenServer.Status == s {
- fmt.Printf("Server reached state %s after %d seconds (approximately)\n", s, 300-timeout)
- break
- }
- }
-
- if err == errTimeout {
- fmt.Printf("Time out -- I'm not waiting around.\n")
- err = nil
- }
-
- return err
-}
-
-func ChangeServerName(ts *testState) error {
- var (
- inProgress bool
- timeout int
- )
-
- ts.alternateName = RandomString("ACPTTEST", 16)
- for ts.alternateName == ts.serverName {
- ts.alternateName = RandomString("ACPTTEST", 16)
- }
- fmt.Println("Attempting to change server name")
-
- ur, err := servers.Update(ts.client, ts.createdServer.Id, map[string]interface{}{
- "name": ts.alternateName,
- })
- if err != nil {
- return err
- }
-
- ts.updatedServer, err = servers.GetServer(ur)
- if err != nil {
- return err
- }
-
- if ts.updatedServer.Id != ts.createdServer.Id {
- return fmt.Errorf("Expected updated and created server to share the same ID")
- }
-
- for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
- if ts.gottenServer.Id != ts.updatedServer.Id {
- return fmt.Errorf("Updated server ID (%s) != gotten server ID (%s)", ts.updatedServer.Id, ts.gottenServer.Id)
- }
-
- if ts.gottenServer.Name == ts.alternateName {
- fmt.Printf("Server updated after %d seconds (approximately)\n", 300-timeout)
- break
- }
- }
-
- if err == errTimeout {
- fmt.Printf("I'm not waiting around.\n")
- err = nil
- }
-
- return err
-}
-
-func MakeNewPassword(oldPass string) string {
- fmt.Println("Current password: " + oldPass)
- randomPassword := RandomString("", 16)
- for randomPassword == oldPass {
- randomPassword = RandomString("", 16)
- }
- fmt.Println(" New password: " + randomPassword)
- return randomPassword
-}
-
-func ChangeAdminPassword(ts *testState) error {
- randomPassword := MakeNewPassword(ts.createdServer.AdminPass)
-
- err := servers.ChangeAdminPassword(ts.client, ts.createdServer.Id, randomPassword)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "PASSWORD")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebootServer(ts *testState) error {
- fmt.Println("Attempting reboot of server " + ts.createdServer.Id)
- err := servers.Reboot(ts.client, ts.createdServer.Id, servers.OSReboot)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "REBOOT")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebuildServer(ts *testState) error {
- fmt.Println("Attempting to rebuild server " + ts.createdServer.Id)
-
- newPassword := MakeNewPassword(ts.createdServer.AdminPass)
- newName := RandomString("ACPTTEST", 16)
- sr, err := servers.Rebuild(ts.client, ts.createdServer.Id, newName, newPassword, ts.imageId, nil)
- if err != nil {
- return err
- }
-
- s, err := servers.GetServer(sr)
- if err != nil {
- return err
- }
- if s.Id != ts.createdServer.Id {
- return fmt.Errorf("Expected rebuilt server ID of %s; got %s", ts.createdServer.Id, s.Id)
- }
-
- err = WaitForStatus(ts, "REBUILD")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func ResizeServer(ts *testState) error {
- fmt.Println("Attempting to resize server " + ts.createdServer.Id)
-
- err := servers.Resize(ts.client, ts.createdServer.Id, ts.flavorIdResize)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "RESIZE")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "VERIFY_RESIZE")
-}
-
-func ConfirmResize(ts *testState) error {
- fmt.Println("Attempting to confirm resize for server " + ts.createdServer.Id)
-
- err := servers.ConfirmResize(ts.client, ts.createdServer.Id)
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func RevertResize(ts *testState) error {
- fmt.Println("Attempting to revert resize for server " + ts.createdServer.Id)
-
- err := servers.RevertResize(ts.client, ts.createdServer.Id)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "REVERT_RESIZE")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-// randomString generates a string of given length, but random content.
-// All content will be within the ASCII graphic character set.
-// (Implementation from Even Shaw's contribution on
-// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
-func RandomString(prefix string, n int) string {
- const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
- var bytes = make([]byte, n)
- rand.Read(bytes)
- for i, b := range bytes {
- bytes[i] = alphanum[b%byte(len(alphanum))]
- }
- return prefix + string(bytes)
-}
diff --git a/acceptance/openstack/compute/v2/compute_test.go b/acceptance/openstack/compute/v2/compute_test.go
new file mode 100644
index 0000000..15b5163
--- /dev/null
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -0,0 +1,98 @@
+// +build acceptance
+
+package v2
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/openstack/utils"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+func waitForStatus(client *gophercloud.ServiceClient, server *servers.Server, status string) error {
+ return tools.WaitFor(func() (bool, error) {
+ latest, err := servers.Get(client, server.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if latest.Status == status {
+ // Success!
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
+
+// ComputeChoices contains image and flavor selections for use by the acceptance tests.
+type ComputeChoices struct {
+ // ImageID contains the ID of a valid image.
+ ImageID string
+
+ // FlavorID contains the ID of a valid flavor.
+ FlavorID string
+
+ // FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct
+ // from FlavorID.
+ FlavorIDResize string
+}
+
+// ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables.
+// If any required state is missing, an `error` will be returned that enumerates the missing properties.
+func ComputeChoicesFromEnv() (*ComputeChoices, error) {
+ imageID := os.Getenv("OS_IMAGE_ID")
+ flavorID := os.Getenv("OS_FLAVOR_ID")
+ flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE")
+
+ missing := make([]string, 0, 3)
+ if imageID == "" {
+ missing = append(missing, "OS_IMAGE_ID")
+ }
+ if flavorID == "" {
+ missing = append(missing, "OS_FLAVOR_ID")
+ }
+ if flavorIDResize == "" {
+ missing = append(missing, "OS_FLAVOR_ID_RESIZE")
+ }
+
+ notDistinct := ""
+ if flavorID == flavorIDResize {
+ notDistinct = "OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE must be distinct."
+ }
+
+ if len(missing) > 0 || notDistinct != "" {
+ text := "You're missing some important setup:\n"
+ if len(missing) > 0 {
+ text += " * These environment variables must be provided: " + strings.Join(missing, ", ") + "\n"
+ }
+ if notDistinct != "" {
+ text += " * " + notDistinct + "\n"
+ }
+
+ return nil, fmt.Errorf(text)
+ }
+
+ return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize}, nil
+}
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
new file mode 100644
index 0000000..ca810bc
--- /dev/null
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -0,0 +1,57 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListFlavors(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+ pager := flavors.List(client, flavors.ListFilterOptions{})
+ count, pages := 0, 0
+ pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("---")
+ pages++
+ flavors, err := flavors.ExtractFlavors(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, f := range flavors {
+ t.Logf("%s\t%s\t%d\t%d\t%d", f.ID, f.Name, f.RAM, f.Disk, f.VCPUs)
+ }
+
+ return true, nil
+ })
+
+ t.Logf("--------\n%d flavors listed on %d pages.", count, pages)
+}
+
+func TestGetFlavor(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ flavor, err := flavors.Get(client, choices.FlavorID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get flavor information: %v", err)
+ }
+
+ t.Logf("Flavor: %#v", flavor)
+}
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
new file mode 100644
index 0000000..7fca3ec
--- /dev/null
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -0,0 +1,37 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/compute/v2/images"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListImages(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute: client: %v", err)
+ }
+
+ t.Logf("ID\tRegion\tName\tStatus\tCreated")
+
+ pager := images.List(client)
+ count, pages := 0, 0
+ pager.EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+ images, err := images.ExtractImages(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, i := range images {
+ t.Logf("%s\t%s\t%s\t%s", i.ID, i.Name, i.Status, i.Created)
+ }
+
+ return true, nil
+ })
+
+ t.Logf("--------\n%d images listed on %d pages.", count, pages)
+}
diff --git a/acceptance/openstack/compute/v2/pkg.go b/acceptance/openstack/compute/v2/pkg.go
new file mode 100644
index 0000000..bb158c3
--- /dev/null
+++ b/acceptance/openstack/compute/v2/pkg.go
@@ -0,0 +1,3 @@
+// The v2 package contains acceptance tests for the Openstack Compute V2 service.
+
+package v2
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
new file mode 100644
index 0000000..620ef1b
--- /dev/null
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -0,0 +1,340 @@
+// +build acceptance
+
+package v2
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/acceptance/tools"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestListServers(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6")
+
+ pager := servers.List(client)
+ count, pages := 0, 0
+ pager.EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+ t.Logf("---")
+
+ servers, err := servers.ExtractServers(page)
+ if err != nil {
+ return false, err
+ }
+
+ for _, s := range servers {
+ t.Logf("%s\t%s\t%s\t%s\t%s\t\n", s.ID, s.Name, s.Status, s.AccessIPv4, s.AccessIPv6)
+ count++
+ }
+
+ return true, nil
+ })
+
+ fmt.Printf("--------\n%d servers listed on %d pages.\n", count, pages)
+}
+
+func createServer(t *testing.T, client *gophercloud.ServiceClient, choices *ComputeChoices) (*servers.Server, error) {
+ name := tools.RandomString("ACPTTEST", 16)
+ t.Logf("Attempting to create server: %s\n", name)
+
+ server, err := servers.Create(client, map[string]interface{}{
+ "flavorRef": choices.FlavorID,
+ "imageRef": choices.ImageID,
+ "name": name,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+
+ return server, err
+}
+
+func TestCreateDestroyServer(t *testing.T) {
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ name := tools.RandomString("ACPTTEST", 16)
+ t.Logf("Attempting to create server: %s\n", name)
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatalf("Unable to create server: %v", err)
+ }
+ defer func() {
+ servers.Delete(client, server.ID)
+ t.Logf("Server deleted.")
+ }()
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatalf("Unable to wait for server: %v", err)
+ }
+}
+
+func TestUpdateServer(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ alternateName := tools.RandomString("ACPTTEST", 16)
+ for alternateName == server.Name {
+ alternateName = tools.RandomString("ACPTTEST", 16)
+ }
+
+ t.Logf("Attempting to rename the server to %s.", alternateName)
+
+ updated, err := servers.Update(client, server.ID, map[string]interface{}{
+ "name": alternateName,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unable to rename server: %v", err)
+ }
+
+ if updated.ID != server.ID {
+ t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID)
+ }
+
+ err = tools.WaitFor(func() (bool, error) {
+ latest, err := servers.Get(client, updated.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ return latest.Name == alternateName, nil
+ })
+}
+
+func TestActionChangeAdminPassword(t *testing.T) {
+ t.Parallel()
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ randomPassword := tools.MakeNewPassword(server.AdminPass)
+ err = servers.ChangeAdminPassword(client, server.ID, randomPassword)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "PASSWORD"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionReboot(t *testing.T) {
+ t.Parallel()
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ err = servers.Reboot(client, server.ID, "aldhjflaskhjf")
+ if err == nil {
+ t.Fatal("Expected the SDK to provide an ArgumentError here")
+ }
+
+ t.Logf("Attempting reboot of server %s", server.ID)
+ err = servers.Reboot(client, server.ID, servers.OSReboot)
+ if err != nil {
+ t.Fatalf("Unable to reboot server: %v", err)
+ }
+
+ if err = waitForStatus(client, server, "REBOOT"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionRebuild(t *testing.T) {
+ t.Parallel()
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("Attempting to rebuild server %s", server.ID)
+
+ newPassword := tools.MakeNewPassword(server.AdminPass)
+ newName := tools.RandomString("ACPTTEST", 16)
+ rebuilt, err := servers.Rebuild(client, server.ID, newName, newPassword, choices.ImageID, nil).Extract()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if rebuilt.ID != server.ID {
+ t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID)
+ }
+
+ if err = waitForStatus(client, rebuilt, "REBUILD"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, rebuilt, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func resizeServer(t *testing.T, client *gophercloud.ServiceClient, server *servers.Server, choices *ComputeChoices) {
+ if err := waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("Attempting to resize server [%s]", server.ID)
+
+ if err := servers.Resize(client, server.ID, choices.FlavorIDResize); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := waitForStatus(client, server, "VERIFY_RESIZE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeConfirm(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+ resizeServer(t, client, server, choices)
+
+ t.Logf("Attempting to confirm resize for server %s", server.ID)
+
+ if err = servers.ConfirmResize(client, server.ID); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeRevert(t *testing.T) {
+ t.Parallel()
+
+ choices, err := ComputeChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ server, err := createServer(t, client, choices)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer servers.Delete(client, server.ID)
+ resizeServer(t, client, server, choices)
+
+ t.Logf("Attempting to revert resize for server %s", server.ID)
+
+ if err := servers.RevertResize(client, server.ID); err != nil {
+ t.Fatal(err)
+ }
+
+ if err = waitForStatus(client, server, "ACTIVE"); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/acceptance/openstack/networking/v2/network_test.go b/acceptance/openstack/networking/v2/network_test.go
index 9f18597..f00edba 100644
--- a/acceptance/openstack/networking/v2/network_test.go
+++ b/acceptance/openstack/networking/v2/network_test.go
@@ -18,6 +18,7 @@
// Create a network
n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
th.AssertNoErr(t, err)
+ defer networks.Delete(Client, n.ID)
th.AssertEquals(t, n.Name, "sample_network")
th.AssertEquals(t, n.AdminStateUp, true)
networkID := n.ID
diff --git a/acceptance/tools/tools.go b/acceptance/tools/tools.go
index 396241c..4771ebb 100644
--- a/acceptance/tools/tools.go
+++ b/acceptance/tools/tools.go
@@ -1,358 +1,41 @@
// +build acceptance
-
package tools
import (
"crypto/rand"
- "fmt"
- "os"
- "text/tabwriter"
+ "errors"
"time"
-
- "github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
- "github.com/rackspace/gophercloud/openstack/utils"
)
-var errTimeout = fmt.Errorf("Timeout.")
+// ErrTimeout is returned if WaitFor takes longer than 300 second to happen.
+var ErrTimeout = errors.New("Timed out")
-type testState struct {
- O gophercloud.AuthOptions
- A identity.AuthResults
- SC *identity.ServiceCatalog
- EPs []identity.Endpoint
- W *tabwriter.Writer
- ImageId string
- FlavorId string
- Region string
- EP string
- Client *servers.Client
- CreatedServer *servers.Server
- GottenServer *servers.Server
- UpdatedServer *servers.Server
- ServerName string
- AlternateName string
- FlavorIdResize string
-}
+// WaitFor polls a predicate function once per second to wait for a certain state to arrive.
+func WaitFor(predicate func() (bool, error)) error {
+ for i := 0; i < 300; i++ {
+ time.Sleep(1 * time.Second)
-func SetupForList(service string) (*testState, error) {
- var err error
-
- ts := new(testState)
-
- ts.O, err = utils.AuthOptions()
- if err != nil {
- return ts, err
- }
-
- client := &gophercloud.ServiceClient{Endpoint: ts.O.IdentityEndpoint + "/"}
- ts.A, err = identity.Authenticate(client, ts.O)
- if err != nil {
- return ts, err
- }
-
- ts.SC, err = identity.GetServiceCatalog(ts.A)
- if err != nil {
- return ts, err
- }
-
- ts.EPs, err = FindAllEndpoints(ts.SC, service)
- if err != nil {
- return ts, err
- }
-
- ts.W = new(tabwriter.Writer)
- ts.W.Init(os.Stdout, 2, 8, 2, ' ', 0)
-
- return ts, nil
-}
-
-func SetupForCRUD() (*testState, error) {
- ts, err := SetupForList("compute")
- if err != nil {
- return ts, err
- }
-
- ts.ImageId = os.Getenv("OS_IMAGE_ID")
- if ts.ImageId == "" {
- return ts, fmt.Errorf("Expected OS_IMAGE_ID environment variable to be set")
- }
-
- ts.FlavorId = os.Getenv("OS_FLAVOR_ID")
- if ts.FlavorId == "" {
- return ts, fmt.Errorf("Expected OS_FLAVOR_ID environment variable to be set")
- }
-
- ts.FlavorIdResize = os.Getenv("OS_FLAVOR_ID_RESIZE")
- if ts.FlavorIdResize == "" {
- return ts, fmt.Errorf("Expected OS_FLAVOR_ID_RESIZE environment variable to be set")
- }
-
- if ts.FlavorIdResize == ts.FlavorId {
- return ts, fmt.Errorf("OS_FLAVOR_ID and OS_FLAVOR_ID_RESIZE cannot be the same")
- }
-
- ts.Region = os.Getenv("OS_REGION_NAME")
- if ts.Region == "" {
- ts.Region = ts.EPs[0].Region
- }
-
- ts.EP, err = FindEndpointForRegion(ts.EPs, ts.Region)
- if err != nil {
- return ts, err
- }
-
- return ts, err
-}
-
-func FindAllEndpoints(sc *identity.ServiceCatalog, service string) ([]identity.Endpoint, error) {
- ces, err := sc.CatalogEntries()
- if err != nil {
- return nil, err
- }
-
- for _, ce := range ces {
- if ce.Type == service {
- return ce.Endpoints, nil
+ satisfied, err := predicate()
+ if err != nil {
+ return err
+ }
+ if satisfied {
+ return nil
}
}
-
- return nil, fmt.Errorf(service + " endpoint not found.")
+ return ErrTimeout
}
-func FindEndpointForRegion(eps []identity.Endpoint, r string) (string, error) {
- for _, ep := range eps {
- if ep.Region == r {
- return ep.PublicURL, nil
- }
- }
- return "", fmt.Errorf("Unknown region %s", r)
-}
-
-func CountDown(ts *testState, timeout int) (bool, int, error) {
- if timeout < 1 {
- return false, 0, errTimeout
- }
- time.Sleep(1 * time.Second)
- timeout--
-
- gr, err := servers.GetDetail(ts.Client, ts.CreatedServer.Id)
- if err != nil {
- return false, timeout, err
- }
-
- ts.GottenServer, err = servers.GetServer(gr)
- if err != nil {
- return false, timeout, err
- }
-
- return true, timeout, nil
-}
-
-func CreateServer(ts *testState) error {
- ts.ServerName = RandomString("ACPTTEST", 16)
- fmt.Printf("Attempting to create server: %s\n", ts.ServerName)
-
- ts.Client = servers.NewClient(ts.EP, ts.A, ts.O)
-
- cr, err := servers.Create(ts.Client, map[string]interface{}{
- "flavorRef": ts.FlavorId,
- "imageRef": ts.ImageId,
- "name": ts.ServerName,
- })
- if err != nil {
- return err
- }
-
- ts.CreatedServer, err = servers.GetServer(cr)
- return err
-}
-
-func WaitForStatus(ts *testState, s string) error {
- var (
- inProgress bool
- timeout int
- err error
- )
-
- for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
- if ts.GottenServer.Id != ts.CreatedServer.Id {
- return fmt.Errorf("created server id (%s) != gotten server id (%s)", ts.CreatedServer.Id, ts.GottenServer.Id)
- }
-
- if ts.GottenServer.Status == s {
- fmt.Printf("Server reached state %s after %d seconds (approximately)\n", s, 300-timeout)
- break
- }
- }
-
- if err == errTimeout {
- fmt.Printf("Time out -- I'm not waiting around.\n")
- err = nil
- }
-
- return err
-}
-
-func ChangeServerName(ts *testState) error {
- var (
- inProgress bool
- timeout int
- )
-
- ts.AlternateName = RandomString("ACPTTEST", 16)
- for ts.AlternateName == ts.ServerName {
- ts.AlternateName = RandomString("ACPTTEST", 16)
- }
- fmt.Println("Attempting to change server name")
-
- ur, err := servers.Update(ts.Client, ts.CreatedServer.Id, map[string]interface{}{
- "name": ts.AlternateName,
- })
- if err != nil {
- return err
- }
-
- ts.UpdatedServer, err = servers.GetServer(ur)
- if err != nil {
- return err
- }
-
- if ts.UpdatedServer.Id != ts.CreatedServer.Id {
- return fmt.Errorf("Expected updated and created server to share the same ID")
- }
-
- for inProgress, timeout, err = CountDown(ts, 300); inProgress; inProgress, timeout, err = CountDown(ts, timeout) {
- if ts.GottenServer.Id != ts.UpdatedServer.Id {
- return fmt.Errorf("Updated server ID (%s) != gotten server ID (%s)", ts.UpdatedServer.Id, ts.GottenServer.Id)
- }
-
- if ts.GottenServer.Name == ts.AlternateName {
- fmt.Printf("Server updated after %d seconds (approximately)\n", 300-timeout)
- break
- }
- }
-
- if err == errTimeout {
- fmt.Printf("I'm not waiting around.\n")
- err = nil
- }
-
- return err
-}
-
+// MakeNewPassword generates a new string that's guaranteed to be different than the given one.
func MakeNewPassword(oldPass string) string {
- fmt.Println("Current password: " + oldPass)
randomPassword := RandomString("", 16)
for randomPassword == oldPass {
randomPassword = RandomString("", 16)
}
- fmt.Println(" New password: " + randomPassword)
return randomPassword
}
-func ChangeAdminPassword(ts *testState) error {
- randomPassword := MakeNewPassword(ts.CreatedServer.AdminPass)
-
- err := servers.ChangeAdminPassword(ts.Client, ts.CreatedServer.Id, randomPassword)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "PASSWORD")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebootServer(ts *testState) error {
- fmt.Println("Attempting reboot of server " + ts.CreatedServer.Id)
- err := servers.Reboot(ts.Client, ts.CreatedServer.Id, servers.OSReboot)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "REBOOT")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func RebuildServer(ts *testState) error {
- fmt.Println("Attempting to rebuild server " + ts.CreatedServer.Id)
-
- newPassword := MakeNewPassword(ts.CreatedServer.AdminPass)
- newName := RandomString("ACPTTEST", 16)
- sr, err := servers.Rebuild(ts.Client, ts.CreatedServer.Id, newName, newPassword, ts.ImageId, nil)
- if err != nil {
- return err
- }
-
- s, err := servers.GetServer(sr)
- if err != nil {
- return err
- }
- if s.Id != ts.CreatedServer.Id {
- return fmt.Errorf("Expected rebuilt server ID of %s; got %s", ts.CreatedServer.Id, s.Id)
- }
-
- err = WaitForStatus(ts, "REBUILD")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func ResizeServer(ts *testState) error {
- fmt.Println("Attempting to resize server " + ts.CreatedServer.Id)
-
- err := servers.Resize(ts.Client, ts.CreatedServer.Id, ts.FlavorIdResize)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "RESIZE")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "VERIFY_RESIZE")
-}
-
-func ConfirmResize(ts *testState) error {
- fmt.Println("Attempting to confirm resize for server " + ts.CreatedServer.Id)
-
- err := servers.ConfirmResize(ts.Client, ts.CreatedServer.Id)
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-func RevertResize(ts *testState) error {
- fmt.Println("Attempting to revert resize for server " + ts.CreatedServer.Id)
-
- err := servers.RevertResize(ts.Client, ts.CreatedServer.Id)
- if err != nil {
- return err
- }
-
- err = WaitForStatus(ts, "REVERT_RESIZE")
- if err != nil {
- return err
- }
-
- return WaitForStatus(ts, "ACTIVE")
-}
-
-// randomString generates a string of given length, but random content.
+// RandomString generates a string of given length, but random content.
// All content will be within the ASCII graphic character set.
// (Implementation from Even Shaw's contribution on
// http://stackoverflow.com/questions/12771930/what-is-the-fastest-way-to-generate-a-long-random-string-in-go).
diff --git a/openstack/client.go b/openstack/client.go
index a73f301..eeeb809 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -294,6 +294,17 @@
return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
}
+// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
+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
+}
+
+// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("network")
url, err := client.EndpointLocator(eo)
diff --git a/openstack/compute/v2/flavors/client.go b/openstack/compute/v2/flavors/client.go
deleted file mode 100644
index edeec66..0000000
--- a/openstack/compute/v2/flavors/client.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package flavors
-
-import (
- "fmt"
- "net/url"
- "strconv"
-
- "github.com/rackspace/gophercloud"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-type Client struct {
- endpoint string
- authority identity.AuthResults
- options gophercloud.AuthOptions
-}
-
-func NewClient(e string, a identity.AuthResults, ao gophercloud.AuthOptions) *Client {
- return &Client{
- endpoint: e,
- authority: a,
- options: ao,
- }
-}
-
-func (c *Client) getListUrl(lfo ListFilterOptions) string {
- v := url.Values{}
- if lfo.ChangesSince != "" {
- v.Set("changes-since", lfo.ChangesSince)
- }
- if lfo.MinDisk != 0 {
- v.Set("minDisk", strconv.Itoa(lfo.MinDisk))
- }
- if lfo.MinRam != 0 {
- v.Set("minRam", strconv.Itoa(lfo.MinRam))
- }
- if lfo.Marker != "" {
- v.Set("marker", lfo.Marker)
- }
- if lfo.Limit != 0 {
- v.Set("limit", strconv.Itoa(lfo.Limit))
- }
- tail := ""
- if len(v) > 0 {
- tail = fmt.Sprintf("?%s", v.Encode())
- }
- return fmt.Sprintf("%s/flavors/detail%s", c.endpoint, tail)
-}
-
-func (c *Client) getGetUrl(id string) string {
- return fmt.Sprintf("%s/flavors/%s", c.endpoint, id)
-}
-
-func (c *Client) getListHeaders() (map[string]string, error) {
- t, err := identity.GetToken(c.authority)
- if err != nil {
- return map[string]string{}, err
- }
-
- return map[string]string{
- "X-Auth-Token": t.ID,
- }, nil
-}
diff --git a/openstack/compute/v2/flavors/flavors.go b/openstack/compute/v2/flavors/flavors.go
deleted file mode 100644
index 146bcc4..0000000
--- a/openstack/compute/v2/flavors/flavors.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package flavors
-
-import (
- "github.com/mitchellh/mapstructure"
- "reflect"
-)
-
-// Flavor records represent (virtual) hardware configurations for server resources in a region.
-//
-// The Id field contains the flavor's unique identifier.
-// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
-//
-// The Disk and Ram fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
-//
-// The Name field provides a human-readable moniker for the flavor.
-//
-// Swap indicates how much space is reserved for swap.
-// If not provided, this field will be set to 0.
-//
-// VCpus indicates how many (virtual) CPUs are available for this flavor.
-type Flavor struct {
- Disk int
- Id string
- Name string
- Ram int
- RxTxFactor float64 `mapstructure:"rxtx_factor"`
- Swap int
- VCpus int
-}
-
-func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
- if (from == reflect.String) && (to == reflect.Int) {
- return 0, nil
- }
- return v, nil
-}
-
-// GetFlavors provides access to the list of flavors returned by the List function.
-func GetFlavors(lr ListResults) ([]Flavor, error) {
- fa, ok := lr["flavors"]
- if !ok {
- return nil, ErrNotImplemented
- }
- fms := fa.([]interface{})
-
- flavors := make([]Flavor, len(fms))
- for i, fm := range fms {
- flavorObj := fm.(map[string]interface{})
- cfg := &mapstructure.DecoderConfig{
- DecodeHook: defaulter,
- Result: &flavors[i],
- }
- decoder, err := mapstructure.NewDecoder(cfg)
- if err != nil {
- return flavors, err
- }
- err = decoder.Decode(flavorObj)
- if err != nil {
- return flavors, err
- }
- }
- return flavors, nil
-}
-
-// GetFlavor provides access to the individual flavor returned by the Get function.
-func GetFlavor(gr GetResults) (*Flavor, error) {
- f, ok := gr["flavor"]
- if !ok {
- return nil, ErrNotImplemented
- }
-
- flav := new(Flavor)
- cfg := &mapstructure.DecoderConfig{
- DecodeHook: defaulter,
- Result: flav,
- }
- decoder, err := mapstructure.NewDecoder(cfg)
- if err != nil {
- return flav, err
- }
- err = decoder.Decode(f)
- return flav, err
-}
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index 3758206..47eb172 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -1,58 +1,48 @@
package flavors
import (
- "fmt"
"github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
)
-var ErrNotImplemented = fmt.Errorf("Flavors functionality not implemented.")
-
-type ListResults map[string]interface{}
-type GetResults map[string]interface{}
-
// ListFilterOptions helps control the results returned by the List() function.
-// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
-// MinDisk and MinRam, if provided, elides flavors which do not meet your criteria.
// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20.
-// Marker and Limit control paging.
-// Limit instructs List to refrain from sending excessively large lists of flavors.
-// Marker instructs List where to start listing from.
// Typically, software will use the last ID of the previous call to List to set the Marker for the current call.
type ListFilterOptions struct {
+
+ // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided.
ChangesSince string
- MinDisk, MinRam int
+
+ // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria.
+ MinDisk, MinRAM int
+
+ // Marker and Limit control paging.
+ // Marker instructs List where to start listing from.
Marker string
+
+ // Limit instructs List to refrain from sending excessively large lists of flavors.
Limit int
}
// List instructs OpenStack to provide a list of flavors.
// You may provide criteria by which List curtails its results for easier processing.
// See ListFilterOptions for more details.
-func List(c *Client, lfo ListFilterOptions) (ListResults, error) {
- var lr ListResults
-
- h, err := c.getListHeaders()
- if err != nil {
- return nil, err
+func List(client *gophercloud.ServiceClient, lfo ListFilterOptions) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return FlavorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- err = perigee.Get(c.getListUrl(lfo), perigee.Options{
- Results: &lr,
- MoreHeaders: h,
- })
- return lr, err
+ return pagination.NewPager(client, listURL(client, lfo), createPage)
}
// Get instructs OpenStack to provide details on a single flavor, identified by its ID.
-func Get(c *Client, id string) (GetResults, error) {
- var gr GetResults
- h, err := c.getListHeaders() // same for Get Flavor API
- if err != nil {
- return gr, err
- }
- err = perigee.Get(c.getGetUrl(id), perigee.Options{
- Results: &gr,
- MoreHeaders: h,
+// Use ExtractFlavor to convert its result into a Flavor.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var gr GetResult
+ gr.Err = perigee.Get(flavorURL(client, id), perigee.Options{
+ Results: &gr.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
})
- return gr, err
+ return gr
}
diff --git a/openstack/compute/v2/flavors/requests_test.go b/openstack/compute/v2/flavors/requests_test.go
new file mode 100644
index 0000000..e1b6b4f
--- /dev/null
+++ b/openstack/compute/v2/flavors/requests_test.go
@@ -0,0 +1,138 @@
+package flavors
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "blerb"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestListFlavors(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/flavors/detail", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "flavors": [
+ {
+ "id": "1",
+ "name": "m1.tiny",
+ "disk": 1,
+ "ram": 512,
+ "vcpus": 1
+ },
+ {
+ "id": "2",
+ "name": "m2.small",
+ "disk": 10,
+ "ram": 1024,
+ "vcpus": 2
+ }
+ ],
+ "flavors_links": [
+ {
+ "href": "%s/flavors/detail?marker=2",
+ "rel": "next"
+ }
+ ]
+ }
+ `, testhelper.Server.URL)
+ case "2":
+ fmt.Fprintf(w, `{ "flavors": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ pages := 0
+ err := List(client, ListFilterOptions{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractFlavors(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Flavor{
+ Flavor{ID: "1", Name: "m1.tiny", Disk: 1, RAM: 512, VCPUs: 1},
+ Flavor{ID: "2", Name: "m2.small", Disk: 10, RAM: 1024, VCPUs: 2},
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but was %#v", expected, actual)
+ }
+
+ return true, nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected one page, got %d", pages)
+ }
+}
+
+func TestGetFlavor(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/flavors/12345", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "flavor": {
+ "id": "1",
+ "name": "m1.tiny",
+ "disk": 1,
+ "ram": 512,
+ "vcpus": 1,
+ "rxtx_factor": 1
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+ actual, err := Get(client, "12345").Extract()
+ if err != nil {
+ t.Fatalf("Unable to get flavor: %v", err)
+ }
+
+ expected := &Flavor{
+ ID: "1",
+ Name: "m1.tiny",
+ Disk: 1,
+ RAM: 512,
+ VCPUs: 1,
+ RxTxFactor: 1,
+ }
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but was %#v", expected, actual)
+ }
+}
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
new file mode 100644
index 0000000..68c8f58
--- /dev/null
+++ b/openstack/compute/v2/flavors/results.go
@@ -0,0 +1,136 @@
+package flavors
+
+import (
+ "errors"
+ "reflect"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ErrCannotInterpret is returned by an Extract call if the response body doesn't have the expected structure.
+var ErrCannotInterpet = errors.New("Unable to interpret a response body.")
+
+// GetResult temporarily holds the reponse from a Get call.
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract provides access to the individual Flavor returned by the Get function.
+func (gr GetResult) Extract() (*Flavor, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ var result struct {
+ Flavor Flavor `mapstructure:"flavor"`
+ }
+
+ cfg := &mapstructure.DecoderConfig{
+ DecodeHook: defaulter,
+ Result: &result,
+ }
+ decoder, err := mapstructure.NewDecoder(cfg)
+ if err != nil {
+ return nil, err
+ }
+ err = decoder.Decode(gr.Resp)
+ return &result.Flavor, err
+}
+
+// Flavor records represent (virtual) hardware configurations for server resources in a region.
+type Flavor struct {
+ // The Id field contains the flavor's unique identifier.
+ // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
+ ID string `mapstructure:"id"`
+
+ // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
+ Disk int `mapstructure:"disk"`
+ RAM int `mapstructure:"ram"`
+
+ // The Name field provides a human-readable moniker for the flavor.
+ Name string `mapstructure:"name"`
+
+ RxTxFactor float64 `mapstructure:"rxtx_factor"`
+
+ // Swap indicates how much space is reserved for swap.
+ // If not provided, this field will be set to 0.
+ Swap int `mapstructure:"swap"`
+
+ // VCPUs indicates how many (virtual) CPUs are available for this flavor.
+ VCPUs int `mapstructure:"vcpus"`
+}
+
+// FlavorPage contains a single page of the response from a List call.
+type FlavorPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty determines if a page contains any results.
+func (p FlavorPage) IsEmpty() (bool, error) {
+ flavors, err := ExtractFlavors(p)
+ if err != nil {
+ return true, err
+ }
+ return len(flavors) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (p FlavorPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"flavors_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+func defaulter(from, to reflect.Kind, v interface{}) (interface{}, error) {
+ if (from == reflect.String) && (to == reflect.Int) {
+ return 0, nil
+ }
+ return v, nil
+}
+
+// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation.
+func ExtractFlavors(page pagination.Page) ([]Flavor, error) {
+ casted := page.(FlavorPage).Body
+ var container struct {
+ Flavors []Flavor `mapstructure:"flavors"`
+ }
+
+ cfg := &mapstructure.DecoderConfig{
+ DecodeHook: defaulter,
+ Result: &container,
+ }
+ decoder, err := mapstructure.NewDecoder(cfg)
+ if err != nil {
+ return container.Flavors, err
+ }
+ err = decoder.Decode(casted)
+ if err != nil {
+ return container.Flavors, err
+ }
+
+ return container.Flavors, nil
+}
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
new file mode 100644
index 0000000..9e5b562
--- /dev/null
+++ b/openstack/compute/v2/flavors/urls.go
@@ -0,0 +1,37 @@
+package flavors
+
+import (
+ "fmt"
+ "net/url"
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func listURL(client *gophercloud.ServiceClient, lfo ListFilterOptions) string {
+ v := url.Values{}
+ if lfo.ChangesSince != "" {
+ v.Set("changes-since", lfo.ChangesSince)
+ }
+ if lfo.MinDisk != 0 {
+ v.Set("minDisk", strconv.Itoa(lfo.MinDisk))
+ }
+ if lfo.MinRAM != 0 {
+ v.Set("minRam", strconv.Itoa(lfo.MinRAM))
+ }
+ if lfo.Marker != "" {
+ v.Set("marker", lfo.Marker)
+ }
+ if lfo.Limit != 0 {
+ v.Set("limit", strconv.Itoa(lfo.Limit))
+ }
+ tail := ""
+ if len(v) > 0 {
+ tail = fmt.Sprintf("?%s", v.Encode())
+ }
+ return client.ServiceURL("flavors", "detail") + tail
+}
+
+func flavorURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("flavors", id)
+}
diff --git a/openstack/compute/v2/images/client.go b/openstack/compute/v2/images/client.go
deleted file mode 100644
index 6322b9c..0000000
--- a/openstack/compute/v2/images/client.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package images
-
-import (
- "github.com/rackspace/gophercloud"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-type Client struct {
- endpoint string
- authority identity.AuthResults
- options gophercloud.AuthOptions
-}
-
-func NewClient(e string, a identity.AuthResults, ao gophercloud.AuthOptions) *Client {
- return &Client{
- endpoint: e,
- authority: a,
- options: ao,
- }
-}
-
-func (c *Client) getListUrl() string {
- return c.endpoint + "/images/detail"
-}
-
-func (c *Client) getListHeaders() (map[string]string, error) {
- t, err := identity.GetToken(c.authority)
- if err != nil {
- return map[string]string{}, err
- }
-
- return map[string]string{
- "X-Auth-Token": t.ID,
- }, nil
-}
diff --git a/openstack/compute/v2/images/images.go b/openstack/compute/v2/images/images.go
deleted file mode 100644
index c881092..0000000
--- a/openstack/compute/v2/images/images.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package images
-
-import "github.com/mitchellh/mapstructure"
-
-// Image is used for JSON (un)marshalling.
-// It provides a description of an OS image.
-//
-// The Id field contains the image's unique identifier.
-// For example, this identifier will be useful for specifying which operating system to install on a new server instance.
-//
-// The MinDisk and MinRam fields specify the minimum resources a server must provide to be able to install the image.
-//
-// The Name field provides a human-readable moniker for the OS image.
-//
-// The Progress and Status fields indicate image-creation status.
-// Any usable image will have 100% progress.
-//
-// The Updated field indicates the last time this image was changed.
-type Image struct {
- Created string
- Id string
- MinDisk int
- MinRam int
- Name string
- Progress int
- Status string
- Updated string
-}
-
-func GetImages(lr ListResults) ([]Image, error) {
- ia, ok := lr["images"]
- if !ok {
- return nil, ErrNotImplemented
- }
- ims := ia.([]interface{})
-
- images := make([]Image, len(ims))
- for i, im := range ims {
- imageObj := im.(map[string]interface{})
- err := mapstructure.Decode(imageObj, &images[i])
- if err != nil {
- return images, err
- }
- }
- return images, nil
-}
-
-func GetImage(ir ImageResults) (Image, error) {
- image := Image{}
- err := mapstructure.Decode(ir, &image)
- return image, err
-}
diff --git a/openstack/compute/v2/images/images_test.go b/openstack/compute/v2/images/images_test.go
deleted file mode 100644
index ee3b79e..0000000
--- a/openstack/compute/v2/images/images_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package images
-
-import (
- "encoding/json"
- "testing"
-)
-
-const (
- // This example was taken from: http://docs.openstack.org/api/openstack-compute/2/content/Rebuild_Server-d1e3538.html
-
- simpleImageJson = `{
- "id": "52415800-8b69-11e0-9b19-734f6f006e54",
- "name": "CentOS 5.2",
- "links": [{
- "rel": "self",
- "href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
- },{
- "rel": "bookmark",
- "href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
- }]
- }`
-)
-
-func TestGetImage(t *testing.T) {
- var simpleImageMap map[string]interface{}
- err := json.Unmarshal([]byte(simpleImageJson), &simpleImageMap)
- if err != nil {
- t.Fatal(err)
- }
-
- image, err := GetImage(simpleImageMap)
- if err != nil {
- t.Fatal(err)
- }
-
- if image.Id != "52415800-8b69-11e0-9b19-734f6f006e54" {
- t.Fatal("I expected an image ID of 52415800-8b69-11e0-9b19-734f6f006e54; got " + image.Id)
- }
-
- if image.Name != "CentOS 5.2" {
- t.Fatal("I expected an image name of CentOS 5.2; got " + image.Name)
- }
-}
diff --git a/openstack/compute/v2/images/requests.go b/openstack/compute/v2/images/requests.go
index 79783c4..a887cc6 100644
--- a/openstack/compute/v2/images/requests.go
+++ b/openstack/compute/v2/images/requests.go
@@ -1,26 +1,28 @@
package images
import (
- "fmt"
"github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
)
-var ErrNotImplemented = fmt.Errorf("Images functionality not implemented.")
-
-type ListResults map[string]interface{}
-type ImageResults map[string]interface{}
-
-func List(c *Client) (ListResults, error) {
- var lr ListResults
-
- h, err := c.getListHeaders()
- if err != nil {
- return nil, err
+// List enumerates the available images.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ImagePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- err = perigee.Get(c.getListUrl(), perigee.Options{
- Results: &lr,
- MoreHeaders: h,
+ return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// Get acquires additional detail about a specific image by ID.
+// Use ExtractImage() to intepret the result as an openstack Image.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", imageURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ Results: &result.Resp,
+ OkCodes: []int{200},
})
- return lr, err
+ return result
}
diff --git a/openstack/compute/v2/images/requests_test.go b/openstack/compute/v2/images/requests_test.go
new file mode 100644
index 0000000..396c21f
--- /dev/null
+++ b/openstack/compute/v2/images/requests_test.go
@@ -0,0 +1,168 @@
+package images
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "aaaaaa"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestListImages(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/images/detail", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `
+ {
+ "images": [
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:54:56Z",
+ "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ "OS-EXT-IMG-SIZE:size": 476704768,
+ "name": "F17-x86_64-cfntools",
+ "created": "2014-09-23T12:54:52Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ },
+ {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:51:43Z",
+ "id": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "OS-EXT-IMG-SIZE:size": 13167616,
+ "name": "cirros-0.3.2-x86_64-disk",
+ "created": "2014-09-23T12:51:42Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ }
+ ]
+ }
+ `)
+ case "2":
+ fmt.Fprintf(w, `{ "images": [] }`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ pages := 0
+ err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractImages(page)
+ if err != nil {
+ return false, err
+ }
+
+ expected := []Image{
+ Image{
+ ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ Name: "F17-x86_64-cfntools",
+ Created: "2014-09-23T12:54:52Z",
+ Updated: "2014-09-23T12:54:56Z",
+ MinDisk: 0,
+ MinRAM: 0,
+ Progress: 100,
+ Status: "ACTIVE",
+ },
+ Image{
+ ID: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ Name: "cirros-0.3.2-x86_64-disk",
+ Created: "2014-09-23T12:51:42Z",
+ Updated: "2014-09-23T12:51:43Z",
+ MinDisk: 0,
+ MinRAM: 0,
+ Progress: 100,
+ Status: "ACTIVE",
+ },
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Unexpected page contents: expected %#v, got %#v", expected, actual)
+ }
+
+ return false, nil
+ })
+
+ if err != nil {
+ t.Fatalf("EachPage error: %v", err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected one page, got %d", pages)
+ }
+}
+
+func TestGetImage(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/images/12345678", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, `
+ {
+ "image": {
+ "status": "ACTIVE",
+ "updated": "2014-09-23T12:54:56Z",
+ "id": "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ "OS-EXT-IMG-SIZE:size": 476704768,
+ "name": "F17-x86_64-cfntools",
+ "created": "2014-09-23T12:54:52Z",
+ "minDisk": 0,
+ "progress": 100,
+ "minRam": 0,
+ "metadata": {}
+ }
+ }
+ `)
+ })
+
+ client := serviceClient()
+ actual, err := Get(client, "12345678").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected error from Get: %v", err)
+ }
+
+ expected := &Image{
+ Status: "ACTIVE",
+ Updated: "2014-09-23T12:54:56Z",
+ ID: "f3e4a95d-1f4f-4989-97ce-f3a1fb8c04d7",
+ Name: "F17-x86_64-cfntools",
+ Created: "2014-09-23T12:54:52Z",
+ MinDisk: 0,
+ Progress: 100,
+ MinRAM: 0,
+ }
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but got %#v", expected, actual)
+ }
+}
diff --git a/openstack/compute/v2/images/results.go b/openstack/compute/v2/images/results.go
new file mode 100644
index 0000000..f93c90c
--- /dev/null
+++ b/openstack/compute/v2/images/results.go
@@ -0,0 +1,104 @@
+package images
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// GetResult temporarily stores a Get response.
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult as an Image.
+func (gr GetResult) Extract() (*Image, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+
+ var decoded struct {
+ Image Image `mapstructure:"image"`
+ }
+
+ err := mapstructure.Decode(gr.Resp, &decoded)
+ return &decoded.Image, err
+}
+
+// Image is used for JSON (un)marshalling.
+// It provides a description of an OS image.
+type Image struct {
+ // ID contains the image's unique identifier.
+ ID string
+
+ Created string
+
+ // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
+ MinDisk int
+ MinRAM int
+
+ // Name provides a human-readable moniker for the OS image.
+ Name string
+
+ // The Progress and Status fields indicate image-creation status.
+ // Any usable image will have 100% progress.
+ Progress int
+ Status string
+
+ Updated string
+}
+
+// ImagePage contains a single page of results from a List operation.
+// Use ExtractImages to convert it into a slice of usable structs.
+type ImagePage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Image results.
+func (page ImagePage) IsEmpty() (bool, error) {
+ images, err := ExtractImages(page)
+ if err != nil {
+ return true, err
+ }
+ return len(images) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ImagePage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"images_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// ExtractImages converts a page of List results into a slice of usable Image structs.
+func ExtractImages(page pagination.Page) ([]Image, error) {
+ casted := page.(ImagePage).Body
+ var results struct {
+ Images []Image `mapstructure:"images"`
+ }
+
+ err := mapstructure.Decode(casted, &results)
+ return results.Images, err
+}
diff --git a/openstack/compute/v2/images/urls.go b/openstack/compute/v2/images/urls.go
new file mode 100644
index 0000000..4ae2269
--- /dev/null
+++ b/openstack/compute/v2/images/urls.go
@@ -0,0 +1,11 @@
+package images
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("images", "detail")
+}
+
+func imageURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("images", id)
+}
diff --git a/openstack/compute/v2/servers/client.go b/openstack/compute/v2/servers/client.go
deleted file mode 100644
index 8c79f94..0000000
--- a/openstack/compute/v2/servers/client.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package servers
-
-import (
- "fmt"
-
- "github.com/rackspace/gophercloud"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-// Client abstracts the connection information needed to make API requests for OpenStack compute endpoints.
-type Client struct {
- endpoint string
- authority identity.AuthResults
- options gophercloud.AuthOptions
- token *identity.Token
-}
-
-// NewClient creates a new Client structure to use when issuing requests to the server.
-func NewClient(e string, a identity.AuthResults, o gophercloud.AuthOptions) *Client {
- return &Client{
- endpoint: e,
- authority: a,
- options: o,
- }
-}
-
-func (c *Client) getListUrl() string {
- return fmt.Sprintf("%s/servers/detail", c.endpoint)
-}
-
-func (c *Client) getCreateUrl() string {
- return fmt.Sprintf("%s/servers", c.endpoint)
-}
-
-func (c *Client) getDeleteUrl(id string) string {
- return fmt.Sprintf("%s/servers/%s", c.endpoint, id)
-}
-
-func (c *Client) getDetailUrl(id string) string {
- return c.getDeleteUrl(id)
-}
-
-func (c *Client) getUpdateUrl(id string) string {
- return c.getDeleteUrl(id)
-}
-
-func (c *Client) getActionUrl(id string) string {
- return fmt.Sprintf("%s/servers/%s/action", c.endpoint, id)
-}
-
-func (c *Client) getListHeaders() (map[string]string, error) {
- t, err := c.getAuthToken()
- if err != nil {
- return map[string]string{}, err
- }
-
- return map[string]string{
- "X-Auth-Token": t,
- }, nil
-}
-
-func (c *Client) getCreateHeaders() (map[string]string, error) {
- return c.getListHeaders()
-}
-
-func (c *Client) getDeleteHeaders() (map[string]string, error) {
- return c.getListHeaders()
-}
-
-func (c *Client) getDetailHeaders() (map[string]string, error) {
- return c.getListHeaders()
-}
-
-func (c *Client) getUpdateHeaders() (map[string]string, error) {
- return c.getListHeaders()
-}
-
-func (c *Client) getActionHeaders() (map[string]string, error) {
- return c.getListHeaders()
-}
-
-func (c *Client) getAuthToken() (string, error) {
- var err error
-
- if c.token == nil {
- c.token, err = identity.GetToken(c.authority)
- if err != nil {
- return "", err
- }
- }
-
- return c.token.ID, err
-}
diff --git a/openstack/compute/v2/servers/data_test.go b/openstack/compute/v2/servers/data_test.go
new file mode 100644
index 0000000..d3a0ee0
--- /dev/null
+++ b/openstack/compute/v2/servers/data_test.go
@@ -0,0 +1,328 @@
+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/requests.go b/openstack/compute/v2/servers/requests.go
index 95818da..6c72a0a 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -2,123 +2,80 @@
import (
"fmt"
+
"github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
)
-// ListResult abstracts the raw results of making a List() request against the
-// API. As OpenStack extensions may freely alter the response bodies of
-// structures returned to the client, you may only safely access the data
-// provided through separate, type-safe accessors or methods.
-type ListResult map[string]interface{}
-
-// ServerResult abstracts a single server description,
-// as returned by the OpenStack provider.
-// As OpenStack extensions may freely alter the response bodies of the
-// structures returned to the client,
-// you may only safely access the data provided through
-// separate, type-safe accessors or methods.
-type ServerResult map[string]interface{}
-
// List makes a request against the API to list servers accessible to you.
-func List(c *Client) (ListResult, error) {
- var lr ListResult
-
- h, err := c.getListHeaders()
- if err != nil {
- return nil, err
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- err = perigee.Get(c.getListUrl(), perigee.Options{
- Results: &lr,
- MoreHeaders: h,
- })
- return lr, err
+ return pagination.NewPager(client, detailURL(client), createPage)
}
// Create requests a server to be provisioned to the user in the current tenant.
-func Create(c *Client, opts map[string]interface{}) (ServerResult, error) {
- var sr ServerResult
-
- h, err := c.getCreateHeaders()
- if err != nil {
- return nil, err
- }
-
- err = perigee.Post(c.getCreateUrl(), perigee.Options{
- Results: &sr,
+func Create(client *gophercloud.ServiceClient, opts map[string]interface{}) CreateResult {
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ Results: &result.Resp,
ReqBody: map[string]interface{}{
"server": opts,
},
- MoreHeaders: h,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
- return sr, err
+ return result
}
// Delete requests that a server previously provisioned be removed from your account.
-func Delete(c *Client, id string) error {
- h, err := c.getDeleteHeaders()
- if err != nil {
- return err
- }
-
- err = perigee.Delete(c.getDeleteUrl(id), perigee.Options{
- MoreHeaders: h,
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", serverURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
return err
}
-// GetDetail requests details on a single server, by ID.
-func GetDetail(c *Client, id string) (ServerResult, error) {
- var sr ServerResult
-
- h, err := c.getDetailHeaders()
- if err != nil {
- return nil, err
- }
-
- err = perigee.Get(c.getDetailUrl(id), perigee.Options{
- Results: &sr,
- MoreHeaders: h,
+// Get requests details on a single server, by ID.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", serverURL(client, id), perigee.Options{
+ Results: &result.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
})
- return sr, err
+ return result
}
// Update requests that various attributes of the indicated server be changed.
-func Update(c *Client, id string, opts map[string]interface{}) (ServerResult, error) {
- var sr ServerResult
-
- h, err := c.getUpdateHeaders()
- if err != nil {
- return nil, err
- }
-
- err = perigee.Put(c.getUpdateUrl(id), perigee.Options{
- Results: &sr,
+func Update(client *gophercloud.ServiceClient, id string, opts map[string]interface{}) UpdateResult {
+ var result UpdateResult
+ _, result.Err = perigee.Request("PUT", serverURL(client, id), perigee.Options{
+ Results: &result.Resp,
ReqBody: map[string]interface{}{
"server": opts,
},
- MoreHeaders: h,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
})
- return sr, err
+ return result
}
-// ChangeAdminPassword alters the administrator or root password for a specified
-// server.
-func ChangeAdminPassword(c *Client, id, newPassword string) error {
- h, err := c.getActionHeaders()
- if err != nil {
- return err
+// ChangeAdminPassword alters the administrator or root password for a specified server.
+func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) error {
+ var req struct {
+ ChangePassword struct {
+ AdminPass string `json:"adminPass"`
+ } `json:"changePassword"`
}
- err = perigee.Post(c.getActionUrl(id), perigee.Options{
- ReqBody: struct {
- C map[string]string `json:"changePassword"`
- }{
- map[string]string{"adminPass": newPassword},
- },
- MoreHeaders: h,
+ req.ChangePassword.AdminPass = newPassword
+
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
+ ReqBody: req,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
return err
@@ -150,28 +107,29 @@
return e.Error()
}
+// RebootMethod describes the mechanisms by which a server reboot can be requested.
+type RebootMethod string
+
// These constants determine how a server should be rebooted.
// See the Reboot() function for further details.
const (
- SoftReboot = "SOFT"
- HardReboot = "HARD"
- OSReboot = SoftReboot
- PowerCycle = HardReboot
+ SoftReboot RebootMethod = "SOFT"
+ HardReboot RebootMethod = "HARD"
+ OSReboot = SoftReboot
+ PowerCycle = HardReboot
)
// Reboot requests that a given server reboot.
// Two methods exist for rebooting a server:
//
-// 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 while, power is
-// restored or the VM instance restarted.
+// 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 while, power is restored or the VM instance restarted.
//
-// SoftReboot (aka OSReboot). This approach 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 wasking Windows to restart the
-// machine.
-func Reboot(c *Client, id, how string) error {
+// 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 RebootMethod) error {
if (how != SoftReboot) && (how != HardReboot) {
return &ErrArgument{
Function: "Reboot",
@@ -180,68 +138,64 @@
}
}
- h, err := c.getActionHeaders()
- if err != nil {
- return err
- }
-
- err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
ReqBody: struct {
C map[string]string `json:"reboot"`
}{
- map[string]string{"type": how},
+ map[string]string{"type": string(how)},
},
- MoreHeaders: h,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
return err
}
-// Rebuild requests that the Openstack provider reprovision the
-// server. The rebuild will need to know the server's name and
-// new image reference or ID. In addition, and unlike building
-// a server with Create(), you must provide an administrator
-// password.
+// Rebuild requests that the Openstack provider reprovision the server.
+// The rebuild will need to know the server's name and new image reference or ID.
+// In addition, and unlike building a server with Create(), you must provide an administrator password.
//
// Additional options may be specified with the additional map.
// This function treats a nil map the same as an empty map.
//
-// Rebuild returns a server result as though you had called
-// GetDetail() on the server's ID. The information, however,
-// refers to the new server, not the old.
-func Rebuild(c *Client, id, name, password, imageRef string, additional map[string]interface{}) (ServerResult, error) {
- var sr ServerResult
+// Rebuild returns a server result as though you had called GetDetail() on the server's ID.
+// The information, however, refers to the new server, not the old.
+func Rebuild(client *gophercloud.ServiceClient, id, name, password, imageRef string, additional map[string]interface{}) RebuildResult {
+ var result RebuildResult
if id == "" {
- return sr, &ErrArgument{
+ result.Err = &ErrArgument{
Function: "Rebuild",
Argument: "id",
Value: "",
}
+ return result
}
if name == "" {
- return sr, &ErrArgument{
+ result.Err = &ErrArgument{
Function: "Rebuild",
Argument: "name",
Value: "",
}
+ return result
}
if password == "" {
- return sr, &ErrArgument{
+ result.Err = &ErrArgument{
Function: "Rebuild",
Argument: "password",
Value: "",
}
+ return result
}
if imageRef == "" {
- return sr, &ErrArgument{
+ result.Err = &ErrArgument{
Function: "Rebuild",
Argument: "imageRef",
Value: "",
}
+ return result
}
if additional == nil {
@@ -252,43 +206,34 @@
additional["imageRef"] = imageRef
additional["adminPass"] = password
- h, err := c.getActionHeaders()
- if err != nil {
- return sr, err
- }
-
- err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ _, result.Err = perigee.Request("POST", actionURL(client, id), perigee.Options{
ReqBody: struct {
R map[string]interface{} `json:"rebuild"`
}{
additional,
},
- Results: &sr,
- MoreHeaders: h,
+ Results: &result.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
- return sr, err
+ return result
}
// Resize instructs the provider to change the flavor of the server.
-// Note that this implies rebuilding it. Unfortunately, one cannot pass rebuild parameters to the resize function.
+// Note that this implies rebuilding it.
+// Unfortunately, one cannot pass rebuild parameters to the resize function.
// When the resize completes, the server will be in RESIZE_VERIFY state.
// While in this state, you can explore the use of the new server's configuration.
// If you like it, call ConfirmResize() to commit the resize permanently.
// Otherwise, call RevertResize() to restore the old configuration.
-func Resize(c *Client, id, flavorRef string) error {
- h, err := c.getActionHeaders()
- if err != nil {
- return err
- }
-
- err = perigee.Post(c.getActionUrl(id), perigee.Options{
+func Resize(client *gophercloud.ServiceClient, id, flavorRef string) error {
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
ReqBody: struct {
R map[string]interface{} `json:"resize"`
}{
map[string]interface{}{"flavorRef": flavorRef},
},
- MoreHeaders: h,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
return err
@@ -296,15 +241,10 @@
// ConfirmResize confirms a previous resize operation on a server.
// See Resize() for more details.
-func ConfirmResize(c *Client, id string) error {
- h, err := c.getActionHeaders()
- if err != nil {
- return err
- }
-
- err = perigee.Post(c.getActionUrl(id), perigee.Options{
+func ConfirmResize(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
ReqBody: map[string]interface{}{"confirmResize": nil},
- MoreHeaders: h,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
return err
@@ -312,15 +252,10 @@
// RevertResize cancels a previous resize operation on a server.
// See Resize() for more details.
-func RevertResize(c *Client, id string) error {
- h, err := c.getActionHeaders()
- if err != nil {
- return err
- }
-
- err = perigee.Post(c.getActionUrl(id), perigee.Options{
+func RevertResize(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("POST", actionURL(client, id), perigee.Options{
ReqBody: map[string]interface{}{"revertResize": nil},
- MoreHeaders: h,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
return err
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
new file mode 100644
index 0000000..fa1a2f5
--- /dev/null
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -0,0 +1,296 @@
+package servers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "bzbzbzbzbz"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestListServers(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.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", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, serverListBody)
+ case "9e5476bd-a4ec-4653-93d6-72c93aa682ba":
+ fmt.Fprintf(w, `{ "servers": [] }`)
+ default:
+ t.Fatalf("/servers/detail invoked with unexpected marker=[%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ pages := 0
+ err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := ExtractServers(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 servers, got %d", len(actual))
+ }
+ equalServers(t, serverHerp, actual[0])
+ equalServers(t, serverDerp, actual[1])
+
+ return true, nil
+ })
+
+ if err != nil {
+ t.Fatalf("Unexpected error from EachPage: %v", err)
+ }
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestCreateServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", 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 := serviceClient()
+ actual, err := Create(client, map[string]interface{}{
+ "name": "derp",
+ "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
+ "flavorRef": "1",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Create error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestDeleteServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Delete(client, "asdfasdfasdf")
+ if err != nil {
+ t.Fatalf("Unexpected Delete error: %v", err)
+ }
+}
+
+func TestGetServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.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", tokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := serviceClient()
+ actual, err := Get(client, "1234asdf").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestUpdateServer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.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", tokenID)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "Content-Type", "application/json")
+ testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
+
+ fmt.Fprintf(w, singleServerBody)
+ })
+
+ client := serviceClient()
+ actual, err := Update(client, "1234asdf", map[string]interface{}{
+ "name": "new-name",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestChangeServerAdminPassword(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", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := serviceClient()
+ err := ChangeAdminPassword(client, "1234asdf", "new-password")
+ if err != nil {
+ t.Errorf("Unexpected ChangeAdminPassword error: %v", err)
+ }
+}
+
+func TestRebootServer(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", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := serviceClient()
+ err := Reboot(client, "1234asdf", SoftReboot)
+ if err != nil {
+ t.Errorf("Unexpected Reboot error: %v", 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", 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)
+ })
+
+ client := serviceClient()
+ actual, err := Rebuild(client,
+ "1234asdf", "new-name", "swordfish",
+ "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb",
+ map[string]interface{}{"accessIPv4": "1.2.3.4"},
+ ).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Rebuild error: %v", err)
+ }
+
+ equalServers(t, serverDerp, *actual)
+}
+
+func TestResizeServer(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", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := serviceClient()
+ err := Resize(client, "1234asdf", "2")
+ if err != nil {
+ t.Errorf("Unexpected Reboot error: %v", err)
+ }
+}
+
+func TestConfirmResize(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", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := ConfirmResize(client, "1234asdf")
+ if err != nil {
+ t.Errorf("Unexpected ConfirmResize error: %v", err)
+ }
+}
+
+func TestRevertResize(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", tokenID)
+ testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := serviceClient()
+ err := RevertResize(client, "1234asdf")
+ if err != nil {
+ t.Errorf("Unexpected RevertResize error: %v", err)
+ }
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
new file mode 100644
index 0000000..dd2e651
--- /dev/null
+++ b/openstack/compute/v2/servers/results.go
@@ -0,0 +1,155 @@
+package servers
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type serverResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets any serverResult as a Server, if possible.
+func (r serverResult) Extract() (*Server, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var response struct {
+ Server Server `mapstructure:"server"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &response)
+ return &response.Server, err
+}
+
+// CreateResult temporarily contains the response from a Create call.
+type CreateResult struct {
+ serverResult
+}
+
+// GetResult temporarily contains the response from a Get call.
+type GetResult struct {
+ serverResult
+}
+
+// UpdateResult temporarily contains the response from an Update call.
+type UpdateResult struct {
+ serverResult
+}
+
+// RebuildResult temporarily contains the response from a Rebuild call.
+type RebuildResult struct {
+ serverResult
+}
+
+// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
+type Server struct {
+ // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
+ ID string
+
+ // TenantID identifies the tenant owning this server resource.
+ TenantID string `mapstructure:"tenant_id"`
+
+ // UserID uniquely identifies the user account owning the tenant.
+ UserID string `mapstructure:"user_id"`
+
+ // Name contains the human-readable name for the server.
+ Name string
+
+ // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
+ Updated string
+ Created string
+
+ HostID string
+
+ // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
+ Status string
+
+ // Progress ranges from 0..100.
+ // A request made against the server completes only once Progress reaches 100.
+ Progress int
+
+ // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
+ AccessIPv4 string
+ AccessIPv6 string
+
+ // Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
+ Image map[string]interface{}
+
+ // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
+ Flavor map[string]interface{}
+
+ // Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
+ Addresses map[string]interface{}
+
+ // Metadata includes a list of all user-specified key-value pairs attached to the server.
+ Metadata map[string]interface{}
+
+ // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
+ Links []interface{}
+
+ // KeyName indicates which public key was injected into the server on launch.
+ KeyName string `mapstructure:"keyname"`
+
+ // 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"`
+}
+
+// ServerPage abstracts the raw results of making a List() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractServers call.
+type ServerPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (page ServerPage) IsEmpty() (bool, error) {
+ servers, err := ExtractServers(page)
+ if err != nil {
+ return true, err
+ }
+ return len(servers) == 0, nil
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (page ServerPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"servers_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities.
+func ExtractServers(page pagination.Page) ([]Server, error) {
+ casted := page.(ServerPage).Body
+
+ var response struct {
+ Servers []Server `mapstructure:"servers"`
+ }
+ err := mapstructure.Decode(casted, &response)
+ return response.Servers, err
+}
diff --git a/openstack/compute/v2/servers/servers.go b/openstack/compute/v2/servers/servers.go
deleted file mode 100644
index 28d66d0..0000000
--- a/openstack/compute/v2/servers/servers.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package servers
-
-import (
- "fmt"
- "github.com/mitchellh/mapstructure"
-)
-
-// ErrNotImplemented indicates a failure to discover a feature of the response from the API.
-// E.g., a missing server field, a missing extension, etc.
-var ErrNotImplemented = fmt.Errorf("Compute Servers feature not implemented.")
-
-// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account.
-//
-// Id uniquely identifies this server amongst all other servers, including those not accessible to the current tenant.
-//
-// TenantId identifies the tenant owning this server resource.
-//
-// UserId uniquely identifies the user account owning the tenant.
-//
-// Name contains the human-readable name for the server.
-//
-// Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created.
-//
-// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE.
-//
-// Progress ranges from 0..100. A request made against the server completes only once Progress reaches 100.
-//
-// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
-//
-// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
-//
-// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
-//
-// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
-//
-// Metadata includes a list of all user-specified key-value pairs attached to the server.
-//
-// Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference.
-//
-// 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.
-type Server struct {
- Id string
- TenantId string `mapstructure:tenant_id`
- UserId string `mapstructure:user_id`
- Name string
- Updated string
- Created string
- HostId string
- Status string
- Progress int
- AccessIPv4 string
- AccessIPv6 string
- Image map[string]interface{}
- Flavor map[string]interface{}
- Addresses map[string]interface{}
- Metadata map[string]interface{}
- Links []interface{}
- AdminPass string `mapstructure:adminPass`
-}
-
-// GetServers interprets the result of a List() call, producing a slice of Server entities.
-func GetServers(lr ListResult) ([]Server, error) {
- sa, ok := lr["servers"]
- if !ok {
- return nil, ErrNotImplemented
- }
- serversArray := sa.([]interface{})
-
- servers := make([]Server, len(serversArray))
- for i, so := range serversArray {
- serverObj := so.(map[string]interface{})
- err := mapstructure.Decode(serverObj, &servers[i])
- if err != nil {
- return servers, err
- }
- }
-
- return servers, nil
-}
-
-// GetServer interprets the result of a call expected to return data on a single server.
-func GetServer(sr ServerResult) (*Server, error) {
- so, ok := sr["server"]
- if !ok {
- return nil, ErrNotImplemented
- }
- serverObj := so.(map[string]interface{})
- s := new(Server)
- err := mapstructure.Decode(serverObj, s)
- return s, err
-}
diff --git a/openstack/compute/v2/servers/servers_test.go b/openstack/compute/v2/servers/servers_test.go
index b4a094c..590fc8b 100644
--- a/openstack/compute/v2/servers/servers_test.go
+++ b/openstack/compute/v2/servers/servers_test.go
@@ -1,188 +1,65 @@
package servers
import (
- "encoding/json"
+ "reflect"
"testing"
)
-// Taken from: http://docs.openstack.org/api/openstack-compute/2/content/List_Servers-d1e2078.html
-const goodListServersResult = `{
-"servers": [
-{
-"id": "52415800-8b69-11e0-9b19-734f6af67565",
-"tenant_id": "1234",
-"user_id": "5678",
-"name": "sample-server",
-"updated": "2010-10-10T12:00:00Z",
-"created": "2010-08-10T12:00:00Z",
-"hostId": "e4d909c290d0fb1ca068ffaddf22cbd0",
-"status": "BUILD",
-"progress": 60,
-"accessIPv4" : "67.23.10.132",
-"accessIPv6" : "::babe:67.23.10.132",
-"image" : {
-"id": "52415800-8b69-11e0-9b19-734f6f006e54",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f6f006e54"
-}
-]
-},
-"flavor" : {
-"id": "52415800-8b69-11e0-9b19-734f216543fd",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-}
-]
-},
-"addresses": {
-"public" : [
-{
-"version": 4,
-"addr": "67.23.10.132"
-},
-{
-"version": 6,
-"addr": "::babe:67.23.10.132"
-},
-{
-"version": 4,
-"addr": "67.23.10.131"
-},
-{
-"version": 6,
-"addr": "::babe:4317:0A83"
-}
-],
-"private" : [
-{
-"version": 4,
-"addr": "10.176.42.16"
-},
-{
-"version": 6,
-"addr": "::babe:10.176.42.16"
-}
-]
-},
-"metadata": {
-"Server Label": "Web Head 1",
-"Image Version": "2.1"
-},
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/servers/52415800-8b69-11e0-9b19-734f6af67565"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/servers/52415800-8b69-11e0-9b19-734f6af67565"
-}
-]
-},
-{
-"id": "52415800-8b69-11e0-9b19-734f1f1350e5",
-"user_id": "5678",
-"name": "sample-server2",
-"tenant_id": "1234",
-"updated": "2010-10-10T12:00:00Z",
-"created": "2010-08-10T12:00:00Z",
-"hostId": "9e107d9d372bb6826bd81d3542a419d6",
-"status": "ACTIVE",
-"accessIPv4" : "67.23.10.133",
-"accessIPv6" : "::babe:67.23.10.133",
-"image" : {
-"id": "52415800-8b69-11e0-9b19-734f5736d2a2",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2"
-}
-]
-},
-"flavor" : {
-"id": "52415800-8b69-11e0-9b19-734f216543fd",
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/flavors/52415800-8b69-11e0-9b19-734f216543fd"
-}
-]
-},
-"addresses": {
-"public" : [
-{
-"version": 4,
-"addr": "67.23.10.133"
-},
-{
-"version": 6,
-"addr": "::babe:67.23.10.133"
-}
-],
-"private" : [
-{
-"version": 4,
-"addr": "10.176.42.17"
-},
-{
-"version": 6,
-"addr": "::babe:10.176.42.17"
-}
-]
-},
-"metadata": {
-"Server Label": "DB 1"
-},
-"links": [
-{
-"rel": "self",
-"href": "http://servers.api.openstack.org/v2/1234/servers/52415800-8b69-11e0-9b19-734f1f1350e5"
-},
-{
-"rel": "bookmark",
-"href": "http://servers.api.openstack.org/1234/servers/52415800-8b69-11e0-9b19-734f1f1350e5"
-}
-]
-}
-]
-}`
-
-func TestGetServer(t *testing.T) {
- var listResults map[string]interface{}
- err := json.Unmarshal([]byte(goodListServersResult), &listResults)
- if err != nil {
- t.Error(err)
- return
+// 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)
}
-
- svrs, err := GetServers(listResults)
- if err != nil {
- t.Error(err)
- return
+ if expected.TenantID != actual.TenantID {
+ t.Errorf("TenantID differs. expected=[%s], actual=[%s]", expected.TenantID, actual.TenantID)
}
-
- if len(svrs) != 2 {
- t.Errorf("Expected 2 servers; got %d", len(svrs))
- return
+ 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/urls.go b/openstack/compute/v2/servers/urls.go
new file mode 100644
index 0000000..52be73e
--- /dev/null
+++ b/openstack/compute/v2/servers/urls.go
@@ -0,0 +1,19 @@
+package servers
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("servers")
+}
+
+func detailURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("servers", "detail")
+}
+
+func serverURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id)
+}
+
+func actionURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "action")
+}
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
index 71be65d..186d0fc 100644
--- a/openstack/identity/v3/endpoints/requests.go
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -111,7 +111,7 @@
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
- return EndpointPage{pagination.LinkedPageBase(r)}
+ return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
u := getListURL(client) + utils.BuildQuery(q)
diff --git a/openstack/identity/v3/services/requests.go b/openstack/identity/v3/services/requests.go
index fbd6a7c..405a9a6 100644
--- a/openstack/identity/v3/services/requests.go
+++ b/openstack/identity/v3/services/requests.go
@@ -57,7 +57,7 @@
u := getListURL(client) + utils.BuildQuery(q)
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
- return ServicePage{pagination.LinkedPageBase(r)}
+ return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
return pagination.NewPager(client, u, createPage)
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index 0d74eb5..7fed58e 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -73,7 +73,7 @@
u := listURL(c) + utils.BuildQuery(q)
return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
- return NetworkPage{pagination.LinkedPageBase(r)}
+ return NetworkPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 3b7e605..0c4ae74 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -97,7 +97,7 @@
u := listURL(c) + utils.BuildQuery(q)
return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
- return PortPage{pagination.LinkedPageBase(r)}
+ return PortPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
}
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 9cbf5ab..591af24 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -78,7 +78,7 @@
u := listURL(c) + utils.BuildQuery(q)
return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
- return SubnetPage{pagination.LinkedPageBase(r)}
+ return SubnetPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
}
diff --git a/pagination/linked.go b/pagination/linked.go
index fc88a55..0376edb 100644
--- a/pagination/linked.go
+++ b/pagination/linked.go
@@ -1,29 +1,63 @@
package pagination
-import "github.com/mitchellh/mapstructure"
+import "fmt"
// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result.
-type LinkedPageBase LastHTTPResponse
+type LinkedPageBase struct {
+ LastHTTPResponse
+
+ // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer.
+ // If any link along the path is missing, an empty URL will be returned.
+ // If any link results in an unexpected value type, an error will be returned.
+ // When left as "nil", []string{"links", "next"} will be used as a default.
+ LinkPath []string
+}
// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present.
// It assumes that the links are available in a "links" element of the top-level response object.
// If this is not the case, override NextPageURL on your result type.
func (current LinkedPageBase) NextPageURL() (string, error) {
- type response struct {
- Links struct {
- Next *string `mapstructure:"next,omitempty"`
- } `mapstructure:"links"`
+ var path []string
+ var key string
+
+ if current.LinkPath == nil {
+ path = []string{"links", "next"}
+ } else {
+ path = current.LinkPath
}
- var r response
- err := mapstructure.Decode(current.Body, &r)
- if err != nil {
- return "", err
+ submap, ok := current.Body.(map[string]interface{})
+ if !ok {
+ return "", fmt.Errorf("Expected an object, but was %#v", current.Body)
}
- if r.Links.Next == nil {
- return "", nil
- }
+ for {
+ key, path = path[0], path[1:len(path)]
- return *r.Links.Next, nil
+ value, ok := submap[key]
+ if !ok {
+ return "", nil
+ }
+
+ fmt.Printf("key = %#v, path = %#v, value = %#v\n", key, path, value)
+
+ if len(path) > 0 {
+ submap, ok = value.(map[string]interface{})
+ if !ok {
+ return "", fmt.Errorf("Expected an object, but was %#v", value)
+ }
+ } else {
+ if value == nil {
+ // Actual null element.
+ return "", nil
+ }
+
+ url, ok := value.(string)
+ if !ok {
+ return "", fmt.Errorf("Expected a string, but was %#v", value)
+ }
+
+ return url, nil
+ }
+ }
}
diff --git a/pagination/linked_test.go b/pagination/linked_test.go
index 2093755..2621f98 100644
--- a/pagination/linked_test.go
+++ b/pagination/linked_test.go
@@ -58,7 +58,7 @@
client := createClient()
createPage := func(r LastHTTPResponse) Page {
- return LinkedPageResult{LinkedPageBase(r)}
+ return LinkedPageResult{LinkedPageBase{LastHTTPResponse: r}}
}
return NewPager(client, testhelper.Server.URL+"/page1", createPage)