Merge ~raharper/curtin:fix/uefi-remove-duplicate-entries into curtin:master

Proposed by Ryan Harper
Status: Merged
Approved by: Ryan Harper
Approved revision: 77bcff3fc6d5e1959000d3fa552acf5007d8b2f3
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~raharper/curtin:fix/uefi-remove-duplicate-entries
Merge into: curtin:master
Diff against target: 286 lines (+140/-25)
6 files modified
curtin/commands/curthooks.py (+37/-18)
curtin/util.py (+4/-2)
examples/tests/uefi_reuse_esp.yaml (+2/-0)
helpers/common (+0/-2)
tests/unittests/test_curthooks.py (+85/-2)
tests/unittests/test_util.py (+12/-1)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Chad Smith Approve
Review via email: mp+379675@code.launchpad.net

Commit message

uefi: refactor efibootmg handling to support removing duplicate entries

When curtin is reusing an existing ESP, it may already contain a previous
ubuntu entry. efibootmgr complains about duplicate labels, but does not
provide a mechanism to remove the duplicate automatically. In curtin
we use our efibootmgr output parser to find duplicates and remove them.

To enable this we need to ensure that 'efivarfs' is mounted inside the
target. In ChrootableTarget we will add the additional mount automatically
if we're running on a uefi system.

This branch also refactors use of ChrootableTarget context manager to
minimize the number of chroot calls (which include 4 mounts and unmounts).

LP: #1864257

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
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Ryan Harper (raharper) :
5a3485b... by Ryan Harper

Add unittests and walk entries via sorted key list

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

Minor unit test suggestion and +1. Thanks for this. I approve once that patch is landed.

review: Approve
77bcff3... by Ryan Harper

Add non-duplicate entry with different path to verify we compare both path and name in entries before removing

Revision history for this message
Chad Smith (chad.smith) wrote :

LGTM! awaiting CI

review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

This branch switches to invoking efibootmgr from curtin's environment vs invoking the one in /target. Is that intended? It breaks subiquity installs on UEFI currently :(

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
2index fd48a5b..3c5d38f 100644
3--- a/curtin/commands/curthooks.py
4+++ b/curtin/commands/curthooks.py
5@@ -443,26 +443,43 @@ def uefi_reorder_loaders(grubcfg, target):
6 front of the BootOrder.
7 """
8 if grubcfg.get('reorder_uefi', True):
9- efi_output = util.get_efibootmgr(target)
10- currently_booted = efi_output.get('current', None)
11- boot_order = efi_output.get('order', [])
12- if currently_booted:
13- if currently_booted in boot_order:
14- boot_order.remove(currently_booted)
15- boot_order = [currently_booted] + boot_order
16- new_boot_order = ','.join(boot_order)
17- LOG.debug(
18- "Setting currently booted %s as the first "
19- "UEFI loader.", currently_booted)
20- LOG.debug(
21- "New UEFI boot order: %s", new_boot_order)
22- with util.ChrootableTarget(target) as in_chroot:
23- in_chroot.subp(['efibootmgr', '-o', new_boot_order])
24+ with util.ChrootableTarget(target):
25+ efi_output = util.get_efibootmgr(target=target)
26+ currently_booted = efi_output.get('current', None)
27+ boot_order = efi_output.get('order', [])
28+ if currently_booted:
29+ if currently_booted in boot_order:
30+ boot_order.remove(currently_booted)
31+ boot_order = [currently_booted] + boot_order
32+ new_boot_order = ','.join(boot_order)
33+ LOG.debug(
34+ "Setting currently booted %s as the first "
35+ "UEFI loader.", currently_booted)
36+ LOG.debug(
37+ "New UEFI boot order: %s", new_boot_order)
38+ util.subp(['efibootmgr', '-o', new_boot_order])
39 else:
40 LOG.debug("Skipped reordering of UEFI boot methods.")
41 LOG.debug("Currently booted UEFI loader might no longer boot.")
42
43
44+def uefi_remove_duplicate_entries(grubcfg, target):
45+ seen = set()
46+ with util.ChrootableTarget(target):
47+ efi_output = util.get_efibootmgr(target=target)
48+ entries = efi_output.get('entries', {})
49+ for bootnum in sorted(entries):
50+ entry = entries[bootnum]
51+ t = tuple(entry.items())
52+ if t not in seen:
53+ seen.add(t)
54+ else:
55+ LOG.debug('Removing duplicate EFI entry (%s, %s)',
56+ bootnum, entry)
57+ util.subp(['efibootmgr', '--bootnum=%s' % bootnum,
58+ '--delete-bootnum'])
59+
60+
61 def setup_grub(cfg, target, osfamily=DISTROS.debian):
62 # target is the path to the mounted filesystem
63
64@@ -583,7 +600,8 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
65 else:
66 instdevs = ["none"]
67
68- if util.is_uefi_bootable() and grubcfg.get('update_nvram', True):
69+ uefi_bootable = util.is_uefi_bootable()
70+ if uefi_bootable and grubcfg.get('update_nvram', True):
71 uefi_remove_old_loaders(grubcfg, target)
72
73 LOG.debug("installing grub to %s [replace_default=%s]",
74@@ -591,7 +609,7 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
75
76 with util.ChrootableTarget(target):
77 args = ['install-grub']
78- if util.is_uefi_bootable():
79+ if uefi_bootable:
80 args.append("--uefi")
81 LOG.debug("grubcfg: %s", grubcfg)
82 if grubcfg.get('update_nvram', True):
83@@ -609,7 +627,8 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
84 join_stdout_err + args + instdevs, env=env, capture=True)
85 LOG.debug("%s\n%s\n", args + instdevs, out)
86
87- if util.is_uefi_bootable() and grubcfg.get('update_nvram', True):
88+ if uefi_bootable and grubcfg.get('update_nvram', True):
89+ uefi_remove_duplicate_entries(grubcfg, target)
90 uefi_reorder_loaders(grubcfg, target)
91
92
93diff --git a/curtin/util.py b/curtin/util.py
94index eb2228f..fa4f3f3 100644
95--- a/curtin/util.py
96+++ b/curtin/util.py
97@@ -633,6 +633,8 @@ class ChrootableTarget(object):
98 self.mounts = mounts
99 else:
100 self.mounts = ["/dev", "/proc", "/run", "/sys"]
101+ if is_uefi_bootable():
102+ self.mounts.append('/sys/firmware/efi/efivars')
103 self.umounts = []
104 self.disabled_daemons = False
105 self.allow_daemons = allow_daemons
106@@ -856,7 +858,7 @@ def parse_efibootmgr(content):
107 return output
108
109
110-def get_efibootmgr(target):
111+def get_efibootmgr(target=None):
112 """Return mapping of EFI information.
113
114 Calls `efibootmgr` inside the `target`.
115@@ -879,7 +881,7 @@ def get_efibootmgr(target):
116 }
117 }
118 """
119- with ChrootableTarget(target) as in_chroot:
120+ with ChrootableTarget(target=target) as in_chroot:
121 stdout, _ = in_chroot.subp(['efibootmgr', '-v'], capture=True)
122 output = parse_efibootmgr(stdout)
123 return output
124diff --git a/examples/tests/uefi_reuse_esp.yaml b/examples/tests/uefi_reuse_esp.yaml
125index c53064c..37a30d3 100644
126--- a/examples/tests/uefi_reuse_esp.yaml
127+++ b/examples/tests/uefi_reuse_esp.yaml
128@@ -1,4 +1,6 @@
129 showtrace: true
130+install:
131+ unmount: disabled
132
133 bucket:
134 - &setup |
135diff --git a/helpers/common b/helpers/common
136index 1def1bc..8230577 100644
137--- a/helpers/common
138+++ b/helpers/common
139@@ -970,8 +970,6 @@ install_grub() {
140 --disk $efi_disk --part $efi_part_num --loader $loader
141 rc=$?
142 [ "$rc" != "0" ] && { exit $rc; }
143- # check and remove duplicates
144- efibootmgr --verbose --remove-dups
145 else
146 echo "skip EFI entry creation due to \"$no_nvram\" flag"
147 fi
148diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
149index ff38240..72bb24e 100644
150--- a/tests/unittests/test_curthooks.py
151+++ b/tests/unittests/test_curthooks.py
152@@ -195,7 +195,9 @@ class TestUpdateInitramfs(CiTestCase):
153 super(TestUpdateInitramfs, self).setUp()
154 self.add_patch('curtin.util.subp', 'mock_subp')
155 self.add_patch('curtin.util.which', 'mock_which')
156+ self.add_patch('curtin.util.is_uefi_bootable', 'mock_uefi')
157 self.mock_which.return_value = self.random_string()
158+ self.mock_uefi.return_value = False
159 self.target = self.tmp_dir()
160 self.boot = os.path.join(self.target, 'boot')
161 os.makedirs(self.boot)
162@@ -787,12 +789,93 @@ class TestSetupGrub(CiTestCase):
163 },
164 }
165 }
166- self.in_chroot_subp_output.append(('', ''))
167+ self.subp_output.append(('', ''))
168 self.mock_haspkg.return_value = False
169 curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
170 self.assertEquals(
171 (['efibootmgr', '-o', '0001,0000'],),
172- self.mock_in_chroot_subp.call_args_list[0][0])
173+ self.mock_subp.call_args_list[1][0])
174+
175+
176+class TestUefiRemoveDuplicateEntries(CiTestCase):
177+
178+ def setUp(self):
179+ super(TestUefiRemoveDuplicateEntries, self).setUp()
180+ self.target = self.tmp_dir()
181+ self.add_patch('curtin.util.get_efibootmgr', 'm_efibootmgr')
182+ self.add_patch('curtin.util.subp', 'm_subp')
183+
184+ @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
185+ def test_uefi_remove_duplicate_entries(self):
186+ cfg = {
187+ 'grub': {
188+ 'install_devices': ['/dev/vdb'],
189+ 'update_nvram': True,
190+ },
191+ }
192+ self.m_efibootmgr.return_value = {
193+ 'current': '0000',
194+ 'entries': {
195+ '0000': {
196+ 'name': 'ubuntu',
197+ 'path': (
198+ 'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
199+ },
200+ '0001': {
201+ 'name': 'ubuntu',
202+ 'path': (
203+ 'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
204+ },
205+ '0002': { # Is not a duplicate because of unique path
206+ 'name': 'ubuntu',
207+ 'path': (
208+ 'HD(2,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
209+ },
210+ '0003': { # Is duplicate of 0000
211+ 'name': 'ubuntu',
212+ 'path': (
213+ 'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
214+ },
215+ }
216+ }
217+
218+ curthooks.uefi_remove_duplicate_entries(cfg, self.target)
219+ self.assertEquals([
220+ call(['efibootmgr', '--bootnum=0001', '--delete-bootnum']),
221+ call(['efibootmgr', '--bootnum=0003', '--delete-bootnum'])],
222+ self.m_subp.call_args_list)
223+
224+ @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
225+ def test_uefi_remove_duplicate_entries_no_change(self):
226+ cfg = {
227+ 'grub': {
228+ 'install_devices': ['/dev/vdb'],
229+ 'update_nvram': True,
230+ },
231+ }
232+ self.m_efibootmgr.return_value = {
233+ 'current': '0000',
234+ 'entries': {
235+ '0000': {
236+ 'name': 'ubuntu',
237+ 'path': (
238+ 'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
239+ },
240+ '0001': {
241+ 'name': 'centos',
242+ 'path': (
243+ 'HD(1,GPT)/File(\\EFI\\centos\\shimx64.efi)'),
244+ },
245+ '0002': {
246+ 'name': 'sles',
247+ 'path': (
248+ 'HD(1,GPT)/File(\\EFI\\sles\\shimx64.efi)'),
249+ },
250+ }
251+ }
252+
253+ curthooks.uefi_remove_duplicate_entries(cfg, self.target)
254+ self.assertEquals([], self.m_subp.call_args_list)
255
256
257 class TestUbuntuCoreHooks(CiTestCase):
258diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
259index 80bb85f..438f7ee 100644
260--- a/tests/unittests/test_util.py
261+++ b/tests/unittests/test_util.py
262@@ -585,12 +585,23 @@ class TestRunInChroot(CiTestCase):
263 class TestChrootableTargetMounts(CiTestCase):
264 """Test ChrootableTargets mounts dirs"""
265
266+ @mock.patch('curtin.util.is_uefi_bootable')
267 @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
268- def test_chrootable_target_default_mounts(self):
269+ def test_chrootable_target_default_mounts(self, m_uefi):
270+ m_uefi.return_value = False
271 in_chroot = util.ChrootableTarget("mytarget")
272 default_mounts = ['/dev', '/proc', '/run', '/sys']
273 self.assertEqual(sorted(default_mounts), sorted(in_chroot.mounts))
274
275+ @mock.patch('curtin.util.is_uefi_bootable')
276+ @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
277+ def test_chrootable_target_default_mounts_uefi(self, m_uefi):
278+ m_uefi.return_value = True
279+ in_chroot = util.ChrootableTarget("mytarget")
280+ default_mounts = ['/dev', '/proc', '/run', '/sys',
281+ '/sys/firmware/efi/efivars']
282+ self.assertEqual(sorted(default_mounts), sorted(in_chroot.mounts))
283+
284 @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
285 def test_chrootable_target_custom_mounts(self):
286 my_mounts = ['/foo', '/bar', '/wark']

Subscribers

People subscribed via source and target branches