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

Proposed by James Tunnicliffe
Status: Merged
Approved by: James Tunnicliffe
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
James Tunnicliffe (community) Needs Resubmitting
Dimiter Naydenov 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.
Revision history for this message
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
Revision history for this message
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.

Revision history for this message
James Tunnicliffe (dooferlad) : Posted in a previous version of this proposal
Revision history for this message
James Tunnicliffe (dooferlad) : Posted in a previous version of this proposal
lp:~dooferlad/gomaasapi/subnets updated
72. By James Tunnicliffe

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

Fixed incorrect JSON name for space

74. By James Tunnicliffe

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

75. By James Tunnicliffe

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

76. By James Tunnicliffe

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

77. By James Tunnicliffe

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

78. By James Tunnicliffe

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

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

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

review: Needs Resubmitting
Revision history for this message
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
79. By James Tunnicliffe

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

Revision history for this message
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: