Merge lp:~rvb/maas/bootimages-api into lp:~maas-committers/maas/trunk
- bootimages-api
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Raphaël Badin |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2149 |
Proposed branch: | lp:~rvb/maas/bootimages-api |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
662 lines (+339/-178) 6 files modified
src/maasserver/api.py (+57/-6) src/maasserver/tests/test_api.py (+1/-168) src/maasserver/tests/test_api_boot_images.py (+266/-0) src/maasserver/urls_api.py (+7/-1) src/provisioningserver/boot_images.py (+2/-2) src/provisioningserver/tests/test_boot_images.py (+6/-1) |
To merge this branch: | bzr merge lp:~rvb/maas/bootimages-api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Julian Edwards (community) | Approve | ||
Review via email: mp+211730@code.launchpad.net |
Commit message
Add API endpoint to list the boot images. Add API endpoint to get the details of a boot image.
Boot images are essential components for a MAAS installation. Being able to track these boot images using the API is useful to know what type of machines a MAAS installation can manage.
Description of the change
I had to refactor the existing endpoint to make the nodegroup UUID part of the URL but I think this modification to the API is okay considering that the method in question, report_
The corresponding UI change (display the list of available boot images on a cluster's page) will follow.
The diff is bigger than it should be because I moved the existing tests from src/maasserver/
Raphaël Badin (rvb) wrote : | # |
> 79 + :param uuid: The UUID of the cluster for which the images are
> 80 + should be listed.
>
> "for which the images are should be listed". Not sure what language this is
> :)
>
> "for which the images should be listed" perhaps?
Right, fixed.
>
> 409 + reverse(
> 410 + 'boot_image_
> 411 + args=[boot_
> 412 + ),
>
> You already tested this part in test_handler_path, no?
No, here I test that the returned object has the right 'resource_uri' field.
> Also please consider using MatchesStructure here (byEquality is also a useful
> helper).
Okay, this means two tests (because 'resource_uri' is not part of the original object) but that's probably still more readable:
# The returned object contains a 'resource_uri' field.
),
# The other fields are the boot image's fields.
del returned_
> Everything else looks marvellous. The change to the api url is pretty nice.
Thanks for the review!
Preview Diff
1 | === modified file 'src/maasserver/api.py' |
2 | --- src/maasserver/api.py 2014-03-19 15:26:59 +0000 |
3 | +++ src/maasserver/api.py 2014-03-20 09:00:00 +0000 |
4 | @@ -61,6 +61,7 @@ |
5 | "AnonNodesHandler", |
6 | "api_doc", |
7 | "api_doc_title", |
8 | + "BootImageHandler", |
9 | "BootImagesHandler", |
10 | "CommissioningScriptHandler", |
11 | "CommissioningScriptsHandler", |
12 | @@ -242,8 +243,8 @@ |
13 | from piston.emitters import JSONEmitter |
14 | from piston.handler import typemapper |
15 | from piston.utils import rc |
16 | +from provisioningserver.kernel_opts import KernelParameters |
17 | from provisioningserver.power_schema import UNKNOWN_POWER_TYPE |
18 | -from provisioningserver.kernel_opts import KernelParameters |
19 | import simplejson as json |
20 | |
21 | # Node's fields exposed on the API. |
22 | @@ -2541,26 +2542,76 @@ |
23 | db_images.delete() |
24 | |
25 | |
26 | +DISPLAYED_BOOTIMAGE_FIELDS = ( |
27 | + 'id', |
28 | + 'release', |
29 | + 'architecture', |
30 | + 'subarchitecture', |
31 | + 'purpose', |
32 | + 'label', |
33 | +) |
34 | + |
35 | + |
36 | +class BootImageHandler(OperationsHandler): |
37 | + """API for boot images.""" |
38 | + create = replace = update = delete = None |
39 | + |
40 | + model = BootImage |
41 | + fields = DISPLAYED_BOOTIMAGE_FIELDS |
42 | + |
43 | + def read(self, request, uuid, id): |
44 | + """Read a boot image.""" |
45 | + nodegroup = get_object_or_404(NodeGroup, uuid=uuid) |
46 | + return get_object_or_404( |
47 | + BootImage, nodegroup=nodegroup, id=id) |
48 | + |
49 | + @classmethod |
50 | + def resource_uri(cls, bootimage=None): |
51 | + if bootimage is None: |
52 | + id = 'id' |
53 | + uuid = 'uuid' |
54 | + else: |
55 | + id = bootimage.id |
56 | + uuid = bootimage.nodegroup.uuid |
57 | + return ('boot_image_handler', (uuid, id)) |
58 | + |
59 | + |
60 | class BootImagesHandler(OperationsHandler): |
61 | |
62 | create = replace = update = delete = None |
63 | |
64 | @classmethod |
65 | - def resource_uri(cls): |
66 | - return ('boot_images_handler', []) |
67 | + def resource_uri(cls, nodegroup=None): |
68 | + if nodegroup is None: |
69 | + uuid = 'uuid' |
70 | + else: |
71 | + uuid = nodegroup.uuid |
72 | + return ('boot_images_handler', [uuid]) |
73 | + |
74 | + def read(self, request, uuid): |
75 | + """List boot images. |
76 | + |
77 | + Get a listing of a cluster's boot images. |
78 | + |
79 | + :param uuid: The UUID of the cluster for which the images |
80 | + should be listed. |
81 | + """ |
82 | + nodegroup = get_object_or_404(NodeGroup, uuid=uuid) |
83 | + return BootImage.objects.filter(nodegroup=nodegroup) |
84 | |
85 | @operation(idempotent=False) |
86 | - def report_boot_images(self, request): |
87 | + def report_boot_images(self, request, uuid): |
88 | """Report images available to net-boot nodes from. |
89 | |
90 | + :param uuid: The UUID of the cluster for which the images are |
91 | + being reported. |
92 | :param images: A list of dicts, each describing a boot image with |
93 | these properties: `architecture`, `subarchitecture`, `release`, |
94 | `purpose`, and optionally, `label` (which defaults to "release"). |
95 | These should match the code that determines TFTP paths for these |
96 | images. |
97 | """ |
98 | - nodegroup_uuid = get_mandatory_param(request.data, "nodegroup") |
99 | - nodegroup = get_object_or_404(NodeGroup, uuid=nodegroup_uuid) |
100 | + nodegroup = get_object_or_404(NodeGroup, uuid=uuid) |
101 | check_nodegroup_access(request, nodegroup) |
102 | reported_images = summarise_reported_images(request) |
103 | existing_images = summarise_stored_images(nodegroup) |
104 | |
105 | === modified file 'src/maasserver/tests/test_api.py' |
106 | --- src/maasserver/tests/test_api.py 2014-03-12 07:33:55 +0000 |
107 | +++ src/maasserver/tests/test_api.py 2014-03-20 09:00:00 +0000 |
108 | @@ -19,16 +19,11 @@ |
109 | from itertools import izip |
110 | import json |
111 | |
112 | -from apiclient.maas_client import MAASClient |
113 | -from django.conf import settings |
114 | from django.core.urlresolvers import reverse |
115 | -from fixtures import EnvironmentVariableFixture |
116 | from maasserver import api |
117 | from maasserver.api import ( |
118 | DISPLAYED_NODEGROUPINTERFACE_FIELDS, |
119 | store_node_power_parameters, |
120 | - summarise_boot_image_dict, |
121 | - summarise_boot_image_object, |
122 | warn_if_missing_boot_images, |
123 | ) |
124 | from maasserver.enum import ( |
125 | @@ -39,14 +34,12 @@ |
126 | from maasserver.exceptions import MAASAPIBadRequest |
127 | from maasserver.forms_settings import INVALID_SETTING_MSG_TEMPLATE |
128 | from maasserver.models import ( |
129 | - BootImage, |
130 | Config, |
131 | NodeGroup, |
132 | NodeGroupInterface, |
133 | SSHKey, |
134 | ) |
135 | from maasserver.models.user import get_auth_tokens |
136 | -from maasserver.refresh_worker import refresh_worker |
137 | from maasserver.testing import ( |
138 | get_data, |
139 | reload_object, |
140 | @@ -61,20 +54,9 @@ |
141 | from maasserver.testing.testcase import MAASServerTestCase |
142 | from maasserver.tests.test_forms import make_interface_settings |
143 | from maasserver.utils.orm import get_one |
144 | -from maastesting.celery import CeleryFixture |
145 | from maastesting.djangotestcase import TransactionTestCase |
146 | from maastesting.matchers import MockCalledOnceWith |
147 | -from mock import ( |
148 | - ANY, |
149 | - Mock, |
150 | - ) |
151 | -from provisioningserver import ( |
152 | - boot_images, |
153 | - tasks, |
154 | - ) |
155 | -from provisioningserver.pxe import tftppath |
156 | -from provisioningserver.testing.boot_images import make_boot_image_params |
157 | -from testresources import FixtureResource |
158 | +from mock import Mock |
159 | from testtools.matchers import ( |
160 | Contains, |
161 | Equals, |
162 | @@ -760,155 +742,6 @@ |
163 | self.assertEqual(None, reload_object(interface).foreign_dhcp_ip) |
164 | |
165 | |
166 | -class TestBootImagesAPI(APITestCase): |
167 | - |
168 | - resources = ( |
169 | - ('celery', FixtureResource(CeleryFixture())), |
170 | - ) |
171 | - |
172 | - def report_images(self, nodegroup, images, client=None): |
173 | - if client is None: |
174 | - client = self.client |
175 | - return client.post( |
176 | - reverse('boot_images_handler'), { |
177 | - 'images': json.dumps(images), |
178 | - 'nodegroup': nodegroup.uuid, |
179 | - 'op': 'report_boot_images', |
180 | - }) |
181 | - |
182 | - def test_summarise_boot_image_object_returns_tuple(self): |
183 | - image = factory.make_boot_image() |
184 | - self.assertEqual( |
185 | - ( |
186 | - image.architecture, |
187 | - image.subarchitecture, |
188 | - image.release, |
189 | - image.label, |
190 | - image.purpose, |
191 | - ), |
192 | - summarise_boot_image_object(image)) |
193 | - |
194 | - def test_summarise_boot_image_dict_returns_tuple(self): |
195 | - image = make_boot_image_params() |
196 | - self.assertEqual( |
197 | - ( |
198 | - image['architecture'], |
199 | - image['subarchitecture'], |
200 | - image['release'], |
201 | - image['label'], |
202 | - image['purpose'], |
203 | - ), |
204 | - summarise_boot_image_dict(image)) |
205 | - |
206 | - def test_summarise_boot_image_dict_substitutes_defaults(self): |
207 | - image = make_boot_image_params() |
208 | - del image['subarchitecture'] |
209 | - del image['label'] |
210 | - _, subarchitecture, _, label, _ = summarise_boot_image_dict(image) |
211 | - self.assertEqual(('generic', 'release'), (subarchitecture, label)) |
212 | - |
213 | - def test_summarise_boot_image_functions_are_compatible(self): |
214 | - image_dict = make_boot_image_params() |
215 | - image_obj = factory.make_boot_image( |
216 | - architecture=image_dict['architecture'], |
217 | - subarchitecture=image_dict['subarchitecture'], |
218 | - release=image_dict['release'], label=image_dict['label'], |
219 | - purpose=image_dict['purpose']) |
220 | - self.assertEqual( |
221 | - summarise_boot_image_dict(image_dict), |
222 | - summarise_boot_image_object(image_obj)) |
223 | - |
224 | - def test_report_boot_images_does_not_work_for_normal_user(self): |
225 | - nodegroup = NodeGroup.objects.ensure_master() |
226 | - log_in_as_normal_user(self.client) |
227 | - response = self.report_images(nodegroup, []) |
228 | - self.assertEqual( |
229 | - httplib.FORBIDDEN, response.status_code, response.content) |
230 | - |
231 | - def test_report_boot_images_works_for_master_worker(self): |
232 | - nodegroup = NodeGroup.objects.ensure_master() |
233 | - client = make_worker_client(nodegroup) |
234 | - response = self.report_images(nodegroup, [], client=client) |
235 | - self.assertEqual(httplib.OK, response.status_code) |
236 | - |
237 | - def test_report_boot_images_stores_images(self): |
238 | - nodegroup = NodeGroup.objects.ensure_master() |
239 | - image = make_boot_image_params() |
240 | - client = make_worker_client(nodegroup) |
241 | - response = self.report_images(nodegroup, [image], client=client) |
242 | - self.assertEqual( |
243 | - (httplib.OK, "OK"), |
244 | - (response.status_code, response.content)) |
245 | - self.assertTrue( |
246 | - BootImage.objects.have_image(nodegroup=nodegroup, **image)) |
247 | - |
248 | - def test_report_boot_images_removes_unreported_images(self): |
249 | - deleted_image = factory.make_boot_image() |
250 | - nodegroup = deleted_image.nodegroup |
251 | - client = make_worker_client(nodegroup) |
252 | - response = self.report_images(nodegroup, [], client=client) |
253 | - self.assertEqual(httplib.OK, response.status_code) |
254 | - self.assertIsNone(reload_object(deleted_image)) |
255 | - |
256 | - def test_report_boot_images_keeps_known_images(self): |
257 | - nodegroup = factory.make_node_group() |
258 | - image = make_boot_image_params() |
259 | - client = make_worker_client(nodegroup) |
260 | - response = self.report_images(nodegroup, [image], client=client) |
261 | - self.assertEqual(httplib.OK, response.status_code) |
262 | - known_image = BootImage.objects.get(nodegroup=nodegroup) |
263 | - response = self.report_images(nodegroup, [image], client=client) |
264 | - self.assertEqual(httplib.OK, response.status_code) |
265 | - self.assertEqual(known_image, reload_object(known_image)) |
266 | - |
267 | - def test_report_boot_images_ignores_images_for_other_nodegroups(self): |
268 | - unrelated_image = factory.make_boot_image() |
269 | - deleted_image = factory.make_boot_image() |
270 | - nodegroup = deleted_image.nodegroup |
271 | - client = make_worker_client(nodegroup) |
272 | - response = self.report_images(nodegroup, [], client=client) |
273 | - self.assertEqual(httplib.OK, response.status_code) |
274 | - self.assertIsNotNone(reload_object(unrelated_image)) |
275 | - |
276 | - def test_report_boot_images_ignores_unknown_image_properties(self): |
277 | - nodegroup = NodeGroup.objects.ensure_master() |
278 | - image = make_boot_image_params() |
279 | - image['nonesuch'] = factory.make_name('nonesuch'), |
280 | - client = make_worker_client(nodegroup) |
281 | - response = self.report_images(nodegroup, [image], client=client) |
282 | - self.assertEqual( |
283 | - (httplib.OK, "OK"), |
284 | - (response.status_code, response.content)) |
285 | - |
286 | - def test_report_boot_images_warns_about_missing_boot_images(self): |
287 | - register_error = self.patch(api, 'register_persistent_error') |
288 | - nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED) |
289 | - response = self.report_images( |
290 | - nodegroup, [], client=make_worker_client(nodegroup)) |
291 | - self.assertEqual(httplib.OK, response.status_code) |
292 | - self.assertThat( |
293 | - register_error, |
294 | - MockCalledOnceWith(COMPONENT.IMPORT_PXE_FILES, ANY)) |
295 | - |
296 | - def test_worker_calls_report_boot_images(self): |
297 | - # report_boot_images() uses the report_boot_images op on the nodes |
298 | - # handlers to send image information. |
299 | - self.useFixture( |
300 | - EnvironmentVariableFixture("MAAS_URL", settings.DEFAULT_MAAS_URL)) |
301 | - refresh_worker(NodeGroup.objects.ensure_master()) |
302 | - self.patch(MAASClient, 'post') |
303 | - self.patch(tftppath, 'list_boot_images', Mock(return_value=[])) |
304 | - self.patch(boot_images, "get_cluster_uuid") |
305 | - |
306 | - tasks.report_boot_images.delay() |
307 | - |
308 | - # We're not concerned about the payloads (images and nodegroup) here; |
309 | - # those are tested in provisioningserver.tests.test_boot_images. |
310 | - MAASClient.post.assert_called_once_with( |
311 | - reverse('boot_images_handler').lstrip('/'), 'report_boot_images', |
312 | - images=ANY, nodegroup=ANY) |
313 | - |
314 | - |
315 | class TestWarnIfMissingBootImages(MAASServerTestCase): |
316 | """Test `warn_if_missing_boot_images`.""" |
317 | |
318 | |
319 | === added file 'src/maasserver/tests/test_api_boot_images.py' |
320 | --- src/maasserver/tests/test_api_boot_images.py 1970-01-01 00:00:00 +0000 |
321 | +++ src/maasserver/tests/test_api_boot_images.py 2014-03-20 09:00:00 +0000 |
322 | @@ -0,0 +1,266 @@ |
323 | +# Copyright 2013 Canonical Ltd. This software is licensed under the |
324 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
325 | + |
326 | +"""Tests for the `Boot Images` API.""" |
327 | + |
328 | +from __future__ import ( |
329 | + absolute_import, |
330 | + print_function, |
331 | + unicode_literals, |
332 | + ) |
333 | + |
334 | +str = None |
335 | + |
336 | +__metaclass__ = type |
337 | +__all__ = [] |
338 | + |
339 | +import httplib |
340 | +import json |
341 | + |
342 | +from apiclient.maas_client import MAASClient |
343 | +from django.conf import settings |
344 | +from django.core.urlresolvers import reverse |
345 | +from fixtures import EnvironmentVariableFixture |
346 | +from maasserver import api |
347 | +from maasserver.api import ( |
348 | + summarise_boot_image_dict, |
349 | + summarise_boot_image_object, |
350 | + ) |
351 | +from maasserver.enum import ( |
352 | + COMPONENT, |
353 | + NODEGROUP_STATUS, |
354 | + ) |
355 | +from maasserver.models import ( |
356 | + BootImage, |
357 | + NodeGroup, |
358 | + ) |
359 | +from maasserver.refresh_worker import refresh_worker |
360 | +from maasserver.testing import reload_object |
361 | +from maasserver.testing.api import ( |
362 | + APITestCase, |
363 | + log_in_as_normal_user, |
364 | + make_worker_client, |
365 | + ) |
366 | +from maasserver.testing.factory import factory |
367 | +from maastesting.celery import CeleryFixture |
368 | +from maastesting.matchers import MockCalledOnceWith |
369 | +from mock import ( |
370 | + ANY, |
371 | + Mock, |
372 | + ) |
373 | +from provisioningserver import ( |
374 | + boot_images, |
375 | + tasks, |
376 | + ) |
377 | +from provisioningserver.pxe import tftppath |
378 | +from provisioningserver.testing.boot_images import make_boot_image_params |
379 | +from testresources import FixtureResource |
380 | +from testtools.matchers import MatchesStructure |
381 | + |
382 | + |
383 | +def get_boot_image_uri(boot_image): |
384 | + """Return a boot image's URI on the API.""" |
385 | + return reverse( |
386 | + 'boot_image_handler', |
387 | + args=[boot_image.nodegroup.uuid, boot_image.id]) |
388 | + |
389 | + |
390 | +class TestBootImageAPI(APITestCase): |
391 | + |
392 | + def test_handler_path(self): |
393 | + self.assertEqual( |
394 | + '/api/1.0/nodegroups/uuid/boot-images/3/', |
395 | + reverse('boot_image_handler', args=['uuid', '3'])) |
396 | + |
397 | + def test_GET_returns_boot_image(self): |
398 | + boot_image = factory.make_boot_image() |
399 | + response = self.client.get(get_boot_image_uri(boot_image)) |
400 | + self.assertEqual(httplib.OK, response.status_code) |
401 | + returned_boot_image = json.loads(response.content) |
402 | + # The returned object contains a 'resource_uri' field. |
403 | + self.assertEqual( |
404 | + reverse( |
405 | + 'boot_image_handler', |
406 | + args=[boot_image.nodegroup.uuid, boot_image.id] |
407 | + ), |
408 | + returned_boot_image['resource_uri']) |
409 | + # The other fields are the boot image's fields. |
410 | + del returned_boot_image['resource_uri'] |
411 | + self.assertThat( |
412 | + boot_image, |
413 | + MatchesStructure.byEquality(**returned_boot_image)) |
414 | + |
415 | + |
416 | +class TestBootImagesAPI(APITestCase): |
417 | + """Test the the boot images API.""" |
418 | + |
419 | + def test_handler_path(self): |
420 | + self.assertEqual( |
421 | + '/api/1.0/nodegroups/uuid/boot-images/', |
422 | + reverse('boot_images_handler', args=['uuid'])) |
423 | + |
424 | + def test_GET_returns_boot_image_list(self): |
425 | + nodegroup = factory.make_node_group() |
426 | + images = [ |
427 | + factory.make_boot_image(nodegroup=nodegroup) for _ in range(3)] |
428 | + # Create images in another nodegroup. |
429 | + [factory.make_boot_image() for _ in range(3)] |
430 | + response = self.client.get( |
431 | + reverse('boot_images_handler', args=[nodegroup.uuid])) |
432 | + self.assertEqual(httplib.OK, response.status_code, response.content) |
433 | + parsed_result = json.loads(response.content) |
434 | + self.assertItemsEqual( |
435 | + [boot_image.id for boot_image in images], |
436 | + [boot_image.get('id') for boot_image in parsed_result]) |
437 | + |
438 | + |
439 | +class TestBootImagesReportImagesAPI(APITestCase): |
440 | + """Test the method report_boot_images from the boot images API.""" |
441 | + |
442 | + resources = ( |
443 | + ('celery', FixtureResource(CeleryFixture())), |
444 | + ) |
445 | + |
446 | + def report_images(self, nodegroup, images, client=None): |
447 | + if client is None: |
448 | + client = self.client |
449 | + return client.post( |
450 | + reverse('boot_images_handler', args=[nodegroup.uuid]), { |
451 | + 'images': json.dumps(images), |
452 | + 'op': 'report_boot_images', |
453 | + }) |
454 | + |
455 | + def test_summarise_boot_image_object_returns_tuple(self): |
456 | + image = factory.make_boot_image() |
457 | + self.assertEqual( |
458 | + ( |
459 | + image.architecture, |
460 | + image.subarchitecture, |
461 | + image.release, |
462 | + image.label, |
463 | + image.purpose, |
464 | + ), |
465 | + summarise_boot_image_object(image)) |
466 | + |
467 | + def test_summarise_boot_image_dict_returns_tuple(self): |
468 | + image = make_boot_image_params() |
469 | + self.assertEqual( |
470 | + ( |
471 | + image['architecture'], |
472 | + image['subarchitecture'], |
473 | + image['release'], |
474 | + image['label'], |
475 | + image['purpose'], |
476 | + ), |
477 | + summarise_boot_image_dict(image)) |
478 | + |
479 | + def test_summarise_boot_image_dict_substitutes_defaults(self): |
480 | + image = make_boot_image_params() |
481 | + del image['subarchitecture'] |
482 | + del image['label'] |
483 | + _, subarchitecture, _, label, _ = summarise_boot_image_dict(image) |
484 | + self.assertEqual(('generic', 'release'), (subarchitecture, label)) |
485 | + |
486 | + def test_summarise_boot_image_functions_are_compatible(self): |
487 | + image_dict = make_boot_image_params() |
488 | + image_obj = factory.make_boot_image( |
489 | + architecture=image_dict['architecture'], |
490 | + subarchitecture=image_dict['subarchitecture'], |
491 | + release=image_dict['release'], label=image_dict['label'], |
492 | + purpose=image_dict['purpose']) |
493 | + self.assertEqual( |
494 | + summarise_boot_image_dict(image_dict), |
495 | + summarise_boot_image_object(image_obj)) |
496 | + |
497 | + def test_report_boot_images_does_not_work_for_normal_user(self): |
498 | + nodegroup = NodeGroup.objects.ensure_master() |
499 | + log_in_as_normal_user(self.client) |
500 | + response = self.report_images(nodegroup, []) |
501 | + self.assertEqual( |
502 | + httplib.FORBIDDEN, response.status_code, response.content) |
503 | + |
504 | + def test_report_boot_images_works_for_master_worker(self): |
505 | + nodegroup = NodeGroup.objects.ensure_master() |
506 | + client = make_worker_client(nodegroup) |
507 | + response = self.report_images(nodegroup, [], client=client) |
508 | + self.assertEqual(httplib.OK, response.status_code) |
509 | + |
510 | + def test_report_boot_images_stores_images(self): |
511 | + nodegroup = NodeGroup.objects.ensure_master() |
512 | + image = make_boot_image_params() |
513 | + client = make_worker_client(nodegroup) |
514 | + response = self.report_images(nodegroup, [image], client=client) |
515 | + self.assertEqual( |
516 | + (httplib.OK, "OK"), |
517 | + (response.status_code, response.content)) |
518 | + self.assertTrue( |
519 | + BootImage.objects.have_image(nodegroup=nodegroup, **image)) |
520 | + |
521 | + def test_report_boot_images_removes_unreported_images(self): |
522 | + deleted_image = factory.make_boot_image() |
523 | + nodegroup = deleted_image.nodegroup |
524 | + client = make_worker_client(nodegroup) |
525 | + response = self.report_images(nodegroup, [], client=client) |
526 | + self.assertEqual(httplib.OK, response.status_code) |
527 | + self.assertIsNone(reload_object(deleted_image)) |
528 | + |
529 | + def test_report_boot_images_keeps_known_images(self): |
530 | + nodegroup = factory.make_node_group() |
531 | + image = make_boot_image_params() |
532 | + client = make_worker_client(nodegroup) |
533 | + response = self.report_images(nodegroup, [image], client=client) |
534 | + self.assertEqual(httplib.OK, response.status_code) |
535 | + known_image = BootImage.objects.get(nodegroup=nodegroup) |
536 | + response = self.report_images(nodegroup, [image], client=client) |
537 | + self.assertEqual(httplib.OK, response.status_code) |
538 | + self.assertEqual(known_image, reload_object(known_image)) |
539 | + |
540 | + def test_report_boot_images_ignores_images_for_other_nodegroups(self): |
541 | + unrelated_image = factory.make_boot_image() |
542 | + deleted_image = factory.make_boot_image() |
543 | + nodegroup = deleted_image.nodegroup |
544 | + client = make_worker_client(nodegroup) |
545 | + response = self.report_images(nodegroup, [], client=client) |
546 | + self.assertEqual(httplib.OK, response.status_code) |
547 | + self.assertIsNotNone(reload_object(unrelated_image)) |
548 | + |
549 | + def test_report_boot_images_ignores_unknown_image_properties(self): |
550 | + nodegroup = NodeGroup.objects.ensure_master() |
551 | + image = make_boot_image_params() |
552 | + image['nonesuch'] = factory.make_name('nonesuch'), |
553 | + client = make_worker_client(nodegroup) |
554 | + response = self.report_images(nodegroup, [image], client=client) |
555 | + self.assertEqual( |
556 | + (httplib.OK, "OK"), |
557 | + (response.status_code, response.content)) |
558 | + |
559 | + def test_report_boot_images_warns_about_missing_boot_images(self): |
560 | + register_error = self.patch(api, 'register_persistent_error') |
561 | + nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED) |
562 | + response = self.report_images( |
563 | + nodegroup, [], client=make_worker_client(nodegroup)) |
564 | + self.assertEqual(httplib.OK, response.status_code) |
565 | + self.assertThat( |
566 | + register_error, |
567 | + MockCalledOnceWith(COMPONENT.IMPORT_PXE_FILES, ANY)) |
568 | + |
569 | + def test_worker_calls_report_boot_images(self): |
570 | + # report_boot_images() uses the report_boot_images op on the nodes |
571 | + # handlers to send image information. |
572 | + self.useFixture( |
573 | + EnvironmentVariableFixture("MAAS_URL", settings.DEFAULT_MAAS_URL)) |
574 | + refresh_worker(NodeGroup.objects.ensure_master()) |
575 | + self.patch(MAASClient, 'post') |
576 | + self.patch(tftppath, 'list_boot_images', Mock(return_value=[])) |
577 | + nodegroup_uuid = factory.make_name('uuid') |
578 | + get_cluster_uuid = self.patch(boot_images, "get_cluster_uuid") |
579 | + get_cluster_uuid.return_value = nodegroup_uuid |
580 | + |
581 | + tasks.report_boot_images.delay() |
582 | + |
583 | + # We're not concerned about the payload (images) here; |
584 | + # this is tested in provisioningserver.tests.test_boot_images. |
585 | + MAASClient.post.assert_called_once_with( |
586 | + path=reverse( |
587 | + 'boot_images_handler', args=[nodegroup_uuid]).lstrip('/'), |
588 | + op='report_boot_images', images=ANY) |
589 | |
590 | === modified file 'src/maasserver/urls_api.py' |
591 | --- src/maasserver/urls_api.py 2014-02-10 07:52:26 +0000 |
592 | +++ src/maasserver/urls_api.py 2014-03-20 09:00:00 +0000 |
593 | @@ -21,6 +21,7 @@ |
594 | from maasserver.api import ( |
595 | AccountHandler, |
596 | api_doc, |
597 | + BootImageHandler, |
598 | BootImagesHandler, |
599 | CommissioningResultsHandler, |
600 | CommissioningScriptHandler, |
601 | @@ -76,6 +77,8 @@ |
602 | NodeGroupInterfacesHandler, authentication=api_auth) |
603 | boot_images_handler = RestrictedResource( |
604 | BootImagesHandler, authentication=api_auth) |
605 | +boot_image_handler = RestrictedResource( |
606 | + BootImageHandler, authentication=api_auth) |
607 | tag_handler = RestrictedResource(TagHandler, authentication=api_auth) |
608 | tags_handler = RestrictedResource(TagsHandler, authentication=api_auth) |
609 | commissioning_results_handler = RestrictedResource( |
610 | @@ -130,6 +133,10 @@ |
611 | nodegroupinterfaces_handler, name='nodegroupinterfaces_handler'), |
612 | url(r'^nodegroups/(?P<uuid>[^/]+)/interfaces/(?P<interface>[^/]+)/$', |
613 | nodegroupinterface_handler, name='nodegroupinterface_handler'), |
614 | + url(r'^nodegroups/(?P<uuid>[^/]+)/boot-images/$', |
615 | + boot_images_handler, name='boot_images_handler'), |
616 | + url(r'^nodegroups/(?P<uuid>[^/]+)/boot-images/(?P<id>[^/]+)/$', |
617 | + boot_image_handler, name='boot_image_handler'), |
618 | url( |
619 | r'^networks/(?P<name>[\w\-]+)/$', |
620 | network_handler, name='network_handler'), |
621 | @@ -141,7 +148,6 @@ |
622 | r'^account/prefs/sshkeys/(?P<keyid>[^/]+)/$', sshkey_handler, |
623 | name='sshkey_handler'), |
624 | url(r'^account/prefs/sshkeys/$', sshkeys_handler, name='sshkeys_handler'), |
625 | - url(r'^boot-images/$', boot_images_handler, name='boot_images_handler'), |
626 | url(r'^tags/(?P<name>[\w\-]+)/$', tag_handler, name='tag_handler'), |
627 | url(r'^tags/$', tags_handler, name='tags_handler'), |
628 | url( |
629 | |
630 | === modified file 'src/provisioningserver/boot_images.py' |
631 | --- src/provisioningserver/boot_images.py 2013-10-07 09:12:40 +0000 |
632 | +++ src/provisioningserver/boot_images.py 2014-03-20 09:00:00 +0000 |
633 | @@ -57,9 +57,9 @@ |
634 | |
635 | def submit(maas_url, api_credentials, images): |
636 | """Submit images to server.""" |
637 | + path = 'api/1.0/nodegroups/%s/boot-images/' % get_cluster_uuid() |
638 | MAASClient(MAASOAuth(*api_credentials), MAASDispatcher(), maas_url).post( |
639 | - 'api/1.0/boot-images/', 'report_boot_images', |
640 | - nodegroup=get_cluster_uuid(), images=json.dumps(images)) |
641 | + path=path, op='report_boot_images', images=json.dumps(images)) |
642 | |
643 | |
644 | def report_to_server(): |
645 | |
646 | === modified file 'src/provisioningserver/tests/test_boot_images.py' |
647 | --- src/provisioningserver/tests/test_boot_images.py 2013-10-07 09:12:40 +0000 |
648 | +++ src/provisioningserver/tests/test_boot_images.py 2014-03-20 09:00:00 +0000 |
649 | @@ -44,7 +44,12 @@ |
650 | self.patch(MAASClient, 'post') |
651 | boot_images.report_to_server() |
652 | args, kwargs = MAASClient.post.call_args |
653 | - self.assertIs(sentinel.uuid, kwargs["nodegroup"]) |
654 | + self.assertEqual( |
655 | + ( |
656 | + 'api/1.0/nodegroups/%s/boot-images/' % sentinel.uuid, |
657 | + 'report_boot_images', |
658 | + ), |
659 | + (kwargs["path"], kwargs["op"])) |
660 | self.assertItemsEqual([image], json.loads(kwargs['images'])) |
661 | |
662 | def test_does_nothing_without_credentials(self): |
79 + :param uuid: The UUID of the cluster for which the images are
80 + should be listed.
"for which the images are should be listed". Not sure what language this is :)
"for which the images should be listed" perhaps?
409 + reverse( handler' , image.nodegroup .uuid, boot_image.id]
410 + 'boot_image_
411 + args=[boot_
412 + ),
You already tested this part in test_handler_path, no?
Also please consider using MatchesStructure here (byEquality is also a useful helper).
Everything else looks marvellous. The change to the api url is pretty nice.