Merge lp:~allenap/maas/pxe-intel-arch-config into lp:maas/trunk

Proposed by Gavin Panella on 2012-08-23
Status: Merged
Approved by: Gavin Panella on 2012-08-24
Approved revision: 900
Merged at revision: 924
Proposed branch: lp:~allenap/maas/pxe-intel-arch-config
Merge into: lp:maas/trunk
Diff against target: 477 lines (+53/-311)
4 files modified
src/maasserver/api.py (+30/-17)
src/maasserver/kernel.py (+0/-63)
src/maasserver/tests/test_api.py (+23/-55)
src/maasserver/tests/test_kernel.py (+0/-176)
To merge this branch: bzr merge lp:~allenap/maas/pxe-intel-arch-config
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) 2012-08-23 Approve on 2012-08-24
Review via email: mp+121039@code.launchpad.net

Commit Message

The pxeconfig API view now returns a representation of a KernelParameters instance.

Description of the Change

Create a KernelParameters object, and serialize it, in pxeconfig, eliminating the need for the transitional "kernel" module.

To post a comment you must log in.
896. By Gavin Panella on 2012-08-23

Merged trunk into pxe-intel-arch-config.

Jeroen T. Vermeulen (jtv) wrote :

Why is it OK to force enlistment to use an i386 initrd? At the very least this is worth a comment.

Also, the way Django's Manager.get() pushes you towards using exceptions as non-exceptional control flow is really getting to me. In this case it's clearly meant to be a normal “if,” but the layer below checks for empty result and raises an exception just so you can then catch it again. Worth a wrapper similar to Storm's result.one() maybe? I think I'll just hack one up so that we can solve this once and for all.

review: Needs Information
Jeroen T. Vermeulen (jtv) wrote :

Quick convenience ref: to use that “get zero or one item” helper:

from maasserver.utils.orm import get_one

model = get_one(ModelClass.objects.filter(field=value))

Jeroen

897. By Gavin Panella on 2012-08-24

Comment on default to i386.

898. By Gavin Panella on 2012-08-24

Merged trunk into pxe-intel-arch-config.

899. By Gavin Panella on 2012-08-24

Use get_one().

Gavin Panella (allenap) wrote :

> Why is it OK to force enlistment to use an i386 initrd? At the very least
> this is worth a comment.

It's a lowest common denominator (for now, until ARM comes). I've
added some comments to explain.

> Also, the way Django's Manager.get() pushes you towards using exceptions as
> non-exceptional control flow is really getting to me. In this case it's
> clearly meant to be a normal “if,” but the layer below checks for empty result
> and raises an exception just so you can then catch it again. Worth a wrapper
> similar to Storm's result.one() maybe? I think I'll just hack one up so that
> we can solve this once and for all.

Tip-top, I've used get_one().

900. By Gavin Panella on 2012-08-24

Merged trunk into pxe-intel-arch-config, resolving conflicts.

Jeroen T. Vermeulen (jtv) wrote :

Thanks for the changes. And, er, sorry for those conflicts... <cough>

