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:
Ramakrishnan G 2015-03-12 09:15:49 +00:00
parent be44ac8d4d
commit ac4e9901d6
2 changed files with 191 additions and 37 deletions

View File

@ -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)

View File

@ -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,