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