Merge lp:~rvb/maas/bootimages-api into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
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
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_boot_images(), was a purely internal method.

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/tests/test_api.py to src/maasserver/tests/test_api_boot_images.py.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) 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?

409 + reverse(
410 + 'boot_image_handler',
411 + args=[boot_image.nodegroup.uuid, boot_image.id]
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.

review: Approve
Revision history for this message
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_handler',
> 411 + args=[boot_image.nodegroup.uuid, boot_image.id]
> 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.
        self.assertEqual(
            reverse(
                    'boot_image_handler',
                    args=[boot_image.nodegroup.uuid, boot_image.id]
            ),
            returned_boot_image['resource_uri'])
        # The other fields are the boot image's fields.
        del returned_boot_image['resource_uri']
        self.assertThat(
            boot_image,
            MatchesStructure.byEquality(**returned_boot_image))

> Everything else looks marvellous. The change to the api url is pretty nice.

Thanks for the review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):