Initial commit of experimental, new, v0.2.0 API
diff --git a/openstack/identity/common_test.go b/openstack/identity/common_test.go
new file mode 100644
index 0000000..81d37cc
--- /dev/null
+++ b/openstack/identity/common_test.go
@@ -0,0 +1,90 @@
+package identity
+
+// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
+const authResultsOK = `{
+ "access":{
+ "token":{
+ "id": "ab48a9efdfedb23ty3494",
+ "expires": "2010-11-01T03:32:15-05:00",
+ "tenant":{
+ "id": "t1000",
+ "name": "My Project"
+ }
+ },
+ "user":{
+ "id": "u123",
+ "name": "jqsmith",
+ "roles":[{
+ "id": "100",
+ "name": "compute:admin"
+ },
+ {
+ "id": "101",
+ "name": "object-store:admin",
+ "tenantId": "t1000"
+ }
+ ],
+ "roles_links":[]
+ },
+ "serviceCatalog":[{
+ "name": "Cloud Servers",
+ "type": "compute",
+ "endpoints":[{
+ "tenantId": "t1000",
+ "publicURL": "https://compute.north.host.com/v1/t1000",
+ "internalURL": "https://compute.north.internal/v1/t1000",
+ "region": "North",
+ "versionId": "1",
+ "versionInfo": "https://compute.north.host.com/v1/",
+ "versionList": "https://compute.north.host.com/"
+ },
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://compute.north.host.com/v1.1/t1000",
+ "internalURL": "https://compute.north.internal/v1.1/t1000",
+ "region": "North",
+ "versionId": "1.1",
+ "versionInfo": "https://compute.north.host.com/v1.1/",
+ "versionList": "https://compute.north.host.com/"
+ }
+ ],
+ "endpoints_links":[]
+ },
+ {
+ "name": "Cloud Files",
+ "type": "object-store",
+ "endpoints":[{
+ "tenantId": "t1000",
+ "publicURL": "https://storage.north.host.com/v1/t1000",
+ "internalURL": "https://storage.north.internal/v1/t1000",
+ "region": "North",
+ "versionId": "1",
+ "versionInfo": "https://storage.north.host.com/v1/",
+ "versionList": "https://storage.north.host.com/"
+ },
+ {
+ "tenantId": "t1000",
+ "publicURL": "https://storage.south.host.com/v1/t1000",
+ "internalURL": "https://storage.south.internal/v1/t1000",
+ "region": "South",
+ "versionId": "1",
+ "versionInfo": "https://storage.south.host.com/v1/",
+ "versionList": "https://storage.south.host.com/"
+ }
+ ]
+ },
+ {
+ "name": "DNS-as-a-Service",
+ "type": "dnsextension:dns",
+ "endpoints":[{
+ "tenantId": "t1000",
+ "publicURL": "https://dns.host.com/v2.0/t1000",
+ "versionId": "2.0",
+ "versionInfo": "https://dns.host.com/v2.0/",
+ "versionList": "https://dns.host.com/"
+ }
+ ]
+ }
+ ]
+ }
+}`
diff --git a/openstack/identity/errors.go b/openstack/identity/errors.go
new file mode 100644
index 0000000..b15a4a2
--- /dev/null
+++ b/openstack/identity/errors.go
@@ -0,0 +1,7 @@
+package identity
+
+import "fmt"
+
+var ErrNotImplemented = fmt.Errorf("Identity feature not yet implemented")
+var ErrEndpoint = fmt.Errorf("Improper or missing Identity endpoint")
+var ErrCredentials = fmt.Errorf("Improper or missing Identity credentials")
diff --git a/openstack/identity/extensions.go b/openstack/identity/extensions.go
new file mode 100644
index 0000000..4a861ff
--- /dev/null
+++ b/openstack/identity/extensions.go
@@ -0,0 +1,52 @@
+package identity
+
+import (
+ "fmt"
+ "github.com/mitchellh/mapstructure"
+)
+
+var (
+ ErrNotFound = fmt.Errorf("Identity extension not found")
+)
+
+type ExtensionDetails struct {
+ Name string
+ Namespace string
+ Updated string
+ Description string
+}
+
+// ExtensionsDesc structures are returned by the Extensions() function for valid input.
+// This structure implements the ExtensionInquisitor interface.
+type ExtensionsDesc struct {
+ extensions []interface{}
+}
+
+func Extensions(m map[string]interface{}) *ExtensionsDesc {
+ return &ExtensionsDesc{extensions: m["extensions"].([]interface{})}
+}
+
+func extensionIndexByAlias(e *ExtensionsDesc, alias string) (int, error) {
+ for i, ee := range e.extensions {
+ extensionRecord := ee.(map[string]interface{})
+ if extensionRecord["alias"] == alias {
+ return i, nil
+ }
+ }
+ return 0, ErrNotFound
+}
+
+func (e *ExtensionsDesc) IsExtensionAvailable(alias string) bool {
+ _, err := extensionIndexByAlias(e, alias)
+ return err == nil
+}
+
+func (e *ExtensionsDesc) ExtensionDetailsByAlias(alias string) (*ExtensionDetails, error) {
+ i, err := extensionIndexByAlias(e, alias)
+ if err != nil {
+ return nil, err
+ }
+ ed := &ExtensionDetails{}
+ err = mapstructure.Decode(e.extensions[i], ed)
+ return ed, err
+}
diff --git a/openstack/identity/extensions_test.go b/openstack/identity/extensions_test.go
new file mode 100644
index 0000000..a5eede2
--- /dev/null
+++ b/openstack/identity/extensions_test.go
@@ -0,0 +1,108 @@
+package identity
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+// Taken from: http://docs.openstack.org/api/openstack-identity-service/2.0/content/GET_listExtensions_v2.0_extensions_.html#GET_listExtensions_v2.0_extensions_-Request
+const queryResults = `{
+ "extensions":[{
+ "name": "Reset Password Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/rpe/v2.0",
+ "alias": "RS-RPE",
+ "updated": "2011-01-22T13:25:27-06:00",
+ "description": "Adds the capability to reset a user's password. The user is emailed when the password has been reset.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe-20111111.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-rpe.wadl"
+ }
+ ]
+ },
+ {
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "alias": "RS-META",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ "links":[{
+ "rel": "describedby",
+ "type": "application/pdf",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta-20111201.pdf"
+ },
+ {
+ "rel": "describedby",
+ "type": "application/vnd.sun.wadl+xml",
+ "href": "http://docs.rackspacecloud.com/identity/api/ext/identity-meta.wadl"
+ }
+ ]
+ }
+ ],
+ "extensions_links":[]
+}`
+
+func TestIsExtensionAvailable(t *testing.T) {
+ // Make a response as we'd expect from the IdentityService.GetExtensions() call.
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ e := Extensions(getExtensionsResults)
+ for _, alias := range []string{"RS-RPE", "RS-META"} {
+ if !e.IsExtensionAvailable(alias) {
+ t.Errorf("Expected extension %s present.", alias)
+ return
+ }
+ }
+ if e.IsExtensionAvailable("blort") {
+ t.Errorf("Input JSON doesn't list blort as an extension")
+ return
+ }
+}
+
+func TestGetExtensionDetails(t *testing.T) {
+ // Make a response as we'd expect from the IdentityService.GetExtensions() call.
+ getExtensionsResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(queryResults), &getExtensionsResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ e := Extensions(getExtensionsResults)
+ ed, err := e.ExtensionDetailsByAlias("RS-META")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ actuals := map[string]string{
+ "name": ed.Name,
+ "namespace": ed.Namespace,
+ "updated": ed.Updated,
+ "description": ed.Description,
+ }
+
+ expecteds := map[string]string{
+ "name": "User Metadata Extension",
+ "namespace": "http://docs.rackspacecloud.com/identity/api/ext/meta/v2.0",
+ "updated": "2011-01-12T11:22:33-06:00",
+ "description": "Allows associating arbritrary metadata with a user.",
+ }
+
+ for k, v := range expecteds {
+ if actuals[k] != v {
+ t.Errorf("Expected %s \"%s\", got \"%s\" instead", k, v, actuals[k])
+ return
+ }
+ }
+}
diff --git a/openstack/identity/requests.go b/openstack/identity/requests.go
new file mode 100644
index 0000000..0012a17
--- /dev/null
+++ b/openstack/identity/requests.go
@@ -0,0 +1,103 @@
+package identity
+
+import (
+ "github.com/racker/perigee"
+)
+
+type AuthResults map[string]interface{}
+
+// AuthOptions lets anyone calling Authenticate() supply the required access credentials.
+// At present, only Identity V2 API support exists; therefore, only Username, Password,
+// and optionally, TenantId are provided. If future Identity API versions become available,
+// alternative fields unique to those versions may appear here.
+type AuthOptions struct {
+ // Endpoint specifies the HTTP endpoint offering the Identity V2 API.
+ // Required.
+ Endpoint string
+
+ // Username is required if using Identity V2 API.
+ // Consult with your provider's control panel to discover your
+ // account's username.
+ Username string
+
+ // At most one of Password or ApiKey is required if using Identity V2 API.
+ // Consult with your provider's control panel to discover your
+ // account's preferred method of authentication.
+ Password, ApiKey string
+
+ // The TenantId field is optional for the Identity V2 API.
+ TenantId string
+
+ // The TenantName can be specified instead of the TenantId
+ TenantName string
+
+ // AllowReauth should be set to true if you grant permission for Gophercloud to cache
+ // your credentials in memory, and to allow Gophercloud to attempt to re-authenticate
+ // automatically if/when your token expires. If you set it to false, it will not cache
+ // these settings, but re-authentication will not be possible. This setting defaults
+ // to false.
+ AllowReauth bool
+}
+
+func Authenticate(options AuthOptions) (AuthResults, error) {
+ var ar AuthResults
+
+ if options.Endpoint == "" {
+ return nil, ErrEndpoint
+ }
+
+ if (options.Username == "") || (options.Password == "" && options.ApiKey == "") {
+ return nil, ErrCredentials
+ }
+
+ err := perigee.Post(options.Endpoint, perigee.Options{
+ ReqBody: &AuthContainer{
+ Auth: getAuthCredentials(options),
+ },
+ Results: &ar,
+ })
+ return ar, err
+}
+
+func getAuthCredentials(options AuthOptions) Auth {
+ if options.ApiKey == "" {
+ return Auth{
+ PasswordCredentials: &struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{
+ Username: options.Username,
+ Password: options.Password,
+ },
+ TenantId: options.TenantId,
+ TenantName: options.TenantName,
+ }
+ } else {
+ return Auth{
+ ApiKeyCredentials: &struct {
+ Username string `json:"username"`
+ ApiKey string `json:"apiKey"`
+ }{
+ Username: options.Username,
+ ApiKey: options.ApiKey,
+ },
+ TenantId: options.TenantId,
+ TenantName: options.TenantName,
+ }
+ }
+}
+
+// AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity
+// service. You will not work with this structure directly.
+type AuthContainer struct {
+ Auth Auth `json:"auth"`
+}
+
+// Auth provides a JSON encoding wrapper for passing credentials to the Identity
+// service. You will not work with this structure directly.
+type Auth struct {
+ PasswordCredentials interface{} `json:"passwordCredentials,omitempty"`
+ ApiKeyCredentials interface{} `json:"RAX-KSKEY:apiKeyCredentials,omitempty"`
+ TenantId string `json:"tenantId,omitempty"`
+ TenantName string `json:"tenantName,omitempty"`
+}
diff --git a/openstack/identity/service_catalog.go b/openstack/identity/service_catalog.go
new file mode 100644
index 0000000..fc2717a
--- /dev/null
+++ b/openstack/identity/service_catalog.go
@@ -0,0 +1,68 @@
+package identity
+
+import "github.com/mitchellh/mapstructure"
+
+type ServiceCatalogDesc struct {
+ serviceDescriptions []interface{}
+}
+
+type CatalogEntry struct {
+ Name string
+ Type string
+ Endpoints []Endpoint
+}
+
+type Endpoint struct {
+ TenantId string
+ PublicURL string
+ InternalURL string
+ Region string
+ VersionId string
+ VersionInfo string
+ VersionList string
+}
+
+func ServiceCatalog(ar AuthResults) (*ServiceCatalogDesc, error) {
+ access := ar["access"].(map[string]interface{})
+ sds := access["serviceCatalog"].([]interface{})
+ sc := &ServiceCatalogDesc{
+ serviceDescriptions: sds,
+ }
+ return sc, nil
+}
+
+func (sc *ServiceCatalogDesc) NumberOfServices() int {
+ return len(sc.serviceDescriptions)
+}
+
+func (sc *ServiceCatalogDesc) CatalogEntries() ([]CatalogEntry, error) {
+ var err error
+ ces := make([]CatalogEntry, sc.NumberOfServices())
+ for i, sd := range sc.serviceDescriptions {
+ d := sd.(map[string]interface{})
+ eps, err := parseEndpoints(d["endpoints"].([]interface{}))
+ if err != nil {
+ return ces, err
+ }
+ ces[i] = CatalogEntry{
+ Name: d["name"].(string),
+ Type: d["type"].(string),
+ Endpoints: eps,
+ }
+ }
+ return ces, err
+}
+
+func parseEndpoints(eps []interface{}) ([]Endpoint, error) {
+ var err error
+ result := make([]Endpoint, len(eps))
+ for i, ep := range eps {
+ e := Endpoint{}
+ err = mapstructure.Decode(ep, &e)
+ if err != nil {
+ return result, err
+ }
+ result[i] = e
+ }
+ return result, err
+}
diff --git a/openstack/identity/service_catalog_test.go b/openstack/identity/service_catalog_test.go
new file mode 100644
index 0000000..4411b9f
--- /dev/null
+++ b/openstack/identity/service_catalog_test.go
@@ -0,0 +1,91 @@
+package identity
+
+import (
+ "testing"
+ "encoding/json"
+)
+
+func TestServiceCatalog(t *testing.T) {
+ authResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(authResultsOK), &authResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ sc, err := ServiceCatalog(authResults)
+ if err != nil {
+ panic(err)
+ }
+
+ if sc.NumberOfServices() != 3 {
+ t.Errorf("Expected 3 services; got %d", sc.NumberOfServices())
+ }
+
+ ces, err := sc.CatalogEntries()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ for _, ce := range ces {
+ if strNotInStrList(ce.Name, "Cloud Servers", "Cloud Files", "DNS-as-a-Service") {
+ t.Errorf("Expected \"%s\" to be one of Cloud Servers, Cloud Files, or DNS-as-a-Service", ce.Name)
+ return
+ }
+
+ if strNotInStrList(ce.Type, "dnsextension:dns", "object-store", "compute") {
+ t.Errorf("Expected \"%s\" to be one of dnsextension:dns, object-store, or compute")
+ return
+ }
+ }
+
+ eps := endpointsFor(ces, "compute")
+ if len(eps) != 2 {
+ t.Errorf("Expected 2 endpoints for compute service")
+ return
+ }
+ for _, ep := range eps {
+ if strNotInStrList(ep.VersionId, "1", "1.1", "1.1") {
+ t.Errorf("Expected versionId field of compute resource to be one of 1 or 1.1")
+ return
+ }
+ }
+
+ eps = endpointsFor(ces, "object-store")
+ if len(eps) != 2 {
+ t.Errorf("Expected 2 endpoints for object-store service")
+ return
+ }
+ for _, ep := range eps {
+ if ep.VersionId != "1" {
+ t.Errorf("Expected only version 1 object store API version")
+ return
+ }
+ }
+
+ eps = endpointsFor(ces, "dnsextension:dns")
+ if len(eps) != 1 {
+ t.Errorf("Expected 1 endpoint for DNS-as-a-Service service")
+ return
+ }
+ if eps[0].VersionId != "2.0" {
+ t.Errorf("Expected version 2.0 of DNS-as-a-Service service")
+ return
+ }
+}
+
+func endpointsFor(ces []CatalogEntry, t string) []Endpoint {
+ for _, ce := range ces {
+ if ce.Type == t {
+ return ce.Endpoints
+ }
+ }
+ panic("Precondition violated")
+}
+
+func strNotInStrList(needle, haystack1, haystack2, haystack3 string) bool {
+ if (needle != haystack1) && (needle != haystack2) && (needle != haystack3) {
+ return true
+ }
+ return false
+}
\ No newline at end of file
diff --git a/openstack/identity/token.go b/openstack/identity/token.go
new file mode 100644
index 0000000..aaea184
--- /dev/null
+++ b/openstack/identity/token.go
@@ -0,0 +1,43 @@
+package identity
+
+import (
+ "github.com/mitchellh/mapstructure"
+)
+
+type TenantDesc struct {
+ Id string
+ Name string
+}
+
+type TokenDesc struct {
+ Id_ string `mapstructure:"Id"`
+ Expires_ string `mapstructure:"Expires"`
+ Tenant TenantDesc
+}
+
+func Token(m AuthResults) (*TokenDesc, error) {
+ accessMap := m["access"].(map[string]interface{})
+ tokenMap := accessMap["token"].(map[string]interface{})
+ td := &TokenDesc{}
+ err := mapstructure.Decode(tokenMap, td)
+ if err != nil {
+ return nil, err
+ }
+ return td, nil
+}
+
+func (td *TokenDesc) Id() string {
+ return td.Id_
+}
+
+func (td *TokenDesc) Expires() string {
+ return td.Expires_
+}
+
+func (td *TokenDesc) TenantId() string {
+ return td.Tenant.Id
+}
+
+func (td *TokenDesc) TenantName() string {
+ return td.Tenant.Name
+}
diff --git a/openstack/identity/token_test.go b/openstack/identity/token_test.go
new file mode 100644
index 0000000..4fdf953
--- /dev/null
+++ b/openstack/identity/token_test.go
@@ -0,0 +1,25 @@
+package identity
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestAccessToken(t *testing.T) {
+ authResults := make(map[string]interface{})
+ err := json.Unmarshal([]byte(authResultsOK), &authResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ tok, err := Token(authResults)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if tok.Id() != "ab48a9efdfedb23ty3494" {
+ t.Errorf("Expected token \"ab48a9efdfedb23ty3494\"; got \"%s\" instead", tok.Id())
+ return
+ }
+}