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

Proposed by James Tunnicliffe on 2015-11-19
Status: Superseded
Proposed branch: lp:~dooferlad/gomaasapi/subnets
Merge into: lp:gomaasapi
Diff against target: 1150 lines (+990/-23)
7 files modified
jsonobject.go (+9/-0)
testservice.go (+76/-13)
testservice_spaces.go (+81/-0)
testservice_subnets.go (+362/-0)
testservice_test.go (+324/-10)
testservice_utils.go (+105/-0)
testservice_vlan.go (+33/-0)
To merge this branch: bzr merge lp:~dooferlad/gomaasapi/subnets
Reviewer Review Type Date Requested Status
Dimiter Naydenov (community) 2015-11-19 Needs Fixing on 2015-11-19
Review via email: mp+277977@code.launchpad.net

This proposal has been superseded by a proposal from 2015-11-23.

Description of the Change

Add subnets support.

To post a comment you must log in.
lp:~dooferlad/gomaasapi/subnets updated on 2015-11-19
67. By James Tunnicliffe on 2015-11-19

Removed unused function.

Dimiter Naydenov (dimitern) wrote :

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 :

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.

lp:~dooferlad/gomaasapi/subnets updated on 2015-11-27
68. By James Tunnicliffe on 2015-11-19

Tidying up subnets.

69. By James Tunnicliffe on 2015-11-20

Subnets now have statistics support.
Fixed up reserved/unreserved ranges after improving tests.
Tidied up some old tests.

70. By James Tunnicliffe on 2015-11-23

Spaces added (for the structs - may never need the API)
Subnets now can be embedded in a nodes JSON.

71. By James Tunnicliffe on 2015-11-23

Added test for node subnets mapping.

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.

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.

Unmerged revisions

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

Subscribers

People subscribed via source and target branches

to all changes: