Moving calls to client helper while I'm at it
diff --git a/_site/openstack/identity/v2/extensions/delegate.go b/_site/openstack/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..cee275f
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,52 @@
+package extensions
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ExtensionPage is a single page of Extension results.
+type ExtensionPage struct {
+ common.ExtensionPage
+}
+
+// IsEmpty returns true if the current page contains at least one Extension.
+func (page ExtensionPage) IsEmpty() (bool, error) {
+ is, err := ExtractExtensions(page)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of Extension structs.
+func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
+ // Identity v2 adds an intermediate "values" object.
+
+ var resp struct {
+ Extensions struct {
+ Values []common.Extension `mapstructure:"values"`
+ } `mapstructure:"extensions"`
+ }
+
+ err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
+ return resp.Extensions.Values, err
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
+ return common.Get(c, alias)
+}
+
+// List returns a Pager which allows you to iterate over the full collection of extensions.
+// It does not accept query parameters.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return common.List(c).WithPageCreator(func(r pagination.LastHTTPResponse) pagination.Page {
+ return ExtensionPage{
+ ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
+ }
+ })
+}
diff --git a/_site/openstack/identity/v2/extensions/delegate_test.go b/_site/openstack/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..504118a
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,38 @@
+package extensions
+
+import (
+ "testing"
+
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListExtensionsSuccessfully(t)
+
+ count := 0
+ err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.ExpectedExtensions, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ common.HandleGetExtensionSuccessfully(t)
+
+ actual, err := Get(client.ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, common.SingleExtension, actual)
+}
diff --git a/_site/openstack/identity/v2/extensions/fixtures.go b/_site/openstack/identity/v2/extensions/fixtures.go
new file mode 100644
index 0000000..96cb7d2
--- /dev/null
+++ b/_site/openstack/identity/v2/extensions/fixtures.go
@@ -0,0 +1,60 @@
+// +build fixtures
+
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single Extension result. It differs from the delegated implementation
+// by the introduction of an intermediate "values" member.
+const ListOutput = `
+{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+ }
+}
+`
+
+// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
+// call.
+func HandleListExtensionsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, `
+{
+ "extensions": {
+ "values": [
+ {
+ "updated": "2013-01-20T00:00:00-00:00",
+ "name": "Neutron Service Type Management",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
+ "alias": "service-type",
+ "description": "API for retrieving service providers for Neutron advanced services"
+ }
+ ]
+ }
+}
+ `)
+ })
+
+}
diff --git a/_site/openstack/identity/v2/tenants/doc.go b/_site/openstack/identity/v2/tenants/doc.go
new file mode 100644
index 0000000..473a302
--- /dev/null
+++ b/_site/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/_site/openstack/identity/v2/tenants/fixtures.go b/_site/openstack/identity/v2/tenants/fixtures.go
new file mode 100644
index 0000000..7f044ac
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/fixtures.go
@@ -0,0 +1,65 @@
+// +build fixtures
+
+package tenants
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput provides a single page of Tenant results.
+const ListOutput = `
+{
+ "tenants": [
+ {
+ "id": "1234",
+ "name": "Red Team",
+ "description": "The team that is red",
+ "enabled": true
+ },
+ {
+ "id": "9876",
+ "name": "Blue Team",
+ "description": "The team that is blue",
+ "enabled": false
+ }
+ ]
+}
+`
+
+// RedTeam is a Tenant fixture.
+var RedTeam = Tenant{
+ ID: "1234",
+ Name: "Red Team",
+ Description: "The team that is red",
+ Enabled: true,
+}
+
+// BlueTeam is a Tenant fixture.
+var BlueTeam = Tenant{
+ ID: "9876",
+ Name: "Blue Team",
+ Description: "The team that is blue",
+ Enabled: false,
+}
+
+// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
+var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
+
+// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
+// responds with a list of two tenants.
+func HandleListTenantsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, ListOutput)
+ })
+}
diff --git a/_site/openstack/identity/v2/tenants/requests.go b/_site/openstack/identity/v2/tenants/requests.go
new file mode 100644
index 0000000..5ffeaa7
--- /dev/null
+++ b/_site/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/_site/openstack/identity/v2/tenants/requests_test.go b/_site/openstack/identity/v2/tenants/requests_test.go
new file mode 100644
index 0000000..e8f172d
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/requests_test.go
@@ -0,0 +1,29 @@
+package tenants
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListTenants(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListTenantsSuccessfully(t)
+
+ count := 0
+ err := List(client.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ExpectedTenantSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
diff --git a/_site/openstack/identity/v2/tenants/results.go b/_site/openstack/identity/v2/tenants/results.go
new file mode 100644
index 0000000..c1220c3
--- /dev/null
+++ b/_site/openstack/identity/v2/tenants/results.go
@@ -0,0 +1,62 @@
+package tenants
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "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 resp struct {
+ Links []gophercloud.Link `mapstructure:"tenants_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(page.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(r.Links)
+}
+
+// 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/_site/openstack/identity/v2/tenants/urls.go b/_site/openstack/identity/v2/tenants/urls.go
new file mode 100644
index 0000000..1dd6ce0
--- /dev/null
+++ b/_site/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/_site/openstack/identity/v2/tokens/doc.go b/_site/openstack/identity/v2/tokens/doc.go
new file mode 100644
index 0000000..d26f642
--- /dev/null
+++ b/_site/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/_site/openstack/identity/v2/tokens/errors.go b/_site/openstack/identity/v2/tokens/errors.go
new file mode 100644
index 0000000..3a9172e
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/errors.go
@@ -0,0 +1,30 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+)
+
+var (
+ // ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
+ ErrUserIDProvided = unacceptedAttributeErr("UserID")
+
+ // ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // 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.")
+
+ // ErrPasswordRequired is returned if you don't provide a password.
+ ErrPasswordRequired = errors.New("Please supply a Password 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/_site/openstack/identity/v2/tokens/fixtures.go b/_site/openstack/identity/v2/tokens/fixtures.go
new file mode 100644
index 0000000..1cb0d05
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/fixtures.go
@@ -0,0 +1,128 @@
+// +build fixtures
+
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+// ExpectedToken is the token that should be parsed from TokenCreationResponse.
+var ExpectedToken = &Token{
+ ID: "aaaabbbbccccdddd",
+ ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
+ Tenant: tenants.Tenant{
+ ID: "fc394f2ab2df4114bde39905f800dc57",
+ Name: "test",
+ Description: "There are many tenants. This one is yours.",
+ Enabled: true,
+ },
+}
+
+// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
+var ExpectedServiceCatalog = &ServiceCatalog{
+ Entries: []CatalogEntry{
+ CatalogEntry{
+ Name: "inscrutablewalrus",
+ Type: "something",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://something0:1234/v2/",
+ Region: "region0",
+ },
+ Endpoint{
+ PublicURL: "http://something1:1234/v2/",
+ Region: "region1",
+ },
+ },
+ },
+ CatalogEntry{
+ Name: "arbitrarypenguin",
+ Type: "else",
+ Endpoints: []Endpoint{
+ Endpoint{
+ PublicURL: "http://else0:4321/v3/",
+ Region: "region0",
+ },
+ },
+ },
+ },
+}
+
+// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
+const TokenCreationResponse = `
+{
+ "access": {
+ "token": {
+ "issued_at": "2014-01-30T15:30:58.000000Z",
+ "expires": "2014-01-31T15:30:58Z",
+ "id": "aaaabbbbccccdddd",
+ "tenant": {
+ "description": "There are many tenants. This one is yours.",
+ "enabled": true,
+ "id": "fc394f2ab2df4114bde39905f800dc57",
+ "name": "test"
+ }
+ },
+ "serviceCatalog": [
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://something0:1234/v2/",
+ "region": "region0"
+ },
+ {
+ "publicURL": "http://something1:1234/v2/",
+ "region": "region1"
+ }
+ ],
+ "type": "something",
+ "name": "inscrutablewalrus"
+ },
+ {
+ "endpoints": [
+ {
+ "publicURL": "http://else0:4321/v3/",
+ "region": "region0"
+ }
+ ],
+ "type": "else",
+ "name": "arbitrarypenguin"
+ }
+ ]
+ }
+}
+`
+
+// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
+// constructed properly given certain auth options, and returns the result.
+func HandleTokenPost(t *testing.T, requestJSON string) {
+ th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ if requestJSON != "" {
+ th.TestJSONRequest(t, r, requestJSON)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, TokenCreationResponse)
+ })
+}
+
+// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
+// service catalog.
+func IsSuccessful(t *testing.T, result CreateResult) {
+ token, err := result.ExtractToken()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedToken, token)
+
+ serviceCatalog, err := result.ExtractServiceCatalog()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
+}
diff --git a/_site/openstack/identity/v2/tokens/requests.go b/_site/openstack/identity/v2/tokens/requests.go
new file mode 100644
index 0000000..c25a72b
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,87 @@
+package tokens
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// AuthOptionsBuilder describes any argument that may be passed to the Create call.
+type AuthOptionsBuilder interface {
+
+ // ToTokenCreateMap assembles the Create request body, returning an error if parameters are
+ // missing or inconsistent.
+ ToTokenCreateMap() (map[string]interface{}, error)
+}
+
+// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
+// interface.
+type AuthOptions struct {
+ gophercloud.AuthOptions
+}
+
+// WrapOptions embeds a root AuthOptions struct in a package-specific one.
+func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
+ return AuthOptions{AuthOptions: original}
+}
+
+// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
+// request.
+func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
+ // Error out if an unsupported auth option is present.
+ if auth.UserID != "" {
+ return nil, ErrUserIDProvided
+ }
+ if auth.APIKey != "" {
+ return nil, ErrAPIKeyProvided
+ }
+ if auth.DomainID != "" {
+ return nil, ErrDomainIDProvided
+ }
+ if auth.DomainName != "" {
+ return nil, ErrDomainNameProvided
+ }
+
+ // Username and Password are always required.
+ if auth.Username == "" {
+ return nil, ErrUsernameRequired
+ }
+ if auth.Password == "" {
+ return nil, ErrPasswordRequired
+ }
+
+ // Populate the request map.
+ authMap := make(map[string]interface{})
+
+ authMap["passwordCredentials"] = map[string]interface{}{
+ "username": auth.Username,
+ "password": auth.Password,
+ }
+
+ if auth.TenantID != "" {
+ authMap["tenantId"] = auth.TenantID
+ }
+ if auth.TenantName != "" {
+ authMap["tenantName"] = auth.TenantName
+ }
+
+ return map[string]interface{}{"auth": authMap}, nil
+}
+
+// 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 AuthOptionsBuilder) CreateResult {
+ request, err := auth.ToTokenCreateMap()
+ if err != nil {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+ }
+
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", CreateURL(client), perigee.Options{
+ ReqBody: &request,
+ Results: &result.Resp,
+ OkCodes: []int{200, 203},
+ })
+ return result
+}
diff --git a/_site/openstack/identity/v2/tokens/requests_test.go b/_site/openstack/identity/v2/tokens/requests_test.go
new file mode 100644
index 0000000..2f02825
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,140 @@
+package tokens
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleTokenPost(t, requestJSON)
+
+ return Create(client.ServiceClient(), AuthOptions{options})
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleTokenPost(t, "")
+
+ actualErr := Create(client.ServiceClient(), AuthOptions{options}).Err
+ th.CheckEquals(t, expectedErr, actualErr)
+}
+
+func TestCreateWithPassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "swordfish",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "passwordCredentials": {
+ "username": "me",
+ "password": "swordfish"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithTenantID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "opensesame",
+ TenantID: "fc394f2ab2df4114bde39905f800dc57",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "tenantId": "fc394f2ab2df4114bde39905f800dc57",
+ "passwordCredentials": {
+ "username": "me",
+ "password": "opensesame"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithTenantName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "opensesame",
+ TenantName: "demo",
+ }
+
+ IsSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "tenantName": "demo",
+ "passwordCredentials": {
+ "username": "me",
+ "password": "opensesame"
+ }
+ }
+ }
+ `))
+}
+
+func TestProhibitUserID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ UserID: "1234",
+ Password: "thing",
+ }
+
+ tokenPostErr(t, options, ErrUserIDProvided)
+}
+
+func TestProhibitAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ APIKey: "123412341234",
+ }
+
+ tokenPostErr(t, options, ErrAPIKeyProvided)
+}
+
+func TestProhibitDomainID(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ DomainID: "1234",
+ }
+
+ tokenPostErr(t, options, ErrDomainIDProvided)
+}
+
+func TestProhibitDomainName(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ DomainName: "wat",
+ }
+
+ tokenPostErr(t, options, ErrDomainNameProvided)
+}
+
+func TestRequireUsername(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Password: "thing",
+ }
+
+ tokenPostErr(t, options, ErrUsernameRequired)
+}
+
+func TestRequirePassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ }
+
+ tokenPostErr(t, options, ErrPasswordRequired)
+}
diff --git a/_site/openstack/identity/v2/tokens/results.go b/_site/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..e88b2c7
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/results.go
@@ -0,0 +1,133 @@
+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
+}
+
+// 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 `mapstructure:"tenantId"`
+ PublicURL string `mapstructure:"publicURL"`
+ InternalURL string `mapstructure:"internalURL"`
+ AdminURL string `mapstructure:"adminURL"`
+ Region string `mapstructure:"region"`
+ VersionID string `mapstructure:"versionId"`
+ VersionInfo string `mapstructure:"versionInfo"`
+ VersionList string `mapstructure:"versionList"`
+}
+
+// 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.
+//
+// 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 will contain the provider-specified name for the service.
+ Name string `mapstructure:"name"`
+
+ // Type will contain a type string if OpenStack defines a type for the service.
+ // Otherwise, for provider-specific services, the provider may assign their own type strings.
+ Type string `mapstructure:"type"`
+
+ // Endpoints will let the caller iterate over all the different endpoints that may exist for
+ // the service.
+ Endpoints []Endpoint `mapstructure:"endpoints"`
+}
+
+// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
+type ServiceCatalog struct {
+ Entries []CatalogEntry
+}
+
+// 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) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ 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
+}
+
+// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
+func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
+ if result.Err != nil {
+ return nil, result.Err
+ }
+
+ var response struct {
+ Access struct {
+ Entries []CatalogEntry `mapstructure:"serviceCatalog"`
+ } `mapstructure:"access"`
+ }
+
+ err := mapstructure.Decode(result.Resp, &response)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ServiceCatalog{Entries: response.Access.Entries}, nil
+}
+
+// createErr quickly packs an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{gophercloud.CommonResult{Err: err}}
+}
diff --git a/_site/openstack/identity/v2/tokens/urls.go b/_site/openstack/identity/v2/tokens/urls.go
new file mode 100644
index 0000000..cd4c696
--- /dev/null
+++ b/_site/openstack/identity/v2/tokens/urls.go
@@ -0,0 +1,8 @@
+package tokens
+
+import "github.com/rackspace/gophercloud"
+
+// CreateURL generates the URL used to create new Tokens.
+func CreateURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("tokens")
+}