1
# Copyright 2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Tests for `provisioningserver.boot.powernv`."""
6
from __future__ import (
20
from maastesting.factory import factory
21
from maastesting.testcase import MAASTestCase
22
from provisioningserver.boot import BytesReader
23
from provisioningserver.boot.powernv import (
29
from provisioningserver.boot.tests.test_pxe import parse_pxe_config
30
from provisioningserver.boot.tftppath import compose_image_path
31
from provisioningserver.testing.config import set_tftp_root
32
from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
33
from provisioningserver.tftp import TFTPBackend
34
from testtools.matchers import (
43
def compose_config_path(mac):
44
"""Compose the TFTP path for a PowerNV PXE configuration file.
46
The path returned is relative to the TFTP root, as it would be
47
identified by clients on the network.
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
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)
62
def get_example_path_and_components():
63
"""Return a plausible path and its components.
65
The path is intended to match `re_config_file`, and the components are
66
the expected groups from a match.
68
components = {"mac": factory.getRandomMACAddress("-")}
69
config_path = compose_config_path(components["mac"])
70
return config_path, components
73
class TestPowerNVBootMethod(MAASTestCase):
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))
81
def test_compose_config_path_follows_maas_pxe_directory_layout(self):
82
name = factory.make_name('config')
84
'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name),
85
compose_config_path(name))
87
def test_compose_config_path_does_not_include_tftp_root(self):
88
tftproot = self.make_tftp_root()
89
name = factory.make_name('config')
91
compose_config_path(name),
92
Not(StartsWith(tftproot)))
94
def test_bootloader_path(self):
95
method = PowerNVBootMethod()
96
self.assertEqual('pxelinux.0', method.bootloader_path)
98
def test_bootloader_path_does_not_include_tftp_root(self):
99
tftproot = self.make_tftp_root()
100
method = PowerNVBootMethod()
102
method.bootloader_path,
103
Not(StartsWith(tftproot)))
106
method = PowerNVBootMethod()
107
self.assertEqual('powernv', method.name)
109
def test_template_subdir(self):
110
method = PowerNVBootMethod()
111
self.assertEqual('pxe', method.template_subdir)
113
def test_arch_octet(self):
114
method = PowerNVBootMethod()
115
self.assertEqual('00:0E', method.arch_octet)
117
def test_path_prefix(self):
118
method = PowerNVBootMethod()
119
self.assertEqual('ppc64el/', method.path_prefix)
122
class TestPowerNVBootMethodMatchPath(MAASTestCase):
124
`provisioningserver.boot.powernv.PowerNVBootMethod.match_path`.
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)
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)
144
self.assertEqual(expected, params)
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)
157
self.assertEqual(expected, params)
160
class TestPowerNVBootMethodRenderConfig(MAASTestCase):
162
`provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`
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)
184
r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir),
185
re.MULTILINE | re.DOTALL),
187
r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir),
188
re.MULTILINE | re.DOTALL),
190
r'.*^\s+APPEND .+?$',
191
re.MULTILINE | re.DOTALL)))
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()
198
"kernel_params": make_kernel_parameters(self, purpose="install"),
200
# Capture the output before sprinking in some random options.
201
output_before = method.get_reader(**options).read(10000)
202
# Sprinkle some magic in.
204
(factory.make_name("name"), factory.make_name("value"))
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)
211
def test_get_reader_with_local_purpose(self):
212
# If purpose is "local", output should be empty string.
213
method = PowerNVBootMethod()
216
"kernel_params": make_kernel_parameters(purpose="local"),
218
output = method.get_reader(**options).read(10000)
219
self.assertIn("", output)
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'])
233
class TestPowerNVBootMethodPathPrefix(MAASTestCase):
235
`provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`.
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()
246
'kernel_params': make_kernel_parameters(),
247
'path': 'ppc64el/example',
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))
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()
265
'kernel_params': make_kernel_parameters(),
266
'path': 'ppc64el/ppc64el/example',
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))
275
class TestPowerNVBootMethodRegex(MAASTestCase):
277
`provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`.
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())
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())
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())
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())
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())
325
def test_re_config_file_does_not_match_non_config_file(self):
326
self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel'))
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'))
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'))
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())