1
# Copyright 2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Tests for the `Boot Resources` API."""
6
from __future__ import (
21
from django.core.urlresolvers import reverse
22
from maasserver.api import boot_resources
23
from maasserver.api.boot_resources import (
24
boot_resource_file_to_dict,
25
boot_resource_set_to_dict,
26
boot_resource_to_dict,
28
from maasserver.enum import (
29
BOOT_RESOURCE_FILE_TYPE,
31
BOOT_RESOURCE_TYPE_CHOICES_DICT,
33
from maasserver.fields import LargeObjectFile
34
from maasserver.models import (
38
from maasserver.testing.api import APITestCase
39
from maasserver.testing.architecture import make_usable_architecture
40
from maasserver.testing.factory import factory
41
from maasserver.testing.orm import reload_object
42
from maasserver.testing.testcase import MAASServerTestCase
43
from maastesting.matchers import MockCalledOnceWith
44
from maastesting.utils import sample_binary_data
45
from mock import MagicMock
46
from testtools.matchers import ContainsAll
49
def get_boot_resource_uri(resource):
50
"""Return a boot resource's URI on the API."""
52
'boot_resource_handler',
56
class TestHelpers(MAASServerTestCase):
58
def test_boot_resource_file_to_dict(self):
59
size = random.randint(512, 1023)
60
total_size = random.randint(1024, 2048)
61
content = factory.make_string(size)
62
largefile = factory.make_LargeFile(content=content, size=total_size)
63
resource = factory.make_BootResource(
64
rtype=BOOT_RESOURCE_TYPE.UPLOADED)
65
resource_set = factory.make_BootResourceSet(resource)
66
rfile = factory.make_BootResourceFile(resource_set, largefile)
67
dict_representation = boot_resource_file_to_dict(rfile)
68
self.assertEqual(rfile.filename, dict_representation['filename'])
69
self.assertEqual(rfile.filetype, dict_representation['filetype'])
70
self.assertEqual(rfile.largefile.sha256, dict_representation['sha256'])
71
self.assertEqual(total_size, dict_representation['size'])
72
self.assertEqual(False, dict_representation['complete'])
74
rfile.largefile.progress, dict_representation['progress'])
77
'boot_resource_file_upload_handler',
78
args=[resource.id, rfile.id]),
79
dict_representation['upload_uri'])
81
def test_boot_resource_set_to_dict(self):
82
resource = factory.make_BootResource()
83
resource_set = factory.make_BootResourceSet(resource)
84
total_size = random.randint(1024, 2048)
85
content = factory.make_string(random.randint(512, 1023))
86
largefile = factory.make_LargeFile(content=content, size=total_size)
87
rfile = factory.make_BootResourceFile(resource_set, largefile)
88
dict_representation = boot_resource_set_to_dict(resource_set)
89
self.assertEqual(resource_set.version, dict_representation['version'])
90
self.assertEqual(resource_set.label, dict_representation['label'])
91
self.assertEqual(resource_set.total_size, dict_representation['size'])
92
self.assertEqual(False, dict_representation['complete'])
94
resource_set.progress, dict_representation['progress'])
95
self.assertItemsEqual(
96
boot_resource_file_to_dict(rfile),
97
dict_representation['files'][rfile.filename])
99
def test_boot_resource_to_dict_without_sets(self):
100
resource = factory.make_BootResource()
101
factory.make_BootResourceSet(resource)
102
dict_representation = boot_resource_to_dict(resource, with_sets=False)
103
self.assertEqual(resource.id, dict_representation['id'])
105
BOOT_RESOURCE_TYPE_CHOICES_DICT[resource.rtype],
106
dict_representation['type'])
107
self.assertEqual(resource.name, dict_representation['name'])
109
resource.architecture, dict_representation['architecture'])
111
get_boot_resource_uri(resource),
112
dict_representation['resource_uri'])
113
self.assertFalse('sets' in dict_representation)
115
def test_boot_resource_to_dict_with_sets(self):
116
resource = factory.make_BootResource()
117
resource_set = factory.make_BootResourceSet(resource)
118
dict_representation = boot_resource_to_dict(resource, with_sets=True)
119
self.assertItemsEqual(
120
boot_resource_set_to_dict(resource_set),
121
dict_representation['sets'][resource_set.version])
124
class TestBootResourcesAPI(APITestCase):
125
"""Test the the boot resource API."""
127
def test_handler_path(self):
129
'/api/1.0/boot-resources/',
130
reverse('boot_resources_handler'))
132
def test_GET_returns_boot_resources_list(self):
134
factory.make_BootResource() for _ in range(3)]
135
response = self.client.get(
136
reverse('boot_resources_handler'))
137
self.assertEqual(httplib.OK, response.status_code, response.content)
138
parsed_result = json.loads(response.content)
139
self.assertItemsEqual(
140
[resource.id for resource in resources],
141
[resource.get('id') for resource in parsed_result])
143
def test_GET_synced_returns_synced_boot_resources(self):
145
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.SYNCED)
148
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.GENERATED)
149
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.UPLOADED)
150
response = self.client.get(
151
reverse('boot_resources_handler'), {'type': 'synced'})
152
self.assertEqual(httplib.OK, response.status_code, response.content)
153
parsed_result = json.loads(response.content)
154
self.assertItemsEqual(
155
[resource.id for resource in resources],
156
[resource.get('id') for resource in parsed_result])
158
def test_GET_generated_returns_generated_boot_resources(self):
160
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.GENERATED)
163
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.SYNCED)
164
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.UPLOADED)
165
response = self.client.get(
166
reverse('boot_resources_handler'), {'type': 'generated'})
167
self.assertEqual(httplib.OK, response.status_code, response.content)
168
parsed_result = json.loads(response.content)
169
self.assertItemsEqual(
170
[resource.id for resource in resources],
171
[resource.get('id') for resource in parsed_result])
173
def test_GET_uploaded_returns_uploaded_boot_resources(self):
175
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.UPLOADED)
178
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.SYNCED)
179
factory.make_BootResource(rtype=BOOT_RESOURCE_TYPE.GENERATED)
180
response = self.client.get(
181
reverse('boot_resources_handler'), {'type': 'uploaded'})
182
self.assertEqual(httplib.OK, response.status_code, response.content)
183
parsed_result = json.loads(response.content)
184
self.assertItemsEqual(
185
[resource.id for resource in resources],
186
[resource.get('id') for resource in parsed_result])
188
def test_GET_doesnt_include_full_definition_of_boot_resource(self):
189
factory.make_BootResource()
190
response = self.client.get(
191
reverse('boot_resources_handler'))
192
self.assertEqual(httplib.OK, response.status_code, response.content)
193
parsed_result = json.loads(response.content)
194
self.assertFalse('sets' in parsed_result[0])
196
def test_POST_requires_admin(self):
198
'name': factory.make_name('name'),
199
'architecture': make_usable_architecture(self),
201
factory.make_file_upload(content=sample_binary_data)),
203
response = self.client.post(
204
reverse('boot_resources_handler'), params)
205
self.assertEqual(httplib.FORBIDDEN, response.status_code)
207
def pick_filetype(self):
208
upload_type = random.choice([
210
if upload_type == 'tgz':
211
filetype = BOOT_RESOURCE_FILE_TYPE.ROOT_TGZ
212
elif upload_type == 'ddtgz':
213
filetype = BOOT_RESOURCE_FILE_TYPE.ROOT_DD
214
return upload_type, filetype
216
def test_POST_creates_boot_resource(self):
219
name = factory.make_name('name')
220
architecture = make_usable_architecture(self)
221
upload_type, filetype = self.pick_filetype()
224
'architecture': architecture,
225
'filetype': upload_type,
227
factory.make_file_upload(content=sample_binary_data)),
229
response = self.client.post(
230
reverse('boot_resources_handler'), params)
231
self.assertEqual(httplib.CREATED, response.status_code)
232
parsed_result = json.loads(response.content)
234
resource = BootResource.objects.get(id=parsed_result['id'])
235
resource_set = resource.sets.first()
236
rfile = resource_set.files.first()
237
self.assertEqual(name, resource.name)
238
self.assertEqual(architecture, resource.architecture)
239
self.assertEqual('uploaded', resource_set.label)
240
self.assertEqual(filetype, rfile.filename)
241
self.assertEqual(filetype, rfile.filetype)
242
with rfile.largefile.content.open('rb') as stream:
243
written_data = stream.read()
244
self.assertEqual(sample_binary_data, written_data)
246
def test_POST_creates_boot_resource_with_default_filetype(self):
249
name = factory.make_name('name')
250
architecture = make_usable_architecture(self)
253
'architecture': architecture,
255
factory.make_file_upload(content=sample_binary_data)),
257
response = self.client.post(
258
reverse('boot_resources_handler'), params)
259
self.assertEqual(httplib.CREATED, response.status_code)
260
parsed_result = json.loads(response.content)
262
resource = BootResource.objects.get(id=parsed_result['id'])
263
resource_set = resource.sets.first()
264
rfile = resource_set.files.first()
265
self.assertEqual(BOOT_RESOURCE_FILE_TYPE.ROOT_TGZ, rfile.filetype)
267
def test_POST_creates_boot_resource_with_already_existing_largefile(self):
270
largefile = factory.make_LargeFile()
271
name = factory.make_name('name')
272
architecture = make_usable_architecture(self)
275
'architecture': architecture,
276
'sha256': largefile.sha256,
277
'size': largefile.total_size,
279
response = self.client.post(
280
reverse('boot_resources_handler'), params)
281
self.assertEqual(httplib.CREATED, response.status_code)
282
parsed_result = json.loads(response.content)
284
resource = BootResource.objects.get(id=parsed_result['id'])
285
resource_set = resource.sets.first()
286
rfile = resource_set.files.first()
287
self.assertEqual(largefile, rfile.largefile)
289
def test_POST_creates_boot_resource_with_empty_largefile(self):
292
# Create a largefile to get a random sha256 and size. We delete it
293
# immediately so the new resource does not pick it up.
294
largefile = factory.make_LargeFile()
297
name = factory.make_name('name')
298
architecture = make_usable_architecture(self)
301
'architecture': architecture,
302
'sha256': largefile.sha256,
303
'size': largefile.total_size,
305
response = self.client.post(
306
reverse('boot_resources_handler'), params)
307
self.assertEqual(httplib.CREATED, response.status_code)
308
parsed_result = json.loads(response.content)
310
resource = BootResource.objects.get(id=parsed_result['id'])
311
resource_set = resource.sets.first()
312
rfile = resource_set.files.first()
314
(largefile.sha256, largefile.total_size, False),
315
(rfile.largefile.sha256, rfile.largefile.total_size,
316
rfile.largefile.complete))
318
def test_POST_validates_size_matches_total_size_for_largefile(self):
321
largefile = factory.make_LargeFile()
322
name = factory.make_name('name')
323
architecture = make_usable_architecture(self)
326
'architecture': architecture,
327
'sha256': largefile.sha256,
328
'size': largefile.total_size + 1,
330
response = self.client.post(
331
reverse('boot_resources_handler'), params)
332
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
334
def test_POST_returns_full_definition_of_boot_resource(self):
337
name = factory.make_name('name')
338
architecture = make_usable_architecture(self)
341
'architecture': architecture,
343
factory.make_file_upload(content=sample_binary_data)),
345
response = self.client.post(
346
reverse('boot_resources_handler'), params)
347
self.assertEqual(httplib.CREATED, response.status_code)
348
parsed_result = json.loads(response.content)
349
self.assertTrue('sets' in parsed_result)
351
def test_POST_validates_boot_resource(self):
355
'name': factory.make_name('name'),
357
response = self.client.post(
358
reverse('boot_resources_handler'), params)
359
self.assertEqual(httplib.BAD_REQUEST, response.status_code)
361
def test_POST_calls_import_boot_images_on_all_clusters(self):
364
nodegroup = MagicMock()
365
self.patch(boot_resources, 'NodeGroup', nodegroup)
367
name = factory.make_name('name')
368
architecture = make_usable_architecture(self)
371
'architecture': architecture,
373
factory.make_file_upload(content=sample_binary_data)),
375
response = self.client.post(
376
reverse('boot_resources_handler'), params)
377
self.assertEqual(httplib.CREATED, response.status_code)
379
nodegroup.objects.import_boot_images_on_accepted_clusters,
380
MockCalledOnceWith())
382
def test_import_requires_admin(self):
383
response = self.client.post(
384
reverse('boot_resources_handler'), {'op': 'import'})
385
self.assertEqual(httplib.FORBIDDEN, response.status_code)
388
class TestBootResourceAPI(APITestCase):
390
def test_handler_path(self):
392
'/api/1.0/boot-resources/3/',
393
reverse('boot_resource_handler', args=['3']))
395
def test_GET_returns_boot_resource(self):
396
resource = factory.make_usable_boot_resource()
397
response = self.client.get(get_boot_resource_uri(resource))
398
self.assertEqual(httplib.OK, response.status_code)
399
returned_resource = json.loads(response.content)
400
# The returned object contains a 'resource_uri' field.
403
'boot_resource_handler',
406
returned_resource['resource_uri'])
409
ContainsAll(['id', 'type', 'name', 'architecture']))
411
def test_DELETE_deletes_boot_resource(self):
413
resource = factory.make_BootResource()
414
response = self.client.delete(get_boot_resource_uri(resource))
415
self.assertEqual(httplib.NO_CONTENT, response.status_code)
416
self.assertIsNone(reload_object(resource))
418
def test_DELETE_requires_admin(self):
419
resource = factory.make_BootResource()
420
response = self.client.delete(get_boot_resource_uri(resource))
421
self.assertEqual(httplib.FORBIDDEN, response.status_code)
424
class TestBootResourceFileUploadAPI(APITestCase):
426
def get_boot_resource_file_upload_uri(self, rfile):
427
"""Return a boot resource file's URI on the API."""
429
'boot_resource_file_upload_handler',
430
args=[rfile.resource_set.resource.id, rfile.id])
432
def make_empty_resource_file(self, rtype=None, content=None):
433
# Create a largefile to use the generated content,
434
# sha256, and total_size.
436
content = factory.make_bytes(1024)
437
total_size = len(content)
438
largefile = factory.make_LargeFile(content=content, size=total_size)
439
sha256 = largefile.sha256
440
with largefile.content.open('rb') as stream:
441
content = stream.read()
445
largeobject = LargeObjectFile()
446
largeobject.open().close()
447
largefile = LargeFile.objects.create(
448
sha256=sha256, total_size=total_size, content=largeobject)
451
rtype = BOOT_RESOURCE_TYPE.UPLOADED
452
resource = factory.make_BootResource(rtype=rtype)
453
resource_set = factory.make_BootResourceSet(resource)
454
rfile = factory.make_BootResourceFile(resource_set, largefile)
455
return rfile, content
457
def read_content(self, rfile):
458
"""Return the content saved in resource file."""
459
with rfile.largefile.content.open('rb') as stream:
462
def test_handler_path(self):
464
'/api/1.0/boot-resources/3/upload/5/',
465
reverse('boot_resource_file_upload_handler', args=['3', '5']))
467
def test_PUT_resource_file_writes_content(self):
469
rfile, content = self.make_empty_resource_file()
470
response = self.client.put(
471
self.get_boot_resource_file_upload_uri(rfile), data=content)
472
self.assertEqual(httplib.OK, response.status_code, response.content)
473
self.assertEqual(content, self.read_content(rfile))
475
def test_PUT_requires_admin(self):
476
rfile, content = self.make_empty_resource_file()
477
response = self.client.put(
478
self.get_boot_resource_file_upload_uri(rfile), data=content)
480
httplib.FORBIDDEN, response.status_code, response.content)
482
def test_PUT_returns_bad_request_when_no_content(self):
484
rfile, _ = self.make_empty_resource_file()
485
response = self.client.put(
486
self.get_boot_resource_file_upload_uri(rfile))
488
httplib.BAD_REQUEST, response.status_code, response.content)
490
def test_PUT_returns_forbidden_when_resource_is_synced(self):
492
rfile, content = self.make_empty_resource_file(
493
BOOT_RESOURCE_TYPE.SYNCED)
494
response = self.client.put(
495
self.get_boot_resource_file_upload_uri(rfile), data=content)
497
httplib.FORBIDDEN, response.status_code, response.content)
499
def test_PUT_returns_bad_request_when_resource_file_is_complete(self):
501
rfile, content = self.make_empty_resource_file()
502
with rfile.largefile.content.open('wb') as stream:
503
stream.write(content)
505
response = self.client.put(
506
self.get_boot_resource_file_upload_uri(rfile), data=content)
508
httplib.BAD_REQUEST, response.status_code, response.content)
510
def test_PUT_returns_bad_request_when_content_is_too_large(self):
512
rfile, content = self.make_empty_resource_file()
513
content = factory.make_bytes(len(content) + 1)
514
response = self.client.put(
515
self.get_boot_resource_file_upload_uri(rfile), data=content)
517
httplib.BAD_REQUEST, response.status_code, response.content)
519
def test_PUT_returns_bad_request_when_content_doesnt_match_sha256(self):
521
rfile, content = self.make_empty_resource_file()
522
content = factory.make_bytes(size=len(content))
523
response = self.client.put(
524
self.get_boot_resource_file_upload_uri(rfile), data=content)
526
httplib.BAD_REQUEST, response.status_code, response.content)
528
def test_PUT_on_complete_calls_clusters_to_import_boot_images(self):
530
nodegroup = MagicMock()
531
self.patch(boot_resources, 'NodeGroup', nodegroup)
533
rfile, content = self.make_empty_resource_file()
534
response = self.client.put(
535
self.get_boot_resource_file_upload_uri(rfile), data=content)
537
httplib.OK, response.status_code, response.content)
539
nodegroup.objects.import_boot_images_on_accepted_clusters,
540
MockCalledOnceWith())
542
def test_PUT_with_multiple_requests_and_large_content(self):
545
# Get large amount of data to test with
546
content = factory.make_bytes(1 << 24) # 16MB
547
rfile, _ = self.make_empty_resource_file(content=content)
549
content[i:i + (1 << 22)]
550
for i in range(0, len(content), 1 << 22) # Loop a total of 4 times
553
for send_content in split_content:
554
response = self.client.put(
555
self.get_boot_resource_file_upload_uri(rfile),
558
httplib.OK, response.status_code, response.content)
559
self.assertEqual(content, self.read_content(rfile))