Merge ~raharper/curtin:fix/uefi-reorder-missing-boot-current into curtin:master

Proposed by Ryan Harper
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)
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_FALLBACK_SUPPORT, is enabled by default. Users may
disable both reordering and fallback support.

- unittest/helpers.py: Add with_logs support for CiTestCase
- docs: document grub: reorder_uefi and grub:
  reorder_uefi_force_fallback config options.

LP: #1789650

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
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/performance/. It's still present in the commit message for 3e1a8, but that will be squashed by CI.

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.

review: Needs Information
Revision history for this message
Paride Legovini (paride) wrote :

Marking as Needs Fixing while waiting for the documentation commits (per IRC conversation).

review: Needs Fixing
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Paride Legovini (paride) wrote :

Thanks!

review: Approve
Revision history for this message
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"...

review: Needs Fixing
Revision history for this message
Server Team CI bot (server-team-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 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
14diff --git a/curtin/__init__.py b/curtin/__init__.py
15index 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"
27diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
28index 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):
200diff --git a/doc/topics/config.rst b/doc/topics/config.rst
201index 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 ~~~~~~~~~~
253diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
254index 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:
301diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
302index 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):
656diff --git a/tests/unittests/test_feature.py b/tests/unittests/test_feature.py
657index 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

Subscribers

People subscribed via source and target branches