Merge branch 'master' into v0.2.0
Conflicts:
acceptance/19-list-addresses-0.1.go
servers.go
I really need to do this more often.
diff --git a/.travis.yml b/.travis.yml
index 6e1dbd0..b7f8d34 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,8 @@
language: go
install:
- go get -v .
+ - go get -v ./openstack/...
+ - go get -v ./rackspace/...
go:
- 1.1
- 1.2
diff --git a/acceptance/openstack/compute_test.go b/acceptance/openstack/compute_test.go
new file mode 100644
index 0000000..f03c0f9
--- /dev/null
+++ b/acceptance/openstack/compute_test.go
@@ -0,0 +1,368 @@
+// +build acceptance
+
+package openstack
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/compute/flavors"
+ "github.com/rackspace/gophercloud/openstack/compute/images"
+ "github.com/rackspace/gophercloud/openstack/compute/servers"
+ "os"
+ "testing"
+)
+
+var service = "compute"
+
+func TestListServers(t *testing.T) {
+ ts, err := 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 := 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 := 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 := 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 := setupForCRUD()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ err = 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 = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func TestUpdateServer(t *testing.T) {
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ defer func() {
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ err = changeServerName(ts)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+}
+
+func TestActionChangeAdminPassword(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = changeAdminPassword(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionReboot(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = 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 = rebootServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionRebuild(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = rebuildServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeConfirm(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = resizeServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = confirmResize(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestActionResizeRevert(t *testing.T) {
+ t.Parallel()
+
+ ts, err := setupForCRUD()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = createServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func(){
+ servers.Delete(ts.client, ts.createdServer.Id)
+ }()
+
+ err = waitForStatus(ts, "ACTIVE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = resizeServer(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = revertResize(ts)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/acceptance/openstack/identity_test.go b/acceptance/openstack/identity_test.go
new file mode 100644
index 0000000..02f86ad
--- /dev/null
+++ b/acceptance/openstack/identity_test.go
@@ -0,0 +1,110 @@
+// +build acceptance
+
+package openstack
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "os"
+ "testing"
+ "text/tabwriter"
+)
+
+type extractor func(*identity.Token) string
+
+func TestAuthentication(t *testing.T) {
+ // Create an initialized set of authentication options based on available OS_*
+ // environment variables.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Attempt to authenticate with them.
+ r, err := identity.Authenticate(ao)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // We're authenticated; now let's grab our authentication token.
+ tok, err := identity.GetToken(r)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Authentication tokens have a variety of fields which might be of some interest.
+ // Let's print a few of them out.
+ table := map[string]extractor{
+ "ID": func(t *identity.Token) string { return tok.Id },
+ "Expires": func(t *identity.Token) string { return tok.Expires },
+ }
+
+ for attr, fn := range table {
+ fmt.Printf("Your token's %s is %s\n", attr, fn(tok))
+ }
+
+ // With each authentication, you receive a master directory of all the services
+ // your account can access. This "service catalog", as OpenStack calls it,
+ // provides you the means to exploit other OpenStack services.
+ sc, err := identity.GetServiceCatalog(r)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Prepare our elastic tabstopped writer for our table.
+ w := new(tabwriter.Writer)
+ w.Init(os.Stdout, 2, 8, 2, ' ', 0)
+
+ // Different providers will provide different services. Let's print them
+ // in summary.
+ ces, err := sc.CatalogEntries()
+ fmt.Println("Service Catalog Summary:")
+ fmt.Fprintln(w, "Name\tType\t")
+ for _, ce := range ces {
+ fmt.Fprintf(w, "%s\t%s\t\n", ce.Name, ce.Type)
+ }
+ w.Flush()
+
+ // Now let's print them in greater detail.
+ for _, ce := range ces {
+ fmt.Printf("Endpoints for %s/%s\n", ce.Name, ce.Type)
+ fmt.Fprintln(w, "Version\tRegion\tTenant\tPublic URL\tInternal URL\t")
+ for _, ep := range ce.Endpoints {
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t\n", ep.VersionId, ep.Region, ep.TenantId, ep.PublicURL, ep.InternalURL)
+ }
+ w.Flush()
+ }
+}
+
+func TestExtensions(t *testing.T) {
+ // Create an initialized set of authentication options based on available OS_*
+ // environment variables.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Attempt to query extensions.
+ exts, err := identity.GetExtensions(ao)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // Print out a summary of supported extensions
+ aliases, err := exts.Aliases()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ fmt.Println("Extension Aliases:")
+ for _, alias := range aliases {
+ fmt.Printf(" %s\n", alias)
+ }
+}
diff --git a/acceptance/openstack/pkg.go b/acceptance/openstack/pkg.go
new file mode 100644
index 0000000..3a8ecdb
--- /dev/null
+++ b/acceptance/openstack/pkg.go
@@ -0,0 +1,4 @@
+// +build acceptance
+
+package openstack
+
diff --git a/acceptance/openstack/storage_test.go b/acceptance/openstack/storage_test.go
new file mode 100644
index 0000000..d194936
--- /dev/null
+++ b/acceptance/openstack/storage_test.go
@@ -0,0 +1,361 @@
+// +build acceptance
+
+package openstack
+
+import (
+ "bytes"
+ storage "github.com/rackspace/gophercloud/openstack/storage/v1"
+ "github.com/rackspace/gophercloud/openstack/storage/v1/accounts"
+ "github.com/rackspace/gophercloud/openstack/storage/v1/containers"
+ "github.com/rackspace/gophercloud/openstack/storage/v1/objects"
+ "os"
+ "strings"
+ "testing"
+)
+
+var objectStorage = "object-store"
+var metadata = map[string]string{"gopher": "cloud"}
+var numContainers = 2
+var numObjects = 2
+
+func TestAccount(t *testing.T) {
+ ts, err := setupForList(objectStorage)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ region := os.Getenv("OS_REGION_NAME")
+ for _, ep := range ts.eps {
+ if (region != "") && (region != ep.Region) {
+ continue
+ }
+
+ client := storage.NewClient(ep.PublicURL, ts.a, ts.o)
+
+ err := accounts.Update(client, accounts.UpdateOpts{
+ Metadata: metadata,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ err = accounts.Update(client, accounts.UpdateOpts{
+ Metadata: tempMap,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ gr, err := accounts.Get(client, accounts.GetOpts{})
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ am := accounts.ExtractMetadata(gr)
+ for k := range metadata {
+ if am[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+ }
+}
+
+func TestContainers(t *testing.T) {
+ ts, err := setupForList(objectStorage)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ region := os.Getenv("OS_REGION_NAME")
+ for _, ep := range ts.eps {
+ if (region != "") && (region != ep.Region) {
+ continue
+ }
+
+ client := storage.NewClient(ep.PublicURL, ts.a, ts.o)
+
+ cNames := make([]string, numContainers)
+ for i := 0; i < numContainers; i++ {
+ cNames[i] = randomString("test-container-", 8)
+ }
+
+ for i := 0; i < len(cNames); i++ {
+ _, err := containers.Create(client, containers.CreateOpts{
+ Name: cNames[i],
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }
+ defer func() {
+ for i := 0; i < len(cNames); i++ {
+ err = containers.Delete(client, containers.DeleteOpts{
+ Name: cNames[i],
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }
+ }()
+
+ lr, err := containers.List(client, containers.ListOpts{
+ Full: false,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ cns, err := containers.ExtractNames(lr)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(cns) != len(cNames) {
+ t.Errorf("Expected %d names and got %d", len(cNames), len(cns))
+ return
+ }
+
+ lr, err = containers.List(client, containers.ListOpts{
+ Full: true,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ cis, err := containers.ExtractInfo(lr)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(cis) != len(cNames) {
+ t.Errorf("Expected %d containers and got %d", len(cNames), len(cis))
+ return
+ }
+
+ err = containers.Update(client, containers.UpdateOpts{
+ Name: cNames[0],
+ Metadata: metadata,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ err = containers.Update(client, containers.UpdateOpts{
+ Name: cNames[0],
+ Metadata: tempMap,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ gr, err := containers.Get(client, containers.GetOpts{})
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ cm := containers.ExtractMetadata(gr)
+ for k := range metadata {
+ if cm[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+ }
+}
+
+func TestObjects(t *testing.T) {
+ ts, err := setupForList(objectStorage)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ region := os.Getenv("OS_REGION_NAME")
+
+ for _, ep := range ts.eps {
+ if (region != "") && (region != ep.Region) {
+ continue
+ }
+
+ client := storage.NewClient(ep.PublicURL, ts.a, ts.o)
+
+ oNames := make([]string, numObjects)
+ for i := 0; i < len(oNames); i++ {
+ oNames[i] = randomString("test-object-", 8)
+ }
+
+ cName := randomString("test-container-", 8)
+ _, err := containers.Create(client, containers.CreateOpts{
+ Name: cName,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ err = containers.Delete(client, containers.DeleteOpts{
+ Name: cName,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ oContents := make([]*bytes.Buffer, numObjects)
+ for i := 0; i < numObjects; i++ {
+ oContents[i] = bytes.NewBuffer([]byte(randomString("", 10)))
+ err = objects.Create(client, objects.CreateOpts{
+ Container: cName,
+ Name: oNames[i],
+ Content: oContents[i],
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }
+ defer func() {
+ for i := 0; i < numObjects; i++ {
+ err = objects.Delete(client, objects.DeleteOpts{
+ Container: cName,
+ Name: oNames[i],
+ })
+ }
+ }()
+
+ lr, err := objects.List(client, objects.ListOpts{
+ Full: false,
+ Container: cName,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ ons, err := objects.ExtractNames(lr)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(ons) != len(oNames) {
+ t.Errorf("Expected %d names and got %d", len(oNames), len(ons))
+ return
+ }
+
+ lr, err = objects.List(client, objects.ListOpts{
+ Full: true,
+ Container: cName,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ ois, err := objects.ExtractInfo(lr)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(ois) != len(oNames) {
+ t.Errorf("Expected %d containers and got %d", len(oNames), len(ois))
+ return
+ }
+
+ err = objects.Copy(client, objects.CopyOpts{
+ Container: cName,
+ Name: oNames[0],
+ NewContainer: cName,
+ NewName: oNames[1],
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ dr, err := objects.Download(client, objects.DownloadOpts{
+ Container: cName,
+ Name: oNames[1],
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ o2Content, err := objects.ExtractContent(dr)
+ if err != nil {
+ t.Error(err)
+ }
+ dr, err = objects.Download(client, objects.DownloadOpts{
+ Container: cName,
+ Name: oNames[0],
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ o1Content, err := objects.ExtractContent(dr)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if string(o2Content) != string(o1Content) {
+ t.Errorf("Copy failed. Expected\n%s\nand got\n%s", string(o1Content), string(o2Content))
+ return
+ }
+
+ err = objects.Update(client, objects.UpdateOpts{
+ Container: cName,
+ Name: oNames[0],
+ Metadata: metadata,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ tempMap := make(map[string]string)
+ for k := range metadata {
+ tempMap[k] = ""
+ }
+ err = objects.Update(client, objects.UpdateOpts{
+ Container: cName,
+ Name: oNames[0],
+ Metadata: tempMap,
+ })
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ gr, err := objects.Get(client, objects.GetOpts{})
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ om := objects.ExtractMetadata(gr)
+ for k := range metadata {
+ if om[k] != metadata[strings.Title(k)] {
+ t.Errorf("Expected custom metadata with key: %s", k)
+ return
+ }
+ }
+ }
+}
diff --git a/acceptance/openstack/tools_test.go b/acceptance/openstack/tools_test.go
new file mode 100644
index 0000000..042ab34
--- /dev/null
+++ b/acceptance/openstack/tools_test.go
@@ -0,0 +1,364 @@
+// +build acceptance
+
+package openstack
+
+import (
+ "crypto/rand"
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/compute/servers"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "os"
+ "text/tabwriter"
+ "time"
+)
+
+var errTimeout = fmt.Errorf("Timeout.")
+
+type testState struct {
+ o identity.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
+ }
+
+ ts.a, err = identity.Authenticate(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/rackspace/monitoring/pkg.go b/acceptance/rackspace/monitoring/pkg.go
new file mode 100644
index 0000000..de6924d
--- /dev/null
+++ b/acceptance/rackspace/monitoring/pkg.go
@@ -0,0 +1,4 @@
+// +build acceptance
+
+package monitoring
+
diff --git a/acceptance/rackspace/monitoring/rbac_test.go b/acceptance/rackspace/monitoring/rbac_test.go
new file mode 100644
index 0000000..a6d1d54
--- /dev/null
+++ b/acceptance/rackspace/monitoring/rbac_test.go
@@ -0,0 +1,67 @@
+// +build acceptance
+
+package monitoring
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/rackspace/monitoring"
+ "github.com/rackspace/gophercloud/rackspace/monitoring/notificationPlans"
+ "testing"
+)
+
+func TestRBACPermissions(t *testing.T) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ao.AllowReauth = true
+ r, err := identity.Authenticate(ao)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Find the cloud monitoring API
+
+ sc, err := identity.GetServiceCatalog(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ces, err := sc.CatalogEntries()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ monUrl, err := findMonitoringEndpoint(ces)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Build ourselves an interface to cloud monitoring!
+
+ np := notificationPlans.NewClient(monitoring.Options{
+ Endpoint: monUrl,
+ AuthOptions: ao,
+ Authentication: r,
+ })
+
+ // Try to delete a bogus notification plan
+
+ dr, err := np.Delete("ajkhdlkajhdflkajshdf")
+ if err != nil {
+ fmt.Printf("%#v\n", err)
+ }
+ fmt.Printf("%#v\n", dr)
+}
+
+func findMonitoringEndpoint(ces []identity.CatalogEntry) (string, error) {
+ for _, ce := range ces {
+ if ce.Type == "rax:monitor" {
+ return ce.Endpoints[0].PublicURL, nil
+ }
+ }
+ return "", fmt.Errorf("No monitoring API in the service catalog")
+}
diff --git a/api_fetch.go b/api_fetch.go
index 5cfa2da..353df75 100644
--- a/api_fetch.go
+++ b/api_fetch.go
@@ -1,7 +1,7 @@
package gophercloud
-import(
- "github.com/mitchellh/mapstructure"
+import (
+ "github.com/mitchellh/mapstructure"
)
//The default generic openstack api
@@ -17,10 +17,9 @@
"UrlChoice": PublicURL,
}
-
//Populates an ApiCriteria struct with the api values
-//from one of the api maps
-func PopulateApi(variant string) (ApiCriteria, error){
+//from one of the api maps
+func PopulateApi(variant string) (ApiCriteria, error) {
var Api ApiCriteria
var variantMap map[string]interface{}
@@ -31,13 +30,13 @@
case "openstack":
variantMap = OpenstackApi
- case "rackspace":
+ case "rackspace":
variantMap = RackspaceApi
}
- err := mapstructure.Decode(variantMap,&Api)
- if err != nil{
- return Api,err
- }
- return Api, err
+ err := mapstructure.Decode(variantMap, &Api)
+ if err != nil {
+ return Api, err
+ }
+ return Api, err
}
diff --git a/openstack/compute/flavors/client.go b/openstack/compute/flavors/client.go
new file mode 100644
index 0000000..2c64908
--- /dev/null
+++ b/openstack/compute/flavors/client.go
@@ -0,0 +1,61 @@
+package flavors
+
+import (
+ "fmt"
+ "net/url"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "strconv"
+)
+
+type Client struct {
+ endpoint string
+ authority identity.AuthResults
+ options identity.AuthOptions
+}
+
+func NewClient(e string, a identity.AuthResults, ao identity.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/flavors/flavors.go b/openstack/compute/flavors/flavors.go
new file mode 100644
index 0000000..146bcc4
--- /dev/null
+++ b/openstack/compute/flavors/flavors.go
@@ -0,0 +1,83 @@
+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/flavors/requests.go b/openstack/compute/flavors/requests.go
new file mode 100644
index 0000000..3758206
--- /dev/null
+++ b/openstack/compute/flavors/requests.go
@@ -0,0 +1,58 @@
+package flavors
+
+import (
+ "fmt"
+ "github.com/racker/perigee"
+)
+
+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 string
+ MinDisk, MinRam int
+ Marker string
+ 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
+ }
+
+ err = perigee.Get(c.getListUrl(lfo), perigee.Options{
+ Results: &lr,
+ MoreHeaders: h,
+ })
+ return lr, err
+}
+
+// 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,
+ })
+ return gr, err
+}
diff --git a/openstack/compute/images/client.go b/openstack/compute/images/client.go
new file mode 100644
index 0000000..c5f1897
--- /dev/null
+++ b/openstack/compute/images/client.go
@@ -0,0 +1,34 @@
+package images
+
+import (
+ "github.com/rackspace/gophercloud/openstack/identity"
+)
+
+type Client struct {
+ endpoint string
+ authority identity.AuthResults
+ options identity.AuthOptions
+}
+
+func NewClient(e string, a identity.AuthResults, ao identity.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/images/images.go b/openstack/compute/images/images.go
new file mode 100644
index 0000000..c881092
--- /dev/null
+++ b/openstack/compute/images/images.go
@@ -0,0 +1,52 @@
+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/images/images_test.go b/openstack/compute/images/images_test.go
new file mode 100644
index 0000000..ee3b79e
--- /dev/null
+++ b/openstack/compute/images/images_test.go
@@ -0,0 +1,43 @@
+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/images/requests.go b/openstack/compute/images/requests.go
new file mode 100644
index 0000000..79783c4
--- /dev/null
+++ b/openstack/compute/images/requests.go
@@ -0,0 +1,26 @@
+package images
+
+import (
+ "fmt"
+ "github.com/racker/perigee"
+)
+
+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
+ }
+
+ err = perigee.Get(c.getListUrl(), perigee.Options{
+ Results: &lr,
+ MoreHeaders: h,
+ })
+ return lr, err
+}
diff --git a/openstack/compute/servers/client.go b/openstack/compute/servers/client.go
new file mode 100644
index 0000000..3ce6965
--- /dev/null
+++ b/openstack/compute/servers/client.go
@@ -0,0 +1,91 @@
+package servers
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+)
+
+// Client abstracts the connection information needed to make API requests for OpenStack compute endpoints.
+type Client struct {
+ endpoint string
+ authority identity.AuthResults
+ options identity.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 identity.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/servers/doc.go b/openstack/compute/servers/doc.go
new file mode 100644
index 0000000..77a7514
--- /dev/null
+++ b/openstack/compute/servers/doc.go
@@ -0,0 +1,2 @@
+// The servers package provides convenient access to standard, OpenStack-defined compute services.
+package servers
diff --git a/openstack/compute/servers/requests.go b/openstack/compute/servers/requests.go
new file mode 100644
index 0000000..95818da
--- /dev/null
+++ b/openstack/compute/servers/requests.go
@@ -0,0 +1,327 @@
+package servers
+
+import (
+ "fmt"
+ "github.com/racker/perigee"
+)
+
+// 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
+ }
+
+ err = perigee.Get(c.getListUrl(), perigee.Options{
+ Results: &lr,
+ MoreHeaders: h,
+ })
+ return lr, err
+}
+
+// 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,
+ ReqBody: map[string]interface{}{
+ "server": opts,
+ },
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return sr, err
+}
+
+// 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,
+ 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,
+ })
+ return sr, err
+}
+
+// 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,
+ ReqBody: map[string]interface{}{
+ "server": opts,
+ },
+ MoreHeaders: h,
+ })
+ return sr, err
+}
+
+// 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
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct {
+ C map[string]string `json:"changePassword"`
+ }{
+ map[string]string{"adminPass": newPassword},
+ },
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// ErrArgument errors occur when an argument supplied to a package function
+// fails to fall within acceptable values. For example, the Reboot() function
+// expects the "how" parameter to be one of HardReboot or SoftReboot. These
+// constants are (currently) strings, leading someone to wonder if they can pass
+// other string values instead, perhaps in an effort to break the API of their
+// provider. Reboot() returns this error in this situation.
+//
+// Function identifies which function was called/which function is generating
+// the error.
+// Argument identifies which formal argument was responsible for producing the
+// error.
+// Value provides the value as it was passed into the function.
+type ErrArgument struct {
+ Function, Argument string
+ Value interface{}
+}
+
+// Error yields a useful diagnostic for debugging purposes.
+func (e *ErrArgument) Error() string {
+ return fmt.Sprintf("Bad argument in call to %s, formal parameter %s, value %#v", e.Function, e.Argument, e.Value)
+}
+
+func (e *ErrArgument) String() string {
+ return e.Error()
+}
+
+// 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
+)
+
+// 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.
+//
+// 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 {
+ if (how != SoftReboot) && (how != HardReboot) {
+ return &ErrArgument{
+ Function: "Reboot",
+ Argument: "how",
+ Value: how,
+ }
+ }
+
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct {
+ C map[string]string `json:"reboot"`
+ }{
+ map[string]string{"type": how},
+ },
+ MoreHeaders: h,
+ 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.
+//
+// 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
+
+ if id == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "id",
+ Value: "",
+ }
+ }
+
+ if name == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "name",
+ Value: "",
+ }
+ }
+
+ if password == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "password",
+ Value: "",
+ }
+ }
+
+ if imageRef == "" {
+ return sr, &ErrArgument{
+ Function: "Rebuild",
+ Argument: "imageRef",
+ Value: "",
+ }
+ }
+
+ if additional == nil {
+ additional = make(map[string]interface{}, 0)
+ }
+
+ additional["name"] = name
+ additional["imageRef"] = imageRef
+ additional["adminPass"] = password
+
+ h, err := c.getActionHeaders()
+ if err != nil {
+ return sr, err
+ }
+
+ err = perigee.Post(c.getActionUrl(id), perigee.Options{
+ ReqBody: struct {
+ R map[string]interface{} `json:"rebuild"`
+ }{
+ additional,
+ },
+ Results: &sr,
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return sr, err
+}
+
+// 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.
+// 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{
+ ReqBody: struct {
+ R map[string]interface{} `json:"resize"`
+ }{
+ map[string]interface{}{"flavorRef": flavorRef},
+ },
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// 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{
+ ReqBody: map[string]interface{}{"confirmResize": nil},
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// 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{
+ ReqBody: map[string]interface{}{"revertResize": nil},
+ MoreHeaders: h,
+ OkCodes: []int{202},
+ })
+ return err
+}
diff --git a/openstack/compute/servers/servers.go b/openstack/compute/servers/servers.go
new file mode 100644
index 0000000..28d66d0
--- /dev/null
+++ b/openstack/compute/servers/servers.go
@@ -0,0 +1,92 @@
+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/servers/servers_test.go b/openstack/compute/servers/servers_test.go
new file mode 100644
index 0000000..b4a094c
--- /dev/null
+++ b/openstack/compute/servers/servers_test.go
@@ -0,0 +1,188 @@
+package servers
+
+import (
+ "encoding/json"
+ "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
+ }
+
+ svrs, err := GetServers(listResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if len(svrs) != 2 {
+ t.Errorf("Expected 2 servers; got %d", len(svrs))
+ return
+ }
+}
diff --git a/openstack/identity/common_test.go b/openstack/identity/common_test.go
new file mode 100644
index 0000000..95a2ccb
--- /dev/null
+++ b/openstack/identity/common_test.go
@@ -0,0 +1,174 @@
+package identity
+
+// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
+const authResultsOK = `{
+ "access":{
+ "token":{
+ "id": "ab48a9efdfedb23ty3494",
+ "expires": "2010-11-01T03:32:15-05:00",
+ "tenant":{
+ "id": "t1000",
+ "name": "My Project"
+ }
+ },
+ "user":{
+ "id": "u123",
+ "name": "jqsmith",
+ "roles":[{
+ "id": "100",
+ "name": "compute:admin"
+ },
+ {
+ "id": "101",
+ "name": "object-store:admin",
+ "tenantId": "t1000"
+ }
+ ],
+ "roles_links":[]
+ },
+ "serviceCatalog":[{
+ "name": "Cloud Servers",
+ "type": "compute",
+ "endpoints":[{
+ "tenantId": "t1000",
+ "publicURL": "https://compute.north.host.com/v1/t1000",
+ "internalURL": "https://compute.north.internal/v1/t1000",
+ "region": "North",
+ "versionId": "1",
+ "versionInfo": "https://compute.north.host.com/v1/",
+ "versionList": "https://compute.north.host.com/"
+ },
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://compute.north.host.com/v1.1/t1000",
+ "internalURL": "https://compute.north.internal/v1.1/t1000",
+ "region": "North",
+ "versionId": "1.1",
+ "versionInfo": "https://compute.north.host.com/v1.1/",
+ "versionList": "https://compute.north.host.com/"
+ }
+ ],
+ "endpoints_links":[]
+ },
+ {
+ "name": "Cloud Files",
+ "type": "object-store",
+ "endpoints":[{
+ "tenantId": "t1000",
+ "publicURL": "https://storage.north.host.com/v1/t1000",
+ "internalURL": "https://storage.north.internal/v1/t1000",
+ "region": "North",
+ "versionId": "1",
+ "versionInfo": "https://storage.north.host.com/v1/",
+ "versionList": "https://storage.north.host.com/"
+ },
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://storage.south.host.com/v1/t1000",
+ "internalURL": "https://storage.south.internal/v1/t1000",
+ "region": "South",
+ "versionId": "1",
+ "versionInfo": "https://storage.south.host.com/v1/",
+ "versionList": "https://storage.south.host.com/"
+ }
+ ]
+ },
+ {
+ "name": "DNS-as-a-Service",
+ "type": "dnsextension:dns",
+ "endpoints":[{
+ "tenantId": "t1000",
+ "publicURL": "https://dns.host.com/v2.0/t1000",
+ "versionId": "2.0",
+ "versionInfo": "https://dns.host.com/v2.0/",
+ "versionList": "https://dns.host.com/"
+ }
+ ]
+ }
+ ]
+ }
+}`
+
+// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listExtensions_v2.0_extensions_.html#GET_listExtensions_v2.0_extensions_-Request
+const queryResults = `{
+ "extensions":[{
+ "name": "Reset Password Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
+ "alias": "RS-RPE",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
+ }
+ ]
+ },
+ {
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "alias": "RS-META",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
+ }
+ ]
+ }
+ ],
+ "extensions_links":[]
+}`
+
+// Same as queryResults above, but with a bogus JSON envelop.
+const bogusExtensionsResults = `{
+ "explosions":[{
+ "name": "Reset Password Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
+ "alias": "RS-RPE",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
+ }
+ ]
+ },
+ {
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "alias": "RS-META",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
+ }
+ ]
+ }
+ ],
+ "extensions_links":[]
+}`
diff --git a/openstack/identity/doc.go b/openstack/identity/doc.go
new file mode 100644
index 0000000..081950d
--- /dev/null
+++ b/openstack/identity/doc.go
@@ -0,0 +1,120 @@
+/*
+The Identity package provides convenient OpenStack Identity V2 API client access.
+This package currently doesn't support the administrative access endpoints, but may appear in the future based on demand.
+
+Authentication
+
+Established convention in the OpenStack community suggests the use of environment variables to hold authentication parameters.
+For example, the following settings would be sufficient to authenticate against Rackspace:
+
+ # assumes Bash shell on a POSIX environment; use SET command for Windows.
+ export OS_AUTH_URL=https://identity.api.rackspacecloud.com/v2.0
+ export OS_USERNAME=xxxx
+ export OS_PASSWORD=yyyy
+
+while you'd need these additional settings to authenticate against, e.g., Nebula One:
+
+ export OS_TENANT_ID=zzzz
+ export OS_TENANT_NAME=wwww
+
+Be sure to consult with your provider to see which settings you'll need to authenticate with.
+
+A skeletal client gets started with Gophercloud by authenticating against his/her provider, like so:
+
+ package main
+
+ import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ )
+
+ func main() {
+ // Create an initialized set of authentication options based on available OS_*
+ // environment variables.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ panic(err)
+ }
+
+ // Attempt to authenticate with them.
+ r, err := identity.Authenticate(ao)
+ if err != nil {
+ panic(err)
+ }
+
+ // With each authentication, you receive a master directory of all the services
+ // your account can access. This "service catalog", as OpenStack calls it,
+ // provides you the means to exploit other OpenStack services.
+ sc, err := identity.GetServiceCatalog(r)
+ if err != nil {
+ panic(err)
+ }
+
+ // Find the desired service(s) for our application.
+ computeService, err := findService(sc, "compute", ...)
+ if err != nil {
+ panic(err)
+ }
+
+ blockStorage, err := findService(sc, "block-storage", ...)
+ if err != nil {
+ panic(err)
+ }
+
+ // ... etc ...
+ }
+
+NOTE!
+Unlike versions 0.1.x of the Gophercloud API,
+0.2.0 and later will not provide a service look-up mechanism as a built-in feature of the Identity SDK binding.
+The 0.1.x behavior potentially opened its non-US users to legal liability by potentially selecting endpoints in undesirable regions
+in a non-obvious manner if a specific region was not explicitly specified.
+Starting with 0.2.0 and beyond, you'll need to use either your own service catalog query function or one in a separate package.
+This makes it plainly visible to a code auditor that if you indeed desired automatic selection of an arbitrary region,
+you made the conscious choice to use that feature.
+
+Extensions
+
+Some OpenStack deployments may support features that other deployments do not.
+Anything beyond the scope of standard OpenStack must be scoped by an "extension," a named, yet well-known, change to the API.
+Users may invoke IsExtensionAvailable() after grabbing a list of extensions from the server with GetExtensions().
+This of course assumes you know the name of the extension ahead of time.
+
+Here's a simple example of listing all the aliases for supported extensions.
+Once you have an alias to an extension, everything else about it may be queried through accessors.
+
+ package main
+
+ import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ )
+
+ func main() {
+ // Create an initialized set of authentication options based on available OS_*
+ // environment variables.
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ panic(err)
+ }
+
+ // Attempt to query extensions.
+ exts, err := identity.GetExtensions(ao)
+ if err != nil {
+ panic(err)
+ }
+
+ // Print out a summary of supported extensions
+ aliases, err := exts.Aliases()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("Extension Aliases:")
+ for _, alias := range aliases {
+ fmt.Printf(" %s\n", alias)
+ }
+ }
+*/
+package identity
diff --git a/openstack/identity/errors.go b/openstack/identity/errors.go
new file mode 100644
index 0000000..efa7c85
--- /dev/null
+++ b/openstack/identity/errors.go
@@ -0,0 +1,17 @@
+package identity
+
+import "fmt"
+
+// ErrNotImplemented errors may occur in two contexts:
+// (1) development versions of this package may return this error for endpoints which are defined but not yet completed, and,
+// (2) production versions of this package may return this error when a provider fails to offer the requested Identity extension.
+//
+// ErrEndpoint errors occur when the authentication URL provided to Authenticate() either isn't valid
+// or the endpoint provided doesn't respond like an Identity V2 API endpoint should.
+//
+// ErrCredentials errors occur when authentication fails due to the caller possessing insufficient access privileges.
+var (
+ ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
+ ErrEndpoint = fmt.Errorf("Improper or missing Identity endpoint")
+ ErrCredentials = fmt.Errorf("Improper or missing Identity credentials")
+)
diff --git a/openstack/identity/extensions.go b/openstack/identity/extensions.go
new file mode 100644
index 0000000..cfd8f24
--- /dev/null
+++ b/openstack/identity/extensions.go
@@ -0,0 +1,90 @@
+package identity
+
+import (
+ "github.com/mitchellh/mapstructure"
+)
+
+// ExtensionDetails provides the details offered by the OpenStack Identity V2 extensions API
+// for a named extension.
+//
+// Name provides the name, presumably the same as that used to query the API with.
+//
+// Updated provides, in a sense, the version of the extension supported. It gives the timestamp
+// of the most recent extension deployment.
+//
+// Description provides a more customer-oriented description of the extension.
+type ExtensionDetails struct {
+ Name string
+ Namespace string
+ Updated string
+ Description string
+}
+
+// ExtensionsResult encapsulates the raw data returned by a call to
+// GetExtensions(). 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 ExtensionsResult map[string]interface{}
+
+// IsExtensionAvailable returns true if and only if the provider supports the named extension.
+func (er ExtensionsResult) IsExtensionAvailable(alias string) bool {
+ e, err := extensions(er)
+ if err != nil {
+ return false
+ }
+ _, err = extensionIndexByAlias(e, alias)
+ return err == nil
+}
+
+// ExtensionDetailsByAlias returns more detail than the mere presence of an extension by the provider.
+// See the ExtensionDetails structure.
+func (er ExtensionsResult) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
+ e, err := extensions(er)
+ if err != nil {
+ return nil, err
+ }
+ i, err := extensionIndexByAlias(e, alias)
+ if err != nil {
+ return nil, err
+ }
+ ed := &ExtensionDetails{}
+ err = mapstructure.Decode(e[i], ed)
+ return ed, err
+}
+
+func extensionIndexByAlias(records []interface{}, alias string) (int, error) {
+ for i, er := range records {
+ extensionRecord := er.(map[string]interface{})
+ if extensionRecord["alias"] == alias {
+ return i, nil
+ }
+ }
+ return 0, ErrNotImplemented
+}
+
+func extensions(er ExtensionsResult) ([]interface{}, error) {
+ e, ok := er["extensions"]
+ if !ok {
+ return nil, ErrNotImplemented
+ }
+ return e.([]interface{}), nil
+}
+
+// Aliases returns the set of extension handles, or "aliases" as OpenStack calls them.
+// These are not the names of the extensions, but rather opaque, symbolic monikers for their corresponding extension.
+// Use the ExtensionDetailsByAlias() method to query more information for an extension if desired.
+func (er ExtensionsResult) Aliases() ([]string, error) {
+ e, err := extensions(er)
+ if err != nil {
+ return nil, err
+ }
+ aliases := make([]string, len(e))
+ for i, ex := range e {
+ ext := ex.(map[string]interface{})
+ extn, ok := ext["alias"]
+ if ok {
+ aliases[i] = extn.(string)
+ }
+ }
+ return aliases, nil
+}
diff --git a/openstack/identity/extensions_test.go b/openstack/identity/extensions_test.go
new file mode 100644
index 0000000..009d29d
--- /dev/null
+++ b/openstack/identity/extensions_test.go
@@ -0,0 +1,111 @@
+package identity
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestIsExtensionAvailable(t *testing.T) {
+ // Make a response as we'd expect from the IdentityService.GetExtensions() call.
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ e := ExtensionsResult(getExtensionsResults)
+ for _, alias := range []string{"RS-RPE", "RS-META"} {
+ if !e.IsExtensionAvailable(alias) {
+ t.Errorf("Expected extension %s present.", alias)
+ return
+ }
+ }
+ if e.IsExtensionAvailable("blort") {
+ t.Errorf("Input JSON doesn't list blort as an extension")
+ return
+ }
+}
+
+func TestGetExtensionDetails(t *testing.T) {
+ // Make a response as we'd expect from the IdentityService.GetExtensions() call.
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ e := ExtensionsResult(getExtensionsResults)
+ ed, err := e.ExtensionDetailsByAlias("RS-META")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ actuals := map[string]string{
+ "name": ed.Name,
+ "namespace": ed.Namespace,
+ "updated": ed.Updated,
+ "description": ed.Description,
+ }
+
+ expecteds := map[string]string{
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ }
+
+ for k, v := range expecteds {
+ if actuals[k] != v {
+ t.Errorf("Expected %s \"%s\", got \"%s\" instead", k, v, actuals[k])
+ return
+ }
+ }
+}
+
+func TestMalformedResponses(t *testing.T) {
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(bogusExtensionsResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ e := ExtensionsResult(getExtensionsResults)
+
+ _, err = e.ExtensionDetailsByAlias("RS-META")
+ if err == nil {
+ t.Error("Expected ErrNotImplemented at least")
+ return
+ }
+ if err != ErrNotImplemented {
+ t.Error("Expected ErrNotImplemented")
+ return
+ }
+
+ if e.IsExtensionAvailable("anything at all") {
+ t.Error("No extensions are available with a bogus result.")
+ return
+ }
+}
+
+func TestAliases(t *testing.T) {
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ e := ExtensionsResult(getExtensionsResults)
+ aliases, err := e.Aliases()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if len(aliases) != len(e) {
+ t.Error("Expected one alias name per extension")
+ return
+ }
+}
diff --git a/openstack/identity/requests.go b/openstack/identity/requests.go
new file mode 100644
index 0000000..dbd367e
--- /dev/null
+++ b/openstack/identity/requests.go
@@ -0,0 +1,120 @@
+package identity
+
+import (
+ "github.com/racker/perigee"
+)
+
+// AuthResults encapsulates the raw results from an authentication request.
+// As OpenStack allows extensions to influence the structure returned in
+// ways that Gophercloud cannot predict at compile-time, you should use
+// type-safe accessors to work with the data represented by this type,
+// such as ServiceCatalog() and Token().
+type AuthResults map[string]interface{}
+
+// AuthOptions lets anyone calling Authenticate() supply the required access
+// credentials. At present, only Identity V2 API support exists; therefore,
+// only Username, Password, and optionally, TenantId are provided. If future
+// Identity API versions become available, alternative fields unique to those
+// versions may appear here.
+//
+// Endpoint specifies the HTTP endpoint offering the Identity V2 API.
+// Required.
+//
+// Username is required if using Identity V2 API. Consult with your provider's
+// control panel to discover your account's username.
+//
+// At most one of Password or ApiKey is required if using Identity V2 API.
+// Consult with your provider's control panel to discover your account's
+// preferred method of authentication.
+//
+// The TenantId and TenantName fields are optional for the Identity V2 API.
+// Some providers allow you to specify a TenantName instead of the TenantId.
+// Some require both. Your provider's authentication policies will determine
+// how these fields influence authentication.
+//
+// AllowReauth should be set to true if you grant permission for Gophercloud to
+// cache your credentials in memory, and to allow Gophercloud to attempt to
+// re-authenticate automatically if/when your token expires. If you set it to
+// false, it will not cache these settings, but re-authentication will not be
+// possible. This setting defaults to false.
+type AuthOptions struct {
+ Endpoint string
+ Username string
+ Password, ApiKey string
+ TenantId string
+ TenantName string
+ AllowReauth bool
+}
+
+// Authenticate passes the supplied credentials to the OpenStack provider for authentication.
+// If successful, the caller may use Token() to retrieve the authentication token,
+// and ServiceCatalog() to retrieve the set of services available to the API user.
+func Authenticate(options AuthOptions) (AuthResults, error) {
+ type AuthContainer struct {
+ Auth auth `json:"auth"`
+ }
+
+ var ar AuthResults
+
+ if options.Endpoint == "" {
+ return nil, ErrEndpoint
+ }
+
+ if (options.Username == "") || (options.Password == "" && options.ApiKey == "") {
+ return nil, ErrCredentials
+ }
+
+ url := options.Endpoint + "/tokens"
+ err := perigee.Post(url, perigee.Options{
+ ReqBody: &AuthContainer{
+ Auth: getAuthCredentials(options),
+ },
+ Results: &ar,
+ })
+ return ar, err
+}
+
+func getAuthCredentials(options AuthOptions) auth {
+ if options.ApiKey == "" {
+ return auth{
+ PasswordCredentials: &struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{
+ Username: options.Username,
+ Password: options.Password,
+ },
+ TenantId: options.TenantId,
+ TenantName: options.TenantName,
+ }
+ } else {
+ return auth{
+ ApiKeyCredentials: &struct {
+ Username string `json:"username"`
+ ApiKey string `json:"apiKey"`
+ }{
+ Username: options.Username,
+ ApiKey: options.ApiKey,
+ },
+ TenantId: options.TenantId,
+ TenantName: options.TenantName,
+ }
+ }
+}
+
+type auth struct {
+ PasswordCredentials interface{} `json:"passwordCredentials,omitempty"`
+ ApiKeyCredentials interface{} `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
+ TenantId string `json:"tenantId,omitempty"`
+ TenantName string `json:"tenantName,omitempty"`
+}
+
+func GetExtensions(options AuthOptions) (ExtensionsResult, error) {
+ var exts ExtensionsResult
+
+ url := options.Endpoint + "/extensions"
+ err := perigee.Get(url, perigee.Options{
+ Results: &exts,
+ })
+ return exts, err
+}
diff --git a/openstack/identity/service_catalog.go b/openstack/identity/service_catalog.go
new file mode 100644
index 0000000..035f671
--- /dev/null
+++ b/openstack/identity/service_catalog.go
@@ -0,0 +1,102 @@
+package identity
+
+import "github.com/mitchellh/mapstructure"
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+// OpenStack extensions may alter the structure of the service catalog in ways unpredictable to Go at compile-time,
+// so this structure serves as a convenient anchor for type-safe accessors and methods.
+type ServiceCatalog struct {
+ serviceDescriptions []interface{}
+}
+
+// CatalogEntry provides a type-safe interface to an Identity API V2 service
+// catalog listing. Each class of service, such as cloud DNS or block storage
+// services, will have a single CatalogEntry representing it.
+//
+// Name will contain the provider-specified name for the service.
+//
+// If OpenStack defines a type for the service, this field will contain that
+// type string. Otherwise, for provider-specific services, the provider may
+// assign their own type strings.
+//
+// Endpoints will let the caller iterate over all the different endpoints that
+// may exist for the service.
+//
+// Note: when looking for the desired service, try, whenever possible, to key
+// off the type field. Otherwise, you'll tie the representation of the service
+// to a specific provider.
+type CatalogEntry struct {
+ Name string
+ Type string
+ Endpoints []Endpoint
+}
+
+// Endpoint represents a single API endpoint offered by a service.
+// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
+// The significance of the Region field will depend upon your provider.
+//
+// In addition, the interface offered by the service will have version information associated with it
+// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
+//
+// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
+type Endpoint struct {
+ TenantId string
+ PublicURL string
+ InternalURL string
+ Region string
+ VersionId string
+ VersionInfo string
+ VersionList string
+}
+
+// GetServiceCatalog acquires the service catalog from a successful authentication's results.
+func GetServiceCatalog(ar AuthResults) (*ServiceCatalog, error) {
+ access := ar["access"].(map[string]interface{})
+ sds := access["serviceCatalog"].([]interface{})
+ sc := &ServiceCatalog{
+ serviceDescriptions: sds,
+ }
+ return sc, nil
+}
+
+// NumberOfServices yields the number of services the caller may use. Note
+// that this does not necessarily equal the number of endpoints available for
+// use.
+func (sc *ServiceCatalog) NumberOfServices() int {
+ return len(sc.serviceDescriptions)
+}
+
+// CatalogEntries returns a slice of service catalog entries.
+// Each entry corresponds to a specific class of service offered by the API provider.
+// See the CatalogEntry structure for more details.
+func (sc *ServiceCatalog) CatalogEntries() ([]CatalogEntry, error) {
+ var err error
+ ces := make([]CatalogEntry, sc.NumberOfServices())
+ for i, sd := range sc.serviceDescriptions {
+ d := sd.(map[string]interface{})
+ eps, err := parseEndpoints(d["endpoints"].([]interface{}))
+ if err != nil {
+ return ces, err
+ }
+ ces[i] = CatalogEntry{
+ Name: d["name"].(string),
+ Type: d["type"].(string),
+ Endpoints: eps,
+ }
+ }
+ return ces, err
+}
+
+func parseEndpoints(eps []interface{}) ([]Endpoint, error) {
+ var err error
+ result := make([]Endpoint, len(eps))
+ for i, ep := range eps {
+ e := Endpoint{}
+ err = mapstructure.Decode(ep, &e)
+ if err != nil {
+ return result, err
+ }
+ result[i] = e
+ }
+ return result, err
+}
diff --git a/openstack/identity/service_catalog_test.go b/openstack/identity/service_catalog_test.go
new file mode 100644
index 0000000..f810609
--- /dev/null
+++ b/openstack/identity/service_catalog_test.go
@@ -0,0 +1,91 @@
+package identity
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestServiceCatalog(t *testing.T) {
+ authResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(authResultsOK), &authResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ sc, err := GetServiceCatalog(authResults)
+ if err != nil {
+ panic(err)
+ }
+
+ if sc.NumberOfServices() != 3 {
+ t.Errorf("Expected 3 services; got %d", sc.NumberOfServices())
+ }
+
+ ces, err := sc.CatalogEntries()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ for _, ce := range ces {
+ if strNotInStrList(ce.Name, "Cloud Servers", "Cloud Files", "DNS-as-a-Service") {
+ t.Errorf("Expected \"%s\" to be one of Cloud Servers, Cloud Files, or DNS-as-a-Service", ce.Name)
+ return
+ }
+
+ if strNotInStrList(ce.Type, "dnsextension:dns", "object-store", "compute") {
+ t.Errorf("Expected \"%s\" to be one of dnsextension:dns, object-store, or compute")
+ return
+ }
+ }
+
+ eps := endpointsFor(ces, "compute")
+ if len(eps) != 2 {
+ t.Errorf("Expected 2 endpoints for compute service")
+ return
+ }
+ for _, ep := range eps {
+ if strNotInStrList(ep.VersionId, "1", "1.1", "1.1") {
+ t.Errorf("Expected versionId field of compute resource to be one of 1 or 1.1")
+ return
+ }
+ }
+
+ eps = endpointsFor(ces, "object-store")
+ if len(eps) != 2 {
+ t.Errorf("Expected 2 endpoints for object-store service")
+ return
+ }
+ for _, ep := range eps {
+ if ep.VersionId != "1" {
+ t.Errorf("Expected only version 1 object store API version")
+ return
+ }
+ }
+
+ eps = endpointsFor(ces, "dnsextension:dns")
+ if len(eps) != 1 {
+ t.Errorf("Expected 1 endpoint for DNS-as-a-Service service")
+ return
+ }
+ if eps[0].VersionId != "2.0" {
+ t.Errorf("Expected version 2.0 of DNS-as-a-Service service")
+ return
+ }
+}
+
+func endpointsFor(ces []CatalogEntry, t string) []Endpoint {
+ for _, ce := range ces {
+ if ce.Type == t {
+ return ce.Endpoints
+ }
+ }
+ panic("Precondition violated")
+}
+
+func strNotInStrList(needle, haystack1, haystack2, haystack3 string) bool {
+ if (needle != haystack1) && (needle != haystack2) && (needle != haystack3) {
+ return true
+ }
+ return false
+}
diff --git a/openstack/identity/token.go b/openstack/identity/token.go
new file mode 100644
index 0000000..d50cce0
--- /dev/null
+++ b/openstack/identity/token.go
@@ -0,0 +1,72 @@
+package identity
+
+import (
+ "github.com/mitchellh/mapstructure"
+)
+
+// Token provides only the most basic information related to an authentication token.
+//
+// Id provides the primary means of identifying a user to the OpenStack API.
+// OpenStack defines this field as an opaque value, so do not depend on its content.
+// It is safe, however, to compare for equality.
+//
+// Expires provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
+// After this point in time, future API requests made using this authentication token will respond with errors.
+// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
+// See the AuthOptions structure for more details.
+//
+// TenantId provides the canonical means of identifying a tenant.
+// As with Id, this field is defined to be opaque, so do not depend on its content.
+// It is safe, however, to compare for equality.
+//
+// TenantName provides a human-readable tenant name corresponding to the TenantId.
+type Token struct {
+ Id, Expires string
+ TenantId, TenantName string
+}
+
+// GetToken, if successful, yields an unpacked collection of fields related to the user's access credentials, called a "token."
+// See the Token structure for more details.
+func GetToken(m AuthResults) (*Token, error) {
+ type (
+ Tenant struct {
+ Id string
+ Name string
+ }
+
+ TokenDesc struct {
+ Id string `mapstructure:"id"`
+ Expires string `mapstructure:"expires"`
+ Tenant
+ }
+ )
+
+ accessMap, err := getSubmap(m, "access")
+ if err != nil {
+ return nil, err
+ }
+ tokenMap, err := getSubmap(accessMap, "token")
+ if err != nil {
+ return nil, err
+ }
+ t := &TokenDesc{}
+ err = mapstructure.Decode(tokenMap, t)
+ if err != nil {
+ return nil, err
+ }
+ td := &Token{
+ Id: t.Id,
+ Expires: t.Expires,
+ TenantId: t.Tenant.Id,
+ TenantName: t.Tenant.Name,
+ }
+ return td, nil
+}
+
+func getSubmap(m map[string]interface{}, name string) (map[string]interface{}, error) {
+ entry, ok := m[name]
+ if !ok {
+ return nil, ErrNotImplemented
+ }
+ return entry.(map[string]interface{}), nil
+}
diff --git a/openstack/identity/token_test.go b/openstack/identity/token_test.go
new file mode 100644
index 0000000..5e96496
--- /dev/null
+++ b/openstack/identity/token_test.go
@@ -0,0 +1,25 @@
+package identity
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestAccessToken(t *testing.T) {
+ authResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(authResultsOK), &authResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ tok, err := GetToken(authResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if tok.Id != "ab48a9efdfedb23ty3494" {
+ t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.Id)
+ return
+ }
+}
diff --git a/openstack/storage/v1/accounts/accounts.go b/openstack/storage/v1/accounts/accounts.go
new file mode 100644
index 0000000..c460e45
--- /dev/null
+++ b/openstack/storage/v1/accounts/accounts.go
@@ -0,0 +1,30 @@
+package accounts
+
+import (
+ "strings"
+)
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or deleting an
+// account's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ Headers map[string]string
+}
+
+// GetOpts is a structure that contains parameters for getting an account's metadata.
+type GetOpts struct {
+ Headers map[string]string
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metatdata associated with the account.
+func ExtractMetadata(gr GetResult) map[string]string {
+ metadata := make(map[string]string)
+ for k, v := range gr.Header {
+ if strings.HasPrefix(k, "X-Account-Meta-") {
+ key := strings.TrimPrefix(k, "X-Account-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata
+}
diff --git a/openstack/storage/v1/accounts/requests.go b/openstack/storage/v1/accounts/requests.go
new file mode 100644
index 0000000..7b84497
--- /dev/null
+++ b/openstack/storage/v1/accounts/requests.go
@@ -0,0 +1,50 @@
+package accounts
+
+import (
+ "github.com/racker/perigee"
+ storage "github.com/rackspace/gophercloud/openstack/storage/v1"
+ "net/http"
+)
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult *http.Response
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+func Update(c *storage.Client, opts UpdateOpts) error {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Account-Meta-"+k] = v
+ }
+
+ url := c.GetAccountURL()
+ _, err = perigee.Request("POST", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return err
+}
+
+// Get is a function that retrieves an account's metadata. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *storage.Client, opts GetOpts) (GetResult, error) {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ url := c.GetAccountURL()
+ resp, err := perigee.Request("HEAD", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return &resp.HttpResponse, err
+}
diff --git a/openstack/storage/v1/client.go b/openstack/storage/v1/client.go
new file mode 100644
index 0000000..f616038
--- /dev/null
+++ b/openstack/storage/v1/client.go
@@ -0,0 +1,68 @@
+package v1
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+)
+
+// Client is a structure that contains information for communicating with a provider.
+type Client struct {
+ endpoint string
+ authority identity.AuthResults
+ options identity.AuthOptions
+ token *identity.Token
+}
+
+// NewClient creates and returns a *Client.
+func NewClient(e string, a identity.AuthResults, o identity.AuthOptions) *Client {
+ return &Client{
+ endpoint: e,
+ authority: a,
+ options: o,
+ }
+}
+
+// GetAccountURL returns the URI for making Account requests. This function is exported to allow
+// the 'Accounts' subpackage to use it. It is not meant for public consumption.
+func (c *Client) GetAccountURL() string {
+ return fmt.Sprintf("%s", c.endpoint)
+}
+
+// GetContainerURL returns the URI for making Container requests. This function is exported to allow
+// the 'Containers' subpackage to use it. It is not meant for public consumption.
+func (c *Client) GetContainerURL(container string) string {
+ return fmt.Sprintf("%s/%s", c.endpoint, container)
+}
+
+// GetObjectURL returns the URI for making Object requests. This function is exported to allow
+// the 'Objects' subpackage to use it. It is not meant for public consumption.
+func (c *Client) GetObjectURL(container, object string) string {
+ return fmt.Sprintf("%s/%s/%s", c.endpoint, container, object)
+}
+
+// GetHeaders is a function that gets the header for token authentication against a client's endpoint.
+// This function is exported to allow the subpackages to use it. It is not meant for public consumption.
+func (c *Client) GetHeaders() (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
+}
+
+// getAuthToken is a function that tries to retrieve an authentication token from a client's endpoint.
+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/storage/v1/containers/containers.go b/openstack/storage/v1/containers/containers.go
new file mode 100644
index 0000000..3a00647
--- /dev/null
+++ b/openstack/storage/v1/containers/containers.go
@@ -0,0 +1,84 @@
+package containers
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "strings"
+)
+
+// Container is a structure that holds information related to a storage container.
+type Container map[string]interface{}
+
+// ListOpts is a structure that holds parameters for listing containers.
+type ListOpts struct {
+ Full bool
+ Params map[string]string
+}
+
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+ Name string
+ Metadata map[string]string
+ Headers map[string]string
+}
+
+// DeleteOpts is a structure that holds parameters for deleting a container.
+type DeleteOpts struct {
+ Name string
+ Params map[string]string
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
+// container's metadata.
+type UpdateOpts struct {
+ Name string
+ Metadata map[string]string
+ Headers map[string]string
+}
+
+// GetOpts is a structure that holds parameters for getting a container's metadata.
+type GetOpts struct {
+ Name string
+ Metadata map[string]string
+}
+
+// ExtractInfo is a function that takes a ListResult (of type *http.Response)
+// and returns the containers' information.
+func ExtractInfo(lr ListResult) ([]Container, error) {
+ var ci []Container
+ defer lr.Body.Close()
+ body, err := ioutil.ReadAll(lr.Body)
+ if err != nil {
+ return ci, err
+ }
+ err = json.Unmarshal(body, &ci)
+ return ci, err
+}
+
+// ExtractNames is a function that takes a ListResult (of type *http.Response)
+// and returns the containers' names.
+func ExtractNames(lr ListResult) ([]string, error) {
+ var cns []string
+ defer lr.Body.Close()
+ body, err := ioutil.ReadAll(lr.Body)
+ if err != nil {
+ return cns, err
+ }
+ jr := string(body)
+ cns = strings.Split(jr, "\n")
+ cns = cns[:len(cns)-1]
+ return cns, nil
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the container.
+func ExtractMetadata(gr GetResult) map[string]string {
+ metadata := make(map[string]string)
+ for k, v := range gr.Header {
+ if strings.HasPrefix(k, "X-Container-Meta-") {
+ key := strings.TrimPrefix(k, "X-Container-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata
+}
diff --git a/openstack/storage/v1/containers/requests.go b/openstack/storage/v1/containers/requests.go
new file mode 100644
index 0000000..b6d3a89
--- /dev/null
+++ b/openstack/storage/v1/containers/requests.go
@@ -0,0 +1,125 @@
+package containers
+
+import (
+ "github.com/racker/perigee"
+ storage "github.com/rackspace/gophercloud/openstack/storage/v1"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "net/http"
+)
+
+// ListResult is a *http.Response that is returned from a call to the List function.
+type ListResult *http.Response
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult *http.Response
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the account. To extract just the container information or names, pass the ListResult
+// response to the ExtractInfo or ExtractNames function, respectively.
+func List(c *storage.Client, opts ListOpts) (ListResult, error) {
+ contentType := ""
+
+ h, err := c.GetHeaders()
+ if err != nil {
+ return nil, err
+ }
+
+ query := utils.BuildQuery(opts.Params)
+
+ if !opts.Full {
+ contentType = "text/plain"
+ }
+
+ url := c.GetAccountURL() + query
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ Accept: contentType,
+ })
+ return &resp.HttpResponse, err
+}
+
+// Create is a function that creates a new container.
+func Create(c *storage.Client, opts CreateOpts) (Container, error) {
+ var ci Container
+
+ h, err := c.GetHeaders()
+ if err != nil {
+ return Container{}, err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+
+ url := c.GetContainerURL(opts.Name)
+ _, err = perigee.Request("PUT", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ if err == nil {
+ ci = Container{
+ "name": opts.Name,
+ }
+ }
+ return ci, err
+}
+
+// Delete is a function that deletes a container.
+func Delete(c *storage.Client, opts DeleteOpts) error {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+
+ query := utils.BuildQuery(opts.Params)
+
+ url := c.GetContainerURL(opts.Name) + query
+ _, err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return err
+}
+
+// Update is a function that creates, updates, or deletes a container's metadata.
+func Update(c *storage.Client, opts UpdateOpts) error {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+
+ url := c.GetContainerURL(opts.Name)
+ _, err = perigee.Request("POST", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return err
+}
+
+// Get is a function that retrieves the metadata of a container. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *storage.Client, opts GetOpts) (GetResult, error) {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+
+ url := c.GetContainerURL(opts.Name)
+ resp, err := perigee.Request("HEAD", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return &resp.HttpResponse, err
+}
diff --git a/openstack/storage/v1/objects/objects.go b/openstack/storage/v1/objects/objects.go
new file mode 100644
index 0000000..ab390fa
--- /dev/null
+++ b/openstack/storage/v1/objects/objects.go
@@ -0,0 +1,120 @@
+package objects
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "strings"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object map[string]interface{}
+
+// ListOpts is a structure that holds parameters for listing objects.
+type ListOpts struct {
+ Container string
+ Full bool
+ Params map[string]string
+}
+
+// DownloadOpts is a structure that holds parameters for downloading an object.
+type DownloadOpts struct {
+ Container string
+ Name string
+ Headers map[string]string
+ Params map[string]string
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts struct {
+ Container string
+ Name string
+ Content *bytes.Buffer
+ Metadata map[string]string
+ Headers map[string]string
+ Params map[string]string
+}
+
+// CopyOpts is a structure that holds parameters for copying one object to another.
+type CopyOpts struct {
+ Container string
+ Name string
+ NewContainer string
+ NewName string
+ Metadata map[string]string
+ Headers map[string]string
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts struct {
+ Container string
+ Name string
+ Params map[string]string
+}
+
+// GetOpts is a structure that holds parameters for getting an object's metadata.
+type GetOpts struct {
+ Container string
+ Name string
+ Headers map[string]string
+ Params map[string]string
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
+// object's metadata.
+type UpdateOpts struct {
+ Container string
+ Name string
+ Metadata map[string]string
+ Headers map[string]string
+}
+
+// ExtractInfo is a function that takes a ListResult (of type *http.Response)
+// and returns the objects' information.
+func ExtractInfo(lr ListResult) ([]Object, error) {
+ var oi []Object
+ defer lr.Body.Close()
+ body, err := ioutil.ReadAll(lr.Body)
+ if err != nil {
+ return oi, err
+ }
+ err = json.Unmarshal(body, &oi)
+ return oi, err
+}
+
+// ExtractNames is a function that takes a ListResult (of type *http.Response)
+// and returns the objects' names.
+func ExtractNames(lr ListResult) ([]string, error) {
+ var ons []string
+ defer lr.Body.Close()
+ body, err := ioutil.ReadAll(lr.Body)
+ if err != nil {
+ return ons, err
+ }
+ jr := string(body)
+ ons = strings.Split(jr, "\n")
+ ons = ons[:len(ons)-1]
+ return ons, nil
+}
+
+// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
+// and returns the object's content.
+func ExtractContent(dr DownloadResult) ([]byte, error) {
+ var body []byte
+ defer dr.Body.Close()
+ body, err := ioutil.ReadAll(dr.Body)
+ return body, err
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func ExtractMetadata(gr GetResult) map[string]string {
+ metadata := make(map[string]string)
+ for k, v := range gr.Header {
+ if strings.HasPrefix(k, "X-Object-Meta-") {
+ key := strings.TrimPrefix(k, "X-Object-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata
+}
diff --git a/openstack/storage/v1/objects/requests.go b/openstack/storage/v1/objects/requests.go
new file mode 100644
index 0000000..a7dff40
--- /dev/null
+++ b/openstack/storage/v1/objects/requests.go
@@ -0,0 +1,178 @@
+package objects
+
+import (
+ "fmt"
+ "github.com/racker/perigee"
+ storage "github.com/rackspace/gophercloud/openstack/storage/v1"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "net/http"
+)
+
+// ListResult is a *http.Response that is returned from a call to the List function.
+type ListResult *http.Response
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult *http.Response
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult *http.Response
+
+// List is a function that retrieves all objects in a container. It also returns the details
+// for the container. To extract only the object information or names, pass the ListResult
+// response to the ExtractInfo or ExtractNames function, respectively.
+func List(c *storage.Client, opts ListOpts) (ListResult, error) {
+ contentType := ""
+
+ h, err := c.GetHeaders()
+ if err != nil {
+ return nil, err
+ }
+
+ query := utils.BuildQuery(opts.Params)
+
+ if !opts.Full {
+ contentType = "text/plain"
+ }
+
+ url := c.GetContainerURL(opts.Container) + query
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ Accept: contentType,
+ })
+ return &resp.HttpResponse, err
+}
+
+// Download is a function that retrieves the content and metadata for an object.
+// To extract just the content, pass the DownloadResult response to the ExtractContent
+// function.
+func Download(c *storage.Client, opts DownloadOpts) (DownloadResult, error) {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ query := utils.BuildQuery(opts.Params)
+
+ url := c.GetObjectURL(opts.Container, opts.Name) + query
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return &resp.HttpResponse, err
+}
+
+// Create is a function that creates a new object or replaces an existing object.
+func Create(c *storage.Client, opts CreateOpts) error {
+ var reqBody []byte
+
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ query := utils.BuildQuery(opts.Params)
+
+ content := opts.Content
+ if content != nil {
+ reqBody = make([]byte, content.Len())
+ _, err = content.Read(reqBody)
+ if err != nil {
+ return err
+ }
+ }
+
+ url := c.GetObjectURL(opts.Container, opts.Name) + query
+ _, err = perigee.Request("PUT", url, perigee.Options{
+ ReqBody: reqBody,
+ MoreHeaders: h,
+ })
+ return err
+}
+
+// Copy is a function that copies one object to another.
+func Copy(c *storage.Client, opts CopyOpts) error {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ h["Destination"] = fmt.Sprintf("/%s/%s", opts.NewContainer, opts.NewName)
+
+ url := c.GetObjectURL(opts.Container, opts.Name)
+ _, err = perigee.Request("COPY", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return err
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *storage.Client, opts DeleteOpts) error {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+
+ query := utils.BuildQuery(opts.Params)
+
+ url := c.GetObjectURL(opts.Container, opts.Name) + query
+ _, err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return err
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *storage.Client, opts GetOpts) (GetResult, error) {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ url := c.GetObjectURL(opts.Container, opts.Name)
+ resp, err := perigee.Request("HEAD", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return &resp.HttpResponse, err
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *storage.Client, opts UpdateOpts) error {
+ h, err := c.GetHeaders()
+ if err != nil {
+ return err
+ }
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ url := c.GetObjectURL(opts.Container, opts.Name)
+ _, err = perigee.Request("POST", url, perigee.Options{
+ MoreHeaders: h,
+ })
+ return err
+}
diff --git a/openstack/utils/utils.go b/openstack/utils/utils.go
new file mode 100644
index 0000000..a814c7e
--- /dev/null
+++ b/openstack/utils/utils.go
@@ -0,0 +1,61 @@
+// This package contains utilities which eases working with Gophercloud's OpenStack APIs.
+package utils
+
+import (
+ "fmt"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "os"
+)
+
+var nilOptions = identity.AuthOptions{}
+
+// ErrNoAuthUrl, ErrNoUsername, and ErrNoPassword errors indicate of the required OS_AUTH_URL, OS_USERNAME, or OS_PASSWORD
+// environment variables, respectively, remain undefined. See the AuthOptions() function for more details.
+var (
+ ErrNoAuthUrl = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
+ ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
+ ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.")
+)
+
+// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack
+// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
+// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
+// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional.
+func AuthOptions() (identity.AuthOptions, error) {
+ authUrl := os.Getenv("OS_AUTH_URL")
+ username := os.Getenv("OS_USERNAME")
+ password := os.Getenv("OS_PASSWORD")
+ tenantId := os.Getenv("OS_TENANT_ID")
+ tenantName := os.Getenv("OS_TENANT_NAME")
+
+ if authUrl == "" {
+ return nilOptions, ErrNoAuthUrl
+ }
+
+ if username == "" {
+ return nilOptions, ErrNoUsername
+ }
+
+ if password == "" {
+ return nilOptions, ErrNoPassword
+ }
+
+ ao := identity.AuthOptions{
+ Endpoint: authUrl,
+ Username: username,
+ Password: password,
+ TenantId: tenantId,
+ TenantName: tenantName,
+ }
+
+ return ao, nil
+}
+
+func BuildQuery(params map[string]string) string {
+ query := "?"
+ for k, v := range params {
+ query += k + "=" + v + "&"
+ }
+ query = query[:len(query)-1]
+ return query
+}
diff --git a/rackspace/monitoring/common.go b/rackspace/monitoring/common.go
new file mode 100644
index 0000000..d68a349
--- /dev/null
+++ b/rackspace/monitoring/common.go
@@ -0,0 +1,11 @@
+package monitoring
+
+import (
+ "github.com/rackspace/gophercloud/openstack/identity"
+)
+
+type Options struct {
+ Endpoint string
+ AuthOptions identity.AuthOptions
+ Authentication identity.AuthResults
+}
diff --git a/rackspace/monitoring/notificationPlans/requests.go b/rackspace/monitoring/notificationPlans/requests.go
new file mode 100644
index 0000000..1bef64d
--- /dev/null
+++ b/rackspace/monitoring/notificationPlans/requests.go
@@ -0,0 +1,40 @@
+package notificationPlans
+
+import (
+ "fmt"
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud/openstack/identity"
+ "github.com/rackspace/gophercloud/rackspace/monitoring"
+)
+
+var ErrNotImplemented = fmt.Errorf("notificationPlans feature not yet implemented")
+
+type Client struct {
+ options monitoring.Options
+}
+
+type DeleteResults map[string]interface{}
+
+func NewClient(mo monitoring.Options) *Client {
+ return &Client{
+ options: mo,
+ }
+}
+
+func (c *Client) Delete(id string) (DeleteResults, error) {
+ var dr DeleteResults
+
+ tok, err := identity.GetToken(c.options.Authentication)
+ if err != nil {
+ return nil, err
+ }
+ url := fmt.Sprintf("%s/notification_plans/%s", c.options.Endpoint, id)
+ err = perigee.Delete(url, perigee.Options{
+ Results: &dr,
+ OkCodes: []int{204},
+ MoreHeaders: map[string]string{
+ "X-Auth-Token": tok.Id,
+ },
+ })
+ return dr, err
+}
diff --git a/servers.go b/servers.go
index 425853b..c102169 100644
--- a/servers.go
+++ b/servers.go
@@ -5,7 +5,6 @@
import (
"fmt"
- "github.com/mitchellh/mapstructure"
"github.com/racker/perigee"
"strings"
)
@@ -56,22 +55,6 @@
},
})
})
-
- // Compatibility with v0.0.x -- we "map" our public and private
- // addresses into a legacy structure field for the benefit of
- // earlier software.
-
- if err != nil {
- return ss, err
- }
-
- for _, s := range ss {
- err = mapstructure.Decode(s.RawAddresses, &s.Addresses)
- if err != nil {
- return ss, err
- }
- }
-
return ss, err
}
@@ -89,17 +72,6 @@
OkCodes: []int{200},
})
})
-
- // Compatibility with v0.0.x -- we "map" our public and private
- // addresses into a legacy structure field for the benefit of
- // earlier software.
-
- if err != nil {
- return s, err
- }
-
- err = mapstructure.Decode(s.RawAddresses, &s.Addresses)
-
return s, err
}
@@ -583,8 +555,6 @@
//
// Addresses provides addresses for any attached isolated networks.
// The version field indicates whether the IP address is version 4 or 6.
-// Note: only public and private pools appear here.
-// To get the complete set, use the AllAddressPools() method instead.
//
// Created tells when the server entity was created.
//
@@ -658,9 +628,9 @@
// http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ch_extensions.html#ext_status
// for more details. It's too lengthy to include here.
type Server struct {
- AccessIPv4 string `json:"accessIPv4"`
- AccessIPv6 string `json:"accessIPv6"`
- Addresses AddressSet
+ AccessIPv4 string `json:"accessIPv4"`
+ AccessIPv6 string `json:"accessIPv6"`
+ Addresses AddressSet `json:"addresses"`
Created string `json:"created"`
Flavor FlavorLink `json:"flavor"`
HostId string `json:"hostId"`
@@ -679,24 +649,6 @@
OsExtStsPowerState int `json:"OS-EXT-STS:power_state"`
OsExtStsTaskState string `json:"OS-EXT-STS:task_state"`
OsExtStsVmState string `json:"OS-EXT-STS:vm_state"`
-
- RawAddresses map[string]interface{} `json:"addresses"`
-}
-
-// AllAddressPools returns a complete set of address pools available on the server.
-// The name of each pool supported keys the map.
-// The value of the map contains the addresses provided in the corresponding pool.
-func (s *Server) AllAddressPools() (map[string][]VersionedAddress, error) {
- pools := make(map[string][]VersionedAddress, 0)
- for pool, subtree := range s.RawAddresses {
- addresses := make([]VersionedAddress, 0)
- err := mapstructure.Decode(subtree, &addresses)
- if err != nil {
- return nil, err
- }
- pools[pool] = addresses
- }
- return pools, nil
}
// NewServerSettings structures record those fields of the Server structure to change
@@ -750,8 +702,6 @@
// The Id field contains the server's unique identifier.
// The identifier's scope is best assumed to be bound by the user's account, unless other arrangements have been made with Rackspace.
//
-// The SecurityGroup field allows the user to specify a security group at launch.
-//
// Any Links provided are used to refer to the server specifically by URL.
// These links are useful for making additional REST calls not explicitly supported by Gorax.
type NewServer struct {