Merge lp:~blake-rouse/maas/powernv-support-1.5 into lp:maas/1.5

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2284
Proposed branch: lp:~blake-rouse/maas/powernv-support-1.5
Merge into: lp:maas/1.5
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-1.5
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+221782@code.launchpad.net

Commit message

Support enlisting and booting PowerNV nodes. Added support for the path-prefix for dhcpd config architectures.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/maas/templates/dhcp/dhcpd.conf.template'
2--- etc/maas/templates/dhcp/dhcpd.conf.template 2014-04-11 05:21:15 +0000
3+++ etc/maas/templates/dhcp/dhcpd.conf.template 2014-06-02 20:19:07 +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 20:19:07 +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 20:19:07 +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-28 13:34:14 +0000
39+++ src/provisioningserver/boot/__init__.py 2014-06-02 20:19:07 +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 20:19:07 +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 20:19:07 +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+ 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 20:19:07 +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-15 17:39:27 +0000
638+++ src/provisioningserver/driver/__init__.py 2014-06-02 20:19:07 +0000
639@@ -139,7 +139,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-06 21:18:17 +0000
658+++ src/provisioningserver/utils/__init__.py 2014-06-02 20:19:07 +0000
659@@ -829,3 +829,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-06 21:18:17 +0000
687+++ src/provisioningserver/utils/tests/test_utils.py 2014-06-02 20:19:07 +0000
688@@ -75,6 +75,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@@ -1322,6 +1323,50 @@
697 self.assertEqual("192.168.0.1", ip_address_observed)
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)

Subscribers

People subscribed via source and target branches

to all changes: