add versioning to identity; add generic 'client' function
diff --git a/openstack/identity/v2/client.go b/openstack/identity/v2/client.go
new file mode 100644
index 0000000..c736833
--- /dev/null
+++ b/openstack/identity/v2/client.go
@@ -0,0 +1,80 @@
+package identity
+
+import (
+ "os"
+)
+
+type Client struct {
+ Endpoint string
+ Authority AuthResults
+ Options AuthOptions
+}
+
+type ClientOpts struct {
+ Type string
+ Name string
+ Region string
+ URLType string
+}
+
+func (ao AuthOptions) NewClient(opts ClientOpts) (Client, error) {
+ client := Client{
+ Options: ao,
+ }
+
+ ar, err := Authenticate(ao)
+ if err != nil {
+ return client, err
+ }
+
+ client.Authority = ar
+
+ sc, err := GetServiceCatalog(ar)
+ if err != nil {
+ return client, err
+ }
+
+ ces, err := sc.CatalogEntries()
+ if err != nil {
+ return client, err
+ }
+
+ var eps []Endpoint
+
+ if opts.Name != "" {
+ for _, ce := range ces {
+ if ce.Type == opts.Type && ce.Name == opts.Name {
+ eps = ce.Endpoints
+ }
+ }
+ } else {
+ for _, ce := range ces {
+ if ce.Type == opts.Type {
+ eps = ce.Endpoints
+ }
+ }
+ }
+
+ region := os.Getenv("OS_REGION_NAME")
+ if opts.Region != "" {
+ region = opts.Region
+ }
+
+ var rep string
+ for _, ep := range eps {
+ if ep.Region == region {
+ switch opts.URLType {
+ case "public":
+ rep = ep.PublicURL
+ case "private":
+ rep = ep.InternalURL
+ default:
+ rep = ep.PublicURL
+ }
+ }
+ }
+
+ client.Endpoint = rep
+
+ return client, nil
+}
diff --git a/openstack/identity/v2/common_test.go b/openstack/identity/v2/common_test.go
new file mode 100644
index 0000000..18c5340
--- /dev/null
+++ b/openstack/identity/v2/common_test.go
@@ -0,0 +1,224 @@
+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://developer.openstack.org/api-ref-identity-v2.html
+const queryResults = `{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-07-07T12:00:0-00:00",
+ "name": "OpenStack S3 API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/ext/s3tokens/v1.0",
+ "alias": "s3tokens",
+ "description": "OpenStack S3 API."
+ },
+ {
+ "updated": "2013-07-23T12:00:0-00:00",
+ "name": "OpenStack Keystone Endpoint Filter API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/identity-api/blob/master/openstack-identity-api/v3/src/markdown/identity-api-v3-os-ep-filter-ext.md",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/ext/OS-EP-FILTER/v1.0",
+ "alias": "OS-EP-FILTER",
+ "description": "OpenStack Keystone Endpoint Filter API."
+ },
+ {
+ "updated": "2013-12-17T12:00:0-00:00",
+ "name": "OpenStack Federation APIs",
+ "links": [
+ {
+ "href": "https://github.com/openstack/identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/ext/OS-FEDERATION/v1.0",
+ "alias": "OS-FEDERATION",
+ "description": "OpenStack Identity Providers Mechanism."
+ },
+ {
+ "updated": "2013-07-11T17:14:00-00:00",
+ "name": "OpenStack Keystone Admin",
+ "links": [
+ {
+ "href": "https://github.com/openstack/identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0",
+ "alias": "OS-KSADM",
+ "description": "OpenStack extensions to Keystone v2.0 API enabling Administrative Operations."
+ },
+ {
+ "updated": "2014-01-20T12:00:0-00:00",
+ "name": "OpenStack Simple Certificate API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/ext/OS-SIMPLE-CERT/v1.0",
+ "alias": "OS-SIMPLE-CERT",
+ "description": "OpenStack simple certificate retrieval extension"
+ },
+ {
+ "updated": "2013-07-07T12:00:0-00:00",
+ "name": "OpenStack EC2 API",
+ "links": [
+ {
+ "href": "https://github.com/openstack/identity-api",
+ "type": "text/html",
+ "rel": "describedby"
+ }
+ ],
+ "namespace": "http://docs.openstack.org/identity/api/ext/OS-EC2/v1.0",
+ "alias": "OS-EC2",
+ "description": "OpenStack EC2 Credentials backend."
+ }
+ ]
+ }
+}`
+
+// Extensions query 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/v2/doc.go b/openstack/identity/v2/doc.go
new file mode 100644
index 0000000..081950d
--- /dev/null
+++ b/openstack/identity/v2/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/v2/errors.go b/openstack/identity/v2/errors.go
new file mode 100644
index 0000000..efa7c85
--- /dev/null
+++ b/openstack/identity/v2/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/v2/extensions.go b/openstack/identity/v2/extensions.go
new file mode 100644
index 0000000..9cf9c78
--- /dev/null
+++ b/openstack/identity/v2/extensions.go
@@ -0,0 +1,96 @@
+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) {
+ ei, ok := er["extensions"]
+ if !ok {
+ return nil, ErrNotImplemented
+ }
+ e := ei.(map[string]interface{})
+ vi, ok := e["values"]
+ if !ok {
+ return nil, ErrNotImplemented
+ }
+ v := vi.([]interface{})
+ return v, 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/v2/extensions_test.go b/openstack/identity/v2/extensions_test.go
new file mode 100644
index 0000000..3000fc0
--- /dev/null
+++ b/openstack/identity/v2/extensions_test.go
@@ -0,0 +1,112 @@
+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{"OS-KSADM", "OS-FEDERATION"} {
+ 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("OS-KSADM")
+ 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": "OpenStack Keystone Admin",
+ "namespace": "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0",
+ "updated": "2013-07-11T17:14:00-00:00",
+ "description": "OpenStack extensions to Keystone v2.0 API enabling Administrative Operations.",
+ }
+
+ 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("OS-KSADM")
+ 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
+ }
+ extensions := (((e["extensions"]).(map[string]interface{}))["values"]).([]interface{})
+ if len(aliases) != len(extensions) {
+ t.Error("Expected one alias name per extension")
+ return
+ }
+}
diff --git a/openstack/identity/v2/requests.go b/openstack/identity/v2/requests.go
new file mode 100644
index 0000000..dbd367e
--- /dev/null
+++ b/openstack/identity/v2/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/v2/service_catalog.go b/openstack/identity/v2/service_catalog.go
new file mode 100644
index 0000000..035f671
--- /dev/null
+++ b/openstack/identity/v2/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/v2/service_catalog_test.go b/openstack/identity/v2/service_catalog_test.go
new file mode 100644
index 0000000..f810609
--- /dev/null
+++ b/openstack/identity/v2/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/v2/token.go b/openstack/identity/v2/token.go
new file mode 100644
index 0000000..d50cce0
--- /dev/null
+++ b/openstack/identity/v2/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/v2/token_test.go b/openstack/identity/v2/token_test.go
new file mode 100644
index 0000000..5e96496
--- /dev/null
+++ b/openstack/identity/v2/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
+ }
+}