blob: aa582daf93cd34dbce5a804236b83087f3cc5646 [file] [log] [blame]
David K. Bainbridge528b3182017-01-23 08:51:59 -08001// Copyright 2012-2016 Canonical Ltd.
2// Licensed under the LGPLv3, see LICENCE file for details.
3
4package gomaasapi
5
6import (
7 "bufio"
8 "bytes"
9 "encoding/base64"
10 "encoding/json"
11 "fmt"
12 "io/ioutil"
13 "mime/multipart"
14 "net"
15 "net/http"
16 "net/http/httptest"
17 "net/url"
18 "regexp"
19 "sort"
20 "strconv"
21 "strings"
22 "sync"
23 "text/template"
24 "time"
25
26 "gopkg.in/mgo.v2/bson"
27)
28
29// TestMAASObject is a fake MAAS server MAASObject.
30type TestMAASObject struct {
31 MAASObject
32 TestServer *TestServer
33}
34
35// checkError is a shorthand helper that panics if err is not nil.
36func checkError(err error) {
37 if err != nil {
38 panic(err)
39 }
40}
41
42// NewTestMAAS returns a TestMAASObject that implements the MAASObject
43// interface and thus can be used as a test object instead of the one returned
44// by gomaasapi.NewMAAS().
45func NewTestMAAS(version string) *TestMAASObject {
46 server := NewTestServer(version)
47 authClient, err := NewAnonymousClient(server.URL, version)
48 checkError(err)
49 maas := NewMAAS(*authClient)
50 return &TestMAASObject{*maas, server}
51}
52
53// Close shuts down the test server.
54func (testMAASObject *TestMAASObject) Close() {
55 testMAASObject.TestServer.Close()
56}
57
58// A TestServer is an HTTP server listening on a system-chosen port on the
59// local loopback interface, which simulates the behavior of a MAAS server.
60// It is intendend for use in end-to-end HTTP tests using the gomaasapi
61// library.
62type TestServer struct {
63 *httptest.Server
64 serveMux *http.ServeMux
65 client Client
66 nodes map[string]MAASObject
67 ownedNodes map[string]bool
68 // mapping system_id -> list of operations performed.
69 nodeOperations map[string][]string
70 // list of operations performed at the /nodes/ level.
71 nodesOperations []string
72 // mapping system_id -> list of Values passed when performing
73 // operations
74 nodeOperationRequestValues map[string][]url.Values
75 // list of Values passed when performing operations at the
76 // /nodes/ level.
77 nodesOperationRequestValues []url.Values
78 nodeMetadata map[string]Node
79 files map[string]MAASObject
80 networks map[string]MAASObject
81 networksPerNode map[string][]string
82 ipAddressesPerNetwork map[string][]string
83 version string
84 macAddressesPerNetwork map[string]map[string]JSONObject
85 nodeDetails map[string]string
86 zones map[string]JSONObject
87 // bootImages is a map of nodegroup UUIDs to boot-image objects.
88 bootImages map[string][]JSONObject
89 // nodegroupsInterfaces is a map of nodegroup UUIDs to interface
90 // objects.
91 nodegroupsInterfaces map[string][]JSONObject
92
93 // versionJSON is the response to the /version/ endpoint listing the
94 // capabilities of the MAAS server.
95 versionJSON string
96
97 // devices is a map of device UUIDs to devices.
98 devices map[string]*TestDevice
99
100 subnets map[uint]TestSubnet
101 subnetNameToID map[string]uint
102 nextSubnet uint
103 spaces map[uint]*TestSpace
104 spaceNameToID map[string]uint
105 nextSpace uint
106 vlans map[int]TestVLAN
107 nextVLAN int
108}
109
110type TestDevice struct {
111 IPAddresses []string
112 SystemId string
113 MACAddresses []string
114 Parent string
115 Hostname string
116
117 // Not part of the device definition but used by the template.
118 APIVersion string
119}
120
121func getNodesEndpoint(version string) string {
122 return fmt.Sprintf("/api/%s/nodes/", version)
123}
124
125func getNodeURL(version, systemId string) string {
126 return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId)
127}
128
129func getNodeURLRE(version string) *regexp.Regexp {
130 reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", regexp.QuoteMeta(version))
131 return regexp.MustCompile(reString)
132}
133
134func getDevicesEndpoint(version string) string {
135 return fmt.Sprintf("/api/%s/devices/", version)
136}
137
138func getDeviceURL(version, systemId string) string {
139 return fmt.Sprintf("/api/%s/devices/%s/", version, systemId)
140}
141
142func getDeviceURLRE(version string) *regexp.Regexp {
143 reString := fmt.Sprintf("^/api/%s/devices/([^/]*)/$", regexp.QuoteMeta(version))
144 return regexp.MustCompile(reString)
145}
146
147func getFilesEndpoint(version string) string {
148 return fmt.Sprintf("/api/%s/files/", version)
149}
150
151func getFileURL(version, filename string) string {
152 // Uses URL object so filename is correctly percent-escaped
153 url := url.URL{}
154 url.Path = fmt.Sprintf("/api/%s/files/%s/", version, filename)
155 return url.String()
156}
157
158func getFileURLRE(version string) *regexp.Regexp {
159 reString := fmt.Sprintf("^/api/%s/files/(.*)/$", regexp.QuoteMeta(version))
160 return regexp.MustCompile(reString)
161}
162
163func getNetworksEndpoint(version string) string {
164 return fmt.Sprintf("/api/%s/networks/", version)
165}
166
167func getNetworkURL(version, name string) string {
168 return fmt.Sprintf("/api/%s/networks/%s/", version, name)
169}
170
171func getNetworkURLRE(version string) *regexp.Regexp {
172 reString := fmt.Sprintf("^/api/%s/networks/(.*)/$", regexp.QuoteMeta(version))
173 return regexp.MustCompile(reString)
174}
175
176func getIPAddressesEndpoint(version string) string {
177 return fmt.Sprintf("/api/%s/ipaddresses/", version)
178}
179
180func getMACAddressURL(version, systemId, macAddress string) string {
181 return fmt.Sprintf("/api/%s/nodes/%s/macs/%s/", version, systemId, url.QueryEscape(macAddress))
182}
183
184func getVersionURL(version string) string {
185 return fmt.Sprintf("/api/%s/version/", version)
186}
187
188func getNodegroupsEndpoint(version string) string {
189 return fmt.Sprintf("/api/%s/nodegroups/", version)
190}
191
192func getNodegroupURL(version, uuid string) string {
193 return fmt.Sprintf("/api/%s/nodegroups/%s/", version, uuid)
194}
195
196func getNodegroupsInterfacesURLRE(version string) *regexp.Regexp {
197 reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/interfaces/$", regexp.QuoteMeta(version))
198 return regexp.MustCompile(reString)
199}
200
201func getBootimagesURLRE(version string) *regexp.Regexp {
202 reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/boot-images/$", regexp.QuoteMeta(version))
203 return regexp.MustCompile(reString)
204}
205
206func getZonesEndpoint(version string) string {
207 return fmt.Sprintf("/api/%s/zones/", version)
208}
209
210// Clear clears all the fake data stored and recorded by the test server
211// (nodes, recorded operations, etc.).
212func (server *TestServer) Clear() {
213 server.nodes = make(map[string]MAASObject)
214 server.ownedNodes = make(map[string]bool)
215 server.nodesOperations = make([]string, 0)
216 server.nodeOperations = make(map[string][]string)
217 server.nodesOperationRequestValues = make([]url.Values, 0)
218 server.nodeOperationRequestValues = make(map[string][]url.Values)
219 server.nodeMetadata = make(map[string]Node)
220 server.files = make(map[string]MAASObject)
221 server.networks = make(map[string]MAASObject)
222 server.networksPerNode = make(map[string][]string)
223 server.ipAddressesPerNetwork = make(map[string][]string)
224 server.macAddressesPerNetwork = make(map[string]map[string]JSONObject)
225 server.nodeDetails = make(map[string]string)
226 server.bootImages = make(map[string][]JSONObject)
227 server.nodegroupsInterfaces = make(map[string][]JSONObject)
228 server.zones = make(map[string]JSONObject)
229 server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses","devices-management","network-deployment-ubuntu"]}`
230 server.devices = make(map[string]*TestDevice)
231 server.subnets = make(map[uint]TestSubnet)
232 server.subnetNameToID = make(map[string]uint)
233 server.nextSubnet = 1
234 server.spaces = make(map[uint]*TestSpace)
235 server.spaceNameToID = make(map[string]uint)
236 server.nextSpace = 1
237 server.vlans = make(map[int]TestVLAN)
238 server.nextVLAN = 1
239}
240
241// SetVersionJSON sets the JSON response (capabilities) returned from the
242// /version/ endpoint.
243func (server *TestServer) SetVersionJSON(json string) {
244 server.versionJSON = json
245}
246
247// NodesOperations returns the list of operations performed at the /nodes/
248// level.
249func (server *TestServer) NodesOperations() []string {
250 return server.nodesOperations
251}
252
253// NodeOperations returns the map containing the list of the operations
254// performed for each node.
255func (server *TestServer) NodeOperations() map[string][]string {
256 return server.nodeOperations
257}
258
259// NodesOperationRequestValues returns the list of url.Values extracted
260// from the request used when performing operations at the /nodes/ level.
261func (server *TestServer) NodesOperationRequestValues() []url.Values {
262 return server.nodesOperationRequestValues
263}
264
265// NodeOperationRequestValues returns the map containing the list of the
266// url.Values extracted from the request used when performing operations
267// on nodes.
268func (server *TestServer) NodeOperationRequestValues() map[string][]url.Values {
269 return server.nodeOperationRequestValues
270}
271
272func parseRequestValues(request *http.Request) url.Values {
273 var requestValues url.Values
274 if request.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
275 if request.PostForm == nil {
276 if err := request.ParseForm(); err != nil {
277 panic(err)
278 }
279 }
280 requestValues = request.PostForm
281 }
282 return requestValues
283}
284
285func (server *TestServer) addNodesOperation(operation string, request *http.Request) url.Values {
286 requestValues := parseRequestValues(request)
287 server.nodesOperations = append(server.nodesOperations, operation)
288 server.nodesOperationRequestValues = append(server.nodesOperationRequestValues, requestValues)
289 return requestValues
290}
291
292func (server *TestServer) addNodeOperation(systemId, operation string, request *http.Request) url.Values {
293 operations, present := server.nodeOperations[systemId]
294 operationRequestValues, present2 := server.nodeOperationRequestValues[systemId]
295 if present != present2 {
296 panic("inconsistent state: nodeOperations and nodeOperationRequestValues don't have the same keys.")
297 }
298 requestValues := parseRequestValues(request)
299 if !present {
300 operations = []string{operation}
301 operationRequestValues = []url.Values{requestValues}
302 } else {
303 operations = append(operations, operation)
304 operationRequestValues = append(operationRequestValues, requestValues)
305 }
306 server.nodeOperations[systemId] = operations
307 server.nodeOperationRequestValues[systemId] = operationRequestValues
308 return requestValues
309}
310
311// NewNode creates a MAAS node. The provided string should be a valid json
312// string representing a map and contain a string value for the key
313// 'system_id'. e.g. `{"system_id": "mysystemid"}`.
314// If one of these conditions is not met, NewNode panics.
315func (server *TestServer) NewNode(jsonText string) MAASObject {
316 var attrs map[string]interface{}
317 err := json.Unmarshal([]byte(jsonText), &attrs)
318 checkError(err)
319 systemIdEntry, hasSystemId := attrs["system_id"]
320 if !hasSystemId {
321 panic("The given map json string does not contain a 'system_id' value.")
322 }
323 systemId := systemIdEntry.(string)
324 attrs[resourceURI] = getNodeURL(server.version, systemId)
325 if _, hasStatus := attrs["status"]; !hasStatus {
326 attrs["status"] = NodeStatusDeployed
327 }
328 obj := newJSONMAASObject(attrs, server.client)
329 server.nodes[systemId] = obj
330 return obj
331}
332
333// Nodes returns a map associating all the nodes' system ids with the nodes'
334// objects.
335func (server *TestServer) Nodes() map[string]MAASObject {
336 return server.nodes
337}
338
339// OwnedNodes returns a map whose keys represent the nodes that are currently
340// allocated.
341func (server *TestServer) OwnedNodes() map[string]bool {
342 return server.ownedNodes
343}
344
345// NewFile creates a file in the test MAAS server.
346func (server *TestServer) NewFile(filename string, filecontent []byte) MAASObject {
347 attrs := make(map[string]interface{})
348 attrs[resourceURI] = getFileURL(server.version, filename)
349 base64Content := base64.StdEncoding.EncodeToString(filecontent)
350 attrs["content"] = base64Content
351 attrs["filename"] = filename
352
353 // Allocate an arbitrary URL here. It would be nice if the caller
354 // could do this, but that would change the API and require many
355 // changes.
356 escapedName := url.QueryEscape(filename)
357 attrs["anon_resource_uri"] = "/maas/1.0/files/?op=get_by_key&key=" + escapedName + "_key"
358
359 obj := newJSONMAASObject(attrs, server.client)
360 server.files[filename] = obj
361 return obj
362}
363
364func (server *TestServer) Files() map[string]MAASObject {
365 return server.files
366}
367
368// ChangeNode updates a node with the given key/value.
369func (server *TestServer) ChangeNode(systemId, key, value string) {
370 node, found := server.nodes[systemId]
371 if !found {
372 panic("No node with such 'system_id'.")
373 }
374 node.GetMap()[key] = maasify(server.client, value)
375}
376
377// NewIPAddress creates a new static IP address reservation for the
378// given network/subnet and ipAddress. While networks is being deprecated
379// try the given name as both a netowrk and a subnet.
380func (server *TestServer) NewIPAddress(ipAddress, networkOrSubnet string) {
381 _, foundNetwork := server.networks[networkOrSubnet]
382 subnetID, foundSubnet := server.subnetNameToID[networkOrSubnet]
383
384 if (foundNetwork || foundSubnet) == false {
385 panic("No such network or subnet: " + networkOrSubnet)
386 }
387 if foundNetwork {
388 ips, found := server.ipAddressesPerNetwork[networkOrSubnet]
389 if found {
390 ips = append(ips, ipAddress)
391 } else {
392 ips = []string{ipAddress}
393 }
394 server.ipAddressesPerNetwork[networkOrSubnet] = ips
395 } else {
396 subnet := server.subnets[subnetID]
397 netIp := net.ParseIP(ipAddress)
398 if netIp == nil {
399 panic(ipAddress + " is invalid")
400 }
401 ip := IPFromNetIP(netIp)
402 ip.Purpose = []string{"assigned-ip"}
403 subnet.InUseIPAddresses = append(subnet.InUseIPAddresses, ip)
404 server.subnets[subnetID] = subnet
405 }
406}
407
408// RemoveIPAddress removes the given existing ipAddress and returns
409// whether it was actually removed.
410func (server *TestServer) RemoveIPAddress(ipAddress string) bool {
411 for network, ips := range server.ipAddressesPerNetwork {
412 for i, ip := range ips {
413 if ip == ipAddress {
414 ips = append(ips[:i], ips[i+1:]...)
415 server.ipAddressesPerNetwork[network] = ips
416 return true
417 }
418 }
419 }
420 for _, device := range server.devices {
421 for i, addr := range device.IPAddresses {
422 if addr == ipAddress {
423 device.IPAddresses = append(device.IPAddresses[:i], device.IPAddresses[i+1:]...)
424 return true
425 }
426 }
427 }
428 return false
429}
430
431// IPAddresses returns the map with network names as keys and slices
432// of IP addresses belonging to each network as values.
433func (server *TestServer) IPAddresses() map[string][]string {
434 return server.ipAddressesPerNetwork
435}
436
437// NewNetwork creates a network in the test MAAS server
438func (server *TestServer) NewNetwork(jsonText string) MAASObject {
439 var attrs map[string]interface{}
440 err := json.Unmarshal([]byte(jsonText), &attrs)
441 checkError(err)
442 nameEntry, hasName := attrs["name"]
443 _, hasIP := attrs["ip"]
444 _, hasNetmask := attrs["netmask"]
445 if !hasName || !hasIP || !hasNetmask {
446 panic("The given map json string does not contain a 'name', 'ip', or 'netmask' value.")
447 }
448 // TODO(gz): Sanity checking done on other fields
449 name := nameEntry.(string)
450 attrs[resourceURI] = getNetworkURL(server.version, name)
451 obj := newJSONMAASObject(attrs, server.client)
452 server.networks[name] = obj
453 return obj
454}
455
456// NewNodegroupInterface adds a nodegroup-interface, for the specified
457// nodegroup, in the test MAAS server.
458func (server *TestServer) NewNodegroupInterface(uuid, jsonText string) JSONObject {
459 _, ok := server.bootImages[uuid]
460 if !ok {
461 panic("no nodegroup with the given UUID")
462 }
463 var attrs map[string]interface{}
464 err := json.Unmarshal([]byte(jsonText), &attrs)
465 checkError(err)
466 requiredMembers := []string{"ip_range_high", "ip_range_low", "broadcast_ip", "static_ip_range_low", "static_ip_range_high", "name", "ip", "subnet_mask", "management", "interface"}
467 for _, member := range requiredMembers {
468 _, hasMember := attrs[member]
469 if !hasMember {
470 panic(fmt.Sprintf("The given map json string does not contain a required %q", member))
471 }
472 }
473 obj := maasify(server.client, attrs)
474 server.nodegroupsInterfaces[uuid] = append(server.nodegroupsInterfaces[uuid], obj)
475 return obj
476}
477
478func (server *TestServer) ConnectNodeToNetwork(systemId, name string) {
479 _, hasNode := server.nodes[systemId]
480 if !hasNode {
481 panic("no node with the given system id")
482 }
483 _, hasNetwork := server.networks[name]
484 if !hasNetwork {
485 panic("no network with the given name")
486 }
487 networkNames, _ := server.networksPerNode[systemId]
488 server.networksPerNode[systemId] = append(networkNames, name)
489}
490
491func (server *TestServer) ConnectNodeToNetworkWithMACAddress(systemId, networkName, macAddress string) {
492 node, hasNode := server.nodes[systemId]
493 if !hasNode {
494 panic("no node with the given system id")
495 }
496 if _, hasNetwork := server.networks[networkName]; !hasNetwork {
497 panic("no network with the given name")
498 }
499 networkNames, _ := server.networksPerNode[systemId]
500 server.networksPerNode[systemId] = append(networkNames, networkName)
501 attrs := make(map[string]interface{})
502 attrs[resourceURI] = getMACAddressURL(server.version, systemId, macAddress)
503 attrs["mac_address"] = macAddress
504 array := []JSONObject{}
505 if set, ok := node.GetMap()["macaddress_set"]; ok {
506 var err error
507 array, err = set.GetArray()
508 if err != nil {
509 panic(err)
510 }
511 }
512 array = append(array, maasify(server.client, attrs))
513 node.GetMap()["macaddress_set"] = JSONObject{value: array, client: server.client}
514 if _, ok := server.macAddressesPerNetwork[networkName]; !ok {
515 server.macAddressesPerNetwork[networkName] = map[string]JSONObject{}
516 }
517 server.macAddressesPerNetwork[networkName][systemId] = maasify(server.client, attrs)
518}
519
520// AddBootImage adds a boot-image object to the specified nodegroup.
521func (server *TestServer) AddBootImage(nodegroupUUID string, jsonText string) {
522 var attrs map[string]interface{}
523 err := json.Unmarshal([]byte(jsonText), &attrs)
524 checkError(err)
525 if _, ok := attrs["architecture"]; !ok {
526 panic("The boot-image json string does not contain an 'architecture' value.")
527 }
528 if _, ok := attrs["release"]; !ok {
529 panic("The boot-image json string does not contain a 'release' value.")
530 }
531 obj := maasify(server.client, attrs)
532 server.bootImages[nodegroupUUID] = append(server.bootImages[nodegroupUUID], obj)
533}
534
535// AddZone adds a physical zone to the server.
536func (server *TestServer) AddZone(name, description string) {
537 attrs := map[string]interface{}{
538 "name": name,
539 "description": description,
540 }
541 obj := maasify(server.client, attrs)
542 server.zones[name] = obj
543}
544
545func (server *TestServer) AddDevice(device *TestDevice) {
546 server.devices[device.SystemId] = device
547}
548
549func (server *TestServer) Devices() map[string]*TestDevice {
550 return server.devices
551}
552
553// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down.
554func NewTestServer(version string) *TestServer {
555 server := &TestServer{version: version}
556
557 serveMux := http.NewServeMux()
558 devicesURL := getDevicesEndpoint(server.version)
559 // Register handler for '/api/<version>/devices/*'.
560 serveMux.HandleFunc(devicesURL, func(w http.ResponseWriter, r *http.Request) {
561 devicesHandler(server, w, r)
562 })
563 nodesURL := getNodesEndpoint(server.version)
564 // Register handler for '/api/<version>/nodes/*'.
565 serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) {
566 nodesHandler(server, w, r)
567 })
568 filesURL := getFilesEndpoint(server.version)
569 // Register handler for '/api/<version>/files/*'.
570 serveMux.HandleFunc(filesURL, func(w http.ResponseWriter, r *http.Request) {
571 filesHandler(server, w, r)
572 })
573 networksURL := getNetworksEndpoint(server.version)
574 // Register handler for '/api/<version>/networks/'.
575 serveMux.HandleFunc(networksURL, func(w http.ResponseWriter, r *http.Request) {
576 networksHandler(server, w, r)
577 })
578 ipAddressesURL := getIPAddressesEndpoint(server.version)
579 // Register handler for '/api/<version>/ipaddresses/'.
580 serveMux.HandleFunc(ipAddressesURL, func(w http.ResponseWriter, r *http.Request) {
581 ipAddressesHandler(server, w, r)
582 })
583 versionURL := getVersionURL(server.version)
584 // Register handler for '/api/<version>/version/'.
585 serveMux.HandleFunc(versionURL, func(w http.ResponseWriter, r *http.Request) {
586 versionHandler(server, w, r)
587 })
588 // Register handler for '/api/<version>/nodegroups/*'.
589 nodegroupsURL := getNodegroupsEndpoint(server.version)
590 serveMux.HandleFunc(nodegroupsURL, func(w http.ResponseWriter, r *http.Request) {
591 nodegroupsHandler(server, w, r)
592 })
593
594 // Register handler for '/api/<version>/zones/*'.
595 zonesURL := getZonesEndpoint(server.version)
596 serveMux.HandleFunc(zonesURL, func(w http.ResponseWriter, r *http.Request) {
597 zonesHandler(server, w, r)
598 })
599
600 subnetsURL := getSubnetsEndpoint(server.version)
601 serveMux.HandleFunc(subnetsURL, func(w http.ResponseWriter, r *http.Request) {
602 subnetsHandler(server, w, r)
603 })
604
605 spacesURL := getSpacesEndpoint(server.version)
606 serveMux.HandleFunc(spacesURL, func(w http.ResponseWriter, r *http.Request) {
607 spacesHandler(server, w, r)
608 })
609
610 vlansURL := getVLANsEndpoint(server.version)
611 serveMux.HandleFunc(vlansURL, func(w http.ResponseWriter, r *http.Request) {
612 vlansHandler(server, w, r)
613 })
614
615 var mu sync.Mutex
616 singleFile := func(w http.ResponseWriter, req *http.Request) {
617 mu.Lock()
618 defer mu.Unlock()
619 serveMux.ServeHTTP(w, req)
620 }
621
622 newServer := httptest.NewServer(http.HandlerFunc(singleFile))
623 client, err := NewAnonymousClient(newServer.URL, "1.0")
624 checkError(err)
625 server.Server = newServer
626 server.serveMux = serveMux
627 server.client = *client
628 server.Clear()
629 return server
630}
631
632// devicesHandler handles requests for '/api/<version>/devices/*'.
633func devicesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
634 values, err := url.ParseQuery(r.URL.RawQuery)
635 checkError(err)
636 op := values.Get("op")
637 deviceURLRE := getDeviceURLRE(server.version)
638 deviceURLMatch := deviceURLRE.FindStringSubmatch(r.URL.Path)
639 devicesURL := getDevicesEndpoint(server.version)
640 switch {
641 case r.URL.Path == devicesURL:
642 devicesTopLevelHandler(server, w, r, op)
643 case deviceURLMatch != nil:
644 // Request for a single device.
645 deviceHandler(server, w, r, deviceURLMatch[1], op)
646 default:
647 // Default handler: not found.
648 http.NotFoundHandler().ServeHTTP(w, r)
649 }
650}
651
652// devicesTopLevelHandler handles a request for /api/<version>/devices/
653// (with no device id following as part of the path).
654func devicesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
655 switch {
656 case r.Method == "GET" && op == "list":
657 // Device listing operation.
658 deviceListingHandler(server, w, r)
659 case r.Method == "POST" && op == "new":
660 newDeviceHandler(server, w, r)
661 default:
662 w.WriteHeader(http.StatusBadRequest)
663 }
664}
665
666func macMatches(mac string, device *TestDevice) bool {
667 return contains(device.MACAddresses, mac)
668}
669
670// deviceListingHandler handles requests for '/devices/'.
671func deviceListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
672 values, err := url.ParseQuery(r.URL.RawQuery)
673 checkError(err)
674 // TODO(mfoord): support filtering by hostname and id
675 macs, hasMac := values["mac_address"]
676 var matchedDevices []*TestDevice
677 if !hasMac {
678 for _, device := range server.devices {
679 matchedDevices = append(matchedDevices, device)
680 }
681 } else {
682 for _, mac := range macs {
683 for _, device := range server.devices {
684 if macMatches(mac, device) {
685 matchedDevices = append(matchedDevices, device)
686 }
687 }
688 }
689 }
690 deviceChunks := make([]string, len(matchedDevices))
691 for i := range matchedDevices {
692 deviceChunks[i] = renderDevice(matchedDevices[i])
693 }
694 json := fmt.Sprintf("[%v]", strings.Join(deviceChunks, ", "))
695
696 w.WriteHeader(http.StatusOK)
697 fmt.Fprint(w, json)
698}
699
700var templateFuncs = template.FuncMap{
701 "quotedList": func(items []string) string {
702 var pieces []string
703 for _, item := range items {
704 pieces = append(pieces, fmt.Sprintf("%q", item))
705 }
706 return strings.Join(pieces, ", ")
707 },
708 "last": func(items []string) []string {
709 if len(items) == 0 {
710 return []string{}
711 }
712 return items[len(items)-1:]
713 },
714 "allButLast": func(items []string) []string {
715 if len(items) < 2 {
716 return []string{}
717 }
718 return items[0 : len(items)-1]
719 },
720}
721
722const (
723 // The json template for generating new devices.
724 // TODO(mfoord): set resource_uri in MAC addresses
725 deviceTemplate = `{
726 "macaddress_set": [{{range .MACAddresses | allButLast}}
727 {
728 "mac_address": "{{.}}"
729 },{{end}}{{range .MACAddresses | last}}
730 {
731 "mac_address": "{{.}}"
732 }{{end}}
733 ],
734 "zone": {
735 "resource_uri": "/MAAS/api/{{.APIVersion}}/zones/default/",
736 "name": "default",
737 "description": ""
738 },
739 "parent": "{{.Parent}}",
740 "ip_addresses": [{{.IPAddresses | quotedList }}],
741 "hostname": "{{.Hostname}}",
742 "tag_names": [],
743 "owner": "maas-admin",
744 "system_id": "{{.SystemId}}",
745 "resource_uri": "/MAAS/api/{{.APIVersion}}/devices/{{.SystemId}}/"
746}`
747)
748
749func renderDevice(device *TestDevice) string {
750 t := template.New("Device template")
751 t = t.Funcs(templateFuncs)
752 t, err := t.Parse(deviceTemplate)
753 checkError(err)
754 var buf bytes.Buffer
755 err = t.Execute(&buf, device)
756 checkError(err)
757 return buf.String()
758}
759
760func getValue(values url.Values, value string) (string, bool) {
761 result, hasResult := values[value]
762 if !hasResult || len(result) != 1 || result[0] == "" {
763 return "", false
764 }
765 return result[0], true
766}
767
768func getValues(values url.Values, key string) ([]string, bool) {
769 result, hasResult := values[key]
770 if !hasResult {
771 return nil, false
772 }
773 var output []string
774 for _, val := range result {
775 if val != "" {
776 output = append(output, val)
777 }
778 }
779 if len(output) == 0 {
780 return nil, false
781 }
782 return output, true
783}
784
785// newDeviceHandler creates, stores and returns new devices.
786func newDeviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
787 err := r.ParseForm()
788 checkError(err)
789 values := r.PostForm
790
791 // TODO(mfood): generate a "proper" uuid for the system Id.
792 uuid, err := generateNonce()
793 checkError(err)
794 systemId := fmt.Sprintf("node-%v", uuid)
795 // At least one MAC address must be specified.
796 // TODO(mfoord) we only support a single MAC in the test server.
797 macs, hasMacs := getValues(values, "mac_addresses")
798
799 // hostname and parent are optional.
800 // TODO(mfoord): we require both to be set in the test server.
801 hostname, hasHostname := getValue(values, "hostname")
802 parent, hasParent := getValue(values, "parent")
803 if !hasHostname || !hasMacs || !hasParent {
804 w.WriteHeader(http.StatusBadRequest)
805 return
806 }
807
808 device := &TestDevice{
809 MACAddresses: macs,
810 APIVersion: server.version,
811 Parent: parent,
812 Hostname: hostname,
813 SystemId: systemId,
814 }
815
816 deviceJSON := renderDevice(device)
817 server.devices[systemId] = device
818
819 w.WriteHeader(http.StatusOK)
820 fmt.Fprint(w, deviceJSON)
821 return
822}
823
824// deviceHandler handles requests for '/api/<version>/devices/<system_id>/'.
825func deviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
826 device, ok := server.devices[systemId]
827 if !ok {
828 http.NotFoundHandler().ServeHTTP(w, r)
829 return
830 }
831 if r.Method == "GET" {
832 deviceJSON := renderDevice(device)
833 if operation == "" {
834 w.WriteHeader(http.StatusOK)
835 fmt.Fprint(w, deviceJSON)
836 return
837 } else {
838 w.WriteHeader(http.StatusBadRequest)
839 return
840 }
841 }
842 if r.Method == "POST" {
843 if operation == "claim_sticky_ip_address" {
844 err := r.ParseForm()
845 checkError(err)
846 values := r.PostForm
847 // TODO(mfoord): support optional mac_address parameter
848 // TODO(mfoord): requested_address should be optional
849 // and we should generate one if it isn't provided.
850 address, hasAddress := getValue(values, "requested_address")
851 if !hasAddress {
852 w.WriteHeader(http.StatusBadRequest)
853 return
854 }
855 checkError(err)
856 device.IPAddresses = append(device.IPAddresses, address)
857 deviceJSON := renderDevice(device)
858 w.WriteHeader(http.StatusOK)
859 fmt.Fprint(w, deviceJSON)
860 return
861 } else {
862 w.WriteHeader(http.StatusBadRequest)
863 return
864 }
865 } else if r.Method == "DELETE" {
866 delete(server.devices, systemId)
867 w.WriteHeader(http.StatusNoContent)
868 return
869
870 }
871
872 // TODO(mfoord): support PUT method for updating device
873 http.NotFoundHandler().ServeHTTP(w, r)
874}
875
876// nodesHandler handles requests for '/api/<version>/nodes/*'.
877func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
878 values, err := url.ParseQuery(r.URL.RawQuery)
879 checkError(err)
880 op := values.Get("op")
881 nodeURLRE := getNodeURLRE(server.version)
882 nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path)
883 nodesURL := getNodesEndpoint(server.version)
884 switch {
885 case r.URL.Path == nodesURL:
886 nodesTopLevelHandler(server, w, r, op)
887 case nodeURLMatch != nil:
888 // Request for a single node.
889 nodeHandler(server, w, r, nodeURLMatch[1], op)
890 default:
891 // Default handler: not found.
892 http.NotFoundHandler().ServeHTTP(w, r)
893 }
894}
895
896// nodeHandler handles requests for '/api/<version>/nodes/<system_id>/'.
897func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
898 node, ok := server.nodes[systemId]
899 if !ok {
900 http.NotFoundHandler().ServeHTTP(w, r)
901 return
902 }
903 UUID, UUIDError := node.values["system_id"].GetString()
904 if UUIDError == nil {
905 i, err := JSONObjectFromStruct(server.client, server.nodeMetadata[UUID].Interfaces)
906 checkError(err)
907 node.values["interface_set"] = i
908 }
909
910 if r.Method == "GET" {
911 if operation == "" {
912 w.WriteHeader(http.StatusOK)
913 fmt.Fprint(w, marshalNode(node))
914 return
915 } else if operation == "details" {
916 nodeDetailsHandler(server, w, r, systemId)
917 return
918 } else {
919 w.WriteHeader(http.StatusBadRequest)
920 return
921 }
922 }
923 if r.Method == "POST" {
924 // The only operations supported are "start", "stop" and "release".
925 if operation == "start" || operation == "stop" || operation == "release" {
926 // Record operation on node.
927 server.addNodeOperation(systemId, operation, r)
928
929 if operation == "release" {
930 delete(server.OwnedNodes(), systemId)
931 }
932
933 w.WriteHeader(http.StatusOK)
934 fmt.Fprint(w, marshalNode(node))
935 return
936 }
937
938 w.WriteHeader(http.StatusBadRequest)
939 return
940 }
941 if r.Method == "DELETE" {
942 delete(server.nodes, systemId)
943 w.WriteHeader(http.StatusOK)
944 return
945 }
946 http.NotFoundHandler().ServeHTTP(w, r)
947}
948
949func contains(slice []string, val string) bool {
950 for _, item := range slice {
951 if item == val {
952 return true
953 }
954 }
955 return false
956}
957
958// nodeListingHandler handles requests for '/nodes/'.
959func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
960 values, err := url.ParseQuery(r.URL.RawQuery)
961 checkError(err)
962 ids, hasId := values["id"]
963 var convertedNodes = []map[string]JSONObject{}
964 for systemId, node := range server.nodes {
965 if !hasId || contains(ids, systemId) {
966 convertedNodes = append(convertedNodes, node.GetMap())
967 }
968 }
969 res, err := json.MarshalIndent(convertedNodes, "", " ")
970 checkError(err)
971 w.WriteHeader(http.StatusOK)
972 fmt.Fprint(w, string(res))
973}
974
975// nodeDeploymentStatusHandler handles requests for '/nodes/?op=deployment_status'.
976func nodeDeploymentStatusHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
977 values, err := url.ParseQuery(r.URL.RawQuery)
978 checkError(err)
979 nodes, _ := values["nodes"]
980 var nodeStatus = make(map[string]interface{})
981 for _, systemId := range nodes {
982 node := server.nodes[systemId]
983 field, err := node.GetField("status")
984 if err != nil {
985 continue
986 }
987 switch field {
988 case NodeStatusDeployed:
989 nodeStatus[systemId] = "Deployed"
990 case NodeStatusFailedDeployment:
991 nodeStatus[systemId] = "Failed deployment"
992 default:
993 nodeStatus[systemId] = "Not in Deployment"
994 }
995 }
996 obj := maasify(server.client, nodeStatus)
997 res, err := json.MarshalIndent(obj, "", " ")
998 checkError(err)
999 w.WriteHeader(http.StatusOK)
1000 fmt.Fprint(w, string(res))
1001}
1002
1003// findFreeNode looks for a node that is currently available, and
1004// matches the specified filter.
1005func findFreeNode(server *TestServer, filter url.Values) *MAASObject {
1006 for systemID, node := range server.Nodes() {
1007 _, present := server.OwnedNodes()[systemID]
1008 if !present {
1009 var agentName, nodeName, zoneName, mem, cpuCores, arch string
1010 for k := range filter {
1011 switch k {
1012 case "agent_name":
1013 agentName = filter.Get(k)
1014 case "name":
1015 nodeName = filter.Get(k)
1016 case "zone":
1017 zoneName = filter.Get(k)
1018 case "mem":
1019 mem = filter.Get(k)
1020 case "arch":
1021 arch = filter.Get(k)
1022 case "cpu-cores":
1023 cpuCores = filter.Get(k)
1024 }
1025 }
1026 if nodeName != "" && !matchField(node, "hostname", nodeName) {
1027 continue
1028 }
1029 if zoneName != "" && !matchField(node, "zone", zoneName) {
1030 continue
1031 }
1032 if mem != "" && !matchNumericField(node, "memory", mem) {
1033 continue
1034 }
1035 if arch != "" && !matchArchitecture(node, "architecture", arch) {
1036 continue
1037 }
1038 if cpuCores != "" && !matchNumericField(node, "cpu_count", cpuCores) {
1039 continue
1040 }
1041 if agentName != "" {
1042 agentNameObj := maasify(server.client, agentName)
1043 node.GetMap()["agent_name"] = agentNameObj
1044 } else {
1045 delete(node.GetMap(), "agent_name")
1046 }
1047 return &node
1048 }
1049 }
1050 return nil
1051}
1052
1053func matchArchitecture(node MAASObject, k, v string) bool {
1054 field, err := node.GetField(k)
1055 if err != nil {
1056 return false
1057 }
1058 baseArch := strings.Split(field, "/")
1059 return v == baseArch[0]
1060}
1061
1062func matchNumericField(node MAASObject, k, v string) bool {
1063 field, ok := node.GetMap()[k]
1064 if !ok {
1065 return false
1066 }
1067 nodeVal, err := field.GetFloat64()
1068 if err != nil {
1069 return false
1070 }
1071 constraintVal, err := strconv.ParseFloat(v, 64)
1072 if err != nil {
1073 return false
1074 }
1075 return constraintVal <= nodeVal
1076}
1077
1078func matchField(node MAASObject, k, v string) bool {
1079 field, err := node.GetField(k)
1080 if err != nil {
1081 return false
1082 }
1083 return field == v
1084}
1085
1086// nodesAcquireHandler simulates acquiring a node.
1087func nodesAcquireHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1088 requestValues := server.addNodesOperation("acquire", r)
1089 node := findFreeNode(server, requestValues)
1090 if node == nil {
1091 w.WriteHeader(http.StatusConflict)
1092 } else {
1093 systemId, err := node.GetField("system_id")
1094 checkError(err)
1095 server.OwnedNodes()[systemId] = true
1096 res, err := json.MarshalIndent(node, "", " ")
1097 checkError(err)
1098 // Record operation.
1099 server.addNodeOperation(systemId, "acquire", r)
1100 w.WriteHeader(http.StatusOK)
1101 fmt.Fprint(w, string(res))
1102 }
1103}
1104
1105// nodesReleaseHandler simulates releasing multiple nodes.
1106func nodesReleaseHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1107 server.addNodesOperation("release", r)
1108 values := server.NodesOperationRequestValues()
1109 systemIds := values[len(values)-1]["nodes"]
1110 var unknown []string
1111 for _, systemId := range systemIds {
1112 if _, ok := server.Nodes()[systemId]; !ok {
1113 unknown = append(unknown, systemId)
1114 }
1115 }
1116 if len(unknown) > 0 {
1117 w.WriteHeader(http.StatusBadRequest)
1118 fmt.Fprintf(w, "Unknown node(s): %s.", strings.Join(unknown, ", "))
1119 return
1120 }
1121 var releasedNodes = []map[string]JSONObject{}
1122 for _, systemId := range systemIds {
1123 if _, ok := server.OwnedNodes()[systemId]; !ok {
1124 continue
1125 }
1126 delete(server.OwnedNodes(), systemId)
1127 node := server.Nodes()[systemId]
1128 releasedNodes = append(releasedNodes, node.GetMap())
1129 }
1130 res, err := json.MarshalIndent(releasedNodes, "", " ")
1131 checkError(err)
1132 w.WriteHeader(http.StatusOK)
1133 fmt.Fprint(w, string(res))
1134}
1135
1136// nodesTopLevelHandler handles a request for /api/<version>/nodes/
1137// (with no node id following as part of the path).
1138func nodesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
1139 switch {
1140 case r.Method == "GET" && op == "list":
1141 // Node listing operation.
1142 nodeListingHandler(server, w, r)
1143 case r.Method == "GET" && op == "deployment_status":
1144 // Node deployment_status operation.
1145 nodeDeploymentStatusHandler(server, w, r)
1146 case r.Method == "POST" && op == "acquire":
1147 nodesAcquireHandler(server, w, r)
1148 case r.Method == "POST" && op == "release":
1149 nodesReleaseHandler(server, w, r)
1150 default:
1151 w.WriteHeader(http.StatusBadRequest)
1152 }
1153}
1154
1155// AddNodeDetails stores node details, expected in XML format.
1156func (server *TestServer) AddNodeDetails(systemId, xmlText string) {
1157 _, hasNode := server.nodes[systemId]
1158 if !hasNode {
1159 panic("no node with the given system id")
1160 }
1161 server.nodeDetails[systemId] = xmlText
1162}
1163
1164const lldpXML = `
1165<?xml version="1.0" encoding="UTF-8"?>
1166<lldp label="LLDP neighbors"/>`
1167
1168// nodeDetailesHandler handles requests for '/api/<version>/nodes/<system_id>/?op=details'.
1169func nodeDetailsHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string) {
1170 attrs := make(map[string]interface{})
1171 attrs["lldp"] = lldpXML
1172 xmlText, _ := server.nodeDetails[systemId]
1173 attrs["lshw"] = []byte(xmlText)
1174 res, err := bson.Marshal(attrs)
1175 checkError(err)
1176 w.Header().Set("Content-Type", "application/bson")
1177 w.WriteHeader(http.StatusOK)
1178 fmt.Fprint(w, string(res))
1179}
1180
1181// filesHandler handles requests for '/api/<version>/files/*'.
1182func filesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1183 values, err := url.ParseQuery(r.URL.RawQuery)
1184 checkError(err)
1185 op := values.Get("op")
1186 fileURLRE := getFileURLRE(server.version)
1187 fileURLMatch := fileURLRE.FindStringSubmatch(r.URL.Path)
1188 fileListingURL := getFilesEndpoint(server.version)
1189 switch {
1190 case r.Method == "GET" && op == "list" && r.URL.Path == fileListingURL:
1191 // File listing operation.
1192 fileListingHandler(server, w, r)
1193 case op == "get" && r.Method == "GET" && r.URL.Path == fileListingURL:
1194 getFileHandler(server, w, r)
1195 case op == "add" && r.Method == "POST" && r.URL.Path == fileListingURL:
1196 addFileHandler(server, w, r)
1197 case fileURLMatch != nil:
1198 // Request for a single file.
1199 fileHandler(server, w, r, fileURLMatch[1], op)
1200 default:
1201 // Default handler: not found.
1202 http.NotFoundHandler().ServeHTTP(w, r)
1203 }
1204
1205}
1206
1207// listFilenames returns the names of those uploaded files whose names start
1208// with the given prefix, sorted lexicographically.
1209func listFilenames(server *TestServer, prefix string) []string {
1210 var filenames = make([]string, 0)
1211 for filename := range server.files {
1212 if strings.HasPrefix(filename, prefix) {
1213 filenames = append(filenames, filename)
1214 }
1215 }
1216 sort.Strings(filenames)
1217 return filenames
1218}
1219
1220// stripFileContent copies a map of attributes representing an uploaded file,
1221// but with the "content" attribute removed.
1222func stripContent(original map[string]JSONObject) map[string]JSONObject {
1223 newMap := make(map[string]JSONObject, len(original)-1)
1224 for key, value := range original {
1225 if key != "content" {
1226 newMap[key] = value
1227 }
1228 }
1229 return newMap
1230}
1231
1232// fileListingHandler handles requests for '/api/<version>/files/?op=list'.
1233func fileListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1234 values, err := url.ParseQuery(r.URL.RawQuery)
1235 checkError(err)
1236 prefix := values.Get("prefix")
1237 filenames := listFilenames(server, prefix)
1238
1239 // Build a sorted list of the files as map[string]JSONObject objects.
1240 convertedFiles := make([]map[string]JSONObject, 0)
1241 for _, filename := range filenames {
1242 // The "content" attribute is not in the listing.
1243 fileMap := stripContent(server.files[filename].GetMap())
1244 convertedFiles = append(convertedFiles, fileMap)
1245 }
1246 res, err := json.MarshalIndent(convertedFiles, "", " ")
1247 checkError(err)
1248 w.WriteHeader(http.StatusOK)
1249 fmt.Fprint(w, string(res))
1250}
1251
1252// fileHandler handles requests for '/api/<version>/files/<filename>/'.
1253func fileHandler(server *TestServer, w http.ResponseWriter, r *http.Request, filename string, operation string) {
1254 switch {
1255 case r.Method == "DELETE":
1256 delete(server.files, filename)
1257 w.WriteHeader(http.StatusOK)
1258 case r.Method == "GET":
1259 // Retrieve a file's information (including content) as a JSON
1260 // object.
1261 file, ok := server.files[filename]
1262 if !ok {
1263 http.NotFoundHandler().ServeHTTP(w, r)
1264 return
1265 }
1266 jsonText, err := json.MarshalIndent(file, "", " ")
1267 if err != nil {
1268 panic(err)
1269 }
1270 w.WriteHeader(http.StatusOK)
1271 w.Write(jsonText)
1272 default:
1273 // Default handler: not found.
1274 http.NotFoundHandler().ServeHTTP(w, r)
1275 }
1276}
1277
1278// InternalError replies to the request with an HTTP 500 internal error.
1279func InternalError(w http.ResponseWriter, r *http.Request, err error) {
1280 http.Error(w, err.Error(), http.StatusInternalServerError)
1281}
1282
1283// getFileHandler handles requests for
1284// '/api/<version>/files/?op=get&filename=filename'.
1285func getFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1286 values, err := url.ParseQuery(r.URL.RawQuery)
1287 checkError(err)
1288 filename := values.Get("filename")
1289 file, found := server.files[filename]
1290 if !found {
1291 http.NotFoundHandler().ServeHTTP(w, r)
1292 return
1293 }
1294 base64Content, err := file.GetField("content")
1295 if err != nil {
1296 InternalError(w, r, err)
1297 return
1298 }
1299 content, err := base64.StdEncoding.DecodeString(base64Content)
1300 if err != nil {
1301 InternalError(w, r, err)
1302 return
1303 }
1304 w.Write(content)
1305}
1306
1307func readMultipart(upload *multipart.FileHeader) ([]byte, error) {
1308 file, err := upload.Open()
1309 if err != nil {
1310 return nil, err
1311 }
1312 defer file.Close()
1313 reader := bufio.NewReader(file)
1314 return ioutil.ReadAll(reader)
1315}
1316
1317// filesHandler handles requests for '/api/<version>/files/?op=add&filename=filename'.
1318func addFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1319 err := r.ParseMultipartForm(10000000)
1320 checkError(err)
1321
1322 filename := r.Form.Get("filename")
1323 if filename == "" {
1324 panic("upload has no filename")
1325 }
1326
1327 uploads := r.MultipartForm.File
1328 if len(uploads) != 1 {
1329 panic("the payload should contain one file and one file only")
1330 }
1331 var upload *multipart.FileHeader
1332 for _, uploadContent := range uploads {
1333 upload = uploadContent[0]
1334 }
1335 content, err := readMultipart(upload)
1336 checkError(err)
1337 server.NewFile(filename, content)
1338 w.WriteHeader(http.StatusOK)
1339}
1340
1341// networkListConnectedMACSHandler handles requests for '/api/<version>/networks/<network>/?op=list_connected_macs'
1342func networkListConnectedMACSHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1343 networkURLRE := getNetworkURLRE(server.version)
1344 networkURLREMatch := networkURLRE.FindStringSubmatch(r.URL.Path)
1345 if networkURLREMatch == nil {
1346 http.NotFoundHandler().ServeHTTP(w, r)
1347 return
1348 }
1349 networkName := networkURLREMatch[1]
1350 convertedMacAddresses := []map[string]JSONObject{}
1351 if macAddresses, ok := server.macAddressesPerNetwork[networkName]; ok {
1352 for _, macAddress := range macAddresses {
1353 m, err := macAddress.GetMap()
1354 checkError(err)
1355 convertedMacAddresses = append(convertedMacAddresses, m)
1356 }
1357 }
1358 res, err := json.MarshalIndent(convertedMacAddresses, "", " ")
1359 checkError(err)
1360 w.WriteHeader(http.StatusOK)
1361 fmt.Fprint(w, string(res))
1362}
1363
1364// networksHandler handles requests for '/api/<version>/networks/?node=system_id'.
1365func networksHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1366 if r.Method != "GET" {
1367 panic("only networks GET operation implemented")
1368 }
1369 values, err := url.ParseQuery(r.URL.RawQuery)
1370 checkError(err)
1371 op := values.Get("op")
1372 systemId := values.Get("node")
1373 if op == "list_connected_macs" {
1374 networkListConnectedMACSHandler(server, w, r)
1375 return
1376 }
1377 if op != "" {
1378 panic("only list_connected_macs and default operations implemented")
1379 }
1380 if systemId == "" {
1381 panic("network missing associated node system id")
1382 }
1383 networks := []MAASObject{}
1384 if networkNames, hasNetworks := server.networksPerNode[systemId]; hasNetworks {
1385 networks = make([]MAASObject, len(networkNames))
1386 for i, networkName := range networkNames {
1387 networks[i] = server.networks[networkName]
1388 }
1389 }
1390 res, err := json.MarshalIndent(networks, "", " ")
1391 checkError(err)
1392 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1393 w.WriteHeader(http.StatusOK)
1394 fmt.Fprint(w, string(res))
1395}
1396
1397// ipAddressesHandler handles requests for '/api/<version>/ipaddresses/'.
1398func ipAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1399 err := r.ParseForm()
1400 checkError(err)
1401 values := r.Form
1402 op := values.Get("op")
1403
1404 switch r.Method {
1405 case "GET":
1406 if op != "" {
1407 panic("expected empty op for GET, got " + op)
1408 }
1409 listIPAddressesHandler(server, w, r)
1410 return
1411 case "POST":
1412 switch op {
1413 case "reserve":
1414 reserveIPAddressHandler(server, w, r, values.Get("network"), values.Get("requested_address"))
1415 return
1416 case "release":
1417 releaseIPAddressHandler(server, w, r, values.Get("ip"))
1418 return
1419 default:
1420 panic("expected op=release|reserve for POST, got " + op)
1421 }
1422 }
1423 http.NotFoundHandler().ServeHTTP(w, r)
1424}
1425
1426func marshalIPAddress(server *TestServer, ipAddress string) (JSONObject, error) {
1427 jsonTemplate := `{"alloc_type": 4, "ip": %q, "resource_uri": %q, "created": %q}`
1428 uri := getIPAddressesEndpoint(server.version)
1429 now := time.Now().UTC().Format(time.RFC3339)
1430 bytes := []byte(fmt.Sprintf(jsonTemplate, ipAddress, uri, now))
1431 return Parse(server.client, bytes)
1432}
1433
1434func badRequestError(w http.ResponseWriter, err error) {
1435 w.WriteHeader(http.StatusBadRequest)
1436 fmt.Fprint(w, err.Error())
1437}
1438
1439func listIPAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1440 results := []MAASObject{}
1441 for _, ips := range server.IPAddresses() {
1442 for _, ip := range ips {
1443 jsonObj, err := marshalIPAddress(server, ip)
1444 if err != nil {
1445 badRequestError(w, err)
1446 return
1447 }
1448 maasObj, err := jsonObj.GetMAASObject()
1449 if err != nil {
1450 badRequestError(w, err)
1451 return
1452 }
1453 results = append(results, maasObj)
1454 }
1455 }
1456 res, err := json.MarshalIndent(results, "", " ")
1457 checkError(err)
1458 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1459 w.WriteHeader(http.StatusOK)
1460 fmt.Fprint(w, string(res))
1461}
1462
1463func reserveIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, network, reqAddress string) {
1464 _, ipNet, err := net.ParseCIDR(network)
1465 if err != nil {
1466 badRequestError(w, fmt.Errorf("Invalid network parameter %s", network))
1467 return
1468 }
1469 if reqAddress != "" {
1470 // Validate "requested_address" parameter.
1471 reqIP := net.ParseIP(reqAddress)
1472 if reqIP == nil {
1473 badRequestError(w, fmt.Errorf("failed to detect a valid IP address from u'%s'", reqAddress))
1474 return
1475 }
1476 if !ipNet.Contains(reqIP) {
1477 badRequestError(w, fmt.Errorf("%s is not inside the range %s", reqAddress, ipNet.String()))
1478 return
1479 }
1480 }
1481 // Find the network name matching the parsed CIDR.
1482 foundNetworkName := ""
1483 for netName, netObj := range server.networks {
1484 // Get the "ip" and "netmask" attributes of the network.
1485 netIP, err := netObj.GetField("ip")
1486 checkError(err)
1487 netMask, err := netObj.GetField("netmask")
1488 checkError(err)
1489
1490 // Convert the netmask string to net.IPMask.
1491 parts := strings.Split(netMask, ".")
1492 ipMask := make(net.IPMask, len(parts))
1493 for i, part := range parts {
1494 intPart, err := strconv.Atoi(part)
1495 checkError(err)
1496 ipMask[i] = byte(intPart)
1497 }
1498 netNet := &net.IPNet{IP: net.ParseIP(netIP), Mask: ipMask}
1499 if netNet.String() == network {
1500 // Exact match found.
1501 foundNetworkName = netName
1502 break
1503 }
1504 }
1505 if foundNetworkName == "" {
1506 badRequestError(w, fmt.Errorf("No network found matching %s", network))
1507 return
1508 }
1509 ips, found := server.ipAddressesPerNetwork[foundNetworkName]
1510 if !found {
1511 // This will be the first address.
1512 ips = []string{}
1513 }
1514 reservedIP := ""
1515 if reqAddress != "" {
1516 // Use what the user provided. NOTE: Because this is testing
1517 // code, no duplicates check is done.
1518 reservedIP = reqAddress
1519 } else {
1520 // Generate an IP in the network range by incrementing the
1521 // last byte of the network's IP.
1522 firstIP := ipNet.IP
1523 firstIP[len(firstIP)-1] += byte(len(ips) + 1)
1524 reservedIP = firstIP.String()
1525 }
1526 ips = append(ips, reservedIP)
1527 server.ipAddressesPerNetwork[foundNetworkName] = ips
1528 jsonObj, err := marshalIPAddress(server, reservedIP)
1529 checkError(err)
1530 maasObj, err := jsonObj.GetMAASObject()
1531 checkError(err)
1532 res, err := json.MarshalIndent(maasObj, "", " ")
1533 checkError(err)
1534 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1535 w.WriteHeader(http.StatusOK)
1536 fmt.Fprint(w, string(res))
1537}
1538
1539func releaseIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, ip string) {
1540 if netIP := net.ParseIP(ip); netIP == nil {
1541 http.NotFoundHandler().ServeHTTP(w, r)
1542 return
1543 }
1544 if server.RemoveIPAddress(ip) {
1545 w.WriteHeader(http.StatusOK)
1546 return
1547 }
1548 http.NotFoundHandler().ServeHTTP(w, r)
1549}
1550
1551// versionHandler handles requests for '/api/<version>/version/'.
1552func versionHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1553 if r.Method != "GET" {
1554 panic("only version GET operation implemented")
1555 }
1556 w.Header().Set("Content-Type", "application/json; charset=utf-8")
1557 w.WriteHeader(http.StatusOK)
1558 fmt.Fprint(w, server.versionJSON)
1559}
1560
1561// nodegroupsHandler handles requests for '/api/<version>/nodegroups/*'.
1562func nodegroupsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1563 values, err := url.ParseQuery(r.URL.RawQuery)
1564 checkError(err)
1565 op := values.Get("op")
1566 bootimagesURLRE := getBootimagesURLRE(server.version)
1567 bootimagesURLMatch := bootimagesURLRE.FindStringSubmatch(r.URL.Path)
1568 nodegroupsInterfacesURLRE := getNodegroupsInterfacesURLRE(server.version)
1569 nodegroupsInterfacesURLMatch := nodegroupsInterfacesURLRE.FindStringSubmatch(r.URL.Path)
1570 nodegroupsURL := getNodegroupsEndpoint(server.version)
1571 switch {
1572 case r.URL.Path == nodegroupsURL:
1573 nodegroupsTopLevelHandler(server, w, r, op)
1574 case bootimagesURLMatch != nil:
1575 bootimagesHandler(server, w, r, bootimagesURLMatch[1], op)
1576 case nodegroupsInterfacesURLMatch != nil:
1577 nodegroupsInterfacesHandler(server, w, r, nodegroupsInterfacesURLMatch[1], op)
1578 default:
1579 // Default handler: not found.
1580 http.NotFoundHandler().ServeHTTP(w, r)
1581 }
1582}
1583
1584// nodegroupsTopLevelHandler handles requests for '/api/<version>/nodegroups/'.
1585func nodegroupsTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
1586 if r.Method != "GET" || op != "list" {
1587 w.WriteHeader(http.StatusBadRequest)
1588 return
1589 }
1590
1591 nodegroups := []JSONObject{}
1592 for uuid := range server.bootImages {
1593 attrs := map[string]interface{}{
1594 "uuid": uuid,
1595 resourceURI: getNodegroupURL(server.version, uuid),
1596 }
1597 obj := maasify(server.client, attrs)
1598 nodegroups = append(nodegroups, obj)
1599 }
1600
1601 res, err := json.MarshalIndent(nodegroups, "", " ")
1602 checkError(err)
1603 w.WriteHeader(http.StatusOK)
1604 fmt.Fprint(w, string(res))
1605}
1606
1607// bootimagesHandler handles requests for '/api/<version>/nodegroups/<uuid>/boot-images/'.
1608func bootimagesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) {
1609 if r.Method != "GET" {
1610 w.WriteHeader(http.StatusBadRequest)
1611 return
1612 }
1613
1614 bootImages, ok := server.bootImages[nodegroupUUID]
1615 if !ok {
1616 http.NotFoundHandler().ServeHTTP(w, r)
1617 return
1618 }
1619
1620 res, err := json.MarshalIndent(bootImages, "", " ")
1621 checkError(err)
1622 w.WriteHeader(http.StatusOK)
1623 fmt.Fprint(w, string(res))
1624}
1625
1626// nodegroupsInterfacesHandler handles requests for '/api/<version>/nodegroups/<uuid>/interfaces/'
1627func nodegroupsInterfacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) {
1628 if r.Method != "GET" {
1629 w.WriteHeader(http.StatusBadRequest)
1630 return
1631 }
1632 _, ok := server.bootImages[nodegroupUUID]
1633 if !ok {
1634 http.NotFoundHandler().ServeHTTP(w, r)
1635 return
1636 }
1637
1638 interfaces, ok := server.nodegroupsInterfaces[nodegroupUUID]
1639 if !ok {
1640 // we already checked the nodegroup exists, so return an empty list
1641 interfaces = []JSONObject{}
1642 }
1643 res, err := json.MarshalIndent(interfaces, "", " ")
1644 checkError(err)
1645 w.WriteHeader(http.StatusOK)
1646 fmt.Fprint(w, string(res))
1647}
1648
1649// zonesHandler handles requests for '/api/<version>/zones/'.
1650func zonesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
1651 if r.Method != "GET" {
1652 w.WriteHeader(http.StatusBadRequest)
1653 return
1654 }
1655
1656 if len(server.zones) == 0 {
1657 // Until a zone is registered, behave as if the endpoint
1658 // does not exist. This way we can simulate older MAAS
1659 // servers that do not support zones.
1660 http.NotFoundHandler().ServeHTTP(w, r)
1661 return
1662 }
1663
1664 zones := make([]JSONObject, 0, len(server.zones))
1665 for _, zone := range server.zones {
1666 zones = append(zones, zone)
1667 }
1668 res, err := json.MarshalIndent(zones, "", " ")
1669 checkError(err)
1670 w.WriteHeader(http.StatusOK)
1671 fmt.Fprint(w, string(res))
1672}