Merge lp:~blake-rouse/maas/backward-compatible-boot-source-api into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2792
Proposed branch: lp:~blake-rouse/maas/backward-compatible-boot-source-api
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 672 lines (+553/-1)
6 files modified
src/maasserver/api/boot_source_selections.py (+62/-0)
src/maasserver/api/boot_sources.py (+60/-0)
src/maasserver/api/doc.py (+4/-1)
src/maasserver/api/tests/test_boot_source_selections.py (+207/-0)
src/maasserver/api/tests/test_boot_sources.py (+196/-0)
src/maasserver/urls_api.py (+24/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/backward-compatible-boot-source-api
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+231499@code.launchpad.net

Commit message

Add backward compatible API for boot source on nodegroups.

Description of the change

Access to the boot sources using the api/1.0/nodegroups/uuid/boot-sources was removed as boot sources are now global. This is an issue as it breaks backwards compatibility.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Excellent stuff. My eyes did glaze over a bit towards the end, but sometimes there's just no helping that with API tests.

I try always to find something to consider changing, just to prove that I'm really reviewing the branch, so here goes:

.

The docstring for BootSourcesBackwardHandler.read explains the uuid parameter, but maybe it should just say that it's a cluster uuid that is now ignored?

.

It looks like test_GET_returns_same_boot_source_for_different_node_groups and the two instances of test_GET_returns_same_list_for_different_node_groups each test the same thing 3 times. Does doing it more than once really contribute anything?

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Thanks for the review.

.

Fixed the docstring.

.

It just to make very sure!!! :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/boot_source_selections.py'
2--- src/maasserver/api/boot_source_selections.py 2014-08-17 01:07:57 +0000
3+++ src/maasserver/api/boot_source_selections.py 2014-08-22 15:30:43 +0000
4@@ -94,6 +94,38 @@
5 return ('boot_source_selection_handler', (boot_source_id, id))
6
7
8+class BootSourceSelectionBackwardHandler(BootSourceSelectionHandler):
9+ """Manage a boot source selection.
10+
11+ It used to be that boot-sources could be set per cluster. Now it can only
12+ be set globally for the whole region and clusters. This api is now
13+ deprecated, and only exists for backwards compatibility.
14+ """
15+ deprecated = True
16+
17+ def read(self, request, uuid, boot_source_id, id):
18+ """Read a boot source selection."""
19+ return super(BootSourceSelectionBackwardHandler, self).read(
20+ request, boot_source_id, id)
21+
22+ def update(self, request, uuid, boot_source_id, id):
23+ """Update a specific boot source selection.
24+
25+ :param release: The release for which to import resources.
26+ :param arches: The list of architectures for which to import resources.
27+ :param subarches: The list of subarchitectures for which to import
28+ resources.
29+ :param labels: The list of labels for which to import resources.
30+ """
31+ return super(BootSourceSelectionBackwardHandler, self).update(
32+ request, boot_source_id, id)
33+
34+ def delete(self, request, uuid, boot_source_id, id):
35+ """Delete a specific boot source."""
36+ return super(BootSourceSelectionBackwardHandler, self).delete(
37+ request, boot_source_id, id)
38+
39+
40 class BootSourceSelectionsHandler(OperationsHandler):
41 """Manage the collection of boot source selections."""
42 api_doc_section_name = "Boot source selections"
43@@ -134,3 +166,33 @@
44 return form.save()
45 else:
46 raise ValidationError(form.errors)
47+
48+
49+class BootSourceSelectionsBackwardHandler(BootSourceSelectionsHandler):
50+ """Manage a boot source selection.
51+
52+ It used to be that boot-sources could be set per cluster. Now it can only
53+ be set globally for the whole region and clusters. This api is now
54+ deprecated, and only exists for backwards compatibility.
55+ """
56+ deprecated = True
57+
58+ def read(self, request, uuid, boot_source_id):
59+ """List boot source selections.
60+
61+ Get a listing of a boot source's selections.
62+ """
63+ return super(BootSourceSelectionsBackwardHandler, self).read(
64+ request, boot_source_id)
65+
66+ def create(self, request, uuid, boot_source_id):
67+ """Create a new boot source selection.
68+
69+ :param release: The release for which to import resources.
70+ :param arches: The architecture list for which to import resources.
71+ :param subarches: The subarchitecture list for which to import
72+ resources.
73+ :param labels: The label lists for which to import resources.
74+ """
75+ return super(BootSourceSelectionsBackwardHandler, self).create(
76+ request, boot_source_id)
77
78=== modified file 'src/maasserver/api/boot_sources.py'
79--- src/maasserver/api/boot_sources.py 2014-08-16 15:43:01 +0000
80+++ src/maasserver/api/boot_sources.py 2014-08-22 15:30:43 +0000
81@@ -112,6 +112,35 @@
82 return ('boot_source_handler', (id, ))
83
84
85+class BootSourceBackwardHandler(BootSourceHandler):
86+ """Manage a boot source.
87+
88+ It used to be that boot-sources could be set per cluster. Now it can only
89+ be set globally for the whole region and clusters. This api is now
90+ deprecated, and only exists for backwards compatibility.
91+ """
92+ deprecated = True
93+
94+ def read(self, request, uuid, id):
95+ """Read a boot source."""
96+ return super(BootSourceBackwardHandler, self).read(request, id)
97+
98+ def update(self, request, uuid, id):
99+ """Update a specific boot source.
100+
101+ :param url: The URL of the BootSource.
102+ :param keyring_filename: The path to the keyring file for this
103+ BootSource.
104+ :param keyring_filename: The GPG keyring for this BootSource,
105+ base64-encoded data.
106+ """
107+ return super(BootSourceBackwardHandler, self).update(request, id)
108+
109+ def delete(self, request, uuid, id):
110+ """Delete a specific boot source."""
111+ return super(BootSourceBackwardHandler, self).delete(request, id)
112+
113+
114 class BootSourcesHandler(OperationsHandler):
115 """Manage the collection of boot sources."""
116 api_doc_section_name = "Boot sources"
117@@ -148,3 +177,34 @@
118 status=httplib.CREATED)
119 else:
120 raise ValidationError(form.errors)
121+
122+
123+class BootSourcesBackwardHandler(BootSourcesHandler):
124+ """Manage the collection of boot sources.
125+
126+ It used to be that boot-sources could be set per cluster. Now it can only
127+ be set globally for the whole region and clusters. This api is now
128+ deprecated, and only exists for backwards compatibility.
129+ """
130+ deprecated = True
131+
132+ def read(self, request, uuid):
133+ """List boot sources.
134+
135+ Get a listing of a cluster's boot sources.
136+
137+ :param uuid: This is deprecated, only exists for backwards
138+ compatibility. Boot sources are now global for all of MAAS.
139+ """
140+ return super(BootSourcesBackwardHandler, self).read(request)
141+
142+ def create(self, request, uuid):
143+ """Create a new boot source.
144+
145+ :param url: The URL of the BootSource.
146+ :param keyring_filename: The path to the keyring file for
147+ this BootSource.
148+ :param keyring_data: The GPG keyring for this BootSource,
149+ base64-encoded.
150+ """
151+ return super(BootSourcesBackwardHandler, self).create(request)
152
153=== modified file 'src/maasserver/api/doc.py'
154--- src/maasserver/api/doc.py 2014-08-16 05:43:33 +0000
155+++ src/maasserver/api/doc.py 2014-08-22 15:30:43 +0000
156@@ -47,13 +47,16 @@
157 """
158 p_has_resource_uri = lambda resource: (
159 getattr(resource.handler, "resource_uri", None) is not None)
160+ p_is_not_deprecated = lambda resource: (
161+ getattr(resource.handler, "deprecated", False))
162 for pattern in resolver.url_patterns:
163 if isinstance(pattern, RegexURLResolver):
164 accumulate_api_resources(pattern, accumulator)
165 elif isinstance(pattern, RegexURLPattern):
166 if isinstance(pattern.callback, Resource):
167 resource = pattern.callback
168- if p_has_resource_uri(resource):
169+ if p_has_resource_uri(resource) and \
170+ not p_is_not_deprecated(resource):
171 accumulator.add(resource)
172 else:
173 raise AssertionError(
174
175=== modified file 'src/maasserver/api/tests/test_boot_source_selections.py'
176--- src/maasserver/api/tests/test_boot_source_selections.py 2014-08-17 01:16:55 +0000
177+++ src/maasserver/api/tests/test_boot_source_selections.py 2014-08-22 15:30:43 +0000
178@@ -41,6 +41,22 @@
179 )
180
181
182+def get_boot_source_selection_backward_uri(
183+ boot_source_selection, nodegroup=None):
184+ """Return a boot source's URI on the API."""
185+ if nodegroup is None:
186+ nodegroup = factory.make_node_group()
187+ boot_source = boot_source_selection.boot_source
188+ return reverse(
189+ 'boot_source_selection_backward_handler',
190+ args=[
191+ nodegroup.uuid,
192+ boot_source.id,
193+ boot_source_selection.id,
194+ ]
195+ )
196+
197+
198 class TestBootSourceSelectionAPI(APITestCase):
199
200 def test_handler_path(self):
201@@ -125,6 +141,107 @@
202 self.assertEqual(httplib.FORBIDDEN, response.status_code)
203
204
205+class TestBootSourceSelectionBackwardAPI(APITestCase):
206+
207+ def test_handler_path(self):
208+ self.assertEqual(
209+ '/api/1.0/nodegroups/uuid/boot-sources/3/selections/4/',
210+ reverse(
211+ 'boot_source_selection_backward_handler',
212+ args=['uuid', '3', '4']))
213+
214+ def test_GET_returns_boot_source(self):
215+ self.become_admin()
216+ boot_source_selection = factory.make_boot_source_selection()
217+ response = self.client.get(
218+ get_boot_source_selection_backward_uri(boot_source_selection))
219+ self.assertEqual(httplib.OK, response.status_code)
220+ returned_boot_source_selection = json.loads(response.content)
221+ boot_source = boot_source_selection.boot_source
222+ # The returned object contains a 'resource_uri' field.
223+ self.assertEqual(
224+ reverse(
225+ 'boot_source_selection_handler',
226+ args=[
227+ boot_source.id,
228+ boot_source_selection.id]
229+ ),
230+ returned_boot_source_selection['resource_uri'])
231+ # The other fields are the boot source selection's fields.
232+ del returned_boot_source_selection['resource_uri']
233+ # All the fields are present.
234+ self.assertItemsEqual(
235+ DISPLAYED_BOOTSOURCESELECTION_FIELDS,
236+ returned_boot_source_selection.keys())
237+ self.assertThat(
238+ boot_source_selection,
239+ MatchesStructure.byEquality(**returned_boot_source_selection))
240+
241+ def test_GET_returns_same_boot_source_for_different_node_groups(self):
242+ self.become_admin()
243+ boot_source_selection = factory.make_boot_source_selection()
244+ for _ in range(3):
245+ nodegroup = factory.make_node_group()
246+ response = self.client.get(
247+ get_boot_source_selection_backward_uri(
248+ boot_source_selection, nodegroup))
249+ self.assertEqual(httplib.OK, response.status_code)
250+ returned_boot_source_selection = json.loads(response.content)
251+ del returned_boot_source_selection['resource_uri']
252+ self.assertThat(
253+ boot_source_selection,
254+ MatchesStructure.byEquality(**returned_boot_source_selection))
255+
256+ def test_GET_requires_admin(self):
257+ boot_source_selection = factory.make_boot_source_selection()
258+ response = self.client.get(
259+ get_boot_source_selection_backward_uri(boot_source_selection))
260+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
261+
262+ def test_DELETE_deletes_boot_source_selection(self):
263+ self.become_admin()
264+ boot_source_selection = factory.make_boot_source_selection()
265+ response = self.client.delete(
266+ get_boot_source_selection_backward_uri(boot_source_selection))
267+ self.assertEqual(httplib.NO_CONTENT, response.status_code)
268+ self.assertIsNone(reload_object(boot_source_selection))
269+
270+ def test_DELETE_requires_admin(self):
271+ boot_source_selection = factory.make_boot_source_selection()
272+ response = self.client.delete(
273+ get_boot_source_selection_backward_uri(boot_source_selection))
274+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
275+
276+ def test_PUT_updates_boot_source_selection(self):
277+ self.become_admin()
278+ boot_source_selection = factory.make_boot_source_selection()
279+ ubuntu_os = UbuntuOS()
280+ new_release = factory.pick_release(ubuntu_os)
281+ new_values = {
282+ 'release': new_release,
283+ 'arches': [factory.make_name('arch'), factory.make_name('arch')],
284+ 'subarches': [
285+ factory.make_name('subarch'), factory.make_name('subarch')],
286+ 'labels': [factory.make_name('label')],
287+ }
288+ response = self.client_put(
289+ get_boot_source_selection_backward_uri(
290+ boot_source_selection), new_values)
291+ self.assertEqual(httplib.OK, response.status_code)
292+ boot_source_selection = reload_object(boot_source_selection)
293+ self.assertAttributes(boot_source_selection, new_values)
294+
295+ def test_PUT_requires_admin(self):
296+ boot_source_selection = factory.make_boot_source_selection()
297+ new_values = {
298+ 'release': factory.make_name('release'),
299+ }
300+ response = self.client_put(
301+ get_boot_source_selection_backward_uri(
302+ boot_source_selection), new_values)
303+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
304+
305+
306 class TestBootSourceSelectionsAPI(APITestCase):
307 """Test the the boot source selections API."""
308
309@@ -198,3 +315,93 @@
310 'boot_source_selections_handler',
311 args=[boot_source.id]), params)
312 self.assertEqual(httplib.FORBIDDEN, response.status_code)
313+
314+
315+class TestBootSourceSelectionsBackwardAPI(APITestCase):
316+ """Test the the boot source selections API."""
317+
318+ def get_uri(self, boot_source, nodegroup=None):
319+ if nodegroup is None:
320+ nodegroup = factory.make_node_group()
321+ return reverse(
322+ 'boot_source_selections_backward_handler',
323+ args=[nodegroup.uuid, boot_source.id])
324+
325+ def test_handler_path(self):
326+ self.assertEqual(
327+ '/api/1.0/nodegroups/uuid/boot-sources/3/selections/',
328+ reverse(
329+ 'boot_source_selections_backward_handler',
330+ args=['uuid', '3']))
331+
332+ def test_GET_returns_boot_source_selection_list(self):
333+ self.become_admin()
334+ boot_source = factory.make_boot_source()
335+ selections = [
336+ factory.make_boot_source_selection(boot_source=boot_source)
337+ for _ in range(3)]
338+ # Create boot source selections in another boot source.
339+ [factory.make_boot_source_selection() for _ in range(3)]
340+ response = self.client.get(self.get_uri(boot_source))
341+ self.assertEqual(httplib.OK, response.status_code, response.content)
342+ parsed_result = json.loads(response.content)
343+ self.assertItemsEqual(
344+ [selection.id for selection in selections],
345+ [selection.get('id') for selection in parsed_result])
346+
347+ def test_GET_returns_same_list_for_different_node_groups(self):
348+ self.become_admin()
349+ boot_source = factory.make_boot_source()
350+ selections = [
351+ factory.make_boot_source_selection(boot_source=boot_source)
352+ for _ in range(3)]
353+ # Create boot source selections in another boot source.
354+ [factory.make_boot_source_selection() for _ in range(3)]
355+ for _ in range(3):
356+ nodegroup = factory.make_node_group()
357+ response = self.client.get(self.get_uri(boot_source, nodegroup))
358+ self.assertEqual(
359+ httplib.OK, response.status_code, response.content)
360+ parsed_result = json.loads(response.content)
361+ self.assertItemsEqual(
362+ [selection.id for selection in selections],
363+ [selection.get('id') for selection in parsed_result])
364+
365+ def test_GET_requires_admin(self):
366+ boot_source = factory.make_boot_source()
367+ response = self.client.get(self.get_uri(boot_source))
368+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
369+
370+ def test_POST_creates_boot_source_selection(self):
371+ self.become_admin()
372+ boot_source = factory.make_boot_source()
373+ ubuntu_os = UbuntuOS()
374+ new_release = factory.pick_release(ubuntu_os)
375+ params = {
376+ 'release': new_release,
377+ 'arches': [factory.make_name('arch'), factory.make_name('arch')],
378+ 'subarches': [
379+ factory.make_name('subarch'), factory.make_name('subarch')],
380+ 'labels': [factory.make_name('label')],
381+ }
382+ response = self.client.post(self.get_uri(boot_source), params)
383+ self.assertEqual(httplib.OK, response.status_code)
384+ parsed_result = json.loads(response.content)
385+
386+ boot_source_selection = BootSourceSelection.objects.get(
387+ id=parsed_result['id'])
388+ self.assertAttributes(boot_source_selection, params)
389+
390+ def test_POST_requires_admin(self):
391+ boot_source = factory.make_boot_source()
392+ ubuntu_os = UbuntuOS()
393+ new_release = factory.pick_release(ubuntu_os)
394+ params = {
395+ 'release': new_release,
396+ 'arches': [factory.make_name('arch'), factory.make_name('arch')],
397+ 'subarches': [
398+ factory.make_name('subarch'), factory.make_name('subarch')],
399+ 'labels': [factory.make_name('label')],
400+ }
401+ response = self.client.post(self.get_uri(boot_source), params)
402+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
403
404=== modified file 'src/maasserver/api/tests/test_boot_sources.py'
405--- src/maasserver/api/tests/test_boot_sources.py 2014-08-16 15:43:01 +0000
406+++ src/maasserver/api/tests/test_boot_sources.py 2014-08-22 15:30:43 +0000
407@@ -34,6 +34,14 @@
408 args=[boot_source.id])
409
410
411+def get_boot_source_backward_uri(boot_source, nodegroup=None):
412+ if nodegroup is None:
413+ nodegroup = factory.make_node_group()
414+ return reverse(
415+ 'boot_source_backward_handler',
416+ args=[nodegroup.uuid, boot_source.id])
417+
418+
419 class TestBootSourceAPI(APITestCase):
420
421 def test_handler_path(self):
422@@ -104,6 +112,92 @@
423 self.assertEqual(httplib.FORBIDDEN, response.status_code)
424
425
426+class TestBootSourceBackwardAPI(APITestCase):
427+
428+ def test_handler_path(self):
429+ self.assertEqual(
430+ '/api/1.0/nodegroups/uuid/boot-sources/3/',
431+ reverse('boot_source_backward_handler', args=['uuid', '3']))
432+
433+ def test_GET_returns_boot_source(self):
434+ self.become_admin()
435+ boot_source = factory.make_boot_source()
436+ response = self.client.get(get_boot_source_backward_uri(boot_source))
437+ self.assertEqual(httplib.OK, response.status_code)
438+ returned_boot_source = json.loads(response.content)
439+ # The returned object contains a 'resource_uri' field.
440+ self.assertEqual(
441+ reverse(
442+ 'boot_source_handler',
443+ args=[boot_source.id]
444+ ),
445+ returned_boot_source['resource_uri'])
446+ # The other fields are the boot source's fields.
447+ del returned_boot_source['resource_uri']
448+ # All the fields are present.
449+ self.assertItemsEqual(
450+ DISPLAYED_BOOTSOURCE_FIELDS, returned_boot_source.keys())
451+ self.assertThat(
452+ boot_source,
453+ MatchesStructure.byEquality(**returned_boot_source))
454+
455+ def test_GET_returns_same_boot_source_no_matter_the_nodegroup(self):
456+ self.become_admin()
457+ boot_source = factory.make_boot_source()
458+ for _ in range(3):
459+ nodegroup = factory.make_node_group()
460+ response = self.client.get(
461+ get_boot_source_backward_uri(boot_source, nodegroup))
462+ self.assertEqual(httplib.OK, response.status_code)
463+ returned_boot_source = json.loads(response.content)
464+ del returned_boot_source['resource_uri']
465+ self.assertThat(
466+ boot_source,
467+ MatchesStructure.byEquality(**returned_boot_source))
468+
469+ def test_GET_requires_admin(self):
470+ boot_source = factory.make_boot_source()
471+ response = self.client.get(get_boot_source_backward_uri(boot_source))
472+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
473+
474+ def test_DELETE_deletes_boot_source(self):
475+ self.become_admin()
476+ boot_source = factory.make_boot_source()
477+ response = self.client.delete(
478+ get_boot_source_backward_uri(boot_source))
479+ self.assertEqual(httplib.NO_CONTENT, response.status_code)
480+ self.assertIsNone(reload_object(boot_source))
481+
482+ def test_DELETE_requires_admin(self):
483+ boot_source = factory.make_boot_source()
484+ response = self.client.delete(
485+ get_boot_source_backward_uri(boot_source))
486+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
487+
488+ def test_PUT_updates_boot_source(self):
489+ self.become_admin()
490+ boot_source = factory.make_boot_source()
491+ new_values = {
492+ 'url': 'http://example.com/',
493+ 'keyring_filename': factory.make_name('filename'),
494+ }
495+ response = self.client_put(
496+ get_boot_source_backward_uri(boot_source), new_values)
497+ self.assertEqual(httplib.OK, response.status_code)
498+ boot_source = reload_object(boot_source)
499+ self.assertAttributes(boot_source, new_values)
500+
501+ def test_PUT_requires_admin(self):
502+ boot_source = factory.make_boot_source()
503+ new_values = {
504+ 'url': 'http://example.com/',
505+ 'keyring_filename': factory.make_name('filename'),
506+ }
507+ response = self.client_put(
508+ get_boot_source_backward_uri(boot_source), new_values)
509+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
510+
511+
512 class TestBootSourcesAPI(APITestCase):
513 """Test the the boot source API."""
514
515@@ -190,3 +284,105 @@
516 response = self.client.post(
517 reverse('boot_sources_handler'), params)
518 self.assertEqual(httplib.FORBIDDEN, response.status_code)
519+
520+
521+class TestBootSourcesBackwardAPI(APITestCase):
522+ """Test the the boot source API."""
523+
524+ def get_uri(self, nodegroup=None):
525+ if nodegroup is None:
526+ nodegroup = factory.make_node_group()
527+ return reverse(
528+ 'boot_sources_backward_handler', args=[nodegroup.uuid])
529+
530+ def test_handler_path(self):
531+ self.assertEqual(
532+ '/api/1.0/nodegroups/uuid/boot-sources/',
533+ reverse('boot_sources_backward_handler', args=['uuid']))
534+
535+ def test_GET_returns_boot_source_list(self):
536+ self.become_admin()
537+ sources = [
538+ factory.make_boot_source() for _ in range(3)]
539+ response = self.client.get(self.get_uri())
540+ self.assertEqual(httplib.OK, response.status_code, response.content)
541+ parsed_result = json.loads(response.content)
542+ self.assertItemsEqual(
543+ [boot_source.id for boot_source in sources],
544+ [boot_source.get('id') for boot_source in parsed_result])
545+
546+ def test_GET_returns_same_list_for_different_node_groups(self):
547+ self.become_admin()
548+ sources = [
549+ factory.make_boot_source() for _ in range(3)]
550+ for _ in range(3):
551+ nodegroup = factory.make_node_group()
552+ response = self.client.get(self.get_uri(nodegroup))
553+ self.assertEqual(
554+ httplib.OK, response.status_code, response.content)
555+ parsed_result = json.loads(response.content)
556+ self.assertItemsEqual(
557+ [boot_source.id for boot_source in sources],
558+ [boot_source.get('id') for boot_source in parsed_result])
559+
560+ def test_GET_requires_admin(self):
561+ response = self.client.get(self.get_uri())
562+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
563+
564+ def test_POST_creates_boot_source_with_keyring_filename(self):
565+ self.become_admin()
566+
567+ params = {
568+ 'url': 'http://example.com/',
569+ 'keyring_filename': factory.make_name('filename'),
570+ 'keyring_data': '',
571+ }
572+ response = self.client.post(self.get_uri(), params)
573+ self.assertEqual(httplib.CREATED, response.status_code)
574+ parsed_result = json.loads(response.content)
575+
576+ boot_source = BootSource.objects.get(id=parsed_result['id'])
577+ # boot_source.keyring_data is returned as a read-only buffer, test
578+ # it separately from the rest of the attributes.
579+ self.assertEqual('', bytes(boot_source.keyring_data))
580+ del params['keyring_data']
581+ self.assertAttributes(boot_source, params)
582+
583+ def test_POST_creates_boot_source_with_keyring_data(self):
584+ self.become_admin()
585+
586+ params = {
587+ 'url': 'http://example.com/',
588+ 'keyring_filename': '',
589+ 'keyring_data': (
590+ factory.make_file_upload(content=sample_binary_data)),
591+ }
592+ response = self.client.post(self.get_uri(), params)
593+ self.assertEqual(httplib.CREATED, response.status_code)
594+ parsed_result = json.loads(response.content)
595+
596+ boot_source = BootSource.objects.get(id=parsed_result['id'])
597+ # boot_source.keyring_data is returned as a read-only buffer, test
598+ # it separately from the rest of the attributes.
599+ self.assertEqual(sample_binary_data, bytes(boot_source.keyring_data))
600+ del params['keyring_data']
601+ self.assertAttributes(boot_source, params)
602+
603+ def test_POST_validates_boot_source(self):
604+ self.become_admin()
605+
606+ params = {
607+ 'url': 'http://example.com/',
608+ }
609+ response = self.client.post(self.get_uri(), params)
610+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
611+
612+ def test_POST_requires_admin(self):
613+ params = {
614+ 'url': 'http://example.com/',
615+ 'keyring_filename': '',
616+ 'keyring_data': (
617+ factory.make_file_upload(content=sample_binary_data)),
618+ }
619+ response = self.client.post(self.get_uri(), params)
620+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
621
622=== modified file 'src/maasserver/urls_api.py'
623--- src/maasserver/urls_api.py 2014-08-21 19:20:31 +0000
624+++ src/maasserver/urls_api.py 2014-08-22 15:30:43 +0000
625@@ -29,11 +29,15 @@
626 BootResourcesHandler,
627 )
628 from maasserver.api.boot_source_selections import (
629+ BootSourceSelectionBackwardHandler,
630 BootSourceSelectionHandler,
631+ BootSourceSelectionsBackwardHandler,
632 BootSourceSelectionsHandler,
633 )
634 from maasserver.api.boot_sources import (
635+ BootSourceBackwardHandler,
636 BootSourceHandler,
637+ BootSourcesBackwardHandler,
638 BootSourcesHandler,
639 )
640 from maasserver.api.commissioning_scripts import (
641@@ -160,6 +164,14 @@
642 BootSourceSelectionHandler, authentication=api_auth)
643 boot_source_selections_handler = AdminRestrictedResource(
644 BootSourceSelectionsHandler, authentication=api_auth)
645+boot_source_backward_handler = AdminRestrictedResource(
646+ BootSourceBackwardHandler, authentication=api_auth)
647+boot_sources_backward_handler = AdminRestrictedResource(
648+ BootSourcesBackwardHandler, authentication=api_auth)
649+boot_source_selection_backward_handler = AdminRestrictedResource(
650+ BootSourceSelectionBackwardHandler, authentication=api_auth)
651+boot_source_selections_backward_handler = AdminRestrictedResource(
652+ BootSourceSelectionsBackwardHandler, authentication=api_auth)
653 license_key_handler = AdminRestrictedResource(
654 LicenseKeyHandler, authentication=api_auth)
655 license_keys_handler = AdminRestrictedResource(
656@@ -267,4 +279,16 @@
657 url(r'^boot-sources/(?P<boot_source_id>[^/]+)/selections/(?P<id>[^/]+)/$',
658 boot_source_selection_handler,
659 name='boot_source_selection_handler'),
660+ url(r'^nodegroups/(?P<uuid>[^/]+)/boot-sources/$',
661+ boot_sources_backward_handler, name='boot_sources_backward_handler'),
662+ url(r'^nodegroups/(?P<uuid>[^/]+)/boot-sources/(?P<id>[^/]+)/$',
663+ boot_source_backward_handler, name='boot_source_backward_handler'),
664+ url(r'^nodegroups/(?P<uuid>[^/]+)/boot-sources/(?P<boot_source_id>[^/]+)/'
665+ 'selections/$',
666+ boot_source_selections_backward_handler,
667+ name='boot_source_selections_backward_handler'),
668+ url(r'^nodegroups/(?P<uuid>[^/]+)/boot-sources/(?P<boot_source_id>[^/]+)/'
669+ 'selections/(?P<id>[^/]+)/$',
670+ boot_source_selection_backward_handler,
671+ name='boot_source_selection_backward_handler'),
672 )