| // 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)) |
| } |