Merge lp:~newell-jensen/maas/fix-1596046 into lp:~maas-committers/maas/trunk
- fix-1596046
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Description of the change
Newell Jensen (newell-jensen) wrote : | # |
Gavin Panella (allenap) wrote : | # |
m.rpc.boot.
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 PowerNVBootMeth
returning a dict with a "path" key. What do you think?
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.
> 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 PowerNVBootMeth
> returning a dict with a "path" key. What do you think?
>
> --
> https:/
> You are the owner of lp:~newell-jensen/maas/fix-1596046.
>
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.
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.
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.
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.
Preview Diff
1 | === modified file 'src/provisioningserver/boot/powernv.py' | |||
2 | --- src/provisioningserver/boot/powernv.py 2016-03-28 13:54:47 +0000 | |||
3 | +++ src/provisioningserver/boot/powernv.py 2016-06-30 19:11:09 +0000 | |||
4 | @@ -49,6 +49,20 @@ | |||
5 | 49 | re_config_file = re_config_file.encode("ascii") | 49 | re_config_file = re_config_file.encode("ascii") |
6 | 50 | re_config_file = re.compile(re_config_file, re.VERBOSE) | 50 | re_config_file = re.compile(re_config_file, re.VERBOSE) |
7 | 51 | 51 | ||
8 | 52 | # Due to the "ppc64el" prefix all files requested from the client using | ||
9 | 53 | # relative paths will have that prefix. Capturing the path after that prefix | ||
10 | 54 | # will give us the correct path in the local tftp root on disk. | ||
11 | 55 | re_other_file = r''' | ||
12 | 56 | # Optional leading slash(es). | ||
13 | 57 | ^/* | ||
14 | 58 | ppc64el # PowerNV PXE prefix, set by dhcpd. | ||
15 | 59 | / | ||
16 | 60 | (?P<path>.+) # Capture path. | ||
17 | 61 | $ | ||
18 | 62 | ''' | ||
19 | 63 | re_other_file = re_other_file.encode("ascii") | ||
20 | 64 | re_other_file = re.compile(re_other_file, re.VERBOSE) | ||
21 | 65 | |||
22 | 52 | 66 | ||
23 | 53 | def format_bootif(mac): | 67 | def format_bootif(mac): |
24 | 54 | """Formats a mac address into the BOOTIF format, expected by | 68 | """Formats a mac address into the BOOTIF format, expected by |
25 | @@ -65,15 +79,16 @@ | |||
26 | 65 | template_subdir = "pxe" | 79 | template_subdir = "pxe" |
27 | 66 | bootloader_path = "pxelinux.0" | 80 | bootloader_path = "pxelinux.0" |
28 | 67 | arch_octet = "00:0E" | 81 | arch_octet = "00:0E" |
30 | 68 | path_prefix = b"ppc64el/" | 82 | path_prefix = "ppc64el/" |
31 | 69 | 83 | ||
32 | 70 | def get_params(self, backend, path): | 84 | def get_params(self, backend, path): |
33 | 71 | """Gets the matching parameters from the requested path.""" | 85 | """Gets the matching parameters from the requested path.""" |
34 | 72 | match = re_config_file.match(path) | 86 | match = re_config_file.match(path) |
35 | 73 | if match is not None: | 87 | if match is not None: |
36 | 74 | return get_parameters(match) | 88 | return get_parameters(match) |
39 | 75 | if path.lstrip(b'/').startswith(self.path_prefix): | 89 | match = re_other_file.match(path) |
40 | 76 | return {'path': path} | 90 | if match is not None: |
41 | 91 | return get_parameters(match) | ||
42 | 77 | return None | 92 | return None |
43 | 78 | 93 | ||
44 | 79 | def match_path(self, backend, path): | 94 | def match_path(self, backend, path): |
45 | @@ -94,23 +109,22 @@ | |||
46 | 94 | params['mac'] = mac | 109 | params['mac'] = mac |
47 | 95 | return params | 110 | return params |
48 | 96 | 111 | ||
50 | 97 | def get_reader(self, backend, kernel_params, **extra): | 112 | def get_reader(self, backend, kernel_params, mac=None, path=None, **extra): |
51 | 98 | """Render a configuration file as a unicode string. | 113 | """Render a configuration file as a unicode string. |
52 | 99 | 114 | ||
53 | 100 | :param backend: requesting backend | 115 | :param backend: requesting backend |
54 | 101 | :param kernel_params: An instance of `KernelParameters`. | 116 | :param kernel_params: An instance of `KernelParameters`. |
55 | 117 | :param path: Optional MAC address discovered by `match_path`. | ||
56 | 118 | :param path: Optional path discovered by `match_path`. | ||
57 | 102 | :param extra: Allow for other arguments. This is a safety valve; | 119 | :param extra: Allow for other arguments. This is a safety valve; |
58 | 103 | parameters generated in another component (for example, see | 120 | parameters generated in another component (for example, see |
59 | 104 | `TFTPBackend.get_config_reader`) won't cause this to break. | 121 | `TFTPBackend.get_config_reader`) won't cause this to break. |
60 | 105 | """ | 122 | """ |
69 | 106 | # Due to the path prefix, all requested files from the client will | 123 | if path is not None: |
70 | 107 | # contain that prefix. Removing the prefix from the path will return | 124 | # This is a request for a static file, not a configuration file. |
71 | 108 | # the correct path in the tftp root. | 125 | # The prefix was already trimmed by `match_path` so we need only |
72 | 109 | if 'path' in extra: | 126 | # return a FilesystemReader for `path` beneath the backend's base. |
73 | 110 | path = extra['path'] | 127 | target_path = backend.base.descendant(path.split('/')) |
66 | 111 | if path.startswith(self.path_prefix): | ||
67 | 112 | path = path[len(self.path_prefix):] | ||
68 | 113 | target_path = backend.base.descendant(path.split(b'/')) | ||
74 | 114 | return FilesystemReader(target_path) | 128 | return FilesystemReader(target_path) |
75 | 115 | 129 | ||
76 | 116 | # Return empty config for PowerNV local. PowerNV fails to | 130 | # Return empty config for PowerNV local. PowerNV fails to |
77 | @@ -128,10 +142,8 @@ | |||
78 | 128 | # support the IPAPPEND pxelinux flag. | 142 | # support the IPAPPEND pxelinux flag. |
79 | 129 | def kernel_command(params): | 143 | def kernel_command(params): |
80 | 130 | cmd_line = compose_kernel_command_line(params) | 144 | cmd_line = compose_kernel_command_line(params) |
85 | 131 | if 'mac' in extra: | 145 | if mac is not None: |
86 | 132 | mac = extra['mac'] | 146 | return '%s BOOTIF=%s' % (cmd_line, format_bootif(mac)) |
83 | 133 | mac = format_bootif(mac) | ||
84 | 134 | return '%s BOOTIF=%s' % (cmd_line, mac) | ||
87 | 135 | return cmd_line | 147 | return cmd_line |
88 | 136 | 148 | ||
89 | 137 | namespace['kernel_command'] = kernel_command | 149 | namespace['kernel_command'] = kernel_command |
90 | 138 | 150 | ||
91 | === modified file 'src/provisioningserver/boot/tests/test_powernv.py' | |||
92 | --- src/provisioningserver/boot/tests/test_powernv.py 2016-06-22 17:03:02 +0000 | |||
93 | +++ src/provisioningserver/boot/tests/test_powernv.py 2016-06-30 19:11:09 +0000 | |||
94 | @@ -5,7 +5,6 @@ | |||
95 | 5 | 5 | ||
96 | 6 | __all__ = [] | 6 | __all__ = [] |
97 | 7 | 7 | ||
98 | 8 | import os | ||
99 | 9 | import re | 8 | import re |
100 | 10 | from unittest.mock import Mock | 9 | from unittest.mock import Mock |
101 | 11 | 10 | ||
102 | @@ -114,7 +113,7 @@ | |||
103 | 114 | 113 | ||
104 | 115 | def test_path_prefix(self): | 114 | def test_path_prefix(self): |
105 | 116 | method = PowerNVBootMethod() | 115 | method = PowerNVBootMethod() |
107 | 117 | self.assertEqual(b'ppc64el/', method.path_prefix) | 116 | self.assertEqual('ppc64el/', method.path_prefix) |
108 | 118 | 117 | ||
109 | 119 | 118 | ||
110 | 120 | class TestPowerNVBootMethodMatchPath(MAASTestCase): | 119 | class TestPowerNVBootMethodMatchPath(MAASTestCase): |
111 | @@ -150,7 +149,8 @@ | |||
112 | 150 | expected = { | 149 | expected = { |
113 | 151 | 'arch': 'ppc64el', | 150 | 'arch': 'ppc64el', |
114 | 152 | 'mac': fake_mac, | 151 | 'mac': fake_mac, |
116 | 153 | 'path': file_path, | 152 | # The "ppc64el/" prefix has been removed from the path. |
117 | 153 | 'path': file_path.decode("utf-8")[8:], | ||
118 | 154 | } | 154 | } |
119 | 155 | self.assertEqual(expected, params) | 155 | self.assertEqual(expected, params) |
120 | 156 | 156 | ||
121 | @@ -242,41 +242,41 @@ | |||
122 | 242 | 242 | ||
123 | 243 | 243 | ||
124 | 244 | class TestPowerNVBootMethodPathPrefix(MAASTestCase): | 244 | class TestPowerNVBootMethodPathPrefix(MAASTestCase): |
130 | 245 | """Tests for | 245 | """Tests for `provisioningserver.boot.powernv.PowerNVBootMethod`.""" |
131 | 246 | `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`. | 246 | |
132 | 247 | """ | 247 | def test_path_prefix_removed(self): |
133 | 248 | 248 | temp_dir = FilePath(self.make_dir()) | |
134 | 249 | def test_get_reader_path_prefix(self): | 249 | backend = Mock(base=temp_dir) # A `TFTPBackend`. |
135 | 250 | |||
136 | 251 | # Create a file in the backend's base directory. | ||
137 | 250 | data = factory.make_string().encode("ascii") | 252 | data = factory.make_string().encode("ascii") |
141 | 251 | temp_file = self.make_file(name="example", contents=data) | 253 | temp_file = temp_dir.child("example") |
142 | 252 | temp_dir = os.path.dirname(temp_file) | 254 | temp_file.setContent(data) |
143 | 253 | backend = Mock(base=FilePath(temp_dir)) # A `TFTPBackend`. | 255 | |
144 | 254 | method = PowerNVBootMethod() | 256 | method = PowerNVBootMethod() |
151 | 255 | options = { | 257 | params = method.get_params(backend, b"ppc64el/example") |
152 | 256 | 'backend': backend, | 258 | self.assertEqual({"path": "example"}, params) |
153 | 257 | 'kernel_params': make_kernel_parameters(), | 259 | reader = method.get_reader(backend, make_kernel_parameters(), **params) |
148 | 258 | 'path': b'ppc64el/example', | ||
149 | 259 | } | ||
150 | 260 | reader = method.get_reader(**options) | ||
154 | 261 | self.addCleanup(reader.finish) | 260 | self.addCleanup(reader.finish) |
155 | 262 | self.assertEqual(len(data), reader.size) | 261 | self.assertEqual(len(data), reader.size) |
156 | 263 | self.assertEqual(data, reader.read(len(data))) | 262 | self.assertEqual(data, reader.read(len(data))) |
157 | 264 | self.assertEqual(b"", reader.read(1)) | 263 | self.assertEqual(b"", reader.read(1)) |
158 | 265 | 264 | ||
160 | 266 | def test_get_reader_path_prefix_only_removes_first_occurrence(self): | 265 | def test_path_prefix_only_first_occurrence_removed(self): |
161 | 266 | temp_dir = FilePath(self.make_dir()) | ||
162 | 267 | backend = Mock(base=temp_dir) # A `TFTPBackend`. | ||
163 | 268 | |||
164 | 269 | # Create a file nested within a "ppc64el" directory. | ||
165 | 267 | data = factory.make_string().encode("ascii") | 270 | data = factory.make_string().encode("ascii") |
171 | 268 | temp_dir = self.make_dir() | 271 | temp_subdir = temp_dir.child("ppc64el") |
172 | 269 | temp_subdir = os.path.join(temp_dir, 'ppc64el') | 272 | temp_subdir.createDirectory() |
173 | 270 | os.mkdir(temp_subdir) | 273 | temp_file = temp_subdir.child("example") |
174 | 271 | factory.make_file(temp_subdir, "example", data) | 274 | temp_file.setContent(data) |
175 | 272 | backend = Mock(base=FilePath(temp_dir)) # A `TFTPBackend`. | 275 | |
176 | 273 | method = PowerNVBootMethod() | 276 | method = PowerNVBootMethod() |
183 | 274 | options = { | 277 | params = method.get_params(backend, b"ppc64el/ppc64el/example") |
184 | 275 | 'backend': backend, | 278 | self.assertEqual({"path": "ppc64el/example"}, params) |
185 | 276 | 'kernel_params': make_kernel_parameters(), | 279 | reader = method.get_reader(backend, make_kernel_parameters(), **params) |
180 | 277 | 'path': b'ppc64el/ppc64el/example', | ||
181 | 278 | } | ||
182 | 279 | reader = method.get_reader(**options) | ||
186 | 280 | self.addCleanup(reader.finish) | 280 | self.addCleanup(reader.finish) |
187 | 281 | self.assertEqual(len(data), reader.size) | 281 | self.assertEqual(len(data), reader.size) |
188 | 282 | self.assertEqual(data, reader.read(len(data))) | 282 | self.assertEqual(data, reader.read(len(data))) |
189 | 283 | 283 | ||
190 | === modified file 'src/provisioningserver/pserv_services/tests/test_tftp.py' | |||
191 | --- src/provisioningserver/pserv_services/tests/test_tftp.py 2016-05-12 19:07:37 +0000 | |||
192 | +++ src/provisioningserver/pserv_services/tests/test_tftp.py 2016-06-30 19:11:09 +0000 | |||
193 | @@ -49,6 +49,7 @@ | |||
194 | 49 | UDPServer, | 49 | UDPServer, |
195 | 50 | ) | 50 | ) |
196 | 51 | from provisioningserver.rpc.exceptions import BootConfigNoResponse | 51 | from provisioningserver.rpc.exceptions import BootConfigNoResponse |
197 | 52 | from provisioningserver.rpc.region import GetBootConfig | ||
198 | 52 | from provisioningserver.testing.boot_images import ( | 53 | from provisioningserver.testing.boot_images import ( |
199 | 53 | make_boot_image_params, | 54 | make_boot_image_params, |
200 | 54 | make_image, | 55 | make_image, |
201 | @@ -504,6 +505,30 @@ | |||
202 | 504 | # ArchitectureRegistry is not stable, so we permit either here. | 505 | # ArchitectureRegistry is not stable, so we permit either here. |
203 | 505 | self.assertIn(observed_params["arch"], ["armhf", "arm64"]) | 506 | self.assertIn(observed_params["arch"], ["armhf", "arm64"]) |
204 | 506 | 507 | ||
205 | 508 | def test_get_kernel_params_filters_out_unnecessary_arguments(self): | ||
206 | 509 | params_okay = { | ||
207 | 510 | name.decode("ascii"): factory.make_name("value") | ||
208 | 511 | for name, _ in GetBootConfig.arguments | ||
209 | 512 | } | ||
210 | 513 | params_other = { | ||
211 | 514 | factory.make_name("name"): factory.make_name("value") | ||
212 | 515 | for _ in range(3) | ||
213 | 516 | } | ||
214 | 517 | params_all = params_okay.copy() | ||
215 | 518 | params_all.update(params_other) | ||
216 | 519 | |||
217 | 520 | client_service = Mock() | ||
218 | 521 | client = client_service.getClient.return_value | ||
219 | 522 | client.localIdent = params_okay["system_id"] | ||
220 | 523 | backend = TFTPBackend(self.make_dir(), client_service) | ||
221 | 524 | backend.fetcher = Mock() | ||
222 | 525 | |||
223 | 526 | backend.get_kernel_params(params_all) | ||
224 | 527 | |||
225 | 528 | self.assertThat( | ||
226 | 529 | backend.fetcher, MockCalledOnceWith( | ||
227 | 530 | client, GetBootConfig, **params_okay)) | ||
228 | 531 | |||
229 | 507 | 532 | ||
230 | 508 | class TestTFTPService(MAASTestCase): | 533 | class TestTFTPService(MAASTestCase): |
231 | 509 | 534 | ||
232 | 510 | 535 | ||
233 | === modified file 'src/provisioningserver/pserv_services/tftp.py' | |||
234 | --- src/provisioningserver/pserv_services/tftp.py 2016-03-28 13:54:47 +0000 | |||
235 | +++ src/provisioningserver/pserv_services/tftp.py 2016-06-30 19:11:09 +0000 | |||
236 | @@ -204,6 +204,17 @@ | |||
237 | 204 | path requested. | 204 | path requested. |
238 | 205 | :return: A `KernelParameters` instance. | 205 | :return: A `KernelParameters` instance. |
239 | 206 | """ | 206 | """ |
240 | 207 | # Extract from params only those arguments that GetBootConfig cares | ||
241 | 208 | # about; params is a context-like object and other stuff (too much?) | ||
242 | 209 | # gets in there. | ||
243 | 210 | arguments = ( | ||
244 | 211 | name.decode("ascii") | ||
245 | 212 | for name, _ in GetBootConfig.arguments | ||
246 | 213 | ) | ||
247 | 214 | params = { | ||
248 | 215 | name: params[name] for name in arguments | ||
249 | 216 | if name in params | ||
250 | 217 | } | ||
251 | 207 | client = self.client_service.getClient() | 218 | client = self.client_service.getClient() |
252 | 208 | params["system_id"] = client.localIdent | 219 | params["system_id"] = client.localIdent |
253 | 209 | d = self.fetcher(client, GetBootConfig, **params) | 220 | d = self.fetcher(client, GetBootConfig, **params) |
254 | @@ -223,9 +234,7 @@ | |||
255 | 223 | return boot_method.get_reader( | 234 | return boot_method.get_reader( |
256 | 224 | self, kernel_params=kernel_params, **params) | 235 | self, kernel_params=kernel_params, **params) |
257 | 225 | 236 | ||
261 | 226 | d = self.get_kernel_params(params) | 237 | return self.get_kernel_params(params).addCallback(generate) |
259 | 227 | d.addCallback(generate) | ||
260 | 228 | return d | ||
262 | 229 | 238 | ||
263 | 230 | @staticmethod | 239 | @staticmethod |
264 | 231 | def no_response_errback(failure, file_name): | 240 | def no_response_errback(failure, file_name): |
This was tested on powerNV hardware in 1ss. The nodes are now able to enlist, commission, and deploy with this fix.