cord-776 create build / runtime containers for autmation uservices
Change-Id: I246973192adef56a250ffe93a5f65fff488840c1
diff --git a/automation/vendor/github.com/juju/gomaasapi/testservice.go b/automation/vendor/github.com/juju/gomaasapi/testservice.go
new file mode 100644
index 0000000..aa582da
--- /dev/null
+++ b/automation/vendor/github.com/juju/gomaasapi/testservice.go
@@ -0,0 +1,1672 @@
+// Copyright 2012-2016 Canonical Ltd.
+// Licensed under the LGPLv3, see LICENCE file for details.
+
+package gomaasapi
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "mime/multipart"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "text/template"
+ "time"
+
+ "gopkg.in/mgo.v2/bson"
+)
+
+// TestMAASObject is a fake MAAS server MAASObject.
+type TestMAASObject struct {
+ MAASObject
+ TestServer *TestServer
+}
+
+// checkError is a shorthand helper that panics if err is not nil.
+func checkError(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
+
+// NewTestMAAS returns a TestMAASObject that implements the MAASObject
+// interface and thus can be used as a test object instead of the one returned
+// by gomaasapi.NewMAAS().
+func NewTestMAAS(version string) *TestMAASObject {
+ server := NewTestServer(version)
+ authClient, err := NewAnonymousClient(server.URL, version)
+ checkError(err)
+ maas := NewMAAS(*authClient)
+ return &TestMAASObject{*maas, server}
+}
+
+// Close shuts down the test server.
+func (testMAASObject *TestMAASObject) Close() {
+ testMAASObject.TestServer.Close()
+}
+
+// A TestServer is an HTTP server listening on a system-chosen port on the
+// local loopback interface, which simulates the behavior of a MAAS server.
+// It is intendend for use in end-to-end HTTP tests using the gomaasapi
+// library.
+type TestServer struct {
+ *httptest.Server
+ serveMux *http.ServeMux
+ client Client
+ nodes map[string]MAASObject
+ ownedNodes map[string]bool
+ // mapping system_id -> list of operations performed.
+ nodeOperations map[string][]string
+ // list of operations performed at the /nodes/ level.
+ nodesOperations []string
+ // mapping system_id -> list of Values passed when performing
+ // operations
+ nodeOperationRequestValues map[string][]url.Values
+ // list of Values passed when performing operations at the
+ // /nodes/ level.
+ nodesOperationRequestValues []url.Values
+ nodeMetadata map[string]Node
+ files map[string]MAASObject
+ networks map[string]MAASObject
+ networksPerNode map[string][]string
+ ipAddressesPerNetwork map[string][]string
+ version string
+ macAddressesPerNetwork map[string]map[string]JSONObject
+ nodeDetails map[string]string
+ zones map[string]JSONObject
+ // bootImages is a map of nodegroup UUIDs to boot-image objects.
+ bootImages map[string][]JSONObject
+ // nodegroupsInterfaces is a map of nodegroup UUIDs to interface
+ // objects.
+ nodegroupsInterfaces map[string][]JSONObject
+
+ // versionJSON is the response to the /version/ endpoint listing the
+ // capabilities of the MAAS server.
+ versionJSON string
+
+ // devices is a map of device UUIDs to devices.
+ devices map[string]*TestDevice
+
+ subnets map[uint]TestSubnet
+ subnetNameToID map[string]uint
+ nextSubnet uint
+ spaces map[uint]*TestSpace
+ spaceNameToID map[string]uint
+ nextSpace uint
+ vlans map[int]TestVLAN
+ nextVLAN int
+}
+
+type TestDevice struct {
+ IPAddresses []string
+ SystemId string
+ MACAddresses []string
+ Parent string
+ Hostname string
+
+ // Not part of the device definition but used by the template.
+ APIVersion string
+}
+
+func getNodesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/nodes/", version)
+}
+
+func getNodeURL(version, systemId string) string {
+ return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId)
+}
+
+func getNodeURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getDevicesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/devices/", version)
+}
+
+func getDeviceURL(version, systemId string) string {
+ return fmt.Sprintf("/api/%s/devices/%s/", version, systemId)
+}
+
+func getDeviceURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/devices/([^/]*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getFilesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/files/", version)
+}
+
+func getFileURL(version, filename string) string {
+ // Uses URL object so filename is correctly percent-escaped
+ url := url.URL{}
+ url.Path = fmt.Sprintf("/api/%s/files/%s/", version, filename)
+ return url.String()
+}
+
+func getFileURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/files/(.*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getNetworksEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/networks/", version)
+}
+
+func getNetworkURL(version, name string) string {
+ return fmt.Sprintf("/api/%s/networks/%s/", version, name)
+}
+
+func getNetworkURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/networks/(.*)/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getIPAddressesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/ipaddresses/", version)
+}
+
+func getMACAddressURL(version, systemId, macAddress string) string {
+ return fmt.Sprintf("/api/%s/nodes/%s/macs/%s/", version, systemId, url.QueryEscape(macAddress))
+}
+
+func getVersionURL(version string) string {
+ return fmt.Sprintf("/api/%s/version/", version)
+}
+
+func getNodegroupsEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/nodegroups/", version)
+}
+
+func getNodegroupURL(version, uuid string) string {
+ return fmt.Sprintf("/api/%s/nodegroups/%s/", version, uuid)
+}
+
+func getNodegroupsInterfacesURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/interfaces/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getBootimagesURLRE(version string) *regexp.Regexp {
+ reString := fmt.Sprintf("^/api/%s/nodegroups/([^/]*)/boot-images/$", regexp.QuoteMeta(version))
+ return regexp.MustCompile(reString)
+}
+
+func getZonesEndpoint(version string) string {
+ return fmt.Sprintf("/api/%s/zones/", version)
+}
+
+// Clear clears all the fake data stored and recorded by the test server
+// (nodes, recorded operations, etc.).
+func (server *TestServer) Clear() {
+ server.nodes = make(map[string]MAASObject)
+ server.ownedNodes = make(map[string]bool)
+ server.nodesOperations = make([]string, 0)
+ server.nodeOperations = make(map[string][]string)
+ server.nodesOperationRequestValues = make([]url.Values, 0)
+ server.nodeOperationRequestValues = make(map[string][]url.Values)
+ server.nodeMetadata = make(map[string]Node)
+ server.files = make(map[string]MAASObject)
+ server.networks = make(map[string]MAASObject)
+ server.networksPerNode = make(map[string][]string)
+ server.ipAddressesPerNetwork = make(map[string][]string)
+ server.macAddressesPerNetwork = make(map[string]map[string]JSONObject)
+ server.nodeDetails = make(map[string]string)
+ server.bootImages = make(map[string][]JSONObject)
+ server.nodegroupsInterfaces = make(map[string][]JSONObject)
+ server.zones = make(map[string]JSONObject)
+ server.versionJSON = `{"capabilities": ["networks-management","static-ipaddresses","devices-management","network-deployment-ubuntu"]}`
+ server.devices = make(map[string]*TestDevice)
+ server.subnets = make(map[uint]TestSubnet)
+ server.subnetNameToID = make(map[string]uint)
+ server.nextSubnet = 1
+ server.spaces = make(map[uint]*TestSpace)
+ server.spaceNameToID = make(map[string]uint)
+ server.nextSpace = 1
+ server.vlans = make(map[int]TestVLAN)
+ server.nextVLAN = 1
+}
+
+// SetVersionJSON sets the JSON response (capabilities) returned from the
+// /version/ endpoint.
+func (server *TestServer) SetVersionJSON(json string) {
+ server.versionJSON = json
+}
+
+// NodesOperations returns the list of operations performed at the /nodes/
+// level.
+func (server *TestServer) NodesOperations() []string {
+ return server.nodesOperations
+}
+
+// NodeOperations returns the map containing the list of the operations
+// performed for each node.
+func (server *TestServer) NodeOperations() map[string][]string {
+ return server.nodeOperations
+}
+
+// NodesOperationRequestValues returns the list of url.Values extracted
+// from the request used when performing operations at the /nodes/ level.
+func (server *TestServer) NodesOperationRequestValues() []url.Values {
+ return server.nodesOperationRequestValues
+}
+
+// NodeOperationRequestValues returns the map containing the list of the
+// url.Values extracted from the request used when performing operations
+// on nodes.
+func (server *TestServer) NodeOperationRequestValues() map[string][]url.Values {
+ return server.nodeOperationRequestValues
+}
+
+func parseRequestValues(request *http.Request) url.Values {
+ var requestValues url.Values
+ if request.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
+ if request.PostForm == nil {
+ if err := request.ParseForm(); err != nil {
+ panic(err)
+ }
+ }
+ requestValues = request.PostForm
+ }
+ return requestValues
+}
+
+func (server *TestServer) addNodesOperation(operation string, request *http.Request) url.Values {
+ requestValues := parseRequestValues(request)
+ server.nodesOperations = append(server.nodesOperations, operation)
+ server.nodesOperationRequestValues = append(server.nodesOperationRequestValues, requestValues)
+ return requestValues
+}
+
+func (server *TestServer) addNodeOperation(systemId, operation string, request *http.Request) url.Values {
+ operations, present := server.nodeOperations[systemId]
+ operationRequestValues, present2 := server.nodeOperationRequestValues[systemId]
+ if present != present2 {
+ panic("inconsistent state: nodeOperations and nodeOperationRequestValues don't have the same keys.")
+ }
+ requestValues := parseRequestValues(request)
+ if !present {
+ operations = []string{operation}
+ operationRequestValues = []url.Values{requestValues}
+ } else {
+ operations = append(operations, operation)
+ operationRequestValues = append(operationRequestValues, requestValues)
+ }
+ server.nodeOperations[systemId] = operations
+ server.nodeOperationRequestValues[systemId] = operationRequestValues
+ return requestValues
+}
+
+// NewNode creates a MAAS node. The provided string should be a valid json
+// string representing a map and contain a string value for the key
+// 'system_id'. e.g. `{"system_id": "mysystemid"}`.
+// If one of these conditions is not met, NewNode panics.
+func (server *TestServer) NewNode(jsonText string) MAASObject {
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ systemIdEntry, hasSystemId := attrs["system_id"]
+ if !hasSystemId {
+ panic("The given map json string does not contain a 'system_id' value.")
+ }
+ systemId := systemIdEntry.(string)
+ attrs[resourceURI] = getNodeURL(server.version, systemId)
+ if _, hasStatus := attrs["status"]; !hasStatus {
+ attrs["status"] = NodeStatusDeployed
+ }
+ obj := newJSONMAASObject(attrs, server.client)
+ server.nodes[systemId] = obj
+ return obj
+}
+
+// Nodes returns a map associating all the nodes' system ids with the nodes'
+// objects.
+func (server *TestServer) Nodes() map[string]MAASObject {
+ return server.nodes
+}
+
+// OwnedNodes returns a map whose keys represent the nodes that are currently
+// allocated.
+func (server *TestServer) OwnedNodes() map[string]bool {
+ return server.ownedNodes
+}
+
+// NewFile creates a file in the test MAAS server.
+func (server *TestServer) NewFile(filename string, filecontent []byte) MAASObject {
+ attrs := make(map[string]interface{})
+ attrs[resourceURI] = getFileURL(server.version, filename)
+ base64Content := base64.StdEncoding.EncodeToString(filecontent)
+ attrs["content"] = base64Content
+ attrs["filename"] = filename
+
+ // Allocate an arbitrary URL here. It would be nice if the caller
+ // could do this, but that would change the API and require many
+ // changes.
+ escapedName := url.QueryEscape(filename)
+ attrs["anon_resource_uri"] = "/maas/1.0/files/?op=get_by_key&key=" + escapedName + "_key"
+
+ obj := newJSONMAASObject(attrs, server.client)
+ server.files[filename] = obj
+ return obj
+}
+
+func (server *TestServer) Files() map[string]MAASObject {
+ return server.files
+}
+
+// ChangeNode updates a node with the given key/value.
+func (server *TestServer) ChangeNode(systemId, key, value string) {
+ node, found := server.nodes[systemId]
+ if !found {
+ panic("No node with such 'system_id'.")
+ }
+ node.GetMap()[key] = maasify(server.client, value)
+}
+
+// NewIPAddress creates a new static IP address reservation for the
+// given network/subnet and ipAddress. While networks is being deprecated
+// try the given name as both a netowrk and a subnet.
+func (server *TestServer) NewIPAddress(ipAddress, networkOrSubnet string) {
+ _, foundNetwork := server.networks[networkOrSubnet]
+ subnetID, foundSubnet := server.subnetNameToID[networkOrSubnet]
+
+ if (foundNetwork || foundSubnet) == false {
+ panic("No such network or subnet: " + networkOrSubnet)
+ }
+ if foundNetwork {
+ ips, found := server.ipAddressesPerNetwork[networkOrSubnet]
+ if found {
+ ips = append(ips, ipAddress)
+ } else {
+ ips = []string{ipAddress}
+ }
+ server.ipAddressesPerNetwork[networkOrSubnet] = ips
+ } else {
+ subnet := server.subnets[subnetID]
+ netIp := net.ParseIP(ipAddress)
+ if netIp == nil {
+ panic(ipAddress + " is invalid")
+ }
+ ip := IPFromNetIP(netIp)
+ ip.Purpose = []string{"assigned-ip"}
+ subnet.InUseIPAddresses = append(subnet.InUseIPAddresses, ip)
+ server.subnets[subnetID] = subnet
+ }
+}
+
+// RemoveIPAddress removes the given existing ipAddress and returns
+// whether it was actually removed.
+func (server *TestServer) RemoveIPAddress(ipAddress string) bool {
+ for network, ips := range server.ipAddressesPerNetwork {
+ for i, ip := range ips {
+ if ip == ipAddress {
+ ips = append(ips[:i], ips[i+1:]...)
+ server.ipAddressesPerNetwork[network] = ips
+ return true
+ }
+ }
+ }
+ for _, device := range server.devices {
+ for i, addr := range device.IPAddresses {
+ if addr == ipAddress {
+ device.IPAddresses = append(device.IPAddresses[:i], device.IPAddresses[i+1:]...)
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// IPAddresses returns the map with network names as keys and slices
+// of IP addresses belonging to each network as values.
+func (server *TestServer) IPAddresses() map[string][]string {
+ return server.ipAddressesPerNetwork
+}
+
+// NewNetwork creates a network in the test MAAS server
+func (server *TestServer) NewNetwork(jsonText string) MAASObject {
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ nameEntry, hasName := attrs["name"]
+ _, hasIP := attrs["ip"]
+ _, hasNetmask := attrs["netmask"]
+ if !hasName || !hasIP || !hasNetmask {
+ panic("The given map json string does not contain a 'name', 'ip', or 'netmask' value.")
+ }
+ // TODO(gz): Sanity checking done on other fields
+ name := nameEntry.(string)
+ attrs[resourceURI] = getNetworkURL(server.version, name)
+ obj := newJSONMAASObject(attrs, server.client)
+ server.networks[name] = obj
+ return obj
+}
+
+// NewNodegroupInterface adds a nodegroup-interface, for the specified
+// nodegroup, in the test MAAS server.
+func (server *TestServer) NewNodegroupInterface(uuid, jsonText string) JSONObject {
+ _, ok := server.bootImages[uuid]
+ if !ok {
+ panic("no nodegroup with the given UUID")
+ }
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ requiredMembers := []string{"ip_range_high", "ip_range_low", "broadcast_ip", "static_ip_range_low", "static_ip_range_high", "name", "ip", "subnet_mask", "management", "interface"}
+ for _, member := range requiredMembers {
+ _, hasMember := attrs[member]
+ if !hasMember {
+ panic(fmt.Sprintf("The given map json string does not contain a required %q", member))
+ }
+ }
+ obj := maasify(server.client, attrs)
+ server.nodegroupsInterfaces[uuid] = append(server.nodegroupsInterfaces[uuid], obj)
+ return obj
+}
+
+func (server *TestServer) ConnectNodeToNetwork(systemId, name string) {
+ _, hasNode := server.nodes[systemId]
+ if !hasNode {
+ panic("no node with the given system id")
+ }
+ _, hasNetwork := server.networks[name]
+ if !hasNetwork {
+ panic("no network with the given name")
+ }
+ networkNames, _ := server.networksPerNode[systemId]
+ server.networksPerNode[systemId] = append(networkNames, name)
+}
+
+func (server *TestServer) ConnectNodeToNetworkWithMACAddress(systemId, networkName, macAddress string) {
+ node, hasNode := server.nodes[systemId]
+ if !hasNode {
+ panic("no node with the given system id")
+ }
+ if _, hasNetwork := server.networks[networkName]; !hasNetwork {
+ panic("no network with the given name")
+ }
+ networkNames, _ := server.networksPerNode[systemId]
+ server.networksPerNode[systemId] = append(networkNames, networkName)
+ attrs := make(map[string]interface{})
+ attrs[resourceURI] = getMACAddressURL(server.version, systemId, macAddress)
+ attrs["mac_address"] = macAddress
+ array := []JSONObject{}
+ if set, ok := node.GetMap()["macaddress_set"]; ok {
+ var err error
+ array, err = set.GetArray()
+ if err != nil {
+ panic(err)
+ }
+ }
+ array = append(array, maasify(server.client, attrs))
+ node.GetMap()["macaddress_set"] = JSONObject{value: array, client: server.client}
+ if _, ok := server.macAddressesPerNetwork[networkName]; !ok {
+ server.macAddressesPerNetwork[networkName] = map[string]JSONObject{}
+ }
+ server.macAddressesPerNetwork[networkName][systemId] = maasify(server.client, attrs)
+}
+
+// AddBootImage adds a boot-image object to the specified nodegroup.
+func (server *TestServer) AddBootImage(nodegroupUUID string, jsonText string) {
+ var attrs map[string]interface{}
+ err := json.Unmarshal([]byte(jsonText), &attrs)
+ checkError(err)
+ if _, ok := attrs["architecture"]; !ok {
+ panic("The boot-image json string does not contain an 'architecture' value.")
+ }
+ if _, ok := attrs["release"]; !ok {
+ panic("The boot-image json string does not contain a 'release' value.")
+ }
+ obj := maasify(server.client, attrs)
+ server.bootImages[nodegroupUUID] = append(server.bootImages[nodegroupUUID], obj)
+}
+
+// AddZone adds a physical zone to the server.
+func (server *TestServer) AddZone(name, description string) {
+ attrs := map[string]interface{}{
+ "name": name,
+ "description": description,
+ }
+ obj := maasify(server.client, attrs)
+ server.zones[name] = obj
+}
+
+func (server *TestServer) AddDevice(device *TestDevice) {
+ server.devices[device.SystemId] = device
+}
+
+func (server *TestServer) Devices() map[string]*TestDevice {
+ return server.devices
+}
+
+// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down.
+func NewTestServer(version string) *TestServer {
+ server := &TestServer{version: version}
+
+ serveMux := http.NewServeMux()
+ devicesURL := getDevicesEndpoint(server.version)
+ // Register handler for '/api/<version>/devices/*'.
+ serveMux.HandleFunc(devicesURL, func(w http.ResponseWriter, r *http.Request) {
+ devicesHandler(server, w, r)
+ })
+ nodesURL := getNodesEndpoint(server.version)
+ // Register handler for '/api/<version>/nodes/*'.
+ serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) {
+ nodesHandler(server, w, r)
+ })
+ filesURL := getFilesEndpoint(server.version)
+ // Register handler for '/api/<version>/files/*'.
+ serveMux.HandleFunc(filesURL, func(w http.ResponseWriter, r *http.Request) {
+ filesHandler(server, w, r)
+ })
+ networksURL := getNetworksEndpoint(server.version)
+ // Register handler for '/api/<version>/networks/'.
+ serveMux.HandleFunc(networksURL, func(w http.ResponseWriter, r *http.Request) {
+ networksHandler(server, w, r)
+ })
+ ipAddressesURL := getIPAddressesEndpoint(server.version)
+ // Register handler for '/api/<version>/ipaddresses/'.
+ serveMux.HandleFunc(ipAddressesURL, func(w http.ResponseWriter, r *http.Request) {
+ ipAddressesHandler(server, w, r)
+ })
+ versionURL := getVersionURL(server.version)
+ // Register handler for '/api/<version>/version/'.
+ serveMux.HandleFunc(versionURL, func(w http.ResponseWriter, r *http.Request) {
+ versionHandler(server, w, r)
+ })
+ // Register handler for '/api/<version>/nodegroups/*'.
+ nodegroupsURL := getNodegroupsEndpoint(server.version)
+ serveMux.HandleFunc(nodegroupsURL, func(w http.ResponseWriter, r *http.Request) {
+ nodegroupsHandler(server, w, r)
+ })
+
+ // Register handler for '/api/<version>/zones/*'.
+ zonesURL := getZonesEndpoint(server.version)
+ serveMux.HandleFunc(zonesURL, func(w http.ResponseWriter, r *http.Request) {
+ zonesHandler(server, w, r)
+ })
+
+ subnetsURL := getSubnetsEndpoint(server.version)
+ serveMux.HandleFunc(subnetsURL, func(w http.ResponseWriter, r *http.Request) {
+ subnetsHandler(server, w, r)
+ })
+
+ spacesURL := getSpacesEndpoint(server.version)
+ serveMux.HandleFunc(spacesURL, func(w http.ResponseWriter, r *http.Request) {
+ spacesHandler(server, w, r)
+ })
+
+ vlansURL := getVLANsEndpoint(server.version)
+ serveMux.HandleFunc(vlansURL, func(w http.ResponseWriter, r *http.Request) {
+ vlansHandler(server, w, r)
+ })
+
+ var mu sync.Mutex
+ singleFile := func(w http.ResponseWriter, req *http.Request) {
+ mu.Lock()
+ defer mu.Unlock()
+ serveMux.ServeHTTP(w, req)
+ }
+
+ newServer := httptest.NewServer(http.HandlerFunc(singleFile))
+ client, err := NewAnonymousClient(newServer.URL, "1.0")
+ checkError(err)
+ server.Server = newServer
+ server.serveMux = serveMux
+ server.client = *client
+ server.Clear()
+ return server
+}
+
+// devicesHandler handles requests for '/api/<version>/devices/*'.
+func devicesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ deviceURLRE := getDeviceURLRE(server.version)
+ deviceURLMatch := deviceURLRE.FindStringSubmatch(r.URL.Path)
+ devicesURL := getDevicesEndpoint(server.version)
+ switch {
+ case r.URL.Path == devicesURL:
+ devicesTopLevelHandler(server, w, r, op)
+ case deviceURLMatch != nil:
+ // Request for a single device.
+ deviceHandler(server, w, r, deviceURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// devicesTopLevelHandler handles a request for /api/<version>/devices/
+// (with no device id following as part of the path).
+func devicesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
+ switch {
+ case r.Method == "GET" && op == "list":
+ // Device listing operation.
+ deviceListingHandler(server, w, r)
+ case r.Method == "POST" && op == "new":
+ newDeviceHandler(server, w, r)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ }
+}
+
+func macMatches(mac string, device *TestDevice) bool {
+ return contains(device.MACAddresses, mac)
+}
+
+// deviceListingHandler handles requests for '/devices/'.
+func deviceListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ // TODO(mfoord): support filtering by hostname and id
+ macs, hasMac := values["mac_address"]
+ var matchedDevices []*TestDevice
+ if !hasMac {
+ for _, device := range server.devices {
+ matchedDevices = append(matchedDevices, device)
+ }
+ } else {
+ for _, mac := range macs {
+ for _, device := range server.devices {
+ if macMatches(mac, device) {
+ matchedDevices = append(matchedDevices, device)
+ }
+ }
+ }
+ }
+ deviceChunks := make([]string, len(matchedDevices))
+ for i := range matchedDevices {
+ deviceChunks[i] = renderDevice(matchedDevices[i])
+ }
+ json := fmt.Sprintf("[%v]", strings.Join(deviceChunks, ", "))
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, json)
+}
+
+var templateFuncs = template.FuncMap{
+ "quotedList": func(items []string) string {
+ var pieces []string
+ for _, item := range items {
+ pieces = append(pieces, fmt.Sprintf("%q", item))
+ }
+ return strings.Join(pieces, ", ")
+ },
+ "last": func(items []string) []string {
+ if len(items) == 0 {
+ return []string{}
+ }
+ return items[len(items)-1:]
+ },
+ "allButLast": func(items []string) []string {
+ if len(items) < 2 {
+ return []string{}
+ }
+ return items[0 : len(items)-1]
+ },
+}
+
+const (
+ // The json template for generating new devices.
+ // TODO(mfoord): set resource_uri in MAC addresses
+ deviceTemplate = `{
+ "macaddress_set": [{{range .MACAddresses | allButLast}}
+ {
+ "mac_address": "{{.}}"
+ },{{end}}{{range .MACAddresses | last}}
+ {
+ "mac_address": "{{.}}"
+ }{{end}}
+ ],
+ "zone": {
+ "resource_uri": "/MAAS/api/{{.APIVersion}}/zones/default/",
+ "name": "default",
+ "description": ""
+ },
+ "parent": "{{.Parent}}",
+ "ip_addresses": [{{.IPAddresses | quotedList }}],
+ "hostname": "{{.Hostname}}",
+ "tag_names": [],
+ "owner": "maas-admin",
+ "system_id": "{{.SystemId}}",
+ "resource_uri": "/MAAS/api/{{.APIVersion}}/devices/{{.SystemId}}/"
+}`
+)
+
+func renderDevice(device *TestDevice) string {
+ t := template.New("Device template")
+ t = t.Funcs(templateFuncs)
+ t, err := t.Parse(deviceTemplate)
+ checkError(err)
+ var buf bytes.Buffer
+ err = t.Execute(&buf, device)
+ checkError(err)
+ return buf.String()
+}
+
+func getValue(values url.Values, value string) (string, bool) {
+ result, hasResult := values[value]
+ if !hasResult || len(result) != 1 || result[0] == "" {
+ return "", false
+ }
+ return result[0], true
+}
+
+func getValues(values url.Values, key string) ([]string, bool) {
+ result, hasResult := values[key]
+ if !hasResult {
+ return nil, false
+ }
+ var output []string
+ for _, val := range result {
+ if val != "" {
+ output = append(output, val)
+ }
+ }
+ if len(output) == 0 {
+ return nil, false
+ }
+ return output, true
+}
+
+// newDeviceHandler creates, stores and returns new devices.
+func newDeviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ checkError(err)
+ values := r.PostForm
+
+ // TODO(mfood): generate a "proper" uuid for the system Id.
+ uuid, err := generateNonce()
+ checkError(err)
+ systemId := fmt.Sprintf("node-%v", uuid)
+ // At least one MAC address must be specified.
+ // TODO(mfoord) we only support a single MAC in the test server.
+ macs, hasMacs := getValues(values, "mac_addresses")
+
+ // hostname and parent are optional.
+ // TODO(mfoord): we require both to be set in the test server.
+ hostname, hasHostname := getValue(values, "hostname")
+ parent, hasParent := getValue(values, "parent")
+ if !hasHostname || !hasMacs || !hasParent {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ device := &TestDevice{
+ MACAddresses: macs,
+ APIVersion: server.version,
+ Parent: parent,
+ Hostname: hostname,
+ SystemId: systemId,
+ }
+
+ deviceJSON := renderDevice(device)
+ server.devices[systemId] = device
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, deviceJSON)
+ return
+}
+
+// deviceHandler handles requests for '/api/<version>/devices/<system_id>/'.
+func deviceHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
+ device, ok := server.devices[systemId]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ if r.Method == "GET" {
+ deviceJSON := renderDevice(device)
+ if operation == "" {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, deviceJSON)
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ if r.Method == "POST" {
+ if operation == "claim_sticky_ip_address" {
+ err := r.ParseForm()
+ checkError(err)
+ values := r.PostForm
+ // TODO(mfoord): support optional mac_address parameter
+ // TODO(mfoord): requested_address should be optional
+ // and we should generate one if it isn't provided.
+ address, hasAddress := getValue(values, "requested_address")
+ if !hasAddress {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ checkError(err)
+ device.IPAddresses = append(device.IPAddresses, address)
+ deviceJSON := renderDevice(device)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, deviceJSON)
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ } else if r.Method == "DELETE" {
+ delete(server.devices, systemId)
+ w.WriteHeader(http.StatusNoContent)
+ return
+
+ }
+
+ // TODO(mfoord): support PUT method for updating device
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+// nodesHandler handles requests for '/api/<version>/nodes/*'.
+func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ nodeURLRE := getNodeURLRE(server.version)
+ nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path)
+ nodesURL := getNodesEndpoint(server.version)
+ switch {
+ case r.URL.Path == nodesURL:
+ nodesTopLevelHandler(server, w, r, op)
+ case nodeURLMatch != nil:
+ // Request for a single node.
+ nodeHandler(server, w, r, nodeURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// nodeHandler handles requests for '/api/<version>/nodes/<system_id>/'.
+func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
+ node, ok := server.nodes[systemId]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ UUID, UUIDError := node.values["system_id"].GetString()
+ if UUIDError == nil {
+ i, err := JSONObjectFromStruct(server.client, server.nodeMetadata[UUID].Interfaces)
+ checkError(err)
+ node.values["interface_set"] = i
+ }
+
+ if r.Method == "GET" {
+ if operation == "" {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, marshalNode(node))
+ return
+ } else if operation == "details" {
+ nodeDetailsHandler(server, w, r, systemId)
+ return
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ }
+ if r.Method == "POST" {
+ // The only operations supported are "start", "stop" and "release".
+ if operation == "start" || operation == "stop" || operation == "release" {
+ // Record operation on node.
+ server.addNodeOperation(systemId, operation, r)
+
+ if operation == "release" {
+ delete(server.OwnedNodes(), systemId)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, marshalNode(node))
+ return
+ }
+
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ if r.Method == "DELETE" {
+ delete(server.nodes, systemId)
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+func contains(slice []string, val string) bool {
+ for _, item := range slice {
+ if item == val {
+ return true
+ }
+ }
+ return false
+}
+
+// nodeListingHandler handles requests for '/nodes/'.
+func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ ids, hasId := values["id"]
+ var convertedNodes = []map[string]JSONObject{}
+ for systemId, node := range server.nodes {
+ if !hasId || contains(ids, systemId) {
+ convertedNodes = append(convertedNodes, node.GetMap())
+ }
+ }
+ res, err := json.MarshalIndent(convertedNodes, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// nodeDeploymentStatusHandler handles requests for '/nodes/?op=deployment_status'.
+func nodeDeploymentStatusHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ nodes, _ := values["nodes"]
+ var nodeStatus = make(map[string]interface{})
+ for _, systemId := range nodes {
+ node := server.nodes[systemId]
+ field, err := node.GetField("status")
+ if err != nil {
+ continue
+ }
+ switch field {
+ case NodeStatusDeployed:
+ nodeStatus[systemId] = "Deployed"
+ case NodeStatusFailedDeployment:
+ nodeStatus[systemId] = "Failed deployment"
+ default:
+ nodeStatus[systemId] = "Not in Deployment"
+ }
+ }
+ obj := maasify(server.client, nodeStatus)
+ res, err := json.MarshalIndent(obj, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// findFreeNode looks for a node that is currently available, and
+// matches the specified filter.
+func findFreeNode(server *TestServer, filter url.Values) *MAASObject {
+ for systemID, node := range server.Nodes() {
+ _, present := server.OwnedNodes()[systemID]
+ if !present {
+ var agentName, nodeName, zoneName, mem, cpuCores, arch string
+ for k := range filter {
+ switch k {
+ case "agent_name":
+ agentName = filter.Get(k)
+ case "name":
+ nodeName = filter.Get(k)
+ case "zone":
+ zoneName = filter.Get(k)
+ case "mem":
+ mem = filter.Get(k)
+ case "arch":
+ arch = filter.Get(k)
+ case "cpu-cores":
+ cpuCores = filter.Get(k)
+ }
+ }
+ if nodeName != "" && !matchField(node, "hostname", nodeName) {
+ continue
+ }
+ if zoneName != "" && !matchField(node, "zone", zoneName) {
+ continue
+ }
+ if mem != "" && !matchNumericField(node, "memory", mem) {
+ continue
+ }
+ if arch != "" && !matchArchitecture(node, "architecture", arch) {
+ continue
+ }
+ if cpuCores != "" && !matchNumericField(node, "cpu_count", cpuCores) {
+ continue
+ }
+ if agentName != "" {
+ agentNameObj := maasify(server.client, agentName)
+ node.GetMap()["agent_name"] = agentNameObj
+ } else {
+ delete(node.GetMap(), "agent_name")
+ }
+ return &node
+ }
+ }
+ return nil
+}
+
+func matchArchitecture(node MAASObject, k, v string) bool {
+ field, err := node.GetField(k)
+ if err != nil {
+ return false
+ }
+ baseArch := strings.Split(field, "/")
+ return v == baseArch[0]
+}
+
+func matchNumericField(node MAASObject, k, v string) bool {
+ field, ok := node.GetMap()[k]
+ if !ok {
+ return false
+ }
+ nodeVal, err := field.GetFloat64()
+ if err != nil {
+ return false
+ }
+ constraintVal, err := strconv.ParseFloat(v, 64)
+ if err != nil {
+ return false
+ }
+ return constraintVal <= nodeVal
+}
+
+func matchField(node MAASObject, k, v string) bool {
+ field, err := node.GetField(k)
+ if err != nil {
+ return false
+ }
+ return field == v
+}
+
+// nodesAcquireHandler simulates acquiring a node.
+func nodesAcquireHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ requestValues := server.addNodesOperation("acquire", r)
+ node := findFreeNode(server, requestValues)
+ if node == nil {
+ w.WriteHeader(http.StatusConflict)
+ } else {
+ systemId, err := node.GetField("system_id")
+ checkError(err)
+ server.OwnedNodes()[systemId] = true
+ res, err := json.MarshalIndent(node, "", " ")
+ checkError(err)
+ // Record operation.
+ server.addNodeOperation(systemId, "acquire", r)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+ }
+}
+
+// nodesReleaseHandler simulates releasing multiple nodes.
+func nodesReleaseHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ server.addNodesOperation("release", r)
+ values := server.NodesOperationRequestValues()
+ systemIds := values[len(values)-1]["nodes"]
+ var unknown []string
+ for _, systemId := range systemIds {
+ if _, ok := server.Nodes()[systemId]; !ok {
+ unknown = append(unknown, systemId)
+ }
+ }
+ if len(unknown) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Unknown node(s): %s.", strings.Join(unknown, ", "))
+ return
+ }
+ var releasedNodes = []map[string]JSONObject{}
+ for _, systemId := range systemIds {
+ if _, ok := server.OwnedNodes()[systemId]; !ok {
+ continue
+ }
+ delete(server.OwnedNodes(), systemId)
+ node := server.Nodes()[systemId]
+ releasedNodes = append(releasedNodes, node.GetMap())
+ }
+ res, err := json.MarshalIndent(releasedNodes, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// nodesTopLevelHandler handles a request for /api/<version>/nodes/
+// (with no node id following as part of the path).
+func nodesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
+ switch {
+ case r.Method == "GET" && op == "list":
+ // Node listing operation.
+ nodeListingHandler(server, w, r)
+ case r.Method == "GET" && op == "deployment_status":
+ // Node deployment_status operation.
+ nodeDeploymentStatusHandler(server, w, r)
+ case r.Method == "POST" && op == "acquire":
+ nodesAcquireHandler(server, w, r)
+ case r.Method == "POST" && op == "release":
+ nodesReleaseHandler(server, w, r)
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ }
+}
+
+// AddNodeDetails stores node details, expected in XML format.
+func (server *TestServer) AddNodeDetails(systemId, xmlText string) {
+ _, hasNode := server.nodes[systemId]
+ if !hasNode {
+ panic("no node with the given system id")
+ }
+ server.nodeDetails[systemId] = xmlText
+}
+
+const lldpXML = `
+<?xml version="1.0" encoding="UTF-8"?>
+<lldp label="LLDP neighbors"/>`
+
+// nodeDetailesHandler handles requests for '/api/<version>/nodes/<system_id>/?op=details'.
+func nodeDetailsHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string) {
+ attrs := make(map[string]interface{})
+ attrs["lldp"] = lldpXML
+ xmlText, _ := server.nodeDetails[systemId]
+ attrs["lshw"] = []byte(xmlText)
+ res, err := bson.Marshal(attrs)
+ checkError(err)
+ w.Header().Set("Content-Type", "application/bson")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// filesHandler handles requests for '/api/<version>/files/*'.
+func filesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ fileURLRE := getFileURLRE(server.version)
+ fileURLMatch := fileURLRE.FindStringSubmatch(r.URL.Path)
+ fileListingURL := getFilesEndpoint(server.version)
+ switch {
+ case r.Method == "GET" && op == "list" && r.URL.Path == fileListingURL:
+ // File listing operation.
+ fileListingHandler(server, w, r)
+ case op == "get" && r.Method == "GET" && r.URL.Path == fileListingURL:
+ getFileHandler(server, w, r)
+ case op == "add" && r.Method == "POST" && r.URL.Path == fileListingURL:
+ addFileHandler(server, w, r)
+ case fileURLMatch != nil:
+ // Request for a single file.
+ fileHandler(server, w, r, fileURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+
+}
+
+// listFilenames returns the names of those uploaded files whose names start
+// with the given prefix, sorted lexicographically.
+func listFilenames(server *TestServer, prefix string) []string {
+ var filenames = make([]string, 0)
+ for filename := range server.files {
+ if strings.HasPrefix(filename, prefix) {
+ filenames = append(filenames, filename)
+ }
+ }
+ sort.Strings(filenames)
+ return filenames
+}
+
+// stripFileContent copies a map of attributes representing an uploaded file,
+// but with the "content" attribute removed.
+func stripContent(original map[string]JSONObject) map[string]JSONObject {
+ newMap := make(map[string]JSONObject, len(original)-1)
+ for key, value := range original {
+ if key != "content" {
+ newMap[key] = value
+ }
+ }
+ return newMap
+}
+
+// fileListingHandler handles requests for '/api/<version>/files/?op=list'.
+func fileListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ prefix := values.Get("prefix")
+ filenames := listFilenames(server, prefix)
+
+ // Build a sorted list of the files as map[string]JSONObject objects.
+ convertedFiles := make([]map[string]JSONObject, 0)
+ for _, filename := range filenames {
+ // The "content" attribute is not in the listing.
+ fileMap := stripContent(server.files[filename].GetMap())
+ convertedFiles = append(convertedFiles, fileMap)
+ }
+ res, err := json.MarshalIndent(convertedFiles, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// fileHandler handles requests for '/api/<version>/files/<filename>/'.
+func fileHandler(server *TestServer, w http.ResponseWriter, r *http.Request, filename string, operation string) {
+ switch {
+ case r.Method == "DELETE":
+ delete(server.files, filename)
+ w.WriteHeader(http.StatusOK)
+ case r.Method == "GET":
+ // Retrieve a file's information (including content) as a JSON
+ // object.
+ file, ok := server.files[filename]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ jsonText, err := json.MarshalIndent(file, "", " ")
+ if err != nil {
+ panic(err)
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Write(jsonText)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// InternalError replies to the request with an HTTP 500 internal error.
+func InternalError(w http.ResponseWriter, r *http.Request, err error) {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+}
+
+// getFileHandler handles requests for
+// '/api/<version>/files/?op=get&filename=filename'.
+func getFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ filename := values.Get("filename")
+ file, found := server.files[filename]
+ if !found {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ base64Content, err := file.GetField("content")
+ if err != nil {
+ InternalError(w, r, err)
+ return
+ }
+ content, err := base64.StdEncoding.DecodeString(base64Content)
+ if err != nil {
+ InternalError(w, r, err)
+ return
+ }
+ w.Write(content)
+}
+
+func readMultipart(upload *multipart.FileHeader) ([]byte, error) {
+ file, err := upload.Open()
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ reader := bufio.NewReader(file)
+ return ioutil.ReadAll(reader)
+}
+
+// filesHandler handles requests for '/api/<version>/files/?op=add&filename=filename'.
+func addFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ err := r.ParseMultipartForm(10000000)
+ checkError(err)
+
+ filename := r.Form.Get("filename")
+ if filename == "" {
+ panic("upload has no filename")
+ }
+
+ uploads := r.MultipartForm.File
+ if len(uploads) != 1 {
+ panic("the payload should contain one file and one file only")
+ }
+ var upload *multipart.FileHeader
+ for _, uploadContent := range uploads {
+ upload = uploadContent[0]
+ }
+ content, err := readMultipart(upload)
+ checkError(err)
+ server.NewFile(filename, content)
+ w.WriteHeader(http.StatusOK)
+}
+
+// networkListConnectedMACSHandler handles requests for '/api/<version>/networks/<network>/?op=list_connected_macs'
+func networkListConnectedMACSHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ networkURLRE := getNetworkURLRE(server.version)
+ networkURLREMatch := networkURLRE.FindStringSubmatch(r.URL.Path)
+ if networkURLREMatch == nil {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ networkName := networkURLREMatch[1]
+ convertedMacAddresses := []map[string]JSONObject{}
+ if macAddresses, ok := server.macAddressesPerNetwork[networkName]; ok {
+ for _, macAddress := range macAddresses {
+ m, err := macAddress.GetMap()
+ checkError(err)
+ convertedMacAddresses = append(convertedMacAddresses, m)
+ }
+ }
+ res, err := json.MarshalIndent(convertedMacAddresses, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// networksHandler handles requests for '/api/<version>/networks/?node=system_id'.
+func networksHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ panic("only networks GET operation implemented")
+ }
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ systemId := values.Get("node")
+ if op == "list_connected_macs" {
+ networkListConnectedMACSHandler(server, w, r)
+ return
+ }
+ if op != "" {
+ panic("only list_connected_macs and default operations implemented")
+ }
+ if systemId == "" {
+ panic("network missing associated node system id")
+ }
+ networks := []MAASObject{}
+ if networkNames, hasNetworks := server.networksPerNode[systemId]; hasNetworks {
+ networks = make([]MAASObject, len(networkNames))
+ for i, networkName := range networkNames {
+ networks[i] = server.networks[networkName]
+ }
+ }
+ res, err := json.MarshalIndent(networks, "", " ")
+ checkError(err)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// ipAddressesHandler handles requests for '/api/<version>/ipaddresses/'.
+func ipAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ checkError(err)
+ values := r.Form
+ op := values.Get("op")
+
+ switch r.Method {
+ case "GET":
+ if op != "" {
+ panic("expected empty op for GET, got " + op)
+ }
+ listIPAddressesHandler(server, w, r)
+ return
+ case "POST":
+ switch op {
+ case "reserve":
+ reserveIPAddressHandler(server, w, r, values.Get("network"), values.Get("requested_address"))
+ return
+ case "release":
+ releaseIPAddressHandler(server, w, r, values.Get("ip"))
+ return
+ default:
+ panic("expected op=release|reserve for POST, got " + op)
+ }
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+func marshalIPAddress(server *TestServer, ipAddress string) (JSONObject, error) {
+ jsonTemplate := `{"alloc_type": 4, "ip": %q, "resource_uri": %q, "created": %q}`
+ uri := getIPAddressesEndpoint(server.version)
+ now := time.Now().UTC().Format(time.RFC3339)
+ bytes := []byte(fmt.Sprintf(jsonTemplate, ipAddress, uri, now))
+ return Parse(server.client, bytes)
+}
+
+func badRequestError(w http.ResponseWriter, err error) {
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprint(w, err.Error())
+}
+
+func listIPAddressesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ results := []MAASObject{}
+ for _, ips := range server.IPAddresses() {
+ for _, ip := range ips {
+ jsonObj, err := marshalIPAddress(server, ip)
+ if err != nil {
+ badRequestError(w, err)
+ return
+ }
+ maasObj, err := jsonObj.GetMAASObject()
+ if err != nil {
+ badRequestError(w, err)
+ return
+ }
+ results = append(results, maasObj)
+ }
+ }
+ res, err := json.MarshalIndent(results, "", " ")
+ checkError(err)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+func reserveIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, network, reqAddress string) {
+ _, ipNet, err := net.ParseCIDR(network)
+ if err != nil {
+ badRequestError(w, fmt.Errorf("Invalid network parameter %s", network))
+ return
+ }
+ if reqAddress != "" {
+ // Validate "requested_address" parameter.
+ reqIP := net.ParseIP(reqAddress)
+ if reqIP == nil {
+ badRequestError(w, fmt.Errorf("failed to detect a valid IP address from u'%s'", reqAddress))
+ return
+ }
+ if !ipNet.Contains(reqIP) {
+ badRequestError(w, fmt.Errorf("%s is not inside the range %s", reqAddress, ipNet.String()))
+ return
+ }
+ }
+ // Find the network name matching the parsed CIDR.
+ foundNetworkName := ""
+ for netName, netObj := range server.networks {
+ // Get the "ip" and "netmask" attributes of the network.
+ netIP, err := netObj.GetField("ip")
+ checkError(err)
+ netMask, err := netObj.GetField("netmask")
+ checkError(err)
+
+ // Convert the netmask string to net.IPMask.
+ parts := strings.Split(netMask, ".")
+ ipMask := make(net.IPMask, len(parts))
+ for i, part := range parts {
+ intPart, err := strconv.Atoi(part)
+ checkError(err)
+ ipMask[i] = byte(intPart)
+ }
+ netNet := &net.IPNet{IP: net.ParseIP(netIP), Mask: ipMask}
+ if netNet.String() == network {
+ // Exact match found.
+ foundNetworkName = netName
+ break
+ }
+ }
+ if foundNetworkName == "" {
+ badRequestError(w, fmt.Errorf("No network found matching %s", network))
+ return
+ }
+ ips, found := server.ipAddressesPerNetwork[foundNetworkName]
+ if !found {
+ // This will be the first address.
+ ips = []string{}
+ }
+ reservedIP := ""
+ if reqAddress != "" {
+ // Use what the user provided. NOTE: Because this is testing
+ // code, no duplicates check is done.
+ reservedIP = reqAddress
+ } else {
+ // Generate an IP in the network range by incrementing the
+ // last byte of the network's IP.
+ firstIP := ipNet.IP
+ firstIP[len(firstIP)-1] += byte(len(ips) + 1)
+ reservedIP = firstIP.String()
+ }
+ ips = append(ips, reservedIP)
+ server.ipAddressesPerNetwork[foundNetworkName] = ips
+ jsonObj, err := marshalIPAddress(server, reservedIP)
+ checkError(err)
+ maasObj, err := jsonObj.GetMAASObject()
+ checkError(err)
+ res, err := json.MarshalIndent(maasObj, "", " ")
+ checkError(err)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+func releaseIPAddressHandler(server *TestServer, w http.ResponseWriter, r *http.Request, ip string) {
+ if netIP := net.ParseIP(ip); netIP == nil {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+ if server.RemoveIPAddress(ip) {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ http.NotFoundHandler().ServeHTTP(w, r)
+}
+
+// versionHandler handles requests for '/api/<version>/version/'.
+func versionHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ panic("only version GET operation implemented")
+ }
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, server.versionJSON)
+}
+
+// nodegroupsHandler handles requests for '/api/<version>/nodegroups/*'.
+func nodegroupsHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ values, err := url.ParseQuery(r.URL.RawQuery)
+ checkError(err)
+ op := values.Get("op")
+ bootimagesURLRE := getBootimagesURLRE(server.version)
+ bootimagesURLMatch := bootimagesURLRE.FindStringSubmatch(r.URL.Path)
+ nodegroupsInterfacesURLRE := getNodegroupsInterfacesURLRE(server.version)
+ nodegroupsInterfacesURLMatch := nodegroupsInterfacesURLRE.FindStringSubmatch(r.URL.Path)
+ nodegroupsURL := getNodegroupsEndpoint(server.version)
+ switch {
+ case r.URL.Path == nodegroupsURL:
+ nodegroupsTopLevelHandler(server, w, r, op)
+ case bootimagesURLMatch != nil:
+ bootimagesHandler(server, w, r, bootimagesURLMatch[1], op)
+ case nodegroupsInterfacesURLMatch != nil:
+ nodegroupsInterfacesHandler(server, w, r, nodegroupsInterfacesURLMatch[1], op)
+ default:
+ // Default handler: not found.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ }
+}
+
+// nodegroupsTopLevelHandler handles requests for '/api/<version>/nodegroups/'.
+func nodegroupsTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
+ if r.Method != "GET" || op != "list" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ nodegroups := []JSONObject{}
+ for uuid := range server.bootImages {
+ attrs := map[string]interface{}{
+ "uuid": uuid,
+ resourceURI: getNodegroupURL(server.version, uuid),
+ }
+ obj := maasify(server.client, attrs)
+ nodegroups = append(nodegroups, obj)
+ }
+
+ res, err := json.MarshalIndent(nodegroups, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// bootimagesHandler handles requests for '/api/<version>/nodegroups/<uuid>/boot-images/'.
+func bootimagesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ bootImages, ok := server.bootImages[nodegroupUUID]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ res, err := json.MarshalIndent(bootImages, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// nodegroupsInterfacesHandler handles requests for '/api/<version>/nodegroups/<uuid>/interfaces/'
+func nodegroupsInterfacesHandler(server *TestServer, w http.ResponseWriter, r *http.Request, nodegroupUUID, op string) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ _, ok := server.bootImages[nodegroupUUID]
+ if !ok {
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ interfaces, ok := server.nodegroupsInterfaces[nodegroupUUID]
+ if !ok {
+ // we already checked the nodegroup exists, so return an empty list
+ interfaces = []JSONObject{}
+ }
+ res, err := json.MarshalIndent(interfaces, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}
+
+// zonesHandler handles requests for '/api/<version>/zones/'.
+func zonesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if len(server.zones) == 0 {
+ // Until a zone is registered, behave as if the endpoint
+ // does not exist. This way we can simulate older MAAS
+ // servers that do not support zones.
+ http.NotFoundHandler().ServeHTTP(w, r)
+ return
+ }
+
+ zones := make([]JSONObject, 0, len(server.zones))
+ for _, zone := range server.zones {
+ zones = append(zones, zone)
+ }
+ res, err := json.MarshalIndent(zones, "", " ")
+ checkError(err)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, string(res))
+}