Merge lp:~blake-rouse/maas/powernv-support into lp:~maas-committers/maas/trunk
- powernv-support
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Blake Rouse |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2397 |
Proposed branch: | lp:~blake-rouse/maas/powernv-support |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
746 lines (+612/-12) 10 files modified
etc/maas/templates/dhcp/dhcpd.conf.template (+1/-0) etc/maas/templates/pxe/config.commissioning.ppc64el.template (+6/-0) etc/maas/templates/pxe/config.install.ppc64el.template (+6/-0) src/provisioningserver/boot/__init__.py (+6/-0) src/provisioningserver/boot/powernv.py (+158/-0) src/provisioningserver/boot/tests/test_powernv.py (+337/-0) src/provisioningserver/dhcp/config.py (+24/-11) src/provisioningserver/driver/__init__.py (+8/-1) src/provisioningserver/utils/__init__.py (+21/-0) src/provisioningserver/utils/tests/test_utils.py (+45/-0) |
To merge this branch: | bzr merge lp:~blake-rouse/maas/powernv-support |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Jason Hobbs (community) | Approve | ||
Review via email: mp+220840@code.launchpad.net |
Commit message
Support enlisting and booting PowerNV nodes. Added support for the path-prefix for dhcpd config architectures.
Description of the change
Add support for PowerNV. PowerNV boots using pxelinux emulation. The dhcpd option 210 is used to prefix all PXE requests with "ppc64el/". This allows the PowerNVBootMethod to identify that the booting node is architecture ppc64el.
compose_
PowerNV has some issues that the boot method handles.
1. IPAPPEND 2 is not support.
- The BOOTIF= kernel parameter has to be manual injected into the kernel parameters as PowerNV does not support the IPAPPEND option in the pxelinux.cfg. This is done by using the provided mac address from the pxelinux.cfg request, or by getting the mac address from the arp cache using the remote_host ip address.
2. LOCALBOOT is not supported.
- Without localboot support MAAS needs to be able to tell the node to boot from local disk. The only work around for this, is to return an empty pxelinux.cfg config when booting local. This will cause the PowerNV node to ignore the PXE boot, and boot from the first disk.
Jason Hobbs (jason-hobbs) wrote : | # |
Blake Rouse (blake-rouse) wrote : | # |
[3] The regex is actually different in that it adds the ppc64el path, and it removes the arch and sub-arch parameters from the regex. So it needs to be seperate from PXEBootMethod.
[4] Also you cannot import those as when you import re_config_file, it is already the compiled regex, which would be the incorrect regex for ppc64el.
[5] Yes the method returns a ByteReader at the end of the method.
[7] MAAS will not boot any normally PXE machine without the PXEBootMethod. So it can easily be assumed that those files will be installed by the PXEBootMethod. No reason to copy the code from the PXEBootMethod, that will already do the same time.
Jason Hobbs (jason-hobbs) wrote : | # |
Here's the rest of my review. Overall it looks really good! I approve as long as the duplication in 1 & 2 get cleaned up. I think some more near duplication could be cleaned up with some creativity but I wouldn't block on it.
[9]
- Architecture(
+ Architecture(
+ name="ppc64el/
+ kernel_
A comment on why the rootdelay is needed and why it's 60 would be nice.
[10]
=== modified file 'src/provisioni
--- src/provisionin
+++ src/provisionin
@@ -19,6 +19,8 @@
from datetime import datetime
from gzip import GzipFile
import os.path
+import tempfile
+from urlparse import urlsplit
from provisioningser
get_
Why are these new imports added when there are no other changes to this file?
=== modified file 'src/provisioni
--- src/provisionin
+++ src/provisionin
@@ -829,3 +829,24 @@
if len(columns) == 5 and columns[2] == mac:
return columns[0]
return None
[11]
+def find_mac_
+ """Find the MAC address for `ip` by reading the output of arp -n.
+
+ Returns `None` if the IP is not found.
+
+ We do this because we aren't necessarily the only DHCP server on the
+ network, so we can't check our own leases file and be guaranteed to find an
+ IP that matches.
+
+ :param ip: The ip address, e.g. '192.168.1.1'.
+ """
+
+ output = call_capture_
+
+ for line in sorted(output):
+ columns = line.split()
+ if len(columns) == 5 and columns[0] == ip:
+ return columns[2]
+ return None
I don't see a unit test for this method.
Blake Rouse (blake-rouse) wrote : | # |
Fixed the requested items. Added tests for find_mac_via_arp.
Jeroen T. Vermeulen (jtv) wrote : | # |
Great reviewing. I have some small things to add as well:
What does it mean for match_path to return None? Is that tested? If that's an "unthinkable" situation, it'd be better to raise an explicit error.
.
A comment in get_reader says: "Lets return the correct file." That's a bit vague — if you're going to return a file, yes, let's make the correct one! I find that "let's" and "the correct" are often telltales of things being left unclear.
.
In test_re_
.
In find_mac_via_arp you use call_capture_
.
All in all the comments and docstrings are very helpful, although I still get lost in places!
Blake Rouse (blake-rouse) wrote : | # |
The return None is required, that tells the TFTP backend that this boot method should not be used for the requested file.
Fixing the other comments. Thanks!
MAAS Lander (maas-lander) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~blake-rouse/maas/powernv-support into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 481 kB in 0s (2,077 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'etc/maas/templates/dhcp/dhcpd.conf.template' |
2 | --- etc/maas/templates/dhcp/dhcpd.conf.template 2014-04-11 03:20:42 +0000 |
3 | +++ etc/maas/templates/dhcp/dhcpd.conf.template 2014-06-02 19:13:29 +0000 |
4 | @@ -6,6 +6,7 @@ |
5 | # the nodegroup's configuration in MAAS to trigger an update. |
6 | |
7 | option arch code 93 = unsigned integer 16; # RFC4578 |
8 | +option path-prefix code 210 = text; #RFC5071 |
9 | {{for dhcp_subnet in dhcp_subnets}} |
10 | subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} { |
11 | {{bootloader}} |
12 | |
13 | === added file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template' |
14 | --- etc/maas/templates/pxe/config.commissioning.ppc64el.template 1970-01-01 00:00:00 +0000 |
15 | +++ etc/maas/templates/pxe/config.commissioning.ppc64el.template 2014-06-02 19:13:29 +0000 |
16 | @@ -0,0 +1,6 @@ |
17 | +DEFAULT execute |
18 | + |
19 | +LABEL execute |
20 | + KERNEL {{kernel_params | kernel_path }} |
21 | + INITRD {{kernel_params | initrd_path }} |
22 | + APPEND {{kernel_params | kernel_command}} |
23 | |
24 | === added file 'etc/maas/templates/pxe/config.install.ppc64el.template' |
25 | --- etc/maas/templates/pxe/config.install.ppc64el.template 1970-01-01 00:00:00 +0000 |
26 | +++ etc/maas/templates/pxe/config.install.ppc64el.template 2014-06-02 19:13:29 +0000 |
27 | @@ -0,0 +1,6 @@ |
28 | +DEFAULT execute |
29 | + |
30 | +LABEL execute |
31 | + KERNEL {{kernel_params | kernel_path }} |
32 | + INITRD {{kernel_params | initrd_path }} |
33 | + APPEND {{kernel_params | kernel_command}} |
34 | |
35 | === added symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template' |
36 | === target is u'config.install.ppc64el.template' |
37 | === modified file 'src/provisioningserver/boot/__init__.py' |
38 | --- src/provisioningserver/boot/__init__.py 2014-05-27 19:43:17 +0000 |
39 | +++ src/provisioningserver/boot/__init__.py 2014-06-02 19:13:29 +0000 |
40 | @@ -99,6 +99,10 @@ |
41 | |
42 | __metaclass__ = ABCMeta |
43 | |
44 | + # Path prefix that is used for the pxelinux.cfg. Used for |
45 | + # the dhcpd.conf that is generated. |
46 | + path_prefix = None |
47 | + |
48 | @abstractproperty |
49 | def name(self): |
50 | """Name of the boot method.""" |
51 | @@ -223,12 +227,14 @@ |
52 | from provisioningserver.boot.pxe import PXEBootMethod |
53 | from provisioningserver.boot.uefi import UEFIBootMethod |
54 | from provisioningserver.boot.powerkvm import PowerKVMBootMethod |
55 | +from provisioningserver.boot.powernv import PowerNVBootMethod |
56 | |
57 | |
58 | builtin_boot_methods = [ |
59 | PXEBootMethod(), |
60 | UEFIBootMethod(), |
61 | PowerKVMBootMethod(), |
62 | + PowerNVBootMethod(), |
63 | ] |
64 | for method in builtin_boot_methods: |
65 | BootMethodRegistry.register_item(method.name, method) |
66 | |
67 | === added file 'src/provisioningserver/boot/powernv.py' |
68 | --- src/provisioningserver/boot/powernv.py 1970-01-01 00:00:00 +0000 |
69 | +++ src/provisioningserver/boot/powernv.py 2014-06-02 19:13:29 +0000 |
70 | @@ -0,0 +1,158 @@ |
71 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
72 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
73 | + |
74 | +"""PowerNV Boot Method""" |
75 | + |
76 | +from __future__ import ( |
77 | + absolute_import, |
78 | + print_function, |
79 | + unicode_literals, |
80 | + ) |
81 | + |
82 | +str = None |
83 | + |
84 | +__metaclass__ = type |
85 | +__all__ = [ |
86 | + 'PowerNVBootMethod', |
87 | + ] |
88 | + |
89 | +import re |
90 | + |
91 | +from provisioningserver.boot import ( |
92 | + BootMethod, |
93 | + BytesReader, |
94 | + get_parameters, |
95 | + ) |
96 | +from provisioningserver.boot.pxe import ( |
97 | + ARP_HTYPE, |
98 | + re_mac_address, |
99 | + ) |
100 | +from provisioningserver.kernel_opts import compose_kernel_command_line |
101 | +from provisioningserver.utils import find_mac_via_arp |
102 | +from tftp.backend import FilesystemReader |
103 | +from twisted.python.context import get |
104 | + |
105 | +# The pxelinux.cfg path is prefixed with the architecture for the |
106 | +# PowerNV nodes. This prefix is set by the path-prefix dhcpd option. |
107 | +# We assume that the ARP HTYPE (hardware type) that PXELINUX sends is |
108 | +# always Ethernet. |
109 | +re_config_file = r''' |
110 | + # Optional leading slash(es). |
111 | + ^/* |
112 | + ppc64el # PowerNV pxe prefix, set by dhcpd |
113 | + / |
114 | + pxelinux[.]cfg # PXELINUX expects this. |
115 | + / |
116 | + (?: # either a MAC |
117 | + {htype:02x} # ARP HTYPE. |
118 | + - |
119 | + (?P<mac>{re_mac_address.pattern}) # Capture MAC. |
120 | + | # or "default" |
121 | + default |
122 | + ) |
123 | + $ |
124 | +''' |
125 | + |
126 | +re_config_file = re_config_file.format( |
127 | + htype=ARP_HTYPE.ETHERNET, re_mac_address=re_mac_address) |
128 | +re_config_file = re.compile(re_config_file, re.VERBOSE) |
129 | + |
130 | + |
131 | +def format_bootif(mac): |
132 | + """Formats a mac address into the BOOTIF format, expected by |
133 | + the linux kernel.""" |
134 | + mac = mac.replace(':', '-') |
135 | + mac = mac.upper() |
136 | + return '%02x-%s' % (ARP_HTYPE.ETHERNET, mac) |
137 | + |
138 | + |
139 | +class PowerNVBootMethod(BootMethod): |
140 | + |
141 | + name = "powernv" |
142 | + template_subdir = "pxe" |
143 | + bootloader_path = "pxelinux.0" |
144 | + arch_octet = "00:0E" |
145 | + path_prefix = "ppc64el/" |
146 | + |
147 | + def get_remote_mac(self): |
148 | + """Gets the requestors MAC address from arp cache. |
149 | + |
150 | + This is used, when the pxelinux.cfg is requested without the mac |
151 | + address appended. This is needed to inject the BOOTIF into the |
152 | + pxelinux.cfg that is returned to the node. |
153 | + """ |
154 | + remote_host, remote_port = get("remote", (None, None)) |
155 | + return find_mac_via_arp(remote_host) |
156 | + |
157 | + def get_params(self, backend, path): |
158 | + """Gets the matching parameters from the requested path.""" |
159 | + match = re_config_file.match(path) |
160 | + if match is not None: |
161 | + return get_parameters(match) |
162 | + if path.lstrip('/').startswith(self.path_prefix): |
163 | + return {'path': path} |
164 | + return None |
165 | + |
166 | + def match_path(self, backend, path): |
167 | + """Checks path for the configuration file that needs to be |
168 | + generated. |
169 | + |
170 | + :param backend: requesting backend |
171 | + :param path: requested path |
172 | + :returns: dict of match params from path, None if no match |
173 | + """ |
174 | + params = self.get_params(backend, path) |
175 | + if params is None: |
176 | + return None |
177 | + params['arch'] = "ppc64el" |
178 | + if 'mac' not in params: |
179 | + mac = self.get_remote_mac() |
180 | + if mac is not None: |
181 | + params['mac'] = mac |
182 | + return params |
183 | + |
184 | + def get_reader(self, backend, kernel_params, **extra): |
185 | + """Render a configuration file as a unicode string. |
186 | + |
187 | + :param backend: requesting backend |
188 | + :param kernel_params: An instance of `KernelParameters`. |
189 | + :param extra: Allow for other arguments. This is a safety valve; |
190 | + parameters generated in another component (for example, see |
191 | + `TFTPBackend.get_config_reader`) won't cause this to break. |
192 | + """ |
193 | + # Due to the path prefix, all requested files from the client will |
194 | + # contain that prefix. Removing the prefix from the path will return |
195 | + # the correct path in the tftp root. |
196 | + if 'path' in extra: |
197 | + path = extra['path'] |
198 | + path = path.replace(self.path_prefix, '', 1) |
199 | + target_path = backend.base.descendant(path.split('/')) |
200 | + return FilesystemReader(target_path) |
201 | + |
202 | + # Return empty config for PowerNV local. PowerNV fails to |
203 | + # support the LOCALBOOT flag. Empty config will allow it |
204 | + # to select the first device. |
205 | + if kernel_params.purpose == 'local': |
206 | + return BytesReader("".encode("utf-8")) |
207 | + |
208 | + template = self.get_template( |
209 | + kernel_params.purpose, kernel_params.arch, |
210 | + kernel_params.subarch) |
211 | + namespace = self.compose_template_namespace(kernel_params) |
212 | + |
213 | + # Modify the kernel_command to inject the BOOTIF. PowerNV fails to |
214 | + # support the IPAPPEND pxelinux flag. |
215 | + def kernel_command(params): |
216 | + cmd_line = compose_kernel_command_line(params) |
217 | + if 'mac' in extra: |
218 | + mac = extra['mac'] |
219 | + mac = format_bootif(mac) |
220 | + return '%s BOOTIF=%s' % (cmd_line, mac) |
221 | + return cmd_line |
222 | + |
223 | + namespace['kernel_command'] = kernel_command |
224 | + return BytesReader(template.substitute(namespace).encode("utf-8")) |
225 | + |
226 | + def install_bootloader(self, destination): |
227 | + """Does nothing. No extra boot files are required. All of the boot |
228 | + files from PXEBootMethod will suffice.""" |
229 | |
230 | === added file 'src/provisioningserver/boot/tests/test_powernv.py' |
231 | --- src/provisioningserver/boot/tests/test_powernv.py 1970-01-01 00:00:00 +0000 |
232 | +++ src/provisioningserver/boot/tests/test_powernv.py 2014-06-02 19:13:29 +0000 |
233 | @@ -0,0 +1,337 @@ |
234 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
235 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
236 | + |
237 | +"""Tests for `provisioningserver.boot.powernv`.""" |
238 | + |
239 | +from __future__ import ( |
240 | + absolute_import, |
241 | + print_function, |
242 | + unicode_literals, |
243 | + ) |
244 | + |
245 | +str = None |
246 | + |
247 | +__metaclass__ = type |
248 | +__all__ = [] |
249 | + |
250 | +import os |
251 | +import re |
252 | + |
253 | +from maastesting.factory import factory |
254 | +from maastesting.testcase import MAASTestCase |
255 | +from provisioningserver.boot import BytesReader |
256 | +from provisioningserver.boot.powernv import ( |
257 | + ARP_HTYPE, |
258 | + format_bootif, |
259 | + PowerNVBootMethod, |
260 | + re_config_file, |
261 | + ) |
262 | +from provisioningserver.boot.tests.test_pxe import parse_pxe_config |
263 | +from provisioningserver.boot.tftppath import compose_image_path |
264 | +from provisioningserver.testing.config import set_tftp_root |
265 | +from provisioningserver.tests.test_kernel_opts import make_kernel_parameters |
266 | +from provisioningserver.tftp import TFTPBackend |
267 | +from testtools.matchers import ( |
268 | + IsInstance, |
269 | + MatchesAll, |
270 | + MatchesRegex, |
271 | + Not, |
272 | + StartsWith, |
273 | + ) |
274 | + |
275 | + |
276 | +def compose_config_path(mac): |
277 | + """Compose the TFTP path for a PowerNV PXE configuration file. |
278 | + |
279 | + The path returned is relative to the TFTP root, as it would be |
280 | + identified by clients on the network. |
281 | + |
282 | + :param mac: A MAC address, in IEEE 802 hyphen-separated form, |
283 | + corresponding to the machine for which this configuration is |
284 | + relevant. This relates to PXELINUX's lookup protocol. |
285 | + :return: Path for the corresponding PXE config file as exposed over |
286 | + TFTP. |
287 | + """ |
288 | + # Not using os.path.join: this is a TFTP path, not a native path. Yes, in |
289 | + # practice for us they're the same. We always assume that the ARP HTYPE |
290 | + # (hardware type) that PXELINUX sends is Ethernet. |
291 | + return "ppc64el/pxelinux.cfg/{htype:02x}-{mac}".format( |
292 | + htype=ARP_HTYPE.ETHERNET, mac=mac) |
293 | + |
294 | + |
295 | +def get_example_path_and_components(): |
296 | + """Return a plausible path and its components. |
297 | + |
298 | + The path is intended to match `re_config_file`, and the components are |
299 | + the expected groups from a match. |
300 | + """ |
301 | + components = {"mac": factory.getRandomMACAddress("-")} |
302 | + config_path = compose_config_path(components["mac"]) |
303 | + return config_path, components |
304 | + |
305 | + |
306 | +class TestPowerNVBootMethod(MAASTestCase): |
307 | + |
308 | + def make_tftp_root(self): |
309 | + """Set, and return, a temporary TFTP root directory.""" |
310 | + tftproot = self.make_dir() |
311 | + self.useFixture(set_tftp_root(tftproot)) |
312 | + return tftproot |
313 | + |
314 | + def test_compose_config_path_follows_maas_pxe_directory_layout(self): |
315 | + name = factory.make_name('config') |
316 | + self.assertEqual( |
317 | + 'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name), |
318 | + compose_config_path(name)) |
319 | + |
320 | + def test_compose_config_path_does_not_include_tftp_root(self): |
321 | + tftproot = self.make_tftp_root() |
322 | + name = factory.make_name('config') |
323 | + self.assertThat( |
324 | + compose_config_path(name), |
325 | + Not(StartsWith(tftproot))) |
326 | + |
327 | + def test_bootloader_path(self): |
328 | + method = PowerNVBootMethod() |
329 | + self.assertEqual('pxelinux.0', method.bootloader_path) |
330 | + |
331 | + def test_bootloader_path_does_not_include_tftp_root(self): |
332 | + tftproot = self.make_tftp_root() |
333 | + method = PowerNVBootMethod() |
334 | + self.assertThat( |
335 | + method.bootloader_path, |
336 | + Not(StartsWith(tftproot))) |
337 | + |
338 | + def test_name(self): |
339 | + method = PowerNVBootMethod() |
340 | + self.assertEqual('powernv', method.name) |
341 | + |
342 | + def test_template_subdir(self): |
343 | + method = PowerNVBootMethod() |
344 | + self.assertEqual('pxe', method.template_subdir) |
345 | + |
346 | + def test_arch_octet(self): |
347 | + method = PowerNVBootMethod() |
348 | + self.assertEqual('00:0E', method.arch_octet) |
349 | + |
350 | + def test_path_prefix(self): |
351 | + method = PowerNVBootMethod() |
352 | + self.assertEqual('ppc64el/', method.path_prefix) |
353 | + |
354 | + |
355 | +class TestPowerNVBootMethodMatchPath(MAASTestCase): |
356 | + """Tests for |
357 | + `provisioningserver.boot.powernv.PowerNVBootMethod.match_path`. |
358 | + """ |
359 | + |
360 | + def test_match_path_pxe_config_with_mac(self): |
361 | + method = PowerNVBootMethod() |
362 | + config_path, expected = get_example_path_and_components() |
363 | + params = method.match_path(None, config_path) |
364 | + expected['arch'] = 'ppc64el' |
365 | + self.assertEqual(expected, params) |
366 | + |
367 | + def test_match_path_pxe_config_without_mac(self): |
368 | + method = PowerNVBootMethod() |
369 | + fake_mac = factory.getRandomMACAddress() |
370 | + self.patch(method, 'get_remote_mac').return_value = fake_mac |
371 | + config_path = 'ppc64el/pxelinux.cfg/default' |
372 | + params = method.match_path(None, config_path) |
373 | + expected = { |
374 | + 'arch': 'ppc64el', |
375 | + 'mac': fake_mac, |
376 | + } |
377 | + self.assertEqual(expected, params) |
378 | + |
379 | + def test_match_path_pxe_prefix_request(self): |
380 | + method = PowerNVBootMethod() |
381 | + fake_mac = factory.getRandomMACAddress() |
382 | + self.patch(method, 'get_remote_mac').return_value = fake_mac |
383 | + file_path = 'ppc64el/file' |
384 | + params = method.match_path(None, file_path) |
385 | + expected = { |
386 | + 'arch': 'ppc64el', |
387 | + 'mac': fake_mac, |
388 | + 'path': file_path, |
389 | + } |
390 | + self.assertEqual(expected, params) |
391 | + |
392 | + |
393 | +class TestPowerNVBootMethodRenderConfig(MAASTestCase): |
394 | + """Tests for |
395 | + `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader` |
396 | + """ |
397 | + |
398 | + def test_get_reader_install(self): |
399 | + # Given the right configuration options, the PXE configuration is |
400 | + # correctly rendered. |
401 | + method = PowerNVBootMethod() |
402 | + params = make_kernel_parameters(self, purpose="install") |
403 | + output = method.get_reader(backend=None, kernel_params=params) |
404 | + # The output is a BytesReader. |
405 | + self.assertThat(output, IsInstance(BytesReader)) |
406 | + output = output.read(10000) |
407 | + # The template has rendered without error. PXELINUX configurations |
408 | + # typically start with a DEFAULT line. |
409 | + self.assertThat(output, StartsWith("DEFAULT ")) |
410 | + # The PXE parameters are all set according to the options. |
411 | + image_dir = compose_image_path( |
412 | + osystem=params.osystem, arch=params.arch, subarch=params.subarch, |
413 | + release=params.release, label=params.label) |
414 | + self.assertThat( |
415 | + output, MatchesAll( |
416 | + MatchesRegex( |
417 | + r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir), |
418 | + re.MULTILINE | re.DOTALL), |
419 | + MatchesRegex( |
420 | + r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir), |
421 | + re.MULTILINE | re.DOTALL), |
422 | + MatchesRegex( |
423 | + r'.*^\s+APPEND .+?$', |
424 | + re.MULTILINE | re.DOTALL))) |
425 | + |
426 | + def test_get_reader_with_extra_arguments_does_not_affect_output(self): |
427 | + # get_reader() allows any keyword arguments as a safety valve. |
428 | + method = PowerNVBootMethod() |
429 | + options = { |
430 | + "backend": None, |
431 | + "kernel_params": make_kernel_parameters(self, purpose="install"), |
432 | + } |
433 | + # Capture the output before sprinking in some random options. |
434 | + output_before = method.get_reader(**options).read(10000) |
435 | + # Sprinkle some magic in. |
436 | + options.update( |
437 | + (factory.make_name("name"), factory.make_name("value")) |
438 | + for _ in range(10)) |
439 | + # Capture the output after sprinking in some random options. |
440 | + output_after = method.get_reader(**options).read(10000) |
441 | + # The generated template is the same. |
442 | + self.assertEqual(output_before, output_after) |
443 | + |
444 | + def test_get_reader_with_local_purpose(self): |
445 | + # If purpose is "local", output should be empty string. |
446 | + method = PowerNVBootMethod() |
447 | + options = { |
448 | + "backend": None, |
449 | + "kernel_params": make_kernel_parameters(purpose="local"), |
450 | + } |
451 | + output = method.get_reader(**options).read(10000) |
452 | + self.assertIn("", output) |
453 | + |
454 | + def test_get_reader_appends_bootif(self): |
455 | + method = PowerNVBootMethod() |
456 | + fake_mac = factory.getRandomMACAddress() |
457 | + params = make_kernel_parameters(self, purpose="install") |
458 | + output = method.get_reader( |
459 | + backend=None, kernel_params=params, arch='ppc64el', mac=fake_mac) |
460 | + output = output.read(10000) |
461 | + config = parse_pxe_config(output) |
462 | + expected = 'BOOTIF=%s' % format_bootif(fake_mac) |
463 | + self.assertIn(expected, config['execute']['APPEND']) |
464 | + |
465 | + |
466 | +class TestPowerNVBootMethodPathPrefix(MAASTestCase): |
467 | + """Tests for |
468 | + `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`. |
469 | + """ |
470 | + |
471 | + def test_get_reader_path_prefix(self): |
472 | + data = factory.getRandomString().encode("ascii") |
473 | + temp_file = self.make_file(name="example", contents=data) |
474 | + temp_dir = os.path.dirname(temp_file) |
475 | + backend = TFTPBackend(temp_dir, "http://nowhere.example.com/") |
476 | + method = PowerNVBootMethod() |
477 | + options = { |
478 | + 'backend': backend, |
479 | + 'kernel_params': make_kernel_parameters(), |
480 | + 'path': 'ppc64el/example', |
481 | + } |
482 | + reader = method.get_reader(**options) |
483 | + self.addCleanup(reader.finish) |
484 | + self.assertEqual(len(data), reader.size) |
485 | + self.assertEqual(data, reader.read(len(data))) |
486 | + self.assertEqual(b"", reader.read(1)) |
487 | + |
488 | + def test_get_reader_path_prefix_only_removes_first_occurrence(self): |
489 | + data = factory.getRandomString().encode("ascii") |
490 | + temp_dir = self.make_dir() |
491 | + temp_subdir = os.path.join(temp_dir, 'ppc64el') |
492 | + os.mkdir(temp_subdir) |
493 | + factory.make_file(temp_subdir, "example", data) |
494 | + backend = TFTPBackend(temp_dir, "http://nowhere.example.com/") |
495 | + method = PowerNVBootMethod() |
496 | + options = { |
497 | + 'backend': backend, |
498 | + 'kernel_params': make_kernel_parameters(), |
499 | + 'path': 'ppc64el/ppc64el/example', |
500 | + } |
501 | + reader = method.get_reader(**options) |
502 | + self.addCleanup(reader.finish) |
503 | + self.assertEqual(len(data), reader.size) |
504 | + self.assertEqual(data, reader.read(len(data))) |
505 | + self.assertEqual(b"", reader.read(1)) |
506 | + |
507 | + |
508 | +class TestPowerNVBootMethodRegex(MAASTestCase): |
509 | + """Tests for |
510 | + `provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`. |
511 | + """ |
512 | + |
513 | + def test_re_config_file_is_compatible_with_config_path_generator(self): |
514 | + # The regular expression for extracting components of the file path is |
515 | + # compatible with the PXE config path generator. |
516 | + for iteration in range(10): |
517 | + config_path, args = get_example_path_and_components() |
518 | + match = re_config_file.match(config_path) |
519 | + self.assertIsNotNone(match, config_path) |
520 | + self.assertEqual(args, match.groupdict()) |
521 | + |
522 | + def test_re_config_file_with_leading_slash(self): |
523 | + # The regular expression for extracting components of the file path |
524 | + # doesn't care if there's a leading forward slash; the TFTP server is |
525 | + # easy on this point, so it makes sense to be also. |
526 | + config_path, args = get_example_path_and_components() |
527 | + # Ensure there's a leading slash. |
528 | + config_path = "/" + config_path.lstrip("/") |
529 | + match = re_config_file.match(config_path) |
530 | + self.assertIsNotNone(match, config_path) |
531 | + self.assertEqual(args, match.groupdict()) |
532 | + |
533 | + def test_re_config_file_without_leading_slash(self): |
534 | + # The regular expression for extracting components of the file path |
535 | + # doesn't care if there's no leading forward slash; the TFTP server is |
536 | + # easy on this point, so it makes sense to be also. |
537 | + config_path, args = get_example_path_and_components() |
538 | + # Ensure there's no leading slash. |
539 | + config_path = config_path.lstrip("/") |
540 | + match = re_config_file.match(config_path) |
541 | + self.assertIsNotNone(match, config_path) |
542 | + self.assertEqual(args, match.groupdict()) |
543 | + |
544 | + def test_re_config_file_matches_classic_pxelinux_cfg(self): |
545 | + # The default config path is simply "pxelinux.cfg" (without |
546 | + # leading slash). The regex matches this. |
547 | + mac = 'aa-bb-cc-dd-ee-ff' |
548 | + match = re_config_file.match('ppc64el/pxelinux.cfg/01-%s' % mac) |
549 | + self.assertIsNotNone(match) |
550 | + self.assertEqual({'mac': mac}, match.groupdict()) |
551 | + |
552 | + def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self): |
553 | + mac = 'aa-bb-cc-dd-ee-ff' |
554 | + match = re_config_file.match('/ppc64el/pxelinux.cfg/01-%s' % mac) |
555 | + self.assertIsNotNone(match) |
556 | + self.assertEqual({'mac': mac}, match.groupdict()) |
557 | + |
558 | + def test_re_config_file_does_not_match_non_config_file(self): |
559 | + self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel')) |
560 | + |
561 | + def test_re_config_file_does_not_match_file_in_root(self): |
562 | + self.assertIsNone(re_config_file.match('01-aa-bb-cc-dd-ee-ff')) |
563 | + |
564 | + def test_re_config_file_does_not_match_file_not_in_pxelinux_cfg(self): |
565 | + self.assertIsNone(re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff')) |
566 | + |
567 | + def test_re_config_file_with_default(self): |
568 | + match = re_config_file.match('ppc64el/pxelinux.cfg/default') |
569 | + self.assertIsNotNone(match) |
570 | + self.assertEqual({'mac': None}, match.groupdict()) |
571 | |
572 | === modified file 'src/provisioningserver/dhcp/config.py' |
573 | --- src/provisioningserver/dhcp/config.py 2014-04-01 12:57:55 +0000 |
574 | +++ src/provisioningserver/dhcp/config.py 2014-06-02 19:13:29 +0000 |
575 | @@ -33,16 +33,22 @@ |
576 | |
577 | # Used to generate the conditional bootloader behaviour |
578 | CONDITIONAL_BOOTLOADER = """ |
579 | -{behaviour} option arch = {arch_octet} {{ |
580 | - filename \"{bootloader}\"; |
581 | - }} |
582 | +{{behaviour}} option arch = {{arch_octet}} { |
583 | + filename \"{{bootloader}}\"; |
584 | + {{if path_prefix}} |
585 | + option path-prefix \"{{path_prefix}}\"; |
586 | + {{endif}} |
587 | + } |
588 | """ |
589 | |
590 | # Used to generate the PXEBootLoader special case |
591 | PXE_BOOTLOADER = """ |
592 | -else {{ |
593 | - filename \"{bootloader}\"; |
594 | - }} |
595 | +else { |
596 | + filename \"{{bootloader}}\"; |
597 | + {{if path_prefix}} |
598 | + option path-prefix \"{{path_prefix}}\"; |
599 | + {{endif}} |
600 | + } |
601 | """ |
602 | |
603 | |
604 | @@ -55,9 +61,13 @@ |
605 | behaviour = chain(["if"], repeat("elsif")) |
606 | for name, method in BootMethodRegistry: |
607 | if name != "pxe": |
608 | - output += CONDITIONAL_BOOTLOADER.format( |
609 | - behaviour=next(behaviour), arch_octet=method.arch_octet, |
610 | - bootloader=method.bootloader_path).strip() + ' ' |
611 | + output += tempita.sub( |
612 | + CONDITIONAL_BOOTLOADER, |
613 | + behaviour=next(behaviour), |
614 | + arch_octet=method.arch_octet, |
615 | + bootloader=method.bootloader_path, |
616 | + path_prefix=method.path_prefix, |
617 | + ).strip() + ' ' |
618 | |
619 | # The PXEBootMethod is used in an else statement for the generated |
620 | # dhcpd config. This ensures that a booting node that does not |
621 | @@ -65,8 +75,11 @@ |
622 | # pxelinux can still boot. |
623 | pxe_method = BootMethodRegistry.get_item('pxe') |
624 | if pxe_method is not None: |
625 | - output += PXE_BOOTLOADER.format( |
626 | - bootloader=pxe_method.bootloader_path).strip() |
627 | + output += tempita.sub( |
628 | + PXE_BOOTLOADER, |
629 | + bootloader=pxe_method.bootloader_path, |
630 | + path_prefix=pxe_method.path_prefix, |
631 | + ).strip() |
632 | return output.strip() |
633 | |
634 | |
635 | |
636 | === modified file 'src/provisioningserver/driver/__init__.py' |
637 | --- src/provisioningserver/driver/__init__.py 2014-05-29 18:20:58 +0000 |
638 | +++ src/provisioningserver/driver/__init__.py 2014-06-02 19:13:29 +0000 |
639 | @@ -216,7 +216,14 @@ |
640 | Architecture( |
641 | name="armhf/generic", description="armhf/generic", |
642 | pxealiases=["arm"], kernel_options=["console=ttyAMA0"]), |
643 | - Architecture(name="ppc64el/generic", description="ppc64el"), |
644 | + # PPC64EL needs a rootdelay for PowerNV. The disk controller |
645 | + # in the hardware, takes a little bit longer to come up then |
646 | + # the initrd wants to wait. Set this to 60 seconds, just to |
647 | + # give the booting machine enough time. This doesn't slow down |
648 | + # the booting process, it just increases the timeout. |
649 | + Architecture( |
650 | + name="ppc64el/generic", description="ppc64el", |
651 | + kernel_options=['rootdelay=60']), |
652 | ] |
653 | for arch in builtin_architectures: |
654 | ArchitectureRegistry.register_item(arch.name, arch) |
655 | |
656 | === modified file 'src/provisioningserver/utils/__init__.py' |
657 | --- src/provisioningserver/utils/__init__.py 2014-05-29 17:42:18 +0000 |
658 | +++ src/provisioningserver/utils/__init__.py 2014-06-02 19:13:29 +0000 |
659 | @@ -823,3 +823,24 @@ |
660 | if len(columns) == 5 and columns[2] == mac: |
661 | return columns[0] |
662 | return None |
663 | + |
664 | + |
665 | +def find_mac_via_arp(ip): |
666 | + """Find the MAC address for `ip` by reading the output of arp -n. |
667 | + |
668 | + Returns `None` if the IP is not found. |
669 | + |
670 | + We do this because we aren't necessarily the only DHCP server on the |
671 | + network, so we can't check our own leases file and be guaranteed to find an |
672 | + IP that matches. |
673 | + |
674 | + :param ip: The ip address, e.g. '192.168.1.1'. |
675 | + """ |
676 | + |
677 | + output = call_capture_and_check(['arp', '-n']).split('\n') |
678 | + |
679 | + for line in sorted(output): |
680 | + columns = line.split() |
681 | + if len(columns) == 5 and columns[0] == ip: |
682 | + return columns[2] |
683 | + return None |
684 | |
685 | === modified file 'src/provisioningserver/utils/tests/test_utils.py' |
686 | --- src/provisioningserver/utils/tests/test_utils.py 2014-05-29 17:42:18 +0000 |
687 | +++ src/provisioningserver/utils/tests/test_utils.py 2014-06-02 19:13:29 +0000 |
688 | @@ -74,6 +74,7 @@ |
689 | ExternalProcessError, |
690 | filter_dict, |
691 | find_ip_via_arp, |
692 | + find_mac_via_arp, |
693 | get_all_interface_addresses, |
694 | get_mtime, |
695 | incremental_write, |
696 | @@ -1347,6 +1348,50 @@ |
697 | self.assertEqual(one_result, other_result) |
698 | |
699 | |
700 | +class TestFindMACViaARP(MAASTestCase): |
701 | + |
702 | + def patch_call(self, output): |
703 | + """Replace `call_capture_and_check` with one that returns `output`.""" |
704 | + fake = self.patch(provisioningserver.utils, 'call_capture_and_check') |
705 | + fake.return_value = output |
706 | + return fake |
707 | + |
708 | + def test__resolves_IP_address_to_MAC(self): |
709 | + sample = """\ |
710 | + Address HWtype HWaddress Flags Mask Iface |
711 | + 192.168.100.20 (incomplete) virbr1 |
712 | + 192.168.0.104 (incomplete) eth0 |
713 | + 192.168.0.5 (incomplete) eth0 |
714 | + 192.168.0.2 (incomplete) eth0 |
715 | + 192.168.0.100 (incomplete) eth0 |
716 | + 192.168.122.20 ether 52:54:00:02:86:4b C virbr0 |
717 | + 192.168.0.4 (incomplete) eth0 |
718 | + 192.168.0.1 ether 90:f6:52:f6:17:92 C eth0 |
719 | + """ |
720 | + |
721 | + call_capture_and_check = self.patch_call(sample) |
722 | + mac_address_observed = find_mac_via_arp("192.168.122.20") |
723 | + self.assertThat( |
724 | + call_capture_and_check, |
725 | + MockCalledOnceWith(['arp', '-n'])) |
726 | + self.assertEqual("52:54:00:02:86:4b", mac_address_observed) |
727 | + |
728 | + def test__returns_consistent_output(self): |
729 | + ip = factory.getRandomIPAddress() |
730 | + macs = [ |
731 | + '52:54:00:02:86:4b', |
732 | + '90:f6:52:f6:17:92', |
733 | + ] |
734 | + lines = ['%s ether %s C eth0' % (ip, mac) for mac in macs] |
735 | + self.patch_call('\n'.join(lines)) |
736 | + one_result = find_mac_via_arp(ip) |
737 | + self.patch_call('\n'.join(reversed(lines))) |
738 | + other_result = find_mac_via_arp(ip) |
739 | + |
740 | + self.assertIn(one_result, macs) |
741 | + self.assertEqual(one_result, other_result) |
742 | + |
743 | + |
744 | class TestAsynchronousDecorator(MAASTestCase): |
745 | |
746 | run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) |
Blake, not done reviewing yet but here's my comments so far:
[1]
=== added file 'etc/maas/ templates/ pxe/config. commissioning. ppc64el. template' templates/ pxe/config. commissioning. ppc64el. template 1970-01-01 00:00:00 +0000 templates/ pxe/config. commissioning. ppc64el. template 2014-05-28 21:38:08 +0000
--- etc/maas/
+++ etc/maas/
@@ -0,0 +1,6 @@
+DEFAULT execute
+
+LABEL execute
+ KERNEL {{kernel_params | kernel_path }}
+ INITRD {{kernel_params | initrd_path }}
+ APPEND {{kernel_params | kernel_command}}
=== added file 'etc/maas/ templates/ pxe/config. install. ppc64el. template' templates/ pxe/config. xinstall. ppc64el. template'
...
=== added file 'etc/maas/
I think the duplicate files here should be symlinks - armhf does this
right now.
[2]
+# PXELINUX represents a MAC address in IEEE 802 hyphen-separated www.syslinux. org/wiki/ index.php/ PXELINUX. address_ octet = r'[0-9a-f]{2}' repeat( re_mac_ address_ octet, 6)))
+# format. See http://
+re_mac_
+re_mac_address = re.compile(
+ "-".join(
This is copied directly from pxe.py. It should either be imported
from there or some other common location.
+# The pxelinux.cfg path is prefixed with the architecture for the {re_mac_ address. pattern} ) # Capture MAC.
+# PowerNV nodes. This prefix is set by the path-prefix dhcpd option.
+# We assume that the ARP HTYPE (hardware type) that PXELINUX sends is
+# always Ethernet.
+re_config_file = r'''
+ # Optional leading slash(es).
+ ^/*
+ ppc64el # PowerNV pxe prefix, set by dhcpd
+ /
+ pxelinux[.]cfg # PXELINUX expects this.
+ /
+ (?: # either a MAC
+ {htype:02x} # ARP HTYPE.
+ -
+ (?P<mac>
+ | # or "default"
+ default
+ )
+ $
+'''
[3]
This is very similar to the re_config_file in pxe.py. Perhaps a common
template could be used to generate them?
+re_config_file = re_config_ file.format( HTYPE.ETHERNET, re_mac_ address= re_mac_ address) re_config_ file, re.VERBOSE)
+ htype=ARP_
+re_config_file = re.compile(
[4]
Copied from boot.py - should be common.
[5]
+ def get_reader(self, backend, kernel_params, **extra): get_config_ reader` ) won't cause this to break. self.path_ prefix, '', 1) base.descendant (path.split( '/')) r(target_ path) params. purpose == 'local': "".encode( "utf-8" ))...
+ """Render a configuration file as a unicode string.
+
+ :param backend: requesting backend
+ :param kernel_params: An instance of `KernelParameters`.
+ :param extra: Allow for other arguments. This is a safety valve;
+ parameters generated in another component (for example, see
+ `TFTPBackend.
+ """
+ # Due to the path prefix, all requested files from the client will
+ # contain that prefix. Lets return the correct file, removing the
+ # prefix from the path
+ if 'path' in extra:
+ path = extra['path']
+ path = path.replace(
+ target_path = backend.
+ return FilesystemReade
+
+ # Return empty config for PowerNV local. PowerNV fails to
+ # support the LOCALBOOT flag. Empty config will allow it
+ # to select the first device.
+ if kernel_
+ return BytesReader(