Merge lp:~dimitern/gomaasapi/static-ipaddresses into lp:gomaasapi

Proposed by Dimiter Naydenov
Status: Merged
Approved by: Dimiter Naydenov
Approved revision: 57
Merged at revision: 57
Proposed branch: lp:~dimitern/gomaasapi/static-ipaddresses
Merge into: lp:gomaasapi
Diff against target: 478 lines (+358/-9)
2 files modified
testservice.go (+209/-3)
testservice_test.go (+149/-6)
To merge this branch: bzr merge lp:~dimitern/gomaasapi/static-ipaddresses
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Juju Engineering Pending
Review via email: mp+238893@code.launchpad.net

Commit message

Static IP addresses API and capabilities for test

Added /ipaddresses/ handlers in the testservice for
GET (list) and POST (release, reserve). Also added
the "static-ipaddresses" capability to the list of
supported ones in the testservice.

All this is needed to test juju-core's MAAS provider
properly with the ongoing work to allow static IP
addresses reservations for containers.

Description of the change

Static IP addresses API and capabilities for test

Added /ipaddresses/ handlers in the testservice for
GET (list) and POST (release, reserve). Also added
the "static-ipaddresses" capability to the list of
supported ones in the testservice.

All this is needed to test juju-core's MAAS provider
properly with the ongoing work to allow static IP
addresses reservations for containers.

To post a comment you must log in.
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Please take a look.

Revision history for this message
Graham Binns (gmb) wrote :

This looks okay to me; I don't have reviewer rights on this project, however.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testservice.go'
2--- testservice.go 2014-10-15 03:12:25 +0000
3+++ testservice.go 2014-10-20 14:21:23 +0000
4@@ -10,12 +10,15 @@
5 "fmt"
6 "io/ioutil"
7 "mime/multipart"
8+ "net"
9 "net/http"
10 "net/http/httptest"
11 "net/url"
12 "regexp"
13 "sort"
14+ "strconv"
15 "strings"
16+ "time"
17
18 "gopkg.in/mgo.v2/bson"
19 )
20@@ -72,6 +75,7 @@
21 files map[string]MAASObject
22 networks map[string]MAASObject
23 networksPerNode map[string][]string
24+ ipAddressesPerNetwork map[string][]string
25 version string
26 macAddressesPerNetwork map[string]map[string]JSONObject
27 nodeDetails map[string]string
28@@ -122,6 +126,10 @@
29 return regexp.MustCompile(reString)
30 }
31
32+func getIPAddressesEndpoint(version string) string {
33+ return fmt.Sprintf("/api/%s/ipaddresses/", version)
34+}
35+
36 func getMACAddressURL(version, systemId, macAddress string) string {
37 return fmt.Sprintf("/api/%s/nodes/%s/macs/%s/", version, systemId, url.QueryEscape(macAddress))
38 }
39@@ -131,7 +139,7 @@
40 }
41
42 func getVersionJSON() string {
43- return `{"capabilities": ["networks-management"]}`
44+ return `{"capabilities": ["networks-management","static-ipaddresses"]}`
45 }
46
47 func getNodegroupsEndpoint(version string) string {
48@@ -163,6 +171,7 @@
49 server.files = make(map[string]MAASObject)
50 server.networks = make(map[string]MAASObject)
51 server.networksPerNode = make(map[string][]string)
52+ server.ipAddressesPerNetwork = make(map[string][]string)
53 server.macAddressesPerNetwork = make(map[string]map[string]JSONObject)
54 server.nodeDetails = make(map[string]string)
55 server.bootImages = make(map[string][]JSONObject)
56@@ -299,14 +308,52 @@
57 node.GetMap()[key] = maasify(server.client, value)
58 }
59
60+// NewIPAddress creates a new static IP address reservation for the
61+// given network and ipAddress.
62+func (server *TestServer) NewIPAddress(ipAddress, network string) {
63+ if _, found := server.networks[network]; !found {
64+ panic("No such network: " + network)
65+ }
66+ ips, found := server.ipAddressesPerNetwork[network]
67+ if found {
68+ ips = append(ips, ipAddress)
69+ } else {
70+ ips = []string{ipAddress}
71+ }
72+ server.ipAddressesPerNetwork[network] = ips
73+}
74+
75+// RemoveIPAddress removes the given existing ipAddress and returns
76+// whether it was actually removed.
77+func (server *TestServer) RemoveIPAddress(ipAddress string) bool {
78+ for network, ips := range server.ipAddressesPerNetwork {
79+ for i, ip := range ips {
80+ if ip == ipAddress {
81+ ips = append(ips[:i], ips[i+1:]...)
82+ server.ipAddressesPerNetwork[network] = ips
83+ return true
84+ }
85+ }
86+ }
87+ return false
88+}
89+
90+// IPAddresses returns the map with network names as keys and slices
91+// of IP addresses belonging to each network as values.
92+func (server *TestServer) IPAddresses() map[string][]string {
93+ return server.ipAddressesPerNetwork
94+}
95+
96 // NewNetwork creates a network in the test MAAS server
97 func (server *TestServer) NewNetwork(jsonText string) MAASObject {
98 var attrs map[string]interface{}
99 err := json.Unmarshal([]byte(jsonText), &attrs)
100 checkError(err)
101 nameEntry, hasName := attrs["name"]
102- if !hasName {
103- panic("The given map json string does not contain a 'name' value.")
104+ _, hasIP := attrs["ip"]
105+ _, hasNetmask := attrs["netmask"]
106+ if !hasName || !hasIP || !hasNetmask {
107+ panic("The given map json string does not contain a 'name', 'ip', or 'netmask' value.")
108 }
109 // TODO(gz): Sanity checking done on other fields
110 name := nameEntry.(string)
111@@ -403,6 +450,11 @@
112 serveMux.HandleFunc(networksURL, func(w http.ResponseWriter, r *http.Request) {
113 networksHandler(server, w, r)
114 })
115+ ipAddressesURL := getIPAddressesEndpoint(server.version)
116+ // Register handler for '/api/<version>/ipaddresses/'.
117+ serveMux.HandleFunc(ipAddressesURL, func(w http.ResponseWriter, r *http.Request) {
118+ ipAddressesHandler(server, w, r)
119+ })
120 versionURL := getVersionURL(server.version)
121 // Register handler for '/api/<version>/version/'.
122 serveMux.HandleFunc(versionURL, func(w http.ResponseWriter, r *http.Request) {
123@@ -872,6 +924,160 @@
124 fmt.Fprint(w, string(res))
125 }
126
127+// ipAddressesHandler handles requests for '/api/<version>/ipaddresses/'.
128+func ipAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
129+ err := r.ParseForm()
130+ checkError(err)
131+ values := r.Form
132+ op := values.Get("op")
133+
134+ switch r.Method {
135+ case "GET":
136+ if op != "" {
137+ panic("expected empty op for GET, got " + op)
138+ }
139+ listIPAddressesHandler(server, w, r)
140+ return
141+ case "POST":
142+ switch op {
143+ case "reserve":
144+ reserveIPAddressHandler(server, w, r, values.Get("network"), values.Get("requested_address"))
145+ return
146+ case "release":
147+ releaseIPAddressHandler(server, w, r, values.Get("ip"))
148+ return
149+ default:
150+ panic("expected op=release|reserve for POST, got " + op)
151+ }
152+ }
153+ http.NotFoundHandler().ServeHTTP(w, r)
154+}
155+
156+func marshalIPAddress(server *TestServer, ipAddress string) (JSONObject, error) {
157+ jsonTemplate := `{"alloc_type": 4, "ip": %q, "resource_uri": %q, "created": %q}`
158+ uri := getIPAddressesEndpoint(server.version)
159+ now := time.Now().UTC().Format(time.RFC3339)
160+ bytes := []byte(fmt.Sprintf(jsonTemplate, ipAddress, uri, now))
161+ return Parse(server.client, bytes)
162+}
163+
164+func badRequestError(w http.ResponseWriter, err error) {
165+ w.WriteHeader(http.StatusBadRequest)
166+ fmt.Fprint(w, err.Error())
167+}
168+
169+func listIPAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
170+ results := []MAASObject{}
171+ for _, ips := range server.IPAddresses() {
172+ for _, ip := range ips {
173+ jsonObj, err := marshalIPAddress(server, ip)
174+ if err != nil {
175+ badRequestError(w, err)
176+ return
177+ }
178+ maasObj, err := jsonObj.GetMAASObject()
179+ if err != nil {
180+ badRequestError(w, err)
181+ return
182+ }
183+ results = append(results, maasObj)
184+ }
185+ }
186+ res, err := json.Marshal(results)
187+ checkError(err)
188+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
189+ w.WriteHeader(http.StatusOK)
190+ fmt.Fprint(w, string(res))
191+}
192+
193+func reserveIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, network, reqAddress string) {
194+ _, ipNet, err := net.ParseCIDR(network)
195+ if err != nil {
196+ badRequestError(w, fmt.Errorf("Invalid network parameter %s", network))
197+ return
198+ }
199+ if reqAddress != "" {
200+ // Validate "requested_address" parameter.
201+ reqIP := net.ParseIP(reqAddress)
202+ if reqIP == nil {
203+ badRequestError(w, fmt.Errorf("failed to detect a valid IP address from u'%s'", reqAddress))
204+ return
205+ }
206+ if !ipNet.Contains(reqIP) {
207+ badRequestError(w, fmt.Errorf("%s is not inside the range %s", reqAddress, ipNet.String()))
208+ return
209+ }
210+ }
211+ // Find the network name matching the parsed CIDR.
212+ foundNetworkName := ""
213+ for netName, netObj := range server.networks {
214+ // Get the "ip" and "netmask" attributes of the network.
215+ netIP, err := netObj.GetField("ip")
216+ checkError(err)
217+ netMask, err := netObj.GetField("netmask")
218+ checkError(err)
219+
220+ // Convert the netmask string to net.IPMask.
221+ parts := strings.Split(netMask, ".")
222+ ipMask := make(net.IPMask, len(parts))
223+ for i, part := range parts {
224+ intPart, err := strconv.Atoi(part)
225+ checkError(err)
226+ ipMask[i] = byte(intPart)
227+ }
228+ netNet := &net.IPNet{IP: net.ParseIP(netIP), Mask: ipMask}
229+ if netNet.String() == network {
230+ // Exact match found.
231+ foundNetworkName = netName
232+ break
233+ }
234+ }
235+ if foundNetworkName == "" {
236+ badRequestError(w, fmt.Errorf("No network found matching %s", network))
237+ return
238+ }
239+ ips, found := server.ipAddressesPerNetwork[foundNetworkName]
240+ if !found {
241+ // This will be the first address.
242+ ips = []string{}
243+ }
244+ reservedIP := ""
245+ if reqAddress != "" {
246+ // Use what the user provided. NOTE: Because this is testing
247+ // code, no duplicates check is done.
248+ reservedIP = reqAddress
249+ } else {
250+ // Generate an IP in the network range by incrementing the
251+ // last byte of the network's IP.
252+ firstIP := ipNet.IP
253+ firstIP[len(firstIP)-1] += byte(len(ips) + 1)
254+ reservedIP = firstIP.String()
255+ }
256+ ips = append(ips, reservedIP)
257+ server.ipAddressesPerNetwork[foundNetworkName] = ips
258+ jsonObj, err := marshalIPAddress(server, reservedIP)
259+ checkError(err)
260+ maasObj, err := jsonObj.GetMAASObject()
261+ checkError(err)
262+ res, err := json.Marshal(maasObj)
263+ checkError(err)
264+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
265+ w.WriteHeader(http.StatusOK)
266+ fmt.Fprint(w, string(res))
267+}
268+
269+func releaseIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, ip string) {
270+ if netIP := net.ParseIP(ip); netIP == nil {
271+ http.NotFoundHandler().ServeHTTP(w, r)
272+ return
273+ }
274+ if server.RemoveIPAddress(ip) {
275+ w.WriteHeader(http.StatusOK)
276+ return
277+ }
278+ http.NotFoundHandler().ServeHTTP(w, r)
279+}
280+
281 // versionHandler handles requests for '/api/<version>/version/'.
282 func versionHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
283 if r.Method != "GET" {
284
285=== modified file 'testservice_test.go'
286--- testservice_test.go 2014-10-15 03:12:25 +0000
287+++ testservice_test.go 2014-10-20 14:21:23 +0000
288@@ -629,7 +629,7 @@
289 func (suite *TestMAASObjectSuite) TestGetNetworks(c *C) {
290 nodeJSON := `{"system_id": "mysystemid"}`
291 suite.TestMAASObject.TestServer.NewNode(nodeJSON)
292- networkJSON := `{"name": "mynetworkname"}`
293+ networkJSON := `{"name": "mynetworkname", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`
294 suite.TestMAASObject.TestServer.NewNetwork(networkJSON)
295 suite.TestMAASObject.TestServer.ConnectNodeToNetwork("mysystemid", "mynetworkname")
296
297@@ -647,7 +647,13 @@
298
299 networkName, err := listNetworks.GetField("name")
300 c.Assert(err, IsNil)
301+ ip, err := listNetworks.GetField("ip")
302+ c.Assert(err, IsNil)
303+ netmask, err := listNetworks.GetField("netmask")
304+ c.Assert(err, IsNil)
305 c.Check(networkName, Equals, "mynetworkname")
306+ c.Check(ip, Equals, "0.1.2.0")
307+ c.Check(netmask, Equals, "255.255.255.0")
308 }
309
310 func (suite *TestMAASObjectSuite) TestGetNetworksNone(c *C) {
311@@ -667,7 +673,7 @@
312 func (suite *TestMAASObjectSuite) TestListNodesWithNetworks(c *C) {
313 nodeJSON := `{"system_id": "mysystemid"}`
314 suite.TestMAASObject.TestServer.NewNode(nodeJSON)
315- networkJSON := `{"name": "mynetworkname"}`
316+ networkJSON := `{"name": "mynetworkname", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`
317 suite.TestMAASObject.TestServer.NewNetwork(networkJSON)
318 suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("mysystemid", "mynetworkname", "aa:bb:cc:dd:ee:ff")
319
320@@ -709,8 +715,12 @@
321 func (suite *TestMAASObjectSuite) TestListNetworkConnectedMACAddresses(c *C) {
322 suite.TestMAASObject.TestServer.NewNode(`{"system_id": "node_1"}`)
323 suite.TestMAASObject.TestServer.NewNode(`{"system_id": "node_2"}`)
324- suite.TestMAASObject.TestServer.NewNetwork(`{"name": "net_1"}`)
325- suite.TestMAASObject.TestServer.NewNetwork(`{"name": "net_2"}`)
326+ suite.TestMAASObject.TestServer.NewNetwork(
327+ `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`,
328+ )
329+ suite.TestMAASObject.TestServer.NewNetwork(
330+ `{"name": "net_2", "ip": "0.2.2.0", "netmask": "255.255.255.0"}`,
331+ )
332 suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_2", "net_2", "aa:bb:cc:dd:ee:22")
333 suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_1", "net_1", "aa:bb:cc:dd:ee:11")
334 suite.TestMAASObject.TestServer.ConnectNodeToNetworkWithMACAddress("node_2", "net_1", "aa:bb:cc:dd:ee:21")
335@@ -770,8 +780,141 @@
336 for _, capJSONName := range capArray {
337 capName, err := capJSONName.GetString()
338 c.Assert(err, IsNil)
339- c.Check(capName, Equals, "networks-management")
340- }
341+ switch capName {
342+ case "networks-management":
343+ case "static-ipaddresses":
344+ default:
345+ c.Fatalf("unknown capability %q", capName)
346+ }
347+ }
348+}
349+
350+func (suite *TestMAASObjectSuite) assertIPAmong(c *C, jsonObjIP JSONObject, expectIPs ...string) {
351+ apiVersion := suite.TestMAASObject.TestServer.version
352+ expectedURI := getIPAddressesEndpoint(apiVersion)
353+
354+ maasObj, err := jsonObjIP.GetMAASObject()
355+ c.Assert(err, IsNil)
356+ attrs := maasObj.GetMap()
357+ uri, err := attrs["resource_uri"].GetString()
358+ c.Assert(err, IsNil)
359+ c.Assert(uri, Equals, expectedURI)
360+ allocType, err := attrs["alloc_type"].GetFloat64()
361+ c.Assert(err, IsNil)
362+ c.Assert(allocType, Equals, 4.0)
363+ created, err := attrs["created"].GetString()
364+ c.Assert(err, IsNil)
365+ c.Assert(created, Not(Equals), "")
366+ ip, err := attrs["ip"].GetString()
367+ c.Assert(err, IsNil)
368+ if !contains(expectIPs, ip) {
369+ c.Fatalf("expected IP in %v, got %q", expectIPs, ip)
370+ }
371+}
372+
373+func (suite *TestMAASObjectSuite) TestListIPAddresses(c *C) {
374+ ipAddresses := suite.TestMAASObject.GetSubObject("ipaddresses")
375+
376+ // First try without any networks and IPs.
377+ listIPObjects, err := ipAddresses.CallGet("", url.Values{})
378+ c.Assert(err, IsNil)
379+ items, err := listIPObjects.GetArray()
380+ c.Assert(err, IsNil)
381+ c.Assert(items, HasLen, 0)
382+
383+ // Add two networks and some addresses to each one.
384+ suite.TestMAASObject.TestServer.NewNetwork(
385+ `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`,
386+ )
387+ suite.TestMAASObject.TestServer.NewNetwork(
388+ `{"name": "net_2", "ip": "0.2.2.0", "netmask": "255.255.255.0"}`,
389+ )
390+ suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.3", "net_1")
391+ suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.4", "net_1")
392+ suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.5", "net_1")
393+ suite.TestMAASObject.TestServer.NewIPAddress("0.2.2.3", "net_2")
394+ suite.TestMAASObject.TestServer.NewIPAddress("0.2.2.4", "net_2")
395+
396+ // List all addresses and verify the needed response fields are set.
397+ listIPObjects, err = ipAddresses.CallGet("", url.Values{})
398+ c.Assert(err, IsNil)
399+ items, err = listIPObjects.GetArray()
400+ c.Assert(err, IsNil)
401+ c.Assert(items, HasLen, 5)
402+
403+ for _, ipObj := range items {
404+ suite.assertIPAmong(
405+ c, ipObj,
406+ "0.1.2.3", "0.1.2.4", "0.1.2.5", "0.2.2.3", "0.2.2.4",
407+ )
408+ }
409+
410+ // Remove all net_1 IPs.
411+ removed := suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.3")
412+ c.Assert(removed, Equals, true)
413+ removed = suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.4")
414+ c.Assert(removed, Equals, true)
415+ removed = suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.5")
416+ c.Assert(removed, Equals, true)
417+ // Remove the last IP twice, should be OK and return false.
418+ removed = suite.TestMAASObject.TestServer.RemoveIPAddress("0.1.2.5")
419+ c.Assert(removed, Equals, false)
420+
421+ // List again.
422+ listIPObjects, err = ipAddresses.CallGet("", url.Values{})
423+ c.Assert(err, IsNil)
424+ items, err = listIPObjects.GetArray()
425+ c.Assert(err, IsNil)
426+ c.Assert(items, HasLen, 2)
427+ for _, ipObj := range items {
428+ suite.assertIPAmong(
429+ c, ipObj,
430+ "0.2.2.3", "0.2.2.4",
431+ )
432+ }
433+}
434+
435+func (suite *TestMAASObjectSuite) TestReserveIPAddress(c *C) {
436+ suite.TestMAASObject.TestServer.NewNetwork(
437+ `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`,
438+ )
439+ ipAddresses := suite.TestMAASObject.GetSubObject("ipaddresses")
440+ // First try "reserve" with requested_address set.
441+ params := url.Values{"network": []string{"0.1.2.0/24"}, "requested_address": []string{"0.1.2.42"}}
442+ res, err := ipAddresses.CallPost("reserve", params)
443+ c.Assert(err, IsNil)
444+ suite.assertIPAmong(c, res, "0.1.2.42")
445+
446+ // Now try "reserve" without requested_address.
447+ delete(params, "requested_address")
448+ res, err = ipAddresses.CallPost("reserve", params)
449+ c.Assert(err, IsNil)
450+ suite.assertIPAmong(c, res, "0.1.2.2")
451+}
452+
453+func (suite *TestMAASObjectSuite) TestReleaseIPAddress(c *C) {
454+ suite.TestMAASObject.TestServer.NewNetwork(
455+ `{"name": "net_1", "ip": "0.1.2.0", "netmask": "255.255.255.0"}`,
456+ )
457+ suite.TestMAASObject.TestServer.NewIPAddress("0.1.2.3", "net_1")
458+ ipAddresses := suite.TestMAASObject.GetSubObject("ipaddresses")
459+
460+ // Try with non-existing address - should return 404.
461+ params := url.Values{"ip": []string{"0.2.2.1"}}
462+ _, err := ipAddresses.CallPost("release", params)
463+ c.Assert(err, ErrorMatches, `(\n|.)*404 Not Found(\n|.)*`)
464+
465+ // Now with existing one - all OK.
466+ params = url.Values{"ip": []string{"0.1.2.3"}}
467+ _, err = ipAddresses.CallPost("release", params)
468+ c.Assert(err, IsNil)
469+
470+ // Ensure it got removed.
471+ c.Assert(suite.TestMAASObject.TestServer.ipAddressesPerNetwork["net_1"], HasLen, 0)
472+
473+ // Try again, should return 404.
474+ _, err = ipAddresses.CallPost("release", params)
475+ c.Assert(err, ErrorMatches, `(\n|.)*404 Not Found(\n|.)*`)
476 }
477
478 const nodeDetailsXML = `<?xml version="1.0" standalone="yes" ?>

Subscribers

People subscribed via source and target branches

to all changes: