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

Proposed by James Tunnicliffe
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) Needs Fixing
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
67. By James Tunnicliffe

Removed unused function.

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

Revision history for this message
James Tunnicliffe (dooferlad) :
Revision history for this message
James Tunnicliffe (dooferlad) :
lp:~dooferlad/gomaasapi/subnets updated
68. By James Tunnicliffe

Tidying up subnets.

69. By James Tunnicliffe

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

70. By James Tunnicliffe

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

71. By James Tunnicliffe

Added test for node subnets mapping.

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.

79. By James Tunnicliffe

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: