Merge lp:~newell-jensen/maas/fix-1596046 into lp:~maas-committers/maas/trunk

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: no longer in the source branch.
Merged at revision: 5149
Proposed branch: lp:~newell-jensen/maas/fix-1596046
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 264 lines (+94/-48)
4 files modified
src/provisioningserver/boot/powernv.py (+28/-16)
src/provisioningserver/boot/tests/test_powernv.py (+29/-29)
src/provisioningserver/pserv_services/tests/test_tftp.py (+25/-0)
src/provisioningserver/pserv_services/tftp.py (+12/-3)
To merge this branch: bzr merge lp:~newell-jensen/maas/fix-1596046
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+298690@code.launchpad.net

Commit message

This branch fixes a regression for powerNV where the "b'ppc64el/" was getting hardcoded into the dhcpd.conf. Additionally, it adds `path` to GetBootConfig (new in MAAS 2.0) on the rack, which was left out and needed.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

This was tested on powerNV hardware in 1ss. The nodes are now able to enlist, commission, and deploy with this fix.

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

m.rpc.boot.get_config, the eventual receiver of the added "path"
argument, doesn't actually use it.

I'm about 95% sure that it should not be passed. The arguments to
GetBootConfig are those _derived_ from the TFTP request; a raw path
should not be amongst them.

I think the bug is that PowerNVBootMethod.get_params() should not be
returning a dict with a "path" key. What do you think?

review: Needs Information
Revision history for this message
Newell Jensen (newell-jensen) wrote :

After looking at it, I agree. I didn't originally write powernv.py but
when I looke through the other boot methods, I don't see them passing
"path" to the paraemters. Will change this and test again.

On Wed, Jun 29, 2016 at 11:41 AM, Gavin Panella <<email address hidden>
> wrote:

> Review: Needs Information
>
> m.rpc.boot.get_config, the eventual receiver of the added "path"
> argument, doesn't actually use it.
>
> I'm about 95% sure that it should not be passed. The arguments to
> GetBootConfig are those _derived_ from the TFTP request; a raw path
> should not be amongst them.
>
> I think the bug is that PowerNVBootMethod.get_params() should not be
> returning a dict with a "path" key. What do you think?
>
> --
> https://code.launchpad.net/~newell-jensen/maas/fix-1596046/+merge/298690
> You are the owner of lp:~newell-jensen/maas/fix-1596046.
>

Revision history for this message
Newell Jensen (newell-jensen) wrote :

Gavin,

So powerNV needs 'path' for its get_reader method. However, I was able to strip the need of modifying the RPC calls. Have another look. Thanks.

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

This looks good but I suggest solving it by selecting arguments to pass to GetBootConfig instead of killing off errant keys.

PowerNV also trims the prefix in two different places, which seems wasteful, and confusing when reading the code.

I've fixed both of these in lp:~allenap/maas/newell-jensen--fix-1596046. If you pull that into your branch you can see what I've fixed in the last couple of revisions.

Revision history for this message
Newell Jensen (newell-jensen) wrote :

Gavin,

Merged your branch, tested it on the hardware, fixed the issues, and pushed again. Have a look again when you can, thanks.

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

Looks good. I'm glad you figured out why it wasn't working. One comment to check out; it won't hold you up long.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/boot/powernv.py'
--- src/provisioningserver/boot/powernv.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/boot/powernv.py 2016-06-30 19:11:09 +0000
@@ -49,6 +49,20 @@
49re_config_file = re_config_file.encode("ascii")49re_config_file = re_config_file.encode("ascii")
50re_config_file = re.compile(re_config_file, re.VERBOSE)50re_config_file = re.compile(re_config_file, re.VERBOSE)
5151
52# Due to the "ppc64el" prefix all files requested from the client using
53# relative paths will have that prefix. Capturing the path after that prefix
54# will give us the correct path in the local tftp root on disk.
55re_other_file = r'''
56 # Optional leading slash(es).
57 ^/*
58 ppc64el # PowerNV PXE prefix, set by dhcpd.
59 /
60 (?P<path>.+) # Capture path.
61 $
62'''
63re_other_file = re_other_file.encode("ascii")
64re_other_file = re.compile(re_other_file, re.VERBOSE)
65
5266
53def format_bootif(mac):67def format_bootif(mac):
54 """Formats a mac address into the BOOTIF format, expected by68 """Formats a mac address into the BOOTIF format, expected by
@@ -65,15 +79,16 @@
65 template_subdir = "pxe"79 template_subdir = "pxe"
66 bootloader_path = "pxelinux.0"80 bootloader_path = "pxelinux.0"
67 arch_octet = "00:0E"81 arch_octet = "00:0E"
68 path_prefix = b"ppc64el/"82 path_prefix = "ppc64el/"
6983
70 def get_params(self, backend, path):84 def get_params(self, backend, path):
71 """Gets the matching parameters from the requested path."""85 """Gets the matching parameters from the requested path."""
72 match = re_config_file.match(path)86 match = re_config_file.match(path)
73 if match is not None:87 if match is not None:
74 return get_parameters(match)88 return get_parameters(match)
75 if path.lstrip(b'/').startswith(self.path_prefix):89 match = re_other_file.match(path)
76 return {'path': path}90 if match is not None:
91 return get_parameters(match)
77 return None92 return None
7893
79 def match_path(self, backend, path):94 def match_path(self, backend, path):
@@ -94,23 +109,22 @@
94 params['mac'] = mac109 params['mac'] = mac
95 return params110 return params
96111
97 def get_reader(self, backend, kernel_params, **extra):112 def get_reader(self, backend, kernel_params, mac=None, path=None, **extra):
98 """Render a configuration file as a unicode string.113 """Render a configuration file as a unicode string.
99114
100 :param backend: requesting backend115 :param backend: requesting backend
101 :param kernel_params: An instance of `KernelParameters`.116 :param kernel_params: An instance of `KernelParameters`.
117 :param path: Optional MAC address discovered by `match_path`.
118 :param path: Optional path discovered by `match_path`.
102 :param extra: Allow for other arguments. This is a safety valve;119 :param extra: Allow for other arguments. This is a safety valve;
103 parameters generated in another component (for example, see120 parameters generated in another component (for example, see
104 `TFTPBackend.get_config_reader`) won't cause this to break.121 `TFTPBackend.get_config_reader`) won't cause this to break.
105 """122 """
106 # Due to the path prefix, all requested files from the client will123 if path is not None:
107 # contain that prefix. Removing the prefix from the path will return124 # This is a request for a static file, not a configuration file.
108 # the correct path in the tftp root.125 # The prefix was already trimmed by `match_path` so we need only
109 if 'path' in extra:126 # return a FilesystemReader for `path` beneath the backend's base.
110 path = extra['path']127 target_path = backend.base.descendant(path.split('/'))
111 if path.startswith(self.path_prefix):
112 path = path[len(self.path_prefix):]
113 target_path = backend.base.descendant(path.split(b'/'))
114 return FilesystemReader(target_path)128 return FilesystemReader(target_path)
115129
116 # Return empty config for PowerNV local. PowerNV fails to130 # Return empty config for PowerNV local. PowerNV fails to
@@ -128,10 +142,8 @@
128 # support the IPAPPEND pxelinux flag.142 # support the IPAPPEND pxelinux flag.
129 def kernel_command(params):143 def kernel_command(params):
130 cmd_line = compose_kernel_command_line(params)144 cmd_line = compose_kernel_command_line(params)
131 if 'mac' in extra:145 if mac is not None:
132 mac = extra['mac']146 return '%s BOOTIF=%s' % (cmd_line, format_bootif(mac))
133 mac = format_bootif(mac)
134 return '%s BOOTIF=%s' % (cmd_line, mac)
135 return cmd_line147 return cmd_line
136148
137 namespace['kernel_command'] = kernel_command149 namespace['kernel_command'] = kernel_command
138150
=== modified file 'src/provisioningserver/boot/tests/test_powernv.py'
--- src/provisioningserver/boot/tests/test_powernv.py 2016-06-22 17:03:02 +0000
+++ src/provisioningserver/boot/tests/test_powernv.py 2016-06-30 19:11:09 +0000
@@ -5,7 +5,6 @@
55
6__all__ = []6__all__ = []
77
8import os
9import re8import re
10from unittest.mock import Mock9from unittest.mock import Mock
1110
@@ -114,7 +113,7 @@
114113
115 def test_path_prefix(self):114 def test_path_prefix(self):
116 method = PowerNVBootMethod()115 method = PowerNVBootMethod()
117 self.assertEqual(b'ppc64el/', method.path_prefix)116 self.assertEqual('ppc64el/', method.path_prefix)
118117
119118
120class TestPowerNVBootMethodMatchPath(MAASTestCase):119class TestPowerNVBootMethodMatchPath(MAASTestCase):
@@ -150,7 +149,8 @@
150 expected = {149 expected = {
151 'arch': 'ppc64el',150 'arch': 'ppc64el',
152 'mac': fake_mac,151 'mac': fake_mac,
153 'path': file_path,152 # The "ppc64el/" prefix has been removed from the path.
153 'path': file_path.decode("utf-8")[8:],
154 }154 }
155 self.assertEqual(expected, params)155 self.assertEqual(expected, params)
156156
@@ -242,41 +242,41 @@
242242
243243
244class TestPowerNVBootMethodPathPrefix(MAASTestCase):244class TestPowerNVBootMethodPathPrefix(MAASTestCase):
245 """Tests for245 """Tests for `provisioningserver.boot.powernv.PowerNVBootMethod`."""
246 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`.246
247 """247 def test_path_prefix_removed(self):
248248 temp_dir = FilePath(self.make_dir())
249 def test_get_reader_path_prefix(self):249 backend = Mock(base=temp_dir) # A `TFTPBackend`.
250
251 # Create a file in the backend's base directory.
250 data = factory.make_string().encode("ascii")252 data = factory.make_string().encode("ascii")
251 temp_file = self.make_file(name="example", contents=data)253 temp_file = temp_dir.child("example")
252 temp_dir = os.path.dirname(temp_file)254 temp_file.setContent(data)
253 backend = Mock(base=FilePath(temp_dir)) # A `TFTPBackend`.255
254 method = PowerNVBootMethod()256 method = PowerNVBootMethod()
255 options = {257 params = method.get_params(backend, b"ppc64el/example")
256 'backend': backend,258 self.assertEqual({"path": "example"}, params)
257 'kernel_params': make_kernel_parameters(),259 reader = method.get_reader(backend, make_kernel_parameters(), **params)
258 'path': b'ppc64el/example',
259 }
260 reader = method.get_reader(**options)
261 self.addCleanup(reader.finish)260 self.addCleanup(reader.finish)
262 self.assertEqual(len(data), reader.size)261 self.assertEqual(len(data), reader.size)
263 self.assertEqual(data, reader.read(len(data)))262 self.assertEqual(data, reader.read(len(data)))
264 self.assertEqual(b"", reader.read(1))263 self.assertEqual(b"", reader.read(1))
265264
266 def test_get_reader_path_prefix_only_removes_first_occurrence(self):265 def test_path_prefix_only_first_occurrence_removed(self):
266 temp_dir = FilePath(self.make_dir())
267 backend = Mock(base=temp_dir) # A `TFTPBackend`.
268
269 # Create a file nested within a "ppc64el" directory.
267 data = factory.make_string().encode("ascii")270 data = factory.make_string().encode("ascii")
268 temp_dir = self.make_dir()271 temp_subdir = temp_dir.child("ppc64el")
269 temp_subdir = os.path.join(temp_dir, 'ppc64el')272 temp_subdir.createDirectory()
270 os.mkdir(temp_subdir)273 temp_file = temp_subdir.child("example")
271 factory.make_file(temp_subdir, "example", data)274 temp_file.setContent(data)
272 backend = Mock(base=FilePath(temp_dir)) # A `TFTPBackend`.275
273 method = PowerNVBootMethod()276 method = PowerNVBootMethod()
274 options = {277 params = method.get_params(backend, b"ppc64el/ppc64el/example")
275 'backend': backend,278 self.assertEqual({"path": "ppc64el/example"}, params)
276 'kernel_params': make_kernel_parameters(),279 reader = method.get_reader(backend, make_kernel_parameters(), **params)
277 'path': b'ppc64el/ppc64el/example',
278 }
279 reader = method.get_reader(**options)
280 self.addCleanup(reader.finish)280 self.addCleanup(reader.finish)
281 self.assertEqual(len(data), reader.size)281 self.assertEqual(len(data), reader.size)
282 self.assertEqual(data, reader.read(len(data)))282 self.assertEqual(data, reader.read(len(data)))
283283
=== modified file 'src/provisioningserver/pserv_services/tests/test_tftp.py'
--- src/provisioningserver/pserv_services/tests/test_tftp.py 2016-05-12 19:07:37 +0000
+++ src/provisioningserver/pserv_services/tests/test_tftp.py 2016-06-30 19:11:09 +0000
@@ -49,6 +49,7 @@
49 UDPServer,49 UDPServer,
50)50)
51from provisioningserver.rpc.exceptions import BootConfigNoResponse51from provisioningserver.rpc.exceptions import BootConfigNoResponse
52from provisioningserver.rpc.region import GetBootConfig
52from provisioningserver.testing.boot_images import (53from provisioningserver.testing.boot_images import (
53 make_boot_image_params,54 make_boot_image_params,
54 make_image,55 make_image,
@@ -504,6 +505,30 @@
504 # ArchitectureRegistry is not stable, so we permit either here.505 # ArchitectureRegistry is not stable, so we permit either here.
505 self.assertIn(observed_params["arch"], ["armhf", "arm64"])506 self.assertIn(observed_params["arch"], ["armhf", "arm64"])
506507
508 def test_get_kernel_params_filters_out_unnecessary_arguments(self):
509 params_okay = {
510 name.decode("ascii"): factory.make_name("value")
511 for name, _ in GetBootConfig.arguments
512 }
513 params_other = {
514 factory.make_name("name"): factory.make_name("value")
515 for _ in range(3)
516 }
517 params_all = params_okay.copy()
518 params_all.update(params_other)
519
520 client_service = Mock()
521 client = client_service.getClient.return_value
522 client.localIdent = params_okay["system_id"]
523 backend = TFTPBackend(self.make_dir(), client_service)
524 backend.fetcher = Mock()
525
526 backend.get_kernel_params(params_all)
527
528 self.assertThat(
529 backend.fetcher, MockCalledOnceWith(
530 client, GetBootConfig, **params_okay))
531
507532
508class TestTFTPService(MAASTestCase):533class TestTFTPService(MAASTestCase):
509534
510535
=== modified file 'src/provisioningserver/pserv_services/tftp.py'
--- src/provisioningserver/pserv_services/tftp.py 2016-03-28 13:54:47 +0000
+++ src/provisioningserver/pserv_services/tftp.py 2016-06-30 19:11:09 +0000
@@ -204,6 +204,17 @@
204 path requested.204 path requested.
205 :return: A `KernelParameters` instance.205 :return: A `KernelParameters` instance.
206 """206 """
207 # Extract from params only those arguments that GetBootConfig cares
208 # about; params is a context-like object and other stuff (too much?)
209 # gets in there.
210 arguments = (
211 name.decode("ascii")
212 for name, _ in GetBootConfig.arguments
213 )
214 params = {
215 name: params[name] for name in arguments
216 if name in params
217 }
207 client = self.client_service.getClient()218 client = self.client_service.getClient()
208 params["system_id"] = client.localIdent219 params["system_id"] = client.localIdent
209 d = self.fetcher(client, GetBootConfig, **params)220 d = self.fetcher(client, GetBootConfig, **params)
@@ -223,9 +234,7 @@
223 return boot_method.get_reader(234 return boot_method.get_reader(
224 self, kernel_params=kernel_params, **params)235 self, kernel_params=kernel_params, **params)
225236
226 d = self.get_kernel_params(params)237 return self.get_kernel_params(params).addCallback(generate)
227 d.addCallback(generate)
228 return d
229238
230 @staticmethod239 @staticmethod
231 def no_response_errback(failure, file_name):240 def no_response_errback(failure, file_name):