1
// Copyright 2013 Canonical Ltd. This software is licensed under the
2
// GNU Lesser General Public License version 3 (see the file COPYING).
21
// TestMAASObject is a fake MAAS server MAASObject.
22
type TestMAASObject struct {
24
TestServer *TestServer
27
// checkError is a shorthand helper that panics if err is not nil.
28
func checkError(err error) {
34
// NewTestMAAS returns a TestMAASObject that implements the MAASObject
35
// interface and thus can be used as a test object instead of the one returned
36
// by gomaasapi.NewMAAS().
37
func NewTestMAAS(version string) *TestMAASObject {
38
server := NewTestServer(version)
39
authClient, err := NewAnonymousClient(server.URL, version)
41
maas := NewMAAS(*authClient)
42
return &TestMAASObject{*maas, server}
45
// Close shuts down the test server.
46
func (testMAASObject *TestMAASObject) Close() {
47
testMAASObject.TestServer.Close()
50
// A TestServer is an HTTP server listening on a system-chosen port on the
51
// local loopback interface, which simulates the behavior of a MAAS server.
52
// It is intendend for use in end-to-end HTTP tests using the gomaasapi
54
type TestServer struct {
56
serveMux *http.ServeMux
58
nodes map[string]MAASObject
59
ownedNodes map[string]bool
60
// mapping system_id -> list of operations performed.
61
nodeOperations map[string][]string
62
// mapping system_id -> list of Values passed when performing
64
nodeOperationRequestValues map[string][]url.Values
65
files map[string]MAASObject
69
func getNodeURI(version, systemId string) string {
70
return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId)
73
func getFileURI(version, filename string) string {
75
uri.Path = fmt.Sprintf("/api/%s/files/%s/", version, filename)
79
// Clear clears all the fake data stored and recorded by the test server
80
// (nodes, recorded operations, etc.).
81
func (server *TestServer) Clear() {
82
server.nodes = make(map[string]MAASObject)
83
server.ownedNodes = make(map[string]bool)
84
server.nodeOperations = make(map[string][]string)
85
server.nodeOperationRequestValues = make(map[string][]url.Values)
86
server.files = make(map[string]MAASObject)
89
// NodeOperations returns the map containing the list of the operations
90
// performed for each node.
91
func (server *TestServer) NodeOperations() map[string][]string {
92
return server.nodeOperations
95
// NodeOperationRequestValues returns the map containing the list of the
96
// url.Values extracted from the request used when performing operations
98
func (server *TestServer) NodeOperationRequestValues() map[string][]url.Values {
99
return server.nodeOperationRequestValues
102
func (server *TestServer) addNodeOperation(systemId, operation string, request *http.Request) {
103
operations, present := server.nodeOperations[systemId]
104
operationRequestValues, present2 := server.nodeOperationRequestValues[systemId]
105
if present != present2 {
106
panic("inconsistent state: nodeOperations and nodeOperationRequestValues don't have the same keys.")
108
requestValues := url.Values{}
109
if request.Body != nil && request.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
110
defer request.Body.Close()
111
body, err := ioutil.ReadAll(request.Body)
115
requestValues, err = url.ParseQuery(string(body))
121
operations = []string{operation}
122
operationRequestValues = []url.Values{requestValues}
124
operations = append(operations, operation)
125
operationRequestValues = append(operationRequestValues, requestValues)
127
server.nodeOperations[systemId] = operations
128
server.nodeOperationRequestValues[systemId] = operationRequestValues
131
// NewNode creates a MAAS node. The provided string should be a valid json
132
// string representing a map and contain a string value for the key
133
// 'system_id'. e.g. `{"system_id": "mysystemid"}`.
134
// If one of these conditions is not met, NewNode panics.
135
func (server *TestServer) NewNode(jsonText string) MAASObject {
136
var attrs map[string]interface{}
137
err := json.Unmarshal([]byte(jsonText), &attrs)
139
systemIdEntry, hasSystemId := attrs["system_id"]
141
panic("The given map json string does not contain a 'system_id' value.")
143
systemId := systemIdEntry.(string)
144
attrs[resourceURI] = getNodeURI(server.version, systemId)
145
obj := newJSONMAASObject(attrs, server.client)
146
server.nodes[systemId] = obj
150
// Nodes returns a map associating all the nodes' system ids with the nodes'
152
func (server *TestServer) Nodes() map[string]MAASObject {
156
// OwnedNodes returns a map whose keys represent the nodes that are currently
158
func (server *TestServer) OwnedNodes() map[string]bool {
159
return server.ownedNodes
162
// NewFile creates a file in the test MAAS server.
163
func (server *TestServer) NewFile(filename string, filecontent []byte) MAASObject {
164
attrs := make(map[string]interface{})
165
attrs[resourceURI] = getFileURI(server.version, filename)
166
base64Content := base64.StdEncoding.EncodeToString(filecontent)
167
attrs["content"] = base64Content
168
attrs["filename"] = filename
170
// Allocate an arbitrary URL here. It would be nice if the caller
171
// could do this, but that would change the API and require many
173
escapedName := url.QueryEscape(filename)
174
attrs["anon_resource_uri"] = "/maas/1.0/files/?op=get_by_key&key=" + escapedName + "_key"
176
obj := newJSONMAASObject(attrs, server.client)
177
server.files[filename] = obj
181
func (server *TestServer) Files() map[string]MAASObject {
185
// ChangeNode updates a node with the given key/value.
186
func (server *TestServer) ChangeNode(systemId, key, value string) {
187
node, found := server.nodes[systemId]
189
panic("No node with such 'system_id'.")
191
node.GetMap()[key] = maasify(server.client, value)
194
func getTopLevelNodesURL(version string) string {
195
return fmt.Sprintf("/api/%s/nodes/", version)
198
func getNodeURLRE(version string) *regexp.Regexp {
199
reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", regexp.QuoteMeta(version))
200
return regexp.MustCompile(reString)
203
func getFilesURL(version string) string {
204
return fmt.Sprintf("/api/%s/files/", version)
207
func getFileURLRE(version string) *regexp.Regexp {
208
reString := fmt.Sprintf("^/api/%s/files/(.*)/$", regexp.QuoteMeta(version))
209
return regexp.MustCompile(reString)
212
// NewTestServer starts and returns a new MAAS test server. The caller should call Close when finished, to shut it down.
213
func NewTestServer(version string) *TestServer {
214
server := &TestServer{version: version}
216
serveMux := http.NewServeMux()
217
nodesURL := getTopLevelNodesURL(server.version)
218
// Register handler for '/api/<version>/nodes/*'.
219
serveMux.HandleFunc(nodesURL, func(w http.ResponseWriter, r *http.Request) {
220
nodesHandler(server, w, r)
222
filesURL := getFilesURL(server.version)
223
// Register handler for '/api/<version>/files/*'.
224
serveMux.HandleFunc(filesURL, func(w http.ResponseWriter, r *http.Request) {
225
filesHandler(server, w, r)
228
newServer := httptest.NewServer(serveMux)
229
client, err := NewAnonymousClient(newServer.URL, "1.0")
231
server.Server = newServer
232
server.serveMux = serveMux
233
server.client = *client
238
// nodesHandler handles requests for '/api/<version>/nodes/*'.
239
func nodesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
240
values, err := url.ParseQuery(r.URL.RawQuery)
242
op := values.Get("op")
243
nodeURLRE := getNodeURLRE(server.version)
244
nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path)
245
nodesURL := getTopLevelNodesURL(server.version)
247
case r.URL.Path == nodesURL:
248
nodesTopLevelHandler(server, w, r, op)
249
case nodeURLMatch != nil:
250
// Request for a single node.
251
nodeHandler(server, w, r, nodeURLMatch[1], op)
253
// Default handler: not found.
254
http.NotFoundHandler().ServeHTTP(w, r)
258
// nodeHandler handles requests for '/api/<version>/nodes/<system_id>/'.
259
func nodeHandler(server *TestServer, w http.ResponseWriter, r *http.Request, systemId string, operation string) {
260
node, ok := server.nodes[systemId]
262
http.NotFoundHandler().ServeHTTP(w, r)
265
if r.Method == "GET" {
267
w.WriteHeader(http.StatusOK)
268
fmt.Fprint(w, marshalNode(node))
271
w.WriteHeader(http.StatusBadRequest)
275
if r.Method == "POST" {
276
// The only operations supported are "start", "stop" and "release".
277
if operation == "start" || operation == "stop" || operation == "release" {
278
// Record operation on node.
279
server.addNodeOperation(systemId, operation, r)
281
if operation == "release" {
282
delete(server.OwnedNodes(), systemId)
285
w.WriteHeader(http.StatusOK)
286
fmt.Fprint(w, marshalNode(node))
289
w.WriteHeader(http.StatusBadRequest)
293
if r.Method == "DELETE" {
294
delete(server.nodes, systemId)
295
w.WriteHeader(http.StatusOK)
298
http.NotFoundHandler().ServeHTTP(w, r)
301
func contains(slice []string, val string) bool {
302
for _, item := range slice {
310
// nodeListingHandler handles requests for '/nodes/'.
311
func nodeListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
312
values, err := url.ParseQuery(r.URL.RawQuery)
314
ids, hasId := values["id"]
315
var convertedNodes = []map[string]JSONObject{}
316
for systemId, node := range server.nodes {
317
if !hasId || contains(ids, systemId) {
318
convertedNodes = append(convertedNodes, node.GetMap())
321
res, err := json.Marshal(convertedNodes)
323
w.WriteHeader(http.StatusOK)
324
fmt.Fprint(w, string(res))
327
// findFreeNode looks for a node that is currently available.
328
func findFreeNode(server *TestServer) *MAASObject {
329
for systemID, node := range server.Nodes() {
330
_, present := server.OwnedNodes()[systemID]
338
// nodesAcquireHandler simulates acquiring a node.
339
func nodesAcquireHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
340
node := findFreeNode(server)
342
w.WriteHeader(http.StatusConflict)
344
systemId, err := node.GetField("system_id")
346
server.OwnedNodes()[systemId] = true
347
res, err := json.Marshal(node)
350
server.addNodeOperation(systemId, "acquire", r)
351
w.WriteHeader(http.StatusOK)
352
fmt.Fprint(w, string(res))
356
// nodesTopLevelHandler handles a request for /api/<version>/nodes/
357
// (with no node id following as part of the path).
358
func nodesTopLevelHandler(server *TestServer, w http.ResponseWriter, r *http.Request, op string) {
360
case r.Method == "GET" && op == "list":
361
// Node listing operation.
362
nodeListingHandler(server, w, r)
363
case r.Method == "POST" && op == "acquire":
364
nodesAcquireHandler(server, w, r)
366
w.WriteHeader(http.StatusBadRequest)
370
// filesHandler handles requests for '/api/<version>/files/*'.
371
func filesHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
372
values, err := url.ParseQuery(r.URL.RawQuery)
374
op := values.Get("op")
375
fileURLRE := getFileURLRE(server.version)
376
fileURLMatch := fileURLRE.FindStringSubmatch(r.URL.Path)
377
fileListingURL := getFilesURL(server.version)
379
case r.Method == "GET" && op == "list" && r.URL.Path == fileListingURL:
380
// File listing operation.
381
fileListingHandler(server, w, r)
382
case op == "get" && r.Method == "GET" && r.URL.Path == fileListingURL:
383
getFileHandler(server, w, r)
384
case op == "add" && r.Method == "POST" && r.URL.Path == fileListingURL:
385
addFileHandler(server, w, r)
386
case fileURLMatch != nil:
387
// Request for a single file.
388
fileHandler(server, w, r, fileURLMatch[1], op)
390
// Default handler: not found.
391
http.NotFoundHandler().ServeHTTP(w, r)
396
// listFilenames returns the names of those uploaded files whose names start
397
// with the given prefix, sorted lexicographically.
398
func listFilenames(server *TestServer, prefix string) []string {
399
var filenames = make([]string, 0)
400
for filename := range server.files {
401
if strings.HasPrefix(filename, prefix) {
402
filenames = append(filenames, filename)
405
sort.Strings(filenames)
409
// stripFileContent copies a map of attributes representing an uploaded file,
410
// but with the "content" attribute removed.
411
func stripContent(original map[string]JSONObject) map[string]JSONObject {
412
newMap := make(map[string]JSONObject, len(original)-1)
413
for key, value := range original {
414
if key != "content" {
421
// fileListingHandler handles requests for '/api/<version>/files/?op=list'.
422
func fileListingHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
423
values, err := url.ParseQuery(r.URL.RawQuery)
425
prefix := values.Get("prefix")
426
filenames := listFilenames(server, prefix)
428
// Build a sorted list of the files as map[string]JSONObject objects.
429
convertedFiles := make([]map[string]JSONObject, 0)
430
for _, filename := range filenames {
431
// The "content" attribute is not in the listing.
432
fileMap := stripContent(server.files[filename].GetMap())
433
convertedFiles = append(convertedFiles, fileMap)
435
res, err := json.Marshal(convertedFiles)
437
w.WriteHeader(http.StatusOK)
438
fmt.Fprint(w, string(res))
441
// fileHandler handles requests for '/api/<version>/files/<filename>/'.
442
func fileHandler(server *TestServer, w http.ResponseWriter, r *http.Request, filename string, operation string) {
444
case r.Method == "DELETE":
445
delete(server.files, filename)
446
w.WriteHeader(http.StatusOK)
447
case r.Method == "GET":
448
// Retrieve a file's information (including content) as a JSON
450
file, ok := server.files[filename]
452
http.NotFoundHandler().ServeHTTP(w, r)
455
jsonText, err := json.Marshal(file)
459
w.WriteHeader(http.StatusOK)
462
// Default handler: not found.
463
http.NotFoundHandler().ServeHTTP(w, r)
467
// InternalError replies to the request with an HTTP 500 internal error.
468
func InternalError(w http.ResponseWriter, r *http.Request, err error) {
469
http.Error(w, err.Error(), http.StatusInternalServerError)
472
// getFileHandler handles requests for
473
// '/api/<version>/files/?op=get&filename=filename'.
474
func getFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
475
values, err := url.ParseQuery(r.URL.RawQuery)
477
filename := values.Get("filename")
478
file, found := server.files[filename]
480
http.NotFoundHandler().ServeHTTP(w, r)
483
base64Content, err := file.GetField("content")
485
InternalError(w, r, err)
488
content, err := base64.StdEncoding.DecodeString(base64Content)
490
InternalError(w, r, err)
496
func readMultipart(upload *multipart.FileHeader) ([]byte, error) {
497
file, err := upload.Open()
502
reader := bufio.NewReader(file)
503
return ioutil.ReadAll(reader)
506
// filesHandler handles requests for '/api/<version>/files/?op=add&filename=filename'.
507
func addFileHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
508
err := r.ParseMultipartForm(10000000)
511
filename := r.Form.Get("filename")
513
panic("upload has no filename")
516
uploads := r.MultipartForm.File
517
if len(uploads) != 1 {
518
panic("the payload should contain one file and one file only")
520
var upload *multipart.FileHeader
521
for _, uploadContent := range uploads {
522
upload = uploadContent[0]
524
content, err := readMultipart(upload)
526
server.NewFile(filename, content)
527
w.WriteHeader(http.StatusOK)