Merge lp:~dooferlad/gomaasapi/subnets into lp:gomaasapi
- subnets
- Merge into trunk
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 |
Related bugs: |
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.
Commit message
Description of the change
Add subnets support.
- 67. By James Tunnicliffe
-
Removed unused function.
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.
James Tunnicliffe (dooferlad) : | # |
James Tunnicliffe (dooferlad) : | # |
- 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
-
AddFixedAddress
Range moved to the server rather than directly acting on a subnet. - 77. By James Tunnicliffe
-
suite.server.
SetNodeNetworkL ink 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
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 | +} |
Most of it looks good, apart from a few concerns around marshalling/ unmarshalling for numbers and reducing some duplication.