Merge lp:~dooferlad/gomaasapi/subnets into lp:gomaasapi

Proposed by James Tunnicliffe on 2015-11-23
Status: Merged
Approved by: James Tunnicliffe on 2015-11-27
Approved revision: 79
Merged at revision: 64
Proposed branch: lp:~dooferlad/gomaasapi/subnets
Merge into: lp:gomaasapi
Diff against target: 1390 lines (+1091/-40)
8 files modified
jsonobject.go (+10/-1)
maasobject.go (+2/-2)
testservice.go (+92/-27)
testservice_spaces.go (+81/-0)
testservice_subnets.go (+393/-0)
testservice_test.go (+361/-10)
testservice_utils.go (+119/-0)
testservice_vlan.go (+33/-0)
To merge this branch: bzr merge lp:~dooferlad/gomaasapi/subnets
Reviewer Review Type Date Requested Status
Michael Foord (community) Approve on 2015-11-27
James Tunnicliffe (community) Resubmit on 2015-11-26
Dimiter Naydenov 2015-11-23 Pending
Review via email: mp+278342@code.launchpad.net

This proposal supersedes a proposal from 2015-11-19.

Description of the Change

Add subnets support.

To post a comment you must log in.
Dimiter Naydenov (dimitern) wrote : Posted in a previous version of this proposal

Most of it looks good, apart from a few concerns around marshalling/unmarshalling for numbers and reducing some duplication.

review: Needs Fixing
James Tunnicliffe (dooferlad) wrote : Posted in a previous version of this proposal

Don't worry about the JSON stuff - it works just fine if you use structs because go can convert according to the type you give it. The de-dup stuff seems worth it though.

James Tunnicliffe (dooferlad) : Posted in a previous version of this proposal
James Tunnicliffe (dooferlad) : Posted in a previous version of this proposal
lp:~dooferlad/gomaasapi/subnets updated on 2015-11-26
72. By James Tunnicliffe on 2015-11-24

Pretty JSON is easier to read.
Fixed api/1.0/subnets/1/?op=reserved_ip_ranges when there is 1 reserved address.

73. By James Tunnicliffe on 2015-11-24

Fixed incorrect JSON name for space

74. By James Tunnicliffe on 2015-11-24

New addresses default to having a purpose of "assigned-ip"
Added AddFixedAddressRange call.
De-duplicated some code.

75. By James Tunnicliffe on 2015-11-24

Purpose back to being an array.
Fixed possible out of range error.

76. By James Tunnicliffe on 2015-11-26

AddFixedAddressRange moved to the server rather than directly acting on a subnet.

77. By James Tunnicliffe on 2015-11-26

suite.server.SetNodeNetworkLink now takes a node.SystemID rather than a node.

78. By James Tunnicliffe on 2015-11-26

Fixed subnets/1/?op=reserved_ip_ranges when no IP addresses had been explicitly assigned.

James Tunnicliffe (dooferlad) wrote :

Now working well enough for voidspace to use. Please take another look.

review: Resubmit
Michael Foord (mfoord) wrote :

The json encoding serialises uninitialised slices as null, which is incompatible with MAAS and will break clients that expect an empty array. Other than that looks good to me.

review: Needs Fixing
lp:~dooferlad/gomaasapi/subnets updated on 2015-11-27
79. By James Tunnicliffe on 2015-11-27

Ensure that all arrays in a posted subnet are non-nil.
Fix typo.
Simplify net.IP --> uint64 logic.

