Initial commit of experimental, new, v0.2.0 API
diff --git a/openstack/identity/common_test.go b/openstack/identity/common_test.go
new file mode 100644
index 0000000..81d37cc
--- /dev/null
+++ b/openstack/identity/common_test.go
@@ -0,0 +1,90 @@
+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/"
+                    }
+                ]
+            }
+        ]
+    }
+}`
diff --git a/openstack/identity/errors.go b/openstack/identity/errors.go
new file mode 100644
index 0000000..b15a4a2
--- /dev/null
+++ b/openstack/identity/errors.go
@@ -0,0 +1,7 @@
+package identity
+
+import "fmt"
+
+var ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
+var ErrEndpoint = fmt.Errorf("Improper or missing Identity endpoint")
+var 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..4a861ff
--- /dev/null
+++ b/openstack/identity/extensions.go
@@ -0,0 +1,52 @@
+package identity
+
+import (
+	"fmt"
+	"github.com/mitchellh/mapstructure"
+)
+
+var (
+	ErrNotFound = fmt.Errorf("Identity extension not found")
+)
+
+type ExtensionDetails struct {
+	Name        string
+	Namespace   string
+	Updated     string
+	Description string
+}
+
+// ExtensionsDesc structures are returned by the Extensions() function for valid input.
+// This structure implements the ExtensionInquisitor interface.
+type ExtensionsDesc struct {
+	extensions []interface{}
+}
+
+func Extensions(m map[string]interface{}) *ExtensionsDesc {
+	return &ExtensionsDesc{extensions: m["extensions"].([]interface{})}
+}
+
+func extensionIndexByAlias(e *ExtensionsDesc, alias string) (int, error) {
+	for i, ee := range e.extensions {
+		extensionRecord := ee.(map[string]interface{})
+		if extensionRecord["alias"] == alias {
+			return i, nil
+		}
+	}
+	return 0, ErrNotFound
+}
+
+func (e *ExtensionsDesc) IsExtensionAvailable(alias string) bool {
+	_, err := extensionIndexByAlias(e, alias)
+	return err == nil
+}
+
+func (e *ExtensionsDesc) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
+	i, err := extensionIndexByAlias(e, alias)
+	if err != nil {
+		return nil, err
+	}
+	ed := &ExtensionDetails{}
+	err = mapstructure.Decode(e.extensions[i], ed)
+	return ed, err
+}
diff --git a/openstack/identity/extensions_test.go b/openstack/identity/extensions_test.go
new file mode 100644
index 0000000..a5eede2
--- /dev/null
+++ b/openstack/identity/extensions_test.go
@@ -0,0 +1,108 @@
+package identity
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+// 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":[]
+}`
+
+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 := Extensions(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 := Extensions(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
+		}
+	}
+}
diff --git a/openstack/identity/requests.go b/openstack/identity/requests.go
new file mode 100644
index 0000000..0012a17
--- /dev/null
+++ b/openstack/identity/requests.go
@@ -0,0 +1,103 @@
+package identity
+
+import (
+	"github.com/racker/perigee"
+)
+
+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.
+type AuthOptions struct {
+	// Endpoint specifies the HTTP endpoint offering the Identity V2 API.
+	// Required.
+	Endpoint string
+
+	// Username is required if using Identity V2 API.
+	// Consult with your provider's control panel to discover your
+	// account's username.
+	Username string
+
+	// 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.
+	Password, ApiKey string
+
+	// The TenantId field is optional for the Identity V2 API.
+	TenantId string
+
+	// The TenantName can be specified instead of the TenantId
+	TenantName string
+
+	// 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.
+	AllowReauth bool
+}
+
+func Authenticate(options AuthOptions) (AuthResults, error) {
+	var ar AuthResults
+
+	if options.Endpoint == "" {
+		return nil, ErrEndpoint
+	}
+
+	if (options.Username == "") || (options.Password == "" && options.ApiKey == "") {
+		return nil, ErrCredentials
+	}
+
+	err := perigee.Post(options.Endpoint, 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,
+		}
+	}
+}
+
+// AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity
+// service.  You will not work with this structure directly.
+type AuthContainer struct {
+	Auth Auth `json:"auth"`
+}
+
+// Auth provides a JSON encoding wrapper for passing credentials to the Identity
+// service.  You will not work with this structure directly.
+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"`
+}
diff --git a/openstack/identity/service_catalog.go b/openstack/identity/service_catalog.go
new file mode 100644
index 0000000..fc2717a
--- /dev/null
+++ b/openstack/identity/service_catalog.go
@@ -0,0 +1,68 @@
+package identity
+
+import "github.com/mitchellh/mapstructure"
+
+type ServiceCatalogDesc struct {
+	serviceDescriptions []interface{}
+}
+
+type CatalogEntry struct {
+	Name string
+	Type string
+	Endpoints []Endpoint
+}
+
+type Endpoint struct {
+	TenantId string
+	PublicURL string
+	InternalURL string
+	Region string
+	VersionId string
+	VersionInfo string
+	VersionList string
+}
+
+func ServiceCatalog(ar AuthResults) (*ServiceCatalogDesc, error) {
+	access := ar["access"].(map[string]interface{})
+	sds := access["serviceCatalog"].([]interface{})
+	sc := &ServiceCatalogDesc{
+		serviceDescriptions: sds,
+	}
+	return sc, nil
+}
+
+func (sc *ServiceCatalogDesc) NumberOfServices() int {
+	return len(sc.serviceDescriptions)
+}
+
+func (sc *ServiceCatalogDesc) 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..4411b9f
--- /dev/null
+++ b/openstack/identity/service_catalog_test.go
@@ -0,0 +1,91 @@
+package identity
+
+import (
+	"testing"
+	"encoding/json"
+)
+
+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 := ServiceCatalog(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
+}
\ No newline at end of file
diff --git a/openstack/identity/token.go b/openstack/identity/token.go
new file mode 100644
index 0000000..aaea184
--- /dev/null
+++ b/openstack/identity/token.go
@@ -0,0 +1,43 @@
+package identity
+
+import (
+	"github.com/mitchellh/mapstructure"
+)
+
+type TenantDesc struct {
+	Id   string
+	Name string
+}
+
+type TokenDesc struct {
+	Id_      string `mapstructure:"Id"`
+	Expires_ string `mapstructure:"Expires"`
+	Tenant   TenantDesc
+}
+
+func Token(m AuthResults) (*TokenDesc, error) {
+	accessMap := m["access"].(map[string]interface{})
+	tokenMap := accessMap["token"].(map[string]interface{})
+	td := &TokenDesc{}
+	err := mapstructure.Decode(tokenMap, td)
+	if err != nil {
+		return nil, err
+	}
+	return td, nil
+}
+
+func (td *TokenDesc) Id() string {
+	return td.Id_
+}
+
+func (td *TokenDesc) Expires() string {
+	return td.Expires_
+}
+
+func (td *TokenDesc) TenantId() string {
+	return td.Tenant.Id
+}
+
+func (td *TokenDesc) TenantName() string {
+	return td.Tenant.Name
+}
diff --git a/openstack/identity/token_test.go b/openstack/identity/token_test.go
new file mode 100644
index 0000000..4fdf953
--- /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 := Token(authResults)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if tok.Id() != "ab48a9efdfedb23ty3494" {
+		t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.Id())
+		return
+	}
+}