~mwhudson/ubuntu/xenial/juju-core/mwhudson

« back to all changes in this revision

Viewing changes to src/launchpad.net/gomaasapi/testservice.go

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2013-04-24 22:34:47 UTC
  • Revision ID: package-import@ubuntu.com-20130424223447-f0qdji7ubnyo0s71
Tags: upstream-1.10.0.1
ImportĀ upstreamĀ versionĀ 1.10.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2013 Canonical Ltd.  This software is licensed under the
 
2
// GNU Lesser General Public License version 3 (see the file COPYING).
 
3
 
 
4
package gomaasapi
 
5
 
 
6
import (
 
7
        "bufio"
 
8
        "encoding/base64"
 
9
        "encoding/json"
 
10
        "fmt"
 
11
        "io/ioutil"
 
12
        "mime/multipart"
 
13
        "net/http"
 
14
        "net/http/httptest"
 
15
        "net/url"
 
16
        "regexp"
 
17
        "sort"
 
18
        "strings"
 
19
)
 
20
 
 
21
// TestMAASObject is a fake MAAS server MAASObject.
 
22
type TestMAASObject struct {
 
23
        MAASObject
 
24
        TestServer *TestServer
 
25
}
 
26
 
 
27
// checkError is a shorthand helper that panics if err is not nil.
 
28
func checkError(err error) {
 
29
        if err != nil {
 
30
                panic(err)
 
31
        }
 
32
}
 
33
 
 
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)
 
40
        checkError(err)
 
41
        maas := NewMAAS(*authClient)
 
42
        return &TestMAASObject{*maas, server}
 
43
}
 
44
 
 
45
// Close shuts down the test server.
 
46
func (testMAASObject *TestMAASObject) Close() {
 
47
        testMAASObject.TestServer.Close()
 
48
}
 
49
 
 
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
 
53
// library.
 
54
type TestServer struct {
 
55
        *httptest.Server
 
56
        serveMux   *http.ServeMux
 
57
        client     Client
 
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
 
63
        // operations
 
64
        nodeOperationRequestValues map[string][]url.Values
 
65
        files                      map[string]MAASObject
 
66
        version                    string
 
67
}
 
68
 
 
69
func getNodeURI(version, systemId string) string {
 
70
        return fmt.Sprintf("/api/%s/nodes/%s/", version, systemId)
 
71
}
 
72
 
 
73
func getFileURI(version, filename string) string {
 
74
        uri := url.URL{}
 
75
        uri.Path = fmt.Sprintf("/api/%s/files/%s/", version, filename)
 
76
        return uri.String()
 
77
}
 
78
 
 
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)
 
87
}
 
88
 
 
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
 
93
}
 
94
 
 
95
// NodeOperationRequestValues returns the map containing the list of the
 
96
// url.Values extracted from the request used when performing operations
 
97
// on nodes.
 
98
func (server *TestServer) NodeOperationRequestValues() map[string][]url.Values {
 
99
        return server.nodeOperationRequestValues
 
100
}
 
101
 
 
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.")
 
107
        }
 
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)
 
112
                if err != nil {
 
113
                        panic(err)
 
114
                }
 
115
                requestValues, err = url.ParseQuery(string(body))
 
116
                if err != nil {
 
117
                        panic(err)
 
118
                }
 
119
        }
 
120
        if !present {
 
121
                operations = []string{operation}
 
122
                operationRequestValues = []url.Values{requestValues}
 
123
        } else {
 
124
                operations = append(operations, operation)
 
125
                operationRequestValues = append(operationRequestValues, requestValues)
 
126
        }
 
127
        server.nodeOperations[systemId] = operations
 
128
        server.nodeOperationRequestValues[systemId] = operationRequestValues
 
129
}
 
130
 
 
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)
 
138
        checkError(err)
 
139
        systemIdEntry, hasSystemId := attrs["system_id"]
 
140
        if !hasSystemId {
 
141
                panic("The given map json string does not contain a 'system_id' value.")
 
142
        }
 
143
        systemId := systemIdEntry.(string)
 
144
        attrs[resourceURI] = getNodeURI(server.version, systemId)
 
145
        obj := newJSONMAASObject(attrs, server.client)
 
146
        server.nodes[systemId] = obj
 
147
        return obj
 
148
}
 
149
 
 
150
// Nodes returns a map associating all the nodes' system ids with the nodes'
 
151
// objects.
 
152
func (server *TestServer) Nodes() map[string]MAASObject {
 
153
        return server.nodes
 
154
}
 
155
 
 
156
// OwnedNodes returns a map whose keys represent the nodes that are currently
 
157
// allocated.
 
158
func (server *TestServer) OwnedNodes() map[string]bool {
 
159
        return server.ownedNodes
 
160
}
 
161
 
 
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
 
169
 
 
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
 
172
        // changes.
 
173
        escapedName := url.QueryEscape(filename)
 
174
        attrs["anon_resource_uri"] = "/maas/1.0/files/?op=get_by_key&key=" + escapedName + "_key"
 
175
 
 
176
        obj := newJSONMAASObject(attrs, server.client)
 
177
        server.files[filename] = obj
 
178
        return obj
 
179
}
 
180
 
 
181
func (server *TestServer) Files() map[string]MAASObject {
 
182
        return server.files
 
183
}
 
184
 
 
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]
 
188
        if !found {
 
189
                panic("No node with such 'system_id'.")
 
190
        }
 
191
        node.GetMap()[key] = maasify(server.client, value)
 
192
}
 
193
 
 
194
func getTopLevelNodesURL(version string) string {
 
195
        return fmt.Sprintf("/api/%s/nodes/", version)
 
196
}
 
197
 
 
198
func getNodeURLRE(version string) *regexp.Regexp {
 
199
        reString := fmt.Sprintf("^/api/%s/nodes/([^/]*)/$", regexp.QuoteMeta(version))
 
200
        return regexp.MustCompile(reString)
 
201
}
 
202
 
 
203
func getFilesURL(version string) string {
 
204
        return fmt.Sprintf("/api/%s/files/", version)
 
205
}
 
206
 
 
207
func getFileURLRE(version string) *regexp.Regexp {
 
208
        reString := fmt.Sprintf("^/api/%s/files/(.*)/$", regexp.QuoteMeta(version))
 
209
        return regexp.MustCompile(reString)
 
210
}
 
211
 
 
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}
 
215
 
 
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)
 
221
        })
 
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)
 
226
        })
 
227
 
 
228
        newServer := httptest.NewServer(serveMux)
 
229
        client, err := NewAnonymousClient(newServer.URL, "1.0")
 
230
        checkError(err)
 
231
        server.Server = newServer
 
232
        server.serveMux = serveMux
 
233
        server.client = *client
 
234
        server.Clear()
 
235
        return server
 
236
}
 
237
 
 
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)
 
241
        checkError(err)
 
242
        op := values.Get("op")
 
243
        nodeURLRE := getNodeURLRE(server.version)
 
244
        nodeURLMatch := nodeURLRE.FindStringSubmatch(r.URL.Path)
 
245
        nodesURL := getTopLevelNodesURL(server.version)
 
246
        switch {
 
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)
 
252
        default:
 
253
                // Default handler: not found.
 
254
                http.NotFoundHandler().ServeHTTP(w, r)
 
255
        }
 
256
}
 
257
 
 
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]
 
261
        if !ok {
 
262
                http.NotFoundHandler().ServeHTTP(w, r)
 
263
                return
 
264
        }
 
265
        if r.Method == "GET" {
 
266
                if operation == "" {
 
267
                        w.WriteHeader(http.StatusOK)
 
268
                        fmt.Fprint(w, marshalNode(node))
 
269
                        return
 
270
                } else {
 
271
                        w.WriteHeader(http.StatusBadRequest)
 
272
                        return
 
273
                }
 
274
        }
 
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)
 
280
 
 
281
                        if operation == "release" {
 
282
                                delete(server.OwnedNodes(), systemId)
 
283
                        }
 
284
 
 
285
                        w.WriteHeader(http.StatusOK)
 
286
                        fmt.Fprint(w, marshalNode(node))
 
287
                        return
 
288
                } else {
 
289
                        w.WriteHeader(http.StatusBadRequest)
 
290
                        return
 
291
                }
 
292
        }
 
293
        if r.Method == "DELETE" {
 
294
                delete(server.nodes, systemId)
 
295
                w.WriteHeader(http.StatusOK)
 
296
                return
 
297
        }
 
298
        http.NotFoundHandler().ServeHTTP(w, r)
 
299
}
 
300
 
 
301
func contains(slice []string, val string) bool {
 
302
        for _, item := range slice {
 
303
                if item == val {
 
304
                        return true
 
305
                }
 
306
        }
 
307
        return false
 
308
}
 
309
 
 
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)
 
313
        checkError(err)
 
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())
 
319
                }
 
320
        }
 
321
        res, err := json.Marshal(convertedNodes)
 
322
        checkError(err)
 
323
        w.WriteHeader(http.StatusOK)
 
324
        fmt.Fprint(w, string(res))
 
325
}
 
326
 
 
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]
 
331
                if !present {
 
332
                        return &node
 
333
                }
 
334
        }
 
335
        return nil
 
336
}
 
337
 
 
338
// nodesAcquireHandler simulates acquiring a node.
 