Michael Foord (mfoord) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'jsonobject.go'
2--- jsonobject.go 2013-02-12 12:26:07 +0000
3+++ jsonobject.go 2015-11-27 13:51:56 +0000
4@@ -104,6 +104,15 @@
5 return obj, nil
6 }
7
8+// JSONObjectFromStruct takes a struct and converts it to a JSONObject
9+func JSONObjectFromStruct(client Client, input interface{}) (JSONObject, error) {
10+ j, err := json.MarshalIndent(input, "", " ")
11+ if err != nil {
12+ return JSONObject{}, err
13+ }
14+ return Parse(client, j)
15+}
16+
17 // Return error value for failed type conversion.
18 func failConversion(wantedType string, obj JSONObject) error {
19 msg := fmt.Sprintf("Requested %v, got %T.", wantedType, obj.value)
20@@ -116,7 +125,7 @@
21 if obj.IsNil() {
22 return json.Marshal(nil)
23 }
24- return json.Marshal(obj.value)
25+ return json.MarshalIndent(obj.value, "", " ")
26 }
27
28 // With MarshalJSON, JSONObject implements json.Marshaler.
29
30=== modified file 'maasobject.go'
31--- maasobject.go 2013-03-04 12:13:33 +0000
32+++ maasobject.go 2015-11-27 13:51:56 +0000
33@@ -36,14 +36,14 @@
34
35 // MarshalJSON tells the standard json package how to serialize a MAASObject.
36 func (obj MAASObject) MarshalJSON() ([]byte, error) {
37- return json.Marshal(obj.GetMap())
38+ return json.MarshalIndent(obj.GetMap(), "", " ")
39 }
40
41 // With MarshalJSON, MAASObject implements json.Marshaler.
42 var _ json.Marshaler = (*MAASObject)(nil)
43
44 func marshalNode(node MAASObject) string {
45- res, _ := json.Marshal(node)
46+ res, _ := json.MarshalIndent(node, "", " ")
47 return string(res)
48
49 }
50
51=== modified file 'testservice.go'
52--- testservice.go 2015-07-02 16:26:22 +0000
53+++ testservice.go 2015-11-27 13:51:56 +0000
54@@ -74,6 +74,7 @@
55 // list of Values passed when performing operations at the
56 // /nodes/ level.
57 nodesOperationRequestValues []url.Values
58+ nodeMetadata map[string]Node
59 files map[string]MAASObject
60 networks map[string]MAASObject
61 networksPerNode map[string][]string
62@@ -94,6 +95,15 @@
63
64 // devices is a map of device UUIDs to devices.
65 devices map[string]*device
66+
67+ subnets map[uint]Subnet
68+ subnetNameToID map[string]uint
69+ nextSubnet uint
70+ spaces map[uint]Space
71+ spaceNameToID map[string]uint
72+ nextSpace uint
73+ vlans map[int]VLAN
74+ nextVLAN int
75 }
76
77 type device struct {
78@@ -205,6 +215,7 @@
79 server.nodeOperations = make(map[string][]string)
80 server.nodesOperationRequestValues = make([]url.Values, 0)
81 server.nodeOperationRequestValues = make(map[string][]url.Values)
82+ server.nodeMetadata = make(map[string]Node)
83 server.files = make(map[string]MAASObject)
84 server.networks = make(map[string]MAASObject)
85 server.networksPerNode = make(map[string][]string)
86@@ -214,8 +225,16 @@
87 server.bootImages = make(map[string][]JSONObject)
88 server.nodegroupsInterfaces = make(map[string][]JSONObject)
89 server.zones = make(map[string]JSONObject)
90- server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses"]}`
91+ server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses","devices-management","network-deployment-ubuntu"]}`
92 server.devices = make(map[string]*device)
93+ server.subnets = make(map[uint]Subnet)
94+ server.subnetNameToID = make(map[string]uint)
95+ server.nextSubnet = 1
96+ server.spaces = make(map[uint]Space)
97+ server.spaceNameToID = make(map[string]uint)
98+ server.nextSpace = 1
99+ server.vlans = make(map[int]VLAN)
100+ server.nextVLAN = 1
101 }
102
103 // SetVersionJSON sets the JSON response (capabilities) returned from the
104@@ -355,18 +374,34 @@
105 }
106
107 // NewIPAddress creates a new static IP address reservation for the
108-// given network and ipAddress.
109-func (server *TestServer) NewIPAddress(ipAddress, network string) {
110- if _, found := server.networks[network]; !found {
111- panic("No such network: " + network)
112+// given network/subnet and ipAddress. While networks is being deprecated
113+// try the given name as both a netowrk and a subnet.
114+func (server *TestServer) NewIPAddress(ipAddress, networkOrSubnet string) {
115+ _, foundNetwork := server.networks[networkOrSubnet]
116+ subnetID, foundSubnet := server.subnetNameToID[networkOrSubnet]
117+
118+ if (foundNetwork || foundSubnet) == false {
119+ panic("No such network or subnet: " + networkOrSubnet)
120 }
121- ips, found := server.ipAddressesPerNetwork[network]
122- if found {
123- ips = append(ips, ipAddress)
124+ if foundNetwork {
125+ ips, found := server.ipAddressesPerNetwork[networkOrSubnet]
126+ if found {
127+ ips = append(ips, ipAddress)
128+ } else {
129+ ips = []string{ipAddress}
130+ }
131+ server.ipAddressesPerNetwork[networkOrSubnet] = ips
132 } else {
133- ips = []string{ipAddress}
134+ subnet := server.subnets[subnetID]
135+ netIp := net.ParseIP(ipAddress)
136+ if netIp == nil {
137+ panic(ipAddress + " is invalid")
138+ }
139+ ip := IPFromNetIP(netIp)
140+ ip.Purpose = []string{"assigned-ip"}
141+ subnet.InUseIPAddresses = append(subnet.InUseIPAddresses, ip)
142+ server.subnets[subnetID] = subnet
143 }
144- server.ipAddressesPerNetwork[network] = ips
145 }
146
147 // RemoveIPAddress removes the given existing ipAddress and returns
148@@ -545,6 +580,21 @@
149 zonesHandler(server, w, r)
150 })
151
152+ subnetsURL := getSubnetsEndpoint(server.version)
153+ serveMux.HandleFunc(subnetsURL, func(w http.ResponseWriter, r *http.Request) {
154+ subnetsHandler(server, w, r)
155+ })
156+
157+ spacesURL := getSpacesEndpoint(server.version)
158+ serveMux.HandleFunc(spacesURL, func(w http.ResponseWriter, r *http.Request) {
159+ spacesHandler(server, w, r)
160+ })
161+
162+ vlansURL := getVLANsEndpoint(server.version)
163+ serveMux.HandleFunc(vlansURL, func(w http.ResponseWriter, r *http.Request) {
164+ vlansHandler(server, w, r)
165+ })
166+
167 newServer := httptest.NewServer(serveMux)
168 client, err := NewAnonymousClient(newServer.URL, "1.0")
169 checkError(err)
170@@ -785,12 +835,27 @@
171 http.NotFoundHandler().ServeHTTP(w, r)
172 return
173 }
174+ UUID, UUIDError := node.values["system_id"].GetString()
175+
176 if r.Method == "GET" {
177 if operation == "" {
178 w.WriteHeader(http.StatusOK)
179+ if UUIDError == nil {
180+ i, err := JSONObjectFromStruct(server.client, server.nodeMetadata[UUID].Interfaces)
181+ checkError(err)
182+ if err == nil {
183+ node.values["interface_set"] = i
184+ }
185+ }
186 fmt.Fprint(w, marshalNode(node))
187 return
188 } else if operation == "details" {
189+ if UUIDError == nil {
190+ i, err := JSONObjectFromStruct(server.client, server.nodeMetadata[UUID].Interfaces)
191+ if err == nil {
192+ node.values["interface_set"] = i
193+ }
194+ }
195 nodeDetailsHandler(server, w, r, systemId)
196 return
197 } else {
198@@ -811,10 +876,10 @@
199 w.WriteHeader(http.StatusOK)
200 fmt.Fprint(w, marshalNode(node))
201 return
202- } else {
203- w.WriteHeader(http.StatusBadRequest)
204- return
205 }
206+
207+ w.WriteHeader(http.StatusBadRequest)
208+ return
209 }
210 if r.Method == "DELETE" {
211 delete(server.nodes, systemId)
212@@ -844,7 +909,7 @@
213 convertedNodes = append(convertedNodes, node.GetMap())
214 }
215 }
216- res, err := json.Marshal(convertedNodes)
217+ res, err := json.MarshalIndent(convertedNodes, "", " ")
218 checkError(err)
219 w.WriteHeader(http.StatusOK)
220 fmt.Fprint(w, string(res))
221@@ -872,7 +937,7 @@
222 }
223 }
224 obj := maasify(server.client, nodeStatus)
225- res, err := json.Marshal(obj)
226+ res, err := json.MarshalIndent(obj, "", " ")
227 checkError(err)
228 w.WriteHeader(http.StatusOK)
229 fmt.Fprint(w, string(res))
230@@ -971,7 +1036,7 @@
231 systemId, err := node.GetField("system_id")
232 checkError(err)
233 server.OwnedNodes()[systemId] = true
234- res, err := json.Marshal(node)
235+ res, err := json.MarshalIndent(node, "", " ")
236 checkError(err)
237 // Record operation.
238 server.addNodeOperation(systemId, "acquire", r)
239@@ -1005,7 +1070,7 @@
240 node := server.Nodes()[systemId]
241 releasedNodes = append(releasedNodes, node.GetMap())
242 }
243- res, err := json.Marshal(releasedNodes)
244+ res, err := json.MarshalIndent(releasedNodes, "", " ")
245 checkError(err)
246 w.WriteHeader(http.StatusOK)
247 fmt.Fprint(w, string(res))
248@@ -1121,7 +1186,7 @@
249 fileMap := stripContent(server.files[filename].GetMap())
250 convertedFiles = append(convertedFiles, fileMap)
251 }
252- res, err := json.Marshal(convertedFiles)
253+ res, err := json.MarshalIndent(convertedFiles, "", " ")
254 checkError(err)
255 w.WriteHeader(http.StatusOK)
256 fmt.Fprint(w, string(res))
257@@ -1141,7 +1206,7 @@
258 http.NotFoundHandler().ServeHTTP(w, r)
259 return
260 }
261- jsonText, err := json.Marshal(file)
262+ jsonText, err := json.MarshalIndent(file, "", " ")
263 if err != nil {
264 panic(err)
265 }
266@@ -1233,7 +1298,7 @@
267 convertedMacAddresses = append(convertedMacAddresses, m)
268 }
269 }
270- res, err := json.Marshal(convertedMacAddresses)
271+ res, err := json.MarshalIndent(convertedMacAddresses, "", " ")
272 checkError(err)
273 w.WriteHeader(http.StatusOK)
274 fmt.Fprint(w, string(res))
275@@ -1265,7 +1330,7 @@
276 networks[i] = server.networks[networkName]
277 }
278 }
279- res, err := json.Marshal(networks)
280+ res, err := json.MarshalIndent(networks, "", " ")
281 checkError(err)
282 w.Header().Set("Content-Type", "application/json; charset=utf-8")
283 w.WriteHeader(http.StatusOK)
284@@ -1331,7 +1396,7 @@
285 results = append(results, maasObj)
286 }
287 }
288- res, err := json.Marshal(results)
289+ res, err := json.MarshalIndent(results, "", " ")
290 checkError(err)
291 w.Header().Set("Content-Type", "application/json; charset=utf-8")
292 w.WriteHeader(http.StatusOK)
293@@ -1407,7 +1472,7 @@
294 checkError(err)
295 maasObj, err := jsonObj.GetMAASObject()
296 checkError(err)
297- res, err := json.Marshal(maasObj)
298+ res, err := json.MarshalIndent(maasObj, "", " ")
299 checkError(err)
300 w.Header().Set("Content-Type", "application/json; charset=utf-8")
301 w.WriteHeader(http.StatusOK)
302@@ -1476,7 +1541,7 @@
303 nodegroups = append(nodegroups, obj)
304 }
305
306- res, err := json.Marshal(nodegroups)
307+ res, err := json.MarshalIndent(nodegroups, "", " ")
308 checkError(err)
309 w.WriteHeader(http.StatusOK)
310 fmt.Fprint(w, string(res))
311@@ -1495,7 +1560,7 @@
312 return
313 }
314
315- res, err := json.Marshal(bootImages)
316+ res, err := json.MarshalIndent(bootImages, "", " ")
317 checkError(err)
318 w.WriteHeader(http.StatusOK)
319 fmt.Fprint(w, string(res))
320@@ -1518,7 +1583,7 @@
321 // we already checked the nodegroup exists, so return an empty list
322 interfaces = []JSONObject{}
323 }
324- res, err := json.Marshal(interfaces)
325+ res, err := json.MarshalIndent(interfaces, "", " ")
326 checkError(err)
327 w.WriteHeader(http.StatusOK)
328 fmt.Fprint(w, string(res))
329@@ -1543,7 +1608,7 @@
330 for _, zone := range server.zones {
331 zones = append(zones, zone)
332 }
333- res, err := json.Marshal(zones)
334+ res, err := json.MarshalIndent(zones, "", " ")
335 checkError(err)
336 w.WriteHeader(http.StatusOK)
337 fmt.Fprint(w, string(res))
338
339=== added file 'testservice_spaces.go'
340--- testservice_spaces.go 1970-01-01 00:00:00 +0000
341+++ testservice_spaces.go 2015-11-27 13:51:56 +0000
342@@ -0,0 +1,81 @@
343+// Copyright 2015 Canonical Ltd. This software is licensed under the
344+// GNU Lesser General Public License version 3 (see the file COPYING).
345+
346+package gomaasapi
347+
348+import (
349+ "encoding/json"
350+ "fmt"
351+ "net/http"
352+ "regexp"
353+)
354+
355+func getSpacesEndpoint(version string) string {
356+ return fmt.Sprintf("/api/%s/spaces/", version)
357+}
358+
359+// Space is the MAAS API space representation
360+type Space struct {
361+ Name string `json:"name"`
362+ Subnets []Subnet `json:"subnets"`
363+ ResourceURI string `json:"resource_uri"`
364+ ID uint `json:"id"`
365+}
366+
367+// spacesHandler handles requests for '/api/<version>/spaces/'.
368+func spacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
369+ var err error
370+ spacesURLRE := regexp.MustCompile(`/spaces/(.+?)/`)
371+ spacesURLMatch := spacesURLRE.FindStringSubmatch(r.URL.Path)
372+ spacesURL := getSpacesEndpoint(server.version)
373+
374+ var ID uint
375+ var gotID bool
376+ if spacesURLMatch != nil {
377+ ID, err = NameOrIDToID(spacesURLMatch[1], server.spaceNameToID, 1, uint(len(server.spaces)))
378+
379+ if err != nil {
380+ http.NotFoundHandler().ServeHTTP(w, r)
381+ return
382+ }
383+
384+ gotID = true
385+ }
386+
387+ switch r.Method {
388+ case "GET":
389+ w.Header().Set("Content-Type", "application/vnd.api+json")
390+ if len(server.spaces) == 0 {
391+ // Until a space is registered, behave as if the endpoint
392+ // does not exist. This way we can simulate older MAAS
393+ // servers that do not support spaces.
394+ http.NotFoundHandler().ServeHTTP(w, r)
395+ return
396+ }
397+
398+ if r.URL.Path == spacesURL {
399+ var spaces []Space
400+ for i := uint(1); i < server.nextSpace; i++ {
401+ s, ok := server.spaces[i]
402+ if ok {
403+ spaces = append(spaces, s)
404+ }
405+ }
406+ err = json.NewEncoder(w).Encode(spaces)
407+ } else if gotID == false {
408+ w.WriteHeader(http.StatusBadRequest)
409+ } else {
410+ err = json.NewEncoder(w).Encode(server.spaces[ID])
411+ }
412+ checkError(err)
413+ case "POST":
414+ //server.NewSpace(r.Body)
415+ case "PUT":
416+ //server.UpdateSpace(r.Body)
417+ case "DELETE":
418+ delete(server.spaces, ID)
419+ w.WriteHeader(http.StatusOK)
420+ default:
421+ w.WriteHeader(http.StatusBadRequest)
422+ }
423+}
424
425=== added file 'testservice_subnets.go'
426--- testservice_subnets.go 1970-01-01 00:00:00 +0000
427+++ testservice_subnets.go 2015-11-27 13:51:56 +0000
428@@ -0,0 +1,393 @@
429+// Copyright 2015 Canonical Ltd. This software is licensed under the
430+// GNU Lesser General Public License version 3 (see the file COPYING).
431+
432+package gomaasapi
433+
434+import (
435+ "encoding/json"
436+ "fmt"
437+ "io"
438+ "net"
439+ "net/http"
440+ "net/url"
441+ "regexp"
442+ "sort"
443+ "strings"
444+)
445+
446+func getSubnetsEndpoint(version string) string {
447+ return fmt.Sprintf("/api/%s/subnets/", version)
448+}
449+
450+// CreateSubnet is used to receive new subnets via the MAAS API
451+type CreateSubnet struct {
452+ DNSServers []string `json:"dns_servers"`
453+ Name string `json:"name"`
454+ Space string `json:"space"`
455+ GatewayIP string `json:"gateway_ip"`
456+ CIDR string `json:"cidr"`
457+
458+ // VLAN this subnet belongs to. Currently ignored.
459+ // TODO: Defaults to the default VLAN
460+ // for the provided fabric or defaults to the default VLAN
461+ // in the default fabric.
462+ VLAN *uint `json:"vlan"`
463+
464+ // Fabric for the subnet. Currently ignored.
465+ // TODO: Defaults to the fabric the provided
466+ // VLAN belongs to or defaults to the default fabric.
467+ Fabric *uint `json:"fabric"`
468+
469+ // VID of the VLAN this subnet belongs to. Currently ignored.
470+ // TODO: Only used when vlan
471+ // is not provided. Picks the VLAN with this VID in the provided
472+ // fabric or the default fabric if one is not given.
473+ VID *uint `json:"vid"`
474+
475+ // This is used for updates (PUT) and is ignored by create (POST)
476+ ID uint `json:"id"`
477+}
478+
479+// Subnet is the MAAS API subnet representation
480+type Subnet struct {
481+ DNSServers []string `json:"dns_servers"`
482+ Name string `json:"name"`
483+ Space string `json:"space"`
484+ VLAN VLAN `json:"vlan"`
485+ GatewayIP string `json:"gateway_ip"`
486+ CIDR string `json:"cidr"`
487+
488+ ResourceURI string `json:"resource_uri"`
489+ ID uint `json:"id"`
490+ InUseIPAddresses []IP `json:"-"`
491+ FixedAddressRanges []AddressRange `json:"-"`
492+}
493+
494+// AddFixedAddressRange adds an AddressRange to the list of fixed address ranges
495+// that subnet stores.
496+func (server *TestServer) AddFixedAddressRange(subnetID uint, ar AddressRange) {
497+ subnet := server.subnets[subnetID]
498+ ar.startUint = IPFromString(ar.Start).UInt64()
499+ ar.endUint = IPFromString(ar.End).UInt64()
500+ subnet.FixedAddressRanges = append(subnet.FixedAddressRanges, ar)
501+ server.subnets[subnetID] = subnet
502+}
503+
504+// subnetsHandler handles requests for '/api/<version>/subnets/'.
505+func subnetsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
506+ var err error
507+ values, err := url.ParseQuery(r.URL.RawQuery)
508+ checkError(err)
509+ op := values.Get("op")
510+ includeRangesString := strings.ToLower(values.Get("include_ranges"))
511+ subnetsURLRE := regexp.MustCompile(`/subnets/(.+?)/`)
512+ subnetsURLMatch := subnetsURLRE.FindStringSubmatch(r.URL.Path)
513+ subnetsURL := getSubnetsEndpoint(server.version)
514+
515+ var ID uint
516+ var gotID bool
517+ if subnetsURLMatch != nil {
518+ ID, err = NameOrIDToID(subnetsURLMatch[1], server.subnetNameToID, 1, uint(len(server.subnets)))
519+
520+ if err != nil {
521+ http.NotFoundHandler().ServeHTTP(w, r)
522+ return
523+ }
524+
525+ gotID = true
526+ }
527+
528+ var includeRanges bool
529+ switch includeRangesString {
530+ case "true", "yes", "1":
531+ includeRanges = true
532+ }
533+
534+ switch r.Method {
535+ case "GET":
536+ w.Header().Set("Content-Type", "application/vnd.api+json")
537+ if len(server.subnets) == 0 {
538+ // Until a subnet is registered, behave as if the endpoint
539+ // does not exist. This way we can simulate older MAAS
540+ // servers that do not support subnets.
541+ http.NotFoundHandler().ServeHTTP(w, r)
542+ return
543+ }
544+
545+ if r.URL.Path == subnetsURL {
546+ var subnets []Subnet
547+ for i := uint(1); i < server.nextSubnet; i++ {
548+ s, ok := server.subnets[i]
549+ if ok {
550+ subnets = append(subnets, s)
551+ }
552+ }
553+ PrettyJsonWriter(subnets, w)
554+ } else if gotID == false {
555+ w.WriteHeader(http.StatusBadRequest)
556+ } else {
557+ switch op {
558+ case "unreserved_ip_ranges":
559+ PrettyJsonWriter(server.subnetUnreservedIPRanges(server.subnets[ID]), w)
560+ case "reserved_ip_ranges":
561+ PrettyJsonWriter(server.subnetReservedIPRanges(server.subnets[ID]), w)
562+ case "statistics":
563+ PrettyJsonWriter(server.subnetStatistics(server.subnets[ID], includeRanges), w)
564+ default:
565+ PrettyJsonWriter(server.subnets[ID], w)
566+ }
567+ }
568+ checkError(err)
569+ case "POST":
570+ server.NewSubnet(r.Body)
571+ case "PUT":
572+ server.UpdateSubnet(r.Body)
573+ case "DELETE":
574+ delete(server.subnets, ID)
575+ w.WriteHeader(http.StatusOK)
576+ default:
577+ w.WriteHeader(http.StatusBadRequest)
578+ }
579+}
580+
581+type addressList []IP
582+
583+func (a addressList) Len() int { return len(a) }
584+func (a addressList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
585+func (a addressList) Less(i, j int) bool { return a[i].UInt64() < a[j].UInt64() }
586+
587+// AddressRange is used to generate reserved IP address range lists
588+type AddressRange struct {
589+ Start string `json:"start"`
590+ startUint uint64
591+ End string `json:"end"`
592+ endUint uint64
593+ Purpose []string `json:"purpose,omitempty"`
594+ NumAddresses uint `json:"num_addresses"`
595+}
596+
597+// AddressRangeList is a list of AddressRange
598+type AddressRangeList struct {
599+ ar []AddressRange
600+}
601+
602+// Append appends a new AddressRange to an AddressRangeList
603+func (ranges *AddressRangeList) Append(startIP, endIP IP) {
604+ var i AddressRange
605+ i.Start, i.End = startIP.String(), endIP.String()
606+ i.startUint, i.endUint = startIP.UInt64(), endIP.UInt64()
607+ i.NumAddresses = uint(1 + endIP.UInt64() - startIP.UInt64())
608+ i.Purpose = startIP.Purpose
609+ ranges.ar = append(ranges.ar, i)
610+}
611+
612+func appendRangesToIPList(subnet Subnet, ipAddresses *[]IP) {
613+ for _, r := range subnet.FixedAddressRanges {
614+ for v := r.startUint; v <= r.endUint; v++ {
615+ ip := IPFromInt64(v)
616+ ip.Purpose = r.Purpose
617+ *ipAddresses = append(*ipAddresses, ip)
618+ }
619+ }
620+}
621+
622+func (server *TestServer) subnetUnreservedIPRanges(subnet Subnet) []AddressRange {
623+ // Make a sorted copy of subnet.InUseIPAddresses
624+ ipAddresses := make([]IP, len(subnet.InUseIPAddresses))
625+ copy(ipAddresses, subnet.InUseIPAddresses)
626+ appendRangesToIPList(subnet, &ipAddresses)
627+ sort.Sort(addressList(ipAddresses))
628+
629+ // We need the first and last address in the subnet
630+ var ranges AddressRangeList
631+ var startIP, endIP, lastUsableIP IP
632+
633+ _, ipNet, err := net.ParseCIDR(subnet.CIDR)
634+ checkError(err)
635+ startIP = IPFromNetIP(ipNet.IP)
636+ // Start with the lowest usable address in the range, which is 1 above
637+ // what net.ParseCIDR will give back.
638+ startIP.SetUInt64(startIP.UInt64() + 1)
639+
640+ ones, bits := ipNet.Mask.Size()
641+ set := ^((^uint64(0)) << uint(bits-ones))
642+
643+ // The last usable address is one below the broadcast address, which is
644+ // what you get by bitwise ORing 'set' with any IP address in the subnet.
645+ lastUsableIP.SetUInt64((startIP.UInt64() | set) - 1)
646+
647+ for _, endIP = range ipAddresses {
648+ end := endIP.UInt64()
649+
650+ if endIP.UInt64() == startIP.UInt64() {
651+ if endIP.UInt64() != lastUsableIP.UInt64() {
652+ startIP.SetUInt64(end + 1)
653+ }
654+ continue
655+ }
656+
657+ if end == lastUsableIP.UInt64() {
658+ continue
659+ }
660+
661+ ranges.Append(startIP, IPFromInt64(end-1))
662+ startIP.SetUInt64(end + 1)
663+ }
664+
665+ if startIP.UInt64() != lastUsableIP.UInt64() {
666+ ranges.Append(startIP, lastUsableIP)
667+ }
668+
669+ return ranges.ar
670+}
671+
672+func (server *TestServer) subnetReservedIPRanges(subnet Subnet) []AddressRange {
673+ var ranges AddressRangeList
674+ var startIP, thisIP IP
675+
676+ // Make a sorted copy of subnet.InUseIPAddresses
677+ ipAddresses := make([]IP, len(subnet.InUseIPAddresses))
678+ copy(ipAddresses, subnet.InUseIPAddresses)
679+ appendRangesToIPList(subnet, &ipAddresses)
680+ sort.Sort(addressList(ipAddresses))
681+ startIP = ipAddresses[0]
682+ lastIP := ipAddresses[0]
683+
684+ if len(ipAddresses) == 0 {
685+ return ranges.ar
686+ }
687+
688+ for _, thisIP = range ipAddresses {
689+ var purposeMissmatch bool
690+ for i, p := range thisIP.Purpose {
691+ if startIP.Purpose[i] != p {
692+ purposeMissmatch = true
693+ }
694+ }
695+ if (thisIP.UInt64() != lastIP.UInt64() && thisIP.UInt64() != lastIP.UInt64()+1) || purposeMissmatch {
696+ ranges.Append(startIP, lastIP)
697+ startIP = thisIP
698+ }
699+ lastIP = thisIP
700+ }
701+
702+ if len(ranges.ar) == 0 || ranges.ar[len(ranges.ar)-1].endUint != lastIP.UInt64() {
703+ ranges.Append(startIP, lastIP)
704+ }
705+
706+ return ranges.ar
707+}
708+
709+// SubnetStats holds statistics about a subnet
710+type SubnetStats struct {
711+ NumAvailable uint `json:"num_available"`
712+ LargestAvailable uint `json:"largest_available"`
713+ NumUnavailable uint `json:"num_unavailable"`
714+ TotalAddresses uint `json:"total_addresses"`
715+ Usage float32 `json:"usage"`
716+ UsageString string `json:"usage_string"`
717+ Ranges []AddressRange `json:"ranges"`
718+}
719+
720+func (server *TestServer) subnetStatistics(subnet Subnet, includeRanges bool) SubnetStats {
721+ var stats SubnetStats
722+ _, ipNet, err := net.ParseCIDR(subnet.CIDR)
723+ checkError(err)
724+
725+ ones, bits := ipNet.Mask.Size()
726+ stats.TotalAddresses = (1 << uint(bits-ones)) - 2
727+ stats.NumUnavailable = uint(len(subnet.InUseIPAddresses))
728+ stats.NumAvailable = stats.TotalAddresses - stats.NumUnavailable
729+ stats.Usage = float32(stats.NumUnavailable) / float32(stats.TotalAddresses)
730+ stats.UsageString = fmt.Sprintf("%0.1f%%", stats.Usage*100)
731+
732+ // Calculate stats.LargestAvailable - the largest contiguous block of IP addresses available
733+ reserved := server.subnetUnreservedIPRanges(subnet)
734+ for _, addressRange := range reserved {
735+ if addressRange.NumAddresses > stats.LargestAvailable {
736+ stats.LargestAvailable = addressRange.NumAddresses
737+ }
738+ }
739+
740+ if includeRanges {
741+ stats.Ranges = reserved
742+ }
743+
744+ return stats
745+}
746+
747+func decodePostedSubnet(subnetJSON io.Reader) CreateSubnet {
748+ var postedSubnet CreateSubnet
749+ decoder := json.NewDecoder(subnetJSON)
750+ err := decoder.Decode(&postedSubnet)
751+ checkError(err)
752+ if postedSubnet.DNSServers == nil {
753+ postedSubnet.DNSServers = []string{}
754+ }
755+ return postedSubnet
756+}
757+
758+// UpdateSubnet creates a subnet in the test server
759+func (server *TestServer) UpdateSubnet(subnetJSON io.Reader) Subnet {
760+ postedSubnet := decodePostedSubnet(subnetJSON)
761+ updatedSubnet := subnetFromCreateSubnet(postedSubnet)
762+ server.subnets[updatedSubnet.ID] = updatedSubnet
763+ return updatedSubnet
764+}
765+
766+// NewSubnet creates a subnet in the test server
767+func (server *TestServer) NewSubnet(subnetJSON io.Reader) *Subnet {
768+ postedSubnet := decodePostedSubnet(subnetJSON)
769+ newSubnet := subnetFromCreateSubnet(postedSubnet)
770+ newSubnet.ID = server.nextSubnet
771+ server.subnets[server.nextSubnet] = newSubnet
772+ server.subnetNameToID[newSubnet.Name] = newSubnet.ID
773+
774+ server.nextSubnet++
775+ return &newSubnet
776+}
777+
778+// NodeNetworkInterface represents a network interface attached to a node
779+type NodeNetworkInterface struct {
780+ Name string `json:"name"`
781+ Links []NetworkLink `json:"links"`
782+}
783+
784+// Node represents a node
785+type Node struct {
786+ SystemID string `json:"system_id"`
787+ Interfaces []NodeNetworkInterface `json:"interface_set"`
788+}
789+
790+// NetworkLink represents a MAAS network link
791+type NetworkLink struct {
792+ ID uint `json:"id"`
793+ Mode string `json:"mode"`
794+ Subnet *Subnet `json:"subnet"`
795+}
796+
797+// SetNodeNetworkLink records that the given node + interface are in subnet
798+func (server *TestServer) SetNodeNetworkLink(SystemID string, nodeNetworkInterface NodeNetworkInterface) {
799+ for i, ni := range server.nodeMetadata[SystemID].Interfaces {
800+ if ni.Name == nodeNetworkInterface.Name {
801+ server.nodeMetadata[SystemID].Interfaces[i] = nodeNetworkInterface
802+ return
803+ }
804+ }
805+ n := server.nodeMetadata[SystemID]
806+ n.Interfaces = append(n.Interfaces, nodeNetworkInterface)
807+ server.nodeMetadata[SystemID] = n
808+}
809+
810+// subnetFromCreateSubnet creates a subnet in the test server
811+func subnetFromCreateSubnet(postedSubnet CreateSubnet) Subnet {
812+ var newSubnet Subnet
813+ newSubnet.DNSServers = postedSubnet.DNSServers
814+ newSubnet.Name = postedSubnet.Name
815+ newSubnet.Space = postedSubnet.Space
816+ //TODO: newSubnet.VLAN = server.postedSubnetVLAN
817+ newSubnet.GatewayIP = postedSubnet.GatewayIP
818+ newSubnet.CIDR = postedSubnet.CIDR
819+ newSubnet.ID = postedSubnet.ID
820+ return newSubnet
821+}
822
823=== modified file 'testservice_test.go'
824--- testservice_test.go 2015-07-02 16:05:11 +0000
825+++ testservice_test.go 2015-11-27 13:51:56 +0000
826@@ -9,10 +9,13 @@
827 "encoding/json"
828 "fmt"
829 "io"
830+ "math/rand"
831 "mime/multipart"
832+ "net"
833 "net/http"
834 "net/url"
835 "sort"
836+ "strconv"
837 "strings"
838
839 "gopkg.in/mgo.v2/bson"
840@@ -603,6 +606,352 @@
841 c.Check(resp.StatusCode, Equals, http.StatusNotFound)
842 }
843
844+func defaultSubnet() CreateSubnet {
845+ var s CreateSubnet
846+ s.DNSServers = []string{"192.168.1.2"}
847+ s.Name = "maas-eth0"
848+ s.Space = "space-0"
849+ s.GatewayIP = "192.168.1.1"
850+ s.CIDR = "192.168.1.0/24"
851+ s.ID = 1
852+ return s
853+}
854+
855+func (suite *TestServerSuite) subnetJSON(subnet CreateSubnet) *bytes.Buffer {
856+ var out bytes.Buffer
857+ err := json.NewEncoder(&out).Encode(subnet)
858+ if err != nil {
859+ panic(err)
860+ }
861+ return &out
862+}
863+
864+func (suite *TestServerSuite) subnetURL(ID int) string {
865+ return suite.subnetsURL() + strconv.Itoa(ID) + "/"
866+}
867+
868+func (suite *TestServerSuite) subnetsURL() string {
869+ return suite.server.Server.URL + getSubnetsEndpoint(suite.server.version)
870+}
871+
872+func (suite *TestServerSuite) getSubnets(c *C) []Subnet {
873+ resp, err := http.Get(suite.subnetsURL())
874+
875+ c.Check(err, IsNil)
876+ c.Check(resp.StatusCode, Equals, http.StatusOK)
877+
878+ var subnets []Subnet
879+ decoder := json.NewDecoder(resp.Body)
880+ err = decoder.Decode(&subnets)
881+ c.Check(err, IsNil)
882+ return subnets
883+}
884+
885+func (suite *TestServerSuite) TestSubnetAdd(c *C) {
886+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
887+
888+ subnets := suite.getSubnets(c)
889+ c.Check(subnets, HasLen, 1)
890+ s := subnets[0]
891+ c.Check(s.DNSServers, DeepEquals, []string{"192.168.1.2"})
892+ c.Check(s.Name, Equals, "maas-eth0")
893+ c.Check(s.Space, Equals, "space-0")
894+ c.Check(s.VLAN.ID, Equals, uint(0))
895+ c.Check(s.CIDR, Equals, "192.168.1.0/24")
896+}
897+
898+func (suite *TestServerSuite) TestSubnetGet(c *C) {
899+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
900+
901+ subnet2 := defaultSubnet()
902+ subnet2.Name = "maas-eth1"
903+ subnet2.CIDR = "192.168.2.0/24"
904+ suite.server.NewSubnet(suite.subnetJSON(subnet2))
905+
906+ subnets := suite.getSubnets(c)
907+ c.Check(subnets, HasLen, 2)
908+ c.Check(subnets[0].CIDR, Equals, "192.168.1.0/24")
909+ c.Check(subnets[1].CIDR, Equals, "192.168.2.0/24")
910+}
911+
912+func (suite *TestServerSuite) TestSubnetPut(c *C) {
913+ subnet1 := defaultSubnet()
914+ suite.server.NewSubnet(suite.subnetJSON(subnet1))
915+
916+ subnets := suite.getSubnets(c)
917+ c.Check(subnets, HasLen, 1)
918+ c.Check(subnets[0].DNSServers, DeepEquals, []string{"192.168.1.2"})
919+
920+ subnet1.DNSServers = []string{"192.168.1.2", "192.168.1.3"}
921+ suite.server.UpdateSubnet(suite.subnetJSON(subnet1))
922+
923+ subnets = suite.getSubnets(c)
924+ c.Check(subnets, HasLen, 1)
925+ c.Check(subnets[0].DNSServers, DeepEquals, []string{"192.168.1.2", "192.168.1.3"})
926+}
927+
928+func (suite *TestServerSuite) TestSubnetDelete(c *C) {
929+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
930+
931+ subnets := suite.getSubnets(c)
932+ c.Check(subnets, HasLen, 1)
933+ c.Check(subnets[0].DNSServers, DeepEquals, []string{"192.168.1.2"})
934+
935+ req, err := http.NewRequest("DELETE", suite.subnetURL(1), nil)
936+ c.Check(err, IsNil)
937+ resp, err := http.DefaultClient.Do(req)
938+ c.Check(err, IsNil)
939+ c.Check(resp.StatusCode, Equals, http.StatusOK)
940+
941+ resp, err = http.Get(suite.subnetsURL())
942+ c.Check(err, IsNil)
943+ c.Check(resp.StatusCode, Equals, http.StatusNotFound)
944+}
945+
946+func (suite *TestServerSuite) reserveSomeAddresses() map[int]bool {
947+ reserved := make(map[int]bool)
948+ rand.Seed(6)
949+
950+ // Insert some random test data
951+ for i := 0; i < 200; i++ {
952+ r := rand.Intn(253) + 1
953+ _, ok := reserved[r]
954+ for ok == true {
955+ r++
956+ if r == 255 {
957+ r = 1
958+ }
959+ _, ok = reserved[r]
960+ }
961+ reserved[r] = true
962+ addr := fmt.Sprintf("192.168.1.%d", r)
963+ suite.server.NewIPAddress(addr, "maas-eth0")
964+ }
965+
966+ return reserved
967+}
968+
969+func (suite *TestServerSuite) TestSubnetReservedIPRanges(c *C) {
970+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
971+ reserved := suite.reserveSomeAddresses()
972+
973+ // Fetch from the server
974+ reservedIPRangeURL := suite.subnetURL(1) + "?op=reserved_ip_ranges"
975+ resp, err := http.Get(reservedIPRangeURL)
976+ c.Check(err, IsNil)
977+
978+ var reservedFromAPI []AddressRange
979+ decoder := json.NewDecoder(resp.Body)
980+ err = decoder.Decode(&reservedFromAPI)
981+ c.Check(err, IsNil)
982+
983+ // Check that anything in a reserved range was an address we allocated
984+ // with NewIPAddress
985+ for _, addressRange := range reservedFromAPI {
986+ var start, end int
987+ fmt.Sscanf(addressRange.Start, "192.168.1.%d", &start)
988+ fmt.Sscanf(addressRange.End, "192.168.1.%d", &end)
989+ c.Check(addressRange.NumAddresses, Equals, uint(1+end-start))
990+ c.Check(start <= end, Equals, true)
991+ c.Check(start < 255, Equals, true)
992+ c.Check(end < 255, Equals, true)
993+ for i := start; i <= end; i++ {
994+ _, ok := reserved[int(i)]
995+ c.Check(ok, Equals, true)
996+ delete(reserved, int(i))
997+ }
998+ }
999+ c.Check(reserved, HasLen, 0)
1000+}
1001+
1002+func (suite *TestServerSuite) TestSubnetUnreservedIPRanges(c *C) {
1003+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
1004+ reserved := suite.reserveSomeAddresses()
1005+ unreserved := make(map[int]bool)
1006+
1007+ // Fetch from the server
1008+ reservedIPRangeURL := suite.subnetURL(1) + "?op=unreserved_ip_ranges"
1009+ resp, err := http.Get(reservedIPRangeURL)
1010+ c.Check(err, IsNil)
1011+
1012+ var unreservedFromAPI []AddressRange
1013+ decoder := json.NewDecoder(resp.Body)
1014+ err = decoder.Decode(&unreservedFromAPI)
1015+ c.Check(err, IsNil)
1016+
1017+ // Check that anything in an unreserved range wasn't an address we allocated
1018+ // with NewIPAddress
1019+ for _, addressRange := range unreservedFromAPI {
1020+ var start, end int
1021+ fmt.Sscanf(addressRange.Start, "192.168.1.%d", &start)
1022+ fmt.Sscanf(addressRange.End, "192.168.1.%d", &end)
1023+ c.Check(addressRange.NumAddresses, Equals, uint(1+end-start))
1024+ c.Check(start <= end, Equals, true)
1025+ c.Check(start < 255, Equals, true)
1026+ c.Check(end < 255, Equals, true)
1027+ for i := start; i <= end; i++ {
1028+ _, ok := reserved[int(i)]
1029+ c.Check(ok, Equals, false)
1030+ unreserved[int(i)] = true
1031+ }
1032+ }
1033+ for i := 1; i < 255; i++ {
1034+ _, r := reserved[i]
1035+ _, u := unreserved[i]
1036+ if (r || u) == false {
1037+ fmt.Println(i, r, u)
1038+ }
1039+ c.Check(r || u, Equals, true)
1040+ }
1041+ c.Check(len(reserved)+len(unreserved), Equals, 254)
1042+}
1043+
1044+func (suite *TestServerSuite) TestSubnetReserveRange(c *C) {
1045+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
1046+ suite.server.NewIPAddress("192.168.1.10", "maas-eth0")
1047+
1048+ var ar AddressRange
1049+ ar.Start = "192.168.1.100"
1050+ ar.End = "192.168.1.200"
1051+ ar.Purpose = []string{"dynamic"}
1052+
1053+ suite.server.AddFixedAddressRange(1, ar)
1054+
1055+ // Fetch from the server
1056+ reservedIPRangeURL := suite.subnetURL(1) + "?op=reserved_ip_ranges"
1057+ resp, err := http.Get(reservedIPRangeURL)
1058+ c.Check(err, IsNil)
1059+
1060+ var reservedFromAPI []AddressRange
1061+ decoder := json.NewDecoder(resp.Body)
1062+ err = decoder.Decode(&reservedFromAPI)
1063+ c.Check(err, IsNil)
1064+
1065+ // Check that the address ranges we got back were as expected
1066+ addressRange := reservedFromAPI[0]
1067+ c.Check(addressRange.Start, Equals, "192.168.1.10")
1068+ c.Check(addressRange.End, Equals, "192.168.1.10")
1069+ c.Check(addressRange.NumAddresses, Equals, uint(1))
1070+ c.Check(addressRange.Purpose[0], Equals, "assigned-ip")
1071+ c.Check(addressRange.Purpose, HasLen, 1)
1072+
1073+ addressRange = reservedFromAPI[1]
1074+ c.Check(addressRange.Start, Equals, "192.168.1.100")
1075+ c.Check(addressRange.End, Equals, "192.168.1.200")
1076+ c.Check(addressRange.NumAddresses, Equals, uint(101))
1077+ c.Check(addressRange.Purpose[0], Equals, "dynamic")
1078+ c.Check(addressRange.Purpose, HasLen, 1)
1079+}
1080+
1081+func (suite *TestServerSuite) getSubnetStats(c *C, subnetID int) SubnetStats {
1082+ URL := suite.subnetURL(1) + "?op=statistics"
1083+ resp, err := http.Get(URL)
1084+ c.Check(err, IsNil)
1085+
1086+ var s SubnetStats
1087+ decoder := json.NewDecoder(resp.Body)
1088+ err = decoder.Decode(&s)
1089+ c.Check(err, IsNil)
1090+ return s
1091+}
1092+
1093+func (suite *TestServerSuite) TestSubnetStats(c *C) {
1094+ suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
1095+
1096+ stats := suite.getSubnetStats(c, 1)
1097+ // There are 254 usable addresses in a class C subnet, so these
1098+ // stats are fixed
1099+ expected := SubnetStats{
1100+ NumAvailable: 254,
1101+ LargestAvailable: 254,
1102+ NumUnavailable: 0,
1103+ TotalAddresses: 254,
1104+ Usage: 0,
1105+ UsageString: "0.0%",
1106+ Ranges: nil,
1107+ }
1108+ c.Check(stats, DeepEquals, expected)
1109+
1110+ suite.reserveSomeAddresses()
1111+ stats = suite.getSubnetStats(c, 1)
1112+ // We have reserved 200 addresses so parts of these
1113+ // stats are fixed.
1114+ expected = SubnetStats{
1115+ NumAvailable: 54,
1116+ NumUnavailable: 200,
1117+ TotalAddresses: 254,
1118+ Usage: 0.787401556968689,
1119+ UsageString: "78.7%",
1120+ Ranges: nil,
1121+ }
1122+
1123+ reserved := suite.server.subnetUnreservedIPRanges(suite.server.subnets[1])
1124+ var largestAvailable uint
1125+ for _, addressRange := range reserved {
1126+ if addressRange.NumAddresses > largestAvailable {
1127+ largestAvailable = addressRange.NumAddresses
1128+ }
1129+ }
1130+
1131+ expected.LargestAvailable = largestAvailable
1132+ c.Check(stats, DeepEquals, expected)
1133+}
1134+
1135+func (suite *TestServerSuite) TestSubnetsInNodes(c *C) {
1136+ // Create a subnet
1137+ subnet := suite.server.NewSubnet(suite.subnetJSON(defaultSubnet()))
1138+
1139+ // Create a node
1140+ var node Node
1141+ node.SystemID = "node-89d832ca-8877-11e5-b5a5-00163e86022b"
1142+ suite.server.NewNode(fmt.Sprintf(`{"system_id": "%s"}`, "node-89d832ca-8877-11e5-b5a5-00163e86022b"))
1143+
1144+ // Put the node in the subnet
1145+ var nni NodeNetworkInterface
1146+ nni.Name = "eth0"
1147+ nni.Links = append(nni.Links, NetworkLink{uint(1), "auto", subnet})
1148+ suite.server.SetNodeNetworkLink(node.SystemID, nni)
1149+
1150+ // Fetch the node details
1151+ URL := suite.server.Server.URL + getNodesEndpoint(suite.server.version) + node.SystemID + "/"
1152+ resp, err := http.Get(URL)
1153+ c.Check(err, IsNil)
1154+
1155+ var n Node
1156+ decoder := json.NewDecoder(resp.Body)
1157+ err = decoder.Decode(&n)
1158+ c.Check(err, IsNil)
1159+ c.Check(n.SystemID, Equals, node.SystemID)
1160+ c.Check(n.Interfaces, HasLen, 1)
1161+ i := n.Interfaces[0]
1162+ c.Check(i.Name, Equals, "eth0")
1163+ c.Check(i.Links, HasLen, 1)
1164+ c.Check(i.Links[0].ID, Equals, uint(1))
1165+ c.Check(i.Links[0].Subnet.Name, Equals, "maas-eth0")
1166+}
1167+
1168+type IPSuite struct {
1169+}
1170+
1171+var _ = Suite(&IPSuite{})
1172+
1173+func (suite *IPSuite) TestIPFromNetIP(c *C) {
1174+ ip := IPFromNetIP(net.ParseIP("1.2.3.4"))
1175+ c.Check(ip.String(), Equals, "1.2.3.4")
1176+}
1177+
1178+func (suite *IPSuite) TestIPUInt64(c *C) {
1179+ ip := IPFromNetIP(net.ParseIP("1.2.3.4"))
1180+ v := ip.UInt64()
1181+ c.Check(v, Equals, uint64(0x01020304))
1182+}
1183+
1184+func (suite *IPSuite) TestIPSetUInt64(c *C) {
1185+ var ip IP
1186+ ip.SetUInt64(0x01020304)
1187+ c.Check(ip.String(), Equals, "1.2.3.4")
1188+}
1189+
1190 // TestMAASObjectSuite validates that the object created by
1191 // NewTestMAAS can be used by the gomaasapi library as if it were a real
1192 // MAAS server.
1193@@ -612,16 +961,16 @@
1194
1195 var _ = Suite(&TestMAASObjectSuite{})
1196
1197-func (s *TestMAASObjectSuite) SetUpSuite(c *C) {
1198- s.TestMAASObject = NewTestMAAS("1.0")
1199-}
1200-
1201-func (s *TestMAASObjectSuite) TearDownSuite(c *C) {
1202- s.TestMAASObject.Close()
1203-}
1204-
1205-func (s *TestMAASObjectSuite) TearDownTest(c *C) {
1206- s.TestMAASObject.TestServer.Clear()
1207+func (suite *TestMAASObjectSuite) SetUpSuite(c *C) {
1208+ suite.TestMAASObject = NewTestMAAS("1.0")
1209+}
1210+
1211+func (suite *TestMAASObjectSuite) TearDownSuite(c *C) {
1212+ suite.TestMAASObject.Close()
1213+}
1214+
1215+func (suite *TestMAASObjectSuite) TearDownTest(c *C) {
1216+ suite.TestMAASObject.TestServer.Clear()
1217 }
1218
1219 func (suite *TestMAASObjectSuite) TestListNodes(c *C) {
1220@@ -1019,6 +1368,8 @@
1221 switch capName {
1222 case "networks-management":
1223 case "static-ipaddresses":
1224+ case "devices-management":
1225+ case "network-deployment-ubuntu":
1226 default:
1227 c.Fatalf("unknown capability %q", capName)
1228 }
1229
1230=== added file 'testservice_utils.go'
1231--- testservice_utils.go 1970-01-01 00:00:00 +0000
1232+++ testservice_utils.go 2015-11-27 13:51:56 +0000
1233@@ -0,0 +1,119 @@
1234+// Copyright 2015 Canonical Ltd. This software is licensed under the
1235+// GNU Lesser General Public License version 3 (see the file COPYING).
1236+
1237+package gomaasapi
1238+
1239+import (
1240+ "bytes"
1241+ "encoding/binary"
1242+ "encoding/json"
1243+ "errors"
1244+ "net"
1245+ "net/http"
1246+ "strconv"
1247+)
1248+
1249+// NameOrIDToID takes a string that contains eiter an integer ID or the
1250+// name of a thing. It returns the integer ID contained or mapped to or panics.
1251+func NameOrIDToID(v string, nameToID map[string]uint, minID, maxID uint) (ID uint, err error) {
1252+ ID, ok := nameToID[v]
1253+ if !ok {
1254+ intID, err := strconv.Atoi(v)
1255+ if err != nil {
1256+ return 0, err
1257+ }
1258+ ID = uint(intID)
1259+ }
1260+
1261+ if ID < minID || ID > maxID {
1262+ return 0, errors.New("ID out of range")
1263+ }
1264+
1265+ return ID, nil
1266+}
1267+
1268+// IP is an enhanced net.IP
1269+type IP struct {
1270+ netIP net.IP
1271+ Purpose []string
1272+}
1273+
1274+// IPFromNetIP creates a IP from a net.IP.
1275+func IPFromNetIP(netIP net.IP) IP {
1276+ var ip IP
1277+ ip.netIP = netIP
1278+ return ip
1279+}
1280+
1281+// IPFromString creates a new IP from a string IP address representation
1282+func IPFromString(v string) IP {
1283+ return IPFromNetIP(net.ParseIP(v))
1284+}
1285+
1286+// IPFromInt64 creates a new IP from a uint64 IP address representation
1287+func IPFromInt64(v uint64) IP {
1288+ var ip IP
1289+ ip.SetUInt64(v)
1290+ return ip
1291+}
1292+
1293+// To4 converts the IPv4 address ip to a 4-byte representation. If ip is not
1294+// an IPv4 address, To4 returns nil.
1295+func (ip IP) To4() net.IP {
1296+ return ip.netIP.To4()
1297+}
1298+
1299+// To16 converts the IP address ip to a 16-byte representation. If ip is not
1300+// an IP address (it is the wrong length), To16 returns nil.
1301+func (ip IP) To16() net.IP {
1302+ return ip.netIP.To16()
1303+}
1304+
1305+func (ip IP) String() string {
1306+ return ip.netIP.String()
1307+}
1308+
1309+// UInt64 returns a uint64 holding the IP address
1310+func (ip IP) UInt64() uint64 {
1311+ if len(ip.netIP) == 0 {
1312+ return uint64(0)
1313+ }
1314+
1315+ if ip.To4() != nil {
1316+ return uint64(binary.BigEndian.Uint32([]byte(ip.To4())))
1317+ }
1318+
1319+ return binary.BigEndian.Uint64([]byte(ip.To16()))
1320+}
1321+
1322+// SetUInt64 sets the IP value to v
1323+func (ip *IP) SetUInt64(v uint64) {
1324+ if len(ip.netIP) == 0 {
1325+ // If we don't have allocated storage make an educated guess
1326+ // at if the address we received is an IPv4 or IPv6 address.
1327+ if v == (v & 0x00000000ffffFFFF) {
1328+ // Guessing IPv4
1329+ ip.netIP = net.ParseIP("0.0.0.0")
1330+ } else {
1331+ ip.netIP = net.ParseIP("2001:4860:0:2001::68")
1332+ }
1333+ }
1334+
1335+ bb := new(bytes.Buffer)
1336+ var first int
1337+ if ip.To4() != nil {
1338+ binary.Write(bb, binary.BigEndian, uint32(v))
1339+ first = len(ip.netIP) - 4
1340+ } else {
1341+ binary.Write(bb, binary.BigEndian, v)
1342+ }
1343+ copy(ip.netIP[first:], bb.Bytes())
1344+}
1345+
1346+func PrettyJsonWriter(thing interface{}, w http.ResponseWriter) {
1347+ var out bytes.Buffer
1348+ b, err := json.MarshalIndent(thing, "", " ")
1349+ checkError(err)
1350+ out.Write(b)
1351+ out.WriteTo(w)
1352+}
1353
1354=== added file 'testservice_vlan.go'
1355--- testservice_vlan.go 1970-01-01 00:00:00 +0000
1356+++ testservice_vlan.go 2015-11-27 13:51:56 +0000
1357@@ -0,0 +1,33 @@
1358+// Copyright 2015 Canonical Ltd. This software is licensed under the
1359+// GNU Lesser General Public License version 3 (see the file COPYING).
1360+
1361+package gomaasapi
1362+
1363+import (
1364+ "fmt"
1365+ "net/http"
1366+)
1367+
1368+func getVLANsEndpoint(version string) string {
1369+ return fmt.Sprintf("/api/%s/vlans/", version)
1370+}
1371+
1372+// VLAN is the MAAS API VLAN representation
1373+type VLAN struct {
1374+ Name string `json:"name"`
1375+ Fabric string `json:"fabric"`
1376+ VID uint `json:"vid"`
1377+
1378+ ResourceURI string `json:"resource_uri"`
1379+ ID uint `json:"id"`
1380+}
1381+
1382+// PostedVLAN is the MAAS API posted VLAN representation
1383+type PostedVLAN struct {
1384+ Name string `json:"name"`
1385+ VID uint `json:"vid"`
1386+}
1387+
1388+func vlansHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1389+ //TODO
1390+}

Subscribers

People subscribed via source and target branches

to all changes: