Software RAID: Add IPA deploy support

This patch proposes to extend the IPA to be able to deploy on
software RAID devices. The IPA needs to be able to detect an
md device, find the underlying holder disks and configure grub
on them.

Change-Id: Ieb2c95ff130b5cc1e643fcde500066d9458ddbec
Story: #2004581
Task: #29102
This commit is contained in:
Arne Wiebalck 2019-02-26 17:56:20 +01:00
parent 2db123d318
commit 8fe390ecdd
4 changed files with 77 additions and 29 deletions

View File

@ -48,6 +48,15 @@ def _get_partition(device, uuid):
LOG.warning("Couldn't re-read the partition table " LOG.warning("Couldn't re-read the partition table "
"on device %s", device) "on device %s", device)
# If the deploy device is an md device, we want to install on
# the first partition. We clearly take a shortcut here for now.
# TODO(arne_wiebalck): Would it possible to use the partition
# UUID and use the "normal" discovery instead?
if hardware.is_md_device(device):
md_partition = device + 'p1'
LOG.debug("Found md device with partition %s", md_partition)
return md_partition
lsblk = utils.execute('lsblk', '-PbioKNAME,UUID,PARTUUID,TYPE', device) lsblk = utils.execute('lsblk', '-PbioKNAME,UUID,PARTUUID,TYPE', device)
report = lsblk[0] report = lsblk[0]
for line in report.split('\n'): for line in report.split('\n'):
@ -102,6 +111,16 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
if prep_boot_part_uuid: if prep_boot_part_uuid:
device = _get_partition(device, uuid=prep_boot_part_uuid) device = _get_partition(device, uuid=prep_boot_part_uuid)
# If the root device is an md device (or partition), restart the device
# (to help grub finding it) and identify the underlying holder disks
# to install grub.
disks = []
if hardware.is_md_device(device):
hardware.md_restart(device)
disks = hardware.get_holder_disks(device)
else:
disks.append(device)
utils.execute('mount', root_partition, path) utils.execute('mount', root_partition, path)
for fs in BIND_MOUNTS: for fs in BIND_MOUNTS:
utils.execute('mount', '-o', 'bind', fs, path + fs) utils.execute('mount', '-o', 'bind', fs, path + fs)
@ -125,11 +144,18 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
path_variable = os.environ.get('PATH', '') path_variable = os.environ.get('PATH', '')
path_variable = '%s:/bin:/usr/sbin' % path_variable path_variable = '%s:/bin:/usr/sbin' % path_variable
# Install grub # Install grub. Normally, grub goes to one disk only. In case of
utils.execute('chroot %(path)s /bin/sh -c ' # md devices, grub goes to all underlying holder (RAID-1) disks.
'"%(bin)s-install %(dev)s"' % LOG.info("GRUB2 will be installed on disks %s", disks)
{'path': path, 'bin': binary_name, 'dev': device}, for grub_disk in disks:
shell=True, env_variables={'PATH': path_variable}) LOG.debug("Installing GRUB2 on disk %s", grub_disk)
utils.execute('chroot %(path)s /bin/sh -c '
'"%(bin)s-install %(dev)s"' %
{'path': path, 'bin': binary_name,
'dev': grub_disk},
shell=True, env_variables={'PATH': path_variable})
LOG.debug("GRUB2 successfully installed on device %s", grub_disk)
# Also run grub-install with --removable, this installs grub to the # Also run grub-install with --removable, this installs grub to the
# EFI fallback path. Useful if the NVRAM wasn't written correctly, # EFI fallback path. Useful if the NVRAM wasn't written correctly,
# was reset or if testing with virt as libvirt resets the NVRAM # was reset or if testing with virt as libvirt resets the NVRAM

View File

@ -143,7 +143,7 @@ def _get_component_devices(raid_device):
return component_devices return component_devices
def _get_holder_disks(raid_device): def get_holder_disks(raid_device):
"""Get the holder disks of a Software RAID device. """Get the holder disks of a Software RAID device.
Examine an md device and return its underlying disks. Examine an md device and return its underlying disks.
@ -155,6 +155,7 @@ def _get_holder_disks(raid_device):
return [] return []
holder_disks = [] holder_disks = []
try: try:
out, _ = utils.execute('mdadm', '--detail', raid_device, out, _ = utils.execute('mdadm', '--detail', raid_device,
use_standard_locale=True) use_standard_locale=True)
@ -173,7 +174,7 @@ def _get_holder_disks(raid_device):
return holder_disks return holder_disks
def _is_md_device(raid_device): def is_md_device(raid_device):
"""Check if a device is an md device """Check if a device is an md device
Check if a device is a Software RAID (md) device. Check if a device is a Software RAID (md) device.
@ -190,7 +191,7 @@ def _is_md_device(raid_device):
return False return False
def _md_restart(raid_device): def md_restart(raid_device):
"""Restart an md device """Restart an md device
Stop and re-assemble a Software RAID (md) device. Stop and re-assemble a Software RAID (md) device.
@ -1282,8 +1283,8 @@ class GenericHardwareManager(HardwareManager):
valid or if there was an error when creating the RAID valid or if there was an error when creating the RAID
devices. devices.
""" """
LOG.info("Creating Software RAID")
# No RAID config: do nothing
raid_config = node.get('target_raid_config', {}) raid_config = node.get('target_raid_config', {})
# No 'software' controller: do nothing. If 'controller' is # No 'software' controller: do nothing. If 'controller' is
@ -1332,7 +1333,6 @@ class GenericHardwareManager(HardwareManager):
raise errors.SoftwareRAIDError(msg) raise errors.SoftwareRAIDError(msg)
# Create the partitions which will become the component devices. # Create the partitions which will become the component devices.
logical_disks = raid_config.get('logical_disks')
sector = '2048s' sector = '2048s'
for logical_disk in logical_disks: for logical_disk in logical_disks:
psize = logical_disk['size_gb'] psize = logical_disk['size_gb']
@ -1404,7 +1404,7 @@ class GenericHardwareManager(HardwareManager):
component_devices = _get_component_devices(raid_device.name) component_devices = _get_component_devices(raid_device.name)
LOG.debug("Found component devices {}".format( LOG.debug("Found component devices {}".format(
component_devices)) component_devices))
holder_disks = _get_holder_disks(raid_device.name) holder_disks = get_holder_disks(raid_device.name)
LOG.debug("Found holder disks {}".format( LOG.debug("Found holder disks {}".format(
holder_disks)) holder_disks))

View File

