Merge lp:~allenap/maas/kernel-initrd-parameters into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 808
Proposed branch: lp:~allenap/maas/kernel-initrd-parameters
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 334 lines (+124/-54)
6 files modified
src/maasserver/api.py (+51/-24)
src/maasserver/tests/test_api.py (+36/-8)
src/provisioningserver/pxe/config.py (+9/-4)
src/provisioningserver/pxe/tests/test_config.py (+26/-6)
src/provisioningserver/tests/test_tftp.py (+2/-10)
src/provisioningserver/tftp.py (+0/-2)
To merge this branch: bzr merge lp:~allenap/maas/kernel-initrd-parameters
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+117432@code.launchpad.net

Commit message

Calculate the PXE options KERNEL and INITRD in the pxeconfig view.

Description of the change

Calculate the kernel and initrd options in the pxeconfig view, instead of relying on them being passed from the dynamic TFTP server. The latter does not have all the necessary information at its disposal.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good. Just a few remarks but nothing major.

[0]

221 + title=title, kernel="{}/kernel".format(image_dir),
222 + initrd="{}/initrd.gz".format(image_dir), append=append)

I also like .format() but I think using "%s/kernel" might be a little bit clearer in this case.

[1]

28 + if node is None:
29 + # This node is enlisting, for which we use a commissioning
30 + # image. TODO: Do we?

I /think/ smoser is working on that, maybe get in touch with to see where he is at with that.

[2]

93 + # TODO: don't hard-code release.

You can reference bug 1013146 here.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for the review, all suggestions gladly accepted.

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-07-31 09:10:44 +0000
3+++ src/maasserver/api.py 2012-07-31 15:56:24 +0000
4@@ -1061,52 +1061,79 @@
5 query={'op': 'get_preseed'})
6
7
8-def compose_preseed_kernel_opt(mac):
9- """Compose a kernel option for preseed URL for node that owns `mac`."""
10- macaddress_match = MACAddress.objects.filter(mac_address=mac)
11- if len(macaddress_match) == 0:
12+def compose_preseed_kernel_opt(node):
13+ """Compose a kernel option for preseed URL for given `node`.
14+
15+ :param mac_address: A `Node`, or `None`.
16+ """
17+ if node is None:
18 preseed_url = compose_enlistment_preseed_url()
19 else:
20- [macaddress] = macaddress_match
21- preseed_url = compose_preseed_url(macaddress.node)
22+ preseed_url = compose_preseed_url(node)
23 return "auto url=%s" % preseed_url
24
25
26+def get_boot_purpose(node):
27+ """Return a suitable "purpose" for this boot, e.g. "install"."""
28+ # XXX: allenap bug=1031406 2012-07-31: The boot purpose is still in
29+ # flux. It may be that there will just be an "ephemeral" environment and
30+ # an "install" environment, and the differing behaviour between, say,
31+ # enlistment and commissioning - both of which will use the "ephemeral"
32+ # environment - will be governed by varying the preseed or PXE
33+ # configuration.
34+ if node is None:
35+ # This node is enlisting, for which we use a commissioning image.
36+ return "commissioning"
37+ elif node.status == NODE_STATUS.COMMISSIONING:
38+ # It is commissioning.
39+ return "commissioning"
40+ elif node.status == NODE_STATUS.ALLOCATED:
41+ # Install the node if netboot is enabled, otherwise boot locally.
42+ if node.netboot:
43+ return "install"
44+ else:
45+ return "local" # TODO: Investigate.
46+ else:
47+ # Just poweroff? TODO: Investigate. Perhaps even send an IPMI signal
48+ # to turn off power.
49+ return "poweroff"
50+
51+
52 def pxeconfig(request):
53 """Get the PXE configuration given a node's details.
54
55 :param mac: MAC address to produce a boot configuration for. This
56 parameter is optional. If it is not given, the configuration
57 will be the "default" one which boots into an enlistment image.
58+ :param arch: Main machine architecture.
59+ :param subarch: Sub-architecture, or "generic" if there is none.
60 :param title: Title that the node should show in its PXE menu.
61- :param kernel: TFTP path to the kernel image that is to be booted.
62- :param initrd: TFTP path to the initrd that is to be booted.
63 :param append: Additional parameters to append to the kernel command
64 line.
65-
66- In addition, the following parameters are expected, but are not used:
67-
68- :param arch: Main machine architecture.
69- :param subarch: Sub-architecture, or "generic" if there is none.
70 """
71- title = get_mandatory_param(request.GET, 'title')
72- kernel = get_mandatory_param(request.GET, 'kernel')
73- initrd = get_mandatory_param(request.GET, 'initrd')
74- append = get_mandatory_param(request.GET, 'append')
75 mac = request.GET.get('mac', None)
76-
77- # The following two parameters - arch and subarch - typically must be
78- # supplied, but are unused right now.
79 arch = get_mandatory_param(request.GET, 'arch')
80 subarch = request.GET.get('subarch', 'generic')
81- arch, subarch # Suppress lint warnings.
82+ title = get_mandatory_param(request.GET, 'title')
83+ append = get_mandatory_param(request.GET, 'append')
84+
85+ # See if we have a record of this MAC address, and thus node.
86+ try:
87+ macaddress = MACAddress.objects.get(mac_address=mac)
88+ except MACAddress.DoesNotExist:
89+ macaddress = node = None
90+ else:
91+ node = macaddress.node
92
93 # In addition to the "append" parameter, also add a URL for the
94 # node's preseed to the kernel command line.
95- append = "%s %s" % (append, compose_preseed_kernel_opt(mac))
96+ append = "%s %s" % (append, compose_preseed_kernel_opt(node))
97+
98+ # XXX: allenap 2012-07-31 bug=1013146: 'precise' is hardcoded here.
99+ release = "precise"
100
101 return HttpResponse(
102 render_pxe_config(
103- title=title, kernel=kernel,
104- initrd=initrd, append=append),
105+ title=title, arch=arch, subarch=subarch, release=release,
106+ purpose=get_boot_purpose(node), append=append),
107 content_type="text/plain; charset=utf-8")
108
109=== modified file 'src/maasserver/tests/test_api.py'
110--- src/maasserver/tests/test_api.py 2012-07-31 09:10:44 +0000
111+++ src/maasserver/tests/test_api.py 2012-07-31 15:56:24 +0000
112@@ -87,6 +87,7 @@
113 POWER_TYPE_CHOICES,
114 )
115 from testtools.matchers import (
116+ Contains,
117 Equals,
118 MatchesListwise,
119 StartsWith,
120@@ -2261,8 +2262,6 @@
121 'subarch': "armadaxp",
122 'mac': factory.make_mac_address().mac_address,
123 'title': factory.make_name("Menu"),
124- 'kernel': factory.make_name("/my/kernel"),
125- 'initrd': factory.make_name("/my/initrd"),
126 'append': factory.make_name("append"),
127 }
128
129@@ -2300,8 +2299,6 @@
130 'subarch': httplib.OK,
131 'mac': httplib.OK,
132 'title': httplib.BAD_REQUEST,
133- 'kernel': httplib.BAD_REQUEST,
134- 'initrd': httplib.BAD_REQUEST,
135 'append': httplib.BAD_REQUEST,
136 }
137 observed = {
138@@ -2337,15 +2334,15 @@
139 api.compose_preseed_url(node), StartsWith(url))
140
141 def test_compose_preseed_kernel_opt_returns_option_for_known_node(self):
142- mac = factory.make_mac_address()
143+ node = factory.make_node()
144 self.assertEqual(
145- "auto url=%s" % api.compose_preseed_url(mac.node),
146- api.compose_preseed_kernel_opt(mac.mac_address))
147+ "auto url=%s" % api.compose_preseed_url(node),
148+ api.compose_preseed_kernel_opt(node))
149
150 def test_compose_preseed_kernel_opt_returns_option_for_unknown_node(self):
151 self.assertEqual(
152 "auto url=%s" % api.compose_enlistment_preseed_url(),
153- api.compose_preseed_kernel_opt(factory.getRandomMACAddress()))
154+ api.compose_preseed_kernel_opt(None))
155
156 def test_pxe_config_appends_enlistment_preseed_url_for_unknown_node(self):
157 params = self.get_params()
158@@ -2359,6 +2356,37 @@
159 response = self.client.get(reverse('pxeconfig'), params)
160 self.assertIn(api.compose_preseed_url(node), response.content)
161
162+ def test_get_boot_purpose_unknown_node(self):
163+ # A node that's not yet known to MAAS is assumed to be enlisting,
164+ # which uses a "commissioning" image.
165+ self.assertEqual("commissioning", api.get_boot_purpose(None))
166+
167+ def test_get_boot_purpose_known_node(self):
168+ # The following table shows the expected boot "purpose" for each set
169+ # of node parameters.
170+ options = [
171+ ("poweroff", {"status": NODE_STATUS.DECLARED}),
172+ ("commissioning", {"status": NODE_STATUS.COMMISSIONING}),
173+ ("poweroff", {"status": NODE_STATUS.FAILED_TESTS}),
174+ ("poweroff", {"status": NODE_STATUS.MISSING}),
175+ ("poweroff", {"status": NODE_STATUS.READY}),
176+ ("poweroff", {"status": NODE_STATUS.RESERVED}),
177+ ("install", {"status": NODE_STATUS.ALLOCATED, "netboot": True}),
178+ ("local", {"status": NODE_STATUS.ALLOCATED, "netboot": False}),
179+ ("poweroff", {"status": NODE_STATUS.RETIRED}),
180+ ]
181+ node = factory.make_node()
182+ for purpose, parameters in options:
183+ for name, value in parameters.items():
184+ setattr(node, name, value)
185+ self.assertEqual(purpose, api.get_boot_purpose(node))
186+
187+ def test_pxe_config_uses_boot_purpose(self):
188+ fake_boot_purpose = factory.make_name("purpose")
189+ self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose)
190+ response = self.client.get(reverse('pxeconfig'), self.get_params())
191+ self.assertThat(response.content, Contains(fake_boot_purpose))
192+
193
194 class TestNodeGroupsAPI(APITestCase):
195
196
197=== modified file 'src/provisioningserver/pxe/config.py'
198--- src/provisioningserver/pxe/config.py 2012-07-30 20:59:31 +0000
199+++ src/provisioningserver/pxe/config.py 2012-07-31 15:56:24 +0000
200@@ -22,6 +22,7 @@
201
202 from os import path
203
204+from provisioningserver.pxe.tftppath import compose_image_path
205 import tempita
206
207
208@@ -30,13 +31,17 @@
209 template = tempita.Template.from_filename(template_filename, encoding="UTF-8")
210
211
212-def render_pxe_config(title, kernel, initrd, append):
213+def render_pxe_config(title, arch, subarch, release, purpose, append):
214 """Render a PXE configuration file as a unicode string.
215
216 :param title: Title that the node should show on its boot menu.
217- :param kernel: TFTP path to the kernel image to boot.
218- :param initrd: TFTP path to the initrd file to boot from.
219+ :param arch: Main machine architecture.
220+ :param subarch: Sub-architecture, or "generic" if there is none.
221+ :param release: The OS release, e.g. "precise".
222+ :param purpose: What's the purpose of this boot, e.g. "install".
223 :param append: Additional kernel parameters.
224 """
225+ image_dir = compose_image_path(arch, subarch, release, purpose)
226 return template.substitute(
227- title=title, kernel=kernel, initrd=initrd, append=append)
228+ title=title, kernel="%s/kernel" % image_dir,
229+ initrd="%s/initrd.gz" % image_dir, append=append)
230
231=== modified file 'src/provisioningserver/pxe/tests/test_config.py'
232--- src/provisioningserver/pxe/tests/test_config.py 2012-07-31 08:34:59 +0000
233+++ src/provisioningserver/pxe/tests/test_config.py 2012-07-31 15:56:24 +0000
234@@ -12,13 +12,16 @@
235 __metaclass__ = type
236 __all__ = []
237
238+import re
239+
240 from maastesting.factory import factory
241 from maastesting.testcase import TestCase
242 from provisioningserver.pxe.config import render_pxe_config
243+from provisioningserver.pxe.tftppath import compose_image_path
244 from testtools.matchers import (
245- Contains,
246 IsInstance,
247 MatchesAll,
248+ MatchesRegex,
249 StartsWith,
250 )
251
252@@ -31,8 +34,10 @@
253 # correctly rendered.
254 options = {
255 "title": factory.make_name("title"),
256- "kernel": factory.make_name("kernel"),
257- "initrd": factory.make_name("initrd"),
258+ "arch": factory.make_name("arch"),
259+ "subarch": factory.make_name("subarch"),
260+ "release": factory.make_name("release"),
261+ "purpose": factory.make_name("purpose"),
262 "append": factory.make_name("append"),
263 }
264 output = render_pxe_config(**options)
265@@ -41,6 +46,21 @@
266 # The template has rendered without error. PXELINUX configurations
267 # typically start with a DEFAULT line.
268 self.assertThat(output, StartsWith("DEFAULT "))
269- # All of the values put in are included somewhere in the output.
270- expected = (Contains(value) for value in options.values())
271- self.assertThat(output, MatchesAll(*expected))
272+ # The PXE parameters are all set according to the options.
273+ image_dir = compose_image_path(
274+ arch=options["arch"], subarch=options["subarch"],
275+ release=options["release"], purpose=options["purpose"])
276+ self.assertThat(
277+ output, MatchesAll(
278+ MatchesRegex(
279+ r'.*^MENU TITLE %s$' % re.escape(options["title"]),
280+ re.MULTILINE | re.DOTALL),
281+ MatchesRegex(
282+ r'.*^\s+KERNEL %s/kernel$' % re.escape(image_dir),
283+ re.MULTILINE | re.DOTALL),
284+ MatchesRegex(
285+ r'.*^\s+INITRD %s/initrd[.]gz$' % re.escape(image_dir),
286+ re.MULTILINE | re.DOTALL),
287+ MatchesRegex(
288+ r'.*^\s+APPEND %s$' % re.escape(options["append"]),
289+ re.MULTILINE | re.DOTALL)))
290
291=== modified file 'src/provisioningserver/tests/test_tftp.py'
292--- src/provisioningserver/tests/test_tftp.py 2012-07-30 21:28:47 +0000
293+++ src/provisioningserver/tests/test_tftp.py 2012-07-31 15:56:24 +0000
294@@ -131,16 +131,10 @@
295 arch = factory.make_name("arch").encode("ascii")
296 subarch = factory.make_name("subarch").encode("ascii")
297 mac = factory.getRandomMACAddress(b"-")
298- kernel = factory.make_name("kernel").encode("ascii")
299- initrd = factory.make_name("initrd").encode("ascii")
300 title = factory.make_name("menu-title").encode("ascii")
301 append = factory.make_name("append").encode("ascii")
302- backend_url = b"http://example.com/?" + urlencode({
303- b"kernel": kernel,
304- b"initrd": initrd,
305- b"title": title,
306- b"append": append,
307- })
308+ backend_url = b"http://example.com/?" + (
309+ urlencode({b"title": title, b"append": append}))
310 backend = TFTPBackend(self.make_dir(), backend_url)
311 # params is an example of the parameters obtained from a request.
312 params = {"arch": arch, "subarch": subarch, "mac": mac}
313@@ -149,8 +143,6 @@
314 query = parse_qsl(generator_url.query)
315 query_expected = [
316 ("append", append),
317- ("kernel", kernel),
318- ("initrd", initrd),
319 ("arch", arch),
320 ("subarch", subarch),
321 ("title", title),
322
323=== modified file 'src/provisioningserver/tftp.py'
324--- src/provisioningserver/tftp.py 2012-07-30 21:28:47 +0000
325+++ src/provisioningserver/tftp.py 2012-07-31 15:56:24 +0000
326@@ -111,8 +111,6 @@
327 # TODO: update query defaults.
328 query = {
329 b"title": b"",
330- b"kernel": b"",
331- b"initrd": b"",
332 b"append": b"",
333 }
334 # Merge parameters from the generator URL.