Skip to content

Commit ac4e990

Browse files
committed
Add uefi support in image extension
This commit adds support for uefi systems in the image extension so that grub can be installed onto efi system partition for uefi machines. Implements: blueprint local-boot-support-with-partition-images Change-Id: I8fbb4b2ebdff991d41c7b618a4d654af26311a56
1 parent be44ac8 commit ac4e990

2 files changed

Lines changed: 191 additions & 37 deletions

File tree

ironic_python_agent/extensions/image.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@
3434
BIND_MOUNTS = ('/dev', '/sys', '/proc')
3535

3636

37-
def _get_root_partition(device, root_uuid):
38-
"""Find the root partition of a given device."""
39-
LOG.debug("Find the root partition %(uuid)s on device %(dev)s",
40-
{'dev': device, 'uuid': root_uuid})
37+
def _get_partition(device, uuid):
38+
"""Find the partition of a given device."""
39+
LOG.debug("Find the partition %(uuid)s on device %(dev)s",
40+
{'dev': device, 'uuid': uuid})
4141

4242
try:
4343
# Try to tell the kernel to re-read the partition table
@@ -59,50 +59,69 @@ def _get_root_partition(device, root_uuid):
5959
if part.get('TYPE') != 'part':
6060
continue
6161

62-
if part.get('UUID') == root_uuid:
63-
LOG.debug("Root partition %(uuid)s found on device "
64-
"%(dev)s", {'uuid': root_uuid, 'dev': device})
62+
if part.get('UUID') == uuid:
63+
LOG.debug("Partition %(uuid)s found on device "
64+
"%(dev)s", {'uuid': uuid, 'dev': device})
6565
return '/dev/' + part.get('KNAME')
6666
else:
67-
error_msg = ("No root partition with UUID %(uuid)s found on "
68-
"device %(dev)s" % {'uuid': root_uuid, 'dev': device})
67+
error_msg = ("No partition with UUID %(uuid)s found on "
68+
"device %(dev)s" % {'uuid': uuid, 'dev': device})
6969
LOG.error(error_msg)
7070
raise errors.DeviceNotFound(error_msg)
7171
except processutils.ProcessExecutionError as e:
72-
error_msg = ('Finding the root partition with UUID %(uuid)s on '
72+
error_msg = ('Finding the partition with UUID %(uuid)s on '
7373
'device %(dev)s failed with %(err)s' %
74-
{'uuid': root_uuid, 'dev': device, 'err': e})
74+
{'uuid': uuid, 'dev': device, 'err': e})
7575
LOG.error(error_msg)
7676
raise errors.CommandExecutionError(error_msg)
7777

7878

79-
def _install_grub2(device, root_uuid):
79+
def _install_grub2(device, root_uuid, efi_system_part_uuid=None):
8080
"""Install GRUB2 bootloader on a given device."""
8181
LOG.debug("Installing GRUB2 bootloader on device %s", device)
82-
root_partition = _get_root_partition(device, root_uuid)
82+
root_partition = _get_partition(device, uuid=root_uuid)
8383

8484
try:
8585
# Mount the partition and binds
8686
path = tempfile.mkdtemp()
87+
88+
if efi_system_part_uuid:
89+
efi_partition = _get_partition(device, uuid=efi_system_part_uuid)
90+
efi_partition_mount_point = os.path.join(path, "boot/efi")
91+
else:
92+
efi_partition = None
93+
efi_partition_mount_point = None
94+
8795
utils.execute('mount', root_partition, path)
8896
for fs in BIND_MOUNTS:
8997
utils.execute('mount', '-o', 'bind', fs, path + fs)
9098

99+
if efi_partition:
100+
if not os.path.exists(efi_partition_mount_point):
101+
os.makedirs(efi_partition_mount_point)
102+
utils.execute('mount', efi_partition, efi_partition_mount_point)
103+
91104
binary_name = "grub"
92105
if os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')):
93106
binary_name = "grub2"
94107

108+
# Add /bin to PATH variable as grub requires it to find efibootmgr
109+
# when running in uefi boot mode.
110+
path_variable = os.environ.get('PATH', '')
111+
path_variable = '%s:/bin' % path_variable
112+
95113
# Install grub
96114
utils.execute('chroot %(path)s /bin/bash -c '
97115
'"/usr/sbin/%(bin)s-install %(dev)s"' %
98116
{'path': path, 'bin': binary_name, 'dev': device},
99-
shell=True)
117+
shell=True, env_variables={'PATH': path_variable})
100118

101119
# Generate the grub configuration file
102120
utils.execute('chroot %(path)s /bin/bash -c '
103121
'"/usr/sbin/%(bin)s-mkconfig -o '
104122
'/boot/%(bin)s/grub.cfg"' %
105-
{'path': path, 'bin': binary_name}, shell=True)
123+
{'path': path, 'bin': binary_name}, shell=True,
124+
env_variables={'PATH': path_variable})
106125

107126
LOG.info("GRUB2 successfully installed on %s", device)
108127

@@ -117,6 +136,19 @@ def _install_grub2(device, root_uuid):
117136
umount_warn_msg = "Unable to umount %(path)s. Error: %(error)s"
118137
# Umount binds and partition
119138
umount_binds_fail = False
139+
140+
# If umount fails for efi partition, then we cannot be sure that all
141+
# the changes were written back to the filesystem.
142+
try:
143+
if efi_partition:
144+
utils.execute('umount', efi_partition_mount_point, attempts=3,
145+
delay_on_retry=True)
146+
except processutils.ProcessExecutionError as e:
147+
error_msg = ('Umounting efi system partition failed. '
148+
'Attempted 3 times. Error: %s' % e)
149+
LOG.error(error_msg)
150+
raise errors.CommandExecutionError(error_msg)
151+
120152
for fs in BIND_MOUNTS:
121153
try:
122154
utils.execute('umount', path + fs, attempts=3,
@@ -140,14 +172,19 @@ def _install_grub2(device, root_uuid):
140172
class ImageExtension(base.BaseAgentExtension):
141173

142174
@base.sync_command('install_bootloader')
143-
def install_bootloader(self, root_uuid):
175+
def install_bootloader(self, root_uuid, efi_system_part_uuid=None):
144176
"""Install the GRUB2 bootloader on the image.
145177
146178
:param root_uuid: The UUID of the root partition.
179+
:param efi_system_part_uuid: The UUID of the efi system partition.
180+
To be used only for uefi boot mode. For uefi boot mode, the
181+
boot loader will be installed here.
147182
:raises: CommandExecutionError if the installation of the
148183
bootloader fails.
149184
:raises: DeviceNotFound if the root partition is not found.
150185
151186
"""
152187
device = hardware.dispatch_to_managers('get_os_install_device')
153-
_install_grub2(device, root_uuid)
188+
_install_grub2(device,
189+
root_uuid=root_uuid,
190+
efi_system_part_uuid=efi_system_part_uuid)

ironic_python_agent/tests/extensions/image.py

Lines changed: 137 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# under the License.
1717

1818
import mock
19+
import os
1920
import shutil
2021
import tempfile
2122

@@ -38,20 +39,41 @@ def setUp(self):
3839
super(TestImageExtension, self).setUp()
3940
self.agent_extension = image.ImageExtension()
4041
self.fake_dev = '/dev/fake'
42+
self.fake_efi_system_part = '/dev/fake1'
4143
self.fake_root_part = '/dev/fake2'
4244
self.fake_root_uuid = '11111111-2222-3333-4444-555555555555'
45+
self.fake_efi_system_part_uuid = '45AB-2312'
4346
self.fake_dir = '/tmp/fake-dir'
4447

4548
@mock.patch.object(image, '_install_grub2')
46-
def test_install_bootloader(self, mock_grub2, mock_execute, mock_dispatch):
49+
def test_install_bootloader_bios(self, mock_grub2, mock_execute,
50+
mock_dispatch):
4751
mock_dispatch.return_value = self.fake_dev
4852
self.agent_extension.install_bootloader(root_uuid=self.fake_root_uuid)
4953
mock_dispatch.assert_called_once_with('get_os_install_device')
50-
mock_grub2.assert_called_once_with(self.fake_dev, self.fake_root_uuid)
54+
mock_grub2.assert_called_once_with(
55+
self.fake_dev, root_uuid=self.fake_root_uuid,
56+
efi_system_part_uuid=None)
5157

52-
@mock.patch.object(image, '_get_root_partition')
53-
def test__install_grub2(self, mock_get_root, mock_execute, mock_dispatch):
54-
mock_get_root.return_value = self.fake_root_part
58+
@mock.patch.object(image, '_install_grub2')
59+
def test_install_bootloader_uefi(self, mock_grub2, mock_execute,
60+
mock_dispatch):
61+
mock_dispatch.return_value = self.fake_dev
62+
self.agent_extension.install_bootloader(
63+
root_uuid=self.fake_root_uuid,
64+
efi_system_part_uuid=self.fake_efi_system_part_uuid)
65+
mock_dispatch.assert_called_once_with('get_os_install_device')
66+
mock_grub2.assert_called_once_with(
67+
self.fake_dev,
68+
root_uuid=self.fake_root_uuid,
69+
efi_system_part_uuid=self.fake_efi_system_part_uuid)
70+
71+
@mock.patch.object(os, 'environ')
72+
@mock.patch.object(image, '_get_partition')
73+
def test__install_grub2(self, mock_get_part_uuid, environ_mock,
74+
mock_execute, mock_dispatch):
75+
mock_get_part_uuid.return_value = self.fake_root_part
76+
environ_mock.get.return_value = '/sbin'
5577
image._install_grub2(self.fake_dev, self.fake_root_uuid)
5678

5779
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
@@ -63,11 +85,13 @@ def test__install_grub2(self, mock_get_root, mock_execute, mock_dispatch):
6385
self.fake_dir + '/proc'),
6486
mock.call(('chroot %s /bin/bash -c '
6587
'"/usr/sbin/grub-install %s"' %
66-
(self.fake_dir, self.fake_dev)), shell=True),
88+
(self.fake_dir, self.fake_dev)), shell=True,
89+
env_variables={'PATH': '/sbin:/bin'}),
6790
mock.call(('chroot %s /bin/bash -c '
6891
'"/usr/sbin/grub-mkconfig -o '
6992
'/boot/grub/grub.cfg"' % self.fake_dir),
70-
shell=True),
93+
shell=True,
94+
env_variables={'PATH': '/sbin:/bin'}),
7195
mock.call('umount', self.fake_dir + '/dev',
7296
attempts=3, delay_on_retry=True),
7397
mock.call('umount', self.fake_dir + '/sys',
@@ -77,30 +101,123 @@ def test__install_grub2(self, mock_get_root, mock_execute, mock_dispatch):
77101
mock.call('umount', self.fake_dir, attempts=3,
78102
delay_on_retry=True)]
79103
mock_execute.assert_has_calls(expected)
80-
mock_get_root.assert_called_once_with(self.fake_dev,
81-
self.fake_root_uuid)
104+
mock_get_part_uuid.assert_called_once_with(self.fake_dev,
105+
uuid=self.fake_root_uuid)
106+
self.assertFalse(mock_dispatch.called)
107+
108+
@mock.patch.object(os, 'environ')
109+
@mock.patch.object(os, 'makedirs')
110+
@mock.patch.object(image, '_get_partition')
111+
def test__install_grub2_uefi(self, mock_get_part_uuid, mkdir_mock,
112+
environ_mock, mock_execute,
113+
mock_dispatch):
114+
mock_get_part_uuid.side_effect = [self.fake_root_part,
115+
self.fake_efi_system_part]
116+
environ_mock.get.return_value = '/sbin'
117+
118+
image._install_grub2(
119+
self.fake_dev, root_uuid=self.fake_root_uuid,
120+
efi_system_part_uuid=self.fake_efi_system_part_uuid)
121+
122+
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
123+
mock.call('mount', '-o', 'bind', '/dev',
124+
self.fake_dir + '/dev'),
125+
mock.call('mount', '-o', 'bind', '/sys',
126+
self.fake_dir + '/sys'),
127+
mock.call('mount', '-o', 'bind', '/proc',
128+
self.fake_dir + '/proc'),
129+
mock.call('mount', self.fake_efi_system_part,
130+
self.fake_dir + '/boot/efi'),
131+
mock.call(('chroot %s /bin/bash -c '
132+
'"/usr/sbin/grub-install %s"' %
133+
(self.fake_dir, self.fake_dev)), shell=True,
134+
env_variables={'PATH': '/sbin:/bin'}),
135+
mock.call(('chroot %s /bin/bash -c '
136+
'"/usr/sbin/grub-mkconfig -o '
137+
'/boot/grub/grub.cfg"' % self.fake_dir),
138+
shell=True,
139+
env_variables={'PATH': '/sbin:/bin'}),
140+
mock.call('umount', self.fake_dir + '/boot/efi',
141+
attempts=3, delay_on_retry=True),
142+
mock.call('umount', self.fake_dir + '/dev',
143+
attempts=3, delay_on_retry=True),
144+
mock.call('umount', self.fake_dir + '/sys',
145+
attempts=3, delay_on_retry=True),
146+
mock.call('umount', self.fake_dir + '/proc',
147+
attempts=3, delay_on_retry=True),
148+
mock.call('umount', self.fake_dir, attempts=3,
149+
delay_on_retry=True)]
150+
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
151+
mock_execute.assert_has_calls(expected)
152+
mock_get_part_uuid.assert_any_call(self.fake_dev,
153+
uuid=self.fake_root_uuid)
154+
mock_get_part_uuid.assert_any_call(self.fake_dev,
155+
uuid=self.fake_efi_system_part_uuid)
82156
self.assertFalse(mock_dispatch.called)
83157

84-
@mock.patch.object(image, '_get_root_partition')
85-
def test__install_grub2_command_fail(self, mock_get_root, mock_execute,
158+
@mock.patch.object(os, 'environ')
159+
@mock.patch.object(os, 'makedirs')
160+
@mock.patch.object(image, '_get_partition')
161+
def test__install_grub2_uefi_umount_fails(
162+
self, mock_get_part_uuid, mkdir_mock, environ_mock,
163+
mock_execute, mock_dispatch):
164+
mock_get_part_uuid.side_effect = [self.fake_root_part,
165+
self.fake_efi_system_part]
166+
167+
def umount_raise_func(*args, **kwargs):
168+
if args[0] == 'umount':
169+
raise processutils.ProcessExecutionError('error')
170+
171+
mock_execute.side_effect = umount_raise_func
172+
environ_mock.get.return_value = '/sbin'
173+
self.assertRaises(errors.CommandExecutionError,
174+
image._install_grub2,
175+
self.fake_dev, root_uuid=self.fake_root_uuid,
176+
efi_system_part_uuid=self.fake_efi_system_part_uuid)
177+
178+
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
179+
mock.call('mount', '-o', 'bind', '/dev',
180+
self.fake_dir + '/dev'),
181+
mock.call('mount', '-o', 'bind', '/sys',
182+
self.fake_dir + '/sys'),
183+
mock.call('mount', '-o', 'bind', '/proc',
184+
self.fake_dir + '/proc'),
185+
mock.call('mount', self.fake_efi_system_part,
186+
self.fake_dir + '/boot/efi'),
187+
mock.call(('chroot %s /bin/bash -c '
188+
'"/usr/sbin/grub-install %s"' %
189+
(self.fake_dir, self.fake_dev)), shell=True,
190+
env_variables={'PATH': '/sbin:/bin'}),
191+
mock.call(('chroot %s /bin/bash -c '
192+
'"/usr/sbin/grub-mkconfig -o '
193+
'/boot/grub/grub.cfg"' % self.fake_dir),
194+
shell=True,
195+
env_variables={'PATH': '/sbin:/bin'}),
196+
mock.call('umount', self.fake_dir + '/boot/efi',
197+
attempts=3, delay_on_retry=True)]
198+
mock_execute.assert_has_calls(expected)
199+
200+
@mock.patch.object(image, '_get_partition')
201+
def test__install_grub2_command_fail(self, mock_get_part_uuid,
202+
mock_execute,
86203
mock_dispatch):
87-
mock_get_root.return_value = self.fake_root_part
204+
mock_get_part_uuid.return_value = self.fake_root_part
88205
mock_execute.side_effect = processutils.ProcessExecutionError('boom')
89206

90207
self.assertRaises(errors.CommandExecutionError, image._install_grub2,
91208
self.fake_dev, self.fake_root_uuid)
92209

93-
mock_get_root.assert_called_once_with(self.fake_dev,
94-
self.fake_root_uuid)
210+
mock_get_part_uuid.assert_called_once_with(self.fake_dev,
211+
uuid=self.fake_root_uuid)
95212
self.assertFalse(mock_dispatch.called)
96213

97-
def test__get_root_partition(self, mock_execute, mock_dispatch):
214+
def test__get_partition(self, mock_execute, mock_dispatch):
98215
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
99216
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
100217
KNAME="test2" UUID="%s" TYPE="part"''' % self.fake_root_uuid)
101218
mock_execute.side_effect = (None, [lsblk_output])
102219

103-
root_part = image._get_root_partition(self.fake_dev,
220+
root_part = image._get_partition(self.fake_dev,
104221
self.fake_root_uuid)
105222
self.assertEqual('/dev/test2', root_part)
106223
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
@@ -109,28 +226,28 @@ def test__get_root_partition(self, mock_execute, mock_dispatch):
109226
mock_execute.assert_has_calls(expected)
110227
self.assertFalse(mock_dispatch.called)
111228

112-
def test__get_root_partition_no_device_found(self, mock_execute,
229+
def test__get_partition_no_device_found(self, mock_execute,
113230
mock_dispatch):
114231
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
115232
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
116233
KNAME="test2" UUID="" TYPE="part"''')
117234
mock_execute.side_effect = (None, [lsblk_output])
118235

119236
self.assertRaises(errors.DeviceNotFound,
120-
image._get_root_partition, self.fake_dev,
237+
image._get_partition, self.fake_dev,
121238
self.fake_root_uuid)
122239
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
123240
delay_on_retry=True),
124241
mock.call('lsblk', '-PbioKNAME,UUID,TYPE', self.fake_dev)]
125242
mock_execute.assert_has_calls(expected)
126243
self.assertFalse(mock_dispatch.called)
127244

128-
def test__get_root_partition_command_fail(self, mock_execute,
245+
def test__get_partition_command_fail(self, mock_execute,
129246
mock_dispatch):
130247
mock_execute.side_effect = (None,
131248
processutils.ProcessExecutionError('boom'))
132249
self.assertRaises(errors.CommandExecutionError,
133-
image._get_root_partition, self.fake_dev,
250+
image._get_partition, self.fake_dev,
134251
self.fake_root_uuid)
135252

136253
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,

0 commit comments

Comments
 (0)