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 {