Merge lp:~rogpeppe/goose/state-of-the-world into lp:~rogpeppe/goose/dummy-trunk
- state-of-the-world
- Merge into dummy-trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Roger Peppe | Pending | ||
Review via email: mp+136144@code.launchpad.net |
Commit message
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.
Roger Peppe (rogpeppe) wrote : | # |
Reviewers: mp+136144_
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:/
File client/client.go (right):
https:/
client/
CAPITAL_
like these aren't intended to be part of the public API.
how about:
const (
apiTokens = "/tokens"
apiFlavors = ... etc
)
https:/
client/
unexport. and... can we just use the string constants? i don't see that
defining constants here adds greatly to safety.
https:/
client/
s/OpenStackClie
after all goose is all about openstack - i doubt we need to restate
that.
also, needs a doc comment.
https:/
client/
goosehttp.Client would be a better name.
https:/
client/
doc comment (what's the key and value in the map?)
https:/
client/
*identity.
doc comment
also, s/auth_
https:/
client/
given that the identify package defines these constants and implements
the authentication schemes, how about
client.auth, err = identity.
let the identity package implement this logic.
https:/
client/
error) {
doc comment.
if you don't use AddErrorContext, you won't need to name the variable.
https:/
client/
d
https:/
client/
been specified")
s/fmt.Errorf/
https:/
client/
failed")
return fmt.Errorf(
https:/
client/
might it make sense to simply add the auth details as a field in the
OpenStackClient type?
https:/
client/
doc comment.
https:/
John A Meinel (jameinel) wrote : | # |
https:/
File identity/
https:/
identity/
On 2012/11/26 12:51:38, rog wrote:
> authLegacy ?
This one is actually part of the public api. Though AuthLegacy would be
ok.
Roger Peppe (rogpeppe) wrote : | # |
https:/
File identity/
https:/
identity/
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.
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://
iEYEARECAAYFAlC
7Q4AoLlK1FjFBaL
=COy9
-----END PGP SIGNATURE-----
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
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 | +} |
Just adding a comment so that I get subscribed to any discussions on
this MP.
https:/ /codereview. appspot. com/6844087/