Merge pull request #219 from smashwilson/update-identity-v2
Update Identity v2
diff --git a/.travis.yml b/.travis.yml
index b7f8d34..9c37aef 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,6 @@
language: go
install:
- - go get -v .
- - go get -v ./openstack/...
- - go get -v ./rackspace/...
+ - go get -v ./...
go:
- 1.1
- 1.2
@@ -13,4 +11,3 @@
- go get github.com/mattn/goveralls
- export PATH=$PATH:$HOME/gopath/bin/
- goveralls 2k7PTU3xa474Hymwgdj6XjqenNfGTNkO8
-
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
new file mode 100644
index 0000000..2b4e062
--- /dev/null
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -0,0 +1,46 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ extensions2 "github.com/rackspace/gophercloud/openstack/identity/v2/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnumerateExtensions(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Extensions available on this identity endpoint:")
+ count := 0
+ err := extensions2.List(service).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ extensions, err := extensions2.ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+
+ for i, ext := range extensions {
+ t.Logf("[%02d] name=[%s] namespace=[%s]", i, ext.Name, ext.Namespace)
+ t.Logf(" alias=[%s] updated=[%s]", ext.Alias, ext.Updated)
+ t.Logf(" description=[%s]", ext.Description)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
+
+func TestGetExtension(t *testing.T) {
+ service := authenticatedClient(t)
+
+ ext, err := extensions2.Get(service, "OS-KSCRUD").Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckEquals(t, "OpenStack Keystone User CRUD", ext.Name)
+ th.CheckEquals(t, "http://docs.openstack.org/identity/api/ext/OS-KSCRUD/v1.0", ext.Namespace)
+ th.CheckEquals(t, "OS-KSCRUD", ext.Alias)
+ th.CheckEquals(t, "OpenStack extensions to Keystone v2.0 API enabling User Operations.", ext.Description)
+}
diff --git a/acceptance/openstack/identity/v2/identity_test.go b/acceptance/openstack/identity/v2/identity_test.go
index ff4c9cd..2ecd3ca 100644
--- a/acceptance/openstack/identity/v2/identity_test.go
+++ b/acceptance/openstack/identity/v2/identity_test.go
@@ -3,112 +3,46 @@
package v2
import (
- "fmt"
- "os"
"testing"
- "text/tabwriter"
"github.com/rackspace/gophercloud"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
+ "github.com/rackspace/gophercloud/openstack"
"github.com/rackspace/gophercloud/openstack/utils"
+ th "github.com/rackspace/gophercloud/testhelper"
)
-type extractor func(*identity.Token) string
-
-func TestAuthentication(t *testing.T) {
- // Create an initialized set of authentication options based on available OS_*
- // environment variables.
+func v2AuthOptions(t *testing.T) gophercloud.AuthOptions {
+ // Obtain credentials from the environment.
ao, err := utils.AuthOptions()
- if err != nil {
- t.Error(err)
- return
+ th.AssertNoErr(t, err)
+
+ // Trim out unused fields. Prefer authentication by API key to password.
+ ao.UserID, ao.DomainID, ao.DomainName = "", "", ""
+ if ao.APIKey != "" {
+ ao.Password = ""
}
- // Attempt to authenticate with them.
- client := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
- r, err := identity.Authenticate(client, 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()
- }
+ return ao
}
-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
+func createClient(t *testing.T, auth bool) *gophercloud.ServiceClient {
+ ao := v2AuthOptions(t)
+
+ provider, err := openstack.NewClient(ao.IdentityEndpoint)
+ th.AssertNoErr(t, err)
+
+ if auth {
+ err = openstack.AuthenticateV2(provider, ao)
+ th.AssertNoErr(t, err)
}
- // Attempt to query extensions.
- client := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
- exts, err := identity.GetExtensions(client, ao)
- if err != nil {
- t.Error(err)
- return
- }
+ return openstack.NewIdentityV2(provider)
+}
- // 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)
- }
+func unauthenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, false)
+}
+
+func authenticatedClient(t *testing.T) *gophercloud.ServiceClient {
+ return createClient(t, true)
}
diff --git a/acceptance/openstack/identity/v2/pkg.go b/acceptance/openstack/identity/v2/pkg.go
new file mode 100644
index 0000000..5ec3cc8
--- /dev/null
+++ b/acceptance/openstack/identity/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
new file mode 100644
index 0000000..2054598
--- /dev/null
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -0,0 +1,32 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ tenants2 "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnumerateTenants(t *testing.T) {
+ service := authenticatedClient(t)
+
+ t.Logf("Tenants to which your current token grants access:")
+ count := 0
+ err := tenants2.List(service, nil).EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page %02d ---", count)
+
+ tenants, err := tenants2.ExtractTenants(page)
+ th.AssertNoErr(t, err)
+ for i, tenant := range tenants {
+ t.Logf("[%02d] name=[%s] id=[%s] description=[%s] enabled=[%v]",
+ i, tenant.Name, tenant.ID, tenant.Description, tenant.Enabled)
+ }
+
+ count++
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
new file mode 100644
index 0000000..47381a2
--- /dev/null
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -0,0 +1,38 @@
+// +build acceptance
+
+package v2
+
+import (
+ "testing"
+
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestAuthenticate(t *testing.T) {
+ ao := v2AuthOptions(t)
+ service := unauthenticatedClient(t)
+
+ // Authenticated!
+ result := tokens2.Create(service, ao)
+
+ // Extract and print the token.
+ token, err := result.ExtractToken()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Acquired token: [%s]", token.ID)
+ t.Logf("The token will expire at: [%s]", token.ExpiresAt.String())
+ t.Logf("The token is valid for tenant: [%#v]", token.Tenant)
+
+ // Extract and print the service catalog.
+ catalog, err := result.ExtractServiceCatalog()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Acquired service catalog listing [%d] services", len(catalog.Entries))
+ for i, entry := range catalog.Entries {
+ t.Logf("[%02d]: name=[%s], type=[%s]", i, entry.Name, entry.Type)
+ for _, endpoint := range entry.Endpoints {
+ t.Logf(" - region=[%s] publicURL=[%s]", endpoint.Region, endpoint.PublicURL)
+ }
+ }
+}
diff --git a/acceptance/openstack/identity/v3/identity_test.go b/acceptance/openstack/identity/v3/identity_test.go
index ec184a0..e0503e2 100644
--- a/acceptance/openstack/identity/v3/identity_test.go
+++ b/acceptance/openstack/identity/v3/identity_test.go
@@ -1,3 +1,5 @@
+// +build acceptance
+
package v3
import (
diff --git a/openstack/client.go b/openstack/client.go
index 1b057d0..f3638be 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -6,7 +6,7 @@
"strings"
"github.com/rackspace/gophercloud"
- identity2 "github.com/rackspace/gophercloud/openstack/identity/v2"
+ tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
@@ -99,38 +99,30 @@
v2Client.Endpoint = endpoint
}
- result, err := identity2.Authenticate(v2Client, options)
+ result := tokens2.Create(v2Client, options)
+
+ token, err := result.ExtractToken()
if err != nil {
return err
}
- token, err := identity2.GetToken(result)
+ catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
- return v2endpointLocator(result, opts)
+ return v2endpointLocator(catalog, opts)
}
return nil
}
-func v2endpointLocator(authResults identity2.AuthResults, opts gophercloud.EndpointOpts) (string, error) {
- catalog, err := identity2.GetServiceCatalog(authResults)
- if err != nil {
- return "", err
- }
-
- entries, err := catalog.CatalogEntries()
- if err != nil {
- return "", err
- }
-
+func v2endpointLocator(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
// Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
- var endpoints = make([]identity2.Endpoint, 0, 1)
- for _, entry := range entries {
+ var endpoints = make([]tokens2.Endpoint, 0, 1)
+ for _, entry := range catalog.Entries {
if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
for _, endpoint := range entry.Endpoints {
if opts.Region == "" || endpoint.Region == opts.Region {
@@ -155,6 +147,8 @@
return normalizeURL(endpoint.PublicURL), nil
case gophercloud.AvailabilityInternal:
return normalizeURL(endpoint.InternalURL), nil
+ case gophercloud.AvailabilityAdmin:
+ return normalizeURL(endpoint.AdminURL), nil
default:
return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
}
@@ -308,7 +302,11 @@
if err != nil {
return nil, err
}
- return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+ return &gophercloud.ServiceClient{
+ Provider: client,
+ Endpoint: url,
+ ResourceBase: url + "v2.0/",
+ }, nil
}
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
diff --git a/openstack/client_test.go b/openstack/client_test.go
index dd39e77..257260c 100644
--- a/openstack/client_test.go
+++ b/openstack/client_test.go
@@ -6,16 +6,16 @@
"testing"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/testhelper"
+ th "github.com/rackspace/gophercloud/testhelper"
)
func TestAuthenticatedClientV3(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
const ID = "0123456789"
- testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
{
"versions": {
@@ -37,10 +37,10 @@
]
}
}
- `, testhelper.Endpoint()+"v3/", testhelper.Endpoint()+"v2.0/")
+ `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
})
- testhelper.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Subject-Token", ID)
w.WriteHeader(http.StatusCreated)
@@ -50,24 +50,18 @@
options := gophercloud.AuthOptions{
UserID: "me",
Password: "secret",
- IdentityEndpoint: testhelper.Endpoint(),
+ IdentityEndpoint: th.Endpoint(),
}
client, err := AuthenticatedClient(options)
-
- if err != nil {
- t.Fatalf("Unexpected error from AuthenticatedClient: %s", err)
- }
-
- if client.TokenID != ID {
- t.Errorf("Expected token ID to be [%s], but was [%s]", ID, client.TokenID)
- }
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, ID, client.TokenID)
}
func TestAuthenticatedClientV2(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
- testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
{
"versions": {
@@ -89,16 +83,16 @@
]
}
}
- `, testhelper.Endpoint()+"v3/", testhelper.Endpoint()+"v2.0/")
+ `, th.Endpoint()+"v3/", th.Endpoint()+"v2.0/")
})
- testhelper.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusCreated)
+ th.Mux.HandleFunc("/v2.0/tokens", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
{
"access": {
"token": {
- "id": "01234567890"
+ "id": "01234567890",
+ "expires": "2014-10-01T10:00:00.000000Z"
},
"serviceCatalog": [
{
@@ -159,15 +153,9 @@
options := gophercloud.AuthOptions{
Username: "me",
Password: "secret",
- IdentityEndpoint: testhelper.Endpoint(),
+ IdentityEndpoint: th.Endpoint(),
}
client, err := AuthenticatedClient(options)
-
- if err != nil {
- t.Fatalf("Unexpected error from AuthenticatedClient: %s", err)
- }
-
- if client.TokenID != "01234567890" {
- t.Errorf("Expected token ID to be [01234567890], but was [%s]", client.TokenID)
- }
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, "01234567890", client.TokenID)
}
diff --git a/openstack/common/README.md b/openstack/common/README.md
new file mode 100644
index 0000000..7b55795
--- /dev/null
+++ b/openstack/common/README.md
@@ -0,0 +1,3 @@
+# Common Resources
+
+This directory is for resources that are shared by multiple services.
diff --git a/openstack/common/extensions/doc.go b/openstack/common/extensions/doc.go
new file mode 100644
index 0000000..4a168f4
--- /dev/null
+++ b/openstack/common/extensions/doc.go
@@ -0,0 +1,15 @@
+// Package extensions provides information and interaction with the different extensions available
+// for an OpenStack service.
+//
+// The purpose of OpenStack API extensions is to:
+//
+// - Introduce new features in the API without requiring a version change.
+// - Introduce vendor-specific niche functionality.
+// - Act as a proving ground for experimental functionalities that might be included in a future
+// version of the API.
+//
+// Extensions usually have tags that prevent conflicts with other extensions that define attributes
+// or resources with the same names, and with core resources and attributes.
+// Because an extension might not be supported by all plug-ins, its availability varies with deployments
+// and the specific plug-in.
+package extensions
diff --git a/openstack/networking/v2/extensions/errors.go b/openstack/common/extensions/errors.go
similarity index 100%
rename from openstack/networking/v2/extensions/errors.go
rename to openstack/common/extensions/errors.go
diff --git a/openstack/networking/v2/extensions/requests.go b/openstack/common/extensions/requests.go
similarity index 76%
rename from openstack/networking/v2/extensions/requests.go
rename to openstack/common/extensions/requests.go
index 2d3dc65..000151b 100755
--- a/openstack/networking/v2/extensions/requests.go
+++ b/openstack/common/extensions/requests.go
@@ -9,7 +9,7 @@
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) GetResult {
var res GetResult
- _, res.Err = perigee.Request("GET", extensionURL(c, alias), perigee.Options{
+ _, res.Err = perigee.Request("GET", ExtensionURL(c, alias), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
Results: &res.Resp,
OkCodes: []int{200},
@@ -17,10 +17,10 @@
return res
}
-// List returns a Pager which allows you to iterate over the full collection of
-// extensions. It does not accept query parameters.
+// 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 pagination.NewPager(c, listExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+ return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
return ExtensionPage{pagination.SinglePageBase(r)}
})
}
diff --git a/openstack/common/extensions/requests_test.go b/openstack/common/extensions/requests_test.go
new file mode 100644
index 0000000..b0f655a
--- /dev/null
+++ b/openstack/common/extensions/requests_test.go
@@ -0,0 +1,113 @@
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ fmt.Fprintf(w, `
+{
+ "extensions": [
+ {
+ "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"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ if err != nil {
+ t.Errorf("Failed to extract extensions: %v", err)
+ }
+
+ expected := []Extension{
+ Extension{
+ Updated: "2013-01-20T00:00:00-00:00",
+ Name: "Neutron Service Type Management",
+ Links: []interface{}{},
+ 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",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "extension": {
+ "updated": "2013-02-03T10:00:00-00:00",
+ "name": "agent",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
+ "alias": "agent",
+ "description": "The agent management extension."
+ }
+}
+ `)
+
+ ext, err := Get(ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
+ th.AssertEquals(t, ext.Name, "agent")
+ th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
+ th.AssertEquals(t, ext.Alias, "agent")
+ th.AssertEquals(t, ext.Description, "The agent management extension.")
+ })
+}
diff --git a/openstack/networking/v2/extensions/results.go b/openstack/common/extensions/results.go
similarity index 74%
rename from openstack/networking/v2/extensions/results.go
rename to openstack/common/extensions/results.go
index 2b8408d..2b8d8b7 100755
--- a/openstack/networking/v2/extensions/results.go
+++ b/openstack/common/extensions/results.go
@@ -8,10 +8,13 @@
"github.com/rackspace/gophercloud/pagination"
)
+// GetResult temporarility stores the result of a Get call.
+// Use its Extract() method to interpret it as an Extension.
type GetResult struct {
gophercloud.CommonResult
}
+// Extract interprets a GetResult as an Extension.
func (r GetResult) Extract() (*Extension, error) {
if r.Err != nil {
return nil, r.Err
@@ -23,13 +26,13 @@
err := mapstructure.Decode(r.Resp, &res)
if err != nil {
- return nil, fmt.Errorf("Error decoding Neutron extension: %v", err)
+ return nil, fmt.Errorf("Error decoding OpenStack extension: %v", err)
}
return res.Extension, nil
}
-// Extension is a struct that represents a Neutron extension.
+// Extension is a struct that represents an OpenStack extension.
type Extension struct {
Updated string `json:"updated"`
Name string `json:"name"`
@@ -39,8 +42,7 @@
Description string `json:"description"`
}
-// ExtensionPage is the page returned by a pager when traversing over a
-// collection of extensions.
+// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
type ExtensionPage struct {
pagination.SinglePageBase
}
@@ -54,9 +56,9 @@
return len(is) == 0, nil
}
-// ExtractExtensions accepts a Page struct, specifically an ExtensionPage
-// struct, and extracts the elements into a slice of Extension structs. In other
-// words, a generic collection is mapped into a relevant slice.
+// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
+// elements into a slice of Extension structs.
+// In other words, a generic collection is mapped into a relevant slice.
func ExtractExtensions(page pagination.Page) ([]Extension, error) {
var resp struct {
Extensions []Extension `mapstructure:"extensions"`
diff --git a/openstack/common/extensions/urls.go b/openstack/common/extensions/urls.go
new file mode 100644
index 0000000..6460c66
--- /dev/null
+++ b/openstack/common/extensions/urls.go
@@ -0,0 +1,13 @@
+package extensions
+
+import "github.com/rackspace/gophercloud"
+
+// ExtensionURL generates the URL for an extension resource by name.
+func ExtensionURL(c *gophercloud.ServiceClient, name string) string {
+ return c.ServiceURL("extensions", name)
+}
+
+// ListExtensionURL generates the URL for the extensions resource collection.
+func ListExtensionURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("extensions")
+}
diff --git a/openstack/networking/v2/extensions/urls_test.go b/openstack/common/extensions/urls_test.go
similarity index 70%
rename from openstack/networking/v2/extensions/urls_test.go
rename to openstack/common/extensions/urls_test.go
index 2a1e6a1..3223b1c 100755
--- a/openstack/networking/v2/extensions/urls_test.go
+++ b/openstack/common/extensions/urls_test.go
@@ -14,13 +14,13 @@
}
func TestExtensionURL(t *testing.T) {
- actual := extensionURL(endpointClient(), "agent")
- expected := endpoint + "v2.0/extensions/agent"
+ actual := ExtensionURL(endpointClient(), "agent")
+ expected := endpoint + "extensions/agent"
th.AssertEquals(t, expected, actual)
}
func TestListExtensionURL(t *testing.T) {
- actual := listExtensionURL(endpointClient())
- expected := endpoint + "v2.0/extensions"
+ actual := ListExtensionURL(endpointClient())
+ expected := endpoint + "extensions"
th.AssertEquals(t, expected, actual)
}
diff --git a/openstack/identity/v2/common_test.go b/openstack/identity/v2/common_test.go
deleted file mode 100644
index 0260424..0000000
--- a/openstack/identity/v2/common_test.go
+++ /dev/null
@@ -1,224 +0,0 @@
-package v2
-
-// 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
deleted file mode 100644
index e8ab21b..0000000
--- a/openstack/identity/v2/doc.go
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
-Package v2 identity 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 v2
diff --git a/openstack/identity/v2/errors.go b/openstack/identity/v2/errors.go
deleted file mode 100644
index 0dd1172..0000000
--- a/openstack/identity/v2/errors.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package v2
-
-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
deleted file mode 100644
index df05a36..0000000
--- a/openstack/identity/v2/extensions.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package v2
-
-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/delegate.go b/openstack/identity/v2/extensions/delegate.go
new file mode 100644
index 0000000..1992a2c
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate.go
@@ -0,0 +1,85 @@
+package extensions
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Extension is a single OpenStack extension.
+type Extension struct {
+ common.Extension
+}
+
+// GetResult wraps a GetResult from common.
+type GetResult struct {
+ common.GetResult
+}
+
+// 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) ([]Extension, error) {
+ // Identity v2 adds an intermediate "values" object.
+
+ type extension struct {
+ Updated string `mapstructure:"updated"`
+ Name string `mapstructure:"name"`
+ Namespace string `mapstructure:"namespace"`
+ Alias string `mapstructure:"alias"`
+ Description string `mapstructure:"description"`
+ Links []interface{} `mapstructure:"links"`
+ }
+
+ var resp struct {
+ Extensions struct {
+ Values []extension `mapstructure:"values"`
+ } `mapstructure:"extensions"`
+ }
+
+ err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ exts := make([]Extension, len(resp.Extensions.Values))
+ for i, original := range resp.Extensions.Values {
+ exts[i] = Extension{common.Extension{
+ Updated: original.Updated,
+ Name: original.Name,
+ Namespace: original.Namespace,
+ Alias: original.Alias,
+ Description: original.Description,
+ Links: original.Links,
+ }}
+ }
+
+ return exts, err
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) GetResult {
+ return GetResult{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 pagination.NewPager(c, common.ListExtensionURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+ return ExtensionPage{common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)}}
+ })
+}
diff --git a/openstack/identity/v2/extensions/delegate_test.go b/openstack/identity/v2/extensions/delegate_test.go
new file mode 100644
index 0000000..90eb21b
--- /dev/null
+++ b/openstack/identity/v2/extensions/delegate_test.go
@@ -0,0 +1,114 @@
+package extensions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", 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"
+ }
+ ]
+ }
+}
+ `)
+ })
+
+ count := 0
+
+ err := List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractExtensions(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Extension{
+ Extension{
+ common.Extension{
+ Updated: "2013-01-20T00:00:00-00:00",
+ Name: "Neutron Service Type Management",
+ Links: []interface{}{},
+ 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",
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "extension": {
+ "updated": "2013-02-03T10:00:00-00:00",
+ "name": "agent",
+ "links": [],
+ "namespace": "http://docs.openstack.org/ext/agent/api/v2.0",
+ "alias": "agent",
+ "description": "The agent management extension."
+ }
+}
+ `)
+
+ ext, err := Get(ServiceClient(), "agent").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
+ th.AssertEquals(t, ext.Name, "agent")
+ th.AssertEquals(t, ext.Namespace, "http://docs.openstack.org/ext/agent/api/v2.0")
+ th.AssertEquals(t, ext.Alias, "agent")
+ th.AssertEquals(t, ext.Description, "The agent management extension.")
+ })
+}
diff --git a/openstack/identity/v2/extensions_test.go b/openstack/identity/v2/extensions_test.go
deleted file mode 100644
index 47f7e06..0000000
--- a/openstack/identity/v2/extensions_test.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package v2
-
-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
deleted file mode 100644
index bb068b6..0000000
--- a/openstack/identity/v2/requests.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package v2
-
-import (
- "github.com/racker/perigee"
- "github.com/rackspace/gophercloud"
-)
-
-// 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{}
-
-// 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(c *gophercloud.ServiceClient, options gophercloud.AuthOptions) (AuthResults, error) {
- type AuthContainer struct {
- Auth auth `json:"auth"`
- }
-
- var ar AuthResults
-
- if c.Endpoint == "" {
- return nil, ErrEndpoint
- }
-
- if (options.Username == "") || (options.Password == "" && options.APIKey == "") {
- return nil, ErrCredentials
- }
-
- url := c.Endpoint + "tokens"
- err := perigee.Post(url, perigee.Options{
- ReqBody: &AuthContainer{
- Auth: getAuthCredentials(options),
- },
- Results: &ar,
- })
- return ar, err
-}
-
-func getAuthCredentials(options gophercloud.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,
- }
- }
- 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"`
-}
-
-// GetExtensions returns the OpenStack extensions available from this service.
-func GetExtensions(c *gophercloud.ServiceClient, options gophercloud.AuthOptions) (ExtensionsResult, error) {
- var exts ExtensionsResult
-
- url := c.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
deleted file mode 100644
index 22d0d7b..0000000
--- a/openstack/identity/v2/service_catalog.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package v2
-
-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
deleted file mode 100644
index 143b48f..0000000
--- a/openstack/identity/v2/service_catalog_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package v2
-
-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/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/requests_test.go b/openstack/identity/v2/tenants/requests_test.go
new file mode 100644
index 0000000..685bf96
--- /dev/null
+++ b/openstack/identity/v2/tenants/requests_test.go
@@ -0,0 +1,79 @@
+package tenants
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "1234123412341234"
+
+func TestListTenants(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", tokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "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
+ }
+ ]
+}
+ `)
+ })
+
+ client := &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: th.Endpoint(),
+ }
+
+ count := 0
+ err := List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ expected := []Tenant{
+ Tenant{
+ ID: "1234",
+ Name: "Red Team",
+ Description: "The team that is red",
+ Enabled: true,
+ },
+ Tenant{
+ ID: "9876",
+ Name: "Blue Team",
+ Description: "The team that is blue",
+ Enabled: false,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
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/token.go b/openstack/identity/v2/token.go
deleted file mode 100644
index df67d76..0000000
--- a/openstack/identity/v2/token.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package v2
-
-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 yields an unpacked collection of fields related to the user's access credentials, called a "token", if successful.
-// 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
deleted file mode 100644
index 9770ed5..0000000
--- a/openstack/identity/v2/token_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package v2
-
-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/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..5bd5a70
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests.go
@@ -0,0 +1,80 @@
+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 {
+ type passwordCredentials struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }
+
+ type apiKeyCredentials struct {
+ Username string `json:"username"`
+ APIKey string `json:"apiKey"`
+ }
+
+ var request struct {
+ Auth struct {
+ PasswordCredentials *passwordCredentials `json:"passwordCredentials,omitempty"`
+ APIKeyCredentials *apiKeyCredentials `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 = &passwordCredentials{
+ Username: auth.Username,
+ Password: auth.Password,
+ }
+ } else if auth.APIKey != "" {
+ // API key authentication.
+ request.Auth.APIKeyCredentials = &apiKeyCredentials{
+ Username: auth.Username,
+ 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/requests_test.go b/openstack/identity/v2/tokens/requests_test.go
new file mode 100644
index 0000000..0e6269f
--- /dev/null
+++ b/openstack/identity/v2/tokens/requests_test.go
@@ -0,0 +1,268 @@
+package tokens
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+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,
+ },
+}
+
+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",
+ },
+ },
+ },
+ },
+}
+
+func tokenPost(t *testing.T, options gophercloud.AuthOptions, requestJSON string) CreateResult {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{Endpoint: th.Endpoint()}
+
+ 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")
+ th.TestJSONRequest(t, r, requestJSON)
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "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"
+ }
+ ]
+ }
+}
+ `)
+ })
+
+ return Create(&client, options)
+}
+
+func tokenPostErr(t *testing.T, options gophercloud.AuthOptions, expectedErr error) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ client := gophercloud.ServiceClient{Endpoint: th.Endpoint()}
+
+ 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")
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `{}`)
+ })
+
+ actualErr := Create(&client, options).Err
+ th.CheckEquals(t, expectedErr, actualErr)
+}
+
+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)
+}
+
+func TestCreateWithPassword(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "swordfish",
+ }
+
+ isSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "passwordCredentials": {
+ "username": "me",
+ "password": "swordfish"
+ }
+ }
+ }
+ `))
+}
+
+func TestCreateTokenWithAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ APIKey: "1234567890abcdef",
+ }
+
+ isSuccessful(t, tokenPost(t, options, `
+ {
+ "auth": {
+ "RAX-KSKEY:apiKeyCredentials": {
+ "username": "me",
+ "apiKey": "1234567890abcdef"
+ }
+ }
+ }
+ `))
+}
+
+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 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 TestProhibitBothPasswordAndAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ Password: "thing",
+ APIKey: "123412341234",
+ }
+ tokenPostErr(t, options, ErrPasswordOrAPIKey)
+}
+
+func TestRequirePasswordOrAPIKey(t *testing.T) {
+ options := gophercloud.AuthOptions{
+ Username: "me",
+ }
+ tokenPostErr(t, options, ErrPasswordOrAPIKey)
+}
diff --git a/openstack/identity/v2/tokens/results.go b/openstack/identity/v2/tokens/results.go
new file mode 100644
index 0000000..e88b2c7
--- /dev/null
+++ b/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/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")
+}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index c970c67..e96da51 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -8,9 +8,6 @@
"github.com/rackspace/gophercloud"
)
-// RFC3339Milli describes the time format used by identity API responses.
-const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
-
// commonResult is the deferred result of a Create or a Get call.
type commonResult struct {
gophercloud.CommonResult
@@ -42,7 +39,7 @@
}
// Attempt to parse the timestamp.
- token.ExpiresAt, err = time.Parse(RFC3339Milli, response.Token.ExpiresAt)
+ token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
if err != nil {
return nil, err
}
diff --git a/openstack/networking/v2/extensions/delegate.go b/openstack/networking/v2/extensions/delegate.go
new file mode 100644
index 0000000..d08e1fd
--- /dev/null
+++ b/openstack/networking/v2/extensions/delegate.go
@@ -0,0 +1,41 @@
+package extensions
+
+import (
+ "github.com/rackspace/gophercloud"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Extension is a single OpenStack extension.
+type Extension struct {
+ common.Extension
+}
+
+// GetResult wraps a GetResult from common.
+type GetResult struct {
+ common.GetResult
+}
+
+// ExtractExtensions interprets a Page as a slice of Extensions.
+func ExtractExtensions(page pagination.Page) ([]Extension, error) {
+ inner, err := common.ExtractExtensions(page)
+ if err != nil {
+ return nil, err
+ }
+ outer := make([]Extension, len(inner))
+ for index, ext := range inner {
+ outer[index] = Extension{ext}
+ }
+ return outer, nil
+}
+
+// Get retrieves information for a specific extension using its alias.
+func Get(c *gophercloud.ServiceClient, alias string) GetResult {
+ return GetResult{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)
+}
diff --git a/openstack/networking/v2/extensions/requests_test.go b/openstack/networking/v2/extensions/delegate_test.go
similarity index 77%
rename from openstack/networking/v2/extensions/requests_test.go
rename to openstack/networking/v2/extensions/delegate_test.go
index db21772..8de8906 100755
--- a/openstack/networking/v2/extensions/requests_test.go
+++ b/openstack/networking/v2/extensions/delegate_test.go
@@ -5,6 +5,7 @@
"net/http"
"testing"
+ common "github.com/rackspace/gophercloud/openstack/common/extensions"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
@@ -14,7 +15,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/extensions", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -33,7 +34,7 @@
}
]
}
- `)
+ `)
})
count := 0
@@ -47,12 +48,14 @@
expected := []Extension{
Extension{
- Updated: "2013-01-20T00:00:00-00:00",
- Name: "Neutron Service Type Management",
- Links: []interface{}{},
- 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",
+ common.Extension{
+ Updated: "2013-01-20T00:00:00-00:00",
+ Name: "Neutron Service Type Management",
+ Links: []interface{}{},
+ 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",
+ },
},
}
@@ -70,7 +73,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -88,7 +91,7 @@
"description": "The agent management extension."
}
}
- `)
+ `)
ext, err := Get(fake.ServiceClient(), "agent").Extract()
th.AssertNoErr(t, err)
diff --git a/openstack/networking/v2/extensions/doc.go b/openstack/networking/v2/extensions/doc.go
deleted file mode 100755
index 7942c39..0000000
--- a/openstack/networking/v2/extensions/doc.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Package extensions provides information and interaction with the different
-// extensions available for the OpenStack Neutron service.
-//
-// The purpose of Networking API v2.0 extensions is to:
-//
-// - Introduce new features in the API without requiring a version change.
-// - Introduce vendor-specific niche functionality.
-// - Act as a proving ground for experimental functionalities that might be
-// included in a future version of the API.
-//
-// Extensions usually have tags that prevent conflicts with other extensions
-// that define attributes or resources with the same names, and with core
-// resources and attributes. Because an extension might not be supported by all
-// plug-ins, its availability varies with deployments and the specific plug-in.
-package extensions
diff --git a/openstack/networking/v2/extensions/external/results_test.go b/openstack/networking/v2/extensions/external/results_test.go
index 41bc0c8..6bed126 100644
--- a/openstack/networking/v2/extensions/external/results_test.go
+++ b/openstack/networking/v2/extensions/external/results_test.go
@@ -15,7 +15,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -101,7 +101,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -137,7 +137,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -186,7 +186,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
diff --git a/openstack/networking/v2/extensions/provider/results_test.go b/openstack/networking/v2/extensions/provider/results_test.go
index ec0b528..74951d9 100644
--- a/openstack/networking/v2/extensions/provider/results_test.go
+++ b/openstack/networking/v2/extensions/provider/results_test.go
@@ -15,7 +15,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -109,7 +109,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -150,7 +150,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -202,7 +202,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
diff --git a/openstack/networking/v2/extensions/urls.go b/openstack/networking/v2/extensions/urls.go
deleted file mode 100755
index e31e76c..0000000
--- a/openstack/networking/v2/extensions/urls.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package extensions
-
-import "github.com/rackspace/gophercloud"
-
-const version = "v2.0"
-
-func extensionURL(c *gophercloud.ServiceClient, name string) string {
- return c.ServiceURL(version, "extensions", name)
-}
-
-func listExtensionURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, "extensions")
-}
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
index 03c297f..6b22acd 100644
--- a/openstack/networking/v2/networks/requests_test.go
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -14,7 +14,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -97,7 +97,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -137,7 +137,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -187,7 +187,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -216,7 +216,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -264,7 +264,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/networks/urls.go b/openstack/networking/v2/networks/urls.go
index 80c307c..33c2387 100644
--- a/openstack/networking/v2/networks/urls.go
+++ b/openstack/networking/v2/networks/urls.go
@@ -2,14 +2,12 @@
import "github.com/rackspace/gophercloud"
-const Version = "v2.0"
-
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(Version, "networks", id)
+ return c.ServiceURL("networks", id)
}
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(Version, "networks")
+ return c.ServiceURL("networks")
}
func getURL(c *gophercloud.ServiceClient, id string) string {
diff --git a/openstack/networking/v2/networks/urls_test.go b/openstack/networking/v2/networks/urls_test.go
index 713a547..caf77db 100644
--- a/openstack/networking/v2/networks/urls_test.go
+++ b/openstack/networking/v2/networks/urls_test.go
@@ -10,7 +10,7 @@
const endpoint = "http://localhost:57909/"
func endpointClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{Endpoint: endpoint}
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
}
func TestGetURL(t *testing.T) {
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
index 662d364..7576341 100644
--- a/openstack/networking/v2/ports/requests_test.go
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -14,7 +14,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -93,7 +93,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -147,7 +147,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/ports", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -215,7 +215,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -288,7 +288,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/ports/urls.go b/openstack/networking/v2/ports/urls.go
index 558b399..6d0572f 100644
--- a/openstack/networking/v2/ports/urls.go
+++ b/openstack/networking/v2/ports/urls.go
@@ -2,14 +2,12 @@
import "github.com/rackspace/gophercloud"
-const version = "v2.0"
-
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, "ports", id)
+ return c.ServiceURL("ports", id)
}
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, "ports")
+ return c.ServiceURL("ports")
}
func listURL(c *gophercloud.ServiceClient) string {
diff --git a/openstack/networking/v2/ports/urls_tests.go b/openstack/networking/v2/ports/urls_tests.go
index 6fb20aa..7fadd4d 100644
--- a/openstack/networking/v2/ports/urls_tests.go
+++ b/openstack/networking/v2/ports/urls_tests.go
@@ -10,7 +10,7 @@
const endpoint = "http://localhost:57909/"
func endpointClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{Endpoint: endpoint}
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
}
func TestListURL(t *testing.T) {
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index c7562f7..9f3e8df 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -14,7 +14,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -128,7 +128,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
@@ -184,7 +184,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/subnets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -252,7 +252,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
@@ -304,7 +304,7 @@
th.SetupHTTP()
defer th.TeardownHTTP()
- th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
+ th.Mux.HandleFunc("/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
diff --git a/openstack/networking/v2/subnets/urls.go b/openstack/networking/v2/subnets/urls.go
index ca70b66..0d02368 100644
--- a/openstack/networking/v2/subnets/urls.go
+++ b/openstack/networking/v2/subnets/urls.go
@@ -2,14 +2,12 @@
import "github.com/rackspace/gophercloud"
-const version = "v2.0"
-
func resourceURL(c *gophercloud.ServiceClient, id string) string {
- return c.ServiceURL(version, "subnets", id)
+ return c.ServiceURL("subnets", id)
}
func rootURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL(version, "subnets")
+ return c.ServiceURL("subnets")
}
func listURL(c *gophercloud.ServiceClient) string {
diff --git a/openstack/networking/v2/subnets/urls_tests.go b/openstack/networking/v2/subnets/urls_tests.go
index b04b432..aeeddf3 100644
--- a/openstack/networking/v2/subnets/urls_tests.go
+++ b/openstack/networking/v2/subnets/urls_tests.go
@@ -10,7 +10,7 @@
const endpoint = "http://localhost:57909/"
func endpointClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{Endpoint: endpoint}
+ return &gophercloud.ServiceClient{Endpoint: endpoint, ResourceBase: endpoint + "v2.0/"}
}
func TestListURL(t *testing.T) {
diff --git a/openstack/utils/client.go b/openstack/utils/client.go
deleted file mode 100644
index f8a6c57..0000000
--- a/openstack/utils/client.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package utils
-
-import (
- "fmt"
-
- "github.com/rackspace/gophercloud"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-// Client contains information that defines a generic Openstack Client.
-type Client struct {
- // Endpoint is the URL against which to authenticate.
- Endpoint string
- // Authority holds the results of authenticating against the Endpoint.
- Authority identity.AuthResults
- // Options holds the authentication options. Useful for auto-reauthentication.
- Options gophercloud.AuthOptions
-}
-
-// EndpointOpts contains options for finding an endpoint for an Openstack Client.
-type EndpointOpts struct {
- // Type is the service type for the client (e.g., "compute", "object-store").
- // Type is a required field.
- Type string
- // Name is the service name for the client (e.g., "nova").
- // Name is not a required field, but it is used if present. Services can have the
- // same Type but different Name, which is one example of when both Type and Name are needed.
- Name string
- // Region is the region in which the service resides.
- Region string
- // URLType is they type of endpoint to be returned (e.g., "public", "private").
- // URLType is not required, and defaults to "public".
- URLType string
-}
-
-// NewClient returns a generic Openstack Client of type identity.Client. This is a helper function
-// to create a client for the various Openstack services.
-// Example (error checking omitted for brevity):
-// ao, err := utils.AuthOptions()
-// c, err := identity.NewClient(ao, identity.EndpointOpts{
-// Type: "compute",
-// Name: "nova",
-// })
-// serversClient := servers.NewClient(c.Endpoint, c.Authority, c.Options)
-func NewClient(ao gophercloud.AuthOptions, eo EndpointOpts) (Client, error) {
- client := Client{
- Options: ao,
- }
-
- c := &gophercloud.ServiceClient{Endpoint: ao.IdentityEndpoint + "/"}
- ar, err := identity.Authenticate(c, ao)
- if err != nil {
- return client, err
- }
-
- client.Authority = ar
-
- sc, err := identity.GetServiceCatalog(ar)
- if err != nil {
- return client, err
- }
-
- ces, err := sc.CatalogEntries()
- if err != nil {
- return client, err
- }
-
- var eps []identity.Endpoint
-
- if eo.Name != "" {
- for _, ce := range ces {
- if ce.Type == eo.Type && ce.Name == eo.Name {
- eps = ce.Endpoints
- }
- }
- } else {
- for _, ce := range ces {
- if ce.Type == eo.Type {
- eps = ce.Endpoints
- }
- }
- }
-
- var rep string
- for _, ep := range eps {
- if ep.Region == eo.Region {
- switch eo.URLType {
- case "public":
- rep = ep.PublicURL
- case "private":
- rep = ep.InternalURL
- default:
- rep = ep.PublicURL
- }
- }
- }
-
- if rep != "" {
- client.Endpoint = rep
- } else {
- return client, fmt.Errorf("No endpoint for given service type (%s) name (%s) and region (%s)", eo.Type, eo.Name, eo.Region)
- }
-
- return client, nil
-}
diff --git a/rackspace/monitoring/common.go b/rackspace/monitoring/common.go
deleted file mode 100644
index 900c86e..0000000
--- a/rackspace/monitoring/common.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package monitoring
-
-import (
- "github.com/rackspace/gophercloud"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
-)
-
-type Options struct {
- Endpoint string
- AuthOptions gophercloud.AuthOptions
- Authentication identity.AuthResults
-}
diff --git a/rackspace/monitoring/notificationPlans/requests.go b/rackspace/monitoring/notificationPlans/requests.go
deleted file mode 100644
index d4878c4..0000000
--- a/rackspace/monitoring/notificationPlans/requests.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package notificationPlans
-
-import (
- "fmt"
-
- "github.com/racker/perigee"
- identity "github.com/rackspace/gophercloud/openstack/identity/v2"
- "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/results.go b/results.go
index 3ca7c94..f7d526c 100644
--- a/results.go
+++ b/results.go
@@ -7,3 +7,6 @@
Resp map[string]interface{}
Err error
}
+
+// RFC3339Milli describes a time format used by API responses.
+const RFC3339Milli = "2006-01-02T15:04:05.999999Z"
diff --git a/service_client.go b/service_client.go
index 83ad69b..55d3b98 100644
--- a/service_client.go
+++ b/service_client.go
@@ -11,9 +11,28 @@
// Endpoint is the base URL of the service's API, acquired from a service catalog.
// It MUST end with a /.
Endpoint string
+
+ // ResourceBase is the base URL shared by the resources within a service's API. It should include
+ // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used
+ // as-is, instead.
+ ResourceBase string
+}
+
+// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
+func (client *ServiceClient) ResourceBaseURL() string {
+ if client.ResourceBase != "" {
+ return client.ResourceBase
+ }
+ return client.Endpoint
}
// ServiceURL constructs a URL for a resource belonging to this provider.
func (client *ServiceClient) ServiceURL(parts ...string) string {
- return client.Endpoint + strings.Join(parts, "/")
+ return client.ResourceBaseURL() + strings.Join(parts, "/")
+}
+
+// AuthenticatedHeaders returns a collection of HTTP request headers that mark a request as
+// belonging to the currently authenticated user.
+func (client *ServiceClient) AuthenticatedHeaders() map[string]string {
+ return client.Provider.AuthenticatedHeaders()
}