review: Approve

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 2012-08-24 10:28:29 +0000
3+++ src/maasserver/api.py 2012-08-24 13:00:26 +0000
4@@ -100,6 +100,7 @@
5 from formencode import validators
6 from formencode.validators import Invalid
7 from maasserver.enum import (
8+ ARCHITECTURE,
9 NODE_PERMISSION,
10 NODE_STATUS,
11 )
12@@ -115,7 +116,6 @@
13 get_node_create_form,
14 get_node_edit_form,
15 )
16-from maasserver.kernel import compose_kernel_command_line
17 from maasserver.models import (
18 Config,
19 DHCPLease,
20@@ -124,6 +124,11 @@
21 Node,
22 NodeGroup,
23 )
24+from maasserver.preseed import (
25+ compose_enlistment_preseed_url,
26+ compose_preseed_url,
27+ )
28+from maasserver.server_address import get_maas_facing_server_address
29 from maasserver.utils.orm import get_one
30 from piston.doc import generate_doc
31 from piston.handler import (
32@@ -134,6 +139,7 @@
33 from piston.models import Token
34 from piston.resource import Resource
35 from piston.utils import rc
36+from provisioningserver.kernel_opts import KernelParameters
37
38
39 dispatch_methods = {
40@@ -1115,31 +1121,38 @@
41 def pxeconfig(request):
42 """Get the PXE configuration given a node's details.
43
44- :param mac: MAC address to produce a boot configuration for. This
45- parameter is optional. If it is not given, the configuration
46- will be the "default" one which boots into an enlistment image.
47- :param arch: Main machine architecture.
48- :param subarch: Sub-architecture, or "generic" if there is none.
49+ Returns a JSON object corresponding to a
50+ :class:`provisioningserver.kernel_opts.KernelParameters` instance.
51+
52+ :param mac: MAC address to produce a boot configuration for.
53 """
54- mac = request.GET.get('mac', None)
55- arch = get_mandatory_param(request.GET, 'arch')
56- subarch = request.GET.get('subarch', 'generic')
57+ mac = get_mandatory_param(request.GET, 'mac')
58
59 macaddress = get_one(MACAddress.objects.filter(mac_address=mac))
60 if macaddress is None:
61+ # Default to i386 as a works-for-all solution. This will not support
62+ # non-x86 architectures, but for now this assumption holds.
63 node = None
64+ arch, subarch = ARCHITECTURE.i386, "generic"
65+ preseed_url = compose_enlistment_preseed_url()
66+ hostname = 'maas-enlist'
67 else:
68 node = macaddress.node
69+ arch, subarch = node.architecture, "generic"
70+ preseed_url = compose_preseed_url(node)
71+ hostname = node.hostname
72
73+ # XXX JeroenVermeulen 2012-08-06 bug=1013146: Stop hard-coding this.
74+ release = 'precise'
75 purpose = get_boot_purpose(node)
76- append = compose_kernel_command_line(node, arch, subarch, purpose=purpose)
77-
78- # XXX: allenap 2012-07-31 bug=1013146: 'precise' is hardcoded here.
79- release = "precise"
80-
81- params = dict(
82+ domain = 'local.lan' # TODO: This is probably not enough!
83+ server_address = get_maas_facing_server_address()
84+
85+ params = KernelParameters(
86 arch=arch, subarch=subarch, release=release, purpose=purpose,
87- append=append)
88+ hostname=hostname, domain=domain, preseed_url=preseed_url,
89+ log_host=server_address, fs_host=server_address)
90
91 return HttpResponse(
92- json.dumps(params), content_type="application/json")
93+ json.dumps(params._asdict()),
94+ content_type="application/json")
95
96=== removed file 'src/maasserver/kernel.py'
97--- src/maasserver/kernel.py 2012-08-22 20:29:50 +0000
98+++ src/maasserver/kernel.py 1970-01-01 00:00:00 +0000
99@@ -1,63 +0,0 @@
100-# Copyright 2012 Canonical Ltd. This software is licensed under the
101-# GNU Affero General Public License version 3 (see the file LICENSE).
102-
103-"""Generate kernel command-line for inclusion in PXE configs.
104-
105-This is a TRANSITIONAL module. This functionality will move to the pxeconfig()
106-view in the most likely scenario.
107-"""
108-
109-from __future__ import (
110- absolute_import,
111- print_function,
112- unicode_literals,
113- )
114-
115-__metaclass__ = type
116-__all__ = [
117- 'compose_kernel_command_line',
118- ]
119-
120-from maasserver.preseed import (
121- compose_enlistment_preseed_url,
122- compose_preseed_url,
123- )
124-from maasserver.server_address import get_maas_facing_server_address
125-from provisioningserver.kernel_opts import (
126- compose_kernel_command_line_new,
127- KernelParameters,
128- )
129-
130-
131-def compose_kernel_command_line(node, arch, subarch, purpose):
132- """Generate a line of kernel options for booting `node`.
133-
134- Include these options in the PXE config file's APPEND argument.
135-
136- The node may be None, in which case it will boot into enlistment.
137- """
138- # XXX JeroenVermeulen 2012-08-06 bug=1013146: Stop hard-coding this.
139- release = 'precise'
140-
141- # TODO: This is probably not enough!
142- domain = 'local.lan'
143-
144- if node is None:
145- preseed_url = compose_enlistment_preseed_url()
146- else:
147- preseed_url = compose_preseed_url(node)
148-
149- if node is None:
150- # Not a known host; still needs enlisting. Make up a name.
151- hostname = 'maas-enlist'
152- else:
153- hostname = node.hostname
154-
155- server_address = get_maas_facing_server_address()
156-
157- params = KernelParameters(
158- arch=arch, subarch=subarch, release=release, purpose=purpose,
159- hostname=hostname, domain=domain, preseed_url=preseed_url,
160- log_host=server_address, fs_host=server_address)
161-
162- return compose_kernel_command_line_new(params)
163
164=== modified file 'src/maasserver/tests/test_api.py'
165--- src/maasserver/tests/test_api.py 2012-08-24 10:28:29 +0000
166+++ src/maasserver/tests/test_api.py 2012-08-24 13:00:26 +0000
167@@ -42,6 +42,7 @@
168 get_overrided_query_dict,
169 )
170 from maasserver.enum import (
171+ ARCHITECTURE,
172 ARCHITECTURE_CHOICES,
173 NODE_AFTER_COMMISSIONING_ACTION,
174 NODE_STATUS,
175@@ -94,6 +95,7 @@
176 POWER_TYPE,
177 POWER_TYPE_CHOICES,
178 )
179+from provisioningserver.kernel_opts import KernelParameters
180 from provisioningserver.omshell import Omshell
181 from testresources import FixtureResource
182 from testtools.matchers import (
183@@ -2245,14 +2247,10 @@
184 class TestPXEConfigAPI(AnonAPITestCase):
185
186 def get_params(self):
187- return {
188- 'arch': "armhf",
189- 'subarch': "armadaxp",
190- 'mac': factory.make_mac_address().mac_address,
191- }
192+ return {'mac': factory.make_mac_address().mac_address}
193
194 def get_optional_params(self):
195- return ['subarch', 'mac']
196+ return ['mac']
197
198 def get_pxeconfig(self, params=None):
199 """Make a request to `pxeconfig`, and return its response dict."""
200@@ -2279,37 +2277,17 @@
201 )),
202 response)
203
204- def test_pxeconfig_returns_complete_config(self):
205+ def test_pxeconfig_returns_all_kernel_parameters(self):
206 self.assertThat(
207 self.get_pxeconfig(),
208- ContainsAll([
209- 'arch',
210- 'subarch',
211- 'append',
212- 'release',
213- 'purpose',
214- ]))
215-
216- def test_pxeconfig_copies_some_parameters_from_request(self):
217- params_in = self.get_params()
218- params_out = self.get_pxeconfig(params_in)
219- copied_params = ['arch', 'subarch']
220- self.assertEqual(
221- {name: params_in[name] for name in copied_params},
222- {name: params_out[name] for name in copied_params})
223-
224- def test_pxeconfig_adds_some_parameters(self):
225- params_in = self.get_params()
226- added_params = {'append', 'release', 'purpose'}
227- for param in added_params:
228- if param in params_in:
229- del params_in[param]
230- params_out = self.get_pxeconfig(params_in)
231- self.assertEqual(
232- set(params_out).difference(params_in), added_params)
233- # The release is always "precise".
234- self.assertEqual('precise', params_out['release'])
235- self.assertThat(params_out['append'], Contains("auto url=http://"))
236+ ContainsAll(KernelParameters._fields))
237+
238+ def test_pxeconfig_defaults_to_i386_when_node_unknown(self):
239+ # As a lowest-common-denominator, i386 is chosen when the node is not
240+ # yet known to MAAS.
241+ params_out = self.get_pxeconfig()
242+ self.assertEqual(ARCHITECTURE.i386, params_out["arch"])
243+ self.assertEqual("generic", params_out["subarch"])
244
245 def get_without_param(self, param):
246 """Request a `pxeconfig()` response, but omit `param` from request."""
247@@ -2324,39 +2302,29 @@
248 kernel_opts, 'get_ephemeral_name',
249 FakeMethod(result=factory.getRandomString()))
250
251- def test_pxeconfig_handles_missing_parameters_appropriately(self):
252- # Some parameters are optional, others are mandatory. The
253- # absence of a mandatory parameter always results in a BAD
254- # REQUEST response.
255+ def test_pxeconfig_requires_mac_address(self):
256+ # The `mac` parameter is mandatory.
257 self.silence_get_ephemeral_name()
258- expected_response_to_missing_parameter = {
259- 'arch': httplib.BAD_REQUEST,
260- 'subarch': httplib.OK,
261- 'mac': httplib.OK,
262- }
263- observed_response = {
264- param: self.get_without_param(param).status_code
265- for param in self.get_params()
266- }
267 self.assertEqual(
268- expected_response_to_missing_parameter, observed_response)
269+ httplib.BAD_REQUEST,
270+ self.get_without_param("mac").status_code)
271
272- def test_pxeconfig_appends_enlistment_preseed_url_for_unknown_node(self):
273+ def test_pxeconfig_has_enlistment_preseed_url_for_unknown_node(self):
274 self.silence_get_ephemeral_name()
275 params = self.get_params()
276 params['mac'] = factory.getRandomMACAddress()
277 response = self.client.get(reverse('pxeconfig'), params)
278- self.assertIn(
279+ self.assertEqual(
280 compose_enlistment_preseed_url(),
281- json.loads(response.content)["append"])
282+ json.loads(response.content)["preseed_url"])
283
284- def test_pxeconfig_appends_preseed_url_for_known_node(self):
285+ def test_pxeconfig_has_preseed_url_for_known_node(self):
286 params = self.get_params()
287 node = MACAddress.objects.get(mac_address=params['mac']).node
288 response = self.client.get(reverse('pxeconfig'), params)
289- self.assertIn(
290+ self.assertEqual(
291 compose_preseed_url(node),
292- json.loads(response.content)["append"])
293+ json.loads(response.content)["preseed_url"])
294
295 def test_get_boot_purpose_unknown_node(self):
296 # A node that's not yet known to MAAS is assumed to be enlisting,
297
298=== removed file 'src/maasserver/tests/test_kernel.py'
299--- src/maasserver/tests/test_kernel.py 2012-08-22 20:53:31 +0000
300+++ src/maasserver/tests/test_kernel.py 1970-01-01 00:00:00 +0000
301@@ -1,176 +0,0 @@
302-# Copyright 2012 Canonical Ltd. This software is licensed under the
303-# GNU Affero General Public License version 3 (see the file LICENSE).
304-
305-"""Test composition of kernel command lines."""
306-
307-from __future__ import (
308- absolute_import,
309- print_function,
310- unicode_literals,
311- )
312-
313-__metaclass__ = type
314-__all__ = []
315-
316-import os
317-
318-from django.conf import settings
319-from maasserver.api import get_boot_purpose
320-from maasserver.kernel import compose_kernel_command_line
321-from maasserver.preseed import (
322- compose_enlistment_preseed_url,
323- compose_preseed_url,
324- )
325-from maasserver.server_address import get_maas_facing_server_address
326-from maasserver.testing.factory import factory
327-from maasserver.testing.testcase import TestCase
328-from maastesting.matchers import ContainsAll
329-from provisioningserver.kernel_opts import (
330- EphemeralImagesDirectoryNotFound,
331- ISCSI_TARGET_NAME_PREFIX,
332- )
333-from provisioningserver.pxe.tftppath import compose_image_path
334-from provisioningserver.testing.config import ConfigFixture
335-
336-
337-class TestComposeKernelCommandLine(TestCase):
338-
339- def test_compose_kernel_command_line_accepts_None_for_unknown_node(self):
340- self.assertIn(
341- 'suite=precise',
342- compose_kernel_command_line(
343- None, factory.make_name('arch'), factory.make_name('subarch'),
344- purpose=factory.make_name('purpose')))
345-
346- def test_compose_kernel_command_line_includes_preseed_url(self):
347- node = factory.make_node()
348- self.assertIn(
349- "auto url=%s" % compose_preseed_url(node),
350- compose_kernel_command_line(
351- node, node.architecture, 'generic',
352- purpose=factory.make_name('purpose')))
353-
354- def test_compose_kernel_command_line_includes_enlistment_preseed_url(self):
355- self.assertIn(
356- "auto url=%s" % compose_enlistment_preseed_url(),
357- compose_kernel_command_line(
358- None, factory.make_name("arch"), 'generic',
359- purpose=factory.make_name('purpose')))
360-
361- def test_compose_kernel_command_line_includes_initrd(self):
362- node = factory.make_node()
363- initrd_path = compose_image_path(
364- node.architecture, 'generic', 'precise',
365- purpose=get_boot_purpose(node))
366- self.assertIn(
367- "initrd=%s" % initrd_path,
368- compose_kernel_command_line(
369- node, node.architecture, 'generic',
370- purpose=get_boot_purpose(node)))
371-
372- def test_compose_kernel_command_line_includes_suite(self):
373- # At the moment, the OS release we use is hard-coded to "precise."
374- node = factory.make_node()
375- suite = "precise"
376- self.assertIn(
377- "suite=%s" % suite,
378- compose_kernel_command_line(
379- node, node.architecture, 'generic',
380- purpose=factory.make_name('purpose')))
381-
382- def test_compose_kernel_command_line_includes_hostname_and_domain(self):
383- node = factory.make_node()
384- # Cobbler seems to hard-code domain to "local.lan"; we may want
385- # to change it, and update this test.
386- domain = "local.lan"
387- self.assertThat(
388- compose_kernel_command_line(
389- node, node.architecture, 'generic',
390- purpose=factory.make_name('purpose')),
391- ContainsAll([
392- "hostname=%s" % node.hostname,
393- "domain=%s" % domain,
394- ]))
395-
396- def test_compose_kernel_command_line_makes_up_hostname_for_new_node(self):
397- dummy_hostname = 'maas-enlist'
398- self.assertIn(
399- "hostname=%s" % dummy_hostname,
400- compose_kernel_command_line(
401- None, factory.make_name('arch'),
402- factory.make_name('subarch'),
403- purpose=factory.make_name('purpose')))
404-
405- def test_compose_kernel_command_line_includes_locale(self):
406- node = factory.make_node()
407- locale = "en_US"
408- self.assertIn(
409- "locale=%s" % locale,
410- compose_kernel_command_line(
411- node, node.architecture, 'generic',
412- purpose=factory.make_name('purpose')))
413-
414- def test_compose_kernel_command_line_includes_log_settings(self):
415- node = factory.make_node()
416- log_host = factory.getRandomIPAddress()
417- self.patch(settings, 'DEFAULT_MAAS_URL', 'http://%s/' % log_host)
418- # Port 514 (UDP) is syslog.
419- log_port = "514"
420- text_priority = "critical"
421- self.assertThat(
422- compose_kernel_command_line(
423- node, node.architecture, 'generic',
424- purpose=factory.make_name('purpose')),
425- ContainsAll([
426- "log_host=%s" % log_host,
427- "log_port=%s" % log_port,
428- "text priority=%s" % text_priority,
429- ]))
430-
431- def create_ephemeral_info(self, name, arch, release):
432- """Create a pseudo-real ephemeral info file."""
433- epheneral_info = """
434- release=%s
435- stream=ephemeral
436- label=release
437- serial=20120424
438- arch=%s
439- name=%s
440- """ % (release, arch, name)
441- ephemeral_root = self.make_dir()
442- config = {"boot": {"ephemeral": {"directory": ephemeral_root}}}
443- self.useFixture(ConfigFixture(config))
444- ephemeral_dir = os.path.join(
445- ephemeral_root, release, 'ephemeral', arch, release)
446- os.makedirs(ephemeral_dir)
447- factory.make_file(
448- ephemeral_dir, name='info', contents=epheneral_info)
449-
450- def test_compose_kernel_command_line_inc_purpose_opts_comm_node(self):
451- # The result of compose_kernel_command_line includes the purpose
452- # options for a "commissioning" node.
453- ephemeral_name = factory.make_name("ephemeral")
454- arch = factory.make_name('arch')
455- self.create_ephemeral_info(ephemeral_name, arch, "precise")
456- node = factory.make_node()
457- self.assertThat(
458- compose_kernel_command_line(
459- node, arch,
460- factory.make_name('subarch'),
461- purpose="commissioning"),
462- ContainsAll([
463- "iscsi_target_name=%s:%s" % (
464- ISCSI_TARGET_NAME_PREFIX, ephemeral_name),
465- "iscsi_target_port=3260",
466- "iscsi_target_ip=%s" % get_maas_facing_server_address(),
467- ]))
468-
469- def test_compose_kernel_command_line_reports_error_about_missing_dir(self):
470- missing_dir = factory.make_name('missing-dir')
471- config = {"boot": {"ephemeral": {"directory": missing_dir}}}
472- self.useFixture(ConfigFixture(config))
473- node = factory.make_node()
474- self.assertRaises(
475- EphemeralImagesDirectoryNotFound,
476- compose_kernel_command_line, node, factory.make_name('arch'),
477- factory.make_name('subarch'), purpose="commissioning")