339
func nodesAcquireHandler(server *TestServer, w http.ResponseWriter, r *http.Request) {
 
340
        node := findFreeNode(server)
 
341
        if node == nil {
 
342
                w.WriteHeader(http.StatusConflict)
 
343
        } else {
 
344
                systemId, err := node.GetField("system_id")
 
345
                checkError(err)
 
346
                server.OwnedNodes()[systemId] = true
 
347
                res, err := json.Marshal(node)
 
348
                checkError(err)
 
349
                // Record operation.
 
350
                server.addNodeOperation(systemId, "acquire", r)
 
351
                w.WriteHeader(http.StatusOK)
 
352
                fmt.Fprint(w, string(res))
 
353
        }
 
354
}
 
355
 
 
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) {
 
359
        switch {
 
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)
 
365
        default:
 
366
                w.WriteHeader(http.StatusBadRequest)
 
367
        }
 
368
}
 
369
 
 
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)
 
373
        checkError(err)
 
374
        op := values.Get("op")
 
375
        fileURLRE := getFileURLRE(server.version)
 
376
        fileURLMatch := fileURLRE.FindStringSubmatch(r.URL.Path)
 
377
        fileListingURL := getFilesURL(server.version)
 
378
        switch {
 
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)
 
389
        default:
 
390
                // Default handler: not found.
 
391
                http.NotFoundHandler().ServeHTTP(w, r)
 
392
        }
 
393
 
 
394
}
 
395
 
 
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)
 
403
                }
 
404
        }
 
405
        sort.Strings(filenames)
 
406
        return filenames
 
407
}
 
408
 
 
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" {
 
415
                        newMap[key] = value
 
416
                }
 
417
        }
 
418
        return newMap
 
419
}
 
420
 
 
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)
 
424
        checkError(err)
 
425
        prefix := values.Get("prefix")
 
426
        filenames := listFilenames(server, prefix)
 
427
 
 
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)
 
434
        }
 
435
        res, err := json.Marshal(convertedFiles)
 
436
        checkError(err)
 
437
        w.WriteHeader(http.StatusOK)
 
438
        fmt.Fprint(w, string(res))
 
439
}
 
440
 
 
441
// fileHandler handles requests for '/api/<version>/files/<filename>/'.
 
442
func fileHandler(server *TestServer, w http.ResponseWriter, r *http.Request, filename string, operation string) {
 
443
        switch {
 
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
 
449
                // object.
 
450
                file, ok := server.files[filename]
 
451
                if !ok {
 
452
                        http.NotFoundHandler().ServeHTTP(w, r)
 
453
                        return
 
454
                }
 
455
                jsonText, err := json.Marshal(file)
 
456
                if err != nil {
 
457
                        panic(err)
 
458
                }
 
459
                w.WriteHeader(http.StatusOK)
 
460
                w.Write(jsonText)
 
461
        default:
 
462
                // Default handler: not found.
 
463
                http.NotFoundHandler().ServeHTTP(w, r)
 
464
        }
 
465
}
 
466
 
 
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)
 
470
}
 
471
 
 
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)
 
476
        checkError(err)
 
477
        filename := values.Get("filename")
 
478
        file, found := server.files[filename]
 
479
        if !found {
 
480
                http.NotFoundHandler().ServeHTTP(w, r)
 
481
                return
 
482
        }
 
483
        base64Content, err := file.GetField("content")
 
484
        if err != nil {
 
485
                InternalError(w, r, err)
 
486
                return
 
487
        }
 
488
        content, err := base64.StdEncoding.DecodeString(base64Content)
 
489
        if err != nil {
 
490
                InternalError(w, r, err)
 
491
                return
 
492
        }
 
493
        w.Write(content)
 
494
}
 
495
 
 
496
func readMultipart(upload *multipart.FileHeader) ([]byte, error) {
 
497
        file, err := upload.Open()
 
498
        if err != nil {
 
499
                return nil, err
 
500
        }
 
501
        defer file.Close()
 
502
        reader := bufio.NewReader(file)
 
503
        return ioutil.ReadAll(reader)
 
504
}
 
505
 
 
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)
 
509
        checkError(err)
 
510
 
 
511
        filename := r.Form.Get("filename")
 
512
        if filename == "" {
 
513
                panic("upload has no filename")
 
514
        }
 
515
 
 
516
        uploads := r.MultipartForm.File
 
517
        if len(uploads) != 1 {
 
518
                panic("the payload should contain one file and one file only")
 
519
        }
 
520
        var upload *multipart.FileHeader
 
521
        for _, uploadContent := range uploads {
 
522
                upload = uploadContent[0]
 
523
        }
 
524
        content, err := readMultipart(upload)
 
525
        checkError(err)
 
526
        server.NewFile(filename, content)
 
527
        w.WriteHeader(http.StatusOK)
 
528
}