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
This commit is contained in:
parent
be44ac8d4d
commit
ac4e9901d6
|
@ -34,10 +34,10 @@ LOG = log.getLogger(__name__)
|
|||
BIND_MOUNTS = ('/dev', '/sys', '/proc')
|
||||
|
||||
|
||||
def _get_root_partition(device, root_uuid):
|
||||
"""Find the root partition of a given device."""
|
||||
LOG.debug("Find the root partition %(uuid)s on device %(dev)s",
|
||||
{'dev': device, 'uuid': root_uuid})
|
||||
def _get_partition(device, uuid):
|
||||
"""Find the partition of a given device."""
|
||||
LOG.debug("Find the partition %(uuid)s on device %(dev)s",
|
||||
{'dev': device, 'uuid': uuid})
|
||||
|
||||
try:
|
||||
# Try to tell the kernel to re-read the partition table
|
||||
|
@ -59,50 +59,69 @@ def _get_root_partition(device, root_uuid):
|
|||
if part.get('TYPE') != 'part':
|
||||
continue
|
||||
|
||||
if part.get('UUID') == root_uuid:
|
||||
LOG.debug("Root partition %(uuid)s found on device "
|
||||
"%(dev)s", {'uuid': root_uuid, 'dev': device})
|
||||
if part.get('UUID') == uuid:
|
||||
LOG.debug("Partition %(uuid)s found on device "
|
||||
"%(dev)s", {'uuid': uuid, 'dev': device})
|
||||
return '/dev/' + part.get('KNAME')
|
||||
else:
|
||||
error_msg = ("No root partition with UUID %(uuid)s found on "
|
||||
"device %(dev)s" % {'uuid': root_uuid, 'dev': device})
|
||||
error_msg = ("No partition with UUID %(uuid)s found on "
|
||||
"device %(dev)s" % {'uuid': uuid, 'dev': device})
|
||||
LOG.error(error_msg)
|
||||
raise errors.DeviceNotFound(error_msg)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
error_msg = ('Finding the root partition with UUID %(uuid)s on '
|
||||
error_msg = ('Finding the partition with UUID %(uuid)s on '
|
||||
'device %(dev)s failed with %(err)s' %
|
||||
{'uuid': root_uuid, 'dev': device, 'err': e})
|
||||
{'uuid': uuid, 'dev': device, 'err': e})
|
||||
LOG.error(error_msg)
|
||||
raise errors.CommandExecutionError(error_msg)
|
||||
|
||||
|
||||
def _install_grub2(device, root_uuid):
|
||||
def _install_grub2(device, root_uuid, efi_system_part_uuid=None):
|
||||
"""Install GRUB2 bootloader on a given device."""
|
||||
LOG.debug("Installing GRUB2 bootloader on device %s", device)
|
||||
root_partition = _get_root_partition(device, root_uuid)
|
||||
root_partition = _get_partition(device, uuid=root_uuid)
|
||||
|
||||
try:
|
||||
# Mount the partition and binds
|
||||
path = tempfile.mkdtemp()
|
||||
|
||||
if efi_system_part_uuid:
|
||||
efi_partition = _get_partition(device, uuid=efi_system_part_uuid)
|
||||
efi_partition_mount_point = os.path.join(path, "boot/efi")
|
||||
else:
|
||||
efi_partition = None
|
||||
efi_partition_mount_point = None
|
||||
|
||||
utils.execute('mount', root_partition, path)
|
||||
for fs in BIND_MOUNTS:
|
||||
utils.execute('mount', '-o', 'bind', fs, path + fs)
|
||||
|
||||
if efi_partition:
|
||||
if not os.path.exists(efi_partition_mount_point):
|
||||
os.makedirs(efi_partition_mount_point)
|
||||
utils.execute('mount', efi_partition, efi_partition_mount_point)
|
||||
|
||||
binary_name = "grub"
|
||||
if os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')):
|
||||
binary_name = "grub2"
|
||||
|
||||
# Add /bin to PATH variable as grub requires it to find efibootmgr
|
||||
# when running in uefi boot mode.
|
||||
path_variable = os.environ.get('PATH', '')
|
||||
path_variable = '%s:/bin' % path_variable
|
||||
|
||||
# Install grub
|
||||
utils.execute('chroot %(path)s /bin/bash -c '
|
||||
'"/usr/sbin/%(bin)s-install %(dev)s"' %
|
||||
{'path': path, 'bin': binary_name, 'dev': device},
|
||||
shell=True)
|
||||
shell=True, env_variables={'PATH': path_variable})
|
||||
|
||||
# Generate the grub configuration file
|
||||
utils.execute('chroot %(path)s /bin/bash -c '
|
||||
'"/usr/sbin/%(bin)s-mkconfig -o '
|
||||
'/boot/%(bin)s/grub.cfg"' %
|
||||
{'path': path, 'bin': binary_name}, shell=True)
|
||||
{'path': path, 'bin': binary_name}, shell=True,
|
||||
env_variables={'PATH': path_variable})
|
||||
|
||||
LOG.info("GRUB2 successfully installed on %s", device)
|
||||
|
||||
|
@ -117,6 +136,19 @@ def _install_grub2(device, root_uuid):
|
|||
umount_warn_msg = "Unable to umount %(path)s. Error: %(error)s"
|
||||
# Umount binds and partition
|
||||
umount_binds_fail = False
|
||||
|
||||
# If umount fails for efi partition, then we cannot be sure that all
|
||||
# the changes were written back to the filesystem.
|
||||
try:
|
||||
if efi_partition:
|
||||
utils.execute('umount', efi_partition_mount_point, attempts=3,
|
||||
delay_on_retry=True)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
error_msg = ('Umounting efi system partition failed. '
|
||||
'Attempted 3 times. Error: %s' % e)
|
||||
LOG.error(error_msg)
|
||||
raise errors.CommandExecutionError(error_msg)
|
||||
|
||||
for fs in BIND_MOUNTS:
|
||||
try:
|
||||
utils.execute('umount', path + fs, attempts=3,
|
||||
|
@ -140,14 +172,19 @@ def _install_grub2(device, root_uuid):
|
|||
class ImageExtension(base.BaseAgentExtension):
|
||||
|
||||
@base.sync_command('install_bootloader')
|
||||
def install_bootloader(self, root_uuid):
|
||||
def install_bootloader(self, root_uuid, efi_system_part_uuid=None):
|
||||
"""Install the GRUB2 bootloader on the image.
|
||||
|
||||
:param root_uuid: The UUID of the root partition.
|
||||
:param efi_system_part_uuid: The UUID of the efi system partition.
|
||||
To be used only for uefi boot mode. For uefi boot mode, the
|
||||
boot loader will be installed here.
|
||||
:raises: CommandExecutionError if the installation of the
|
||||
bootloader fails.
|
||||
:raises: DeviceNotFound if the root partition is not found.
|
||||
|
||||
"""
|
||||
device = hardware.dispatch_to_managers('get_os_install_device')
|
||||
_install_grub2(device, root_uuid)
|
||||
_install_grub2(device,
|
||||
root_uuid=root_uuid,
|
||||
efi_system_part_uuid=efi_system_part_uuid)
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# under the License.
|
||||
|
||||
import mock
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
|
@ -38,20 +39,41 @@ class TestImageExtension(test_base.BaseTestCase):
|
|||
super(TestImageExtension, self).setUp()
|
||||
self.agent_extension = image.ImageExtension()
|
||||
self.fake_dev = '/dev/fake'
|
||||
self.fake_efi_system_part = '/dev/fake1'
|
||||
self.fake_root_part = '/dev/fake2'
|
||||
self.fake_root_uuid = '11111111-2222-3333-4444-555555555555'
|
||||
self.fake_efi_system_part_uuid = '45AB-2312'
|
||||
self.fake_dir = '/tmp/fake-dir'
|
||||
|
||||
@mock.patch.object(image, '_install_grub2')
|
||||
def test_install_bootloader(self, mock_grub2, mock_execute, mock_dispatch):
|
||||
def test_install_bootloader_bios(self, mock_grub2, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
self.agent_extension.install_bootloader(root_uuid=self.fake_root_uuid)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
mock_grub2.assert_called_once_with(self.fake_dev, self.fake_root_uuid)
|
||||
mock_grub2.assert_called_once_with(
|
||||
self.fake_dev, root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=None)
|
||||
|
||||
@mock.patch.object(image, '_get_root_partition')
|
||||
def test__install_grub2(self, mock_get_root, mock_execute, mock_dispatch):
|
||||
mock_get_root.return_value = self.fake_root_part
|
||||
@mock.patch.object(image, '_install_grub2')
|
||||
def test_install_bootloader_uefi(self, mock_grub2, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
self.agent_extension.install_bootloader(
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=self.fake_efi_system_part_uuid)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
mock_grub2.assert_called_once_with(
|
||||
self.fake_dev,
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=self.fake_efi_system_part_uuid)
|
||||
|
||||
@mock.patch.object(os, 'environ')
|
||||
@mock.patch.object(image, '_get_partition')
|
||||
def test__install_grub2(self, mock_get_part_uuid, environ_mock,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_get_part_uuid.return_value = self.fake_root_part
|
||||
environ_mock.get.return_value = '/sbin'
|
||||
image._install_grub2(self.fake_dev, self.fake_root_uuid)
|
||||
|
||||
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
|
||||
|
@ -63,11 +85,13 @@ class TestImageExtension(test_base.BaseTestCase):
|
|||
self.fake_dir + '/proc'),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-install %s"' %
|
||||
(self.fake_dir, self.fake_dev)), shell=True),
|
||||
(self.fake_dir, self.fake_dev)), shell=True,
|
||||
env_variables={'PATH': '/sbin:/bin'}),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-mkconfig -o '
|
||||
'/boot/grub/grub.cfg"' % self.fake_dir),
|
||||
shell=True),
|
||||
shell=True,
|
||||
env_variables={'PATH': '/sbin:/bin'}),
|
||||
mock.call('umount', self.fake_dir + '/dev',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir + '/sys',
|
||||
|
@ -77,30 +101,123 @@ class TestImageExtension(test_base.BaseTestCase):
|
|||
mock.call('umount', self.fake_dir, attempts=3,
|
||||
delay_on_retry=True)]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_get_root.assert_called_once_with(self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
mock_get_part_uuid.assert_called_once_with(self.fake_dev,
|
||||
uuid=self.fake_root_uuid)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
@mock.patch.object(image, '_get_root_partition')
|
||||
def test__install_grub2_command_fail(self, mock_get_root, mock_execute,
|
||||
@mock.patch.object(os, 'environ')
|
||||
@mock.patch.object(os, 'makedirs')
|
||||
@mock.patch.object(image, '_get_partition')
|
||||
def test__install_grub2_uefi(self, mock_get_part_uuid, mkdir_mock,
|
||||
environ_mock, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_get_part_uuid.side_effect = [self.fake_root_part,
|
||||
self.fake_efi_system_part]
|
||||
environ_mock.get.return_value = '/sbin'
|
||||
|
||||
image._install_grub2(
|
||||
self.fake_dev, root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=self.fake_efi_system_part_uuid)
|
||||
|
||||
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
|
||||
mock.call('mount', '-o', 'bind', '/dev',
|
||||
self.fake_dir + '/dev'),
|
||||
mock.call('mount', '-o', 'bind', '/sys',
|
||||
self.fake_dir + '/sys'),
|
||||
mock.call('mount', '-o', 'bind', '/proc',
|
||||
self.fake_dir + '/proc'),
|
||||
mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-install %s"' %
|
||||
(self.fake_dir, self.fake_dev)), shell=True,
|
||||
env_variables={'PATH': '/sbin:/bin'}),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-mkconfig -o '
|
||||
'/boot/grub/grub.cfg"' % self.fake_dir),
|
||||
shell=True,
|
||||
env_variables={'PATH': '/sbin:/bin'}),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir + '/dev',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir + '/sys',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir + '/proc',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('umount', self.fake_dir, attempts=3,
|
||||
delay_on_retry=True)]
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_get_part_uuid.assert_any_call(self.fake_dev,
|
||||
uuid=self.fake_root_uuid)
|
||||
mock_get_part_uuid.assert_any_call(self.fake_dev,
|
||||
uuid=self.fake_efi_system_part_uuid)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
@mock.patch.object(os, 'environ')
|
||||
@mock.patch.object(os, 'makedirs')
|
||||
@mock.patch.object(image, '_get_partition')
|
||||
def test__install_grub2_uefi_umount_fails(
|
||||
self, mock_get_part_uuid, mkdir_mock, environ_mock,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_get_part_uuid.side_effect = [self.fake_root_part,
|
||||
self.fake_efi_system_part]
|
||||
|
||||
def umount_raise_func(*args, **kwargs):
|
||||
if args[0] == 'umount':
|
||||
raise processutils.ProcessExecutionError('error')
|
||||
|
||||
mock_execute.side_effect = umount_raise_func
|
||||
environ_mock.get.return_value = '/sbin'
|
||||
self.assertRaises(errors.CommandExecutionError,
|
||||
image._install_grub2,
|
||||
self.fake_dev, root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=self.fake_efi_system_part_uuid)
|
||||
|
||||
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
|
||||
mock.call('mount', '-o', 'bind', '/dev',
|
||||
self.fake_dir + '/dev'),
|
||||
mock.call('mount', '-o', 'bind', '/sys',
|
||||
self.fake_dir + '/sys'),
|
||||
mock.call('mount', '-o', 'bind', '/proc',
|
||||
self.fake_dir + '/proc'),
|
||||
mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-install %s"' %
|
||||
(self.fake_dir, self.fake_dev)), shell=True,
|
||||
env_variables={'PATH': '/sbin:/bin'}),
|
||||
mock.call(('chroot %s /bin/bash -c '
|
||||
'"/usr/sbin/grub-mkconfig -o '
|
||||
'/boot/grub/grub.cfg"' % self.fake_dir),
|
||||
shell=True,
|
||||
env_variables={'PATH': '/sbin:/bin'}),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True)]
|
||||
mock_execute.assert_has_calls(expected)
|
||||
|
||||
@mock.patch.object(image, '_get_partition')
|
||||
def test__install_grub2_command_fail(self, mock_get_part_uuid,
|
||||
mock_execute,
|
||||
mock_dispatch):
|
||||
mock_get_root.return_value = self.fake_root_part
|
||||
mock_get_part_uuid.return_value = self.fake_root_part
|
||||
mock_execute.side_effect = processutils.ProcessExecutionError('boom')
|
||||
|
||||
self.assertRaises(errors.CommandExecutionError, image._install_grub2,
|
||||
self.fake_dev, self.fake_root_uuid)
|
||||
|
||||
mock_get_root.assert_called_once_with(self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
mock_get_part_uuid.assert_called_once_with(self.fake_dev,
|
||||
uuid=self.fake_root_uuid)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
def test__get_root_partition(self, mock_execute, mock_dispatch):
|
||||
def test__get_partition(self, mock_execute, mock_dispatch):
|
||||
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
|
||||
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
|
||||
KNAME="test2" UUID="%s" TYPE="part"''' % self.fake_root_uuid)
|
||||
mock_execute.side_effect = (None, [lsblk_output])
|
||||
|
||||
root_part = image._get_root_partition(self.fake_dev,
|
||||
root_part = image._get_partition(self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
self.assertEqual('/dev/test2', root_part)
|
||||
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
|
||||
|
@ -109,7 +226,7 @@ class TestImageExtension(test_base.BaseTestCase):
|
|||
mock_execute.assert_has_calls(expected)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
def test__get_root_partition_no_device_found(self, mock_execute,
|
||||
def test__get_partition_no_device_found(self, mock_execute,
|
||||
mock_dispatch):
|
||||
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
|
||||
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
|
||||
|
@ -117,7 +234,7 @@ class TestImageExtension(test_base.BaseTestCase):
|
|||
mock_execute.side_effect = (None, [lsblk_output])
|
||||
|
||||
self.assertRaises(errors.DeviceNotFound,
|
||||
image._get_root_partition, self.fake_dev,
|
||||
image._get_partition, self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
|
||||
delay_on_retry=True),
|
||||
|
@ -125,12 +242,12 @@ class TestImageExtension(test_base.BaseTestCase):
|
|||
mock_execute.assert_has_calls(expected)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
def test__get_root_partition_command_fail(self, mock_execute,
|
||||
def test__get_partition_command_fail(self, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_execute.side_effect = (None,
|
||||
processutils.ProcessExecutionError('boom'))
|
||||
self.assertRaises(errors.CommandExecutionError,
|
||||
image._get_root_partition, self.fake_dev,
|
||||
image._get_partition, self.fake_dev,
|
||||
self.fake_root_uuid)
|
||||
|
||||
expected = [mock.call('partx', '-u', self.fake_dev, attempts=3,
|
||||
|
|
Loading…
Reference in New Issue