Merge lp:~gz/gomaasapi/gomaasapi into lp:~gophers/gomaasapi/initial_for_review

Proposed by Martin Packman
Status: Merged
Merge reported by: Martin Packman
Merged at revision: not available
Proposed branch: lp:~gz/gomaasapi/gomaasapi
Merge into: lp:~gophers/gomaasapi/initial_for_review
Diff against target: 1964 lines (+1859/-0)
20 files modified
README (+12/-0)
client.go (+138/-0)
client_test.go (+142/-0)
example/live_example.go (+106/-0)
gomaasapi.go (+3/-0)
gomaasapi_test.go (+3/-0)
jsonobject.go (+197/-0)
jsonobject_test.go (+228/-0)
maas.go (+10/-0)
maas_test.go (+22/-0)
maasobject.go (+167/-0)
maasobject_test.go (+133/-0)
oauth.go (+81/-0)
templates/source.go (+4/-0)
templates/source_test.go (+13/-0)
testing.go (+46/-0)
testservice.go (+240/-0)
testservice_test.go (+255/-0)
util.go (+27/-0)
util_test.go (+32/-0)
To merge this branch: bzr merge lp:~gz/gomaasapi/gomaasapi
Reviewer Review Type Date Requested Status
The Go Language Gophers Pending
Review via email: mp+145811@code.launchpad.net

Description of the change

Rollup of all relevent changes on lp:gomaasapi

This is for review only, showing the current trunk of the project in
the diff (from the initial commit of the licence and a couple of stub
go files). Note it's not possible for the Red squad to update this
proposal directly, so don't expect code changes in response to comments
to appear here, please watch for other merge proposals. :)

https://codereview.appspot.com/7228069/

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Reviewers: mp+145811_code.launchpad.net,

Message:
Please take a look.

Description:
Rollup of all relevent changes on lp:gomaasapi

This is for review only, showing the current trunk of the project in
the diff (from the initial commit of the licence and a couple of stub
go files). Note it's not possible for the Red squad to update this
proposal directly, so don't expect code changes in response to comments
to appear here, please watch for other merge proposals. :)

https://code.launchpad.net/~gz/gomaasapi/gomaasapi/+merge/145811

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7228069/

Affected files:
   A README
   A [revision details]
   A client.go
   A client_test.go
   A example/live_example.go
   M gomaasapi.go
   M gomaasapi_test.go
   A jsonobject.go
   A jsonobject_test.go
   A maas.go
   A maas_test.go
   A maasobject.go
   A maasobject_test.go
   A oauth.go
   A templates/source.go
   A templates/source_test.go
   A testing.go
   A util.go
   A util_test.go

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

seems like a good start.

lots of minor comments, but one or two more significant queries. in
particular, it looks like it's only possible to use the maas package if
you know exactly how maas works. i'd like to see all the gruesome http
details hidden behind an API that actually reflects the operations and
entities that maas provides.

i'm +1 on TheMue's comment on JSONObject. it seems like it's a lot of
code that doesn't solve any real problem.

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

https://codereview.appspot.com/7228069/diff/1/client.go#newcode15
client.go:15: type Client struct {
On 2013/02/01 10:43:41, TheMue wrote:
> Please add godoc documentation on all public types and functions.

+1
also please unexport any methods that aren't essential to using the
package.

https://codereview.appspot.com/7228069/diff/1/client.go#newcode21
client.go:21: operationParamName = "op"
this seems a very long name for a very short string :-)

