Merge lp:~mfoord/gomaasapi/devices into lp:gomaasapi
- devices
- Merge into trunk
Proposed by
Michael Foord
Status: | Merged |
---|---|
Approved by: | Michael Foord |
Approved revision: | 95 |
Merged at revision: | 63 |
Proposed branch: | lp:~mfoord/gomaasapi/devices |
Merge into: | lp:gomaasapi |
Diff against target: |
559 lines (+478/-5) 2 files modified
testservice.go (+250/-5) testservice_test.go (+228/-0) |
To merge this branch: | bzr merge lp:~mfoord/gomaasapi/devices |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Dimiter Naydenov (community) | Approve | ||
Review via email: mp+263370@code.launchpad.net |
Commit message
Adding devices support to gomaasapi test server.
Description of the change
Adding devices support to gomaasapi test server.
To post a comment you must log in.
lp:~mfoord/gomaasapi/devices
updated
- 95. By Michael Foord
-
Updated comments
Revision history for this message
Michael Foord (mfoord) wrote : | # |
> LGTM in general, apart from the one comment inline below, if I wish we had
> logging in places where StatusBadRequest is returned to explain the reason and
> make the test server a bit easier to use.
>
> Also, as discussed on IRC we need support for op=claim_sticky_ip (or was it
> claim-sticky-ip?).
There's no logging in gomaasapi I'm afraid. The rest is now done.
Revision history for this message
Dimiter Naydenov (dimitern) wrote : | # |
Looks good to land, thanks!
review:
Approve
Revision history for this message
Raphaël Badin (rvb) wrote : | # |
Looks good to me as well… lots of TODOs in there but I understand creating a test double is costly and you're only implementing what you need right now.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'testservice.go' | |||
2 | --- testservice.go 2015-06-02 02:28:04 +0000 | |||
3 | +++ testservice.go 2015-07-02 16:26:32 +0000 | |||
4 | @@ -5,6 +5,7 @@ | |||
5 | 5 | 5 | ||
6 | 6 | import ( | 6 | import ( |
7 | 7 | "bufio" | 7 | "bufio" |
8 | 8 | "bytes" | ||
9 | 8 | "encoding/base64" | 9 | "encoding/base64" |
10 | 9 | "encoding/json" | 10 | "encoding/json" |
11 | 10 | "fmt" | 11 | "fmt" |
12 | @@ -18,6 +19,7 @@ | |||
13 | 18 | "sort" | 19 | "sort" |
14 | 19 | "strconv" | 20 | "strconv" |
15 | 20 | "strings" | 21 | "strings" |
16 | 22 | "text/template" | ||
17 | 21 | "time" | 23 | "time" |
18 | 22 | 24 | ||
19 | 23 | "gopkg.in/mgo.v2/bson" | 25 | "gopkg.in/mgo.v2/bson" |
20 | @@ -85,6 +87,24 @@ | |||
21 | 85 | // nodegroupsInterfaces is a map of nodegroup UUIDs to interface | 87 | // nodegroupsInterfaces is a map of nodegroup UUIDs to interface |
22 | 86 | // objects. | 88 | // objects. |
23 | 87 | nodegroupsInterfaces map[string][]JSONObject | 89 | nodegroupsInterfaces map[string][]JSONObject |
24 | 90 | |||
25 | 91 | // versionJSON is the response to the /version/ endpoint listing the | ||
26 | 92 | // capabilities of the MAAS server. | ||
27 | 93 | versionJSON string | ||
28 | 94 | |||
29 | 95 | // devices is a map of device UUIDs to devices. | ||
30 | 96 | devices map[string]*device | ||
31 | 97 | } | ||
32 | 98 | |||
33 | 99 | type device struct { | ||
34 | 100 | IPAddresses []string | ||
35 | 101 | SystemId string | ||
36 | 102 | MACAddress string | ||
37 | 103 | Parent string | ||
38 | 104 | Hostname string | ||
39 | 105 | |||
40 | 106 | // Not part of the device definition but used by the template. | ||
41 | 107 | APIVersion string | ||
42 | 88 | } | 108 | } |
43 | 89 | 109 | ||
44 | 90 | func getNodesEndpoint(version string) string { | 110 | func getNodesEndpoint(version string) string { |
45 | @@ -100,6 +120,19 @@ | |||
46 | 100 | return regexp.MustCompile(reString) | 120 | return regexp.MustCompile(reString) |
47 | 101 | } | 121 | } |
48 | 102 | 122 | ||
49 | 123 | func getDevicesEndpoint(version string) string { | ||
50 | 124 | return fmt.Sprintf("/api/%s/devices/", version) | ||
51 | 125 | } | ||
52 | 126 | |||
53 | 127 | func getDeviceURL(version, systemId string) string { | ||
54 | 128 | return fmt.Sprintf("/api/%s/devices/%s/", version, systemId) | ||
55 | 129 | } | ||
56 | 130 | |||
57 | 131 | func getDeviceURLRE(version string) *regexp.Regexp { | ||
58 | 132 | reString := fmt.Sprintf("^/api/%s/devices/([^/]*)/$", regexp.QuoteMeta(version)) | ||
59 | 133 | return regexp.MustCompile(reString) | ||
60 | 134 | } | ||
61 | 135 | |||
62 | 103 | func getFilesEndpoint(version string) string { | 136 | func getFilesEndpoint(version string) string { |
63 | 104 | return fmt.Sprintf("/api/%s/files/", version) | 137 | return fmt.Sprintf("/api/%s/files/", version) |
64 | 105 | } | 138 | } |
65 | @@ -141,10 +174,6 @@ | |||
66 | 141 | return fmt.Sprintf("/api/%s/version/", version) | 174 | return fmt.Sprintf("/api/%s/version/", version) |
67 | 142 | } | 175 | } |
68 | 143 | 176 | ||
69 | 144 | func getVersionJSON() string { | ||
70 | 145 | return `{"capabilities": ["networks-management","static-ipaddresses"]}` | ||
71 | 146 | } | ||
72 | 147 | |||
73 | 148 | func getNodegroupsEndpoint(version string) string { | 177 | func getNodegroupsEndpoint(version string) string { |
74 | 149 | return fmt.Sprintf("/api/%s/nodegroups/", version) | 178 | return fmt.Sprintf("/api/%s/nodegroups/", version) |
75 | 150 | } | 179 | } |
76 | @@ -185,6 +214,14 @@ | |||
77 | 185 | server.bootImages = make(map[string][]JSONObject) | 214 | server.bootImages = make(map[string][]JSONObject) |
78 | 186 | server.nodegroupsInterfaces = make(map[string][]JSONObject) | 215 | server.nodegroupsInterfaces = make(map[string][]JSONObject) |
79 | 187 | server.zones = make(map[string]JSONObject) | 216 | server.zones = make(map[string]JSONObject) |
80 | 217 | server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses"]}` | ||
81 | 218 | server.devices = make(map[string]*device) | ||
82 | 219 | } | ||
83 | 220 | |||
84 | 221 | // SetVersionJSON sets the JSON response (capabilities) returned from the | ||
85 | 222 | // /version/ endpoint. | ||
86 | 223 | func (server *TestServer) SetVersionJSON(json string) { | ||
87 | 224 | server.versionJSON = json | ||
88 | 188 | } | 225 | } |
89 | 189 | 226 | ||
90 | 190 | // NodesOperations returns the list of operations performed at the /nodes/ | 227 | // NodesOperations returns the list of operations performed at the /nodes/ |
91 | @@ -466,6 +503,11 @@ | |||
92 | 466 | server := &TestServer{version: version} | 503 | server := &TestServer{version: version} |
93 | 467 | 504 | ||
94 | 468 | serveMux := http.NewServeMux() | 505 | serveMux := http.NewServeMux() |
95 | 506 | devicesURL := getDevicesEndpoint(server.version) | ||
96 | 507 | // Register handler for '/api/<version>/devices/*'. | ||
97 | 508 | serveMux.HandleFunc(devicesURL, func(w http.ResponseWriter, r *http.Request) { | ||
98 | 509 | devicesHandler(server, w, r) | ||
99 | 510 | }) | ||
100 | 469 | nodesURL := getNodesEndpoint(server.version) | 511 | nodesURL := getNodesEndpoint(server.version) |
101 | 470 | // Register handler for '/api/<version>/nodes/*'. | 512 | // Register handler for '/api/<version>/nodes/*'. |
102 | 471 | serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) { | 513 | serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) { |
103 | @@ -513,6 +555,209 @@ | |||
104 | 513 | return server | 555 | return server |
105 | 514 | } | 556 | } |
106 | 515 | 557 | ||
107 | 558 | // devicesHandler handles requests for '/api/<version>/devices/*'. | ||
108 | 559 | func devicesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { | ||
109 | 560 | values, err := url.ParseQuery(r.URL.RawQuery) | ||
110 | 561 | checkError(err) | ||
111 | 562 | op := values.Get("op") | ||
112 | 563 | deviceURLRE := getDeviceURLRE(server.version) | ||
113 | 564 | deviceURLMatch := deviceURLRE.FindStringSubmatch(r.URL.Path) | ||
114 | 565 | devicesURL := getDevicesEndpoint(server.version) | ||
115 | 566 | switch { | ||
116 | 567 | case r.URL.Path == devicesURL: | ||
117 | 568 | devicesTopLevelHandler(server, w, r, op) | ||
118 | 569 | case deviceURLMatch != nil: | ||
119 | 570 | // Request for a single device. | ||
120 | 571 | deviceHandler(server, w, r, deviceURLMatch[1], op) | ||
121 | 572 | default: | ||
122 | 573 | // Default handler: not found. | ||
123 | 574 | http.NotFoundHandler().ServeHTTP(w, r) | ||
124 | 575 | } | ||
125 | 576 | } | ||
126 | 577 | |||
127 | 578 | // devicesTopLevelHandler handles a request for /api/<version>/devices/ | ||
128 | 579 | // (with no device id following as part of the path). | ||
129 | 580 | func devicesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) { | ||
130 | 581 | switch { | ||
131 | 582 | case r.Method == "GET" && op == "list": | ||
132 | 583 | // Device listing operation. | ||
133 | 584 | deviceListingHandler(server, w, r) | ||
134 | 585 | case r.Method == "POST" && op == "new": | ||
135 | 586 | newDeviceHandler(server, w, r) | ||
136 | 587 | default: | ||
137 | 588 | w.WriteHeader(http.StatusBadRequest) | ||
138 | 589 | } | ||
139 | 590 | } | ||
140 | 591 | |||
141 | 592 | func macMatches(device *device, macs []string, hasMac bool) bool { | ||
142 | 593 | if !hasMac { | ||
143 | 594 | return true | ||
144 | 595 | } | ||
145 | 596 | return contains(macs, device.MACAddress) | ||
146 | 597 | } | ||
147 | 598 | |||
148 | 599 | // deviceListingHandler handles requests for '/devices/'. | ||
149 | 600 | func deviceListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { | ||
150 | 601 | values, err := url.ParseQuery(r.URL.RawQuery) | ||
151 | 602 | checkError(err) | ||
152 | 603 | // TODO(mfoord): support filtering by hostname and id | ||
153 | 604 | macs, hasMac := values["mac_address"] | ||
154 | 605 | var matchedDevices []string | ||
155 | 606 | for _, device := range server.devices { | ||
156 | 607 | if macMatches(device, macs, hasMac) { | ||
157 | 608 | matchedDevices = append(matchedDevices, renderDevice(device)) | ||
158 | 609 | } | ||
159 | 610 | } | ||
160 | 611 | json := fmt.Sprintf("[%v]", strings.Join(matchedDevices, ", ")) | ||
161 | 612 | |||
162 | 613 | w.WriteHeader(http.StatusOK) | ||
163 | 614 | fmt.Fprint(w, json) | ||
164 | 615 | } | ||
165 | 616 | |||
166 | 617 | var templateFuncs = template.FuncMap{ | ||
167 | 618 | "quotedList": func(items []string) string { | ||
168 | 619 | var pieces []string | ||
169 | 620 | for _, item := range items { | ||
170 | 621 | pieces = append(pieces, fmt.Sprintf("%q", item)) | ||
171 | 622 | } | ||
172 | 623 | return strings.Join(pieces, ", ") | ||
173 | 624 | }, | ||
174 | 625 | } | ||
175 | 626 | |||
176 | 627 | const ( | ||
177 | 628 | // The json template for generating new devices. | ||
178 | 629 | // TODO(mfoord): set resource_uri in MAC addresses | ||
179 | 630 | deviceTemplate = `{ | ||
180 | 631 | "macaddress_set": [ | ||
181 | 632 | { | ||
182 | 633 | "mac_address": "{{.MACAddress}}" | ||
183 | 634 | } | ||
184 | 635 | ], | ||
185 | 636 | "zone": { | ||
186 | 637 | "resource_uri": "/MAAS/api/{{.APIVersion}}/zones/default/", | ||
187 | 638 | "name": "default", | ||
188 | 639 | "description": "" | ||
189 | 640 | }, | ||
190 | 641 | "parent": "{{.Parent}}", | ||
191 | 642 | "ip_addresses": [{{.IPAddresses | quotedList }}], | ||
192 | 643 | "hostname": "{{.Hostname}}", | ||
193 | 644 | "tag_names": [], | ||
194 | 645 | "owner": "maas-admin", | ||
195 | 646 | "system_id": "{{.SystemId}}", | ||
196 | 647 | "resource_uri": "/MAAS/api/{{.APIVersion}}/devices/{{.SystemId}}/" | ||
197 | 648 | }` | ||
198 | 649 | ) | ||
199 | 650 | |||
200 | 651 | func renderDevice(device *device) string { | ||
201 | 652 | t := template.New("Device template") | ||
202 | 653 | t = t.Funcs(templateFuncs) | ||
203 | 654 | t, err := t.Parse(deviceTemplate) | ||
204 | 655 | checkError(err) | ||
205 | 656 | var buf bytes.Buffer | ||
206 | 657 | err = t.Execute(&buf, device) | ||
207 | 658 | checkError(err) | ||
208 | 659 | return buf.String() | ||
209 | 660 | } | ||
210 | 661 | |||
211 | 662 | func getValue(values url.Values, value string) (string, bool) { | ||
212 | 663 | result, hasResult := values[value] | ||
213 | 664 | if !hasResult || len(result) != 1 || result[0] == "" { | ||
214 | 665 | return "", false | ||
215 | 666 | } | ||
216 | 667 | return result[0], true | ||
217 | 668 | } | ||
218 | 669 | |||
219 | 670 | // newDeviceHandler creates, stores and returns new devices. | ||
220 | 671 | func newDeviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { | ||
221 | 672 | err := r.ParseForm() | ||
222 | 673 | checkError(err) | ||
223 | 674 | values := r.PostForm | ||
224 | 675 | |||
225 | 676 | // TODO(mfood): generate a "proper" uuid for the system Id. | ||
226 | 677 | uuid, err := generateNonce() | ||
227 | 678 | checkError(err) | ||
228 | 679 | systemId := fmt.Sprintf("node-%v", uuid) | ||
229 | 680 | // At least one MAC address must be specified. | ||
230 | 681 | // TODO(mfoord) we only support a single MAC in the test server. | ||
231 | 682 | mac, hasMac := getValue(values, "mac_addresses") | ||
232 | 683 | |||
233 | 684 | // hostname and parent are optional. | ||
234 | 685 | // TODO(mfoord): we require both to be set in the test server. | ||
235 | 686 | hostname, hasHostname := getValue(values, "hostname") | ||
236 | 687 | parent, hasParent := getValue(values, "parent") | ||
237 | 688 | if !hasHostname || !hasMac || !hasParent { | ||
238 | 689 | w.WriteHeader(http.StatusBadRequest) | ||
239 | 690 | return | ||
240 | 691 | } | ||
241 | 692 | |||
242 | 693 | device := &device{ | ||
243 | 694 | MACAddress: mac, | ||
244 | 695 | APIVersion: server.version, | ||
245 | 696 | Parent: parent, | ||
246 | 697 | Hostname: hostname, | ||
247 | 698 | SystemId: systemId, | ||
248 | 699 | } | ||
249 | 700 | |||
250 | 701 | deviceJSON := renderDevice(device) | ||
251 | 702 | server.devices[systemId] = device | ||
252 | 703 | |||
253 | 704 | w.WriteHeader(http.StatusOK) | ||
254 | 705 | fmt.Fprint(w, deviceJSON) | ||
255 | 706 | return | ||
256 | 707 | } | ||
257 | 708 | |||
258 | 709 | // deviceHandler handles requests for '/api/<version>/devices/<system_id>/'. | ||
259 | 710 | func deviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) { | ||
260 | 711 | device, ok := server.devices[systemId] | ||
261 | 712 | if !ok { | ||
262 | 713 | http.NotFoundHandler().ServeHTTP(w, r) | ||
263 | 714 | return | ||
264 | 715 | } | ||
265 | 716 | if r.Method == "GET" { | ||
266 | 717 | deviceJSON := renderDevice(device) | ||
267 | 718 | if operation == "" { | ||
268 | 719 | w.WriteHeader(http.StatusOK) | ||
269 | 720 | fmt.Fprint(w, deviceJSON) | ||
270 | 721 | return | ||
271 | 722 | } else { | ||
272 | 723 | w.WriteHeader(http.StatusBadRequest) | ||
273 | 724 | return | ||
274 | 725 | } | ||
275 | 726 | } | ||
276 | 727 | if r.Method == "POST" { | ||
277 | 728 | if operation == "claim_sticky_ip_address" { | ||
278 | 729 | err := r.ParseForm() | ||
279 | 730 | checkError(err) | ||
280 | 731 | values := r.PostForm | ||
281 | 732 | // TODO(mfoord): support optional mac_address parameter | ||
282 | 733 | // TODO(mfoord): requested_address should be optional | ||
283 | 734 | // and we should generate one if it isn't provided. | ||
284 | 735 | address, hasAddress := getValue(values, "requested_address") | ||
285 | 736 | if !hasAddress { | ||
286 | 737 | w.WriteHeader(http.StatusBadRequest) | ||
287 | 738 | return | ||
288 | 739 | } | ||
289 | 740 | checkError(err) | ||
290 | 741 | device.IPAddresses = append(device.IPAddresses, address) | ||
291 | 742 | deviceJSON := renderDevice(device) | ||
292 | 743 | w.WriteHeader(http.StatusOK) | ||
293 | 744 | fmt.Fprint(w, deviceJSON) | ||
294 | 745 | return | ||
295 | 746 | } else { | ||
296 | 747 | w.WriteHeader(http.StatusBadRequest) | ||
297 | 748 | return | ||
298 | 749 | } | ||
299 | 750 | } else if r.Method == "DELETE" { | ||
300 | 751 | delete(server.devices, systemId) | ||
301 | 752 | w.WriteHeader(http.StatusNoContent) | ||
302 | 753 | return | ||
303 | 754 | |||
304 | 755 | } | ||
305 | 756 | |||
306 | 757 | // TODO(mfoord): support PUT method for updating device | ||
307 | 758 | http.NotFoundHandler().ServeHTTP(w, r) | ||
308 | 759 | } | ||
309 | 760 | |||
310 | 516 | // nodesHandler handles requests for '/api/<version>/nodes/*'. | 761 | // nodesHandler handles requests for '/api/<version>/nodes/*'. |
311 | 517 | func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { | 762 | func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) { |
312 | 518 | values, err := url.ParseQuery(r.URL.RawQuery) | 763 | values, err := url.ParseQuery(r.URL.RawQuery) |
313 | @@ -1188,7 +1433,7 @@ | |||
314 | 1188 | } | 1433 | } |
315 | 1189 | w.Header().Set("Content-Type", "application/json; charset=utf-8") | 1434 | w.Header().Set("Content-Type", "application/json; charset=utf-8") |
316 | 1190 | w.WriteHeader(http.StatusOK) | 1435 | w.WriteHeader(http.StatusOK) |
318 | 1191 | fmt.Fprint(w, getVersionJSON()) | 1436 | fmt.Fprint(w, server.versionJSON) |
319 | 1192 | } | 1437 | } |
320 | 1193 | 1438 | ||
321 | 1194 | // nodegroupsHandler handles requests for '/api/<version>/nodegroups/*'. | 1439 | // nodegroupsHandler handles requests for '/api/<version>/nodegroups/*'. |
322 | 1195 | 1440 | ||
323 | === modified file 'testservice_test.go' | |||
324 | --- testservice_test.go 2015-01-13 03:11:30 +0000 | |||
325 | +++ testservice_test.go 2015-07-02 16:26:32 +0000 | |||
326 | @@ -49,6 +49,234 @@ | |||
327 | 49 | c.Check(getNodeURL("0.1", "test"), Equals, "/api/0.1/nodes/test/") | 49 | c.Check(getNodeURL("0.1", "test"), Equals, "/api/0.1/nodes/test/") |
328 | 50 | } | 50 | } |
329 | 51 | 51 | ||
330 | 52 | func (suite *TestServerSuite) TestSetVersionJSON(c *C) { | ||
331 | 53 | capabilities := `{"capabilities": ["networks-management","static-ipaddresses", "devices-management"]}` | ||
332 | 54 | suite.server.SetVersionJSON(capabilities) | ||
333 | 55 | |||
334 | 56 | url := fmt.Sprintf("/api/%s/version/", suite.server.version) | ||
335 | 57 | resp, err := http.Get(suite.server.Server.URL + url) | ||
336 | 58 | c.Assert(err, IsNil) | ||
337 | 59 | c.Check(resp.StatusCode, Equals, http.StatusOK) | ||
338 | 60 | content, err := readAndClose(resp.Body) | ||
339 | 61 | c.Assert(err, IsNil) | ||
340 | 62 | c.Assert(string(content), Equals, capabilities) | ||
341 | 63 | } | ||
342 | 64 | |||
343 | 65 | func (suite *TestServerSuite) createDevice(c *C, mac, hostname, parent string) string { | ||
344 | 66 | devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=new" | ||
345 | 67 | values := url.Values{} | ||
346 | 68 | values.Add("mac_addresses", mac) | ||
347 | 69 | values.Add("hostname", hostname) | ||
348 | 70 | values.Add("parent", parent) | ||
349 | 71 | result := suite.post(c, devicesURL, values) | ||
350 | 72 | resultMap, err := result.GetMap() | ||
351 | 73 | c.Assert(err, IsNil) | ||
352 | 74 | systemId, err := resultMap["system_id"].GetString() | ||
353 | 75 | c.Assert(err, IsNil) | ||
354 | 76 | return systemId | ||
355 | 77 | } | ||
356 | 78 | |||
357 | 79 | func getString(c *C, object map[string]JSONObject, key string) string { | ||
358 | 80 | value, err := object[key].GetString() | ||
359 | 81 | c.Assert(err, IsNil) | ||
360 | 82 | return value | ||
361 | 83 | } | ||
362 | 84 | |||
363 | 85 | func (suite *TestServerSuite) post(c *C, url string, values url.Values) JSONObject { | ||
364 | 86 | resp, err := http.Post(suite.server.Server.URL+url, "application/x-www-form-urlencoded", strings.NewReader(values.Encode())) | ||
365 | 87 | c.Assert(err, IsNil) | ||
366 | 88 | c.Check(resp.StatusCode, Equals, http.StatusOK) | ||
367 | 89 | content, err := readAndClose(resp.Body) | ||
368 | 90 | c.Assert(err, IsNil) | ||
369 | 91 | result, err := Parse(suite.server.client, content) | ||
370 | 92 | c.Assert(err, IsNil) | ||
371 | 93 | return result | ||
372 | 94 | } | ||
373 | 95 | |||
374 | 96 | func (suite *TestServerSuite) get(c *C, url string) JSONObject { | ||
375 | 97 | resp, err := http.Get(suite.server.Server.URL + url) | ||
376 | 98 | c.Assert(err, IsNil) | ||
377 | 99 | c.Assert(resp.StatusCode, Equals, http.StatusOK) | ||
378 | 100 | |||
379 | 101 | content, err := readAndClose(resp.Body) | ||
380 | 102 | c.Assert(err, IsNil) | ||
381 | 103 | |||
382 | 104 | result, err := Parse(suite.server.client, content) | ||
383 | 105 | c.Assert(err, IsNil) | ||
384 | 106 | return result | ||
385 | 107 | } | ||
386 | 108 | |||
387 | 109 | func checkDevice(c *C, device map[string]JSONObject, mac, hostname, parent string) { | ||
388 | 110 | macArray, err := device["macaddress_set"].GetArray() | ||
389 | 111 | c.Assert(err, IsNil) | ||
390 | 112 | c.Assert(macArray, HasLen, 1) | ||
391 | 113 | macMap, err := macArray[0].GetMap() | ||
392 | 114 | c.Assert(err, IsNil) | ||
393 | 115 | |||
394 | 116 | actualMac := getString(c, macMap, "mac_address") | ||
395 | 117 | c.Assert(actualMac, Equals, mac) | ||
396 | 118 | |||
397 | 119 | actualParent := getString(c, device, "parent") | ||
398 | 120 | c.Assert(actualParent, Equals, parent) | ||
399 | 121 | actualHostname := getString(c, device, "hostname") | ||
400 | 122 | c.Assert(actualHostname, Equals, hostname) | ||
401 | 123 | } | ||
402 | 124 | |||
403 | 125 | func (suite *TestServerSuite) TestNewDeviceRequiredParameters(c *C) { | ||
404 | 126 | devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=new" | ||
405 | 127 | values := url.Values{} | ||
406 | 128 | values.Add("mac_addresses", "foo") | ||
407 | 129 | values.Add("hostname", "bar") | ||
408 | 130 | post := func(values url.Values) int { | ||
409 | 131 | resp, err := http.Post(suite.server.Server.URL+devicesURL, "application/x-www-form-urlencoded", strings.NewReader(values.Encode())) | ||
410 | 132 | c.Assert(err, IsNil) | ||
411 | 133 | return resp.StatusCode | ||
412 | 134 | } | ||
413 | 135 | c.Check(post(values), Equals, http.StatusBadRequest) | ||
414 | 136 | values.Del("hostname") | ||
415 | 137 | values.Add("parent", "baz") | ||
416 | 138 | c.Check(post(values), Equals, http.StatusBadRequest) | ||
417 | 139 | values.Del("mac_addresses") | ||
418 | 140 | values.Add("hostname", "bam") | ||
419 | 141 | c.Check(post(values), Equals, http.StatusBadRequest) | ||
420 | 142 | } | ||
421 | 143 | |||
422 | 144 | func (suite *TestServerSuite) TestNewDevice(c *C) { | ||
423 | 145 | devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=new" | ||
424 | 146 | |||
425 | 147 | values := url.Values{} | ||
426 | 148 | values.Add("mac_addresses", "foo") | ||
427 | 149 | values.Add("hostname", "bar") | ||
428 | 150 | values.Add("parent", "baz") | ||
429 | 151 | result := suite.post(c, devicesURL, values) | ||
430 | 152 | |||
431 | 153 | resultMap, err := result.GetMap() | ||
432 | 154 | c.Assert(err, IsNil) | ||
433 | 155 | |||
434 | 156 | macArray, err := resultMap["macaddress_set"].GetArray() | ||
435 | 157 | c.Assert(err, IsNil) | ||
436 | 158 | c.Assert(macArray, HasLen, 1) | ||
437 | 159 | macMap, err := macArray[0].GetMap() | ||
438 | 160 | c.Assert(err, IsNil) | ||
439 | 161 | |||
440 | 162 | mac := getString(c, macMap, "mac_address") | ||
441 | 163 | c.Assert(mac, Equals, "foo") | ||
442 | 164 | |||
443 | 165 | parent := getString(c, resultMap, "parent") | ||
444 | 166 | c.Assert(parent, Equals, "baz") | ||
445 | 167 | hostname := getString(c, resultMap, "hostname") | ||
446 | 168 | c.Assert(hostname, Equals, "bar") | ||
447 | 169 | |||
448 | 170 | addresses, err := resultMap["ip_addresses"].GetArray() | ||
449 | 171 | c.Assert(err, IsNil) | ||
450 | 172 | c.Assert(addresses, HasLen, 0) | ||
451 | 173 | |||
452 | 174 | systemId := getString(c, resultMap, "system_id") | ||
453 | 175 | resourceURI := getString(c, resultMap, "resource_uri") | ||
454 | 176 | c.Assert(resourceURI, Equals, fmt.Sprintf("/MAAS/api/%v/devices/%v/", suite.server.version, systemId)) | ||
455 | 177 | } | ||
456 | 178 | |||
457 | 179 | func (suite *TestServerSuite) TestGetDevice(c *C) { | ||
458 | 180 | systemId := suite.createDevice(c, "foo", "bar", "baz") | ||
459 | 181 | deviceURL := fmt.Sprintf("/api/%v/devices/%v/", suite.server.version, systemId) | ||
460 | 182 | |||
461 | 183 | result := suite.get(c, deviceURL) | ||
462 | 184 | resultMap, err := result.GetMap() | ||
463 | 185 | c.Assert(err, IsNil) | ||
464 | 186 | checkDevice(c, resultMap, "foo", "bar", "baz") | ||
465 | 187 | actualId, err := resultMap["system_id"].GetString() | ||
466 | 188 | c.Assert(actualId, Equals, systemId) | ||
467 | 189 | } | ||
468 | 190 | |||
469 | 191 | func (suite *TestServerSuite) TestDevicesList(c *C) { | ||
470 | 192 | firstId := suite.createDevice(c, "foo", "bar", "baz") | ||
471 | 193 | c.Assert(firstId, Not(Equals), "") | ||
472 | 194 | secondId := suite.createDevice(c, "bam", "bing", "bong") | ||
473 | 195 | c.Assert(secondId, Not(Equals), "") | ||
474 | 196 | |||
475 | 197 | devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + "?op=list" | ||
476 | 198 | result := suite.get(c, devicesURL) | ||
477 | 199 | |||
478 | 200 | devicesArray, err := result.GetArray() | ||
479 | 201 | c.Assert(err, IsNil) | ||
480 | 202 | c.Assert(devicesArray, HasLen, 2) | ||
481 | 203 | |||
482 | 204 | for _, device := range devicesArray { | ||
483 | 205 | deviceMap, err := device.GetMap() | ||
484 | 206 | c.Assert(err, IsNil) | ||
485 | 207 | systemId, err := deviceMap["system_id"].GetString() | ||
486 | 208 | c.Assert(err, IsNil) | ||
487 | 209 | switch systemId { | ||
488 | 210 | case firstId: | ||
489 | 211 | checkDevice(c, deviceMap, "foo", "bar", "baz") | ||
490 | 212 | case secondId: | ||
491 | 213 | checkDevice(c, deviceMap, "bam", "bing", "bong") | ||
492 | 214 | default: | ||
493 | 215 | c.Fatalf("unknown system id %q", systemId) | ||
494 | 216 | } | ||
495 | 217 | } | ||
496 | 218 | } | ||
497 | 219 | |||
498 | 220 | func (suite *TestServerSuite) TestDevicesListMacFiltering(c *C) { | ||
499 | 221 | firstId := suite.createDevice(c, "foo", "bar", "baz") | ||
500 | 222 | c.Assert(firstId, Not(Equals), "") | ||
501 | 223 | secondId := suite.createDevice(c, "bam", "bing", "bong") | ||
502 | 224 | c.Assert(secondId, Not(Equals), "") | ||
503 | 225 | |||
504 | 226 | op := fmt.Sprintf("?op=list&mac_address=%v", "foo") | ||
505 | 227 | devicesURL := fmt.Sprintf("/api/%s/devices/", suite.server.version) + op | ||
506 | 228 | result := suite.get(c, devicesURL) | ||
507 | 229 | |||
508 | 230 | devicesArray, err := result.GetArray() | ||
509 | 231 | c.Assert(err, IsNil) | ||
510 | 232 | c.Assert(devicesArray, HasLen, 1) | ||
511 | 233 | deviceMap, err := devicesArray[0].GetMap() | ||
512 | 234 | c.Assert(err, IsNil) | ||
513 | 235 | checkDevice(c, deviceMap, "foo", "bar", "baz") | ||
514 | 236 | } | ||
515 | 237 | |||
516 | 238 | func (suite *TestServerSuite) TestDeviceClaimStickyIPRequiresAddress(c *C) { | ||
517 | 239 | systemId := suite.createDevice(c, "foo", "bar", "baz") | ||
518 | 240 | op := "?op=claim_sticky_ip_address" | ||
519 | 241 | deviceURL := fmt.Sprintf("/api/%s/devices/%s/%s", suite.server.version, systemId, op) | ||
520 | 242 | values := url.Values{} | ||
521 | 243 | resp, err := http.Post(suite.server.Server.URL+deviceURL, "application/x-www-form-urlencoded", strings.NewReader(values.Encode())) | ||
522 | 244 | c.Assert(err, IsNil) | ||
523 | 245 | c.Assert(resp.StatusCode, Equals, http.StatusBadRequest) | ||
524 | 246 | } | ||
525 | 247 | |||
526 | 248 | func (suite *TestServerSuite) TestDeviceClaimStickyIP(c *C) { | ||
527 | 249 | systemId := suite.createDevice(c, "foo", "bar", "baz") | ||
528 | 250 | op := "?op=claim_sticky_ip_address" | ||
529 | 251 | deviceURL := fmt.Sprintf("/api/%s/devices/%s/", suite.server.version, systemId) | ||
530 | 252 | values := url.Values{} | ||
531 | 253 | values.Add("requested_address", "127.0.0.1") | ||
532 | 254 | result := suite.post(c, deviceURL+op, values) | ||
533 | 255 | resultMap, err := result.GetMap() | ||
534 | 256 | c.Assert(err, IsNil) | ||
535 | 257 | |||
536 | 258 | addresses, err := resultMap["ip_addresses"].GetArray() | ||
537 | 259 | c.Assert(err, IsNil) | ||
538 | 260 | c.Assert(addresses, HasLen, 1) | ||
539 | 261 | address, err := addresses[0].GetString() | ||
540 | 262 | c.Assert(err, IsNil) | ||
541 | 263 | c.Assert(address, Equals, "127.0.0.1") | ||
542 | 264 | } | ||
543 | 265 | |||
544 | 266 | func (suite *TestServerSuite) TestDeleteDevice(c *C) { | ||
545 | 267 | systemId := suite.createDevice(c, "foo", "bar", "baz") | ||
546 | 268 | deviceURL := fmt.Sprintf("/api/%s/devices/%s/", suite.server.version, systemId) | ||
547 | 269 | req, err := http.NewRequest("DELETE", suite.server.Server.URL+deviceURL, nil) | ||
548 | 270 | c.Assert(err, IsNil) | ||
549 | 271 | resp, err := http.DefaultClient.Do(req) | ||
550 | 272 | c.Assert(err, IsNil) | ||
551 | 273 | c.Assert(resp.StatusCode, Equals, http.StatusNoContent) | ||
552 | 274 | |||
553 | 275 | resp, err = http.Get(suite.server.Server.URL + deviceURL) | ||
554 | 276 | c.Assert(err, IsNil) | ||
555 | 277 | c.Assert(resp.StatusCode, Equals, http.StatusNotFound) | ||
556 | 278 | } | ||
557 | 279 | |||
558 | 52 | func (suite *TestServerSuite) TestInvalidOperationOnNodesIsBadRequest(c *C) { | 280 | func (suite *TestServerSuite) TestInvalidOperationOnNodesIsBadRequest(c *C) { |
559 | 53 | badURL := getNodesEndpoint(suite.server.version) + "?op=procrastinate" | 281 | badURL := getNodesEndpoint(suite.server.version) + "?op=procrastinate" |
560 | 54 | 282 |
LGTM in general, apart from the one comment inline below, if I wish we had logging in places where StatusBadRequest is returned to explain the reason and make the test server a bit easier to use.
Also, as discussed on IRC we need support for op=claim_sticky_ip (or was it claim-sticky-ip?).