@ -91,12 +91,15 @@ class TestImageExtension(base.IronicAgentTest):
prep_boot_part_uuid=self.fake_prep_boot_part_uuid) prep_boot_part_uuid=self.fake_prep_boot_part_uuid)
mock_iscsi_clean.assert_called_once_with(self.fake_dev) mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(os, 'environ', autospec=True) @mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True) @mock.patch.object(image, '_get_partition', autospec=True)
def test__install_grub2(self, mock_get_part_uuid, environ_mock, def test__install_grub2(self, mock_get_part_uuid, environ_mock,
mock_execute, mock_dispatch): mock_is_md_device, mock_execute,
mock_dispatch):
mock_get_part_uuid.return_value = self.fake_root_part mock_get_part_uuid.return_value = self.fake_root_part
environ_mock.get.return_value = '/sbin' environ_mock.get.return_value = '/sbin'
mock_is_md_device.side_effect = [False]
image._install_grub2(self.fake_dev, self.fake_root_uuid) image._install_grub2(self.fake_dev, self.fake_root_uuid)
expected = [mock.call('mount', '/dev/fake2', self.fake_dir), expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
@ -128,13 +131,16 @@ class TestImageExtension(base.IronicAgentTest):
uuid=self.fake_root_uuid) uuid=self.fake_root_uuid)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(os, 'environ', autospec=True) @mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True) @mock.patch.object(image, '_get_partition', autospec=True)
def test__install_grub2_prep(self, mock_get_part_uuid, environ_mock, def test__install_grub2_prep(self, mock_get_part_uuid, environ_mock,
mock_execute, mock_dispatch): mock_is_md_device, mock_execute,
mock_dispatch):
mock_get_part_uuid.side_effect = [self.fake_root_part, mock_get_part_uuid.side_effect = [self.fake_root_part,
self.fake_prep_boot_part] self.fake_prep_boot_part]
environ_mock.get.return_value = '/sbin' environ_mock.get.return_value = '/sbin'
mock_is_md_device.side_effect = [False]
image._install_grub2(self.fake_dev, self.fake_root_uuid, image._install_grub2(self.fake_dev, self.fake_root_uuid,
prep_boot_part_uuid=self.fake_prep_boot_part_uuid) prep_boot_part_uuid=self.fake_prep_boot_part_uuid)
@ -170,15 +176,17 @@ class TestImageExtension(base.IronicAgentTest):
uuid=self.fake_prep_boot_part_uuid) uuid=self.fake_prep_boot_part_uuid)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(os, 'environ', autospec=True) @mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True) @mock.patch.object(image, '_get_partition', autospec=True)
def test__install_grub2_uefi(self, mock_get_part_uuid, mkdir_mock, def test__install_grub2_uefi(self, mock_get_part_uuid, mkdir_mock,
environ_mock, mock_execute, environ_mock, mock_is_md_device,
mock_dispatch): mock_execute, mock_dispatch):
mock_get_part_uuid.side_effect = [self.fake_root_part, mock_get_part_uuid.side_effect = [self.fake_root_part,
self.fake_efi_system_part] self.fake_efi_system_part]
environ_mock.get.return_value = '/sbin' environ_mock.get.return_value = '/sbin'
mock_is_md_device.return_value = False
image._install_grub2( image._install_grub2(
self.fake_dev, root_uuid=self.fake_root_uuid, self.fake_dev, root_uuid=self.fake_root_uuid,
@ -224,14 +232,16 @@ class TestImageExtension(base.IronicAgentTest):
uuid=self.fake_efi_system_part_uuid) uuid=self.fake_efi_system_part_uuid)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(os, 'environ', autospec=True) @mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True) @mock.patch.object(image, '_get_partition', autospec=True)
def test__install_grub2_uefi_umount_fails( def test__install_grub2_uefi_umount_fails(
self, mock_get_part_uuid, mkdir_mock, environ_mock, mock_execute, self, mock_get_part_uuid, mkdir_mock, environ_mock,
mock_dispatch): mock_is_md_device, mock_execute, mock_dispatch):
mock_get_part_uuid.side_effect = [self.fake_root_part, mock_get_part_uuid.side_effect = [self.fake_root_part,
self.fake_efi_system_part] self.fake_efi_system_part]
mock_is_md_device.return_value = False
def umount_raise_func(*args, **kwargs): def umount_raise_func(*args, **kwargs):
if args[0] == 'umount': if args[0] == 'umount':
@ -270,14 +280,16 @@ class TestImageExtension(base.IronicAgentTest):
attempts=3, delay_on_retry=True)] attempts=3, delay_on_retry=True)]
mock_execute.assert_has_calls(expected) mock_execute.assert_has_calls(expected)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(os, 'environ', autospec=True) @mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True) @mock.patch.object(image, '_get_partition', autospec=True)
def test__install_grub2_uefi_mount_fails( def test__install_grub2_uefi_mount_fails(
self, mock_get_part_uuid, mkdir_mock, environ_mock, mock_execute, self, mock_get_part_uuid, mkdir_mock, environ_mock,
mock_dispatch): mock_is_md_device, mock_execute, mock_dispatch):
mock_get_part_uuid.side_effect = [self.fake_root_part, mock_get_part_uuid.side_effect = [self.fake_root_part,
self.fake_efi_system_part] self.fake_efi_system_part]
mock_is_md_device.side_effect = [False]
def mount_raise_func(*args, **kwargs): def mount_raise_func(*args, **kwargs):
if args[0] == 'mount': if args[0] == 'mount':
@ -314,7 +326,10 @@ class TestImageExtension(base.IronicAgentTest):
uuid=self.fake_root_uuid) uuid=self.fake_root_uuid)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
def test__get_partition(self, mock_execute, mock_dispatch): @mock.patch.object(hardware, 'is_md_device', autospec=True)
def test__get_partition(self, mock_is_md_device, mock_execute,
mock_dispatch):
mock_is_md_device.side_effect = [False]
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk" lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part" KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
KNAME="test2" UUID="%s" TYPE="part"''' % self.fake_root_uuid) KNAME="test2" UUID="%s" TYPE="part"''' % self.fake_root_uuid)
@ -330,8 +345,10 @@ class TestImageExtension(base.IronicAgentTest):
mock_execute.assert_has_calls(expected) mock_execute.assert_has_calls(expected)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
def test__get_partition_no_device_found(self, mock_execute, @mock.patch.object(hardware, 'is_md_device', autospec=True)
mock_dispatch): def test__get_partition_no_device_found(self, mock_is_md_device,
mock_execute, mock_dispatch):
mock_is_md_device.side_effect = [False]
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk" lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part" KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
KNAME="test2" UUID="" TYPE="part"''') KNAME="test2" UUID="" TYPE="part"''')
@ -348,8 +365,10 @@ class TestImageExtension(base.IronicAgentTest):
mock_execute.assert_has_calls(expected) mock_execute.assert_has_calls(expected)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
def test__get_partition_command_fail(self, mock_execute, @mock.patch.object(hardware, 'is_md_device', autospec=True)
mock_dispatch): def test__get_partition_command_fail(self, mock_is_md_device,
mock_execute, mock_dispatch):
mock_is_md_device.side_effect = [False]
mock_execute.side_effect = (None, None, mock_execute.side_effect = (None, None,
processutils.ProcessExecutionError('boom')) processutils.ProcessExecutionError('boom'))
self.assertRaises(errors.CommandExecutionError, self.assertRaises(errors.CommandExecutionError,
@ -364,7 +383,10 @@ class TestImageExtension(base.IronicAgentTest):
mock_execute.assert_has_calls(expected) mock_execute.assert_has_calls(expected)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
def test__get_partition_partuuid(self, mock_execute, mock_dispatch): @mock.patch.object(hardware, 'is_md_device', autospec=True)
def test__get_partition_partuuid(self, mock_is_md_device, mock_execute,
mock_dispatch):
mock_is_md_device.side_effect = [False]
lsblk_output = ('''KNAME="test" UUID="" TYPE="disk" lsblk_output = ('''KNAME="test" UUID="" TYPE="disk"
KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part" KNAME="test1" UUID="256a39e3-ca3c-4fb8-9cc2-b32eec441f47" TYPE="part"
KNAME="test2" PARTUUID="%s" TYPE="part"''' % self.fake_root_uuid) KNAME="test2" PARTUUID="%s" TYPE="part"''' % self.fake_root_uuid)

View File

@ -2291,11 +2291,11 @@ class TestGenericHardwareManager(base.IronicAgentTest):
self.assertEqual(['/dev/vde1', '/dev/vdf1'], component_devices) self.assertEqual(['/dev/vde1', '/dev/vdf1'], component_devices)
@mock.patch.object(utils, 'execute', autospec=True) @mock.patch.object(utils, 'execute', autospec=True)
def test__get_holder_disks(self, mocked_execute): def test_get_holder_disks(self, mocked_execute):
mocked_execute.side_effect = [(MDADM_DETAIL_OUTPUT, '')] mocked_execute.side_effect = [(MDADM_DETAIL_OUTPUT, '')]
raid_device = hardware.BlockDevice('/dev/md0', 'RAID-1', raid_device = hardware.BlockDevice('/dev/md0', 'RAID-1',
1073741824, True) 1073741824, True)
holder_disks = hardware._get_holder_disks(raid_device.name) holder_disks = hardware.get_holder_disks(raid_device.name)
self.assertEqual(['/dev/vde', '/dev/vdf'], holder_disks) self.assertEqual(['/dev/vde', '/dev/vdf'], holder_disks)
@mock.patch.object(hardware, 'list_all_block_devices', autospec=True) @mock.patch.object(hardware, 'list_all_block_devices', autospec=True)
@ -2311,8 +2311,8 @@ class TestGenericHardwareManager(base.IronicAgentTest):
hardware._get_component_devices.side_effect = [ hardware._get_component_devices.side_effect = [
["/dev/sda1", "/dev/sda2"], ["/dev/sda1", "/dev/sda2"],
["/dev/sdb1", "/dev/sdb2"]] ["/dev/sdb1", "/dev/sdb2"]]
hardware._get_holder_disks = mock.Mock() hardware.get_holder_disks = mock.Mock()
hardware._get_holder_disks.side_effect = [ hardware.get_holder_disks.side_effect = [
["/dev/sda", "/dev/sdb"], ["/dev/sda", "/dev/sdb"],
["/dev/sda", "/dev/sdb"]] ["/dev/sda", "/dev/sdb"]]
mocked_execute.side_effect = [ mocked_execute.side_effect = [