From 9de84b3c5d13b4dcc00e0ef3c0392e0abf3078ff Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sat, 18 Apr 2015 14:04:57 -0500 Subject: [PATCH] Add auth package Includes AuthOpts struct and AuthRef interface, plus an Identity v2 password auth implementation. Note: the examples work, the objectstore_test is broken, will be fixed along with additional session and auth tests. Change-Id: I77b07c92586c37e855b466e18dea133a4a938aaa --- README.md | 26 +++-- examples/00-authentication.go | 51 ++++++---- examples/10-objectstore.go | 81 ++++++++------- examples/30-image-v1.go | 44 +++++---- identity/v2/{auth.go => unused-auth.go} | 0 image/v1/image.go | 15 ++- image/v1/image_test.go | 14 ++- objectstorage/v1/objectstorage.go | 79 +++++++-------- openstack/README.md | 9 ++ openstack/auth-password.go | 81 +++++++++++++++ openstack/auth-token.go | 81 +++++++++++++++ openstack/auth.go | 99 +++++++++++++++++++ openstack/session.go | 126 ++++++++++-------------- openstack/session_test.go | 61 ++++++------ testUtil/testUtil.go | 34 +++---- 15 files changed, 551 insertions(+), 250 deletions(-) rename identity/v2/{auth.go => unused-auth.go} (100%) create mode 100644 openstack/auth-password.go create mode 100644 openstack/auth-token.go create mode 100644 openstack/auth.go diff --git a/README.md b/README.md index 03ea79e..32a4751 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,32 @@ OpenStack Golang Client NOTE(dtroyer) Apr 2015: This repo is under heavy revision as it is being revived. -stackforge/golang-client is yet another implementation of [OpenStack] +`stackforge/golang-client` is an implementation of [OpenStack] (http://www.openstack.org/) API client in [Go language](http://golang.org). -The code follows OpenStack licensing and borrows its infrastructure for code -hosting. It currently implements [Identity Service v2] +The code follows OpenStack licensing and uses the Stackforge infrastructure +for hosting. It currently implements [Identity Service v2] (http://docs.openstack.org/api/openstack-identity-service/2.0/content/) and [Object Storage v1] (http://docs.openstack.org/api/openstack-object-storage/1.0/content/). -Some API calls are not implemented initially, but the intention is to expand -the lib over time (where pragmatic). +The initial focus is on building a solid core REST Session and OpenStack +authentication on which to build the usual API interfaces. The architecture +if the `Session` and authentication is similar to that used in the current +Python Keystone client library: The `Session` object contains the HTTP +interface methods and an authentication object that provides access to +the auth token and service catalog. + +Current State +------------- Code maturity is considered experimental. +* The new Session object is functional and used by most of the code now. +* The examples work. +* The image tests work. +* The obejct store tests do not work. +* identity/v2/auth.go is now unused, will be kept around for a short time + for easier reference. + Installation ------------ Use `go get git.openstack.org/stackforge/golang-client.git`. Or alternatively, @@ -53,7 +67,7 @@ Apache v2. Contributing ------------ -The code repository borrows OpenStack StackForge infrastructure. +The code repository utilizes the OpenStack StackForge infrastructure. Please use the [recommended workflow] (http://docs.openstack.org/infra/manual/developers.html#development-workflow). If you are not a member yet, please consider joining as an [OpenStack contributor] diff --git a/examples/00-authentication.go b/examples/00-authentication.go index 941b3fb..2222be8 100644 --- a/examples/00-authentication.go +++ b/examples/00-authentication.go @@ -16,8 +16,10 @@ package main import ( "fmt" - "git.openstack.org/stackforge/golang-client.git/identity/v2" + // "git.openstack.org/stackforge/golang-client.git/identity/v2" "time" + + "git.openstack.org/stackforge/golang-client.git/openstack" ) // Authentication examples. @@ -26,42 +28,51 @@ func main() { // Authenticate with just a username and password. The returned token is // unscoped to a tenant. - auth, err := identity.AuthUserName(config.Host, - config.Username, - config.Password) + creds := openstack.AuthOpts{ + AuthUrl: config.Host, + Username: config.Username, + Password: config.Password, + } + auth, err := openstack.DoAuthRequest(creds) if err != nil { - fmt.Println("There was an error authenticating:", err) + fmt.Println("Error authenticating username/password:", err) return } - if !auth.Access.Token.Expires.After(time.Now()) { + if !auth.GetExpiration().After(time.Now()) { fmt.Println("There was an error. The auth token has an invalid expiration.") return } - // Authenticate with a username, password, tenant name. - auth, err = identity.AuthUserNameTenantName(config.Host, - config.Username, - config.Password, - config.ProjectName) + // Authenticate with a project name, username, password. + creds = openstack.AuthOpts{ + AuthUrl: config.Host, + Project: config.ProjectName, + Username: config.Username, + Password: config.Password, + } + auth, err = openstack.DoAuthRequest(creds) if err != nil { - fmt.Println("There was an error authenticating:", err) + fmt.Println("Error authenticating project/username/password:", err) return } - if !auth.Access.Token.Expires.After(time.Now()) { + if !auth.GetExpiration().After(time.Now()) { fmt.Println("There was an error. The auth token has an invalid expiration.") return } - // Authenticate with a username, password, tenant id. - auth, err = identity.AuthUserNameTenantId(config.Host, - config.Username, - config.Password, - config.ProjectID) + // Authenticate with a project id, username, password. + creds = openstack.AuthOpts{ + AuthUrl: config.Host, + Project: config.ProjectID, + Username: config.Username, + Password: config.Password, + } + auth, err = openstack.DoAuthRequest(creds) if err != nil { - fmt.Println("There was an error authenticating:", err) + fmt.Println("Error authenticating project/username/password:", err) return } - if !auth.Access.Token.Expires.After(time.Now()) { + if !auth.GetExpiration().After(time.Now()) { fmt.Println("There was an error. The auth token has an invalid expiration.") return } diff --git a/examples/10-objectstore.go b/examples/10-objectstore.go index 12d9897..5fff8b9 100644 --- a/examples/10-objectstore.go +++ b/examples/10-objectstore.go @@ -18,10 +18,12 @@ import ( "bytes" "encoding/json" "fmt" - "git.openstack.org/stackforge/golang-client.git/identity/v2" - "git.openstack.org/stackforge/golang-client.git/objectstorage/v1" "io/ioutil" + "net/http" "time" + + "git.openstack.org/stackforge/golang-client.git/objectstorage/v1" + "git.openstack.org/stackforge/golang-client.git/openstack" ) func main() { @@ -29,45 +31,52 @@ func main() { // Before working with object storage we need to authenticate with a project // that has active object storage. - auth, err := identity.AuthUserNameTenantName(config.Host, - config.Username, - config.Password, - config.ProjectName) + // Authenticate with a project name, username, password. + creds := openstack.AuthOpts{ + AuthUrl: config.Host, + Project: config.ProjectName, + Username: config.Username, + Password: config.Password, + } + auth, err := openstack.DoAuthRequest(creds) if err != nil { panicString := fmt.Sprint("There was an error authenticating:", err) panic(panicString) } - if !auth.Access.Token.Expires.After(time.Now()) { + if !auth.GetExpiration().After(time.Now()) { panic("There was an error. The auth token has an invalid expiration.") } // Find the endpoint for object storage. - url := "" - for _, svc := range auth.Access.ServiceCatalog { - if svc.Type == "object-store" { - url = svc.Endpoints[0].PublicURL + "/" - break - } - } - if url == "" { + url, err := auth.GetEndpoint("object-store", "") + if url == "" || err != nil { panic("object-store url not found during authentication") } - hdr, err := objectstorage.GetAccountMeta(url, auth.Access.Token.Id) + // Make a new client with these creds + sess, err := openstack.NewSession(nil, auth, nil) + if err != nil { + panicString := fmt.Sprint("Error crating new Session:", err) + panic(panicString) + } + + hdr, err := objectstorage.GetAccountMeta(sess, url) if err != nil { panicString := fmt.Sprint("There was an error getting account metadata:", err) panic(panicString) } + _ = hdr // Create a new container. - if err = objectstorage.PutContainer(url+config.Container, auth.Access.Token.Id, - "X-Log-Retention", "true"); err != nil { + var headers http.Header = http.Header{} + headers.Add("X-Log-Retention", "true") + if err = objectstorage.PutContainer(sess, url+"/"+config.Container, headers); err != nil { panicString := fmt.Sprint("PutContainer Error:", err) panic(panicString) } // Get a list of all the containers at the selected endoint. - containersJson, err := objectstorage.ListContainers(0, "", url, auth.Access.Token.Id) + containersJson, err := objectstorage.ListContainers(sess, 0, "", url) if err != nil { panic(err) } @@ -93,12 +102,13 @@ func main() { } // Set and Get container metadata. - if err = objectstorage.SetContainerMeta(url+config.Container, auth.Access.Token.Id, - "X-Container-Meta-fubar", "false"); err != nil { + headers = http.Header{} + headers.Add("X-Container-Meta-fubar", "false") + if err = objectstorage.SetContainerMeta(sess, url+"/"+config.Container, headers); err != nil { panic(err) } - hdr, err = objectstorage.GetContainerMeta(url+config.Container, auth.Access.Token.Id) + hdr, err = objectstorage.GetContainerMeta(sess, url+"/"+config.Container) if err != nil { panicString := fmt.Sprint("GetContainerMeta Error:", err) panic(panicString) @@ -115,13 +125,14 @@ func main() { panic(err) } + headers = http.Header{} + headers.Add("X-Container-Meta-fubar", "false") object := config.Container + "/" + srcFile - if err = objectstorage.PutObject(&fContent, url+object, auth.Access.Token.Id, - "X-Object-Meta-fubar", "false"); err != nil { + if err = objectstorage.PutObject(sess, &fContent, url+"/"+object, headers); err != nil { panic(err) } - objectsJson, err := objectstorage.ListObjects(0, "", "", "", "", - url+config.Container, auth.Access.Token.Id) + objectsJson, err := objectstorage.ListObjects(sess, 0, "", "", "", "", + url+"/"+config.Container) type objectType struct { Name, Hash, Content_type, Last_modified string @@ -143,12 +154,13 @@ func main() { } // Manage object metadata - if err = objectstorage.SetObjectMeta(url+object, auth.Access.Token.Id, - "X-Object-Meta-fubar", "true"); err != nil { + headers = http.Header{} + headers.Add("X-Object-Meta-fubar", "true") + if err = objectstorage.SetObjectMeta(sess, url+"/"+object, headers); err != nil { panicString := fmt.Sprint("SetObjectMeta Error:", err) panic(panicString) } - hdr, err = objectstorage.GetObjectMeta(url+object, auth.Access.Token.Id) + hdr, err = objectstorage.GetObjectMeta(sess, url+"/"+object) if err != nil { panicString := fmt.Sprint("GetObjectMeta Error:", err) panic(panicString) @@ -159,7 +171,7 @@ func main() { } // Retrieve an object and check that it is the same as what as uploaded. - _, body, err := objectstorage.GetObject(url+object, auth.Access.Token.Id) + _, body, err := objectstorage.GetObject(sess, url+"/"+object) if err != nil { panicString := fmt.Sprint("GetObject Error:", err) panic(panicString) @@ -170,24 +182,23 @@ func main() { } // Duplication (Copy) an existing object. - if err = objectstorage.CopyObject(url+object, "/"+object+".dup", auth.Access.Token.Id); err != nil { + if err = objectstorage.CopyObject(sess, url+"/"+object, "/"+object+".dup"); err != nil { panicString := fmt.Sprint("CopyObject Error:", err) panic(panicString) } // Delete the objects. - if err = objectstorage.DeleteObject(url+object, auth.Access.Token.Id); err != nil { + if err = objectstorage.DeleteObject(sess, url+"/"+object); err != nil { panicString := fmt.Sprint("DeleteObject Error:", err) panic(panicString) } - if err = objectstorage.DeleteObject(url+object+".dup", auth.Access.Token.Id); err != nil { + if err = objectstorage.DeleteObject(sess, url+"/"+object+".dup"); err != nil { panicString := fmt.Sprint("DeleteObject Error:", err) panic(panicString) } // Delete the container that was previously created. - if err = objectstorage.DeleteContainer(url+config.Container, - auth.Access.Token.Id); err != nil { + if err = objectstorage.DeleteContainer(sess, url+"/"+config.Container); err != nil { panicString := fmt.Sprint("DeleteContainer Error:", err) panic(panicString) } diff --git a/examples/30-image-v1.go b/examples/30-image-v1.go index e7027dd..49ae777 100644 --- a/examples/30-image-v1.go +++ b/examples/30-image-v1.go @@ -16,10 +16,11 @@ package main import ( "fmt" - "git.openstack.org/stackforge/golang-client.git/identity/v2" - "git.openstack.org/stackforge/golang-client.git/image/v1" "net/http" "time" + + "git.openstack.org/stackforge/golang-client.git/image/v1" + "git.openstack.org/stackforge/golang-client.git/openstack" ) // Image examples. @@ -27,34 +28,39 @@ func main() { config := getConfig() // Authenticate with a username, password, tenant id. - auth, err := identity.AuthUserNameTenantName(config.Host, - config.Username, - config.Password, - config.ProjectName) + creds := openstack.AuthOpts{ + AuthUrl: config.Host, + Project: config.ProjectName, + Username: config.Username, + Password: config.Password, + } + auth, err := openstack.DoAuthRequest(creds) if err != nil { panicString := fmt.Sprint("There was an error authenticating:", err) panic(panicString) } - if !auth.Access.Token.Expires.After(time.Now()) { + if !auth.GetExpiration().After(time.Now()) { panic("There was an error. The auth token has an invalid expiration.") } // Find the endpoint for the image service. - url := "" - for _, svc := range auth.Access.ServiceCatalog { - if svc.Type == "image" { - for _, ep := range svc.Endpoints { - url = ep.PublicURL + "/v1" - break - } - } - } - - if url == "" { + url, err := auth.GetEndpoint("image", "") + if url == "" || err != nil { panic("v1 image service url not found during authentication") } - imageService := image.Service{TokenID: auth.Access.Token.Id, Client: *http.DefaultClient, URL: url} + // Make a new client with these creds + sess, err := openstack.NewSession(nil, auth, nil) + if err != nil { + panicString := fmt.Sprint("Error crating new Session:", err) + panic(panicString) + } + + imageService := image.Service{ + Session: *sess, + Client: *http.DefaultClient, + URL: url + "/v1", // We're forcing Image v1 for now + } imagesDetails, err := imageService.ImagesDetail() if err != nil { panicString := fmt.Sprint("Cannot access images:", err) diff --git a/identity/v2/auth.go b/identity/v2/unused-auth.go similarity index 100% rename from identity/v2/auth.go rename to identity/v2/unused-auth.go diff --git a/image/v1/image.go b/image/v1/image.go index 4937bd8..bcd46f0 100644 --- a/image/v1/image.go +++ b/image/v1/image.go @@ -24,7 +24,9 @@ package image import ( "encoding/json" + "errors" "fmt" + "io/ioutil" "net/http" "net/url" @@ -38,8 +40,8 @@ import ( // imageService := image.ImageService{Client: *http.DefaultClient, TokenId: tokenId, Url: "http://imageservicelocation"} // images:= imageService.Images() type Service struct { + Session openstack.Session Client http.Client - TokenID string URL string } @@ -150,19 +152,22 @@ func (imageService Service) queryImages(includeDetails bool, imagesResponseConta } var headers http.Header = http.Header{} - headers.Set("X-Auth-Token", imageService.TokenID) headers.Set("Accept", "application/json") - resp, err := openstack.Get(reqURL.String(), nil, &headers) + resp, err := imageService.Session.Get(reqURL.String(), nil, &headers) if err != nil { return err } - err = util.CheckHTTPResponseStatusCode(resp.Resp) + err = util.CheckHTTPResponseStatusCode(resp) if err != nil { return err } - if err = json.Unmarshal(resp.Body, &imagesResponseContainer); err != nil { + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.New("aaa") + } + if err = json.Unmarshal(rbody, &imagesResponseContainer); err != nil { return err } return nil diff --git a/image/v1/image_test.go b/image/v1/image_test.go index 707d07d..e49e7cb 100644 --- a/image/v1/image_test.go +++ b/image/v1/image_test.go @@ -22,6 +22,7 @@ import ( "testing" "git.openstack.org/stackforge/golang-client.git/image/v1" + "git.openstack.org/stackforge/golang-client.git/openstack" "git.openstack.org/stackforge/golang-client.git/testUtil" "git.openstack.org/stackforge/golang-client.git/util" ) @@ -159,7 +160,18 @@ func testImageServiceAction(t *testing.T, uriEndsWith string, testData string, i apiServer := testUtil.CreateGetJSONTestRequestServer(t, tokn, testData, anon) defer apiServer.Close() - imageService := image.Service{TokenID: tokn, Client: *http.DefaultClient, URL: apiServer.URL} + auth := openstack.AuthToken{ + Access: openstack.AccessType{ + Token: openstack.Token{ + ID: tokn, + }, + }, + } + sess, _ := openstack.NewSession(http.DefaultClient, auth, nil) + imageService := image.Service{ + Session: *sess, + URL: apiServer.URL, + } imageServiceAction(&imageService) } diff --git a/objectstorage/v1/objectstorage.go b/objectstorage/v1/objectstorage.go index 2c4ae75..51444bb 100644 --- a/objectstorage/v1/objectstorage.go +++ b/objectstorage/v1/objectstorage.go @@ -31,65 +31,65 @@ var zeroByte = &([]byte{}) //pointer to empty []byte //"limit" and "marker" corresponds to the API's "limit" and "marker". //"url" can be regular storage or cdn-enabled storage URL. //It returns []byte which then needs to be unmarshalled to decode the JSON. -func ListContainers(limit int64, marker, url, token string) ([]byte, error) { - return ListObjects(limit, marker, "", "", "", url, token) +func ListContainers(session *openstack.Session, limit int64, marker, url string) ([]byte, error) { + return ListObjects(session, limit, marker, "", "", "", url) } //GetAccountMeta calls the OpenStack retrieve account metadata API using //previously obtained token. -func GetAccountMeta(url, token string) (http.Header, error) { - return GetObjectMeta(url, token) +func GetAccountMeta(session *openstack.Session, url string) (http.Header, error) { + return GetObjectMeta(session, url) } //DeleteContainer calls the OpenStack delete container API using //previously obtained token. -func DeleteContainer(url, token string) error { - return DeleteObject(url, token) +func DeleteContainer(session *openstack.Session, url string) error { + return DeleteObject(session, url) } //GetContainerMeta calls the OpenStack retrieve object metadata API //using previously obtained token. //url can be regular storage or CDN-enabled storage URL. -func GetContainerMeta(url, token string) (http.Header, error) { - return GetObjectMeta(url, token) +func GetContainerMeta(session *openstack.Session, url string) (http.Header, error) { + return GetObjectMeta(session, url) } //SetContainerMeta calls the OpenStack API to create / update meta data //for container using previously obtained token. //url can be regular storage or CDN-enabled storage URL. -func SetContainerMeta(url string, token string, s ...string) (err error) { - return SetObjectMeta(url, token, s...) +func SetContainerMeta(session *openstack.Session, url string, headers http.Header) (err error) { + return SetObjectMeta(session, url, headers) } //PutContainer calls the OpenStack API to create / update //container using previously obtained token. -func PutContainer(url, token string, s ...string) error { - return PutObject(zeroByte, url, token, s...) +func PutContainer(session *openstack.Session, url string, headers http.Header) error { + return PutObject(session, zeroByte, url, headers) } //ListObjects calls the OpenStack list object API using previously //obtained token. "Limit", "marker", "prefix", "path", "delim" corresponds //to the API's "limit", "marker", "prefix", "path", and "delimiter". -func ListObjects(limit int64, - marker, prefix, path, delim, conURL, token string) ([]byte, error) { - var query = "?format=json" +func ListObjects(session *openstack.Session, limit int64, + marker, prefix, path, delim, conURL string) ([]byte, error) { + var query url.Values = url.Values{} + query.Add("format", "json") if limit > 0 { - query += "&limit=" + strconv.FormatInt(limit, 10) + query.Add("limit", strconv.FormatInt(limit, 10)) } if marker != "" { - query += "&marker=" + url.QueryEscape(marker) + query.Add("marker", url.QueryEscape(marker)) } if prefix != "" { - query += "&prefix=" + url.QueryEscape(prefix) + query.Add("prefix", url.QueryEscape(prefix)) } if path != "" { - query += "&path=" + url.QueryEscape(path) + query.Add("path", url.QueryEscape(path)) } if delim != "" { - query += "&delimiter=" + url.QueryEscape(delim) + query.Add("delimiter", url.QueryEscape(delim)) } - resp, err := util.CallAPI("GET", conURL+query, zeroByte, - "X-Auth-Token", token) + resp, err := session.Get(conURL, &query, nil) if err != nil { return nil, err } @@ -107,23 +107,21 @@ func ListObjects(limit int64, //PutObject calls the OpenStack create object API using previously //obtained token. //url can be regular storage or CDN-enabled storage URL. -func PutObject(fContent *[]byte, url, token string, s ...string) (err error) { - var headers http.Header = http.Header{} - headers.Set("X-Auth-Token", token) - resp, err := openstack.Put(url, nil, &headers, fContent) +func PutObject(session *openstack.Session, fContent *[]byte, url string, headers http.Header) (err error) { + resp, err := session.Put(url, nil, &headers, fContent) if err != nil { return err } - return util.CheckHTTPResponseStatusCode(resp.Resp) + return util.CheckHTTPResponseStatusCode(resp) } //CopyObject calls the OpenStack copy object API using previously obtained //token. Note from API doc: "The destination container must exist before //attempting the copy." -func CopyObject(srcURL, destURL, token string) (err error) { - resp, err := util.CallAPI("COPY", srcURL, zeroByte, - "X-Auth-Token", token, - "Destination", destURL) +func CopyObject(session *openstack.Session, srcURL, destURL string) (err error) { + var headers http.Header = http.Header{} + headers.Add("Destination", destURL) + resp, err := session.Request("COPY", srcURL, nil, &headers, zeroByte) if err != nil { return err } @@ -138,8 +136,8 @@ func CopyObject(srcURL, destURL, token string) (err error) { //from the non-current container to the current." .. "If you want to completely //remove an object and you have five total versions of it, you must DELETE it //five times." -func DeleteObject(url, token string) (err error) { - resp, err := util.CallAPI("DELETE", url, zeroByte, "X-Auth-Token", token) +func DeleteObject(session *openstack.Session, url string) (err error) { + resp, err := session.Delete(url, nil, nil) if err != nil { return err } @@ -148,10 +146,9 @@ func DeleteObject(url, token string) (err error) { //SetObjectMeta calls the OpenStack API to create/update meta data for //object using previously obtained token. -func SetObjectMeta(url string, token string, s ...string) (err error) { - s = append(s, "X-Auth-Token") - s = append(s, token) - resp, err := util.CallAPI("POST", url, zeroByte, s...) +func SetObjectMeta(session *openstack.Session, url string, headers http.Header) (err error) { + // headers.Add("X-Auth-Token", token) + resp, err := session.Post(url, nil, &headers, zeroByte) if err != nil { return err } @@ -160,8 +157,8 @@ func SetObjectMeta(url string, token string, s ...string) (err error) { //GetObjectMeta calls the OpenStack retrieve object metadata API using //previously obtained token. -func GetObjectMeta(url, token string) (http.Header, error) { - resp, err := util.CallAPI("HEAD", url, zeroByte, "X-Auth-Token", token) +func GetObjectMeta(session *openstack.Session, url string) (http.Header, error) { + resp, err := session.Head(url, nil, nil) if err != nil { return nil, err } @@ -175,8 +172,8 @@ func GetObjectMeta(url, token string) (http.Header, error) { //Since this implementation of GetObject retrieves header info, it //effectively executes GetObjectMeta also in addition to getting the //object content. -func GetObject(url, token string) (http.Header, []byte, error) { - resp, err := util.CallAPI("GET", url, zeroByte, "X-Auth-Token", token) +func GetObject(session *openstack.Session, url string) (http.Header, []byte, error) { + resp, err := session.Get(url, nil, nil) if err != nil { return nil, nil, err } diff --git a/openstack/README.md b/openstack/README.md index e12d14f..fd60af1 100644 --- a/openstack/README.md +++ b/openstack/README.md @@ -2,3 +2,12 @@ openstack ========= `openstack` is the API to an OpenStack cloud. + +* `session.go` - A Session object that encapsulates the HTTP REST handler + and authentication and logging + +* `auth.go` - The basic authentication interface + +* `auth-password.go` - Implements password authentication (v2 only at present) + +* `auth-token.go` - The returned token objects diff --git a/openstack/auth-password.go b/openstack/auth-password.go new file mode 100644 index 0000000..0c3b530 --- /dev/null +++ b/openstack/auth-password.go @@ -0,0 +1,81 @@ +// auth-password - Username/Password Authentication +// Copyright 2015 Dean Troyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openstack + +import ( + "encoding/json" + "errors" + // "strings" +) + +// The token request structure for Identity v2 + +type PasswordCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type OSAuth struct { + PasswordCredentials `json:"passwordCredentials"` + Project string `json:"tenantName"` +} + +type UserPassV2 struct { + OSAuth `json:"auth"` + AuthUrl string `json:"-"` +} + +func NewUserPassV2(ao AuthOpts) (upv2 *UserPassV2, err error) { + // Validate incoming values + if ao.AuthUrl == "" { + err = errors.New("AuthUrl required") + return nil, err + } + if ao.Username == "" { + err = errors.New("Username required") + return nil, err + } + if ao.Password == "" { + err = errors.New("Password required") + return nil, err + } + upv2 = &UserPassV2{ + AuthUrl: ao.AuthUrl, + OSAuth: OSAuth{ + PasswordCredentials: PasswordCredentials{ + Username: ao.Username, + Password: ao.Password, + }, + Project: ao.Project, + }, + } + return upv2, nil +} + +// Produce JSON output +func (s *UserPassV2) JSON() []byte { + reqAuth, err := json.Marshal(s) + if err != nil { + // Return an empty structure + reqAuth = []byte{'{', '}'} + } + return reqAuth +} + +// func (self *UserPassV2) AuthUserPassV2(opts interface{}) (AuthRef, error) { +// auth, err := self.GetAuthRef() +// return AuthRef(auth), err +// } diff --git a/openstack/auth-token.go b/openstack/auth-token.go new file mode 100644 index 0000000..48beb65 --- /dev/null +++ b/openstack/auth-token.go @@ -0,0 +1,81 @@ +// auth-token - Token Authentication +// Copyright 2015 Dean Troyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openstack + +import ( + "errors" + "time" +) + +// Identity Response Types + +type AccessType struct { + Token Token `json:"token"` + User interface{} `json:"id"` + ServiceCatalog []ServiceCatalogEntry `json:"servicecatalog"` +} + +type AuthToken struct { + Access AccessType `json:"access"` +} + +type Token struct { + ID string `json:"id"` + Expires time.Time `json:"expires"` + Project struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"tenant"` +} + +type ServiceCatalogEntry struct { + Name string `json:"name"` + Type string `json:"type"` + Endpoints []ServiceEndpoint `json:"endpoints"` + // Endpoints []map[string]string `json:"endpoints"` +} + +type ServiceEndpoint struct { + Type string `json:"type"` + Region string `json:"region"` + PublicURL string `json:"publicurl"` + AdminURL string `json:"adminurl"` + InternalURL string `json:"internalurl"` + VersionID string `json:"versionid"` +} + +func (s AuthToken) GetToken() string { + return s.Access.Token.ID +} + +func (s AuthToken) GetExpiration() time.Time { + return s.Access.Token.Expires +} + +func (s AuthToken) GetEndpoint(serviceType string, regionName string) (string, error) { + + // Parse service catalog + for _, v := range s.Access.ServiceCatalog { + if v.Type == serviceType { + for _, r := range v.Endpoints { + if regionName == "" || r.Region == regionName { + return r.PublicURL, nil + } + } + } + } + return "", errors.New("err: endpoint not found") +} diff --git a/openstack/auth.go b/openstack/auth.go new file mode 100644 index 0000000..63cf8e5 --- /dev/null +++ b/openstack/auth.go @@ -0,0 +1,99 @@ +// auth - Authentication interface +// Copyright 2015 Dean Troyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openstack + +import ( + "encoding/json" + "errors" + "io/ioutil" + "strings" + "time" +) + +// AuthRef is the returned authentication object, maybe v2 or v3 +type AuthRef interface { + GetToken() string + GetExpiration() time.Time + GetEndpoint(string, string) (string, error) +} + +// AuthOpts is the set of credentials used to authenticate to OpenStack +type AuthOpts struct { + // AuthUrl is always required + AuthUrl string + + // Domain is ignored for v2 and required for v3 auth + Domain string + + // Project is optional to get an unscoped token but required for + // a scoped token, which is required to do pretty much everything + // except list projects + Project string + + // Username is required for password auth + Username string + + // Password is required for password auth + Password string + + // Token is required for Toekn auth + Token string +} + +func (s *AuthOpts) GetAuthType() (string, error) { + var auth_type string + if s.AuthUrl != "" && s.Token != "" { + auth_type = "token" + } else if s.Username != "" { + auth_type = "password" + } + return auth_type, nil +} + +// Basic auth call +// These args should be an interface?? +func DoAuthRequest(authopts AuthOpts) (AuthRef, error) { + // url string, body []byte) + var auth = AuthToken{} + + auth_mod, err := NewUserPassV2(authopts) + if err != nil { + err = errors.New("Failed to get auth options") + return nil, err + } + + path := auth_mod.AuthUrl + "/tokens" + body := auth_mod.JSON() + resp, err := Post(path, nil, nil, &body) + if err != nil { + return nil, err + } + + contentType := strings.ToLower(resp.Header.Get("Content-Type")) + if strings.Contains(contentType, "json") != true { + return nil, errors.New("err: header Content-Type is not JSON") + } + + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.New("aaa") + } + if err = json.Unmarshal(rbody, &auth); err != nil { + return nil, errors.New("bbb") + } + + return auth, nil +} diff --git a/openstack/session.go b/openstack/session.go index 235cabb..18a6ff2 100644 --- a/openstack/session.go +++ b/openstack/session.go @@ -19,12 +19,11 @@ import ( "bytes" "crypto/tls" "io" - "io/ioutil" + // "io/ioutil" "log" "net/http" "net/http/httputil" "net/url" - "strings" ) var Debug = new(bool) @@ -34,46 +33,28 @@ type Response struct { Body []byte } -type TokenInterface interface { - GetTokenId() string -} - -type Token struct { - Expires string - Id string - Project struct { - Id string - Name string - } -} - -func (t Token) GetTokenId() string { - return t.Id -} - // Generic callback to get a token from the auth plugin -type AuthFunc func(s *Session, opts interface{}) (TokenInterface, error) +type AuthFunc func(s *Session, opts interface{}) (AuthRef, error) type Session struct { - httpClient *http.Client - endpoint string - authenticate AuthFunc - Token TokenInterface - Headers http.Header - // ServCat map[string]ServiceEndpoint + httpClient *http.Client + AuthToken AuthRef + Headers http.Header } -func NewSession(af AuthFunc, endpoint string, tls *tls.Config) (session *Session, err error) { - tr := &http.Transport{ - TLSClientConfig: tls, - DisableCompression: true, +func NewSession(hclient *http.Client, auth AuthRef, tls *tls.Config) (session *Session, err error) { + if hclient == nil { + // Only build a transport if we're also building the client + tr := &http.Transport{ + TLSClientConfig: tls, + DisableCompression: true, + } + hclient = &http.Client{Transport: tr} } session = &Session{ - // TODO(dtroyer): httpClient needs to be able to be passed in, or set externally - httpClient: &http.Client{Transport: tr}, - endpoint: strings.TrimRight(endpoint, "/"), - authenticate: af, - Headers: http.Header{}, + httpClient: hclient, + AuthToken: auth, + Headers: http.Header{}, } return session, nil } @@ -83,47 +64,35 @@ func (s *Session) NewRequest(method, url string, headers *http.Header, body io.R if err != nil { return nil, err } - // add token, get one if needed - if s.Token == nil && s.authenticate != nil { - var tok TokenInterface - tok, err = s.authenticate(s, nil) - if err != nil { - // (re-)auth failure!! - return nil, err - } - s.Token = tok - } if headers != nil { req.Header = *headers } - if s.Token != nil { - req.Header.Add("X-Auth-Token", s.Token.GetTokenId()) + if s.AuthToken != nil { + req.Header.Add("X-Auth-Token", s.AuthToken.GetToken()) } return } -func (s *Session) Do(req *http.Request) (*Response, error) { - if *Debug { - d, _ := httputil.DumpRequestOut(req, true) - log.Printf(">>>>>>>>>> REQUEST:\n", string(d)) - } - +func (s *Session) Do(req *http.Request) (*http.Response, error) { // Add session headers for k := range s.Headers { req.Header.Set(k, s.Headers.Get(k)) } - hresp, err := s.httpClient.Do(req) + if *Debug { + d, _ := httputil.DumpRequestOut(req, true) + log.Printf(">>>>>>>>>> REQUEST:\n", string(d)) + } + + resp, err := s.httpClient.Do(req) if err != nil { return nil, err } if *Debug { - dr, _ := httputil.DumpResponse(hresp, true) + dr, _ := httputil.DumpResponse(resp, true) log.Printf("<<<<<<<<<< RESULT:\n", string(dr)) } - resp := new(Response) - resp.Resp = hresp return resp, nil } @@ -134,7 +103,7 @@ func (s *Session) Request( params *url.Values, headers *http.Header, body *[]byte, -) (resp *Response, err error) { +) (resp *http.Response, err error) { // add params to url here if params != nil { url = url + "?" + params.Encode() @@ -151,35 +120,42 @@ func (s *Session) Request( return nil, err } - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") resp, err = s.Do(req) if err != nil { return nil, err } - // do we need to parse this in this func? yes... - defer resp.Resp.Body.Close() - - resp.Body, err = ioutil.ReadAll(resp.Resp.Body) - if err != nil { - return nil, err - } return resp, nil } +func (s *Session) Delete( + url string, + params *url.Values, + headers *http.Header) (resp *http.Response, err error) { + return s.Request("DELETE", url, params, headers, nil) +} + func (s *Session) Get( url string, params *url.Values, - headers *http.Header) (resp *Response, err error) { + headers *http.Header) (resp *http.Response, err error) { return s.Request("GET", url, params, headers, nil) } +func (s *Session) Head( + url string, + params *url.Values, + headers *http.Header) (resp *http.Response, err error) { + return s.Request("HEAD", url, params, headers, nil) +} + func (s *Session) Post( url string, params *url.Values, headers *http.Header, - body *[]byte) (resp *Response, err error) { + body *[]byte) (resp *http.Response, err error) { return s.Request("POST", url, params, headers, body) } @@ -187,7 +163,7 @@ func (s *Session) Put( url string, params *url.Values, headers *http.Header, - body *[]byte) (resp *Response, err error) { + body *[]byte) (resp *http.Response, err error) { return s.Request("PUT", url, params, headers, body) } @@ -195,8 +171,8 @@ func (s *Session) Put( func Get( url string, params *url.Values, - headers *http.Header) (resp *Response, err error) { - s, _ := NewSession(nil, "", nil) + headers *http.Header) (resp *http.Response, err error) { + s, _ := NewSession(nil, nil, nil) return s.Get(url, params, headers) } @@ -205,8 +181,8 @@ func Post( url string, params *url.Values, headers *http.Header, - body *[]byte) (resp *Response, err error) { - s, _ := NewSession(nil, "", nil) + body *[]byte) (resp *http.Response, err error) { + s, _ := NewSession(nil, nil, nil) return s.Post(url, params, headers, body) } @@ -215,7 +191,7 @@ func Put( url string, params *url.Values, headers *http.Header, - body *[]byte) (resp *Response, err error) { - s, _ := NewSession(nil, "", nil) + body *[]byte) (resp *http.Response, err error) { + s, _ := NewSession(nil, nil, nil) return s.Put(url, params, headers, body) } diff --git a/openstack/session_test.go b/openstack/session_test.go index 7c172b8..0b5ee91 100644 --- a/openstack/session_test.go +++ b/openstack/session_test.go @@ -13,48 +13,47 @@ // See the License for the specific language governing permissions and // limitations under the License. - package openstack_test import ( - "encoding/json" - "net/http" - "testing" + "encoding/json" + "net/http" + "testing" - "git.openstack.org/stackforge/golang-client.git/openstack" - "git.openstack.org/stackforge/golang-client.git/testUtil" + "git.openstack.org/stackforge/golang-client.git/openstack" + "git.openstack.org/stackforge/golang-client.git/testUtil" ) type TestStruct struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id"` + Name string `json:"name"` } func TestSessionGet(t *testing.T) { - tokn := "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb" - var apiServer = testUtil.CreateGetJsonTestServer( - t, - tokn, - `{"id":"id1","name":"Chris"}`, - nil, - ) - expected := TestStruct{ID: "id1", Name: "Chris"} - actual := TestStruct{} + tokn := "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb" + var apiServer = testUtil.CreateGetJsonTestServer( + t, + tokn, + `{"id":"id1","name":"Chris"}`, + nil, + ) + expected := TestStruct{ID: "id1", Name: "Chris"} + actual := TestStruct{} - s, _ := openstack.NewSession(nil, "", nil) - var headers http.Header = http.Header{} - headers.Set("X-Auth-Token", tokn) - headers.Set("Accept", "application/json") - headers.Set("Etag", "md5hash-blahblah") - resp, err := s.Get(apiServer.URL, nil, &headers) - if err != nil { - t.Error(err) - } - testUtil.IsNil(t, err) + s, _ := openstack.NewSession(nil, "", nil) + var headers http.Header = http.Header{} + headers.Set("X-Auth-Token", tokn) + headers.Set("Accept", "application/json") + headers.Set("Etag", "md5hash-blahblah") + resp, err := s.Get(apiServer.URL, nil, &headers) + if err != nil { + t.Error(err) + } + testUtil.IsNil(t, err) - if err = json.Unmarshal(resp.Body, &actual); err != nil { - t.Error(err) - } + if err = json.Unmarshal(resp.Body, &actual); err != nil { + t.Error(err) + } - testUtil.Equals(t, expected, actual) + testUtil.Equals(t, expected, actual) } diff --git a/testUtil/testUtil.go b/testUtil/testUtil.go index 720486b..ad48a53 100644 --- a/testUtil/testUtil.go +++ b/testUtil/testUtil.go @@ -61,24 +61,24 @@ func IsNil(tb testing.TB, act interface{}) { // JSON Get requests. Takes a token, JSON payload, and a verification function // to do additional validation func CreateGetJsonTestServer( - t *testing.T, - expectedAuthToken string, - jsonResponsePayload string, - verifyRequest func(*http.Request)) *httptest.Server { - return httptest.NewServer(http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - headerValuesEqual(t, r, "X-Auth-Token", expectedAuthToken) - headerValuesEqual(t, r, "Accept", "application/json") - // verifyRequest(r) - if r.Method == "GET" { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(jsonResponsePayload)) - w.WriteHeader(http.StatusOK) - return - } + t *testing.T, + expectedAuthToken string, + jsonResponsePayload string, + verifyRequest func(*http.Request)) *httptest.Server { + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + headerValuesEqual(t, r, "X-Auth-Token", expectedAuthToken) + headerValuesEqual(t, r, "Accept", "application/json") + // verifyRequest(r) + if r.Method == "GET" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(jsonResponsePayload)) + w.WriteHeader(http.StatusOK) + return + } - t.Error(errors.New("Failed: r.Method == GET")) - })) + t.Error(errors.New("Failed: r.Method == GET")) + })) } // CreateGetJSONTestRequestServer creates a httptest.Server that can be used to test GetJson requests. Just specify the token,