Merge ~raharper/curtin:fix/uefi-reorder-missing-boot-current into curtin:master
- Git
- lp:~raharper/curtin
- fix/uefi-reorder-missing-boot-current
- Merge into master
Status: | Merged |
---|---|
Approved by: | Paride Legovini |
Approved revision: | d5b589e59bc550748d1b93e17b37930c0d524080 |
Merge reported by: | Server Team CI bot |
Merged at revision: | not available |
Proposed branch: | ~raharper/curtin:fix/uefi-reorder-missing-boot-current |
Merge into: | curtin:master |
Diff against target: |
667 lines (+423/-14) 7 files modified
Makefile (+1/-1) curtin/__init__.py (+2/-0) curtin/commands/curthooks.py (+99/-5) doc/topics/config.rst (+35/-0) tests/unittests/helpers.py (+18/-0) tests/unittests/test_curthooks.py (+265/-8) tests/unittests/test_feature.py (+3/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Paride Legovini | Approve | ||
Review via email: mp+387981@code.launchpad.net |
Commit message
UEFI: Handle missing BootCurrent entry when reordering UEFI entries
Curtin typically reorders UEFI boot entries to place the currently
booted entry at the front of the list after install. In some cases a
system UEFI may not present a BootCurrent entry and curtin will not
perform any reordering leaving a node to boot from the newly
installed entry. For MAAS deployments, this causes issues as MAAS
expects to retain control over the node via PXE booting.
Curtin will attempt to reorder the boot menu when BootCurrent is
missing (or when forced via curtin config option) by placing all
network boot entries first followed by the entry curtin installed
and appending all other entries afterward. The feature,
UEFI_REORDER_
disable both reordering and fallback support.
- unittest/
- docs: document grub: reorder_uefi and grub:
reorder_
LP: #1789650
Description of the change
Server Team CI bot (server-team-bot) wrote : | # |
Paride Legovini (paride) wrote : | # |
LGTM, the needs-fixing is just for the typo (see inline comment).
In another inline comment I suggest documenting the reason for the BootOrder reordering a bit more, but take it with "importance: nit".
I fixed a typo in the MP commit message s/perform/
Ryan Harper (raharper) wrote : | # |
Thanks for the review. I've at least one update to push; I'm still working with Rod (you can see where we're at in the bug) to get positive feedback on addressing the issue. We're working on the logic to be reliable for the systems we know don't have BootCurrent.
I'll push an update and move back to Needs Review when we've got something working.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:01f30826981
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:fc95d59be45
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Paride Legovini (paride) wrote : | # |
I went through the code and it looks good, I'd just clarify a bit more the reason why we want to keep the current boot entry the first one (see inline comment). Take it as a "nit" comment: up to you to decide if it's worth fixing or not.
Just to be sure: this is the code that Rod validated in LP: #1789650 using your PPA, correct? While the code looks good and CI passes with the new tests it's not immediate to test it in practice.
Paride Legovini (paride) wrote : | # |
Marking as Needs Fixing while waiting for the documentation commits (per IRC conversation).
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:d5b589e59bc
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
Commit message lints:
- Line #12 has 44 too many characters. Line starts with: "and appending all other"...
Server Team CI bot (server-team-bot) : | # |
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index 68a3ad3..187132c 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -9,7 +9,7 @@ ifeq ($(COVERAGE), 1) |
6 | endif |
7 | CURTIN_VMTEST_IMAGE_SYNC ?= False |
8 | export CURTIN_VMTEST_IMAGE_SYNC |
9 | -noseopts ?= -vv --nologcapture |
10 | +noseopts ?= -vv |
11 | pylintopts ?= --rcfile=pylintrc --errors-only |
12 | target_dirs ?= curtin tests tools |
13 | |
14 | diff --git a/curtin/__init__.py b/curtin/__init__.py |
15 | index ae4ce11..092020b 100644 |
16 | --- a/curtin/__init__.py |
17 | +++ b/curtin/__init__.py |
18 | @@ -34,6 +34,8 @@ FEATURES = [ |
19 | 'APT_CONFIG_V1', |
20 | # has version module |
21 | 'HAS_VERSION_MODULE', |
22 | + # uefi_reoder has fallback support if BootCurrent is missing |
23 | + 'UEFI_REORDER_FALLBACK_SUPPORT', |
24 | ] |
25 | |
26 | __version__ = "20.1" |
27 | diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py |
28 | index 51f11d6..92230fc 100644 |
29 | --- a/curtin/commands/curthooks.py |
30 | +++ b/curtin/commands/curthooks.py |
31 | @@ -85,6 +85,8 @@ do_initrd = yes |
32 | link_in_boot = {inboot} |
33 | """ |
34 | |
35 | +UEFI_BOOT_ENTRY_IS_NETWORK = r'.*(Network|PXE|NIC|Ethernet|LAN|IP4|IP6)+.*' |
36 | + |
37 | |
38 | def do_apt_config(cfg, target): |
39 | cfg = apt_config.translate_old_apt_features(cfg) |
40 | @@ -411,6 +413,7 @@ def install_kernel(cfg, target): |
41 | def uefi_remove_old_loaders(grubcfg, target): |
42 | """Removes the old UEFI loaders from efibootmgr.""" |
43 | efi_output = util.get_efibootmgr(target) |
44 | + LOG.debug('UEFI remove old olders efi output:\n%s', efi_output) |
45 | current_uefi_boot = efi_output.get('current', None) |
46 | old_efi_entries = { |
47 | entry: info |
48 | @@ -437,18 +440,90 @@ def uefi_remove_old_loaders(grubcfg, target): |
49 | "should be removed.", info['name']) |
50 | |
51 | |
52 | -def uefi_reorder_loaders(grubcfg, target): |
53 | +def uefi_boot_entry_is_network(boot_entry_name): |
54 | + """ |
55 | + Return boolean if boot entry name looks like a known network entry. |
56 | + """ |
57 | + return re.match(UEFI_BOOT_ENTRY_IS_NETWORK, |
58 | + boot_entry_name, re.IGNORECASE) is not None |
59 | + |
60 | + |
61 | +def _reorder_new_entry(boot_order, efi_output, efi_orig=None, variant=None): |
62 | + """ |
63 | + Reorder the EFI boot menu as follows |
64 | + |
65 | + 1. All PXE/Network boot entries |
66 | + 2. The newly installed entry variant (ubuntu/centos) |
67 | + 3. The other items in the boot order that are not in [1, 2] |
68 | + |
69 | + returns a list of bootnum strings |
70 | + """ |
71 | + |
72 | + if not boot_order: |
73 | + raise RuntimeError('boot_order is not a list') |
74 | + |
75 | + if efi_orig is None: |
76 | + raise RuntimeError('Missing efi_orig boot dictionary') |
77 | + |
78 | + if variant is None: |
79 | + variant = "" |
80 | + |
81 | + net_boot = [] |
82 | + other = [] |
83 | + target = [] |
84 | + |
85 | + LOG.debug("UEFI previous boot order: %s", efi_orig['order']) |
86 | + LOG.debug("UEFI current boot order: %s", boot_order) |
87 | + new_entries = list(set(boot_order).difference(set(efi_orig['order']))) |
88 | + if new_entries: |
89 | + LOG.debug("UEFI Found new boot entries: %s", new_entries) |
90 | + LOG.debug('UEFI Looking for installed entry variant=%s', variant.lower()) |
91 | + for bootnum in boot_order: |
92 | + entry = efi_output['entries'][bootnum] |
93 | + if uefi_boot_entry_is_network(entry['name']): |
94 | + net_boot.append(bootnum) |
95 | + else: |
96 | + if entry['name'].lower() == variant.lower(): |
97 | + target.append(bootnum) |
98 | + else: |
99 | + other.append(bootnum) |
100 | + |
101 | + if net_boot: |
102 | + LOG.debug("UEFI found netboot entries: %s", net_boot) |
103 | + if other: |
104 | + LOG.debug("UEFI found other entries: %s", other) |
105 | + if target: |
106 | + LOG.debug("UEFI found target entry: %s", target) |
107 | + else: |
108 | + LOG.debug("UEFI Did not find an entry with variant=%s", |
109 | + variant.lower()) |
110 | + new_order = net_boot + target + other |
111 | + if boot_order == new_order: |
112 | + LOG.debug("UEFI Current and Previous bootorders match") |
113 | + return new_order |
114 | + |
115 | + |
116 | +def uefi_reorder_loaders(grubcfg, target, efi_orig=None, variant=None): |
117 | """Reorders the UEFI BootOrder to place BootCurrent first. |
118 | |
119 | The specifically doesn't try to do to much. The order in which grub places |
120 | a new EFI loader is up to grub. This only moves the BootCurrent to the |
121 | front of the BootOrder. |
122 | + |
123 | + In some systems, BootCurrent may not be set/present. In this case |
124 | + curtin will attempt to place the new boot entry created when grub |
125 | + is installed after the the previous first entry (before we installed grub). |
126 | + |
127 | """ |
128 | if grubcfg.get('reorder_uefi', True): |
129 | efi_output = util.get_efibootmgr(target=target) |
130 | + LOG.debug('UEFI efibootmgr output after install:\n%s', efi_output) |
131 | currently_booted = efi_output.get('current', None) |
132 | boot_order = efi_output.get('order', []) |
133 | - if currently_booted: |
134 | + new_boot_order = None |
135 | + force_fallback_reorder = config.value_as_boolean( |
136 | + grubcfg.get('reorder_uefi_force_fallback', False)) |
137 | + if currently_booted and force_fallback_reorder is False: |
138 | if currently_booted in boot_order: |
139 | boot_order.remove(currently_booted) |
140 | boot_order = [currently_booted] + boot_order |
141 | @@ -456,6 +531,23 @@ def uefi_reorder_loaders(grubcfg, target): |
142 | LOG.debug( |
143 | "Setting currently booted %s as the first " |
144 | "UEFI loader.", currently_booted) |
145 | + else: |
146 | + reason = ( |
147 | + "config 'reorder_uefi_force_fallback' is True" if |
148 | + force_fallback_reorder else "missing 'BootCurrent' value") |
149 | + LOG.debug("Using fallback UEFI reordering: " + reason) |
150 | + if len(boot_order) < 2: |
151 | + LOG.debug( |
152 | + 'UEFI BootOrder has less than 2 entries, cannot reorder') |
153 | + return |
154 | + # look at efi entries before we added one to find the new addition |
155 | + new_order = _reorder_new_entry( |
156 | + copy.deepcopy(boot_order), efi_output, efi_orig, variant) |
157 | + if new_order != boot_order: |
158 | + new_boot_order = ','.join(new_order) |
159 | + else: |
160 | + LOG.debug("UEFI No changes to boot order.") |
161 | + if new_boot_order: |
162 | LOG.debug( |
163 | "New UEFI boot order: %s", new_boot_order) |
164 | with util.ChrootableTarget(target) as in_chroot: |
165 | @@ -592,7 +684,7 @@ def uefi_find_grub_device_ids(sconfig): |
166 | return grub_device_ids |
167 | |
168 | |
169 | -def setup_grub(cfg, target, osfamily=DISTROS.debian): |
170 | +def setup_grub(cfg, target, osfamily=DISTROS.debian, variant=None): |
171 | # target is the path to the mounted filesystem |
172 | |
173 | # FIXME: these methods need moving to curtin.block |
174 | @@ -692,13 +784,14 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian): |
175 | |
176 | update_nvram = grubcfg.get('update_nvram', True) |
177 | if uefi_bootable and update_nvram: |
178 | + efi_orig_output = util.get_efibootmgr(target) |
179 | uefi_remove_old_loaders(grubcfg, target) |
180 | |
181 | install_grub(instdevs, target, uefi=uefi_bootable, grubcfg=grubcfg) |
182 | |
183 | if uefi_bootable and update_nvram: |
184 | + uefi_reorder_loaders(grubcfg, target, efi_orig_output, variant) |
185 | uefi_remove_duplicate_entries(grubcfg, target) |
186 | - uefi_reorder_loaders(grubcfg, target) |
187 | |
188 | |
189 | def update_initramfs(target=None, all_kernels=False): |
190 | @@ -1734,7 +1827,8 @@ def builtin_curthooks(cfg, target, state): |
191 | name=stack_prefix + '/install-grub', |
192 | reporting_enabled=True, level="INFO", |
193 | description="installing grub to target devices"): |
194 | - setup_grub(cfg, target, osfamily=osfamily) |
195 | + setup_grub(cfg, target, osfamily=osfamily, |
196 | + variant=distro_info.variant) |
197 | |
198 | |
199 | def curthooks(args): |
200 | diff --git a/doc/topics/config.rst b/doc/topics/config.rst |
201 | index 7f8396e..951f07f 100644 |
202 | --- a/doc/topics/config.rst |
203 | +++ b/doc/topics/config.rst |
204 | @@ -226,6 +226,35 @@ not provided, Curtin will set the value to 'console'. If the ``terminal`` |
205 | value is 'unmodified' then Curtin will not set any value at all and will |
206 | use Grub defaults. |
207 | |
208 | +**reorder_uefi**: *<boolean: default True>* |
209 | + |
210 | +Curtin is typically used with MAAS where the systems are configured to boot |
211 | +from the network leaving MAAS in control. On UEFI systems, after installing |
212 | +a bootloader the systems BootOrder may be updated to boot from the new entry. |
213 | +This breaks MAAS control over the system as all subsequent reboots of the node |
214 | +will no longer boot over the network. Therefore, if ``reorder_uefi`` is True |
215 | +curtin will modify the UEFI BootOrder settings to place the currently booted |
216 | +entry (BootCurrent) to the first option after installing the new target OS into |
217 | +the UEFI boot menu. The result is that the system will boot from the same |
218 | +device that it booted to run curtin; for MAAS this will be a network device. |
219 | + |
220 | +On some UEFI systems the BootCurrent entry may not be present. This can |
221 | +cause a system to not boot to the same device that it was previously booting. |
222 | +If BootCurrent is not present, curtin will update the BootOrder such that |
223 | +all Network related entries are placed before the newly installed boot entry and |
224 | +all other entries are placed at the end. This enables the system to network |
225 | +boot first and on failure will boot the most recently installed entry. |
226 | + |
227 | +This setting is ignored if *update_nvram* is False. |
228 | + |
229 | +**reorder_uefi_force_fallback**: *<boolean: default False>* |
230 | + |
231 | +The fallback reodering mechanism is only active if BootCurrent is not present |
232 | +in the efibootmgr output. The fallback reordering method may be enabled |
233 | +even if BootCurrent is present if *reorder_uefi_force_fallback* is True. |
234 | + |
235 | +This setting is ignored if *update_nvram* or *reorder_uefi* are False. |
236 | + |
237 | |
238 | **Example**:: |
239 | |
240 | @@ -264,6 +293,12 @@ use Grub defaults. |
241 | probe_additional_os: True |
242 | terminal: unmodified |
243 | |
244 | +**Enable Fallback UEFI Reordering**:: |
245 | + |
246 | + grub: |
247 | + reorder_uefi: true |
248 | + reorder_uefi_force_fallback: true |
249 | + |
250 | |
251 | http_proxy |
252 | ~~~~~~~~~~ |
253 | diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py |
254 | index 2f5e51a..64a79ca 100644 |
255 | --- a/tests/unittests/helpers.py |
256 | +++ b/tests/unittests/helpers.py |
257 | @@ -2,11 +2,13 @@ |
258 | |
259 | import imp |
260 | import importlib |
261 | +import logging |
262 | import mock |
263 | import os |
264 | import random |
265 | import shutil |
266 | import string |
267 | +import sys |
268 | import tempfile |
269 | from unittest import TestCase, skipIf |
270 | from contextlib import contextmanager |
271 | @@ -55,6 +57,7 @@ def skipUnlessJsonSchema(): |
272 | class CiTestCase(TestCase): |
273 | """Common testing class which all curtin unit tests subclass.""" |
274 | |
275 | + with_logs = False |
276 | allowed_subp = False |
277 | SUBP_SHELL_TRUE = "shell=True" |
278 | |
279 | @@ -69,6 +72,21 @@ class CiTestCase(TestCase): |
280 | |
281 | def setUp(self): |
282 | super(CiTestCase, self).setUp() |
283 | + if self.with_logs: |
284 | + # Create a log handler so unit tests can search expected logs. |
285 | + self.logger = logging.getLogger() |
286 | + if sys.version_info[0] == 2: |
287 | + import StringIO |
288 | + self.logs = StringIO.StringIO() |
289 | + else: |
290 | + import io |
291 | + self.logs = io.StringIO() |
292 | + formatter = logging.Formatter('%(levelname)s: %(message)s') |
293 | + handler = logging.StreamHandler(self.logs) |
294 | + handler.setFormatter(formatter) |
295 | + self.old_handlers = self.logger.handlers |
296 | + self.logger.handlers = [handler] |
297 | + |
298 | if self.allowed_subp is True: |
299 | util.subp = _real_subp |
300 | else: |
301 | diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py |
302 | index a6ae86e..ddae280 100644 |
303 | --- a/tests/unittests/test_curthooks.py |
304 | +++ b/tests/unittests/test_curthooks.py |
305 | @@ -1,5 +1,6 @@ |
306 | # This file is part of curtin. See LICENSE file for copyright and license info. |
307 | |
308 | +import copy |
309 | import os |
310 | from mock import call, patch |
311 | import textwrap |
312 | @@ -10,7 +11,7 @@ from curtin import distro |
313 | from curtin import util |
314 | from curtin import config |
315 | from curtin.reporter import events |
316 | -from .helpers import CiTestCase, dir2dict, populate_dir |
317 | +from .helpers import CiTestCase, dir2dict, populate_dir, random |
318 | |
319 | |
320 | class TestGetFlashKernelPkgs(CiTestCase): |
321 | @@ -531,12 +532,55 @@ class TestSetupZipl(CiTestCase): |
322 | content) |
323 | |
324 | |
325 | +class EfiOutput(object): |
326 | + |
327 | + def __init__(self, current=None, order=None, entries=None): |
328 | + self.entries = {} |
329 | + if entries: |
330 | + for entry in entries: |
331 | + self.entries.update(entry) |
332 | + self.current = current |
333 | + self.order = order |
334 | + if not order and self.entries: |
335 | + self.order = sorted(self.entries.keys()) |
336 | + |
337 | + def add_entry(self, bootnum=None, name=None, path=None, current=False): |
338 | + if not bootnum: |
339 | + bootnum = "%04x" % random.randint(0, 1000) |
340 | + if not name: |
341 | + name = CiTestCase.random_string() |
342 | + if not path: |
343 | + path = '' |
344 | + if bootnum not in self.entries: |
345 | + self.entries[bootnum] = {'name': name, 'path': path} |
346 | + if not self.order: |
347 | + self.order = [] |
348 | + self.order.append(bootnum) |
349 | + if current: |
350 | + self.current = bootnum |
351 | + |
352 | + def set_order(self, new_order): |
353 | + self.order = new_order |
354 | + |
355 | + def as_dict(self): |
356 | + output = {} |
357 | + if self.current: |
358 | + output['current'] = self.current |
359 | + if self.order: |
360 | + output['order'] = self.order |
361 | + output['entries'] = self.entries |
362 | + return output |
363 | + |
364 | + |
365 | class TestSetupGrub(CiTestCase): |
366 | |
367 | + with_logs = True |
368 | + |
369 | def setUp(self): |
370 | super(TestSetupGrub, self).setUp() |
371 | self.target = self.tmp_dir() |
372 | self.distro_family = distro.DISTROS.debian |
373 | + self.variant = 'ubuntu' |
374 | self.add_patch('curtin.distro.lsb_release', 'mock_lsb_release') |
375 | self.mock_lsb_release.return_value = {'codename': 'xenial'} |
376 | self.add_patch('curtin.util.is_uefi_bootable', |
377 | @@ -556,7 +600,8 @@ class TestSetupGrub(CiTestCase): |
378 | updated_cfg = { |
379 | 'install_devices': ['/dev/vdb'] |
380 | } |
381 | - curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family) |
382 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
383 | + variant=self.variant) |
384 | self.m_install_grub.assert_called_with( |
385 | ['/dev/vdb'], self.target, uefi=False, grubcfg=updated_cfg) |
386 | |
387 | @@ -588,7 +633,8 @@ class TestSetupGrub(CiTestCase): |
388 | }, |
389 | } |
390 | m_exists.return_value = True |
391 | - curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family) |
392 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
393 | + variant=self.variant) |
394 | self.m_install_grub.assert_called_with( |
395 | ['/dev/vdb'], self.target, uefi=False, |
396 | grubcfg={'install_devices': ['/dev/vdb']}) |
397 | @@ -638,7 +684,8 @@ class TestSetupGrub(CiTestCase): |
398 | } |
399 | m_exists.return_value = True |
400 | m_is_valid_device.side_effect = (False, True, False, True) |
401 | - curthooks.setup_grub(cfg, self.target, osfamily=distro.DISTROS.redhat) |
402 | + curthooks.setup_grub(cfg, self.target, osfamily=distro.DISTROS.redhat, |
403 | + variant='centos') |
404 | self.m_install_grub.assert_called_with( |
405 | ['/dev/vdb1'], self.target, uefi=True, |
406 | grubcfg={'update_nvram': False, 'install_devices': ['/dev/vdb1']} |
407 | @@ -650,7 +697,8 @@ class TestSetupGrub(CiTestCase): |
408 | 'install_devices': None, |
409 | }, |
410 | } |
411 | - curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family) |
412 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
413 | + variant=self.variant) |
414 | self.m_install_grub.assert_called_with( |
415 | ['none'], self.target, uefi=False, |
416 | grubcfg={'install_devices': None} |
417 | @@ -681,7 +729,8 @@ class TestSetupGrub(CiTestCase): |
418 | } |
419 | } |
420 | } |
421 | - curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family) |
422 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
423 | + variant=self.variant) |
424 | self.m_install_grub.assert_called_with( |
425 | ['/dev/vdb'], self.target, uefi=True, grubcfg=cfg.get('grub') |
426 | ) |
427 | @@ -721,7 +770,8 @@ class TestSetupGrub(CiTestCase): |
428 | } |
429 | } |
430 | self.mock_haspkg.return_value = False |
431 | - curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family) |
432 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
433 | + variant=self.variant) |
434 | |
435 | expected_calls = [ |
436 | call(['efibootmgr', '-B', '-b', '0001'], |
437 | @@ -762,10 +812,217 @@ class TestSetupGrub(CiTestCase): |
438 | } |
439 | } |
440 | self.mock_haspkg.return_value = False |
441 | - curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family) |
442 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
443 | + variant=self.variant) |
444 | + self.assertEquals([ |
445 | + call(['efibootmgr', '-o', '0001,0000'], target=self.target)], |
446 | + self.mock_subp.call_args_list) |
447 | + |
448 | + @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
449 | + def test_grub_install_uefi_reorders_no_current_new_entry(self): |
450 | + self.add_patch('curtin.distro.install_packages', 'mock_install') |
451 | + self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg') |
452 | + self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr') |
453 | + self.mock_is_uefi_bootable.return_value = True |
454 | + cfg = { |
455 | + 'grub': { |
456 | + 'install_devices': ['/dev/vdb'], |
457 | + 'update_nvram': True, |
458 | + 'remove_old_uefi_loaders': False, |
459 | + 'reorder_uefi': True, |
460 | + }, |
461 | + } |
462 | + |
463 | + # Single existing entry 0001 |
464 | + efi_orig = EfiOutput() |
465 | + efi_orig.add_entry(bootnum='0001', name='centos') |
466 | + |
467 | + # After install add a second entry, 0000 to the front of order |
468 | + efi_post = copy.deepcopy(efi_orig) |
469 | + efi_post.add_entry(bootnum='0000', name='ubuntu') |
470 | + efi_post.set_order(['0000', '0001']) |
471 | + |
472 | + # After reorder we should have the target install first |
473 | + efi_final = copy.deepcopy(efi_post) |
474 | + |
475 | + self.mock_efibootmgr.side_effect = iter([ |
476 | + efi_orig.as_dict(), # collect original order before install |
477 | + efi_orig.as_dict(), # remove_old_loaders query (no change) |
478 | + efi_post.as_dict(), # efi table after grub install, (changed) |
479 | + efi_final.as_dict(), # remove duplicates checks and finds reorder |
480 | + # has changed |
481 | + ]) |
482 | + self.mock_haspkg.return_value = False |
483 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
484 | + variant=self.variant) |
485 | + logs = self.logs.getvalue() |
486 | + print(logs) |
487 | + self.assertEquals([], self.mock_subp.call_args_list) |
488 | + self.assertIn("Using fallback UEFI reordering:", logs) |
489 | + self.assertIn("missing 'BootCurrent' value", logs) |
490 | + self.assertIn("Found new boot entries: ['0000']", logs) |
491 | + |
492 | + @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
493 | + def test_grub_install_uefi_reorders_no_curr_same_size_order_no_match(self): |
494 | + self.add_patch('curtin.distro.install_packages', 'mock_install') |
495 | + self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg') |
496 | + self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr') |
497 | + self.add_patch('curtin.commands.curthooks.uefi_remove_old_loaders', |
498 | + 'mock_remove_old_loaders') |
499 | + self.mock_is_uefi_bootable.return_value = True |
500 | + cfg = { |
501 | + 'grub': { |
502 | + 'install_devices': ['/dev/vdb'], |
503 | + 'update_nvram': True, |
504 | + 'remove_old_uefi_loaders': False, |
505 | + 'reorder_uefi': True, |
506 | + }, |
507 | + } |
508 | + |
509 | + # Existing Custom Ubuntu, usb and cd/dvd entry, booting Ubuntu |
510 | + efi_orig = EfiOutput() |
511 | + efi_orig.add_entry(bootnum='0001', name='Ubuntu Deluxe Edition') |
512 | + efi_orig.add_entry(bootnum='0002', name='USB Device') |
513 | + efi_orig.add_entry(bootnum='0000', name='CD/DVD') |
514 | + efi_orig.set_order(['0001', '0002', '0000']) |
515 | + |
516 | + # after install existing ubuntu entry is reused, no change in order |
517 | + efi_post = efi_orig |
518 | + |
519 | + # after reorder, no change is made due to the installed distro variant |
520 | + # string 'ubuntu' is not found in the boot entries so we retain the |
521 | + # original efi order. |
522 | + efi_final = efi_post |
523 | + |
524 | + self.mock_efibootmgr.side_effect = iter([ |
525 | + efi_orig.as_dict(), # collect original order before install |
526 | + efi_orig.as_dict(), # remove_old_loaders query |
527 | + efi_post.as_dict(), # reorder entries queries post install |
528 | + efi_final.as_dict(), # remove duplicates checks and finds reorder |
529 | + ]) |
530 | + |
531 | + self.mock_haspkg.return_value = False |
532 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
533 | + variant=self.variant) |
534 | + |
535 | + logs = self.logs.getvalue() |
536 | + print(logs) |
537 | + self.assertEquals([], self.mock_subp.call_args_list) |
538 | + self.assertIn("Using fallback UEFI reordering:", logs) |
539 | + self.assertIn("missing 'BootCurrent' value", logs) |
540 | + self.assertIn("Current and Previous bootorders match", logs) |
541 | + self.assertIn("Looking for installed entry variant=", logs) |
542 | + self.assertIn("Did not find an entry with variant=", logs) |
543 | + self.assertIn("No changes to boot order.", logs) |
544 | + |
545 | + @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
546 | + def test_grub_install_uefi_reorders_force_fallback(self): |
547 | + self.add_patch('curtin.distro.install_packages', 'mock_install') |
548 | + self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg') |
549 | + self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr') |
550 | + self.mock_is_uefi_bootable.return_value = True |
551 | + cfg = { |
552 | + 'grub': { |
553 | + 'install_devices': ['/dev/vdb'], |
554 | + 'update_nvram': True, |
555 | + 'remove_old_uefi_loaders': True, |
556 | + 'reorder_uefi': True, |
557 | + 'reorder_uefi_force_fallback': True, |
558 | + }, |
559 | + } |
560 | + # Single existing entry 0001 and set as current, which should avoid |
561 | + # any fallback logic, but we're forcing fallback pack via config |
562 | + efi_orig = EfiOutput() |
563 | + efi_orig.add_entry(bootnum='0001', name='PXE', current=True) |
564 | + print(efi_orig.as_dict()) |
565 | + |
566 | + # After install add a second entry, 0000 to the front of order |
567 | + efi_post = copy.deepcopy(efi_orig) |
568 | + efi_post.add_entry(bootnum='0000', name='ubuntu') |
569 | + efi_post.set_order(['0000', '0001']) |
570 | + print(efi_orig.as_dict()) |
571 | + |
572 | + # After reorder we should have the original boot entry 0001 as first |
573 | + efi_final = copy.deepcopy(efi_post) |
574 | + efi_final.set_order(['0001', '0000']) |
575 | + |
576 | + self.mock_efibootmgr.side_effect = iter([ |
577 | + efi_orig.as_dict(), # collect original order before install |
578 | + efi_orig.as_dict(), # remove_old_loaders query (no change) |
579 | + efi_post.as_dict(), # efi table after grub install, (changed) |
580 | + efi_final.as_dict(), # remove duplicates checks and finds reorder |
581 | + # has changed |
582 | + ]) |
583 | + |
584 | + self.mock_haspkg.return_value = False |
585 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
586 | + variant=self.variant) |
587 | + logs = self.logs.getvalue() |
588 | + print(logs) |
589 | self.assertEquals([ |
590 | call(['efibootmgr', '-o', '0001,0000'], target=self.target)], |
591 | self.mock_subp.call_args_list) |
592 | + self.assertIn("Using fallback UEFI reordering:", logs) |
593 | + self.assertIn("config 'reorder_uefi_force_fallback' is True", logs) |
594 | + |
595 | + @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
596 | + def test_grub_install_uefi_reorders_network_first(self): |
597 | + self.add_patch('curtin.distro.install_packages', 'mock_install') |
598 | + self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg') |
599 | + self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr') |
600 | + self.mock_is_uefi_bootable.return_value = True |
601 | + cfg = { |
602 | + 'grub': { |
603 | + 'install_devices': ['/dev/vdb'], |
604 | + 'update_nvram': True, |
605 | + 'remove_old_uefi_loaders': True, |
606 | + 'reorder_uefi': True, |
607 | + }, |
608 | + } |
609 | + |
610 | + # Existing ubuntu, usb and cd/dvd entry, booting ubuntu |
611 | + efi_orig = EfiOutput() |
612 | + efi_orig.add_entry(bootnum='0001', name='centos') |
613 | + efi_orig.add_entry(bootnum='0002', name='Network') |
614 | + efi_orig.add_entry(bootnum='0003', name='PXE') |
615 | + efi_orig.add_entry(bootnum='0004', name='LAN') |
616 | + efi_orig.add_entry(bootnum='0000', name='CD/DVD') |
617 | + efi_orig.set_order(['0001', '0002', '0003', '0004', '0000']) |
618 | + print(efi_orig.as_dict()) |
619 | + |
620 | + # after install we add an ubuntu entry, and grub puts it first |
621 | + efi_post = copy.deepcopy(efi_orig) |
622 | + efi_post.add_entry(bootnum='0007', name='ubuntu') |
623 | + efi_post.set_order(['0007'] + efi_orig.order) |
624 | + print(efi_post.as_dict()) |
625 | + |
626 | + # reorder must place all network devices first, then ubuntu, and others |
627 | + efi_final = copy.deepcopy(efi_post) |
628 | + expected_order = ['0002', '0003', '0004', '0007', '0001', '0000'] |
629 | + efi_final.set_order(expected_order) |
630 | + |
631 | + self.mock_efibootmgr.side_effect = iter([ |
632 | + efi_orig.as_dict(), # collect original order before install |
633 | + efi_orig.as_dict(), # remove_old_loaders query |
634 | + efi_post.as_dict(), # reorder entries queries post install |
635 | + efi_final.as_dict(), # remove duplicates checks and finds reorder |
636 | + ]) |
637 | + self.mock_haspkg.return_value = False |
638 | + curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family, |
639 | + variant=self.variant) |
640 | + logs = self.logs.getvalue() |
641 | + print(logs) |
642 | + print('Number of bootmgr calls: %s' % self.mock_efibootmgr.call_count) |
643 | + self.assertEquals([ |
644 | + call(['efibootmgr', '-o', '%s' % (",".join(expected_order))], |
645 | + target=self.target)], |
646 | + self.mock_subp.call_args_list) |
647 | + self.assertIn("Using fallback UEFI reordering:", logs) |
648 | + self.assertIn("missing 'BootCurrent' value", logs) |
649 | + self.assertIn("Looking for installed entry variant=", logs) |
650 | + self.assertIn("found netboot entries: ['0002', '0003', '0004']", logs) |
651 | + self.assertIn("found other entries: ['0001', '0000']", logs) |
652 | + self.assertIn("found target entry: ['0007']", logs) |
653 | |
654 | |
655 | class TestUefiRemoveDuplicateEntries(CiTestCase): |
656 | diff --git a/tests/unittests/test_feature.py b/tests/unittests/test_feature.py |
657 | index 84325ef..8690ad8 100644 |
658 | --- a/tests/unittests/test_feature.py |
659 | +++ b/tests/unittests/test_feature.py |
660 | @@ -27,4 +27,7 @@ class TestExportsFeatures(CiTestCase): |
661 | def test_has_btrfs_swapfile_support(self): |
662 | self.assertIn('BTRFS_SWAPFILE', curtin.FEATURES) |
663 | |
664 | + def test_has_uefi_reorder_fallback_support(self): |
665 | + self.assertIn('UEFI_REORDER_FALLBACK_SUPPORT', curtin.FEATURES) |
666 | + |
667 | # vi: ts=4 expandtab syntax=python |
PASSED: Continuous integration, rev:3e1a8e20c28 ed7feadda9714ef 6823d2fd5d5ad0 /jenkins. ubuntu. com/server/ job/curtin- ci/161/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-amd64/ 161/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-arm64/ 161/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-ppc64el/ 161/ /jenkins. ubuntu. com/server/ job/curtin- ci/nodes= metal-s390x/ 161/
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/curtin- ci/161/ /rebuild
https:/