Completely untested code for tokens and tenants.
diff --git a/openstack/identity/v2/tenants/doc.go b/openstack/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..473a302
--- /dev/null
+++ b/openstack/identity/v2/tenants/doc.go
@@ -0,0 +1,7 @@
+/*
+Package tenants contains API calls that query for information about tenants on an OpenStack deployment.
+
+See: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+And: http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
+*/
+package tenants
diff --git a/openstack/identity/v2/tenants/requests.go b/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..5ffeaa7
--- /dev/null
+++ b/openstack/identity/v2/tenants/requests.go
@@ -0,0 +1,33 @@
+package tenants
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts filters the Tenants that are returned by the List call.
+type ListOpts struct {
+ // Marker is the ID of the last Tenant on the previous page.
+ Marker string `q:"marker"`
+
+ // Limit specifies the page size.
+ Limit int `q:"limit"`
+}
+
+// List enumerates the Tenants to which the current token has access.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return TenantPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ }
+
+ url := listURL(client)
+ if opts != nil {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += q.String()
+ }
+
+ return pagination.NewPager(client, url, createPage)
+}
diff --git a/openstack/identity/v2/tenants/results.go b/openstack/identity/v2/tenants/results.go
new file mode 100644
index 0000000..e4e3f47
--- /dev/null
+++ b/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,75 @@
+package tenants
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Tenant is a grouping of users in the identity service.
+type Tenant struct {
+ // ID is a unique identifier for this tenant.
+ ID string `mapstructure:"id"`
+
+ // Name is a friendlier user-facing name for this tenant.
+ Name string `mapstructure:"name"`
+
+ // Description is a human-readable explanation of this Tenant's purpose.
+ Description string `mapstructure:"description"`
+
+ // Enabled indicates whether or not a tenant is active.
+ Enabled bool `mapstructure:"enabled"`
+}
+
+// TenantPage is a single page of Tenant results.
+type TenantPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty determines whether or not a page of Tenants contains any results.
+func (page TenantPage) IsEmpty() (bool, error) {
+ tenants, err := ExtractTenants(page)
+ if err != nil {
+ return false, err
+ }
+ return len(tenants) == 0, nil
+}
+
+// NextPageURL extracts the "next" link from the tenants_links section of the result.
+func (page TenantPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"tenants_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// ExtractTenants returns a slice of Tenants contained in a single page of results.
+func ExtractTenants(page pagination.Page) ([]Tenant, error) {
+ casted := page.(TenantPage).Body
+ var response struct {
+ Tenants []Tenant `mapstructure:"tenants"`
+ }
+
+ err := mapstructure.Decode(casted, &response)
+ return response.Tenants, err
+}
diff --git a/openstack/identity/v2/tenants/urls.go b/openstack/identity/v2/tenants/urls.go
new file mode 100644
index 0000000..1dd6ce0
--- /dev/null
+++ b/openstack/identity/v2/tenants/urls.go
@@ -0,0 +1,7 @@
+package tenants
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tenants")
+}
diff --git a/openstack/identity/v2/tokens/doc.go b/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..d26f642
--- /dev/null
+++ b/openstack/identity/v2/tokens/doc.go
@@ -0,0 +1,6 @@
+/*
+Package tokens contains functions that issue and manipulate identity tokens.
+
+Reference: http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
+*/
+package tokens
diff --git a/openstack/identity/v2/tokens/errors.go b/openstack/identity/v2/tokens/errors.go
new file mode 100644
index 0000000..244db1b
--- /dev/null
+++ b/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,27 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+var (
+ // ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+ ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+ // ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
+ ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
+
+ // ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
+ ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
+
+ // ErrUsernameRequired is returned if you attempt ot authenticate without a Username.
+ ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
+
+ // ErrPasswordOrAPIKey is returned if you provide both a password and an API key.
+ ErrPasswordOrAPIKey = errors.New("Please supply exactly one of Password or APIKey in your AuthOptions.")
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
+}
diff --git a/openstack/identity/v2/tokens/requests.go b/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..c0441ab
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,72 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Create authenticates to the identity service and attempts to acquire a Token.
+// If successful, the CreateResult
+// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
+// which abstracts all of the gory details about navigating service catalogs and such.
+func Create(client *gophercloud.ServiceClient, auth gophercloud.AuthOptions) CreateResult {
+ var request struct {
+ Auth struct {
+ PasswordCredentials struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ } `json:"passwordCredentials,omitempty"`
+ APIKeyCredentials struct {
+ Username string `json:"username"`
+ APIKey string `json:"apiKey"`
+ } `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
+ TenantID string `json:"tenantId,omitempty"`
+ TenantName string `json:"tenantName,omitempty"`
+ } `json:"auth"`
+ }
+
+ // Error out if an unsupported auth option is present.
+ if auth.UserID != "" {
+ return createErr(ErrUserIDProvided)
+ }
+ if auth.DomainID != "" {
+ return createErr(ErrDomainIDProvided)
+ }
+ if auth.DomainName != "" {
+ return createErr(ErrDomainNameProvided)
+ }
+
+ // Username is always required.
+ if auth.Username == "" {
+ return createErr(ErrUsernameRequired)
+ }
+
+ // Populate either PasswordCredentials or APIKeyCredentials
+ if auth.Password != "" {
+ if auth.APIKey != "" {
+ return createErr(ErrPasswordOrAPIKey)
+ }
+
+ // Username + Password
+ request.Auth.PasswordCredentials.Username = auth.Username
+ request.Auth.PasswordCredentials.Password = auth.Password
+ } else if auth.APIKey != "" {
+ // API key authentication.
+ request.Auth.APIKeyCredentials.Username = auth.Username
+ request.Auth.APIKeyCredentials.APIKey = auth.APIKey
+ } else {
+ return createErr(ErrPasswordOrAPIKey)
+ }
+
+ // Populate the TenantName or TenantID, if provided.
+ request.Auth.TenantID = auth.TenantID
+ request.Auth.TenantName = auth.TenantName
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
+ ReqBody: &request,
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ return result
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..7a245ab
--- /dev/null
+++ b/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,66 @@
+package tokens
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// Token provides only the most basic information related to an authentication token.
+type Token struct {
+ // 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.
+ ID string
+
+ // ExpiresAt 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.
+ ExpiresAt time.Time
+
+ // Tenant provides information about the tenant to which this token grants access.
+ Tenant tenants.Tenant
+}
+
+// CreateResult defers the interpretation of a created token.
+// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
+type CreateResult struct {
+ gophercloud.CommonResult
+}
+
+// ExtractToken returns the just-created Token from a CreateResult.
+func (result CreateResult) ExtractToken() (*Token, error) {
+ var response struct {
+ Access struct {
+ Token struct {
+ Expires string `mapstructure:"expires"`
+ ID string `mapstructure:"id"`
+ Tenant tenants.Tenant `mapstructure:"tenant"`
+ } `mapstructure:"token"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Token{
+ ID: response.Access.Token.ID,
+ ExpiresAt: expiresTs,
+ Tenant: response.Access.Token.Tenant,
+ }, nil
+}
+
+// createErr quickly packs an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+}
diff --git a/openstack/identity/v2/tokens/urls.go b/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..86d19f2
--- /dev/null
+++ b/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tokens")
+}