diff --git a/ironic_python_agent/extensions/image.py b/ironic_python_agent/extensions/image.py index 9eb70c469..b6d1f0eaf 100644 --- a/ironic_python_agent/extensions/image.py +++ b/ironic_python_agent/extensions/image.py @@ -76,7 +76,8 @@ def _get_partition(device, uuid): raise errors.CommandExecutionError(error_msg) -def _install_grub2(device, root_uuid, efi_system_part_uuid=None): +def _install_grub2(device, root_uuid, efi_system_part_uuid=None, + prep_boot_part_uuid=None): """Install GRUB2 bootloader on a given device.""" LOG.debug("Installing GRUB2 bootloader on device %s", device) root_partition = _get_partition(device, uuid=root_uuid) @@ -92,6 +93,10 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None): efi_partition = _get_partition(device, uuid=efi_system_part_uuid) efi_partition_mount_point = os.path.join(path, "boot/efi") + # For power we want to install grub directly onto the PreP partition + if prep_boot_part_uuid: + device = _get_partition(device, uuid=prep_boot_part_uuid) + utils.execute('mount', root_partition, path) for fs in BIND_MOUNTS: utils.execute('mount', '-o', 'bind', fs, path + fs) @@ -196,13 +201,17 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None): class ImageExtension(base.BaseAgentExtension): @base.sync_command('install_bootloader') - def install_bootloader(self, root_uuid, efi_system_part_uuid=None): + def install_bootloader(self, root_uuid, efi_system_part_uuid=None, + prep_boot_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. + :param prep_boot_part_uuid: The UUID of the PReP Boot partition. + Used only for booting ppc64* partition images locally. In this + scenario the bootloader will be installed here. :raises: CommandExecutionError if the installation of the bootloader fails. :raises: DeviceNotFound if the root partition is not found. @@ -212,4 +221,5 @@ class ImageExtension(base.BaseAgentExtension): iscsi.clean_up(device) _install_grub2(device, root_uuid=root_uuid, - efi_system_part_uuid=efi_system_part_uuid) + efi_system_part_uuid=efi_system_part_uuid, + prep_boot_part_uuid=prep_boot_part_uuid) diff --git a/ironic_python_agent/extensions/standby.py b/ironic_python_agent/extensions/standby.py index 5ab84715f..2f224fe6a 100644 --- a/ironic_python_agent/extensions/standby.py +++ b/ironic_python_agent/extensions/standby.py @@ -73,6 +73,9 @@ def _write_partition_image(image, image_info, device): disk_label = image_info.get('disk_label', 'msdos') image_mb = disk_utils.get_image_mb(image) root_mb = image_info['root_mb'] + + cpu_arch = hardware.dispatch_to_managers('get_cpus').architecture + if image_mb > int(root_mb): msg = ('Root partition is too small for requested image. Image ' 'virtual size: {} MB, Root size: {} MB').format(image_mb, @@ -88,7 +91,8 @@ def _write_partition_image(image, image_info, device): configdrive=configdrive, boot_option=boot_option, boot_mode=boot_mode, - disk_label=disk_label) + disk_label=disk_label, + cpu_arch=cpu_arch) except processutils.ProcessExecutionError as e: raise errors.ImageWriteError(device, e.exit_code, e.stdout, e.stderr) diff --git a/ironic_python_agent/tests/unit/extensions/test_image.py b/ironic_python_agent/tests/unit/extensions/test_image.py index d23db27f7..347e24502 100644 --- a/ironic_python_agent/tests/unit/extensions/test_image.py +++ b/ironic_python_agent/tests/unit/extensions/test_image.py @@ -40,8 +40,10 @@ class TestImageExtension(base.IronicAgentTest): self.fake_dev = '/dev/fake' self.fake_efi_system_part = '/dev/fake1' self.fake_root_part = '/dev/fake2' + self.fake_prep_boot_part = '/dev/fake3' self.fake_root_uuid = '11111111-2222-3333-4444-555555555555' self.fake_efi_system_part_uuid = '45AB-2312' + self.fake_prep_boot_part_uuid = '76937797-3253-8843-999999999999' self.fake_dir = '/tmp/fake-dir' @mock.patch.object(iscsi, 'clean_up', autospec=True) @@ -53,7 +55,7 @@ class TestImageExtension(base.IronicAgentTest): 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=None) + 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) @@ -68,7 +70,25 @@ class TestImageExtension(base.IronicAgentTest): 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) + efi_system_part_uuid=self.fake_efi_system_part_uuid, + 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, '_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 + 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_grub2.assert_called_once_with( + self.fake_dev, + root_uuid=self.fake_root_uuid, + efi_system_part_uuid=None, + prep_boot_part_uuid=self.fake_prep_boot_part_uuid) mock_iscsi_clean.assert_called_once_with(self.fake_dev) @mock.patch.object(os, 'environ', autospec=True) @@ -108,6 +128,48 @@ class TestImageExtension(base.IronicAgentTest): uuid=self.fake_root_uuid) self.assertFalse(mock_dispatch.called) + @mock.patch.object(os, 'environ', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + def test__install_grub2_prep(self, mock_get_part_uuid, environ_mock, + mock_execute, mock_dispatch): + mock_get_part_uuid.side_effect = [self.fake_root_part, + self.fake_prep_boot_part] + environ_mock.get.return_value = '/sbin' + image._install_grub2(self.fake_dev, self.fake_root_uuid, + prep_boot_part_uuid=self.fake_prep_boot_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', '/proc', + self.fake_dir + '/proc'), + mock.call('mount', '-t', 'sysfs', 'none', + self.fake_dir + '/sys'), + mock.call(('chroot %s /bin/sh -c ' + '"grub-install %s"' % + (self.fake_dir, self.fake_prep_boot_part)), + shell=True, + env_variables={'PATH': '/sbin:/bin:/usr/sbin'}), + mock.call(('chroot %s /bin/sh -c ' + '"grub-mkconfig -o ' + '/boot/grub/grub.cfg"' % self.fake_dir), + shell=True, + env_variables={'PATH': '/sbin:/bin:/usr/sbin'}), + mock.call('umount', self.fake_dir + '/dev', + 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 + '/sys', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir, attempts=3, + delay_on_retry=True)] + 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_prep_boot_part_uuid) + self.assertFalse(mock_dispatch.called) + @mock.patch.object(os, 'environ', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True) @mock.patch.object(image, '_get_partition', autospec=True) diff --git a/ironic_python_agent/tests/unit/extensions/test_standby.py b/ironic_python_agent/tests/unit/extensions/test_standby.py index 149d1adb8..8b61301f6 100644 --- a/ironic_python_agent/tests/unit/extensions/test_standby.py +++ b/ironic_python_agent/tests/unit/extensions/test_standby.py @@ -19,6 +19,7 @@ from oslo_concurrency import processutils from ironic_python_agent import errors from ironic_python_agent.extensions import standby +from ironic_python_agent import hardware from ironic_python_agent.tests.unit import base @@ -58,6 +59,11 @@ class TestStandbyExtension(base.IronicAgentTest): def setUp(self): super(TestStandbyExtension, self).setUp() self.agent_extension = standby.StandbyExtension() + self.fake_cpu = hardware.CPU(model_name='fuzzypickles', + frequency=1024, + count=1, + architecture='generic', + flags='') def test_validate_image_info_success(self): standby._validate_image_info(None, _build_fake_image_info()) @@ -137,13 +143,15 @@ class TestStandbyExtension(base.IronicAgentTest): execute_mock.assert_called_once_with(*command, check_exit_code=[0]) + @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) @mock.patch('six.moves.builtins.open', autospec=True) @mock.patch('ironic_python_agent.utils.execute', autospec=True) @mock.patch('ironic_lib.disk_utils.get_image_mb', autospec=True) @mock.patch('ironic_lib.disk_utils.work_on_disk', autospec=True) def test_write_partition_image_exception(self, work_on_disk_mock, image_mb_mock, - execute_mock, open_mock): + execute_mock, open_mock, + dispatch_mock): image_info = _build_fake_partition_image_info() device = '/dev/sda' root_mb = image_info['root_mb'] @@ -156,10 +164,12 @@ class TestStandbyExtension(base.IronicAgentTest): boot_mode = image_info['deploy_boot_mode'] boot_option = image_info['boot_option'] disk_label = image_info['disk_label'] + cpu_arch = self.fake_cpu.architecture image_path = standby._image_location(image_info) image_mb_mock.return_value = 1 + dispatch_mock.return_value = self.fake_cpu exc = errors.ImageWriteError Exception_returned = processutils.ProcessExecutionError work_on_disk_mock.side_effect = Exception_returned @@ -176,15 +186,18 @@ class TestStandbyExtension(base.IronicAgentTest): preserve_ephemeral=pr_ep, boot_mode=boot_mode, boot_option=boot_option, - disk_label=disk_label) + disk_label=disk_label, + cpu_arch=cpu_arch) + @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) @mock.patch('six.moves.builtins.open', autospec=True) @mock.patch('ironic_python_agent.utils.execute', autospec=True) @mock.patch('ironic_lib.disk_utils.get_image_mb', autospec=True) @mock.patch('ironic_lib.disk_utils.work_on_disk', autospec=True) def test_write_partition_image_no_node_uuid(self, work_on_disk_mock, image_mb_mock, - execute_mock, open_mock): + execute_mock, open_mock, + dispatch_mock): image_info = _build_fake_partition_image_info() image_info['node_uuid'] = None device = '/dev/sda' @@ -198,10 +211,12 @@ class TestStandbyExtension(base.IronicAgentTest): boot_mode = image_info['deploy_boot_mode'] boot_option = image_info['boot_option'] disk_label = image_info['disk_label'] + cpu_arch = self.fake_cpu.architecture image_path = standby._image_location(image_info) image_mb_mock.return_value = 1 + dispatch_mock.return_value = self.fake_cpu uuids = {'root uuid': 'root_uuid'} expected_uuid = {'root uuid': 'root_uuid'} image_mb_mock.return_value = 1 @@ -218,11 +233,13 @@ class TestStandbyExtension(base.IronicAgentTest): preserve_ephemeral=pr_ep, boot_mode=boot_mode, boot_option=boot_option, - disk_label=disk_label) + disk_label=disk_label, + cpu_arch=cpu_arch) self.assertEqual(expected_uuid, work_on_disk_mock.return_value) self.assertIsNone(node_uuid) + @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) @mock.patch('six.moves.builtins.open', autospec=True) @mock.patch('ironic_python_agent.utils.execute', autospec=True) @mock.patch('ironic_lib.disk_utils.get_image_mb', autospec=True) @@ -231,12 +248,15 @@ class TestStandbyExtension(base.IronicAgentTest): work_on_disk_mock, image_mb_mock, execute_mock, - open_mock): + open_mock, + dispatch_mock): + dispatch_mock.return_value = self.fake_cpu image_info = _build_fake_partition_image_info() device = '/dev/sda' image_path = standby._image_location(image_info) image_mb_mock.return_value = 20 + exc = errors.InvalidCommandParamsError self.assertRaises(exc, standby._write_image, image_info, @@ -244,12 +264,13 @@ class TestStandbyExtension(base.IronicAgentTest): image_mb_mock.assert_called_once_with(image_path) self.assertFalse(work_on_disk_mock.called) + @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) @mock.patch('six.moves.builtins.open', autospec=True) @mock.patch('ironic_python_agent.utils.execute', autospec=True) @mock.patch('ironic_lib.disk_utils.work_on_disk', autospec=True) @mock.patch('ironic_lib.disk_utils.get_image_mb', autospec=True) def test_write_partition_image(self, image_mb_mock, work_on_disk_mock, - execute_mock, open_mock): + execute_mock, open_mock, dispatch_mock): image_info = _build_fake_partition_image_info() device = '/dev/sda' root_mb = image_info['root_mb'] @@ -262,11 +283,13 @@ class TestStandbyExtension(base.IronicAgentTest): boot_mode = image_info['deploy_boot_mode'] boot_option = image_info['boot_option'] disk_label = image_info['disk_label'] + cpu_arch = self.fake_cpu.architecture image_path = standby._image_location(image_info) uuids = {'root uuid': 'root_uuid'} expected_uuid = {'root uuid': 'root_uuid'} image_mb_mock.return_value = 1 + dispatch_mock.return_value = self.fake_cpu work_on_disk_mock.return_value = uuids standby._write_image(image_info, device) @@ -280,7 +303,8 @@ class TestStandbyExtension(base.IronicAgentTest): preserve_ephemeral=pr_ep, boot_mode=boot_mode, boot_option=boot_option, - disk_label=disk_label) + disk_label=disk_label, + cpu_arch=cpu_arch) self.assertEqual(expected_uuid, work_on_disk_mock.return_value) diff --git a/lower-constraints.txt b/lower-constraints.txt index edcb18681..4e8d4e082 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -24,7 +24,7 @@ greenlet==0.4.13 hacking==1.0.0 idna==2.6 imagesize==1.0.0 -ironic-lib==2.5.0 +ironic-lib==2.14.0 iso8601==0.1.11 Jinja2==2.10 keystoneauth1==3.4.0 diff --git a/releasenotes/notes/support-prep-partitions-5e273572ab7ce018.yaml b/releasenotes/notes/support-prep-partitions-5e273572ab7ce018.yaml new file mode 100644 index 000000000..09c4e1bd2 --- /dev/null +++ b/releasenotes/notes/support-prep-partitions-5e273572ab7ce018.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + If a PReP boot partition is created, and the machine being deployed to is + of ppc64le architecture, the grub2 bootloader will be installed directly + there. This enables booting partition images locally on ppc64* hardware. + + Using this feature requires ``ironic-lib`` version 2.14 as support to + create the PReP partition was introduced there. diff --git a/requirements.txt b/requirements.txt index f37c1b079..aeaad2752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,4 @@ rtslib-fb>=2.1.65 # Apache-2.0 six>=1.10.0 # MIT stevedore>=1.20.0 # Apache-2.0 WSME>=0.8.0 # MIT -ironic-lib>=2.5.0 # Apache-2.0 +ironic-lib>=2.14.0 # Apache-2.0