Merge lp:~rogpeppe/goose/state-of-the-world into lp:~rogpeppe/goose/dummy-trunk

Proposed by Roger Peppe
Status: Work in progress
Proposed branch: lp:~rogpeppe/goose/state-of-the-world
Merge into: lp:~rogpeppe/goose/dummy-trunk
Diff against target: 2685 lines (+2521/-2)
30 files modified
.bzrignore (+2/-0)
.lbox (+1/-0)
.lbox.check (+20/-0)
client/client.go (+658/-0)
client/client_test.go (+428/-0)
errors/errors.go (+17/-0)
goose.go (+0/-1)
goose_test.go (+0/-1)
http/client.go (+185/-0)
identity/identity.go (+50/-0)
identity/identity_test.go (+67/-0)
identity/legacy.go (+45/-0)
identity/legacy_test.go (+37/-0)
identity/setup_test.go (+10/-0)
identity/userpass.go (+111/-0)
identity/userpass_test.go (+25/-0)
testing/envsuite/envsuite.go (+33/-0)
testing/envsuite/envsuite_test.go (+48/-0)
testing/httpsuite/httpsuite.go (+43/-0)
testing/httpsuite/httpsuite_test.go (+39/-0)
testservices/identityservice/identityservice.go (+10/-0)
testservices/identityservice/legacy.go (+44/-0)
testservices/identityservice/legacy_test.go (+96/-0)
testservices/identityservice/service_test.go (+23/-0)
testservices/identityservice/setup_test.go (+10/-0)
testservices/identityservice/userpass.go (+241/-0)
testservices/identityservice/userpass_test.go (+150/-0)
testservices/identityservice/util.go (+31/-0)
testservices/identityservice/util_test.go (+28/-0)
testservices/main.go (+69/-0)
To merge this branch: bzr merge lp:~rogpeppe/goose/state-of-the-world
Reviewer Review Type Date Requested Status
Roger Peppe Pending
Review via email: mp+136144@code.launchpad.net

Description of the change

goose: all current code

This CL is a way for anyone to comment on
the current state of the Goose world.

https://codereview.appspot.com/6844087/

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

Just adding a comment so that I get subscribed to any discussions on
this MP.

https://codereview.appspot.com/6844087/

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (15.7 KiB)

Reviewers: mp+136144_code.launchpad.net, john.meinel,

Message:
lots of comments but mostly superficial.

i haven't looked at the tests at all yet, and i have no knowledge of
openstack at all, so can't comment on the deeper aspects of the code.

https://codereview.appspot.com/6844087/diff/1/client/client.go
File client/client.go (right):

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode15
client/client.go:15: OS_API_TOKENS = "/tokens"
CAPITAL_LETTER_CONSTANTS aren't conventional in Go. Moreover, it looks
like these aren't intended to be part of the public API.

how about:

