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
=== modified file 'etc/maas/templates/dhcp/dhcpd.conf.template'
--- etc/maas/templates/dhcp/dhcpd.conf.template 2014-04-11 05:21:15 +0000
+++ etc/maas/templates/dhcp/dhcpd.conf.template 2014-06-02 20:19:07 +0000
@@ -6,6 +6,7 @@
6# the nodegroup's configuration in MAAS to trigger an update.6# the nodegroup's configuration in MAAS to trigger an update.
77
8option arch code 93 = unsigned integer 16; # RFC45788option arch code 93 = unsigned integer 16; # RFC4578
9option path-prefix code 210 = text; #RFC5071
9{{for dhcp_subnet in dhcp_subnets}}10{{for dhcp_subnet in dhcp_subnets}}
10subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} {11subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} {
11 {{bootloader}}12 {{bootloader}}
1213
=== added file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template'
--- etc/maas/templates/pxe/config.commissioning.ppc64el.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/pxe/config.commissioning.ppc64el.template 2014-06-02 20:19:07 +0000
@@ -0,0 +1,6 @@
1DEFAULT execute
2
3LABEL execute
4 KERNEL {{kernel_params | kernel_path }}
5 INITRD {{kernel_params | initrd_path }}
6 APPEND {{kernel_params | kernel_command}}
07
=== added file 'etc/maas/templates/pxe/config.install.ppc64el.template'
--- etc/maas/templates/pxe/config.install.ppc64el.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/pxe/config.install.ppc64el.template 2014-06-02 20:19:07 +0000
@@ -0,0 +1,6 @@
1DEFAULT execute
2
3LABEL execute
4 KERNEL {{kernel_params | kernel_path }}
5 INITRD {{kernel_params | initrd_path }}
6 APPEND {{kernel_params | kernel_command}}
07
=== added symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template'
=== target is u'config.install.ppc64el.template'
=== modified file 'src/provisioningserver/boot/__init__.py'
--- src/provisioningserver/boot/__init__.py 2014-05-28 13:34:14 +0000
+++ src/provisioningserver/boot/__init__.py 2014-06-02 20:19:07 +0000
@@ -99,6 +99,10 @@
9999
100 __metaclass__ = ABCMeta100 __metaclass__ = ABCMeta
101101
102 # Path prefix that is used for the pxelinux.cfg. Used for
103 # the dhcpd.conf that is generated.
104 path_prefix = None
105
102 @abstractproperty106 @abstractproperty
103 def name(self):107 def name(self):
104 """Name of the boot method."""108 """Name of the boot method."""
@@ -223,12 +227,14 @@
223from provisioningserver.boot.pxe import PXEBootMethod227from provisioningserver.boot.pxe import PXEBootMethod
224from provisioningserver.boot.uefi import UEFIBootMethod228from provisioningserver.boot.uefi import UEFIBootMethod
225from provisioningserver.boot.powerkvm import PowerKVMBootMethod229from provisioningserver.boot.powerkvm import PowerKVMBootMethod
230from provisioningserver.boot.powernv import PowerNVBootMethod
226231
227232
228builtin_boot_methods = [233builtin_boot_methods = [
229 PXEBootMethod(),234 PXEBootMethod(),
230 UEFIBootMethod(),235 UEFIBootMethod(),
231 PowerKVMBootMethod(),236 PowerKVMBootMethod(),
237 PowerNVBootMethod(),
232]238]
233for method in builtin_boot_methods:239for method in builtin_boot_methods:
234 BootMethodRegistry.register_item(method.name, method)240 BootMethodRegistry.register_item(method.name, method)
235241
=== added file 'src/provisioningserver/boot/powernv.py'
--- src/provisioningserver/boot/powernv.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/boot/powernv.py 2014-06-02 20:19:07 +0000
@@ -0,0 +1,158 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""PowerNV Boot Method"""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'PowerNVBootMethod',
17 ]
18
19import re
20
21from provisioningserver.boot import (
22 BootMethod,
23 BytesReader,
24 get_parameters,
25 )
26from provisioningserver.boot.pxe import (
27 ARP_HTYPE,
28 re_mac_address,
29 )
30from provisioningserver.kernel_opts import compose_kernel_command_line
31from provisioningserver.utils import find_mac_via_arp
32from tftp.backend import FilesystemReader
33from twisted.python.context import get
34
35# The pxelinux.cfg path is prefixed with the architecture for the
36# PowerNV nodes. This prefix is set by the path-prefix dhcpd option.
37# We assume that the ARP HTYPE (hardware type) that PXELINUX sends is
38# always Ethernet.
39re_config_file = r'''
40 # Optional leading slash(es).
41 ^/*
42 ppc64el # PowerNV pxe prefix, set by dhcpd
43 /
44 pxelinux[.]cfg # PXELINUX expects this.
45 /
46 (?: # either a MAC
47 {htype:02x} # ARP HTYPE.
48 -
49 (?P<mac>{re_mac_address.pattern}) # Capture MAC.
50 | # or "default"
51 default
52 )
53 $
54'''
55
56re_config_file = re_config_file.format(
57 htype=ARP_HTYPE.ETHERNET, re_mac_address=re_mac_address)
58re_config_file = re.compile(re_config_file, re.VERBOSE)
59
60
61def format_bootif(mac):
62 """Formats a mac address into the BOOTIF format, expected by
63 the linux kernel."""
64 mac = mac.replace(':', '-')
65 mac = mac.upper()
66 return '%02x-%s' % (ARP_HTYPE.ETHERNET, mac)
67
68
69class PowerNVBootMethod(BootMethod):
70
71 name = "powernv"
72 template_subdir = "pxe"
73 bootloader_path = "pxelinux.0"
74 arch_octet = "00:0E"
75 path_prefix = "ppc64el/"
76
77 def get_remote_mac(self):
78 """Gets the requestors MAC address from arp cache.
79
80 This is used, when the pxelinux.cfg is requested without the mac
81 address appended. This is needed to inject the BOOTIF into the
82 pxelinux.cfg that is returned to the node.
83 """
84 remote_host, remote_port = get("remote", (None, None))
85 return find_mac_via_arp(remote_host)
86
87 def get_params(self, backend, path):
88 """Gets the matching parameters from the requested path."""
89 match = re_config_file.match(path)
90 if match is not None:
91 return get_parameters(match)
92 if path.lstrip('/').startswith(self.path_prefix):
93 return {'path': path}
94 return None
95
96 def match_path(self, backend, path):
97 """Checks path for the configuration file that needs to be
98 generated.
99
100 :param backend: requesting backend
101 :param path: requested path
102 :returns: dict of match params from path, None if no match
103 """
104 params = self.get_params(backend, path)
105 if params is None:
106 return None
107 params['arch'] = "ppc64el"
108 if 'mac' not in params:
109 mac = self.get_remote_mac()
110 if mac is not None:
111 params['mac'] = mac
112 return params
113
114 def get_reader(self, backend, kernel_params, **extra):
115 """Render a configuration file as a unicode string.
116
117 :param backend: requesting backend
118 :param kernel_params: An instance of `KernelParameters`.
119 :param extra: Allow for other arguments. This is a safety valve;
120 parameters generated in another component (for example, see
121 `TFTPBackend.get_config_reader`) won't cause this to break.
122 """
123 # Due to the path prefix, all requested files from the client will
124 # contain that prefix. Removing the prefix from the path will return
125 # the correct path in the tftp root.
126 if 'path' in extra:
127 path = extra['path']
128 path = path.replace(self.path_prefix, '', 1)
129 target_path = backend.base.descendant(path.split('/'))
130 return FilesystemReader(target_path)
131
132 # Return empty config for PowerNV local. PowerNV fails to
133 # support the LOCALBOOT flag. Empty config will allow it
134 # to select the first device.
135 if kernel_params.purpose == 'local':
136 return BytesReader("".encode("utf-8"))
137
138 template = self.get_template(
139 kernel_params.purpose, kernel_params.arch,
140 kernel_params.subarch)
141 namespace = self.compose_template_namespace(kernel_params)
142
143 # Modify the kernel_command to inject the BOOTIF. PowerNV fails to
144 # support the IPAPPEND pxelinux flag.
145 def kernel_command(params):
146 cmd_line = compose_kernel_command_line(params)
147 if 'mac' in extra:
148 mac = extra['mac']
149 mac = format_bootif(mac)
150 return '%s BOOTIF=%s' % (cmd_line, mac)
151 return cmd_line
152
153 namespace['kernel_command'] = kernel_command
154 return BytesReader(template.substitute(namespace).encode("utf-8"))
155
156 def install_bootloader(self, destination):
157 """Does nothing. No extra boot files are required. All of the boot
158 files from PXEBootMethod will suffice."""
0159
=== added file 'src/provisioningserver/boot/tests/test_powernv.py'
--- src/provisioningserver/boot/tests/test_powernv.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/boot/tests/test_powernv.py 2014-06-02 20:19:07 +0000
@@ -0,0 +1,337 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for `provisioningserver.boot.powernv`."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17import os
18import re
19
20from maastesting.factory import factory
21from maastesting.testcase import MAASTestCase
22from provisioningserver.boot import BytesReader
23from provisioningserver.boot.powernv import (
24 ARP_HTYPE,
25 format_bootif,
26 PowerNVBootMethod,
27 re_config_file,
28 )
29from provisioningserver.boot.tests.test_pxe import parse_pxe_config
30from provisioningserver.boot.tftppath import compose_image_path
31from provisioningserver.testing.config import set_tftp_root
32from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
33from provisioningserver.tftp import TFTPBackend
34from testtools.matchers import (
35 IsInstance,
36 MatchesAll,
37 MatchesRegex,
38 Not,
39 StartsWith,
40 )
41
42
43def compose_config_path(mac):
44 """Compose the TFTP path for a PowerNV PXE configuration file.
45
46 The path returned is relative to the TFTP root, as it would be
47 identified by clients on the network.
48
49 :param mac: A MAC address, in IEEE 802 hyphen-separated form,
50 corresponding to the machine for which this configuration is
51 relevant. This relates to PXELINUX's lookup protocol.
52 :return: Path for the corresponding PXE config file as exposed over
53 TFTP.
54 """
55 # Not using os.path.join: this is a TFTP path, not a native path. Yes, in
56 # practice for us they're the same. We always assume that the ARP HTYPE
57 # (hardware type) that PXELINUX sends is Ethernet.
58 return "ppc64el/pxelinux.cfg/{htype:02x}-{mac}".format(
59 htype=ARP_HTYPE.ETHERNET, mac=mac)
60
61
62def get_example_path_and_components():
63 """Return a plausible path and its components.
64
65 The path is intended to match `re_config_file`, and the components are
66 the expected groups from a match.
67 """
68 components = {"mac": factory.getRandomMACAddress("-")}
69 config_path = compose_config_path(components["mac"])
70 return config_path, components
71
72
73class TestPowerNVBootMethod(MAASTestCase):
74
75 def make_tftp_root(self):
76 """Set, and return, a temporary TFTP root directory."""
77 tftproot = self.make_dir()
78 self.useFixture(set_tftp_root(tftproot))
79 return tftproot
80
81 def test_compose_config_path_follows_maas_pxe_directory_layout(self):
82 name = factory.make_name('config')
83 self.assertEqual(
84 'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name),
85 compose_config_path(name))
86
87 def test_compose_config_path_does_not_include_tftp_root(self):
88 tftproot = self.make_tftp_root()
89 name = factory.make_name('config')
90 self.assertThat(
91 compose_config_path(name),
92 Not(StartsWith(tftproot)))
93
94 def test_bootloader_path(self):
95 method = PowerNVBootMethod()
96 self.assertEqual('pxelinux.0', method.bootloader_path)
97
98 def test_bootloader_path_does_not_include_tftp_root(self):
99 tftproot = self.make_tftp_root()
100 method = PowerNVBootMethod()
101 self.assertThat(
102 method.bootloader_path,
103 Not(StartsWith(tftproot)))
104
105 def test_name(self):
106 method = PowerNVBootMethod()
107 self.assertEqual('powernv', method.name)
108
109 def test_template_subdir(self):
110 method = PowerNVBootMethod()
111 self.assertEqual('pxe', method.template_subdir)
112
113 def test_arch_octet(self):
114 method = PowerNVBootMethod()
115 self.assertEqual('00:0E', method.arch_octet)
116
117 def test_path_prefix(self):
118 method = PowerNVBootMethod()
119 self.assertEqual('ppc64el/', method.path_prefix)
120
121
122class TestPowerNVBootMethodMatchPath(MAASTestCase):
123 """Tests for
124 `provisioningserver.boot.powernv.PowerNVBootMethod.match_path`.
125 """
126
127 def test_match_path_pxe_config_with_mac(self):
128 method = PowerNVBootMethod()
129 config_path, expected = get_example_path_and_components()
130 params = method.match_path(None, config_path)
131 expected['arch'] = 'ppc64el'
132 self.assertEqual(expected, params)
133
134 def test_match_path_pxe_config_without_mac(self):
135 method = PowerNVBootMethod()
136 fake_mac = factory.getRandomMACAddress()
137 self.patch(method, 'get_remote_mac').return_value = fake_mac
138 config_path = 'ppc64el/pxelinux.cfg/default'
139 params = method.match_path(None, config_path)
140 expected = {
141 'arch': 'ppc64el',
142 'mac': fake_mac,
143 }
144 self.assertEqual(expected, params)
145
146 def test_match_path_pxe_prefix_request(self):
147 method = PowerNVBootMethod()
148 fake_mac = factory.getRandomMACAddress()
149 self.patch(method, 'get_remote_mac').return_value = fake_mac
150 file_path = 'ppc64el/file'
151 params = method.match_path(None, file_path)
152 expected = {
153 'arch': 'ppc64el',
154 'mac': fake_mac,
155 'path': file_path,
156 }
157 self.assertEqual(expected, params)
158
159
160class TestPowerNVBootMethodRenderConfig(MAASTestCase):
161 """Tests for
162 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`
163 """
164
165 def test_get_reader_install(self):
166 # Given the right configuration options, the PXE configuration is
167 # correctly rendered.
168 method = PowerNVBootMethod()
169 params = make_kernel_parameters(self, purpose="install")
170 output = method.get_reader(backend=None, kernel_params=params)
171 # The output is a BytesReader.
172 self.assertThat(output, IsInstance(BytesReader))
173 output = output.read(10000)
174 # The template has rendered without error. PXELINUX configurations
175 # typically start with a DEFAULT line.
176 self.assertThat(output, StartsWith("DEFAULT "))
177 # The PXE parameters are all set according to the options.
178 image_dir = compose_image_path(
179 arch=params.arch, subarch=params.subarch,
180 release=params.release, label=params.label)
181 self.assertThat(
182 output, MatchesAll(
183 MatchesRegex(
184 r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir),
185 re.MULTILINE | re.DOTALL),
186 MatchesRegex(
187 r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir),
188 re.MULTILINE | re.DOTALL),
189 MatchesRegex(
190 r'.*^\s+APPEND .+?$',
191 re.MULTILINE | re.DOTALL)))
192
193 def test_get_reader_with_extra_arguments_does_not_affect_output(self):
194 # get_reader() allows any keyword arguments as a safety valve.
195 method = PowerNVBootMethod()
196 options = {
197 "backend": None,
198 "kernel_params": make_kernel_parameters(self, purpose="install"),
199 }
200 # Capture the output before sprinking in some random options.
201 output_before = method.get_reader(**options).read(10000)
202 # Sprinkle some magic in.
203 options.update(
204 (factory.make_name("name"), factory.make_name("value"))
205 for _ in range(10))
206 # Capture the output after sprinking in some random options.
207 output_after = method.get_reader(**options).read(10000)
208 # The generated template is the same.
209 self.assertEqual(output_before, output_after)
210
211 def test_get_reader_with_local_purpose(self):
212 # If purpose is "local", output should be empty string.
213 method = PowerNVBootMethod()
214 options = {
215 "backend": None,
216 "kernel_params": make_kernel_parameters(purpose="local"),
217 }
218 output = method.get_reader(**options).read(10000)
219 self.assertIn("", output)
220
221 def test_get_reader_appends_bootif(self):
222 method = PowerNVBootMethod()
223 fake_mac = factory.getRandomMACAddress()
224 params = make_kernel_parameters(self, purpose="install")
225 output = method.get_reader(
226 backend=None, kernel_params=params, arch='ppc64el', mac=fake_mac)
227 output = output.read(10000)
228 config = parse_pxe_config(output)
229 expected = 'BOOTIF=%s' % format_bootif(fake_mac)
230 self.assertIn(expected, config['execute']['APPEND'])
231
232
233class TestPowerNVBootMethodPathPrefix(MAASTestCase):
234 """Tests for
235 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`.
236 """
237
238 def test_get_reader_path_prefix(self):
239 data = factory.getRandomString().encode("ascii")
240 temp_file = self.make_file(name="example", contents=data)
241 temp_dir = os.path.dirname(temp_file)
242 backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
243 method = PowerNVBootMethod()
244 options = {
245 'backend': backend,
246 'kernel_params': make_kernel_parameters(),
247 'path': 'ppc64el/example',
248 }
249 reader = method.get_reader(**options)
250 self.addCleanup(reader.finish)
251 self.assertEqual(len(data), reader.size)
252 self.assertEqual(data, reader.read(len(data)))
253 self.assertEqual(b"", reader.read(1))
254
255 def test_get_reader_path_prefix_only_removes_first_occurrence(self):
256 data = factory.getRandomString().encode("ascii")
257 temp_dir = self.make_dir()
258 temp_subdir = os.path.join(temp_dir, 'ppc64el')
259 os.mkdir(temp_subdir)
260 factory.make_file(temp_subdir, "example", data)
261 backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
262 method = PowerNVBootMethod()
263 options = {
264 'backend': backend,
265 'kernel_params': make_kernel_parameters(),
266 'path': 'ppc64el/ppc64el/example',
267 }
268 reader = method.get_reader(**options)
269 self.addCleanup(reader.finish)
270 self.assertEqual(len(data), reader.size)
271 self.assertEqual(data, reader.read(len(data)))
272 self.assertEqual(b"", reader.read(1))
273
274
275class TestPowerNVBootMethodRegex(MAASTestCase):
276 """Tests for
277 `provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`.
278 """
279
280 def test_re_config_file_is_compatible_with_config_path_generator(self):
281 # The regular expression for extracting components of the file path is
282 # compatible with the PXE config path generator.
283 for iteration in range(10):
284 config_path, args = get_example_path_and_components()
285 match = re_config_file.match(config_path)
286 self.assertIsNotNone(match, config_path)
287 self.assertEqual(args, match.groupdict())
288
289 def test_re_config_file_with_leading_slash(self):
290 # The regular expression for extracting components of the file path
291 # doesn't care if there's a leading forward slash; the TFTP server is
292 # easy on this point, so it makes sense to be also.
293 config_path, args = get_example_path_and_components()
294 # Ensure there's a leading slash.
295 config_path = "/" + config_path.lstrip("/")
296 match = re_config_file.match(config_path)
297 self.assertIsNotNone(match, config_path)
298 self.assertEqual(args, match.groupdict())
299
300 def test_re_config_file_without_leading_slash(self):
301 # The regular expression for extracting components of the file path
302 # doesn't care if there's no leading forward slash; the TFTP server is
303 # easy on this point, so it makes sense to be also.
304 config_path, args = get_example_path_and_components()
305 # Ensure there's no leading slash.
306 config_path = config_path.lstrip("/")
307 match = re_config_file.match(config_path)
308 self.assertIsNotNone(match, config_path)
309 self.assertEqual(args, match.groupdict())
310
311 def test_re_config_file_matches_classic_pxelinux_cfg(self):
312 # The default config path is simply "pxelinux.cfg" (without
313 # leading slash). The regex matches this.
314 mac = 'aa-bb-cc-dd-ee-ff'
315 match = re_config_file.match('ppc64el/pxelinux.cfg/01-%s' % mac)
316 self.assertIsNotNone(match)
317 self.assertEqual({'mac': mac}, match.groupdict())
318
319 def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self):
320 mac = 'aa-bb-cc-dd-ee-ff'
321 match = re_config_file.match('/ppc64el/pxelinux.cfg/01-%s' % mac)
322 self.assertIsNotNone(match)
323 self.assertEqual({'mac': mac}, match.groupdict())
324
325 def test_re_config_file_does_not_match_non_config_file(self):
326 self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel'))
327
328 def test_re_config_file_does_not_match_file_in_root(self):
329 self.assertIsNone(re_config_file.match('01-aa-bb-cc-dd-ee-ff'))
330
331 def test_re_config_file_does_not_match_file_not_in_pxelinux_cfg(self):
332 self.assertIsNone(re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff'))
333
334 def test_re_config_file_with_default(self):
335 match = re_config_file.match('ppc64el/pxelinux.cfg/default')
336 self.assertIsNotNone(match)
337 self.assertEqual({'mac': None}, match.groupdict())
0338
=== modified file 'src/provisioningserver/dhcp/config.py'
--- src/provisioningserver/dhcp/config.py 2014-04-01 12:57:55 +0000
+++ src/provisioningserver/dhcp/config.py 2014-06-02 20:19:07 +0000
@@ -33,16 +33,22 @@
3333
34# Used to generate the conditional bootloader behaviour34# Used to generate the conditional bootloader behaviour
35CONDITIONAL_BOOTLOADER = """35CONDITIONAL_BOOTLOADER = """
36{behaviour} option arch = {arch_octet} {{36{{behaviour}} option arch = {{arch_octet}} {
37 filename \"{bootloader}\";37 filename \"{{bootloader}}\";
38 }}38 {{if path_prefix}}
39 option path-prefix \"{{path_prefix}}\";
40 {{endif}}
41 }
39"""42"""
4043
41# Used to generate the PXEBootLoader special case44# Used to generate the PXEBootLoader special case
42PXE_BOOTLOADER = """45PXE_BOOTLOADER = """
43else {{46else {
44 filename \"{bootloader}\";47 filename \"{{bootloader}}\";
45 }}48 {{if path_prefix}}
49 option path-prefix \"{{path_prefix}}\";
50 {{endif}}
51 }
46"""52"""
4753
4854
@@ -55,9 +61,13 @@
55 behaviour = chain(["if"], repeat("elsif"))61 behaviour = chain(["if"], repeat("elsif"))
56 for name, method in BootMethodRegistry:62 for name, method in BootMethodRegistry:
57 if name != "pxe":63 if name != "pxe":
58 output += CONDITIONAL_BOOTLOADER.format(64 output += tempita.sub(
59 behaviour=next(behaviour), arch_octet=method.arch_octet,65 CONDITIONAL_BOOTLOADER,
60 bootloader=method.bootloader_path).strip() + ' '66 behaviour=next(behaviour),
67 arch_octet=method.arch_octet,
68 bootloader=method.bootloader_path,
69 path_prefix=method.path_prefix,
70 ).strip() + ' '
6171
62 # The PXEBootMethod is used in an else statement for the generated72 # The PXEBootMethod is used in an else statement for the generated
63 # dhcpd config. This ensures that a booting node that does not73 # dhcpd config. This ensures that a booting node that does not
@@ -65,8 +75,11 @@
65 # pxelinux can still boot.75 # pxelinux can still boot.
66 pxe_method = BootMethodRegistry.get_item('pxe')76 pxe_method = BootMethodRegistry.get_item('pxe')
67 if pxe_method is not None:77 if pxe_method is not None:
68 output += PXE_BOOTLOADER.format(78 output += tempita.sub(
69 bootloader=pxe_method.bootloader_path).strip()79 PXE_BOOTLOADER,
80 bootloader=pxe_method.bootloader_path,
81 path_prefix=pxe_method.path_prefix,
82 ).strip()
70 return output.strip()83 return output.strip()
7184
7285
7386
=== modified file 'src/provisioningserver/driver/__init__.py'
--- src/provisioningserver/driver/__init__.py 2014-05-15 17:39:27 +0000
+++ src/provisioningserver/driver/__init__.py 2014-06-02 20:19:07 +0000
@@ -139,7 +139,14 @@
139 Architecture(139 Architecture(
140 name="armhf/generic", description="armhf/generic",140 name="armhf/generic", description="armhf/generic",
141 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),141 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),
142 Architecture(name="ppc64el/generic", description="ppc64el"),142 # PPC64EL needs a rootdelay for PowerNV. The disk controller
143 # in the hardware, takes a little bit longer to come up then
144 # the initrd wants to wait. Set this to 60 seconds, just to
145 # give the booting machine enough time. This doesn't slow down
146 # the booting process, it just increases the timeout.
147 Architecture(
148 name="ppc64el/generic", description="ppc64el",
149 kernel_options=['rootdelay=60']),
143]150]
144for arch in builtin_architectures:151for arch in builtin_architectures:
145 ArchitectureRegistry.register_item(arch.name, arch)152 ArchitectureRegistry.register_item(arch.name, arch)
146153
=== modified file 'src/provisioningserver/utils/__init__.py'
--- src/provisioningserver/utils/__init__.py 2014-05-06 21:18:17 +0000
+++ src/provisioningserver/utils/__init__.py 2014-06-02 20:19:07 +0000
@@ -829,3 +829,24 @@
829 if len(columns) == 5 and columns[2] == mac:829 if len(columns) == 5 and columns[2] == mac:
830 return columns[0]830 return columns[0]
831 return None831 return None
832
833
834def find_mac_via_arp(ip):
835 """Find the MAC address for `ip` by reading the output of arp -n.
836
837 Returns `None` if the IP is not found.
838
839 We do this because we aren't necessarily the only DHCP server on the
840 network, so we can't check our own leases file and be guaranteed to find an
841 IP that matches.
842
843 :param ip: The ip address, e.g. '192.168.1.1'.
844 """
845
846 output = call_capture_and_check(['arp', '-n']).split('\n')
847
848 for line in sorted(output):
849 columns = line.split()
850 if len(columns) == 5 and columns[0] == ip:
851 return columns[2]
852 return None
832853
=== modified file 'src/provisioningserver/utils/tests/test_utils.py'
--- src/provisioningserver/utils/tests/test_utils.py 2014-05-06 21:18:17 +0000
+++ src/provisioningserver/utils/tests/test_utils.py 2014-06-02 20:19:07 +0000
@@ -75,6 +75,7 @@
75 ExternalProcessError,75 ExternalProcessError,
76 filter_dict,76 filter_dict,
77 find_ip_via_arp,77 find_ip_via_arp,
78 find_mac_via_arp,
78 get_all_interface_addresses,79 get_all_interface_addresses,
79 get_mtime,80 get_mtime,
80 incremental_write,81 incremental_write,
@@ -1322,6 +1323,50 @@
1322 self.assertEqual("192.168.0.1", ip_address_observed)1323 self.assertEqual("192.168.0.1", ip_address_observed)
13231324
13241325
1326class TestFindMACViaARP(MAASTestCase):
1327
1328 def patch_call(self, output):
1329 """Replace `call_capture_and_check` with one that returns `output`."""
1330 fake = self.patch(provisioningserver.utils, 'call_capture_and_check')
1331 fake.return_value = output
1332 return fake
1333
1334 def test__resolves_IP_address_to_MAC(self):
1335 sample = """\
1336 Address HWtype HWaddress Flags Mask Iface
1337 192.168.100.20 (incomplete) virbr1
1338 192.168.0.104 (incomplete) eth0
1339 192.168.0.5 (incomplete) eth0
1340 192.168.0.2 (incomplete) eth0
1341 192.168.0.100 (incomplete) eth0
1342 192.168.122.20 ether 52:54:00:02:86:4b C virbr0
1343 192.168.0.4 (incomplete) eth0
1344 192.168.0.1 ether 90:f6:52:f6:17:92 C eth0
1345 """
1346
1347 call_capture_and_check = self.patch_call(sample)
1348 mac_address_observed = find_mac_via_arp("192.168.122.20")
1349 self.assertThat(
1350 call_capture_and_check,
1351 MockCalledOnceWith(['arp', '-n']))
1352 self.assertEqual("52:54:00:02:86:4b", mac_address_observed)
1353
1354 def test__returns_consistent_output(self):
1355 ip = factory.getRandomIPAddress()
1356 macs = [
1357 '52:54:00:02:86:4b',
1358 '90:f6:52:f6:17:92',
1359 ]
1360 lines = ['%s ether %s C eth0' % (ip, mac) for mac in macs]
1361 self.patch_call('\n'.join(lines))
1362 one_result = find_mac_via_arp(ip)
1363 self.patch_call('\n'.join(reversed(lines)))
1364 other_result = find_mac_via_arp(ip)
1365
1366 self.assertIn(one_result, macs)
1367 self.assertEqual(one_result, other_result)
1368
1369
1325class TestAsynchronousDecorator(MAASTestCase):1370class TestAsynchronousDecorator(MAASTestCase):
13261371
1327 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)1372 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)

Subscribers

People subscribed via source and target branches

to all changes: