diff options
| author | Iury Gregory Melo Ferreira <imelofer@redhat.com> | 2019-12-02 16:56:42 +0100 |
|---|---|---|
| committer | Iury Gregory Melo Ferreira <imelofer@redhat.com> | 2020-01-16 11:23:41 +0100 |
| commit | b6210be196fea271b2c49f89d3e1638517c1198c (patch) | |
| tree | 27e8dfd35248a8ece8d7bcffdd9b12f4a03018f8 /ironic_python_agent/tests | |
| parent | 1b20dd9b96c9b73e45618fc88bfddfe82652d203 (diff) | |
| download | ironic-python-agent-b6210be196fea271b2c49f89d3e1638517c1198c.tar.gz | |
Avoid grub2-install when on UEFI boot mode
This patch changes the workflow for whole disk images when using uefi.
If we can identify the bootloader and it's valid we can update using
efibootmgr since grub2-install have problems specially on secure boot
mode.
We also updated the regex to search for the uefi partition on the disk,
since in some cases the parted command output can be without the FS
for the partition with esp Flag.
Change-Id: I7167e71e5d2352a045565289b200e5530d0ba11d
Story: #2006847
Task: #37435
Diffstat (limited to 'ironic_python_agent/tests')
| -rw-r--r-- | ironic_python_agent/tests/unit/extensions/test_image.py | 384 | ||||
| -rw-r--r-- | ironic_python_agent/tests/unit/test_utils.py | 24 |
2 files changed, 396 insertions, 12 deletions
diff --git a/ironic_python_agent/tests/unit/extensions/test_image.py b/ironic_python_agent/tests/unit/extensions/test_image.py index aa802275..44dce7b7 100644 --- a/ironic_python_agent/tests/unit/extensions/test_image.py +++ b/ironic_python_agent/tests/unit/extensions/test_image.py @@ -48,25 +48,36 @@ class TestImageExtension(base.IronicAgentTest): @mock.patch.object(iscsi, 'clean_up', autospec=True) @mock.patch.object(image, '_install_grub2', autospec=True) - def test_install_bootloader_bios(self, mock_grub2, mock_iscsi_clean, - mock_execute, mock_dispatch): - mock_dispatch.return_value = self.fake_dev + def test__install_bootloader_bios(self, mock_grub2, mock_iscsi_clean, + mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='bios') + ] self.agent_extension.install_bootloader(root_uuid=self.fake_root_uuid) - mock_dispatch.assert_called_once_with('get_os_install_device') + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + self.assertEqual(2, mock_dispatch.call_count) mock_grub2.assert_called_once_with( self.fake_dev, root_uuid=self.fake_root_uuid, efi_system_part_uuid=None, prep_boot_part_uuid=None) mock_iscsi_clean.assert_called_once_with(self.fake_dev) @mock.patch.object(iscsi, 'clean_up', autospec=True) + @mock.patch.object(image, '_manage_uefi', autospec=True) @mock.patch.object(image, '_install_grub2', autospec=True) - def test_install_bootloader_uefi(self, mock_grub2, mock_iscsi_clean, - mock_execute, mock_dispatch): - mock_dispatch.return_value = self.fake_dev + def test__install_bootloader_uefi(self, mock_grub2, mock_uefi, + mock_iscsi_clean, + mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='uefi') + ] + mock_uefi.return_value = False 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_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + self.assertEqual(2, mock_dispatch.call_count) mock_grub2.assert_called_once_with( self.fake_dev, root_uuid=self.fake_root_uuid, @@ -74,16 +85,207 @@ class TestImageExtension(base.IronicAgentTest): prep_boot_part_uuid=None) mock_iscsi_clean.assert_called_once_with(self.fake_dev) + @mock.patch.object(os.path, 'exists', lambda *_: False) + @mock.patch.object(iscsi, 'clean_up', autospec=True) + @mock.patch.object(image, '_get_efi_bootloaders', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=False) + @mock.patch.object(os, 'makedirs', autospec=True) + def test__uefi_bootloader_given_partition( + self, mkdir_mock, mock_utils_efi_part, mock_partition, + mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='uefi') + ] + mock_partition.side_effect = [self.fake_dev, self.fake_efi_system_part] + mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI'] + mock_utils_efi_part.return_value = '1' + + mock_execute.side_effect = iter([('', ''), ('', ''), + ('', ''), ('', ''), + ('', ''), ('', '')]) + + expected = [mock.call('efibootmgr', '--version'), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI'), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('sync')] + + self.agent_extension.install_bootloader( + root_uuid=self.fake_root_uuid, + efi_system_part_uuid=self.fake_efi_system_part_uuid) + + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_execute.assert_has_calls(expected) + mock_utils_efi_part.assert_called_once_with(self.fake_dev) + self.assertEqual(6, mock_execute.call_count) + + @mock.patch.object(os.path, 'exists', lambda *_: False) + @mock.patch.object(iscsi, 'clean_up', autospec=True) + @mock.patch.object(image, '_get_efi_bootloaders', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + def test__uefi_bootloader_find_partition( + self, mkdir_mock, mock_utils_efi_part, mock_partition, + mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='uefi') + ] + mock_partition.return_value = self.fake_dev + mock_utils_efi_part.return_value = '1' + mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI'] + mock_execute.side_effect = iter([('', ''), ('', ''), + ('', ''), ('', ''), + ('', ''), ('', '')]) + + expected = [mock.call('efibootmgr', '--version'), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI'), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('sync')] + + self.agent_extension.install_bootloader( + root_uuid=self.fake_root_uuid, + efi_system_part_uuid=None) + + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_execute.assert_has_calls(expected) + mock_utils_efi_part.assert_called_once_with(self.fake_dev) + self.assertEqual(6, mock_execute.call_count) + + @mock.patch.object(os.path, 'exists', lambda *_: False) + @mock.patch.object(iscsi, 'clean_up', autospec=True) + @mock.patch.object(image, '_get_efi_bootloaders', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + def test__uefi_bootloader_with_entry_removal( + self, mkdir_mock, mock_utils_efi_part, mock_partition, + mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='uefi') + ] + mock_partition.return_value = self.fake_dev + mock_utils_efi_part.return_value = '1' + mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI'] + stdeer_msg = """ +efibootmgr: ** Warning ** : Boot0004 has same label ironic1\n +efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n +""" + mock_execute.side_effect = iter([('', ''), ('', ''), + ('', ''), ('', stdeer_msg), + ('', ''), ('', ''), + ('', ''), ('', '')]) + + expected = [mock.call('efibootmgr', '--version'), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI'), + mock.call('efibootmgr', '-b', '0004', '-B'), + mock.call('efibootmgr', '-b', '0005', '-B'), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('sync')] + + self.agent_extension.install_bootloader( + root_uuid=self.fake_root_uuid, + efi_system_part_uuid=None) + + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_execute.assert_has_calls(expected) + mock_utils_efi_part.assert_called_once_with(self.fake_dev) + self.assertEqual(8, mock_execute.call_count) + + @mock.patch.object(os.path, 'exists', lambda *_: False) + @mock.patch.object(iscsi, 'clean_up', autospec=True) + @mock.patch.object(image, '_get_efi_bootloaders', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + def test__add_multi_bootloaders( + self, mkdir_mock, mock_utils_efi_part, mock_partition, + mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='uefi') + ] + mock_partition.return_value = self.fake_dev + mock_utils_efi_part.return_value = '1' + mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI', + '\\WINDOWS\\system32\\winload.efi'] + + mock_execute.side_effect = iter([('', ''), ('', ''), + ('', ''), ('', ''), + ('', ''), ('', ''), + ('', '')]) + + expected = [mock.call('efibootmgr', '--version'), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic2', '-l', + '\\WINDOWS\\system32\\winload.efi'), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('sync')] + + self.agent_extension.install_bootloader( + root_uuid=self.fake_root_uuid, + efi_system_part_uuid=None) + + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_execute.assert_has_calls(expected) + mock_utils_efi_part.assert_called_once_with(self.fake_dev) + self.assertEqual(7, mock_execute.call_count) + @mock.patch.object(iscsi, 'clean_up', autospec=True) @mock.patch.object(image, '_install_grub2', autospec=True) - def test_install_bootloader_prep(self, mock_grub2, mock_iscsi_clean, - mock_execute, mock_dispatch): - mock_dispatch.return_value = self.fake_dev + def test__install_bootloader_prep(self, mock_grub2, mock_iscsi_clean, + mock_execute, mock_dispatch): + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='bios') + ] self.agent_extension.install_bootloader( root_uuid=self.fake_root_uuid, efi_system_part_uuid=None, prep_boot_part_uuid=self.fake_prep_boot_part_uuid) - mock_dispatch.assert_called_once_with('get_os_install_device') + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + self.assertEqual(2, mock_dispatch.call_count) mock_grub2.assert_called_once_with( self.fake_dev, root_uuid=self.fake_root_uuid, @@ -190,6 +392,7 @@ class TestImageExtension(base.IronicAgentTest): uuid=self.fake_prep_boot_part_uuid) self.assertFalse(mock_dispatch.called) + @mock.patch.object(os.path, 'exists', lambda *_: False) @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: True) @mock.patch.object(hardware, 'is_md_device', autospec=True) @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) @@ -523,3 +726,160 @@ class TestImageExtension(base.IronicAgentTest): mock_execute.return_value = (parted_output, '') result = image._is_bootloader_loaded(self.fake_dev) self.assertFalse(result) + + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=True) + def test__manage_uefi_no_partition(self, mock_utils_efi_part, + mock_get_part_uuid, + mock_execute, mock_dispatch): + mock_utils_efi_part.return_value = None + mock_get_part_uuid.return_value = self.fake_root_part + result = image._manage_uefi(self.fake_dev, self.fake_root_uuid) + self.assertFalse(result) + + @mock.patch.object(os.path, 'exists', lambda *_: False) + @mock.patch.object(image, '_get_efi_bootloaders', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + def test__manage_uefi(self, mkdir_mock, mock_utils_efi_part, + mock_get_part_uuid, mock_efi_bl, mock_execute, + mock_dispatch): + mock_utils_efi_part.return_value = '1' + mock_get_part_uuid.return_value = self.fake_dev + + mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI'] + + mock_execute.side_effect = iter([('', ''), ('', ''), + ('', ''), ('', ''), + ('', '')]) + + expected = [mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI'), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('sync')] + + result = image._manage_uefi(self.fake_dev, self.fake_root_uuid) + self.assertTrue(result) + mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_execute.assert_has_calls(expected) + self.assertEqual(5, mock_execute.call_count) + + @mock.patch.object(os.path, 'exists', lambda *_: False) + @mock.patch.object(image, '_get_efi_bootloaders', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + @mock.patch.object(utils, 'get_efi_part_on_device', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + def test__manage_uefi_wholedisk( + self, mkdir_mock, mock_utils_efi_part, + mock_get_part_uuid, mock_efi_bl, mock_execute, + mock_dispatch): + mock_utils_efi_part.return_value = '1' + mock_get_part_uuid.side_effect = Exception + + mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI'] + + mock_execute.side_effect = iter([('', ''), ('', ''), + ('', ''), ('', ''), + ('', '')]) + + expected = [mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', '1', '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI'), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('sync')] + + result = image._manage_uefi(self.fake_dev, None) + self.assertTrue(result) + mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi') + mock_execute.assert_has_calls(expected) + self.assertEqual(5, mock_execute.call_count) + + @mock.patch.object(os, 'walk', autospec=True) + @mock.patch.object(os, 'access', autospec=False) + def test__no_efi_bootloaders(self, mock_access, mock_walk, mock_execute, + mock_dispatch): + # No valid efi file. + mock_walk.return_value = [ + ('/boot/efi', ['EFI'], []), + ('/boot/efi/EFI', ['centos', 'BOOT'], []), + ('/boot/efi/EFI/centos', ['fw', 'fonts'], + ['shimx64-centos.efi', 'BOOT.CSV', 'BOOTX64.CSV', + 'MokManager.efi', 'mmx64.efi', 'shim.efi', 'fwupia32.efi', + 'fwupx64.efi', 'shimx64.efi', 'grubenv', 'grubx64.efi', + 'grub.cfg']), + ('/boot/efi/EFI/centos/fw', [], []), + ('/boot/efi/EFI/centos/fonts', [], ['unicode.pf2']), + ('/boot/efi/EFI/BOOT', [], []) + ] + + result = image._get_efi_bootloaders("/boot/efi") + self.assertEqual(result, []) + mock_access.assert_not_called() + + @mock.patch.object(os, 'walk', autospec=True) + @mock.patch.object(os, 'access', autospec=True) + def test__get_efi_bootloaders(self, mock_access, mock_walk, mock_execute, + mock_dispatch): + mock_walk.return_value = [ + ('/boot/efi', ['EFI'], []), + ('/boot/efi/EFI', ['centos', 'BOOT'], []), + ('/boot/efi/EFI/centos', ['fw', 'fonts'], + ['shimx64-centos.efi', 'BOOT.CSV', 'BOOTX64.CSV', + 'MokManager.efi', 'mmx64.efi', 'shim.efi', 'fwupia32.efi', + 'fwupx64.efi', 'shimx64.efi', 'grubenv', 'grubx64.efi', + 'grub.cfg']), + ('/boot/efi/EFI/centos/fw', [], []), + ('/boot/efi/EFI/centos/fonts', [], ['unicode.pf2']), + ('/boot/efi/EFI/BOOT', [], + ['BOOTX64.EFI', 'fallback.efi', 'fbx64.efi']) + ] + mock_access.return_value = True + result = image._get_efi_bootloaders("/boot/efi") + self.assertEqual(result[0], '\\EFI\\BOOT\\BOOTX64.EFI') + + @mock.patch.object(os, 'walk', autospec=True) + @mock.patch.object(os, 'access', autospec=True) + def test__get_windows_efi_bootloaders(self, mock_access, mock_walk, + mock_execute, mock_dispatch): + mock_walk.return_value = [ + ('/boot/efi', ['WINDOWS'], []), + ('/boot/efi/WINDOWS', ['system32'], []), + ('/boot/efi/WINDOWS/system32', [], + ['winload.efi']) + ] + mock_access.return_value = True + result = image._get_efi_bootloaders("/boot/efi") + self.assertEqual(result[0], '\\WINDOWS\\system32\\winload.efi') + + def test__run_efibootmgr_no_bootloaders(self, mock_execute, mock_dispatch): + result = image._run_efibootmgr([], self.fake_dev, + self.fake_efi_system_part) + expected = [] + self.assertIsNone(result) + mock_execute.assert_has_calls(expected) + + def test__run_efibootmgr(self, mock_execute, mock_dispatch): + result = image._run_efibootmgr(['\\EFI\\BOOT\\BOOTX64.EFI'], + self.fake_dev, + self.fake_efi_system_part) + expected = [mock.call('efibootmgr'), + mock.call('efibootmgr', '-c', '-d', self.fake_dev, + '-p', self.fake_efi_system_part, '-w', + '-L', 'ironic1', '-l', + '\\EFI\\BOOT\\BOOTX64.EFI')] + self.assertIsNone(result) + mock_execute.assert_has_calls(expected) diff --git a/ironic_python_agent/tests/unit/test_utils.py b/ironic_python_agent/tests/unit/test_utils.py index 94c3a31f..8eb505ef 100644 --- a/ironic_python_agent/tests/unit/test_utils.py +++ b/ironic_python_agent/tests/unit/test_utils.py @@ -45,6 +45,18 @@ Number Start End Size File system Name Flags 1 116MB 2361MB 2245MB ext4 ''' +PARTED_OUTPUT_UNFORMATTED_NOFS = '''Model: whatever +Disk /dev/sda: 480GB +Sector size (logical/physical): 512B/512B +Partition Table: gpt +Disk Flags: + +Number Start End Size File system Name Flags +1 1049kB 9437kB 8389kB ESP boot, esp +2 9437kB 17.8MB 8389kB BSP bios_grub +3 17.8MB 40.0GB 40.0GB +4 479GB 480GB 68.1MB +''' PARTED_OUTPUT_NO_EFI = '''Model: whatever Disk /dev/sda: 450GB @@ -631,6 +643,18 @@ class TestUtils(testtools.TestCase): self.assertEqual('15', ret) @mock.patch.object(utils, 'execute', autospec=True) + def test_get_efi_part_on_device_without_fs(self, mocked_execute): + parted_ret = PARTED_OUTPUT_UNFORMATTED_NOFS.format('gpt') + mocked_execute.side_effect = [ + (parted_ret, None) + ] + ret = utils.get_efi_part_on_device('/dev/sda') + mocked_execute.assert_has_calls( + [mock.call('parted', '-s', '/dev/sda', '--', 'print')] + ) + self.assertEqual('1', ret) + + @mock.patch.object(utils, 'execute', autospec=True) def test_get_efi_part_on_device_not_found(self, mocked_execute): mocked_execute.side_effect = [ (PARTED_OUTPUT_NO_EFI, None) |