https://codereview.appspot.com/7228069/diff/1/client.go#newcode24
client.go:24: func (client Client) dispatchRequest(request
*http.Request) ([]byte, error) {
On 2013/02/01 10:43:41, TheMue wrote:
> Here (c Client) is more usual.

further to that, i'd suggest that on type with side effects, it's
conventional to use pointer types, so you'd use *Client throughout.

https://codereview.appspot.com/7228069/diff/1/client.go#newcode26
client.go:26: httpClient := http.Client{}
unless there's a specific reason not to, it would be more conventional
to use http.DefaultClient here rather than creating a new instance of
http.Client. that way connections can be reused.

https://codereview.appspot.com/7228069/diff/1/client.go#newcode36
client.go:36: return body, errors.New("Error requesting the MAAS server:
" + response.Status + ".")
On 2013/02/01 10:43:41, TheMue wrote:
> return nil, fmt.Errorf("gomaasapi: cannot dispatch request: %v",
> response.Status)

> In case of an error no value is returned.

+1

but we wouldn't usually include the package name in the error message
although the Go core would. either is acceptable.

https://codereview.appspot.com/7228069/diff/1/client.go#newcode54
client.go:54: queryUrl := client.GetURL(URI)
the distinction between "URI" and "URL" is unclear to me here. perhaps
some documentation might make things more clear?

also, we tend to use lower case names for variables, so "uri" rather
than "URI".

https://codereview.appspot.com/7228069/diff/1/client.go#newcode66
client.go:66: request, err := http.NewRequest(method, URL.String(),
strings.NewReader(string(parameters.Encode())))
the string conversion on the result of Encode is unnecessary here.

https://codereview.appspot.com/7228069/diff/1/client.go#newcode90
client.go:90: _, err2 := client.dispatchRequest(request)
s/err2/err/
no real need to make another variable.

https://codereview.appspot.com/7228069/diff/1/client.go#newcode104
client.go:104: var _ OAuthSigner = (*anonSigner)(nil)
given that you're using a by-value method above, this may as well be:

var _ OAuthSigner = anonSigner{}

https://codereview.appspot.com/7228069/diff/1/client.go#newcode121
client.go:121: errString := "Invalid API key. The format ...

lp:~gz/gomaasapi/gomaasapi updated
13. By Raphaël Badin

[r=jtv][bug=][author=rvb] Add testservice that can be used to write tests for libraries using gomaasapi.

Revision history for this message
William Reade (fwereade) wrote :

+1s to just about everything rog and frank said, and a few comments on
JSONObject:

https://codereview.appspot.com/7228069/diff/1/jsonobject.go
File jsonobject.go (right):

https://codereview.appspot.com/7228069/diff/1/jsonobject.go#newcode32
jsonobject.go:32: GetString() (string, error)
On 2013/02/01 17:30:05, rog wrote:
> i don't really see what these methods have to offer over:

> s, ok := someValue.(string)

The errors are quite helpful, I think. But I don't think JSONObject
needs all these Get methods when we could have a bunch of GetFoo(obj
jsonObject) functions -- I think it's be a lot cleaner. See below.

https://codereview.appspot.com/7228069/diff/1/jsonobject.go#newcode65
jsonobject.go:65: // Internal: turn a completely untyped json.Unmarshal
result into a
It's also conventional to start doc comments with the name of the thing
being commented on; it makes it neater to use the comment as a sentence
in other contexts. I know it's academic in this case, but worth bearing
in mind in general.

https://codereview.appspot.com/7228069/diff/1/jsonobject.go#newcode119
jsonobject.go:119: return errors.New(msg)
On 2013/02/01 17:30:05, rog wrote:
> fmt.Errorf("cannot convert %#v to %s", obj, wanted_type)
+1

https://codereview.appspot.com/7228069/diff/1/jsonobject.go#newcode159
jsonobject.go:159: func (obj jsonString) GetBool() (bool, error)
         { return failBool(obj) }
This bit is awfully dense and repetitive. I think a few standalone Get*
funcs would work a bit better here, as mentioned above:

func (jsonString) Type() string { return "string" }
func (jsonFloat64) Type() string { return "float64" }
...

func GetString(obj JSONObject) (string, error) {
     if obj.Type() == "string" {
         return string(obj), nil
     }
     return "", failConversion("string", obj)
}

func GetFloat64(obj JSONObject) (float64, error) {
     if obj.Type() == "float64" {
         return float64(obj), nil
     }
     return "", failConversion("float64", obj)
}

...

https://codereview.appspot.com/7228069/diff/1/maasobject.go
File maasobject.go (right):

https://codereview.appspot.com/7228069/diff/1/maasobject.go#newcode57
maasobject.go:57: panic("Cannot create jsonMAASObject object, no
'resource_uri' key present in the given jsonMap.")
Conventional to panic with errors

https://codereview.appspot.com/7228069/

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

https://codereview.appspot.com/7228069/diff/1/jsonobject.go
File jsonobject.go (right):

https://codereview.appspot.com/7228069/diff/1/jsonobject.go#newcode159
jsonobject.go:159: func (obj jsonString) GetBool() (bool, error)
         { return failBool(obj) }
On 2013/02/05 12:26:31, fwereade wrote:
> This bit is awfully dense and repetitive. I think a few standalone
Get* funcs
> would work a bit better here, as mentioned above:

> func (jsonString) Type() string { return "string" }
> func (jsonFloat64) Type() string { return "float64" }
> ...

> func GetString(obj JSONObject) (string, error) {
> if obj.Type() == "string" {
> return string(obj), nil
> }
> return "", failConversion("string", obj)
> }

> func GetFloat64(obj JSONObject) (float64, error) {
> if obj.Type() == "float64" {
> return float64(obj), nil
> }
> return "", failConversion("float64", obj)
> }

> ...

if that's the case, why bother with the JSONObject type at all?
why not:

func GetString(obj interface{}) (string, error) {
     if s, ok := obj.(string); ok {
         return s, nil
     }
     return "", failConversion("string", obj)
}

or even a single function:

// Get sets the value of into (which must be a pointer
// to one of float64, string, map[string]interface{},
// bool or MaasObject) to the value of jsonObj.
func Get(jsonObj interface{}, into interface{}) error {
     var ok bool
     switch into := into.(type) {
     case *string:
         *into, ok = jsonObj.(string)
     case *float64:
         *into, ok = jsonObj.(float64)
     etc
     }
     if !ok {
         return failConversion(reflect.TypeOf(into).Elem().String(),
jsonObj)
     }
     return nil
}

but all this seems like it's trying to do something
similar to launchpad.net/juju-core/schema without
the nice generality that that package provides.

https://codereview.appspot.com/7228069/

lp:~gz/gomaasapi/gomaasapi updated
14. By Jeroen T. Vermeulen

[r=rvb][bug=][author=jtv] Address a large number of smaller review points.

Revision history for this message
Raphaël Badin (rvb) wrote :
lp:~gz/gomaasapi/gomaasapi updated
15. By Raphaël Badin

[r=jtv][bug=][author=rvb] Address a large number of small review points.

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

it would be fantastic if these changes could be proposed
with lbox -cr. if that's not feasible, i'll review in the old fashioned
way, but i'd prefer to be able to comment inline.

On 6 February 2013 04:01, <email address hidden> wrote:
> And another:
> https://code.launchpad.net/~jtv/gomaasapi/review-changes-3/+merge/146768
>
> https://codereview.appspot.com/7228069/

lp:~gz/gomaasapi/gomaasapi updated
16. By Jeroen T. Vermeulen

[r=rvb][bug=][author=jtv] More of the simple review changes from https://codereview.appspot.com/7228069/

Revision history for this message
Martin Packman (gz) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'README'
2--- README 1970-01-01 00:00:00 +0000
3+++ README 2013-02-06 17:03:19 +0000
4@@ -0,0 +1,12 @@
5+.. -*- mode: rst -*-
6+
7+******************************
8+MAAS API client library for Go
9+******************************
10+
11+This library serves as a minimal client for communicating with the MAAS web
12+API in Go programs.
13+
14+For more information see the `project homepage`_.
15+
16+.. _project homepage: https://launchpad.net/gomaasapi
17
18=== added file 'client.go'
19--- client.go 1970-01-01 00:00:00 +0000
20+++ client.go 2013-02-06 17:03:19 +0000
21@@ -0,0 +1,138 @@
22+// Copyright 2013 Canonical Ltd. This software is licensed under the
23+// GNU Lesser General Public License version 3 (see the file COPYING).
24+
25+package gomaasapi
26+
27+import (
28+ "errors"
29+ "fmt"
30+ "io/ioutil"
31+ "net/http"
32+ "net/url"
33+ "strings"
34+)
35+
36+// Client represents a way ot communicating with a MAAS API instance.
37+// It is stateless, so it can have concurrent requests in progress.
38+type Client struct {
39+ BaseURL *url.URL
40+ Signer OAuthSigner
41+}
42+
43+func (client Client) dispatchRequest(request *http.Request) ([]byte, error) {
44+ client.Signer.OAuthSign(request)
45+ httpClient := http.Client{}
46+ response, err := httpClient.Do(request)
47+ if err != nil {
48+ return nil, err
49+ }
50+ body, err := ioutil.ReadAll(response.Body)
51+ if err != nil {
52+ return nil, err
53+ }
54+ if response.StatusCode < 200 || response.StatusCode > 299 {
55+ return body, fmt.Errorf("gomaasapi: got error back from server: %v", response.Status)
56+ }
57+ return body, nil
58+}
59+
60+func (client Client) GetURL(uri *url.URL) *url.URL {
61+ return client.BaseURL.ResolveReference(uri)
62+}
63+
64+func (client Client) Get(uri *url.URL, operation string, parameters url.Values) ([]byte, error) {
65+ opParameter := parameters.Get("op")
66+ if opParameter != "" {
67+ errString := fmt.Sprintf("The parameters contain a value for '%s' which is reserved parameter.")
68+ return nil, errors.New(errString)
69+ }
70+ if operation != "" {
71+ parameters.Set("op", operation)
72+ }
73+ queryUrl := client.GetURL(uri)
74+ queryUrl.RawQuery = parameters.Encode()
75+ request, err := http.NewRequest("GET", queryUrl.String(), nil)
76+ if err != nil {
77+ return nil, err
78+ }
79+ return client.dispatchRequest(request)
80+}
81+
82+// nonIdempotentRequest is a utility method to issue a PUT or a POST request.
83+func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values) ([]byte, error) {
84+ url := client.GetURL(uri)
85+ request, err := http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode())))
86+ if err != nil {
87+ return nil, err
88+ }
89+ request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
90+ return client.dispatchRequest(request)
91+}
92+
93+func (client Client) Post(uri *url.URL, operation string, parameters url.Values) ([]byte, error) {
94+ queryParams := url.Values{"op": {operation}}
95+ uri.RawQuery = queryParams.Encode()
96+ return client.nonIdempotentRequest("POST", uri, parameters)
97+}
98+
99+func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) {
100+ return client.nonIdempotentRequest("PUT", uri, parameters)
101+}
102+
103+func (client Client) Delete(uri *url.URL) error {
104+ url := client.GetURL(uri)
105+ request, err := http.NewRequest("DELETE", url.String(), strings.NewReader(""))
106+ if err != nil {
107+ return err
108+ }
109+ _, err = client.dispatchRequest(request)
110+ if err != nil {
111+ return err
112+ }
113+ return nil
114+}
115+
116+type anonSigner struct{}
117+
118+func (signer anonSigner) OAuthSign(request *http.Request) error {
119+ return nil
120+}
121+
122+// Trick to ensure *anonSigner implements the OAuthSigner interface.
123+var _ OAuthSigner = anonSigner{}
124+
125+// NewAnonymousClient creates a client that issues anonymous requests.
126+func NewAnonymousClient(BaseURL string) (*Client, error) {
127+ parsedBaseURL, err := url.Parse(BaseURL)
128+ if err != nil {
129+ return nil, err
130+ }
131+ return &Client{Signer: &anonSigner{}, BaseURL: parsedBaseURL}, nil
132+}
133+
134+// NewAuthenticatedClient parses the given MAAS API key into the individual
135+// OAuth tokens and creates an Client that will use these tokens to sign the
136+// requests it issues.
137+func NewAuthenticatedClient(BaseURL string, apiKey string) (*Client, error) {
138+ elements := strings.Split(apiKey, ":")
139+ if len(elements) != 3 {
140+ errString := "invalid API key %q; expected \"<consumer secret>:<token key>:<token secret>\""
141+ return nil, fmt.Errorf(errString, apiKey)
142+ }
143+ token := &OAuthToken{
144+ ConsumerKey: elements[0],
145+ // The consumer secret is the empty string in MAAS' authentication.
146+ ConsumerSecret: "",
147+ TokenKey: elements[1],
148+ TokenSecret: elements[2],
149+ }
150+ signer, err := NewPlainTestOAuthSigner(token, "MAAS API")
151+ if err != nil {
152+ return nil, err
153+ }
154+ parsedBaseURL, err := url.Parse(BaseURL)
155+ if err != nil {
156+ return nil, err
157+ }
158+ return &Client{Signer: signer, BaseURL: parsedBaseURL}, nil
159+}
160
161=== added file 'client_test.go'
162--- client_test.go 1970-01-01 00:00:00 +0000
163+++ client_test.go 2013-02-06 17:03:19 +0000
164@@ -0,0 +1,142 @@
165+// Copyright 2013 Canonical Ltd. This software is licensed under the
166+// GNU Lesser General Public License version 3 (see the file COPYING).
167+
168+package gomaasapi
169+
170+import (
171+ . "launchpad.net/gocheck"
172+ "net/http"
173+ "net/url"
174+ "strings"
175+)
176+
177+type ClientSuite struct{}
178+
179+var _ = Suite(&ClientSuite{})
180+
181+func (suite *ClientSuite) TestClientdispatchRequestReturnsError(c *C) {
182+ URI := "/some/url/?param1=test"
183+ expectedResult := "expected:result"
184+ server := newSingleServingServer(URI, expectedResult, http.StatusBadRequest)
185+ defer server.Close()
186+ client, _ := NewAnonymousClient(server.URL)
187+ request, err := http.NewRequest("GET", server.URL+URI, nil)
188+
189+ result, err := client.dispatchRequest(request)
190+
191+ c.Check(err, ErrorMatches, "gomaasapi: got error back from server: 400 Bad Request.*")
192+ c.Check(string(result), Equals, expectedResult)
193+}
194+
195+func (suite *ClientSuite) TestClientdispatchRequestSignsRequest(c *C) {
196+ URI := "/some/url/?param1=test"
197+ expectedResult := "expected:result"
198+ server := newSingleServingServer(URI, expectedResult, http.StatusOK)
199+ defer server.Close()
200+ client, _ := NewAuthenticatedClient(server.URL, "the:api:key")
201+ request, err := http.NewRequest("GET", server.URL+URI, nil)
202+
203+ result, err := client.dispatchRequest(request)
204+
205+ c.Check(err, IsNil)
206+ c.Check(string(result), Equals, expectedResult)
207+ c.Check((*server.requestHeader)["Authorization"][0], Matches, "^OAuth .*")
208+}
209+
210+func (suite *ClientSuite) TestClientGetFormatsGetParameters(c *C) {
211+ URI, _ := url.Parse("/some/url")
212+ expectedResult := "expected:result"
213+ params := url.Values{"test": {"123"}}
214+ fullURI := URI.String() + "?test=123"
215+ server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
216+ defer server.Close()
217+ client, _ := NewAnonymousClient(server.URL)
218+
219+ result, err := client.Get(URI, "", params)
220+
221+ c.Check(err, IsNil)
222+ c.Check(string(result), Equals, expectedResult)
223+}
224+
225+func (suite *ClientSuite) TestClientGetFormatsOperationAsGetParameter(c *C) {
226+ URI, _ := url.Parse("/some/url")
227+ expectedResult := "expected:result"
228+ fullURI := URI.String() + "?op=list"
229+ server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
230+ defer server.Close()
231+ client, _ := NewAnonymousClient(server.URL)
232+
233+ result, err := client.Get(URI, "list", url.Values{})
234+
235+ c.Check(err, IsNil)
236+ c.Check(string(result), Equals, expectedResult)
237+}
238+
239+func (suite *ClientSuite) TestClientPostSendsRequest(c *C) {
240+ URI, _ := url.Parse("/some/url")
241+ expectedResult := "expected:result"
242+ fullURI := URI.String() + "?op=list"
243+ params := url.Values{"test": {"123"}}
244+ server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
245+ defer server.Close()
246+ client, _ := NewAnonymousClient(server.URL)
247+
248+ result, err := client.Post(URI, "list", params)
249+
250+ c.Check(err, IsNil)
251+ c.Check(string(result), Equals, expectedResult)
252+ c.Check(*server.requestContent, Equals, "test=123")
253+}
254+
255+func (suite *ClientSuite) TestClientPutSendsRequest(c *C) {
256+ URI, _ := url.Parse("/some/url")
257+ expectedResult := "expected:result"
258+ params := url.Values{"test": {"123"}}
259+ server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK)
260+ defer server.Close()
261+ client, _ := NewAnonymousClient(server.URL)
262+
263+ result, err := client.Put(URI, params)
264+
265+ c.Check(err, IsNil)
266+ c.Check(string(result), Equals, expectedResult)
267+ c.Check(*server.requestContent, Equals, "test=123")
268+}
269+
270+func (suite *ClientSuite) TestClientDeleteSendsRequest(c *C) {
271+ URI, _ := url.Parse("/some/url")
272+ expectedResult := "expected:result"
273+ server := newSingleServingServer(URI.String(), expectedResult, http.StatusOK)
274+ defer server.Close()
275+ client, _ := NewAnonymousClient(server.URL)
276+
277+ err := client.Delete(URI)
278+
279+ c.Check(err, IsNil)
280+}
281+
282+func (suite *ClientSuite) TestNewAuthenticatedClientParsesApiKey(c *C) {
283+ // NewAuthenticatedClient returns a plainTextOAuthSigneri configured
284+ // to use the given API key.
285+ consumerKey := "consumerKey"
286+ tokenKey := "tokenKey"
287+ tokenSecret := "tokenSecret"
288+ keyElements := []string{consumerKey, tokenKey, tokenSecret}
289+ apiKey := strings.Join(keyElements, ":")
290+
291+ client, err := NewAuthenticatedClient("http://example.com/api", apiKey)
292+
293+ c.Check(err, IsNil)
294+ signer := client.Signer.(*plainTextOAuthSigner)
295+ c.Check(signer.token.ConsumerKey, Equals, consumerKey)
296+ c.Check(signer.token.TokenKey, Equals, tokenKey)
297+ c.Check(signer.token.TokenSecret, Equals, tokenSecret)
298+}
299+
300+func (suite *ClientSuite) TestNewAuthenticatedClientFailsIfInvalidKey(c *C) {
301+ client, err := NewAuthenticatedClient("", "invalid-key")
302+
303+ c.Check(err, ErrorMatches, "invalid API key.*")
304+ c.Check(client, IsNil)
305+
306+}
307
308=== added directory 'example'
309=== added file 'example/live_example.go'
310--- example/live_example.go 1970-01-01 00:00:00 +0000
311+++ example/live_example.go 2013-02-06 17:03:19 +0000
312@@ -0,0 +1,106 @@
313+// Copyright 2013 Canonical Ltd. This software is licensed under the
314+// GNU Lesser General Public License version 3 (see the file COPYING).
315+
316+/*
317+This is an example on how the Go library gomaasapi can be used to interact with
318+a real MAAS server.
319+Note that this is a provided only as an example and that real code should probably do something more sensible with errors than ignoring them or panicking.
320+*/
321+package main
322+
323+import (
324+ "fmt"
325+ "launchpad.net/gomaasapi"
326+ "net/url"
327+)
328+
329+var apiKey string
330+var apiURL string
331+
332+func getParams() {
333+ fmt.Println("Warning: this will create a node on the MAAS server; it should be deleted at the end of the run but if something goes wrong, that test node might be left over. You've been warned.")
334+ fmt.Print("Enter API key: ")
335+ _, err := fmt.Scanf("%s", &apiKey)
336+ if err != nil {
337+ panic(err)
338+ }
339+ fmt.Print("Enter API URL: ")
340+ _, err = fmt.Scanf("%s", &apiURL)
341+ if err != nil {
342+ panic(err)
343+ }
344+}
345+
346+func checkError(err error) {
347+ if err != nil {
348+ panic(err)
349+ }
350+}
351+
352+func main() {
353+ getParams()
354+ authClient, err := gomaasapi.NewAuthenticatedClient(apiURL, apiKey)
355+ checkError(err)
356+
357+ maas := gomaasapi.NewMAAS(*authClient)
358+
359+ nodeListing := maas.GetSubObject("nodes")
360+
361+ // List nodes.
362+ fmt.Println("Fetching list of nodes...")
363+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
364+ checkError(err)
365+ listNodes, err := listNodeObjects.GetArray()
366+ checkError(err)
367+ fmt.Printf("Got list of %v nodes\n", len(listNodes))
368+ for index, nodeObj := range listNodes {
369+ node, err := nodeObj.GetMAASObject()
370+ checkError(err)
371+ hostname, err := node.GetField("hostname")
372+ checkError(err)
373+ fmt.Printf("Node #%d is named '%v' (%v)\n", index, hostname, node.URL())
374+ }
375+
376+ // Create a node.
377+ fmt.Println("Creating a new node...")
378+ params := url.Values{"architecture": {"i386/generic"}, "mac_addresses": {"AA:BB:CC:DD:EE:FF"}}
379+ newNodeObj, err := nodeListing.CallPost("new", params)
380+ checkError(err)
381+ newNode, err := newNodeObj.GetMAASObject()
382+ checkError(err)
383+ newNodeName, err := newNode.GetField("hostname")
384+ checkError(err)
385+ fmt.Printf("New node created: %s (%s)\n", newNodeName, newNode.URL())
386+
387+ // Update the new node.
388+ fmt.Println("Updating the new node...")
389+ updateParams := url.Values{"hostname": {"mynewname"}}
390+ newNodeObj2, err := newNode.Update(updateParams)
391+ checkError(err)
392+ newNode2, err := newNodeObj2.GetMAASObject()
393+ checkError(err)
394+ newNodeName2, err := newNode2.GetField("hostname")
395+ checkError(err)
396+ fmt.Printf("New node updated, now named: %s\n", newNodeName2)
397+
398+ // Count the nodes.
399+ listNodeObjects2, err := nodeListing.CallGet("list", url.Values{})
400+ checkError(err)
401+ listNodes2, err := listNodeObjects2.GetArray()
402+ checkError(err)
403+ fmt.Printf("We've got %v nodes\n", len(listNodes2))
404+
405+ // Delete the new node.
406+ fmt.Println("Deleting the new node...")
407+ errDelete := newNode.Delete()
408+ checkError(errDelete)
409+
410+ // Count the nodes.
411+ listNodeObjects3, err := nodeListing.CallGet("list", url.Values{})
412+ checkError(err)
413+ listNodes3, err := listNodeObjects3.GetArray()
414+ checkError(err)
415+ fmt.Printf("We've got %v nodes\n", len(listNodes3))
416+
417+ fmt.Println("All done.")
418+}
419
420=== modified file 'gomaasapi.go'
421--- gomaasapi.go 2013-01-22 10:29:56 +0000
422+++ gomaasapi.go 2013-02-06 17:03:19 +0000
423@@ -1,1 +1,4 @@
424+// Copyright 2013 Canonical Ltd. This software is licensed under the
425+// GNU Lesser General Public License version 3 (see the file COPYING).
426+
427 package gomaasapi
428
429=== modified file 'gomaasapi_test.go'
430--- gomaasapi_test.go 2013-01-22 10:29:56 +0000
431+++ gomaasapi_test.go 2013-02-06 17:03:19 +0000
432@@ -1,3 +1,6 @@
433+// Copyright 2013 Canonical Ltd. This software is licensed under the
434+// GNU Lesser General Public License version 3 (see the file COPYING).
435+
436 package gomaasapi
437
438 import (
439
440=== added file 'jsonobject.go'
441--- jsonobject.go 1970-01-01 00:00:00 +0000
442+++ jsonobject.go 2013-02-06 17:03:19 +0000
443@@ -0,0 +1,197 @@
444+// Copyright 2013 Canonical Ltd. This software is licensed under the
445+// GNU Lesser General Public License version 3 (see the file COPYING).
446+
447+package gomaasapi
448+
449+import (
450+ "encoding/json"
451+ "errors"
452+ "fmt"
453+)
454+
455+// JSONObject is a wrapper around a JSON structure which provides
456+// methods to extract data from that structure.
457+// A JSONObject provides a simple structure consisting of the data types
458+// defined in JSON: string, number, object, list, and bool. To get the
459+// value you want out of a JSONObject, you must know (or figure out) which
460+// kind of value you have, and then call the appropriate Get*() method to
461+// get at it. Reading an item as the wrong type will return an error.
462+// For instance, if your JSONObject consists of a number, call GetFloat64()
463+// to get the value as a float64. If it's a list, call GetArray() to get
464+// a slice of JSONObjects. To read any given item from the slice, you'll
465+// need to "Get" that as the right type as well.
466+// There is one exception: a MAASObject is really a special kind of map,
467+// so you can read it as either.
468+// Reading a null item is also an error. So before you try obj.Get*(),
469+// first check that obj != nil.
470+type JSONObject interface {
471+ // Type of this value:
472+ // "string", "float64", "map", "maasobject", "array", or "bool".
473+ Type() string
474+ // Read as string.
475+ GetString() (string, error)
476+ // Read number as float64.
477+ GetFloat64() (float64, error)
478+ // Read object as map.
479+ GetMap() (map[string]JSONObject, error)
480+ // Read object as MAAS object.
481+ GetMAASObject() (MAASObject, error)
482+ // Read list as array.
483+ GetArray() ([]JSONObject, error)
484+ // Read as bool.
485+ GetBool() (bool, error)
486+}
487+
488+// Internally, each JSONObject already knows what type it is. It just
489+// can't tell the caller yet because the caller may not have the right
490+// hard-coded variable type.
491+// So for each JSON type, there is a separate implementation of JSONObject
492+// that converts only to that type. Any other conversion is an error.
493+// One type is special: jsonMAASObject is an object in the MAAS sense. It
494+// behaves just like a jsonMap if you want it to, but it also implements
495+// MAASObject.
496+type jsonString string
497+type jsonFloat64 float64
498+type jsonMap map[string]JSONObject
499+type jsonArray []JSONObject
500+type jsonBool bool
501+
502+// Our JSON processor distinguishes a MAASObject from a jsonMap by the fact
503+// that it contains a key "resource_uri". (A regular map might contain the
504+// same key through sheer coincide, but never mind: you can still treat it
505+// as a jsonMap and never notice the difference.)
506+const resourceURI = "resource_uri"
507+
508+// maasify is internal. It turns a completely untyped json.Unmarshal result
509+// into a JSONObject (with the appropriate implementation of course).
510+// This function is recursive. Maps and arrays are deep-copied, with each
511+// individual value being converted to a JSONObject type.
512+func maasify(client Client, value interface{}) JSONObject {
513+ if value == nil {
514+ return nil
515+ }
516+ switch value.(type) {
517+ case string:
518+ return jsonString(value.(string))
519+ case float64:
520+ return jsonFloat64(value.(float64))
521+ case map[string]interface{}:
522+ original := value.(map[string]interface{})
523+ result := make(map[string]JSONObject, len(original))
524+ for key, value := range original {
525+ result[key] = maasify(client, value)
526+ }
527+ if _, ok := result[resourceURI]; ok {
528+ // If the map contains "resource-uri", we can treat
529+ // it as a MAAS object.
530+ return newJSONMAASObject(result, client)
531+ }
532+ return jsonMap(result)
533+ case []interface{}:
534+ original := value.([]interface{})
535+ result := make([]JSONObject, len(original))
536+ for index, value := range original {
537+ result[index] = maasify(client, value)
538+ }
539+ return jsonArray(result)
540+ case bool:
541+ return jsonBool(value.(bool))
542+ }
543+ msg := fmt.Sprintf("Unknown JSON type, can't be converted to JSONObject: %v", value)
544+ panic(msg)
545+}
546+
547+// Parse a JSON blob into a JSONObject.
548+func Parse(client Client, input []byte) (JSONObject, error) {
549+ var obj interface{}
550+ err := json.Unmarshal(input, &obj)
551+ if err != nil {
552+ return nil, err
553+ }
554+ return maasify(client, obj), nil
555+}
556+
557+// Return error value for failed type conversion.
558+func failConversion(wantedType string, obj JSONObject) error {
559+ msg := fmt.Sprintf("Requested %v, got %v.", wantedType, obj.Type())
560+ return errors.New(msg)
561+}
562+
563+// Error return values for failure to convert to string.
564+func failString(obj JSONObject) (string, error) {
565+ return "", failConversion("string", obj)
566+}
567+
568+// Error return values for failure to convert to float64.
569+func failFloat64(obj JSONObject) (float64, error) {
570+ return 0.0, failConversion("float64", obj)
571+}
572+
573+// Error return values for failure to convert to map.
574+func failMap(obj JSONObject) (map[string]JSONObject, error) {
575+ return make(map[string]JSONObject, 0), failConversion("map", obj)
576+}
577+
578+// Error return values for failure to convert to MAAS object.
579+func failMAASObject(obj JSONObject) (MAASObject, error) {
580+ return jsonMAASObject{}, failConversion("maasobject", obj)
581+}
582+
583+// Error return values for failure to convert to array.
584+func failArray(obj JSONObject) ([]JSONObject, error) {
585+ return make([]JSONObject, 0), failConversion("array", obj)
586+}
587+
588+// Error return values for failure to convert to bool.
589+func failBool(obj JSONObject) (bool, error) {
590+ return false, failConversion("bool", obj)
591+}
592+
593+// JSONObject implementation for jsonString.
594+func (jsonString) Type() string { return "string" }
595+func (obj jsonString) GetString() (string, error) { return string(obj), nil }
596+func (obj jsonString) GetFloat64() (float64, error) { return failFloat64(obj) }
597+func (obj jsonString) GetMap() (map[string]JSONObject, error) { return failMap(obj) }
598+func (obj jsonString) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) }
599+func (obj jsonString) GetArray() ([]JSONObject, error) { return failArray(obj) }
600+func (obj jsonString) GetBool() (bool, error) { return failBool(obj) }
601+
602+// JSONObject implementation for jsonFloat64.
603+func (jsonFloat64) Type() string { return "float64" }
604+func (obj jsonFloat64) GetString() (string, error) { return failString(obj) }
605+func (obj jsonFloat64) GetFloat64() (float64, error) { return float64(obj), nil }
606+func (obj jsonFloat64) GetMap() (map[string]JSONObject, error) { return failMap(obj) }
607+func (obj jsonFloat64) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) }
608+func (obj jsonFloat64) GetArray() ([]JSONObject, error) { return failArray(obj) }
609+func (obj jsonFloat64) GetBool() (bool, error) { return failBool(obj) }
610+
611+// JSONObject implementation for jsonMap.
612+func (jsonMap) Type() string { return "map" }
613+func (obj jsonMap) GetString() (string, error) { return failString(obj) }
614+func (obj jsonMap) GetFloat64() (float64, error) { return failFloat64(obj) }
615+func (obj jsonMap) GetMap() (map[string]JSONObject, error) {
616+ return (map[string]JSONObject)(obj), nil
617+}
618+func (obj jsonMap) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) }
619+func (obj jsonMap) GetArray() ([]JSONObject, error) { return failArray(obj) }
620+func (obj jsonMap) GetBool() (bool, error) { return failBool(obj) }
621+
622+// JSONObject implementation for jsonArray.
623+func (jsonArray) Type() string { return "array" }
624+func (obj jsonArray) GetString() (string, error) { return failString(obj) }
625+func (obj jsonArray) GetFloat64() (float64, error) { return failFloat64(obj) }
626+func (obj jsonArray) GetMap() (map[string]JSONObject, error) { return failMap(obj) }
627+func (obj jsonArray) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) }
628+func (obj jsonArray) GetArray() ([]JSONObject, error) {
629+ return ([]JSONObject)(obj), nil
630+}
631+func (obj jsonArray) GetBool() (bool, error) { return failBool(obj) }
632+
633+// JSONObject implementation for jsonBool.
634+func (jsonBool) Type() string { return "bool" }
635+func (obj jsonBool) GetString() (string, error) { return failString(obj) }
636+func (obj jsonBool) GetFloat64() (float64, error) { return failFloat64(obj) }
637+func (obj jsonBool) GetMap() (map[string]JSONObject, error) { return failMap(obj) }
638+func (obj jsonBool) GetMAASObject() (MAASObject, error) { return failMAASObject(obj) }
639+func (obj jsonBool) GetArray() ([]JSONObject, error) { return failArray(obj) }
640+func (obj jsonBool) GetBool() (bool, error) { return bool(obj), nil }
641
642=== added file 'jsonobject_test.go'
643--- jsonobject_test.go 1970-01-01 00:00:00 +0000
644+++ jsonobject_test.go 2013-02-06 17:03:19 +0000
645@@ -0,0 +1,228 @@
646+// Copyright 2013 Canonical Ltd. This software is licensed under the
647+// GNU Lesser General Public License version 3 (see the file COPYING).
648+
649+package gomaasapi
650+
651+import (
652+ . "launchpad.net/gocheck"
653+)
654+
655+type JSONObjectSuite struct {
656+}
657+
658+var _ = Suite(&JSONObjectSuite{})
659+
660+// maasify() converts nil.
661+func (suite *JSONObjectSuite) TestMaasifyConvertsNil(c *C) {
662+ c.Check(maasify(Client{}, nil), Equals, nil)
663+}
664+
665+// maasify() converts strings.
666+func (suite *JSONObjectSuite) TestMaasifyConvertsString(c *C) {
667+ const text = "Hello"
668+ c.Check(string(maasify(Client{}, text).(jsonString)), Equals, text)
669+}
670+
671+// maasify() converts float64 numbers.
672+func (suite *JSONObjectSuite) TestMaasifyConvertsNumber(c *C) {
673+ const number = 3.1415926535
674+ c.Check(float64(maasify(Client{}, number).(jsonFloat64)), Equals, number)
675+}
676+
677+// maasify() converts array slices.
678+func (suite *JSONObjectSuite) TestMaasifyConvertsArray(c *C) {
679+ original := []interface{}{3.0, 2.0, 1.0}
680+ output := maasify(Client{}, original).(jsonArray)
681+ c.Check(len(output), Equals, len(original))
682+}
683+
684+// When maasify() converts an array slice, the result contains JSONObjects.
685+func (suite *JSONObjectSuite) TestMaasifyArrayContainsJSONObjects(c *C) {
686+ arr := maasify(Client{}, []interface{}{9.9}).(jsonArray)
687+ var entry JSONObject
688+ entry = arr[0]
689+ c.Check((float64)(entry.(jsonFloat64)), Equals, 9.9)
690+}
691+
692+// maasify() converts maps.
693+func (suite *JSONObjectSuite) TestMaasifyConvertsMap(c *C) {
694+ original := map[string]interface{}{"1": "one", "2": "two", "3": "three"}
695+ output := maasify(Client{}, original).(jsonMap)
696+ c.Check(len(output), Equals, len(original))
697+}
698+
699+// When maasify() converts a map, the result contains JSONObjects.
700+func (suite *JSONObjectSuite) TestMaasifyMapContainsJSONObjects(c *C) {
701+ mp := maasify(Client{}, map[string]interface{}{"key": "value"}).(jsonMap)
702+ var entry JSONObject
703+ entry = mp["key"]
704+ c.Check((string)(entry.(jsonString)), Equals, "value")
705+}
706+
707+// maasify() converts MAAS objects.
708+func (suite *JSONObjectSuite) TestMaasifyConvertsMAASObject(c *C) {
709+ original := map[string]interface{}{
710+ "resource_uri": "http://example.com/foo",
711+ "size": "3",
712+ }
713+ output := maasify(Client{}, original).(jsonMAASObject)
714+ c.Check(len(output.jsonMap), Equals, len(original))
715+ c.Check((string)(output.jsonMap["size"].(jsonString)), Equals, "3")
716+}
717+
718+// maasify() passes its client to a MAASObject it creates.
719+func (suite *JSONObjectSuite) TestMaasifyPassesInfoToMAASObject(c *C) {
720+ client := Client{}
721+ original := map[string]interface{}{"resource_uri": "/foo"}
722+ output := maasify(client, original).(jsonMAASObject)
723+ c.Check(output.client, Equals, client)
724+}
725+
726+// maasify() passes its client into an array of MAASObjects it creates.
727+func (suite *JSONObjectSuite) TestMaasifyPassesInfoIntoArray(c *C) {
728+ client := Client{}
729+ obj := map[string]interface{}{"resource_uri": "/foo"}
730+ list := []interface{}{obj}
731+ output := maasify(client, list).(jsonArray)
732+ c.Check(output[0].(jsonMAASObject).client, Equals, client)
733+}
734+
735+// maasify() passes its client into a map of MAASObjects it creates.
736+func (suite *JSONObjectSuite) TestMaasifyPassesInfoIntoMap(c *C) {
737+ client := Client{}
738+ obj := map[string]interface{}{"resource_uri": "/foo"}
739+ mp := map[string]interface{}{"key": obj}
740+ output := maasify(client, mp).(jsonMap)
741+ c.Check(output["key"].(jsonMAASObject).client, Equals, client)
742+}
743+
744+// maasify() passes its client all the way down into any MAASObjects in the
745+// object structure it creates.
746+func (suite *JSONObjectSuite) TestMaasifyPassesInfoAllTheWay(c *C) {
747+ client := Client{}
748+ obj := map[string]interface{}{"resource_uri": "/foo"}
749+ mp := map[string]interface{}{"key": obj}
750+ list := []interface{}{mp}
751+ output := maasify(client, list).(jsonArray)
752+ maasobj := output[0].(jsonMap)["key"]
753+ c.Check(maasobj.(jsonMAASObject).client, Equals, client)
754+}
755+
756+// maasify() converts Booleans.
757+func (suite *JSONObjectSuite) TestMaasifyConvertsBool(c *C) {
758+ c.Check(bool(maasify(Client{}, true).(jsonBool)), Equals, true)
759+ c.Check(bool(maasify(Client{}, false).(jsonBool)), Equals, false)
760+}
761+
762+// Parse takes you from a JSON blob to a JSONObject.
763+func (suite *JSONObjectSuite) TestParseMaasifiesJSONBlob(c *C) {
764+ blob := []byte("[12]")
765+ obj, err := Parse(Client{}, blob)
766+ c.Check(err, IsNil)
767+ c.Check(float64(obj.(jsonArray)[0].(jsonFloat64)), Equals, 12.0)
768+}
769+
770+// String-type JSONObjects convert only to string.
771+func (suite *JSONObjectSuite) TestConversionsString(c *C) {
772+ obj := jsonString("Test string")
773+
774+ value, err := obj.GetString()
775+ c.Check(err, IsNil)
776+ c.Check(value, Equals, "Test string")
777+
778+ _, err = obj.GetFloat64()
779+ c.Check(err, NotNil)
780+ _, err = obj.GetMap()
781+ c.Check(err, NotNil)
782+ _, err = obj.GetMAASObject()
783+ c.Check(err, NotNil)
784+ _, err = obj.GetArray()
785+ c.Check(err, NotNil)
786+ _, err = obj.GetBool()
787+ c.Check(err, NotNil)
788+}
789+
790+// Number-type JSONObjects convert only to float64.
791+func (suite *JSONObjectSuite) TestConversionsFloat64(c *C) {
792+ obj := jsonFloat64(1.1)
793+
794+ value, err := obj.GetFloat64()
795+ c.Check(err, IsNil)
796+ c.Check(value, Equals, 1.1)
797+
798+ _, err = obj.GetString()
799+ c.Check(err, NotNil)
800+ _, err = obj.GetMap()
801+ c.Check(err, NotNil)
802+ _, err = obj.GetMAASObject()
803+ c.Check(err, NotNil)
804+ _, err = obj.GetArray()
805+ c.Check(err, NotNil)
806+ _, err = obj.GetBool()
807+ c.Check(err, NotNil)
808+}
809+
810+// Map-type JSONObjects convert only to map.
811+func (suite *JSONObjectSuite) TestConversionsMap(c *C) {
812+ input := map[string]JSONObject{"x": jsonString("y")}
813+ obj := jsonMap(input)
814+
815+ value, err := obj.GetMap()
816+ c.Check(err, IsNil)
817+ text, err := value["x"].GetString()
818+ c.Check(err, IsNil)
819+ c.Check(text, Equals, "y")
820+
821+ _, err = obj.GetString()
822+ c.Check(err, NotNil)
823+ _, err = obj.GetFloat64()
824+ c.Check(err, NotNil)
825+ _, err = obj.GetMAASObject()
826+ c.Check(err, NotNil)
827+ _, err = obj.GetArray()
828+ c.Check(err, NotNil)
829+ _, err = obj.GetBool()
830+ c.Check(err, NotNil)
831+}
832+
833+// Array-type JSONObjects convert only to array.
834+func (suite *JSONObjectSuite) TestConversionsArray(c *C) {
835+ obj := jsonArray([]JSONObject{jsonString("item")})
836+
837+ value, err := obj.GetArray()
838+ c.Check(err, IsNil)
839+ text, err := value[0].GetString()
840+ c.Check(err, IsNil)
841+ c.Check(text, Equals, "item")
842+
843+ _, err = obj.GetString()
844+ c.Check(err, NotNil)
845+ _, err = obj.GetFloat64()
846+ c.Check(err, NotNil)
847+ _, err = obj.GetMap()
848+ c.Check(err, NotNil)
849+ _, err = obj.GetMAASObject()
850+ c.Check(err, NotNil)
851+ _, err = obj.GetBool()
852+ c.Check(err, NotNil)
853+}
854+
855+// Boolean-type JSONObjects convert only to bool.
856+func (suite *JSONObjectSuite) TestConversionsBool(c *C) {
857+ obj := jsonBool(false)
858+
859+ value, err := obj.GetBool()
860+ c.Check(err, IsNil)
861+ c.Check(value, Equals, false)
862+
863+ _, err = obj.GetString()
864+ c.Check(err, NotNil)
865+ _, err = obj.GetFloat64()
866+ c.Check(err, NotNil)
867+ _, err = obj.GetMap()
868+ c.Check(err, NotNil)
869+ _, err = obj.GetMAASObject()
870+ c.Check(err, NotNil)
871+ _, err = obj.GetArray()
872+ c.Check(err, NotNil)
873+}
874
875=== added file 'maas.go'
876--- maas.go 1970-01-01 00:00:00 +0000
877+++ maas.go 2013-02-06 17:03:19 +0000
878@@ -0,0 +1,10 @@
879+// Copyright 2013 Canonical Ltd. This software is licensed under the
880+// GNU Lesser General Public License version 3 (see the file COPYING).
881+
882+package gomaasapi
883+
884+// NewMAAS returns an interface to the MAAS API as a MAASObject.
885+func NewMAAS(client Client) MAASObject {
886+ input := map[string]JSONObject{resourceURI: jsonString(client.BaseURL.String())}
887+ return newJSONMAASObject(jsonMap(input), client)
888+}
889
890=== added file 'maas_test.go'
891--- maas_test.go 1970-01-01 00:00:00 +0000
892+++ maas_test.go 2013-02-06 17:03:19 +0000
893@@ -0,0 +1,22 @@
894+// Copyright 2013 Canonical Ltd. This software is licensed under the
895+// GNU Lesser General Public License version 3 (see the file COPYING).
896+
897+package gomaasapi
898+
899+import (
900+ . "launchpad.net/gocheck"
901+ "net/url"
902+)
903+
904+type MAASSuite struct{}
905+
906+var _ = Suite(&MAASSuite{})
907+
908+func (suite *MAASSuite) TestNewMAASUsesBaseURLFromClient(c *C) {
909+ baseURLString := "https://server.com:888/path/to/api"
910+ baseURL, _ := url.Parse(baseURLString)
911+ client := Client{BaseURL: baseURL}
912+ maas := NewMAAS(client)
913+ URL := maas.URL()
914+ c.Check(URL, DeepEquals, baseURL)
915+}
916
917=== added file 'maasobject.go'
918--- maasobject.go 1970-01-01 00:00:00 +0000
919+++ maasobject.go 2013-02-06 17:03:19 +0000
920@@ -0,0 +1,167 @@
921+// Copyright 2013 Canonical Ltd. This software is licensed under the
922+// GNU Lesser General Public License version 3 (see the file COPYING).
923+
924+package gomaasapi
925+
926+import (
927+ "errors"
928+ "net/url"
929+)
930+
931+// MAASObject represents a MAAS object as returned by the MAAS API, such as a
932+// Node or a Tag.
933+// This is a special kind of JSONObject. A MAAS API call will usually return
934+// either a MAASObject or a list of MAASObjects. (The list itself will be
935+// wrapped in a JSONObject).
936+type MAASObject interface {
937+ JSONObject
938+
939+ // Utility method to extract a string field from this MAAS object.
940+ GetField(name string) (string, error)
941+ // URL for this MAAS object.
942+ URL() *url.URL
943+ // Resource URI for this MAAS object.
944+ URI() *url.URL
945+ // Retrieve the MAAS object located at thisObject.URI()+name.
946+ GetSubObject(name string) MAASObject
947+ // Retrieve this MAAS object.
948+ Get() (MAASObject, error)
949+ // Write this MAAS object.
950+ Post(params url.Values) (JSONObject, error)
951+ // Update this MAAS object with the given values.
952+ Update(params url.Values) (MAASObject, error)
953+ // Delete this MAAS object.
954+ Delete() error
955+ // Invoke a GET-based method on this MAAS object.
956+ CallGet(operation string, params url.Values) (JSONObject, error)
957+ // Invoke a POST-based method on this MAAS object.
958+ CallPost(operation string, params url.Values) (JSONObject, error)
959+}
960+
961+// JSONObject implementation for a MAAS object. From a decoding perspective,
962+// a jsonMAASObject is just like a jsonMap except it contains a key
963+// "resource_uri", and it keeps track of the Client you got it from so that
964+// you can invoke API methods directly on their MAAS objects.
965+// jsonMAASObject implements both JSONObject and MAASObject.
966+type jsonMAASObject struct {
967+ jsonMap
968+ client Client
969+ uri *url.URL
970+}
971+
972+// newJSONMAASObject creates a new MAAS object. It will panic if the given map
973+// does not contain a valid URL for the 'resource_uri' key.
974+func newJSONMAASObject(jmap jsonMap, client Client) jsonMAASObject {
975+ const panicPrefix = "Error processing MAAS object: "
976+ uriObj, ok := jmap[resourceURI]
977+ if !ok {
978+ panic(errors.New(panicPrefix + "no 'resource_uri' key present in the given jsonMap."))
979+ }
980+ uriString, err := uriObj.GetString()
981+ if err != nil {
982+ panic(errors.New(panicPrefix + "the value of 'resource_uri' is not a string."))
983+ }
984+ uri, err := url.Parse(uriString)
985+ if err != nil {
986+ panic(errors.New(panicPrefix + "the value of 'resource_uri' is not a valid URL."))
987+ }
988+ return jsonMAASObject{jmap, client, uri}
989+}
990+
991+var _ JSONObject = (*jsonMAASObject)(nil)
992+var _ MAASObject = (*jsonMAASObject)(nil)
993+
994+// JSONObject implementation for jsonMAASObject.
995+func (jsonMAASObject) Type() string { return "maasobject" }
996+func (obj jsonMAASObject) GetString() (string, error) { return failString(obj) }
997+func (obj jsonMAASObject) GetFloat64() (float64, error) { return failFloat64(obj) }
998+func (obj jsonMAASObject) GetMap() (map[string]JSONObject, error) { return obj.jsonMap.GetMap() }
999+func (obj jsonMAASObject) GetMAASObject() (MAASObject, error) { return obj, nil }
1000+func (obj jsonMAASObject) GetArray() ([]JSONObject, error) { return failArray(obj) }
1001+func (obj jsonMAASObject) GetBool() (bool, error) { return failBool(obj) }
1002+
1003+// MAASObject implementation for jsonMAASObject.
1004+
1005+func (obj jsonMAASObject) GetField(name string) (string, error) {
1006+ return obj.jsonMap[name].GetString()
1007+}
1008+
1009+func (obj jsonMAASObject) URI() *url.URL {
1010+ // Duplicate the URL.
1011+ uri, err := url.Parse(obj.uri.String())
1012+ if err != nil {
1013+ panic(err)
1014+ }
1015+ return uri
1016+}
1017+
1018+func (obj jsonMAASObject) URL() *url.URL {
1019+ return obj.client.GetURL(obj.URI())
1020+}
1021+
1022+func (obj jsonMAASObject) GetSubObject(name string) MAASObject {
1023+ uri := obj.URI()
1024+ uri.Path = EnsureTrailingSlash(JoinURLs(uri.Path, name))
1025+ input := map[string]JSONObject{resourceURI: jsonString(uri.String())}
1026+ return newJSONMAASObject(jsonMap(input), obj.client)
1027+}
1028+
1029+var NotImplemented = errors.New("Not implemented")
1030+
1031+func (obj jsonMAASObject) Get() (MAASObject, error) {
1032+ uri := obj.URI()
1033+ result, err := obj.client.Get(uri, "", url.Values{})
1034+ if err != nil {
1035+ return nil, err
1036+ }
1037+ jsonObj, err := Parse(obj.client, result)
1038+ if err != nil {
1039+ return nil, err
1040+ }
1041+ return jsonObj.GetMAASObject()
1042+}
1043+
1044+func (obj jsonMAASObject) Post(params url.Values) (JSONObject, error) {
1045+ uri := obj.URI()
1046+ result, err := obj.client.Post(uri, "", params)
1047+ if err != nil {
1048+ return nil, err
1049+ }
1050+ return Parse(obj.client, result)
1051+}
1052+
1053+func (obj jsonMAASObject) Update(params url.Values) (MAASObject, error) {
1054+ uri := obj.URI()
1055+ result, err := obj.client.Put(uri, params)
1056+ if err != nil {
1057+ return nil, err
1058+ }
1059+ jsonObj, err := Parse(obj.client, result)
1060+ if err != nil {
1061+ return nil, err
1062+ }
1063+ return jsonObj.GetMAASObject()
1064+}
1065+
1066+func (obj jsonMAASObject) Delete() error {
1067+ uri := obj.URI()
1068+ return obj.client.Delete(uri)
1069+}
1070+
1071+func (obj jsonMAASObject) CallGet(operation string, params url.Values) (JSONObject, error) {
1072+ uri := obj.URI()
1073+ result, err := obj.client.Get(uri, operation, params)
1074+ if err != nil {
1075+ return nil, err
1076+ }
1077+ return Parse(obj.client, result)
1078+}
1079+
1080+func (obj jsonMAASObject) CallPost(operation string, params url.Values) (JSONObject, error) {
1081+ uri := obj.URI()
1082+ result, err := obj.client.Post(uri, operation, params)
1083+ if err != nil {
1084+ return nil, err
1085+ }
1086+ return Parse(obj.client, result)
1087+}
1088
1089=== added file 'maasobject_test.go'
1090--- maasobject_test.go 1970-01-01 00:00:00 +0000
1091+++ maasobject_test.go 2013-02-06 17:03:19 +0000
1092@@ -0,0 +1,133 @@
1093+// Copyright 2013 Canonical Ltd. This software is licensed under the
1094+// GNU Lesser General Public License version 3 (see the file COPYING).
1095+
1096+package gomaasapi
1097+
1098+import (
1099+ "fmt"
1100+ . "launchpad.net/gocheck"
1101+ "math/rand"
1102+ "net/url"
1103+)
1104+
1105+type MAASObjectSuite struct{}
1106+
1107+var _ = Suite(&MAASObjectSuite{})
1108+
1109+func makeFakeResourceURI() string {
1110+ return "http://example.com/" + fmt.Sprint(rand.Int31())
1111+}
1112+
1113+func makeFakeMAASObject() jsonMAASObject {
1114+ attrs := make(map[string]JSONObject)
1115+ attrs[resourceURI] = jsonString(makeFakeResourceURI())
1116+ return jsonMAASObject{jsonMap: jsonMap(attrs)}
1117+}
1118+
1119+// jsonMAASObjects convert only to map or to MAASObject.
1120+func (suite *MAASObjectSuite) TestConversionsMAASObject(c *C) {
1121+ input := map[string]JSONObject{resourceURI: jsonString("someplace")}
1122+ obj := jsonMAASObject{jsonMap: jsonMap(input)}
1123+
1124+ mp, err := obj.GetMap()
1125+ c.Check(err, IsNil)
1126+ text, err := mp[resourceURI].GetString()
1127+ c.Check(err, IsNil)
1128+ c.Check(text, Equals, "someplace")
1129+
1130+ maasobj, err := obj.GetMAASObject()
1131+ c.Check(err, IsNil)
1132+ _ = maasobj.(jsonMAASObject)
1133+
1134+ _, err = obj.GetString()
1135+ c.Check(err, NotNil)
1136+ _, err = obj.GetFloat64()
1137+ c.Check(err, NotNil)
1138+ _, err = obj.GetArray()
1139+ c.Check(err, NotNil)
1140+ _, err = obj.GetBool()
1141+ c.Check(err, NotNil)
1142+}
1143+
1144+func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfNoResourceURI(c *C) {
1145+ defer func() {
1146+ recoveredError := recover()
1147+ c.Check(recoveredError, NotNil)
1148+ msg := recoveredError.(error).Error()
1149+ c.Check(msg, Matches, ".*no 'resource_uri' key.*")
1150+ }()
1151+ input := map[string]JSONObject{"test": jsonString("test")}
1152+ newJSONMAASObject(jsonMap(input), Client{})
1153+}
1154+
1155+func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfResourceURINotString(c *C) {
1156+ defer func() {
1157+ recoveredError := recover()
1158+ c.Check(recoveredError, NotNil)
1159+ msg := recoveredError.(error).Error()
1160+ c.Check(msg, Matches, ".*the value of 'resource_uri' is not a string.*")
1161+ }()
1162+ input := map[string]JSONObject{resourceURI: jsonFloat64(77.7)}
1163+ newJSONMAASObject(jsonMap(input), Client{})
1164+}
1165+
1166+func (suite *MAASObjectSuite) TestNewJSONMAASObjectPanicsIfResourceURINotURL(c *C) {
1167+ defer func() {
1168+ recoveredError := recover()
1169+ c.Check(recoveredError, NotNil)
1170+ msg := recoveredError.(error).Error()
1171+ c.Check(msg, Matches, ".*the value of 'resource_uri' is not a valid URL.*")
1172+ }()
1173+ input := map[string]JSONObject{resourceURI: jsonString("")}
1174+ newJSONMAASObject(jsonMap(input), Client{})
1175+}
1176+
1177+func (suite *MAASObjectSuite) TestNewJSONMAASObjectSetsUpURI(c *C) {
1178+ URI, _ := url.Parse("http://example.com/a/resource")
1179+ input := map[string]JSONObject{resourceURI: jsonString(URI.String())}
1180+ obj := newJSONMAASObject(jsonMap(input), Client{})
1181+ c.Check(obj.uri, DeepEquals, URI)
1182+}
1183+
1184+func (suite *MAASObjectSuite) TestURL(c *C) {
1185+ baseURL, _ := url.Parse("http://example.com/")
1186+ uri := "http://example.com/a/resource"
1187+ resourceURL, _ := url.Parse(uri)
1188+ input := map[string]JSONObject{resourceURI: jsonString(uri)}
1189+ client := Client{BaseURL: baseURL}
1190+ obj := newJSONMAASObject(jsonMap(input), client)
1191+
1192+ URL := obj.URL()
1193+
1194+ c.Check(URL, DeepEquals, resourceURL)
1195+}
1196+
1197+func (suite *MAASObjectSuite) TestGetSubObject(c *C) {
1198+ baseURL, _ := url.Parse("http://example.com/")
1199+ uri := "http://example.com/a/resource/"
1200+ input := map[string]JSONObject{resourceURI: jsonString(uri)}
1201+ client := Client{BaseURL: baseURL}
1202+ obj := newJSONMAASObject(jsonMap(input), client)
1203+ subName := "/test"
1204+
1205+ subObj := obj.GetSubObject(subName)
1206+ subURL := subObj.URL()
1207+
1208+ // uri ends with a slash and subName starts with one, but the two paths
1209+ // should be concatenated as "http://example.com/a/resource/test/".
1210+ expectedSubURL, _ := url.Parse("http://example.com/a/resource/test/")
1211+ c.Check(subURL, DeepEquals, expectedSubURL)
1212+}
1213+
1214+func (suite *MAASObjectSuite) TestGetField(c *C) {
1215+ uri := "http://example.com/a/resource"
1216+ fieldName := "field name"
1217+ fieldValue := "a value"
1218+ input := map[string]JSONObject{
1219+ resourceURI: jsonString(uri), fieldName: jsonString(fieldValue),
1220+ }
1221+ obj := jsonMAASObject{jsonMap: jsonMap(input)}
1222+ value, err := obj.GetField(fieldName)
1223+ c.Check(err, IsNil)
1224+ c.Check(value, Equals, fieldValue)
1225+}
1226
1227=== added file 'oauth.go'
1228--- oauth.go 1970-01-01 00:00:00 +0000
1229+++ oauth.go 2013-02-06 17:03:19 +0000
1230@@ -0,0 +1,81 @@
1231+// Copyright 2013 Canonical Ltd. This software is licensed under the
1232+// GNU Lesser General Public License version 3 (see the file COPYING).
1233+
1234+package gomaasapi
1235+
1236+import (
1237+ "crypto/rand"
1238+ "fmt"
1239+ "math/big"
1240+ "net/http"
1241+ "net/url"
1242+ "strconv"
1243+ "strings"
1244+ "time"
1245+)
1246+
1247+var nonceMax = big.NewInt(100000000)
1248+
1249+func generateNonce() (string, error) {
1250+ randInt, err := rand.Int(rand.Reader, nonceMax)
1251+ if err != nil {
1252+ return "", err
1253+ }
1254+ return strconv.Itoa(int(randInt.Int64())), nil
1255+}
1256+
1257+func generateTimestamp() string {
1258+ return strconv.Itoa(int(time.Now().Unix()))
1259+}
1260+
1261+type OAuthSigner interface {
1262+ OAuthSign(request *http.Request) error
1263+}
1264+
1265+type OAuthToken struct {
1266+ ConsumerKey string
1267+ ConsumerSecret string
1268+ TokenKey string
1269+ TokenSecret string
1270+}
1271+
1272+// Trick to ensure *plainTextOAuthSigner implements the OAuthSigner interface.
1273+var _ OAuthSigner = (*plainTextOAuthSigner)(nil)
1274+
1275+type plainTextOAuthSigner struct {
1276+ token *OAuthToken
1277+ realm string
1278+}
1279+
1280+func NewPlainTestOAuthSigner(token *OAuthToken, realm string) (OAuthSigner, error) {
1281+ return &plainTextOAuthSigner{token, realm}, nil
1282+}
1283+
1284+// OAuthSignPLAINTEXT signs the provided request using the OAuth PLAINTEXT
1285+// method: http://oauth.net/core/1.0/#anchor22.
1286+func (signer plainTextOAuthSigner) OAuthSign(request *http.Request) error {
1287+
1288+ signature := signer.token.ConsumerSecret + `&` + signer.token.TokenSecret
1289+ nonce, err := generateNonce()
1290+ if err != nil {
1291+ return err
1292+ }
1293+ authData := map[string]string{
1294+ "realm": signer.realm,
1295+ "oauth_consumer_key": signer.token.ConsumerKey,
1296+ "oauth_token": signer.token.TokenKey,
1297+ "oauth_signature_method": "PLAINTEXT",
1298+ "oauth_signature": signature,
1299+ "oauth_timestamp": generateTimestamp(),
1300+ "oauth_nonce": nonce,
1301+ "oauth_version": "1.0",
1302+ }
1303+ // Build OAuth header.
1304+ var authHeader []string
1305+ for key, value := range authData {
1306+ authHeader = append(authHeader, fmt.Sprintf(`%s="%s"`, key, url.QueryEscape(value)))
1307+ }
1308+ strHeader := "OAuth " + strings.Join(authHeader, ", ")
1309+ request.Header.Add("Authorization", strHeader)
1310+ return nil
1311+}
1312
1313=== added directory 'templates'
1314=== added file 'templates/source.go'
1315--- templates/source.go 1970-01-01 00:00:00 +0000
1316+++ templates/source.go 2013-02-06 17:03:19 +0000
1317@@ -0,0 +1,4 @@
1318+// Copyright 2013 Canonical Ltd. This software is licensed under the
1319+// GNU Lesser General Public License version 3 (see the file COPYING).
1320+
1321+package gomaasapi
1322
1323=== added file 'templates/source_test.go'
1324--- templates/source_test.go 1970-01-01 00:00:00 +0000
1325+++ templates/source_test.go 2013-02-06 17:03:19 +0000
1326@@ -0,0 +1,13 @@
1327+// Copyright 2013 Canonical Ltd. This software is licensed under the
1328+// GNU Lesser General Public License version 3 (see the file COPYING).
1329+
1330+package gomaasapi
1331+
1332+import (
1333+ . "launchpad.net/gocheck"
1334+)
1335+
1336+// TODO: Replace with real test functions. Give them real names.
1337+func (suite *GomaasapiTestSuite) TestXXX(c *C) {
1338+ c.Check(2+2, Equals, 4)
1339+}
1340
1341=== added file 'testing.go'
1342--- testing.go 1970-01-01 00:00:00 +0000
1343+++ testing.go 2013-02-06 17:03:19 +0000
1344@@ -0,0 +1,46 @@
1345+// Copyright 2013 Canonical Ltd. This software is licensed under the
1346+// GNU Lesser General Public License version 3 (see the file COPYING).
1347+
1348+package gomaasapi
1349+
1350+import (
1351+ "fmt"
1352+ "io/ioutil"
1353+ "net/http"
1354+ "net/http/httptest"
1355+)
1356+
1357+type singleServingServer struct {
1358+ *httptest.Server
1359+ requestContent *string
1360+ requestHeader *http.Header
1361+}
1362+
1363+// newSingleServingServer creates a single-serving test http server which will
1364+// return only one response as defined by the passed arguments.
1365+func newSingleServingServer(uri string, response string, code int) *singleServingServer {
1366+ var requestContent string
1367+ var requestHeader http.Header
1368+ var requested bool
1369+ handler := func(writer http.ResponseWriter, request *http.Request) {
1370+ if requested {
1371+ http.Error(writer, "Already requested", http.StatusServiceUnavailable)
1372+ }
1373+ res, err := ioutil.ReadAll(request.Body)
1374+ if err != nil {
1375+ panic(err)
1376+ }
1377+ requestContent = string(res)
1378+ requestHeader = request.Header
1379+ if request.URL.String() != uri {
1380+ errorMsg := fmt.Sprintf("Error 404: page not found (expected '%v', got '%v').", uri, request.URL.String())
1381+ http.Error(writer, errorMsg, http.StatusNotFound)
1382+ } else {
1383+ writer.WriteHeader(code)
1384+ fmt.Fprint(writer, response)
1385+ }
1386+ requested = true
1387+ }
1388+ server := httptest.NewServer(http.HandlerFunc(handler))
1389+ return &singleServingServer{server, &requestContent, &requestHeader}
1390+}
1391
1392=== added file 'testservice.go'
1393--- testservice.go 1970-01-01 00:00:00 +0000
1394+++ testservice.go 2013-02-06 17:03:19 +0000
1395@@ -0,0 +1,240 @@
1396+// Copyright 2013 Canonical Ltd. This software is licensed under the
1397+// GNU Lesser General Public License version 3 (see the file COPYING).
1398+
1399+package gomaasapi
1400+
1401+import (
1402+ "encoding/json"
1403+ "fmt"
1404+ "net/http"
1405+ "net/http/httptest"
1406+ "net/url"
1407+ "regexp"
1408+)
1409+
1410+// TestMAASObject is a fake MAAS server MAASObject.
1411+type TestMAASObject struct {
1412+ MAASObject
1413+ TestServer *TestServer
1414+}
1415+
1416+// TestMAASObject implements the MAASObject interface.
1417+var _ MAASObject = (*TestMAASObject)(nil)
1418+
1419+// NewTestMAAS returns a TestMAASObject that implements the MAASObject
1420+// interface and thus can be used as a test object instead of the one returned
1421+// by gomaasapi.NewMAAS().
1422+func NewTestMAAS(version string) *TestMAASObject {
1423+ server := NewTestServer(version)
1424+ authClient, _ := NewAnonymousClient(server.URL + fmt.Sprintf("/api/%s/", version))
1425+ return &TestMAASObject{NewMAAS(*authClient), server}
1426+}
1427+
1428+// Close shuts down the test server.
1429+func (testMAASObject *TestMAASObject) Close() {
1430+ testMAASObject.TestServer.Close()
1431+}
1432+
1433+// A TestServer is an HTTP server listening on a system-chosen port on the
1434+// local loopback interface, which simulates the behavior of a MAAS server.
1435+// It is intendend for use in end-to-end HTTP tests using the gomaasapi
1436+// library.
1437+type TestServer struct {
1438+ *httptest.Server
1439+ serveMux *http.ServeMux
1440+ nodes map[string]MAASObject
1441+ client Client
1442+ nodeOperations map[string][]string
1443+ version string
1444+}
1445+
1446+func getNodeURI(version, systemId string) string {
1447+ return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId)
1448+}
1449+
1450+// Clear clears all the fake data stored and recorded by the test server
1451+// (nodes, recorded operations, etc.).
1452+func (server *TestServer) Clear() {
1453+ server.nodes = make(map[string]MAASObject)
1454+ server.nodeOperations = make(map[string][]string)
1455+}
1456+
1457+// NodeOperations returns the map containing the list of the operations
1458+// performed for each node.
1459+func (server *TestServer) NodeOperations() map[string][]string {
1460+ return server.nodeOperations
1461+}
1462+
1463+func (server *TestServer) addNodeOperation(systemId, operation string) {
1464+ operations, present := server.nodeOperations[systemId]
1465+ if !present {
1466+ operations = []string{operation}
1467+ } else {
1468+ operations = append(operations, operation)
1469+ }
1470+ server.nodeOperations[systemId] = operations
1471+}
1472+
1473+// NewNode creates a MAAS node. The provided string should be a valid json
1474+// string representing a map and contain a string value for the key
1475+// 'system_id'. e.g. `{"system_id": "mysystemid"}`.
1476+// If one of these conditions is not met, NewNode panics.
1477+func (server *TestServer) NewNode(json string) MAASObject {
1478+ obj, err := Parse(server.client, []byte(json))
1479+ if err != nil {
1480+ panic(err)
1481+ }
1482+ mapobj, err := obj.GetMap()
1483+ if err != nil {
1484+ panic(err)
1485+ }
1486+ systemId, hasSystemId := mapobj["system_id"]
1487+ if !hasSystemId {
1488+ panic("The given map json string does not contain a 'system_id' value.")
1489+ }
1490+ stringSystemId, err := systemId.GetString()
1491+ if err != nil {
1492+ panic(err)
1493+ }
1494+ resourceUri := getNodeURI(server.version, stringSystemId)
1495+ mapobj[resourceURI] = jsonString(resourceUri)
1496+ maasobj := newJSONMAASObject(mapobj, server.client)
1497+ server.nodes[stringSystemId] = maasobj
1498+ return maasobj
1499+}
1500+
1501+// Returns a map associating all the nodes' system ids with the nodes'
1502+// objects.
1503+func (server *TestServer) Nodes() map[string]MAASObject {
1504+ return server.nodes
1505+}
1506+
1507+// ChangeNode updates a node with the given key/value.
1508+func (server *TestServer) ChangeNode(systemId, key, value string) {
1509+ node, found := server.nodes[systemId]
1510+ if !found {
1511+ panic("No node with such 'system_id'.")
1512+ }
1513+ mapObj, _ := node.GetMap()
1514+ mapObj[key] = jsonString(value)
1515+}
1516+
1517+func getNodeListingURL(version string) string {
1518+ return fmt.Sprintf("/api/%s/nodes/", version)
1519+}
1520+
1521+func getNodeURLRE(version string) *regexp.Regexp {
1522+ reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", version)
1523+ return regexp.MustCompile(reString)
1524+}
1525+
1526+// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down.
1527+func NewTestServer(version string) *TestServer {
1528+ server := &TestServer{version: version}
1529+
1530+ serveMux := http.NewServeMux()
1531+ nodeListingURL := getNodeListingURL(server.version)
1532+ // Register handler for '/api/<version>/nodes/*'.
1533+ serveMux.HandleFunc(nodeListingURL, func(w http.ResponseWriter, r *http.Request) {
1534+ nodesHandler(server, w, r)
1535+ })
1536+
1537+ newServer := httptest.NewServer(serveMux)
1538+ client, _ := NewAnonymousClient(newServer.URL)
1539+ server.Server = newServer
1540+ server.serveMux = serveMux
1541+ server.client = *client
1542+ server.Clear()
1543+ return server
1544+}
1545+
1546+// nodesHandler handles requests for '/api/<version>/nodes/*'.
1547+func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1548+ values, _ := url.ParseQuery(r.URL.RawQuery)
1549+ op := values.Get("op")
1550+ nodeURLRE := getNodeURLRE(server.version)
1551+ nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path)
1552+ nodeListingURL := getNodeListingURL(server.version)
1553+ switch {
1554+ case r.Method == "GET" && op == "list" && r.URL.Path == nodeListingURL:
1555+ // Node listing operation.
1556+ nodeListingHandler(server, w, r)
1557+ case nodeURLMatch != nil:
1558+ // Request for a single node.
1559+ nodeHandler(server, w, r, nodeURLMatch[1], op)
1560+ default:
1561+ // Default handler: not found.
1562+ http.NotFoundHandler().ServeHTTP(w, r)
1563+ }
1564+}
1565+
1566+func marshalNode(node MAASObject) string {
1567+ mapObj, _ := node.GetMap()
1568+ res, _ := json.Marshal(mapObj)
1569+ return string(res)
1570+
1571+}
1572+
1573+// nodeHandler handles requests for '/api/<version>/nodes/<system_id>/'.
1574+func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
1575+ node, ok := server.nodes[systemId]
1576+ if !ok {
1577+ http.NotFoundHandler().ServeHTTP(w, r)
1578+ return
1579+ }
1580+ if r.Method == "GET" {
1581+ if operation == "" {
1582+ w.WriteHeader(http.StatusOK)
1583+ fmt.Fprint(w, marshalNode(node))
1584+ return
1585+ } else {
1586+ w.WriteHeader(http.StatusBadRequest)
1587+ return
1588+ }
1589+ }
1590+ if r.Method == "POST" {
1591+ // The only operations supported are "start", "stop" and "release".
1592+ if operation == "start" || operation == "stop" || operation == "release" {
1593+ // Record operation on node.
1594+ server.addNodeOperation(systemId, operation)
1595+
1596+ w.WriteHeader(http.StatusOK)
1597+ fmt.Fprint(w, marshalNode(node))
1598+ return
1599+ } else {
1600+ w.WriteHeader(http.StatusBadRequest)
1601+ return
1602+ }
1603+ }
1604+ if r.Method == "DELETE" {
1605+ delete(server.nodes, systemId)
1606+ w.WriteHeader(http.StatusOK)
1607+ return
1608+ }
1609+ http.NotFoundHandler().ServeHTTP(w, r)
1610+}
1611+
1612+func contains(slice []string, val string) bool {
1613+ for _, item := range slice {
1614+ if item == val {
1615+ return true
1616+ }
1617+ }
1618+ return false
1619+}
1620+
1621+// nodeListingHandler handles requests for '/nodes/'.
1622+func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1623+ values, _ := url.ParseQuery(r.URL.RawQuery)
1624+ ids, hasId := values["id"]
1625+ var convertedNodes = []map[string]JSONObject{}
1626+ for systemId, node := range server.nodes {
1627+ if !hasId || contains(ids, systemId) {
1628+ mapp, _ := node.GetMap()
1629+ convertedNodes = append(convertedNodes, mapp)
1630+ }
1631+ }
1632+ res, _ := json.Marshal(convertedNodes)
1633+ w.WriteHeader(http.StatusOK)
1634+ fmt.Fprint(w, string(res))
1635+}
1636
1637=== added file 'testservice_test.go'
1638--- testservice_test.go 1970-01-01 00:00:00 +0000
1639+++ testservice_test.go 2013-02-06 17:03:19 +0000
1640@@ -0,0 +1,255 @@
1641+// Copyright 2013 Canonical Ltd. This software is licensed under the
1642+// GNU Lesser General Public License version 3 (see the file COPYING).
1643+
1644+package gomaasapi
1645+
1646+import (
1647+ "encoding/json"
1648+ "fmt"
1649+ . "launchpad.net/gocheck"
1650+ "net/http"
1651+ "net/url"
1652+)
1653+
1654+type TestServerSuite struct {
1655+ server *TestServer
1656+}
1657+
1658+var _ = Suite(&TestServerSuite{})
1659+
1660+func (suite *TestServerSuite) SetUpTest(c *C) {
1661+ server := NewTestServer("1.0")
1662+ suite.server = server
1663+}
1664+
1665+func (suite *TestServerSuite) TearDownTest(c *C) {
1666+ suite.server.Close()
1667+}
1668+
1669+func (suite *TestServerSuite) TestNewTestServerReturnsTestServer(c *C) {
1670+ handler := func(w http.ResponseWriter, r *http.Request) {
1671+ w.WriteHeader(http.StatusAccepted)
1672+ }
1673+ suite.server.serveMux.HandleFunc("/test/", handler)
1674+ resp, err := http.Get(suite.server.Server.URL + "/test/")
1675+
1676+ c.Check(err, IsNil)
1677+ c.Check(resp.StatusCode, Equals, http.StatusAccepted)
1678+}
1679+
1680+func (suite *TestServerSuite) TestGetResourceURI(c *C) {
1681+ c.Check(getNodeURI("version", "test"), Equals, "/api/version/nodes/test/")
1682+}
1683+
1684+func (suite *TestServerSuite) TestHandlesNodeListingUnknownPath(c *C) {
1685+ invalidPath := fmt.Sprintf("/api/%s/nodes/invalid/path/", suite.server.version)
1686+ resp, err := http.Get(suite.server.Server.URL + invalidPath)
1687+
1688+ c.Check(err, IsNil)
1689+ c.Check(resp.StatusCode, Equals, http.StatusNotFound)
1690+}
1691+
1692+func (suite *TestServerSuite) TestNewNode(c *C) {
1693+ input := `{"system_id": "mysystemid"}`
1694+
1695+ newNode := suite.server.NewNode(input)
1696+
1697+ c.Check(len(suite.server.nodes), Equals, 1)
1698+ c.Check(suite.server.nodes["mysystemid"], DeepEquals, newNode)
1699+}
1700+
1701+func (suite *TestServerSuite) TestNodesReturnsNodes(c *C) {
1702+ input := `{"system_id": "mysystemid"}`
1703+ newNode := suite.server.NewNode(input)
1704+
1705+ nodesMap := suite.server.Nodes()
1706+
1707+ c.Check(len(nodesMap), Equals, 1)
1708+ c.Check(nodesMap["mysystemid"], DeepEquals, newNode)
1709+}
1710+
1711+func (suite *TestServerSuite) TestChangeNode(c *C) {
1712+ input := `{"system_id": "mysystemid"}`
1713+ suite.server.NewNode(input)
1714+ suite.server.ChangeNode("mysystemid", "newfield", "newvalue")
1715+
1716+ node, _ := suite.server.nodes["mysystemid"]
1717+ mapObj, _ := node.GetMap()
1718+ field, _ := mapObj["newfield"].GetString()
1719+ c.Check(field, Equals, "newvalue")
1720+}
1721+
1722+func (suite *TestServerSuite) TestClearClearsData(c *C) {
1723+ input := `{"system_id": "mysystemid"}`
1724+ suite.server.NewNode(input)
1725+ suite.server.addNodeOperation("mysystemid", "start")
1726+
1727+ suite.server.Clear()
1728+
1729+ c.Check(len(suite.server.nodes), Equals, 0)
1730+ c.Check(len(suite.server.nodeOperations), Equals, 0)
1731+}
1732+
1733+func (suite *TestServerSuite) TestAddNodeOperationPopulatesOperations(c *C) {
1734+ input := `{"system_id": "mysystemid"}`
1735+ suite.server.NewNode(input)
1736+
1737+ suite.server.addNodeOperation("mysystemid", "start")
1738+ suite.server.addNodeOperation("mysystemid", "stop")
1739+
1740+ nodeOperations := suite.server.NodeOperations()
1741+ operations := nodeOperations["mysystemid"]
1742+ c.Check(operations, DeepEquals, []string{"start", "stop"})
1743+}
1744+
1745+func (suite *TestServerSuite) TestNewNodeRequiresJSONString(c *C) {
1746+ input := `invalid:json`
1747+ defer func() {
1748+ recoveredError := recover().(*json.SyntaxError)
1749+ c.Check(recoveredError, NotNil)
1750+ c.Check(recoveredError.Error(), Matches, ".*invalid character.*")
1751+ }()
1752+ suite.server.NewNode(input)
1753+}
1754+
1755+func (suite *TestServerSuite) TestNewNodeRequiresSystemIdKey(c *C) {
1756+ input := `{"test": "test"}`
1757+ defer func() {
1758+ recoveredError := recover()
1759+ c.Check(recoveredError, NotNil)
1760+ c.Check(recoveredError, Matches, ".*does not contain a 'system_id' value.")
1761+ }()
1762+ suite.server.NewNode(input)
1763+}
1764+
1765+func (suite *TestServerSuite) TestHandlesNodeRequestNotFound(c *C) {
1766+ getURI := fmt.Sprintf("/api/%s/nodes/test/", suite.server.version)
1767+ resp, err := http.Get(suite.server.Server.URL + getURI)
1768+
1769+ c.Check(err, IsNil)
1770+ c.Check(resp.StatusCode, Equals, http.StatusNotFound)
1771+}
1772+
1773+func (suite *TestServerSuite) TestHandlesNodeUnknownOperation(c *C) {
1774+ input := `{"system_id": "mysystemid"}`
1775+ suite.server.NewNode(input)
1776+ postURI := fmt.Sprintf("/api/%s/nodes/mysystemid/?op=unknown/", suite.server.version)
1777+ respStart, err := http.Post(suite.server.Server.URL+postURI, "", nil)
1778+
1779+ c.Check(err, IsNil)
1780+ c.Check(respStart.StatusCode, Equals, http.StatusBadRequest)
1781+}
1782+
1783+func (suite *TestServerSuite) TestHandlesNodeDelete(c *C) {
1784+ input := `{"system_id": "mysystemid"}`
1785+ suite.server.NewNode(input)
1786+ deleteURI := fmt.Sprintf("/api/%s/nodes/mysystemid/?op=mysystemid", suite.server.version)
1787+ req, err := http.NewRequest("DELETE", suite.server.Server.URL+deleteURI, nil)
1788+ client := &http.Client{}
1789+ resp, err := client.Do(req)
1790+
1791+ c.Check(err, IsNil)
1792+ c.Check(resp.StatusCode, Equals, http.StatusOK)
1793+ c.Check(len(suite.server.nodes), Equals, 0)
1794+}
1795+
1796+// TestMAASObjectSuite validates that the object created by
1797+// TestMAASObject can be used by the gomaasapi library as if it were a real
1798+// MAAS server.
1799+type TestMAASObjectSuite struct {
1800+ TestMAASObject *TestMAASObject
1801+}
1802+
1803+var _ = Suite(&TestMAASObjectSuite{})
1804+
1805+func (s *TestMAASObjectSuite) SetUpSuite(c *C) {
1806+ s.TestMAASObject = NewTestMAAS("1.0")
1807+}
1808+
1809+func (s *TestMAASObjectSuite) TearDownSuite(c *C) {
1810+ s.TestMAASObject.Close()
1811+}
1812+
1813+func (s *TestMAASObjectSuite) TearDownTest(c *C) {
1814+ s.TestMAASObject.TestServer.Clear()
1815+}
1816+
1817+func (suite *TestMAASObjectSuite) TestListNodes(c *C) {
1818+ input := `{"system_id": "mysystemid"}`
1819+ suite.TestMAASObject.TestServer.NewNode(input)
1820+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
1821+
1822+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
1823+
1824+ c.Check(err, IsNil)
1825+ listNodes, err := listNodeObjects.GetArray()
1826+ c.Check(err, IsNil)
1827+ c.Check(len(listNodes), Equals, 1)
1828+ node, _ := listNodes[0].GetMAASObject()
1829+ systemId, _ := node.GetField("system_id")
1830+ c.Check(systemId, Equals, "mysystemid")
1831+ resourceURI, _ := node.GetField(resourceURI)
1832+ expectedResourceURI := fmt.Sprintf("/api/%s/nodes/mysystemid/", suite.TestMAASObject.TestServer.version)
1833+ c.Check(resourceURI, Equals, expectedResourceURI)
1834+}
1835+
1836+func (suite *TestMAASObjectSuite) TestListNodesNoNodes(c *C) {
1837+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
1838+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{})
1839+ c.Check(err, IsNil)
1840+
1841+ listNodes, err := listNodeObjects.GetArray()
1842+
1843+ c.Check(err, IsNil)
1844+ c.Check(listNodes, DeepEquals, []JSONObject{})
1845+}
1846+
1847+func (suite *TestMAASObjectSuite) TestListNodesSelectedNodes(c *C) {
1848+ input := `{"system_id": "mysystemid"}`
1849+ suite.TestMAASObject.TestServer.NewNode(input)
1850+ input2 := `{"system_id": "mysystemid2"}`
1851+ suite.TestMAASObject.TestServer.NewNode(input2)
1852+ nodeListing := suite.TestMAASObject.GetSubObject("nodes")
1853+
1854+ listNodeObjects, err := nodeListing.CallGet("list", url.Values{"id": {"mysystemid2"}})
1855+
1856+ c.Check(err, IsNil)
1857+ listNodes, err := listNodeObjects.GetArray()
1858+ c.Check(err, IsNil)
1859+ c.Check(len(listNodes), Equals, 1)
1860+ node, _ := listNodes[0].GetMAASObject()
1861+ systemId, _ := node.GetField("system_id")
1862+ c.Check(systemId, Equals, "mysystemid2")
1863+}
1864+
1865+func (suite *TestMAASObjectSuite) TestDeleteNode(c *C) {
1866+ input := `{"system_id": "mysystemid"}`
1867+ node := suite.TestMAASObject.TestServer.NewNode(input)
1868+
1869+ err := node.Delete()
1870+
1871+ c.Check(err, IsNil)
1872+ c.Check(suite.TestMAASObject.TestServer.Nodes(), DeepEquals, map[string]MAASObject{})
1873+}
1874+
1875+func (suite *TestMAASObjectSuite) TestOperationsOnNode(c *C) {
1876+ input := `{"system_id": "mysystemid"}`
1877+ node := suite.TestMAASObject.TestServer.NewNode(input)
1878+ operations := []string{"start", "stop", "release"}
1879+ for _, operation := range operations {
1880+ _, err := node.CallPost(operation, url.Values{})
1881+ c.Check(err, IsNil)
1882+ }
1883+}
1884+
1885+func (suite *TestMAASObjectSuite) TestOperationsOnNodeGetRecorded(c *C) {
1886+ input := `{"system_id": "mysystemid"}`
1887+ node := suite.TestMAASObject.TestServer.NewNode(input)
1888+
1889+ _, err := node.CallPost("start", url.Values{})
1890+
1891+ c.Check(err, IsNil)
1892+ nodeOperations := suite.TestMAASObject.TestServer.NodeOperations()
1893+ operations := nodeOperations["mysystemid"]
1894+ c.Check(operations, DeepEquals, []string{"start"})
1895+}
1896
1897=== added file 'util.go'
1898--- util.go 1970-01-01 00:00:00 +0000
1899+++ util.go 2013-02-06 17:03:19 +0000
1900@@ -0,0 +1,27 @@
1901+// Copyright 2013 Canonical Ltd. This software is licensed under the
1902+// GNU Lesser General Public License version 3 (see the file COPYING).
1903+
1904+package gomaasapi
1905+
1906+import (
1907+ "strings"
1908+)
1909+
1910+// JoinURLs joins a base URL and a subpath together.
1911+// Regardless of whether baseURL ends in a trailing slash (or even multiple
1912+// trailing slashes), or whether there are any leading slashes at the begining
1913+// of path, the two will always be joined together by a single slash.
1914+func JoinURLs(baseURL, path string) string {
1915+ return strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(path, "/")
1916+}
1917+
1918+// EnsureTrailingSlash appends a slash at the end of the given string unless
1919+// there already is one.
1920+// This is used to create the kind of normalized URLs that Django expects.
1921+// (to avoid Django's redirection when an URL does not ends with a slash.)
1922+func EnsureTrailingSlash(URL string) string {
1923+ if strings.HasSuffix(URL, "/") {
1924+ return URL
1925+ }
1926+ return URL + "/"
1927+}
1928
1929=== added file 'util_test.go'
1930--- util_test.go 1970-01-01 00:00:00 +0000
1931+++ util_test.go 2013-02-06 17:03:19 +0000
1932@@ -0,0 +1,32 @@
1933+// Copyright 2013 Canonical Ltd. This software is licensed under the
1934+// GNU Lesser General Public License version 3 (see the file COPYING).
1935+
1936+package gomaasapi
1937+
1938+import (
1939+ . "launchpad.net/gocheck"
1940+)
1941+
1942+func (suite *GomaasapiTestSuite) TestJoinURLsAppendsPathToBaseURL(c *C) {
1943+ c.Check(JoinURLs("http://example.com/", "foo"), Equals, "http://example.com/foo")
1944+}
1945+
1946+func (suite *GomaasapiTestSuite) TestJoinURLsAddsSlashIfNeeded(c *C) {
1947+ c.Check(JoinURLs("http://example.com/foo", "bar"), Equals, "http://example.com/foo/bar")
1948+}
1949+
1950+func (suite *GomaasapiTestSuite) TestJoinURLsNormalizesDoubleSlash(c *C) {
1951+ c.Check(JoinURLs("http://example.com/base/", "/szot"), Equals, "http://example.com/base/szot")
1952+}
1953+
1954+func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashAppendsSlashIfMissing(c *C) {
1955+ c.Check(EnsureTrailingSlash("test"), Equals, "test/")
1956+}
1957+
1958+func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashDoesNotAppendIfPresent(c *C) {
1959+ c.Check(EnsureTrailingSlash("test/"), Equals, "test/")
1960+}
1961+
1962+func (suite *GomaasapiTestSuite) TestEnsureTrailingSlashReturnsSlashIfEmpty(c *C) {
1963+ c.Check(EnsureTrailingSlash(""), Equals, "/")
1964+}

Subscribers

People subscribed via source and target branches