const (
     apiTokens = "/tokens"
     apiFlavors = ... etc
)

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode24
client/client.go:24: GET = "GET"
unexport. and... can we just use the string constants? i don't see that
defining constants here adds greatly to safety.

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode32
client/client.go:32: type OpenStackClient struct {
s/OpenStackClient/Client/ ?

after all goose is all about openstack - i doubt we need to restate
that.

also, needs a doc comment.

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode33
client/client.go:33: client *goosehttp.GooseHTTPClient
goosehttp.Client would be a better name.

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode39
client/client.go:39: ServiceURLs map[string]string
doc comment (what's the key and value in the map?)

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode45
client/client.go:45: func NewOpenStackClient(creds
*identity.Credentials, auth_method int) *OpenStackClient {
doc comment

also, s/auth_method/authMethod/

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode51
client/client.go:51: case identity.AUTH_LEGACY:
given that the identify package defines these constants and implements
the authentication schemes, how about

    client.auth, err = identity.AuthMethod(authMethod)

let the identity package implement this logic.

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode59
client/client.go:59: func (c *OpenStackClient) Authenticate() (err
error) {
doc comment.

if you don't use AddErrorContext, you won't need to name the variable.

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode60
client/client.go:60: err = nil
d

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode62
client/client.go:62: return fmt.Errorf("Authentication method has not
been specified")
s/fmt.Errorf/errors.New/

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode66
client/client.go:66: gooseerrors.AddErrorContext(&err, "authentication
failed")
return fmt.Errorf("authentication failed: %v", err)

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode70
client/client.go:70: c.TokenId = authDetails.TokenId
might it make sense to simply add the auth details as a field in the
OpenStackClient type?

https://codereview.appspot.com/6844087/diff/1/client/client.go#newcode77
client/client.go:77: func (c *OpenStackClient) IsAuthenticated() bool {
doc comment.

https://code...

Revision history for this message
John A Meinel (jameinel) wrote :

https://codereview.appspot.com/6844087/diff/1/identity/identity.go
File identity/identity.go (right):

https://codereview.appspot.com/6844087/diff/1/identity/identity.go#newcode8
identity/identity.go:8: AUTH_LEGACY = iota
On 2012/11/26 12:51:38, rog wrote:
> authLegacy ?

This one is actually part of the public api. Though AuthLegacy would be
ok.

https://codereview.appspot.com/6844087/

Revision history for this message
Roger Peppe (rogpeppe) wrote :

https://codereview.appspot.com/6844087/diff/1/identity/identity.go
File identity/identity.go (right):

https://codereview.appspot.com/6844087/diff/1/identity/identity.go#newcode8
identity/identity.go:8: AUTH_LEGACY = iota
On 2012/11/26 16:30:58, john.meinel wrote:
> On 2012/11/26 12:51:38, rog wrote:
> > authLegacy ?

> This one is actually part of the public api. Though AuthLegacy would
be ok.

yeah, i saw that. although, given that the auth method is an argument to
NewOpenStackClient, perhaps it wouldn't be unreasonable to ask clients
to pass in an auth method value themselves, rather than a constant that
implies one. alternatively, you could make these constants of a type
that has a Method() Authenticator method.

requiring external clients to enumerate all the possible auth methods
when we can easily make the linkage here seems somewhat unnecessary.

i'm probably missing something crucial though.

https://codereview.appspot.com/6844087/

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

...

>> This one is actually part of the public api. Though AuthLegacy
>> would
> be ok.
>
> yeah, i saw that. although, given that the auth method is an
> argument to NewOpenStackClient, perhaps it wouldn't be unreasonable
> to ask clients to pass in an auth method value themselves, rather
> than a constant that implies one. alternatively, you could make
> these constants of a type that has a Method() Authenticator
> method.
>
> requiring external clients to enumerate all the possible auth
> methods when we can easily make the linkage here seems somewhat
> unnecessary.
>
> i'm probably missing something crucial though.
>

Ultimately the auth that we pick has to come from the
environments.yaml file. Because it depends heavily on what openstack
cloud we are connecting to. (HP's auth is different from Rackspace, is
different from Canonistack, etc.)

So ideally the outside-of-goose level would just be a string request
of an auth to use, of a specific set of enumerated constants.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.12 (Cygwin)
Comment: Using GnuPG with undefined - http://www.enigmail.net/

iEYEARECAAYFAlC0QPcACgkQJdeBCYSNAANURwCgzxS64UxSnFL2spmLcUEbtKPI
7Q4AoLlK1FjFBaLcHgdxoaARbM193kSx
=COy9
-----END PGP SIGNATURE-----

Revision history for this message
Roger Peppe (rogpeppe) wrote :

On 27 November 2012 04:26, John Arbash Meinel <email address hidden> wrote:
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> ...
>
>
>>> This one is actually part of the public api. Though AuthLegacy
>>> would
>> be ok.
>>
>> yeah, i saw that. although, given that the auth method is an
>> argument to NewOpenStackClient, perhaps it wouldn't be unreasonable
>> to ask clients to pass in an auth method value themselves, rather
>> than a constant that implies one. alternatively, you could make
>> these constants of a type that has a Method() Authenticator
>> method.
>>
>> requiring external clients to enumerate all the possible auth
>> methods when we can easily make the linkage here seems somewhat
>> unnecessary.
>>
>> i'm probably missing something crucial though.
>>
>
> Ultimately the auth that we pick has to come from the
> environments.yaml file. Because it depends heavily on what openstack
> cloud we are connecting to. (HP's auth is different from Rackspace, is
> different from Canonistack, etc.)
>
> So ideally the outside-of-goose level would just be a string request
> of an auth to use, of a specific set of enumerated constants.

if this is the case, then something that maps from string to authenticator
type might seem more appropriate - the intermediate iota-based constants
still seem a little redundant.

Unmerged revisions

19. By Ian Booth

gofmt fixes

18. By Ian Booth

Extract identity functionality from client, and also extract common HTTP methods

17. By Martin Packman

Various changes including swift client calls

16. By Martin Packman

Add .lbox config to make proposing using lbox easier

Also add .lbox.check script from juju-core, to make sure go fmt is run before
proposing a change.

15. By John A Meinel

Implement getting credentials from environment variables.

14. By Martin Packman

Add customisation of service catalog in test identity service

13. By John A Meinel

Remove an unused import in client.

12. By John A Meinel

go fmt

11. By Dimiter Naydenov

Finished live server tests for all implemented API calls

10. By Dimiter Naydenov

Merge trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2012-11-26 11:24:05 +0000
4@@ -0,0 +1,2 @@
5+./tags
6+testservices/testservices*
7
8=== added file '.lbox'
9--- .lbox 1970-01-01 00:00:00 +0000
10+++ .lbox 2012-11-26 11:24:05 +0000
11@@ -0,0 +1,1 @@
12+propose -cr -for lp:goose
13
14=== added file '.lbox.check'
15--- .lbox.check 1970-01-01 00:00:00 +0000
16+++ .lbox.check 2012-11-26 11:24:05 +0000
17@@ -0,0 +1,20 @@
18+#!/bin/sh
19+
20+set -e
21+
22+BADFMT=`find * -name '*.go' | xargs gofmt -l`
23+if [ -n "$BADFMT" ]; then
24+ BADFMT=`echo "$BADFMT" | sed "s/^/ /"`
25+ echo "gofmt is sad:\n\n$BADFMT"
26+ exit 1
27+fi
28+
29+VERSION=`go version | awk '{print $3}'`
30+if [ $VERSION == 'devel' ]; then
31+ go tool vet \
32+ -methods \
33+ -printf \
34+ -rangeloops \
35+ -printfuncs ErrorContextf:1 \
36+ .
37+fi
38
39=== added directory 'client'
40=== added file 'client/client.go'
41--- client/client.go 1970-01-01 00:00:00 +0000
42+++ client/client.go 2012-11-26 11:24:05 +0000
43@@ -0,0 +1,658 @@
44+package client
45+
46+import (
47+ "encoding/base64"
48+ "errors"
49+ "fmt"
50+ gooseerrors "launchpad.net/goose/errors"
51+ goosehttp "launchpad.net/goose/http"
52+ "launchpad.net/goose/identity"
53+ "net/http"
54+ "net/url"
55+)
56+
57+const (
58+ OS_API_TOKENS = "/tokens"
59+ OS_API_FLAVORS = "/flavors"
60+ OS_API_FLAVORS_DETAIL = "/flavors/detail"
61+ OS_API_SERVERS = "/servers"
62+ OS_API_SERVERS_DETAIL = "/servers/detail"
63+ OS_API_SECURITY_GROUPS = "/os-security-groups"
64+ OS_API_SECURITY_GROUP_RULES = "/os-security-group-rules"
65+ OS_API_FLOATING_IPS = "/os-floating-ips"
66+
67+ GET = "GET"
68+ POST = "POST"
69+ PUT = "PUT"
70+ DELETE = "DELETE"
71+ HEAD = "HEAD"
72+ COPY = "COPY"
73+)
74+
75+type OpenStackClient struct {
76+ client *goosehttp.GooseHTTPClient
77+
78+ creds *identity.Credentials
79+ auth identity.Authenticator
80+
81+ //TODO - store service urls by region.
82+ ServiceURLs map[string]string
83+ TokenId string
84+ TenantId string
85+ UserId string
86+}
87+
88+func NewOpenStackClient(creds *identity.Credentials, auth_method int) *OpenStackClient {
89+ client := OpenStackClient{creds: creds}
90+ client.creds.URL = client.creds.URL + OS_API_TOKENS
91+ switch auth_method {
92+ default:
93+ panic(fmt.Errorf("Invalid identity authorisation method: %d", auth_method))
94+ case identity.AUTH_LEGACY:
95+ client.auth = &identity.Legacy{}
96+ case identity.AUTH_USERPASS:
97+ client.auth = &identity.UserPass{}
98+ }
99+ return &client
100+}
101+
102+func (c *OpenStackClient) Authenticate() (err error) {
103+ err = nil
104+ if c.auth == nil {
105+ return fmt.Errorf("Authentication method has not been specified")
106+ }
107+ authDetails, err := c.auth.Auth(c.creds)
108+ if err != nil {
109+ gooseerrors.AddErrorContext(&err, "authentication failed")
110+ return
111+ }
112+
113+ c.TokenId = authDetails.TokenId
114+ c.TenantId = authDetails.TenantId
115+ c.UserId = authDetails.UserId
116+ c.ServiceURLs = authDetails.ServiceURLs
117+ return nil
118+}
119+
120+func (c *OpenStackClient) IsAuthenticated() bool {
121+ return c.TokenId != ""
122+}
123+
124+type Link struct {
125+ Href string
126+ Rel string
127+ Type string
128+}
129+
130+// Entity can describe a flavor, flavor detail or server.
131+// Contains a list of links.
132+type Entity struct {
133+ Id string
134+ Links []Link
135+ Name string
136+}
137+
138+func (c *OpenStackClient) ListFlavors() (flavors []Entity, err error) {
139+
140+ var resp struct {
141+ Flavors []Entity
142+ }
143+ requestData := goosehttp.RequestData{RespValue: &resp}
144+ err = c.authRequest(GET, "compute", OS_API_FLAVORS, nil, &requestData)
145+ if err != nil {
146+ gooseerrors.AddErrorContext(&err, "failed to get list of flavors")
147+ return
148+ }
149+
150+ return resp.Flavors, nil
151+}
152+
153+type FlavorDetail struct {
154+ Name string
155+ RAM int
156+ VCPUs int
157+ Disk int
158+ Id string
159+ Swap interface{} // Can be an empty string (?!)
160+}
161+
162+func (c *OpenStackClient) ListFlavorsDetail() (flavors []FlavorDetail, err error) {
163+
164+ var resp struct {
165+ Flavors []FlavorDetail
166+ }
167+ requestData := goosehttp.RequestData{RespValue: &resp}
168+ err = c.authRequest(GET, "compute", OS_API_FLAVORS_DETAIL, nil, &requestData)
169+ if err != nil {
170+ gooseerrors.AddErrorContext(&err, "failed to get list of flavors details")
171+ return
172+ }
173+
174+ return resp.Flavors, nil
175+}
176+
177+func (c *OpenStackClient) ListServers() (servers []Entity, err error) {
178+
179+ var resp struct {
180+ Servers []Entity
181+ }
182+ requestData := goosehttp.RequestData{RespValue: &resp, ExpectedStatus: []int{http.StatusOK}}
183+ err = c.authRequest(GET, "compute", OS_API_SERVERS, nil, &requestData)
184+ if err != nil {
185+ gooseerrors.AddErrorContext(&err, "failed to get list of servers")
186+ return
187+ }
188+ return resp.Servers, nil
189+}
190+
191+type ServerDetail struct {
192+ AddressIPv4 string
193+ AddressIPv6 string
194+ Created string
195+ Flavor Entity
196+ HostId string
197+ Id string
198+ Image Entity
199+ Links []Link
200+ Name string
201+ Progress int
202+ Status string
203+ TenantId string `json:"tenant_id"`
204+ Updated string
205+ UserId string `json:"user_id"`
206+}
207+
208+func (c *OpenStackClient) ListServersDetail() (servers []ServerDetail, err error) {
209+
210+ var resp struct {
211+ Servers []ServerDetail
212+ }
213+ requestData := goosehttp.RequestData{RespValue: &resp}
214+ err = c.authRequest(GET, "compute", OS_API_SERVERS_DETAIL, nil, &requestData)
215+ if err != nil {
216+ gooseerrors.AddErrorContext(&err, "failed to get list of servers details")
217+ return
218+ }
219+
220+ return resp.Servers, nil
221+}
222+
223+func (c *OpenStackClient) GetServer(serverId string) (ServerDetail, error) {
224+
225+ var resp struct {
226+ Server ServerDetail
227+ }
228+ url := fmt.Sprintf("%s/%s", OS_API_SERVERS, serverId)
229+ requestData := goosehttp.RequestData{RespValue: &resp}
230+ err := c.authRequest(GET, "compute", url, nil, &requestData)
231+ if err != nil {
232+ gooseerrors.AddErrorContext(&err, "failed to get details for serverId=%s", serverId)
233+ return ServerDetail{}, err
234+ }
235+
236+ return resp.Server, nil
237+}
238+
239+func (c *OpenStackClient) DeleteServer(serverId string) error {
240+
241+ var resp struct {
242+ Server ServerDetail
243+ }
244+ url := fmt.Sprintf("%s/%s", OS_API_SERVERS, serverId)
245+ requestData := goosehttp.RequestData{RespValue: &resp, ExpectedStatus: []int{http.StatusNoContent}}
246+ err := c.authRequest(DELETE, "compute", url, nil, &requestData)
247+ if err != nil {
248+ gooseerrors.AddErrorContext(&err, "failed to delete server with serverId=%s", serverId)
249+ return err
250+ }
251+
252+ return nil
253+}
254+
255+type RunServerOpts struct {
256+ Name string `json:"name"`
257+ FlavorId string `json:"flavorRef"`
258+ ImageId string `json:"imageRef"`
259+ UserData *string `json:"user_data"`
260+ SecurityGroupNames []struct {
261+ Name string `json:"name"`
262+ } `json:"security_groups"`
263+}
264+
265+func (c *OpenStackClient) RunServer(opts RunServerOpts) (err error) {
266+
267+ var req struct {
268+ Server RunServerOpts `json:"server"`
269+ }
270+ req.Server = opts
271+ if opts.UserData != nil {
272+ data := []byte(*opts.UserData)
273+ encoded := base64.StdEncoding.EncodeToString(data)
274+ req.Server.UserData = &encoded
275+ }
276+ requestData := goosehttp.RequestData{ReqValue: req, ExpectedStatus: []int{http.StatusAccepted}}
277+ err = c.authRequest(POST, "compute", OS_API_SERVERS, nil, &requestData)
278+ if err != nil {
279+ gooseerrors.AddErrorContext(&err, "failed to run a server with %#v", opts)
280+ }
281+
282+ return
283+}
284+
285+type SecurityGroupRule struct {
286+ FromPort *int `json:"from_port"` // Can be nil
287+ IPProtocol *string `json:"ip_protocol"` // Can be nil
288+ ToPort *int `json:"to_port"` // Can be nil
289+ ParentGroupId int `json:"parent_group_id"`
290+ IPRange map[string]string `json:"ip_range"` // Can be empty
291+ Id int
292+ Group map[string]string // Can be empty
293+}
294+
295+type SecurityGroup struct {
296+ Rules []SecurityGroupRule
297+ TenantId string `json:"tenant_id"`
298+ Id int
299+ Name string
300+ Description string
301+}
302+
303+func (c *OpenStackClient) ListSecurityGroups() (groups []SecurityGroup, err error) {
304+
305+ var resp struct {
306+ Groups []SecurityGroup `json:"security_groups"`
307+ }
308+ requestData := goosehttp.RequestData{RespValue: &resp}
309+ err = c.authRequest(GET, "compute", OS_API_SECURITY_GROUPS, nil, &requestData)
310+ if err != nil {
311+ gooseerrors.AddErrorContext(&err, "failed to list security groups")
312+ return nil, err
313+ }
314+
315+ return resp.Groups, nil
316+}
317+
318+func (c *OpenStackClient) GetServerSecurityGroups(serverId string) (groups []SecurityGroup, err error) {
319+
320+ var resp struct {
321+ Groups []SecurityGroup `json:"security_groups"`
322+ }
323+ url := fmt.Sprintf("%s/%s/%s", OS_API_SERVERS, serverId, OS_API_SECURITY_GROUPS)
324+ requestData := goosehttp.RequestData{RespValue: &resp}
325+ err = c.authRequest(GET, "compute", url, nil, &requestData)
326+ if err != nil {
327+ gooseerrors.AddErrorContext(&err, "failed to list server (%s) security groups", serverId)
328+ return nil, err
329+ }
330+
331+ return resp.Groups, nil
332+}
333+
334+func (c *OpenStackClient) CreateSecurityGroup(name, description string) (group SecurityGroup, err error) {
335+
336+ var req struct {
337+ SecurityGroup struct {
338+ Name string `json:"name"`
339+ Description string `json:"description"`
340+ } `json:"security_group"`
341+ }
342+ req.SecurityGroup.Name = name
343+ req.SecurityGroup.Description = description
344+
345+ var resp struct {
346+ SecurityGroup SecurityGroup `json:"security_group"`
347+ }
348+ requestData := goosehttp.RequestData{ReqValue: req, RespValue: &resp, ExpectedStatus: []int{http.StatusOK}}
349+ err = c.authRequest(POST, "compute", OS_API_SECURITY_GROUPS, nil, &requestData)
350+ if err != nil {
351+ gooseerrors.AddErrorContext(&err, "failed to create a security group with name=%s", name)
352+ }
353+ group = resp.SecurityGroup
354+
355+ return
356+}
357+
358+func (c *OpenStackClient) DeleteSecurityGroup(groupId int) (err error) {
359+
360+ url := fmt.Sprintf("%s/%d", OS_API_SECURITY_GROUPS, groupId)
361+ requestData := goosehttp.RequestData{ExpectedStatus: []int{http.StatusAccepted}}
362+ err = c.authRequest(DELETE, "compute", url, nil, &requestData)
363+ if err != nil {
364+ gooseerrors.AddErrorContext(&err, "failed to delete a security group with id=%d", groupId)
365+ }
366+
367+ return
368+}
369+
370+type RuleInfo struct {
371+ IPProtocol string `json:"ip_protocol"` // Required, if GroupId is nil
372+ FromPort int `json:"from_port"` // Required, if GroupId is nil
373+ ToPort int `json:"to_port"` // Required, if GroupId is nil
374+ Cidr string `json:"cidr"` // Required, if GroupId is nil
375+ GroupId *int `json:"group_id"` // If nil, FromPort/ToPort/IPProtocol must be set
376+ ParentGroupId int `json:"parent_group_id"` // Required always
377+}
378+
379+func (c *OpenStackClient) CreateSecurityGroupRule(ruleInfo RuleInfo) (rule SecurityGroupRule, err error) {
380+
381+ var req struct {
382+ SecurityGroupRule RuleInfo `json:"security_group_rule"`
383+ }
384+ req.SecurityGroupRule = ruleInfo
385+
386+ var resp struct {
387+ SecurityGroupRule SecurityGroupRule `json:"security_group_rule"`
388+ }
389+
390+ requestData := goosehttp.RequestData{ReqValue: req, RespValue: &resp}
391+ err = c.authRequest(POST, "compute", OS_API_SECURITY_GROUP_RULES, nil, &requestData)
392+ if err != nil {
393+ gooseerrors.AddErrorContext(&err, "failed to create a rule for the security group with id=%s", ruleInfo.GroupId)
394+ }
395+
396+ return resp.SecurityGroupRule, err
397+}
398+
399+func (c *OpenStackClient) DeleteSecurityGroupRule(ruleId int) (err error) {
400+
401+ url := fmt.Sprintf("%s/%d", OS_API_SECURITY_GROUP_RULES, ruleId)
402+ requestData := goosehttp.RequestData{ExpectedStatus: []int{http.StatusAccepted}}
403+ err = c.authRequest(DELETE, "compute", url, nil, &requestData)
404+ if err != nil {
405+ gooseerrors.AddErrorContext(&err, "failed to delete a security group rule with id=%d", ruleId)
406+ }
407+
408+ return
409+}
410+
411+func (c *OpenStackClient) AddServerSecurityGroup(serverId, groupName string) (err error) {
412+
413+ var req struct {
414+ AddSecurityGroup struct {
415+ Name string `json:"name"`
416+ } `json:"addSecurityGroup"`
417+ }
418+ req.AddSecurityGroup.Name = groupName
419+
420+ url := fmt.Sprintf("%s/%s/action", OS_API_SERVERS, serverId)
421+ requestData := goosehttp.RequestData{ReqValue: req, ExpectedStatus: []int{http.StatusAccepted}}
422+ err = c.authRequest(POST, "compute", url, nil, &requestData)
423+ if err != nil {
424+ gooseerrors.AddErrorContext(&err, "failed to add security group '%s' from server with id=%s", groupName, serverId)
425+ }
426+ return
427+}
428+
429+func (c *OpenStackClient) RemoveServerSecurityGroup(serverId, groupName string) (err error) {
430+
431+ var req struct {
432+ RemoveSecurityGroup struct {
433+ Name string `json:"name"`
434+ } `json:"removeSecurityGroup"`
435+ }
436+ req.RemoveSecurityGroup.Name = groupName
437+
438+ url := fmt.Sprintf("%s/%s/action", OS_API_SERVERS, serverId)
439+ requestData := goosehttp.RequestData{ReqValue: req, ExpectedStatus: []int{http.StatusAccepted}}
440+ err = c.authRequest(POST, "compute", url, nil, &requestData)
441+ if err != nil {
442+ gooseerrors.AddErrorContext(&err, "failed to remove security group '%s' from server with id=%s", groupName, serverId)
443+ }
444+ return
445+}
446+
447+type FloatingIP struct {
448+ FixedIP interface{} `json:"fixed_ip"` // Can be a string or null
449+ Id int `json:"id"`
450+ InstanceId interface{} `json:"instance_id"` // Can be a string or null
451+ IP string `json:"ip"`
452+ Pool string `json:"pool"`
453+}
454+
455+func (c *OpenStackClient) ListFloatingIPs() (ips []FloatingIP, err error) {
456+
457+ var resp struct {
458+ FloatingIPs []FloatingIP `json:"floating_ips"`
459+ }
460+
461+ requestData := goosehttp.RequestData{RespValue: &resp}
462+ err = c.authRequest(GET, "compute", OS_API_FLOATING_IPS, nil, &requestData)
463+ if err != nil {
464+ gooseerrors.AddErrorContext(&err, "failed to list floating ips")
465+ }
466+
467+ return resp.FloatingIPs, err
468+}
469+
470+func (c *OpenStackClient) GetFloatingIP(ipId int) (ip FloatingIP, err error) {
471+
472+ var resp struct {
473+ FloatingIP FloatingIP `json:"floating_ip"`
474+ }
475+
476+ url := fmt.Sprintf("%s/%d", OS_API_FLOATING_IPS, ipId)
477+ requestData := goosehttp.RequestData{RespValue: &resp}
478+ err = c.authRequest(GET, "compute", url, nil, &requestData)
479+ if err != nil {
480+ gooseerrors.AddErrorContext(&err, "failed to get floating ip %d details", ipId)
481+ }
482+
483+ return resp.FloatingIP, err
484+}
485+
486+func (c *OpenStackClient) AllocateFloatingIP() (ip FloatingIP, err error) {
487+
488+ var resp struct {
489+ FloatingIP FloatingIP `json:"floating_ip"`
490+ }
491+
492+ requestData := goosehttp.RequestData{RespValue: &resp}
493+ err = c.authRequest(POST, "compute", OS_API_FLOATING_IPS, nil, &requestData)
494+ if err != nil {
495+ gooseerrors.AddErrorContext(&err, "failed to allocate a floating ip")
496+ }
497+
498+ return resp.FloatingIP, err
499+}
500+
501+func (c *OpenStackClient) DeleteFloatingIP(ipId int) (err error) {
502+
503+ url := fmt.Sprintf("%s/%d", OS_API_FLOATING_IPS, ipId)
504+ requestData := goosehttp.RequestData{ExpectedStatus: []int{http.StatusAccepted}}
505+ err = c.authRequest(DELETE, "compute", url, nil, &requestData)
506+ if err != nil {
507+ gooseerrors.AddErrorContext(&err, "failed to delete floating ip %d details", ipId)
508+ }
509+
510+ return
511+}
512+
513+func (c *OpenStackClient) AddServerFloatingIP(serverId, address string) (err error) {
514+
515+ var req struct {
516+ AddFloatingIP struct {
517+ Address string `json:"address"`
518+ } `json:"addFloatingIp"`
519+ }
520+ req.AddFloatingIP.Address = address
521+
522+ url := fmt.Sprintf("%s/%s/action", OS_API_SERVERS, serverId)
523+ requestData := goosehttp.RequestData{ReqValue: req, ExpectedStatus: []int{http.StatusAccepted}}
524+ err = c.authRequest(POST, "compute", url, nil, &requestData)
525+ if err != nil {
526+ gooseerrors.AddErrorContext(&err, "failed to add floating ip %s to server %s", address, serverId)
527+ }
528+
529+ return
530+}
531+
532+func (c *OpenStackClient) RemoveServerFloatingIP(serverId, address string) (err error) {
533+
534+ var req struct {
535+ RemoveFloatingIP struct {
536+ Address string `json:"address"`
537+ } `json:"removeFloatingIp"`
538+ }
539+ req.RemoveFloatingIP.Address = address
540+
541+ url := fmt.Sprintf("%s/%s/action", OS_API_SERVERS, serverId)
542+ requestData := goosehttp.RequestData{ReqValue: req, ExpectedStatus: []int{http.StatusAccepted}}
543+ err = c.authRequest(POST, "compute", url, nil, &requestData)
544+ if err != nil {
545+ gooseerrors.AddErrorContext(&err, "failed to remove floating ip %s to server %s", address, serverId)
546+ }
547+
548+ return
549+}
550+
551+func (c *OpenStackClient) CreateContainer(containerName string) (err error) {
552+
553+ // Juju expects there to be a (semi) public url for some objects. This
554+ // could probably be more restrictive or placed in a seperate container
555+ // with some refactoring, but for now just make everything public.
556+ headers := make(http.Header)
557+ headers.Add("X-Container-Read", ".r:*")
558+ url := fmt.Sprintf("/%s", containerName)
559+ requestData := goosehttp.RequestData{ReqHeaders: headers, ExpectedStatus: []int{http.StatusAccepted, http.StatusCreated}}
560+ err = c.authRequest(PUT, "object-store", url, nil, &requestData)
561+ if err != nil {
562+ gooseerrors.AddErrorContext(&err, "failed to create container %s.", containerName)
563+ }
564+
565+ return
566+}
567+
568+func (c *OpenStackClient) DeleteContainer(containerName string) (err error) {
569+
570+ url := fmt.Sprintf("/%s", containerName)
571+ requestData := goosehttp.RequestData{ExpectedStatus: []int{http.StatusNoContent}}
572+ err = c.authRequest(DELETE, "object-store", url, nil, &requestData)
573+ if err != nil {
574+ gooseerrors.AddErrorContext(&err, "failed to delete container %s.", containerName)
575+ }
576+
577+ return
578+}
579+
580+func (c *OpenStackClient) PublicObjectURL(containerName, objectName string) (url string, err error) {
581+ path := fmt.Sprintf("/%s/%s", containerName, objectName)
582+ return c.makeUrl("object-store", []string{path}, nil)
583+}
584+
585+func (c *OpenStackClient) HeadObject(containerName, objectName string) (headers http.Header, err error) {
586+
587+ url, err := c.PublicObjectURL(containerName, objectName)
588+ if err != nil {
589+ return nil, err
590+ }
591+ requestData := goosehttp.RequestData{ReqHeaders: headers, ExpectedStatus: []int{http.StatusOK}}
592+ err = c.authRequest(HEAD, "object-store", url, nil, &requestData)
593+ if err != nil {
594+ gooseerrors.AddErrorContext(&err, "failed to HEAD object %s from container %s", objectName, containerName)
595+ return nil, err
596+ }
597+ return headers, nil
598+}
599+
600+func (c *OpenStackClient) GetObject(containerName, objectName string) (obj []byte, err error) {
601+
602+ url, err := c.PublicObjectURL(containerName, objectName)
603+ if err != nil {
604+ return nil, err
605+ }
606+ requestData := goosehttp.RequestData{RespData: &obj, ExpectedStatus: []int{http.StatusOK}}
607+ err = c.authBinaryRequest(GET, "object-store", url, nil, &requestData)
608+ if err != nil {
609+ gooseerrors.AddErrorContext(&err, "failed to GET object %s content from container %s", objectName, containerName)
610+ return nil, err
611+ }
612+ return obj, nil
613+}
614+
615+func (c *OpenStackClient) DeleteObject(containerName, objectName string) (err error) {
616+
617+ url, err := c.PublicObjectURL(containerName, objectName)
618+ if err != nil {
619+ return err
620+ }
621+ requestData := goosehttp.RequestData{ExpectedStatus: []int{http.StatusAccepted}}
622+ err = c.authRequest(DELETE, "object-store", url, nil, &requestData)
623+ if err != nil {
624+ gooseerrors.AddErrorContext(&err, "failed to DELETE object %s content from container %s", objectName, containerName)
625+ }
626+ return err
627+}
628+
629+func (c *OpenStackClient) PutObject(containerName, objectName string, data []byte) (err error) {
630+
631+ url, err := c.PublicObjectURL(containerName, objectName)
632+ if err != nil {
633+ return err
634+ }
635+ requestData := goosehttp.RequestData{ReqData: data, ExpectedStatus: []int{http.StatusAccepted}}
636+ err = c.authBinaryRequest(PUT, "object-store", url, nil, &requestData)
637+ if err != nil {
638+ gooseerrors.AddErrorContext(&err, "failed to PUT object %s content from container %s", objectName, containerName)
639+ }
640+ return err
641+}
642+
643+////////////////////////////////////////////////////////////////////////
644+// Private helpers
645+
646+// makeUrl prepares a full URL to a service endpoint, with optional
647+// URL parts, appended to it and optional query string params. It
648+// uses the first endpoint it can find for the given service type
649+func (c *OpenStackClient) makeUrl(serviceType string, parts []string, params url.Values) (string, error) {
650+ url, ok := c.ServiceURLs[serviceType]
651+ if !ok {
652+ return "", errors.New("no endpoints known for service type: " + serviceType)
653+ }
654+ for _, part := range parts {
655+ url += part
656+ }
657+ if params != nil {
658+ url += "?" + params.Encode()
659+ }
660+ return url, nil
661+}
662+
663+func (c *OpenStackClient) setupRequest(svcType, apiCall string, params url.Values, requestData *goosehttp.RequestData) (url string, err error) {
664+ if !c.IsAuthenticated() {
665+ return "", errors.New("not authenticated")
666+ }
667+
668+ url, err = c.makeUrl(svcType, []string{apiCall}, params)
669+ if err != nil {
670+ gooseerrors.AddErrorContext(&err, "cannot find a '%s' node endpoint", svcType)
671+ return
672+ }
673+
674+ if c.client == nil {
675+ c.client = &goosehttp.GooseHTTPClient{http.Client{CheckRedirect: nil}}
676+ }
677+
678+ if requestData.ReqHeaders == nil {
679+ requestData.ReqHeaders = make(http.Header)
680+ }
681+ requestData.ReqHeaders.Add("X-Auth-Token", c.TokenId)
682+ return
683+}
684+
685+func (c *OpenStackClient) authRequest(method, svcType, apiCall string, params url.Values, requestData *goosehttp.RequestData) (err error) {
686+ url, err := c.setupRequest(svcType, apiCall, params, requestData)
687+ if err != nil {
688+ return
689+ }
690+ err = c.client.JsonRequest(method, url, requestData)
691+ return
692+}
693+
694+func (c *OpenStackClient) authBinaryRequest(method, svcType, apiCall string, params url.Values, requestData *goosehttp.RequestData) (err error) {
695+ url, err := c.setupRequest(svcType, apiCall, params, requestData)
696+ if err != nil {
697+ return
698+ }
699+ err = c.client.BinaryRequest(method, url, requestData)
700+ return
701+}
702
703=== added file 'client/client_test.go'
704--- client/client_test.go 1970-01-01 00:00:00 +0000
705+++ client/client_test.go 2012-11-26 11:24:05 +0000
706@@ -0,0 +1,428 @@
707+package client_test
708+
709+import (
710+ "flag"
711+ . "launchpad.net/gocheck"
712+ "launchpad.net/goose/client"
713+ "launchpad.net/goose/identity"
714+ "testing"
715+ "time"
716+)
717+
718+// Hook up gocheck into the gotest runner.
719+func Test(t *testing.T) { TestingT(t) }
720+
721+var live = flag.Bool("live", false, "Include live OpenStack (Canonistack) tests")
722+
723+type ClientSuite struct {
724+ client *client.OpenStackClient
725+ testServerId string
726+ skipAuth bool
727+}
728+
729+func (s *ClientSuite) SetUpSuite(c *C) {
730+ if !*live {
731+ c.Skip("-live not provided")
732+ }
733+
734+ cred := identity.CredentialsFromEnv()
735+ for i, p := range []string{cred.User, cred.Secrets, cred.TenantName, cred.Region, cred.URL} {
736+ if p == "" {
737+ c.Fatalf("required environment variable not set: %d", i)
738+ }
739+ }
740+
741+ s.client = client.NewOpenStackClient(cred, identity.AUTH_USERPASS)
742+ s.skipAuth = true // set after TestAuthenticate
743+
744+}
745+
746+// Create a test server needed to run some of the tests
747+func (s *ClientSuite) SetUpTestServer(c *C) {
748+ if s.testServerId != "" {
749+ return // Already done
750+ }
751+ s.SetUpTest(c) // Authenticate if needed
752+ ro := client.RunServerOpts{
753+ Name: "test_server1",
754+ FlavorId: "1", // m1.tiny
755+ ImageId: "3fc0ef0b-82a9-4f44-a797-a43f0f73b20e", // smoser-cloud-images/ubuntu-precise-12.04-i386-server-20120424.manifest.xml
756+ }
757+ err := s.client.RunServer(ro)
758+ if err != nil {
759+ c.Fatal("Error starting test server: " + err.Error())
760+ }
761+
762+ // Now find it and save its ID
763+ servers, err := s.client.ListServers()
764+ c.Check(err, IsNil)
765+ for _, sr := range servers {
766+ if sr.Name == "test_server1" {
767+ s.testServerId = sr.Id
768+ // Give it some time to initialize
769+ time.Sleep(8 * time.Second)
770+ break
771+ }
772+ }
773+ if s.testServerId == "" {
774+ c.Fatalf("cannot start test server")
775+ }
776+}
777+
778+func (s *ClientSuite) TearDownSuite(c *C) {
779+ if s.testServerId != "" {
780+ // Remove the test server we created earlier
781+ err := s.client.DeleteServer(s.testServerId)
782+ c.Assert(err, IsNil)
783+ }
784+}
785+
786+func (s *ClientSuite) SetUpTest(c *C) {
787+ if !s.skipAuth && !s.client.IsAuthenticated() {
788+ err := s.client.Authenticate()
789+ c.Assert(err, IsNil)
790+ c.Logf("authenticated")
791+ }
792+}
793+
794+var suite = Suite(&ClientSuite{})
795+
796+func (s *ClientSuite) TestAuthenticateFail(c *C) {
797+ cred := identity.CredentialsFromEnv()
798+ cred.User = "fred"
799+ cred.Secrets = "broken"
800+ cred.Region = ""
801+ var osclient *client.OpenStackClient = client.NewOpenStackClient(cred, identity.AUTH_USERPASS)
802+ c.Assert(osclient.IsAuthenticated(), Equals, false)
803+ var err error
804+ err = osclient.Authenticate()
805+ c.Assert(err, ErrorMatches, "authentication failed.*")
806+}
807+
808+func (s *ClientSuite) TestAuthenticate(c *C) {
809+ var err error
810+ err = s.client.Authenticate()
811+ c.Assert(err, IsNil)
812+ c.Assert(s.client.IsAuthenticated(), Equals, true)
813+
814+ // Check service endpoints are discovered
815+ c.Assert(s.client.ServiceURLs["compute"], NotNil)
816+ c.Assert(s.client.ServiceURLs["swift"], NotNil)
817+
818+ s.skipAuth = false
819+
820+ // Since this is the first test, get the test server running ASAP
821+ s.SetUpTestServer(c)
822+}
823+
824+func (s *ClientSuite) TestListFlavors(c *C) {
825+ flavors, err := s.client.ListFlavors()
826+ c.Assert(err, IsNil)
827+ if len(flavors) < 1 {
828+ c.Fatalf("no flavors to list")
829+ }
830+ for _, f := range flavors {
831+ c.Assert(f.Id, Not(Equals), "")
832+ c.Assert(f.Name, Not(Equals), "")
833+ for _, l := range f.Links {
834+ c.Assert(l.Href, Matches, "https?://.*")
835+ c.Assert(l.Rel, Matches, "self|bookmark")
836+ }
837+ }
838+}
839+
840+func (s *ClientSuite) TestListFlavorsDetail(c *C) {
841+ flavors, err := s.client.ListFlavorsDetail()
842+ c.Assert(err, IsNil)
843+ if len(flavors) < 1 {
844+ c.Fatalf("no flavors (details) to list")
845+ }
846+ for _, f := range flavors {
847+ c.Assert(f.Name, Not(Equals), "")
848+ c.Assert(f.Id, Not(Equals), "")
849+ if f.RAM < 0 || f.VCPUs < 0 || f.Disk < 0 {
850+ c.Fatalf("invalid flavor found: %#v", f)
851+ }
852+ }
853+}
854+
855+func (s *ClientSuite) TestListServers(c *C) {
856+ servers, err := s.client.ListServers()
857+ c.Assert(err, IsNil)
858+ foundTest := false
859+ for _, sr := range servers {
860+ c.Assert(sr.Id, Not(Equals), "")
861+ c.Assert(sr.Name, Not(Equals), "")
862+ if sr.Id == s.testServerId {
863+ c.Assert(sr.Name, Equals, "test_server1")
864+ foundTest = true
865+ }
866+ for _, l := range sr.Links {
867+ c.Assert(l.Href, Matches, "https?://.*")
868+ c.Assert(l.Rel, Matches, "self|bookmark")
869+ }
870+ }
871+ if !foundTest {
872+ c.Fatalf("test server (%s) not found in server list", s.testServerId)
873+ }
874+}
875+
876+func (s *ClientSuite) TestListServersDetail(c *C) {
877+ servers, err := s.client.ListServersDetail()
878+ c.Assert(err, IsNil)
879+ if len(servers) < 1 {
880+ c.Fatalf("no servers to list (expected at least 1)")
881+ }
882+ foundTest := false
883+ for _, sr := range servers {
884+ c.Assert(sr.Created, Matches, `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*`)
885+ c.Assert(sr.Updated, Matches, `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*`)
886+ c.Assert(sr.Id, Not(Equals), "")
887+ c.Assert(sr.HostId, Not(Equals), "")
888+ c.Assert(sr.TenantId, Equals, s.client.TenantId)
889+ c.Assert(sr.UserId, Equals, s.client.UserId)
890+ c.Assert(sr.Status, Not(Equals), "")
891+ c.Assert(sr.Name, Not(Equals), "")
892+ if sr.Id == s.testServerId {
893+ c.Assert(sr.Name, Equals, "test_server1")
894+ c.Assert(sr.Flavor.Id, Equals, "1")
895+ c.Assert(sr.Image.Id, Equals, "3fc0ef0b-82a9-4f44-a797-a43f0f73b20e")
896+ foundTest = true
897+ }
898+ for _, l := range sr.Links {
899+ c.Assert(l.Href, Matches, "https?://.*")
900+ c.Assert(l.Rel, Matches, "self|bookmark")
901+ }
902+ c.Assert(sr.Flavor.Id, Not(Equals), "")
903+ for _, f := range sr.Flavor.Links {
904+ c.Assert(f.Href, Matches, "https?://.*")
905+ c.Assert(f.Rel, Matches, "self|bookmark")
906+ }
907+ c.Assert(sr.Image.Id, Not(Equals), "")
908+ for _, i := range sr.Image.Links {
909+ c.Assert(i.Href, Matches, "https?://.*")
910+ c.Assert(i.Rel, Matches, "self|bookmark")
911+ }
912+ }
913+ if !foundTest {
914+ c.Fatalf("test server (%s) not found in server list (details)", s.testServerId)
915+ }
916+}
917+
918+func (s *ClientSuite) TestListSecurityGroups(c *C) {
919+ groups, err := s.client.ListSecurityGroups()
920+ c.Assert(err, IsNil)
921+ if len(groups) < 1 {
922+ c.Fatalf("no security groups found (expected at least 1)")
923+ }
924+ for _, g := range groups {
925+ c.Assert(g.TenantId, Equals, s.client.TenantId)
926+ c.Assert(g.Name, Not(Equals), "")
927+ c.Assert(g.Description, Not(Equals), "")
928+ c.Assert(g.Rules, NotNil)
929+ }
930+}
931+
932+func (s *ClientSuite) TestCreateAndDeleteSecurityGroup(c *C) {
933+ group, err := s.client.CreateSecurityGroup("test_secgroup", "test_desc")
934+ c.Check(err, IsNil)
935+ c.Check(group.Name, Equals, "test_secgroup")
936+ c.Check(group.Description, Equals, "test_desc")
937+
938+ groups, err := s.client.ListSecurityGroups()
939+ found := false
940+ for _, g := range groups {
941+ if g.Id == group.Id {
942+ found = true
943+ break
944+ }
945+ }
946+ if found {
947+ err = s.client.DeleteSecurityGroup(group.Id)
948+ c.Check(err, IsNil)
949+ } else {
950+ c.Fatalf("test security group (%d) not found", group.Id)
951+ }
952+}
953+
954+func (s *ClientSuite) TestCreateAndDeleteSecurityGroupRules(c *C) {
955+ group1, err := s.client.CreateSecurityGroup("test_secgroup1", "test_desc")
956+ c.Check(err, IsNil)
957+ group2, err := s.client.CreateSecurityGroup("test_secgroup2", "test_desc")
958+ c.Check(err, IsNil)
959+
960+ // First type of rule - port range + protocol
961+ ri := client.RuleInfo{
962+ IPProtocol: "tcp",
963+ FromPort: 1234,
964+ ToPort: 4321,
965+ Cidr: "10.0.0.0/8",
966+ ParentGroupId: group1.Id,
967+ }
968+ rule, err := s.client.CreateSecurityGroupRule(ri)
969+ c.Check(err, IsNil)
970+ c.Check(*rule.FromPort, Equals, 1234)
971+ c.Check(*rule.ToPort, Equals, 4321)
972+ c.Check(rule.ParentGroupId, Equals, group1.Id)
973+ c.Check(*rule.IPProtocol, Equals, "tcp")
974+ c.Check(rule.Group, HasLen, 0)
975+ err = s.client.DeleteSecurityGroupRule(rule.Id)
976+ c.Check(err, IsNil)
977+
978+ // Second type of rule - inherited from another group
979+ ri = client.RuleInfo{
980+ GroupId: &group2.Id,
981+ ParentGroupId: group1.Id,
982+ }
983+ rule, err = s.client.CreateSecurityGroupRule(ri)
984+ c.Check(err, IsNil)
985+ c.Check(rule.ParentGroupId, Equals, group1.Id)
986+ c.Check(rule.Group["tenant_id"], Equals, s.client.TenantId)
987+ c.Check(rule.Group["name"], Equals, "test_secgroup2")
988+ err = s.client.DeleteSecurityGroupRule(rule.Id)
989+ c.Check(err, IsNil)
990+
991+ err = s.client.DeleteSecurityGroup(group1.Id)
992+ c.Check(err, IsNil)
993+ err = s.client.DeleteSecurityGroup(group2.Id)
994+ c.Check(err, IsNil)
995+}
996+
997+func (s *ClientSuite) TestGetServer(c *C) {
998+ server, err := s.client.GetServer(s.testServerId)
999+ c.Assert(err, IsNil)
1000+ c.Assert(server.Id, Equals, s.testServerId)
1001+ c.Assert(server.Name, Equals, "test_server1")
1002+ c.Assert(server.Flavor.Id, Equals, "1")
1003+ c.Assert(server.Image.Id, Equals, "3fc0ef0b-82a9-4f44-a797-a43f0f73b20e")
1004+}
1005+
1006+func (s *ClientSuite) waitTestServerToStart(c *C) {
1007+ // Wait until the test server is actually running
1008+ c.Logf("waiting the test server %s to start...", s.testServerId)
1009+ for {
1010+ server, err := s.client.GetServer(s.testServerId)
1011+ c.Check(err, IsNil)
1012+ if server.Status == "ACTIVE" {
1013+ break
1014+ }
1015+ // There's a rate limit of max 10 POSTs per minute!
1016+ time.Sleep(10 * time.Second)
1017+ }
1018+ c.Logf("started")
1019+}
1020+
1021+func (s *ClientSuite) TestServerAddGetRemoveSecurityGroup(c *C) {
1022+ group, err := s.client.CreateSecurityGroup("test_server_secgroup", "test desc")
1023+ c.Assert(err, IsNil)
1024+
1025+ s.waitTestServerToStart(c)
1026+ err = s.client.AddServerSecurityGroup(s.testServerId, group.Name)
1027+ c.Check(err, IsNil)
1028+ groups, err := s.client.GetServerSecurityGroups(s.testServerId)
1029+ c.Check(err, IsNil)
1030+ found := false
1031+ for _, g := range groups {
1032+ if g.Id == group.Id || g.Name == group.Name {
1033+ found = true
1034+ break
1035+ }
1036+ }
1037+ err = s.client.RemoveServerSecurityGroup(s.testServerId, group.Name)
1038+ c.Check(err, IsNil)
1039+
1040+ err = s.client.DeleteSecurityGroup(group.Id)
1041+ c.Assert(err, IsNil)
1042+
1043+ if !found {
1044+ c.Fail()
1045+ }
1046+}
1047+
1048+func (s *ClientSuite) TestFloatingIPs(c *C) {
1049+ ip, err := s.client.AllocateFloatingIP()
1050+ c.Assert(err, IsNil)
1051+ c.Check(ip.IP, Not(Equals), "")
1052+ c.Check(ip.Pool, Not(Equals), "")
1053+ c.Check(ip.FixedIP, IsNil)
1054+ c.Check(ip.InstanceId, IsNil)
1055+
1056+ ips, err := s.client.ListFloatingIPs()
1057+ c.Check(err, IsNil)
1058+ if len(ips) < 1 {
1059+ c.Errorf("no floating IPs found (expected at least 1)")
1060+ } else {
1061+ found := false
1062+ for _, i := range ips {
1063+ c.Check(i.IP, Not(Equals), "")
1064+ c.Check(i.Pool, Not(Equals), "")
1065+ if i.Id == ip.Id {
1066+ c.Check(i.IP, Equals, ip.IP)
1067+ c.Check(i.Pool, Equals, ip.Pool)
1068+ found = true
1069+ }
1070+ }
1071+ if !found {
1072+ c.Errorf("expected to find added floating IP: %#v", ip)
1073+ }
1074+
1075+ fip, err := s.client.GetFloatingIP(ip.Id)
1076+ c.Check(err, IsNil)
1077+ c.Check(fip.Id, Equals, ip.Id)
1078+ c.Check(fip.IP, Equals, ip.IP)
1079+ c.Check(fip.Pool, Equals, ip.Pool)
1080+ }
1081+ err = s.client.DeleteFloatingIP(ip.Id)
1082+ c.Check(err, IsNil)
1083+}
1084+
1085+func (s *ClientSuite) TestServerFloatingIPs(c *C) {
1086+ ip, err := s.client.AllocateFloatingIP()
1087+ c.Assert(err, IsNil)
1088+ c.Check(ip.IP, Matches, `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
1089+
1090+ s.waitTestServerToStart(c)
1091+ err = s.client.AddServerFloatingIP(s.testServerId, ip.IP)
1092+ c.Check(err, IsNil)
1093+
1094+ fip, err := s.client.GetFloatingIP(ip.Id)
1095+ c.Check(err, IsNil)
1096+ c.Check(fip.FixedIP, Matches, `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
1097+ c.Check(fip.InstanceId, Equals, s.testServerId)
1098+
1099+ err = s.client.RemoveServerFloatingIP(s.testServerId, ip.IP)
1100+ c.Check(err, IsNil)
1101+ fip, err = s.client.GetFloatingIP(ip.Id)
1102+ c.Check(err, IsNil)
1103+ c.Check(fip.FixedIP, IsNil)
1104+ c.Check(fip.InstanceId, IsNil)
1105+
1106+ err = s.client.DeleteFloatingIP(ip.Id)
1107+ c.Check(err, IsNil)
1108+}
1109+
1110+func (s *ClientSuite) TestCreateAndDeleteContainer(c *C) {
1111+ container := "test_container"
1112+ err := s.client.CreateContainer(container)
1113+ c.Check(err, IsNil)
1114+ err = s.client.DeleteContainer(container)
1115+ c.Check(err, IsNil)
1116+}
1117+
1118+func (s *ClientSuite) TestObjects(c *C) {
1119+
1120+ container := "test_container"
1121+ object := "test_obj"
1122+ data := []byte("...some data...")
1123+ err := s.client.CreateContainer(container)
1124+ c.Check(err, IsNil)
1125+ err = s.client.PutObject(container, object, data)
1126+ c.Check(err, IsNil)
1127+ objdata, err := s.client.GetObject(container, object)
1128+ c.Check(err, IsNil)
1129+ c.Check(objdata, Equals, data)
1130+ err = s.client.DeleteObject(container, object)
1131+ c.Check(err, IsNil)
1132+ err = s.client.DeleteContainer(container)
1133+ c.Check(err, IsNil)
1134+}
1135
1136=== added directory 'errors'
1137=== added file 'errors/errors.go'
1138--- errors/errors.go 1970-01-01 00:00:00 +0000
1139+++ errors/errors.go 2012-11-26 11:24:05 +0000
1140@@ -0,0 +1,17 @@
1141+// Utility functions for reporting errors.
1142+
1143+package errors
1144+
1145+import (
1146+ "errors"
1147+ "fmt"
1148+)
1149+
1150+// AddErrorContext prefixes any error stored in err with text formatted
1151+// according to the format specifier. If err does not contain an error,
1152+// AddErrorContext does nothing.
1153+func AddErrorContext(err *error, format string, args ...interface{}) {
1154+ if *err != nil {
1155+ *err = errors.New(fmt.Sprintf(format, args...) + ": " + (*err).Error())
1156+ }
1157+}
1158
1159=== modified file 'goose.go'
1160--- goose.go 2012-10-30 08:18:14 +0000
1161+++ goose.go 2012-11-26 11:24:05 +0000
1162@@ -1,2 +1,1 @@
1163 package goose
1164-
1165
1166=== modified file 'goose_test.go'
1167--- goose_test.go 2012-10-30 08:18:14 +0000
1168+++ goose_test.go 2012-11-26 11:24:05 +0000
1169@@ -13,4 +13,3 @@
1170 }
1171
1172 var _ = Suite(&GooseTestSuite{})
1173-
1174
1175=== added directory 'http'
1176=== added file 'http/client.go'
1177--- http/client.go 1970-01-01 00:00:00 +0000
1178+++ http/client.go 2012-11-26 11:24:05 +0000
1179@@ -0,0 +1,185 @@
1180+// An HTTP Client which sends json and binary requests, handling data marshalling and response processing.
1181+
1182+package http
1183+
1184+import (
1185+ "bytes"
1186+ "encoding/json"
1187+ "errors"
1188+ "fmt"
1189+ "io/ioutil"
1190+ gooseerrors "launchpad.net/goose/errors"
1191+ "net/http"
1192+ "strings"
1193+)
1194+
1195+type GooseHTTPClient struct {
1196+ http.Client
1197+}
1198+
1199+type ErrorResponse struct {
1200+ Message string `json:"message"`
1201+ Code int `json:"code"`
1202+ Title string `json:"title"`
1203+}
1204+
1205+func (e *ErrorResponse) Error() string {
1206+ return fmt.Sprintf("Failed: %d %s: %s", e.Code, e.Title, e.Message)
1207+}
1208+
1209+type ErrorWrapper struct {
1210+ Error ErrorResponse `json:"error"`
1211+}
1212+
1213+type RequestData struct {
1214+ ReqHeaders http.Header
1215+ ExpectedStatus []int
1216+ ReqValue interface{}
1217+ RespValue interface{}
1218+ ReqData []byte
1219+ RespData *[]byte
1220+}
1221+
1222+// JsonRequest JSON encodes and sends the supplied object (if any) to the specified URL.
1223+// Optional method arguments are pass using the RequestData object.
1224+// Relevant RequestData fields:
1225+// ReqHeaders: additional HTTP header values to add to the request.
1226+// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
1227+// ReqValue: the data object to send.
1228+// RespValue: the data object to decode the result into.
1229+func (c *GooseHTTPClient) JsonRequest(method, url string, reqData *RequestData) (err error) {
1230+ err = nil
1231+ var (
1232+ req *http.Request
1233+ body []byte
1234+ )
1235+ if reqData.ReqValue != nil {
1236+ body, err = json.Marshal(reqData.ReqValue)
1237+ if err != nil {
1238+ gooseerrors.AddErrorContext(&err, "failed marshalling the request body")
1239+ return
1240+ }
1241+ reqBody := strings.NewReader(string(body))
1242+ req, err = http.NewRequest(method, url, reqBody)
1243+ } else {
1244+ req, err = http.NewRequest(method, url, nil)
1245+ }
1246+ if err != nil {
1247+ gooseerrors.AddErrorContext(&err, "failed creating the request")
1248+ return
1249+ }
1250+ req.Header.Add("Content-Type", "application/json")
1251+ req.Header.Add("Accept", "application/json")
1252+
1253+ respBody, err := c.sendRequest(req, reqData.ReqHeaders, reqData.ExpectedStatus, string(body))
1254+ if err != nil {
1255+ return
1256+ }
1257+
1258+ if len(respBody) > 0 {
1259+ if reqData.RespValue != nil {
1260+ err = json.Unmarshal(respBody, &reqData.RespValue)
1261+ if err != nil {
1262+ gooseerrors.AddErrorContext(&err, "failed unmarshaling the response body: %s", respBody)
1263+ }
1264+ }
1265+ }
1266+ return
1267+}
1268+
1269+// Sends the supplied byte array (if any) to the specified URL.
1270+// Optional method arguments are pass using the RequestData object.
1271+// Relevant RequestData fields:
1272+// ReqHeaders: additional HTTP header values to add to the request.
1273+// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
1274+// ReqData: the byte array to send.
1275+// RespData: the byte array to decode the result into.
1276+func (c *GooseHTTPClient) BinaryRequest(method, url string, reqData *RequestData) (err error) {
1277+ err = nil
1278+
1279+ var req *http.Request
1280+
1281+ if reqData.ReqData != nil {
1282+ rawReqReader := bytes.NewReader(reqData.ReqData)
1283+ req, err = http.NewRequest(method, url, rawReqReader)
1284+ } else {
1285+ req, err = http.NewRequest(method, url, nil)
1286+ }
1287+ if err != nil {
1288+ gooseerrors.AddErrorContext(&err, "failed creating the request")
1289+ return
1290+ }
1291+ req.Header.Add("Content-Type", "application/octet-stream")
1292+ req.Header.Add("Accept", "application/octet-stream")
1293+
1294+ respBody, err := c.sendRequest(req, reqData.ReqHeaders, reqData.ExpectedStatus, string(reqData.ReqData))
1295+ if err != nil {
1296+ return
1297+ }
1298+
1299+ if len(respBody) > 0 {
1300+ if reqData.RespData != nil {
1301+ *reqData.RespData = respBody
1302+ }
1303+ }
1304+ return
1305+}
1306+
1307+// Sends the specified request and checks that the HTTP response status is as expected.
1308+// req: the request to send.
1309+// extraHeaders: additional HTTP headers to include with the request.
1310+// expectedStatus: a slice of allowed response status codes.
1311+// payloadInfo: a string to include with an error message if something goes wrong.
1312+func (c *GooseHTTPClient) sendRequest(req *http.Request, extraHeaders http.Header, expectedStatus []int, payloadInfo string) (respBody []byte, err error) {
1313+ if extraHeaders != nil {
1314+ for header, values := range extraHeaders {
1315+ for _, value := range values {
1316+ req.Header.Add(header, value)
1317+ }
1318+ }
1319+ }
1320+
1321+ rawResp, err := c.Do(req)
1322+ if err != nil {
1323+ gooseerrors.AddErrorContext(&err, "failed executing the request")
1324+ return
1325+ }
1326+ foundStatus := false
1327+ if len(expectedStatus) == 0 {
1328+ expectedStatus = []int{http.StatusOK}
1329+ }
1330+ for _, status := range expectedStatus {
1331+ if rawResp.StatusCode == status {
1332+ foundStatus = true
1333+ break
1334+ }
1335+ }
1336+ if !foundStatus && len(expectedStatus) > 0 {
1337+ defer rawResp.Body.Close()
1338+ var errInfo interface{}
1339+ errInfo, _ = ioutil.ReadAll(rawResp.Body)
1340+ // Check if we have a JSON representation of the failure, if so decode it.
1341+ if rawResp.Header.Get("Content-Type") == "application/json" {
1342+ var wrappedErr ErrorWrapper
1343+ if err := json.Unmarshal(errInfo.([]byte), &wrappedErr); err == nil {
1344+ errInfo = wrappedErr.Error
1345+ }
1346+ }
1347+ err = errors.New(
1348+ fmt.Sprintf(
1349+ "request (%s) returned unexpected status: %s; error info: %v; request body: %s",
1350+ req.URL,
1351+ rawResp.Status,
1352+ errInfo,
1353+ payloadInfo))
1354+ return
1355+ }
1356+
1357+ respBody, err = ioutil.ReadAll(rawResp.Body)
1358+ rawResp.Body.Close()
1359+ if err != nil {
1360+ gooseerrors.AddErrorContext(&err, "failed reading the response body")
1361+ return
1362+ }
1363+ return
1364+}
1365
1366=== added directory 'identity'
1367=== added file 'identity/identity.go'
1368--- identity/identity.go 1970-01-01 00:00:00 +0000
1369+++ identity/identity.go 2012-11-26 11:24:05 +0000
1370@@ -0,0 +1,50 @@
1371+package identity
1372+
1373+import (
1374+ "os"
1375+)
1376+
1377+const (
1378+ AUTH_LEGACY = iota
1379+ AUTH_USERPASS
1380+)
1381+
1382+type AuthDetails struct {
1383+ TokenId string
1384+ TenantId string
1385+ UserId string
1386+ ServiceURLs map[string]string
1387+}
1388+
1389+type Credentials struct {
1390+ URL string // The URL to authenticate against
1391+ User string // The username to authenticate as
1392+ Secrets string // The secrets to pass
1393+ Region string // Region to send requests to
1394+ TenantName string // The tenant information for this connection
1395+}
1396+
1397+type Authenticator interface {
1398+ Auth(creds *Credentials) (*AuthDetails, error)
1399+}
1400+
1401+func getConfig(envVars ...string) (value string) {
1402+ value = ""
1403+ for _, v := range envVars {
1404+ value = os.Getenv(v)
1405+ if value != "" {
1406+ break
1407+ }
1408+ }
1409+ return
1410+}
1411+
1412+func CredentialsFromEnv() *Credentials {
1413+ return &Credentials{
1414+ URL: getConfig("OS_AUTH_URL"),
1415+ User: getConfig("OS_USERNAME", "NOVA_USERNAME"),
1416+ Secrets: getConfig("OS_PASSWORD", "NOVA_PASSWORD"),
1417+ Region: getConfig("OS_REGION_NAME", "NOVA_REGION"),
1418+ TenantName: getConfig("OS_TENANT_NAME", "NOVA_PROJECT_ID"),
1419+ }
1420+}
1421
1422=== added file 'identity/identity_test.go'
1423--- identity/identity_test.go 1970-01-01 00:00:00 +0000
1424+++ identity/identity_test.go 2012-11-26 11:24:05 +0000
1425@@ -0,0 +1,67 @@
1426+package identity
1427+
1428+import (
1429+ . "launchpad.net/gocheck"
1430+ "launchpad.net/goose/testing/envsuite"
1431+ "os"
1432+)
1433+
1434+type CredentialsTestSuite struct {
1435+ // Isolate all of these tests from the real Environ.
1436+ envsuite.EnvSuite
1437+}
1438+
1439+var _ = Suite(&CredentialsTestSuite{})
1440+
1441+func (s *CredentialsTestSuite) TestCredentialsFromEnv(c *C) {
1442+ var scenarios = []struct {
1443+ summary string
1444+ env map[string]string
1445+ username string
1446+ password string
1447+ tenant string
1448+ region string
1449+ authURL string
1450+ }{
1451+ {summary: "Old 'NOVA' style creds",
1452+ env: map[string]string{
1453+ "NOVA_USERNAME": "test-user",
1454+ "NOVA_PASSWORD": "test-pass",
1455+ // TODO: JAM 20121118 There exists a 'tenant
1456+ // name' and a 'tenant id'. Does
1457+ // NOVA_PROJECT_ID map to the 'tenant id' or to
1458+ // the tenant name? ~/.canonistack/novarc says
1459+ // tenant_name.
1460+ "NOVA_PROJECT_ID": "tenant-name",
1461+ "NOVA_REGION": "region",
1462+ },
1463+ username: "test-user",
1464+ password: "test-pass",
1465+ tenant: "tenant-name",
1466+ region: "region",
1467+ },
1468+ {summary: "New 'OS' style environment",
1469+ env: map[string]string{
1470+ "OS_USERNAME": "test-user",
1471+ "OS_PASSWORD": "test-pass",
1472+ "OS_TENANT_NAME": "tenant-name",
1473+ "OS_REGION_NAME": "region",
1474+ },
1475+ username: "test-user",
1476+ password: "test-pass",
1477+ tenant: "tenant-name",
1478+ region: "region",
1479+ },
1480+ }
1481+ for _, scenario := range scenarios {
1482+ for key, value := range scenario.env {
1483+ os.Setenv(key, value)
1484+ }
1485+ creds := CredentialsFromEnv()
1486+ c.Check(creds.URL, Equals, scenario.authURL)
1487+ c.Check(creds.User, Equals, scenario.username)
1488+ c.Check(creds.Secrets, Equals, scenario.password)
1489+ c.Check(creds.Region, Equals, scenario.region)
1490+ c.Check(creds.TenantName, Equals, scenario.tenant)
1491+ }
1492+}
1493
1494=== added file 'identity/legacy.go'
1495--- identity/legacy.go 1970-01-01 00:00:00 +0000
1496+++ identity/legacy.go 2012-11-26 11:24:05 +0000
1497@@ -0,0 +1,45 @@
1498+package identity
1499+
1500+import (
1501+ "fmt"
1502+ "io/ioutil"
1503+ "net/http"
1504+)
1505+
1506+type Legacy struct {
1507+ client *http.Client
1508+}
1509+
1510+func (l *Legacy) Auth(creds *Credentials) (*AuthDetails, error) {
1511+ if l.client == nil {
1512+ l.client = &http.Client{CheckRedirect: nil}
1513+ }
1514+ request, err := http.NewRequest("GET", creds.URL, nil)
1515+ if err != nil {
1516+ return nil, err
1517+ }
1518+ request.Header.Set("X-Auth-User", creds.User)
1519+ request.Header.Set("X-Auth-Key", creds.Secrets)
1520+ response, err := l.client.Do(request)
1521+ defer response.Body.Close()
1522+ if err != nil {
1523+ return nil, err
1524+ }
1525+ if response.StatusCode != http.StatusNoContent {
1526+ content, _ := ioutil.ReadAll(response.Body)
1527+ return nil, fmt.Errorf("Failed to Authenticate (code %d %s): %s",
1528+ response.StatusCode, response.Status, content)
1529+ }
1530+ details := &AuthDetails{}
1531+ details.TokenId = response.Header.Get("X-Auth-Token")
1532+ if details.TokenId == "" {
1533+ return nil, fmt.Errorf("Did not get valid Token from auth request")
1534+ }
1535+ nova_url := response.Header.Get("X-Server-Management-Url")
1536+ if nova_url == "" {
1537+ return nil, fmt.Errorf("Did not get valid management URL from auth request")
1538+ }
1539+ details.ServiceURLs = map[string]string{"compute": nova_url}
1540+
1541+ return details, nil
1542+}
1543
1544=== added file 'identity/legacy_test.go'
1545--- identity/legacy_test.go 1970-01-01 00:00:00 +0000
1546+++ identity/legacy_test.go 2012-11-26 11:24:05 +0000
1547@@ -0,0 +1,37 @@
1548+package identity
1549+
1550+import (
1551+ . "launchpad.net/gocheck"
1552+ "launchpad.net/goose/testing/httpsuite"
1553+ "launchpad.net/goose/testservices/identityservice"
1554+)
1555+
1556+type LegacyTestSuite struct {
1557+ httpsuite.HTTPSuite
1558+}
1559+
1560+var _ = Suite(&LegacyTestSuite{})
1561+
1562+func (s *LegacyTestSuite) TestAuthAgainstServer(c *C) {
1563+ service := identityservice.NewLegacy()
1564+ s.Mux.Handle("/", service)
1565+ token := service.AddUser("joe-user", "secrets")
1566+ service.SetManagementURL("http://management/url")
1567+ var l Authenticator = &Legacy{}
1568+ creds := Credentials{User: "joe-user", URL: s.Server.URL, Secrets: "secrets"}
1569+ auth, err := l.Auth(creds)
1570+ c.Assert(err, IsNil)
1571+ c.Assert(auth.Token, Equals, token)
1572+ c.Assert(auth.ServiceURLs, DeepEquals, map[string]string{"compute": "http://management/url"})
1573+}
1574+
1575+func (s *LegacyTestSuite) TestBadAuth(c *C) {
1576+ service := identityservice.NewLegacy()
1577+ s.Mux.Handle("/", service)
1578+ _ = service.AddUser("joe-user", "secrets")
1579+ var l Authenticator = &Legacy{}
1580+ creds := Credentials{User: "joe-user", URL: s.Server.URL, Secrets: "bad-secrets"}
1581+ auth, err := l.Auth(creds)
1582+ c.Assert(err, NotNil)
1583+ c.Assert(auth, IsNil)
1584+}
1585
1586=== added file 'identity/setup_test.go'
1587--- identity/setup_test.go 1970-01-01 00:00:00 +0000
1588+++ identity/setup_test.go 2012-11-26 11:24:05 +0000
1589@@ -0,0 +1,10 @@
1590+package identity
1591+
1592+import (
1593+ . "launchpad.net/gocheck"
1594+ "testing"
1595+)
1596+
1597+func Test(t *testing.T) {
1598+ TestingT(t)
1599+}
1600
1601=== added file 'identity/userpass.go'
1602--- identity/userpass.go 1970-01-01 00:00:00 +0000
1603+++ identity/userpass.go 2012-11-26 11:24:05 +0000
1604@@ -0,0 +1,111 @@
1605+package identity
1606+
1607+import (
1608+ "fmt"
1609+ goosehttp "launchpad.net/goose/http"
1610+ "net/http"
1611+)
1612+
1613+type PasswordCredentials struct {
1614+ Username string `json:"username"`
1615+ Password string `json:"password"`
1616+}
1617+
1618+type AuthRequest struct {
1619+ PasswordCredentials PasswordCredentials `json:"passwordCredentials"`
1620+ TenantName string `json:"tenantName"`
1621+}
1622+
1623+type AuthWrapper struct {
1624+ Auth AuthRequest `json:"auth"`
1625+}
1626+
1627+type Endpoint struct {
1628+ AdminURL string `json:"adminURL"`
1629+ InternalURL string `json:"internalURL"`
1630+ PublicURL string `json:"publicURL"`
1631+ Region string `json:"region"`
1632+}
1633+
1634+type ServiceResponse struct {
1635+ Name string `json:"name"`
1636+ Type string `json:"type"`
1637+ Endpoints []Endpoint
1638+}
1639+
1640+type TokenResponse struct {
1641+ Expires string `json:"expires"` // should this be a date object?
1642+ Id string `json:"id"` // Actual token string
1643+ Tenant struct {
1644+ Id string `json:"id"`
1645+ Name string `json:"name"`
1646+ Description string `json:"description"`
1647+ Enabled bool `json:"enabled"`
1648+ } `json:"tenant"`
1649+}
1650+
1651+type RoleResponse struct {
1652+ Id string `json:"id"`
1653+ Name string `json:"name"`
1654+ TenantId string `json:"tenantId"`
1655+}
1656+
1657+type UserResponse struct {
1658+ Id string `json:"id"`
1659+ Name string `json:"name"`
1660+ Roles []RoleResponse `json:"roles"`
1661+}
1662+
1663+type AccessWrapper struct {
1664+ Access AccessResponse `json:"access"`
1665+}
1666+
1667+type AccessResponse struct {
1668+ ServiceCatalog []ServiceResponse `json:"serviceCatalog"`
1669+ Token TokenResponse `json:"token"`
1670+ User UserResponse `json:"user"`
1671+}
1672+
1673+type UserPass struct {
1674+ client *goosehttp.GooseHTTPClient
1675+}
1676+
1677+func (u *UserPass) Auth(creds *Credentials) (*AuthDetails, error) {
1678+ if u.client == nil {
1679+ u.client = &goosehttp.GooseHTTPClient{http.Client{CheckRedirect: nil}}
1680+ }
1681+ auth := AuthWrapper{Auth: AuthRequest{
1682+ PasswordCredentials: PasswordCredentials{
1683+ Username: creds.User,
1684+ Password: creds.Secrets,
1685+ },
1686+ TenantName: creds.TenantName}}
1687+
1688+ var accessWrapper AccessWrapper
1689+ requestData := goosehttp.RequestData{ReqValue: auth, RespValue: &accessWrapper}
1690+ err := u.client.JsonRequest("POST", creds.URL, &requestData)
1691+ if err != nil {
1692+ return nil, err
1693+ }
1694+
1695+ details := &AuthDetails{}
1696+ access := accessWrapper.Access
1697+ respToken := access.Token
1698+ if respToken.Id == "" {
1699+ return nil, fmt.Errorf("Did not get valid Token from auth request")
1700+ }
1701+ details.TokenId = respToken.Id
1702+ details.TenantId = respToken.Tenant.Id
1703+ details.UserId = access.User.Id
1704+ details.ServiceURLs = make(map[string]string, len(access.ServiceCatalog))
1705+ for _, service := range access.ServiceCatalog {
1706+ for i, e := range service.Endpoints {
1707+ if e.Region != creds.Region {
1708+ service.Endpoints = append(service.Endpoints[:i], service.Endpoints[i+1:]...)
1709+ }
1710+ }
1711+ details.ServiceURLs[service.Type] = service.Endpoints[0].PublicURL
1712+ }
1713+
1714+ return details, nil
1715+}
1716
1717=== added file 'identity/userpass_test.go'
1718--- identity/userpass_test.go 1970-01-01 00:00:00 +0000
1719+++ identity/userpass_test.go 2012-11-26 11:24:05 +0000
1720@@ -0,0 +1,25 @@
1721+package identity
1722+
1723+import (
1724+ . "launchpad.net/gocheck"
1725+ "launchpad.net/goose/testing/httpsuite"
1726+ "launchpad.net/goose/testservices/identityservice"
1727+)
1728+
1729+type UserPassTestSuite struct {
1730+ httpsuite.HTTPSuite
1731+}
1732+
1733+var _ = Suite(&UserPassTestSuite{})
1734+
1735+func (s *UserPassTestSuite) TestAuthAgainstServer(c *C) {
1736+ service := identityservice.NewUserPass()
1737+ s.Mux.Handle("/", service)
1738+ token := service.AddUser("joe-user", "secrets")
1739+ var l Authenticator = &UserPass{}
1740+ creds := Credentials{User: "joe-user", URL: s.Server.URL, Secrets: "secrets"}
1741+ auth, err := l.Auth(creds)
1742+ c.Assert(err, IsNil)
1743+ c.Assert(auth.Token, Equals, token)
1744+ // c.Assert(auth.ServiceURLs, DeepEquals, map[string]string{"compute": "http://management/url"})
1745+}
1746
1747=== added directory 'testing'
1748=== added directory 'testing/envsuite'
1749=== added file 'testing/envsuite/envsuite.go'
1750--- testing/envsuite/envsuite.go 1970-01-01 00:00:00 +0000
1751+++ testing/envsuite/envsuite.go 2012-11-26 11:24:05 +0000
1752@@ -0,0 +1,33 @@
1753+package envsuite
1754+
1755+// Provides an EnvSuite type which makes sure this test suite gets an isolated
1756+// environment settings. Settings will be saved on start and then cleared, and
1757+// reset on tear down.
1758+
1759+import (
1760+ . "launchpad.net/gocheck"
1761+ "os"
1762+ "strings"
1763+)
1764+
1765+type EnvSuite struct {
1766+ environ []string
1767+}
1768+
1769+func (s *EnvSuite) SetUpSuite(c *C) {
1770+ s.environ = os.Environ()
1771+}
1772+
1773+func (s *EnvSuite) SetUpTest(c *C) {
1774+ os.Clearenv()
1775+}
1776+
1777+func (s *EnvSuite) TearDownTest(c *C) {
1778+ for _, envstring := range s.environ {
1779+ kv := strings.SplitN(envstring, "=", 2)
1780+ os.Setenv(kv[0], kv[1])
1781+ }
1782+}
1783+
1784+func (s *EnvSuite) TearDownSuite(c *C) {
1785+}
1786
1787=== added file 'testing/envsuite/envsuite_test.go'
1788--- testing/envsuite/envsuite_test.go 1970-01-01 00:00:00 +0000
1789+++ testing/envsuite/envsuite_test.go 2012-11-26 11:24:05 +0000
1790@@ -0,0 +1,48 @@
1791+package envsuite
1792+
1793+import (
1794+ . "launchpad.net/gocheck"
1795+ "os"
1796+ "testing"
1797+)
1798+
1799+type EnvTestSuite struct {
1800+ EnvSuite
1801+}
1802+
1803+func Test(t *testing.T) {
1804+ TestingT(t)
1805+}
1806+
1807+var _ = Suite(&EnvTestSuite{})
1808+
1809+func (s *EnvTestSuite) TestGrabsCurrentEnvironment(c *C) {
1810+ envsuite := &EnvSuite{}
1811+ // EnvTestSuite is an EnvSuite, so we should have already isolated
1812+ // ourselves from the world. So we set a single env value, and we
1813+ // assert that SetUpSuite is able to see that.
1814+ os.Setenv("TEST_KEY", "test-value")
1815+ envsuite.SetUpSuite(c)
1816+ c.Assert(envsuite.environ, DeepEquals, []string{"TEST_KEY=test-value"})
1817+}
1818+
1819+func (s *EnvTestSuite) TestClearsEnvironment(c *C) {
1820+ envsuite := &EnvSuite{}
1821+ os.Setenv("TEST_KEY", "test-value")
1822+ envsuite.SetUpSuite(c)
1823+ // SetUpTest should reset the current environment back to being
1824+ // completely empty.
1825+ envsuite.SetUpTest(c)
1826+ c.Assert(os.Getenv("TEST_KEY"), Equals, "")
1827+ c.Assert(os.Environ(), DeepEquals, []string{})
1828+}
1829+
1830+func (s *EnvTestSuite) TestRestoresEnvironment(c *C) {
1831+ envsuite := &EnvSuite{}
1832+ os.Setenv("TEST_KEY", "test-value")
1833+ envsuite.SetUpSuite(c)
1834+ envsuite.SetUpTest(c)
1835+ envsuite.TearDownTest(c)
1836+ c.Assert(os.Getenv("TEST_KEY"), Equals, "test-value")
1837+ c.Assert(os.Environ(), DeepEquals, []string{"TEST_KEY=test-value"})
1838+}
1839
1840=== added directory 'testing/httpsuite'
1841=== added file 'testing/httpsuite/httpsuite.go'
1842--- testing/httpsuite/httpsuite.go 1970-01-01 00:00:00 +0000
1843+++ testing/httpsuite/httpsuite.go 2012-11-26 11:24:05 +0000
1844@@ -0,0 +1,43 @@
1845+package httpsuite
1846+
1847+// This package provides an HTTPSuite infrastructure that lets you bring up an
1848+// HTTP server. The server will handle requests based on whatever Handlers are
1849+// attached to HTTPSuite.Mux. This Mux is reset after every test case, and the
1850+// server is shut down at the end of the test suite.
1851+
1852+import (
1853+ . "launchpad.net/gocheck"
1854+ "net/http"
1855+ "net/http/httptest"
1856+)
1857+
1858+var _ = Suite(&HTTPSuite{})
1859+
1860+type HTTPSuite struct {
1861+ Server *httptest.Server
1862+ Mux *http.ServeMux
1863+ oldHandler http.Handler
1864+}
1865+
1866+func (s *HTTPSuite) SetUpSuite(c *C) {
1867+ // fmt.Printf("Starting New Server\n")
1868+ s.Server = httptest.NewServer(nil)
1869+}
1870+
1871+func (s *HTTPSuite) SetUpTest(c *C) {
1872+ s.oldHandler = s.Server.Config.Handler
1873+ s.Mux = http.NewServeMux()
1874+ s.Server.Config.Handler = s.Mux
1875+}
1876+
1877+func (s *HTTPSuite) TearDownTest(c *C) {
1878+ s.Mux = nil
1879+ s.Server.Config.Handler = s.oldHandler
1880+}
1881+
1882+func (s *HTTPSuite) TearDownSuite(c *C) {
1883+ if s.Server != nil {
1884+ // fmt.Printf("Stopping Server\n")
1885+ s.Server.Close()
1886+ }
1887+}
1888
1889=== added file 'testing/httpsuite/httpsuite_test.go'
1890--- testing/httpsuite/httpsuite_test.go 1970-01-01 00:00:00 +0000
1891+++ testing/httpsuite/httpsuite_test.go 2012-11-26 11:24:05 +0000
1892@@ -0,0 +1,39 @@
1893+package httpsuite
1894+
1895+import (
1896+ "io/ioutil"
1897+ . "launchpad.net/gocheck"
1898+ "net/http"
1899+ "testing"
1900+)
1901+
1902+type HTTPTestSuite struct {
1903+ HTTPSuite
1904+}
1905+
1906+func Test(t *testing.T) {
1907+ TestingT(t)
1908+}
1909+
1910+var _ = Suite(&HTTPTestSuite{})
1911+
1912+type HelloHandler struct{}
1913+
1914+func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1915+ w.Header().Set("Content-Type", "text/plain")
1916+ w.WriteHeader(200)
1917+ w.Write([]byte("Hello World\n"))
1918+}
1919+
1920+func (s *HTTPTestSuite) TestHelloWorld(c *C) {
1921+ s.Mux.Handle("/", &HelloHandler{})
1922+ // fmt.Printf("Running HelloWorld\n")
1923+ response, err := http.Get(s.Server.URL)
1924+ c.Check(err, IsNil)
1925+ content, err := ioutil.ReadAll(response.Body)
1926+ response.Body.Close()
1927+ c.Check(err, IsNil)
1928+ c.Check(response.Status, Equals, "200 OK")
1929+ c.Check(response.StatusCode, Equals, 200)
1930+ c.Check(string(content), Equals, "Hello World\n")
1931+}
1932
1933=== added directory 'testservices'
1934=== added directory 'testservices/identityservice'
1935=== added file 'testservices/identityservice/identityservice.go'
1936--- testservices/identityservice/identityservice.go 1970-01-01 00:00:00 +0000
1937+++ testservices/identityservice/identityservice.go 2012-11-26 11:24:05 +0000
1938@@ -0,0 +1,10 @@
1939+package identityservice
1940+
1941+import (
1942+ "net/http"
1943+)
1944+
1945+type IdentityService interface {
1946+ AddUser(user, secret string) (token string)
1947+ ServeHTTP(w http.ResponseWriter, r *http.Request)
1948+}
1949
1950=== added file 'testservices/identityservice/legacy.go'
1951--- testservices/identityservice/legacy.go 1970-01-01 00:00:00 +0000
1952+++ testservices/identityservice/legacy.go 2012-11-26 11:24:05 +0000
1953@@ -0,0 +1,44 @@
1954+package identityservice
1955+
1956+import (
1957+ "net/http"
1958+)
1959+
1960+type Legacy struct {
1961+ tokens map[string]UserInfo
1962+ managementURL string
1963+}
1964+
1965+func NewLegacy() *Legacy {
1966+ service := &Legacy{}
1967+ service.tokens = make(map[string]UserInfo)
1968+ return service
1969+}
1970+
1971+func (lis *Legacy) SetManagementURL(URL string) {
1972+ lis.managementURL = URL
1973+}
1974+
1975+func (lis *Legacy) AddUser(user, secret string) string {
1976+ token := randomHexToken()
1977+ lis.tokens[user] = UserInfo{secret: secret, token: token}
1978+ return token
1979+}
1980+
1981+func (lis *Legacy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1982+ username := r.Header.Get("X-Auth-User")
1983+ info, ok := lis.tokens[username]
1984+ if !ok {
1985+ w.WriteHeader(http.StatusUnauthorized)
1986+ return
1987+ }
1988+ auth_key := r.Header.Get("X-Auth-Key")
1989+ if auth_key != info.secret {
1990+ w.WriteHeader(http.StatusUnauthorized)
1991+ return
1992+ }
1993+ header := w.Header()
1994+ header.Set("X-Auth-Token", info.token)
1995+ header.Set("X-Server-Management-Url", lis.managementURL)
1996+ w.WriteHeader(http.StatusNoContent)
1997+}
1998
1999=== added file 'testservices/identityservice/legacy_test.go'
2000--- testservices/identityservice/legacy_test.go 1970-01-01 00:00:00 +0000
2001+++ testservices/identityservice/legacy_test.go 2012-11-26 11:24:05 +0000
2002@@ -0,0 +1,96 @@
2003+package identityservice
2004+
2005+import (
2006+ "io/ioutil"
2007+ . "launchpad.net/gocheck"
2008+ "launchpad.net/goose/testing/httpsuite"
2009+ "net/http"
2010+)
2011+
2012+type LegacySuite struct {
2013+ httpsuite.HTTPSuite
2014+}
2015+
2016+var _ = Suite(&LegacySuite{})
2017+
2018+func (s *LegacySuite) setupLegacy(user, secret string) (token, managementURL string) {
2019+ managementURL = s.Server.URL
2020+ identity := NewLegacy()
2021+ // Ensure that it conforms to the interface
2022+ var _ IdentityService = identity
2023+ identity.SetManagementURL(managementURL)
2024+ s.Mux.Handle("/", identity)
2025+ if user != "" {
2026+ token = identity.AddUser(user, secret)
2027+ }
2028+ return
2029+}
2030+
2031+func LegacyAuthRequest(URL, user, key string) (*http.Response, error) {
2032+ client := &http.Client{}
2033+ request, err := http.NewRequest("GET", URL, nil)
2034+ if err != nil {
2035+ return nil, err
2036+ }
2037+ if user != "" {
2038+ request.Header.Set("X-Auth-User", user)
2039+ }
2040+ if key != "" {
2041+ request.Header.Set("X-Auth-Key", key)
2042+ }
2043+ return client.Do(request)
2044+}
2045+
2046+func AssertUnauthorized(c *C, response *http.Response) {
2047+ content, err := ioutil.ReadAll(response.Body)
2048+ c.Assert(err, IsNil)
2049+ response.Body.Close()
2050+ c.Check(response.Header.Get("X-Auth-Token"), Equals, "")
2051+ c.Check(response.Header.Get("X-Server-Management-Url"), Equals, "")
2052+ c.Check(string(content), Equals, "")
2053+ c.Check(response.StatusCode, Equals, http.StatusUnauthorized)
2054+}
2055+
2056+func (s *LegacySuite) TestLegacyFailedAuth(c *C) {
2057+ s.setupLegacy("", "")
2058+ // No headers set for Authentication
2059+ response, err := LegacyAuthRequest(s.Server.URL, "", "")
2060+ c.Assert(err, IsNil)
2061+ AssertUnauthorized(c, response)
2062+}
2063+
2064+func (s *LegacySuite) TestLegacyFailedOnlyUser(c *C) {
2065+ s.setupLegacy("", "")
2066+ // Missing secret key
2067+ response, err := LegacyAuthRequest(s.Server.URL, "user", "")
2068+ c.Assert(err, IsNil)
2069+ AssertUnauthorized(c, response)
2070+}
2071+
2072+func (s *LegacySuite) TestLegacyNoSuchUser(c *C) {
2073+ s.setupLegacy("user", "key")
2074+ // No user matching the username
2075+ response, err := LegacyAuthRequest(s.Server.URL, "notuser", "key")
2076+ c.Assert(err, IsNil)
2077+ AssertUnauthorized(c, response)
2078+}
2079+
2080+func (s *LegacySuite) TestLegacyInvalidAuth(c *C) {
2081+ s.setupLegacy("user", "secret-key")
2082+ // Wrong key
2083+ response, err := LegacyAuthRequest(s.Server.URL, "user", "bad-key")
2084+ c.Assert(err, IsNil)
2085+ AssertUnauthorized(c, response)
2086+}
2087+
2088+func (s *LegacySuite) TestLegacyAuth(c *C) {
2089+ token, serverURL := s.setupLegacy("user", "secret-key")
2090+ response, err := LegacyAuthRequest(s.Server.URL, "user", "secret-key")
2091+ c.Assert(err, IsNil)
2092+ content, err := ioutil.ReadAll(response.Body)
2093+ response.Body.Close()
2094+ c.Check(response.Header.Get("X-Auth-Token"), Equals, token)
2095+ c.Check(response.Header.Get("X-Server-Management-Url"), Equals, serverURL)
2096+ c.Check(string(content), Equals, "")
2097+ c.Check(response.StatusCode, Equals, http.StatusNoContent)
2098+}
2099
2100=== added file 'testservices/identityservice/service_test.go'
2101--- testservices/identityservice/service_test.go 1970-01-01 00:00:00 +0000
2102+++ testservices/identityservice/service_test.go 2012-11-26 11:24:05 +0000
2103@@ -0,0 +1,23 @@
2104+package identityservice
2105+
2106+import (
2107+ . "launchpad.net/gocheck"
2108+ "launchpad.net/goose/testing/httpsuite"
2109+)
2110+
2111+// All tests in the IdentityServiceSuite run against each IdentityService
2112+// implementation.
2113+
2114+type IdentityServiceSuite struct {
2115+ httpsuite.HTTPSuite
2116+ service IdentityService
2117+}
2118+
2119+var _ = Suite(&IdentityServiceSuite{service: NewUserPass()})
2120+var _ = Suite(&IdentityServiceSuite{service: NewLegacy()})
2121+
2122+func (s *IdentityServiceSuite) TestAddUserGivesNewToken(c *C) {
2123+ token1 := s.service.AddUser("user-1", "password-1")
2124+ token2 := s.service.AddUser("user-2", "password-2")
2125+ c.Assert(token1, Not(Equals), token2)
2126+}
2127
2128=== added file 'testservices/identityservice/setup_test.go'
2129--- testservices/identityservice/setup_test.go 1970-01-01 00:00:00 +0000
2130+++ testservices/identityservice/setup_test.go 2012-11-26 11:24:05 +0000
2131@@ -0,0 +1,10 @@
2132+package identityservice
2133+
2134+import (
2135+ . "launchpad.net/gocheck"
2136+ "testing"
2137+)
2138+
2139+func Test(t *testing.T) {
2140+ TestingT(t)
2141+}
2142
2143=== added file 'testservices/identityservice/userpass.go'
2144--- testservices/identityservice/userpass.go 1970-01-01 00:00:00 +0000
2145+++ testservices/identityservice/userpass.go 2012-11-26 11:24:05 +0000
2146@@ -0,0 +1,241 @@
2147+package identityservice
2148+
2149+import (
2150+ "encoding/json"
2151+ "fmt"
2152+ "io/ioutil"
2153+ "net/http"
2154+)
2155+
2156+// Implement the v2 User Pass form of identity (Keystone)
2157+
2158+type ErrorResponse struct {
2159+ Message string `json:"message"`
2160+ Code int `json:"code"`
2161+ Title string `json:"title"`
2162+}
2163+
2164+type ErrorWrapper struct {
2165+ Error ErrorResponse `json:"error"`
2166+}
2167+
2168+type UserPassRequest struct {
2169+ Auth struct {
2170+ PasswordCredentials struct {
2171+ Username string `json:"username"`
2172+ Password string `json:"password"`
2173+ } `json:"passwordCredentials"`
2174+ TenantName string `json:"tenantName"`
2175+ } `json:"auth"`
2176+}
2177+
2178+type Endpoint struct {
2179+ AdminURL string `json:"adminURL"`
2180+ InternalURL string `json:"internalURL"`
2181+ PublicURL string `json:"publicURL"`
2182+ Region string `json:"region"`
2183+}
2184+
2185+type Service struct {
2186+ Name string `json:"name"`
2187+ Type string `json:"type"`
2188+ Endpoints []Endpoint
2189+}
2190+
2191+type TokenResponse struct {
2192+ Expires string `json:"expires"` // should this be a date object?
2193+ Id string `json:"id"` // Actual token string
2194+ Tenant struct {
2195+ Id string `json:"id"`
2196+ Name string `json:"name"`
2197+ } `json:"tenant"`
2198+}
2199+
2200+type RoleResponse struct {
2201+ Id string `json:"id"`
2202+ Name string `json:"name"`
2203+ TenantId string `json:"tenantId"`
2204+}
2205+
2206+type UserResponse struct {
2207+ Id string `json:"id"`
2208+ Name string `json:"name"`
2209+ Roles []RoleResponse `json:"roles"`
2210+}
2211+
2212+type AccessResponse struct {
2213+ Access struct {
2214+ ServiceCatalog []Service `json:"serviceCatalog"`
2215+ Token TokenResponse `json:"token"`
2216+ User UserResponse `json:"user"`
2217+ } `json:"access"`
2218+}
2219+
2220+// Taken from: http://docs.openstack.org/api/quick-start/content/index.html#Getting-Credentials-a00665
2221+var exampleResponse = `{
2222+ "access": {
2223+ "serviceCatalog": [
2224+ {
2225+ "endpoints": [
2226+ {
2227+ "adminURL": "https://nova-api.trystack.org:9774/v1.1/1",
2228+ "internalURL": "https://nova-api.trystack.org:9774/v1.1/1",
2229+ "publicURL": "https://nova-api.trystack.org:9774/v1.1/1",
2230+ "region": "RegionOne"
2231+ }
2232+ ],
2233+ "name": "nova",
2234+ "type": "compute"
2235+ },
2236+ {
2237+ "endpoints": [
2238+ {
2239+ "adminURL": "https://GLANCE_API_IS_NOT_DISCLOSED/v1.1/1",
2240+ "internalURL": "https://GLANCE_API_IS_NOT_DISCLOSED/v1.1/1",
2241+ "publicURL": "https://GLANCE_API_IS_NOT_DISCLOSED/v1.1/1",
2242+ "region": "RegionOne"
2243+ }
2244+ ],
2245+ "name": "glance",
2246+ "type": "image"
2247+ },
2248+ {
2249+ "endpoints": [
2250+ {
2251+ "adminURL": "https://nova-api.trystack.org:5443/v2.0",
2252+ "internalURL": "https://keystone.trystack.org:5000/v2.0",
2253+ "publicURL": "https://keystone.trystack.org:5000/v2.0",
2254+ "region": "RegionOne"
2255+ }
2256+ ],
2257+ "name": "keystone",
2258+ "type": "identity"
2259+ }
2260+ ],
2261+ "token": {
2262+ "expires": "2012-02-15T19:32:21",
2263+ "id": "5df9d45d-d198-4222-9b4c-7a280aa35666",
2264+ "tenant": {
2265+ "id": "1",
2266+ "name": "admin"
2267+ }
2268+ },
2269+ "user": {
2270+ "id": "14",
2271+ "name": "annegentle",
2272+ "roles": [
2273+ {
2274+ "id": "2",
2275+ "name": "Member",
2276+ "tenantId": "1"
2277+ }
2278+ ]
2279+ }
2280+ }
2281+}`
2282+
2283+type UserPass struct {
2284+ users map[string]UserInfo
2285+ services []Service
2286+}
2287+
2288+func NewUserPass() *UserPass {
2289+ userpass := &UserPass{
2290+ users: make(map[string]UserInfo),
2291+ services: make([]Service, 0),
2292+ }
2293+ return userpass
2294+}
2295+
2296+func (u *UserPass) AddUser(user, secret string) string {
2297+ token := randomHexToken()
2298+ u.users[user] = UserInfo{secret: secret, token: token}
2299+ return token
2300+}
2301+
2302+func (u *UserPass) AddService(service Service) {
2303+ u.services = append(u.services, service)
2304+}
2305+
2306+var internalError = []byte(`{
2307+ "error": {
2308+ "message": "Internal failure",
2309+ "code": 500,
2310+ "title": Internal Server Error"
2311+ }
2312+}`)
2313+
2314+func (u *UserPass) ReturnFailure(w http.ResponseWriter, status int, message string) {
2315+ e := ErrorWrapper{
2316+ Error: ErrorResponse{
2317+ Message: message,
2318+ Code: status,
2319+ Title: http.StatusText(status),
2320+ },
2321+ }
2322+ if content, err := json.Marshal(e); err != nil {
2323+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(internalError)))
2324+ w.WriteHeader(http.StatusInternalServerError)
2325+ w.Write(internalError)
2326+ } else {
2327+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
2328+ w.WriteHeader(status)
2329+ w.Write(content)
2330+ }
2331+}
2332+
2333+// Taken from an actual responses, however it may vary based on actual Openstack implementation
2334+const (
2335+ notJSON = ("Expecting to find application/json in Content-Type header." +
2336+ " The server could not comply with the request since it is either malformed" +
2337+ " or otherwise incorrect. The client is assumed to be in error.")
2338+ notAuthorized = "The request you have made requires authentication."
2339+ invalidUser = "Invalid user / password"
2340+)
2341+
2342+func (u *UserPass) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2343+ var req UserPassRequest
2344+ // Testing against Canonistack, all responses are application/json, even failures
2345+ w.Header().Set("Content-Type", "application/json")
2346+ if r.Header.Get("Content-Type") != "application/json" {
2347+ u.ReturnFailure(w, http.StatusBadRequest, notJSON)
2348+ return
2349+ }
2350+ if content, err := ioutil.ReadAll(r.Body); err != nil {
2351+ w.WriteHeader(http.StatusBadRequest)
2352+ return
2353+ } else {
2354+ if err := json.Unmarshal(content, &req); err != nil {
2355+ u.ReturnFailure(w, http.StatusBadRequest, notJSON)
2356+ return
2357+ }
2358+ }
2359+ userInfo, ok := u.users[req.Auth.PasswordCredentials.Username]
2360+ if !ok {
2361+ u.ReturnFailure(w, http.StatusUnauthorized, notAuthorized)
2362+ return
2363+ }
2364+ if userInfo.secret != req.Auth.PasswordCredentials.Password {
2365+ u.ReturnFailure(w, http.StatusUnauthorized, invalidUser)
2366+ return
2367+ }
2368+ res := AccessResponse{}
2369+ // We pre-populate the response with genuine entries so that it looks sane.
2370+ // XXX: We should really build up valid state for this instead, at the
2371+ // very least, we should manage the URLs better.
2372+ if err := json.Unmarshal([]byte(exampleResponse), &res); err != nil {
2373+ u.ReturnFailure(w, http.StatusInternalServerError, err.Error())
2374+ return
2375+ }
2376+ res.Access.ServiceCatalog = u.services
2377+ res.Access.Token.Id = userInfo.token
2378+ if content, err := json.Marshal(res); err != nil {
2379+ u.ReturnFailure(w, http.StatusInternalServerError, err.Error())
2380+ return
2381+ } else {
2382+ w.WriteHeader(http.StatusOK)
2383+ w.Write(content)
2384+ return
2385+ }
2386+ panic("All paths should have already returned")
2387+}
2388
2389=== added file 'testservices/identityservice/userpass_test.go'
2390--- testservices/identityservice/userpass_test.go 1970-01-01 00:00:00 +0000
2391+++ testservices/identityservice/userpass_test.go 2012-11-26 11:24:05 +0000
2392@@ -0,0 +1,150 @@
2393+package identityservice
2394+
2395+import (
2396+ "encoding/json"
2397+ "fmt"
2398+ "io/ioutil"
2399+ . "launchpad.net/gocheck"
2400+ "launchpad.net/goose/testing/httpsuite"
2401+ "net/http"
2402+ "strings"
2403+)
2404+
2405+type UserPassSuite struct {
2406+ httpsuite.HTTPSuite
2407+}
2408+
2409+var _ = Suite(&UserPassSuite{})
2410+
2411+func makeUserPass(user, secret string) (identity *UserPass, token string) {
2412+ identity = NewUserPass()
2413+ // Ensure that it conforms to the interface
2414+ var _ IdentityService = identity
2415+ if user != "" {
2416+ token = identity.AddUser(user, secret)
2417+ }
2418+ return
2419+}
2420+
2421+func (s *UserPassSuite) setupUserPass(user, secret string) (token string) {
2422+ var identity *UserPass
2423+ identity, token = makeUserPass(user, secret)
2424+ s.Mux.Handle("/", identity)
2425+ return
2426+}
2427+
2428+func (s *UserPassSuite) setupUserPassWithServices(user, secret string, services []Service) (token string) {
2429+ var identity *UserPass
2430+ identity, token = makeUserPass(user, secret)
2431+ for _, service := range services {
2432+ identity.AddService(service)
2433+ }
2434+ s.Mux.Handle("/", identity)
2435+ return
2436+}
2437+
2438+var authTemplate = `{
2439+ "auth": {
2440+ "tenantName": "tenant-something",
2441+ "passwordCredentials": {
2442+ "username": "%s",
2443+ "password": "%s"
2444+ }
2445+ }
2446+}`
2447+
2448+func userPassAuthRequest(URL, user, key string) (*http.Response, error) {
2449+ client := &http.Client{}
2450+ body := strings.NewReader(fmt.Sprintf(authTemplate, user, key))
2451+ request, err := http.NewRequest("POST", URL, body)
2452+ request.Header.Set("Content-Type", "application/json")
2453+ if err != nil {
2454+ return nil, err
2455+ }
2456+ return client.Do(request)
2457+}
2458+
2459+func CheckErrorResponse(c *C, r *http.Response, status int, msg string) {
2460+ c.Check(r.StatusCode, Equals, status)
2461+ c.Assert(r.Header.Get("Content-Type"), Equals, "application/json")
2462+ body, err := ioutil.ReadAll(r.Body)
2463+ c.Assert(err, IsNil)
2464+ var errmsg ErrorWrapper
2465+ err = json.Unmarshal(body, &errmsg)
2466+ c.Assert(err, IsNil)
2467+ c.Check(errmsg.Error.Code, Equals, status)
2468+ c.Check(errmsg.Error.Title, Equals, http.StatusText(status))
2469+ if msg != "" {
2470+ c.Check(errmsg.Error.Message, Equals, msg)
2471+ }
2472+}
2473+
2474+func (s *UserPassSuite) TestNotJSON(c *C) {
2475+ // We do everything in userPassAuthRequest, except set the Content-Type
2476+ token := s.setupUserPass("user", "secret")
2477+ c.Assert(token, NotNil)
2478+ client := &http.Client{}
2479+ body := strings.NewReader(fmt.Sprintf(authTemplate, "user", "secret"))
2480+ request, err := http.NewRequest("POST", s.Server.URL, body)
2481+ c.Assert(err, IsNil)
2482+ res, err := client.Do(request)
2483+ defer res.Body.Close()
2484+ c.Assert(err, IsNil)
2485+ CheckErrorResponse(c, res, http.StatusBadRequest, notJSON)
2486+}
2487+
2488+func (s *UserPassSuite) TestBadJSON(c *C) {
2489+ // We do everything in userPassAuthRequest, except set the Content-Type
2490+ token := s.setupUserPass("user", "secret")
2491+ c.Assert(token, NotNil)
2492+ res, err := userPassAuthRequest(s.Server.URL, "garbage\"in", "secret")
2493+ defer res.Body.Close()
2494+ c.Assert(err, IsNil)
2495+ CheckErrorResponse(c, res, http.StatusBadRequest, notJSON)
2496+}
2497+
2498+func (s *UserPassSuite) TestNoSuchUser(c *C) {
2499+ token := s.setupUserPass("user", "secret")
2500+ c.Assert(token, NotNil)
2501+ res, err := userPassAuthRequest(s.Server.URL, "not-user", "secret")
2502+ defer res.Body.Close()
2503+ c.Assert(err, IsNil)
2504+ CheckErrorResponse(c, res, http.StatusUnauthorized, notAuthorized)
2505+}
2506+
2507+func (s *UserPassSuite) TestBadPassword(c *C) {
2508+ token := s.setupUserPass("user", "secret")
2509+ c.Assert(token, NotNil)
2510+ res, err := userPassAuthRequest(s.Server.URL, "user", "not-secret")
2511+ defer res.Body.Close()
2512+ c.Assert(err, IsNil)
2513+ CheckErrorResponse(c, res, http.StatusUnauthorized, invalidUser)
2514+}
2515+
2516+func (s *UserPassSuite) TestValidAuthorization(c *C) {
2517+ compute_url := "http://testing.invalid/compute"
2518+ token := s.setupUserPassWithServices("user", "secret", []Service{
2519+ {"nova", "compute", []Endpoint{
2520+ {PublicURL: compute_url},
2521+ }}})
2522+ c.Assert(token, NotNil)
2523+ res, err := userPassAuthRequest(s.Server.URL, "user", "secret")
2524+ defer res.Body.Close()
2525+ c.Assert(err, IsNil)
2526+ c.Check(res.StatusCode, Equals, http.StatusOK)
2527+ c.Check(res.Header.Get("Content-Type"), Equals, "application/json")
2528+ content, err := ioutil.ReadAll(res.Body)
2529+ c.Assert(err, IsNil)
2530+ var response AccessResponse
2531+ err = json.Unmarshal(content, &response)
2532+ c.Assert(err, IsNil)
2533+ c.Check(response.Access.Token.Id, Equals, token)
2534+ novaURL := ""
2535+ for _, service := range response.Access.ServiceCatalog {
2536+ if service.Type == "compute" {
2537+ novaURL = service.Endpoints[0].PublicURL
2538+ break
2539+ }
2540+ }
2541+ c.Assert(novaURL, Equals, compute_url)
2542+}
2543
2544=== added file 'testservices/identityservice/util.go'
2545--- testservices/identityservice/util.go 1970-01-01 00:00:00 +0000
2546+++ testservices/identityservice/util.go 2012-11-26 11:24:05 +0000
2547@@ -0,0 +1,31 @@
2548+package identityservice
2549+
2550+import (
2551+ "crypto/rand"
2552+ "encoding/hex"
2553+ "fmt"
2554+)
2555+
2556+type UserInfo struct {
2557+ secret string
2558+ token string
2559+}
2560+
2561+// Generate a bit of random hex data for
2562+func randomHexToken() string {
2563+ raw_bytes := make([]byte, 16)
2564+ n, err := rand.Read(raw_bytes)
2565+ if n != 16 || err != nil {
2566+ panic(fmt.Sprintf(
2567+ "Could not read 16 random bytes safely: %d %s",
2568+ n, err.Error()))
2569+ }
2570+ hex_bytes := make([]byte, 32)
2571+ n = hex.Encode(hex_bytes, raw_bytes)
2572+ if n != 32 || err != nil {
2573+ panic(fmt.Sprintf(
2574+ "Failed to Encode 32 bytes: %d %s",
2575+ n, err.Error()))
2576+ }
2577+ return string(hex_bytes)
2578+}
2579
2580=== added file 'testservices/identityservice/util_test.go'
2581--- testservices/identityservice/util_test.go 1970-01-01 00:00:00 +0000
2582+++ testservices/identityservice/util_test.go 2012-11-26 11:24:05 +0000
2583@@ -0,0 +1,28 @@
2584+package identityservice
2585+
2586+import (
2587+ . "launchpad.net/gocheck"
2588+)
2589+
2590+type UtilSuite struct{}
2591+
2592+var _ = Suite(&UtilSuite{})
2593+
2594+func (s *UtilSuite) TestRandomHexTokenHasLength(c *C) {
2595+ val := randomHexToken()
2596+ c.Assert(val, HasLen, 32)
2597+}
2598+
2599+func (s *UtilSuite) TestRandomHexTokenIsHex(c *C) {
2600+ val := randomHexToken()
2601+ for i, b := range val {
2602+ switch {
2603+ case (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9'):
2604+ continue
2605+ default:
2606+ c.Logf("char %d of %s was not in the right range",
2607+ i, val)
2608+ c.Fail()
2609+ }
2610+ }
2611+}
2612
2613=== added file 'testservices/main.go'
2614--- testservices/main.go 1970-01-01 00:00:00 +0000
2615+++ testservices/main.go 2012-11-26 11:24:05 +0000
2616@@ -0,0 +1,69 @@
2617+package main
2618+
2619+import (
2620+ "fmt"
2621+ "launchpad.net/gnuflag"
2622+ "launchpad.net/goose/testservices/identityservice"
2623+ "log"
2624+ "net/http"
2625+ "strings"
2626+)
2627+
2628+type userInfo struct {
2629+ user, secret string
2630+}
2631+type userValues struct {
2632+ users []userInfo
2633+}
2634+
2635+func (uv *userValues) Set(s string) error {
2636+ vals := strings.Split(s, ":")
2637+ if len(vals) != 2 {
2638+ return fmt.Errorf("Invalid --user option, should be: user:secret")
2639+ }
2640+ uv.users = append(uv.users, userInfo{
2641+ user: vals[0],
2642+ secret: vals[1],
2643+ })
2644+ return nil
2645+}
2646+func (uv *userValues) String() string {
2647+ return fmt.Sprintf("%v", uv.users)
2648+}
2649+
2650+var provider = gnuflag.String("provider", "userpass", "provide the name of the identity service to run")
2651+
2652+var serveAddr = gnuflag.String("addr", "localhost:8080", "serve the provider on the given address.")
2653+
2654+var users userValues
2655+
2656+func init() {
2657+
2658+ gnuflag.Var(&users, "user", "supply to add a user to the identity provider. Can be supplied multiple times. Should be of the form \"user:secret:token\".")
2659+}
2660+
2661+var providerMap = map[string]identityservice.IdentityService{
2662+ "legacy": identityservice.NewLegacy(),
2663+ "userpass": identityservice.NewUserPass(),
2664+}
2665+
2666+func providers() []string {
2667+ out := make([]string, 0, len(providerMap))
2668+ for provider := range providerMap {
2669+ out = append(out, provider)
2670+ }
2671+ return out
2672+}
2673+
2674+func main() {
2675+ gnuflag.Parse(true)
2676+ p, ok := providerMap[*provider]
2677+ if !ok {
2678+ log.Fatalf("No such provider: %s, pick one of: %v", provider, providers())
2679+ }
2680+ http.Handle("/", p)
2681+ for _, u := range users.users {
2682+ p.AddUser(u.user, u.secret)
2683+ }
2684+ log.Fatal(http.ListenAndServe(*serveAddr, nil))
2685+}

Subscribers

People subscribed via source and target branches

to all changes: