diff --git a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py index a8a3b1d37..76718b9db 100644 --- a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py @@ -25,6 +25,7 @@ import openstack from osc_lib import exceptions as oscexc from osc_lib.tests import utils as test_utils +from oslo_utils import units import yaml from tripleoclient import exceptions @@ -697,6 +698,14 @@ class TestImportNodeMultiArch(fakes.TestOvercloudNode): self._check_workflow_call(parsed_args, no_deploy_image=True) +@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy', + autospec=True, name='mock_bm') +@mock.patch('openstack.config', autospec=True, + name='mock_conf') +@mock.patch('openstack.connect', autospec=True, + name='mock_connect') +@mock.patch.object(openstack.connection, + 'Connection', autospec=True) class TestConfigureNode(fakes.TestOvercloudNode): def setUp(self): @@ -714,26 +723,62 @@ class TestConfigureNode(fakes.TestOvercloudNode): 'root_device_minimum_size': 4, 'overwrite_root_device_hints': False } + # Mock disks + self.disks = [ + {'name': '/dev/sda', 'size': 11 * units.Gi}, + {'name': '/dev/sdb', 'size': 2 * units.Gi}, + {'name': '/dev/sdc', 'size': 5 * units.Gi}, + {'name': '/dev/sdd', 'size': 21 * units.Gi}, + {'name': '/dev/sde', 'size': 13 * units.Gi}, + ] - def test_configure_all_manageable_nodes(self): + for i, disk in enumerate(self.disks): + disk['wwn'] = 'wwn%d' % i + disk['serial'] = 'serial%d' % i + + self.fake_baremetal_node = fakes.make_fake_machine( + machine_name='node1', + machine_id='4e540e11-1366-4b57-85d5-319d168d98a1' + ) + self.fake_baremetal_node2 = fakes.make_fake_machine( + machine_name='node2', + machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176' + ) + + def test_configure_all_manageable_nodes(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node])] parsed_args = self.check_parser(self.cmd, ['--all-manageable'], [('all_manageable', True)]) self.cmd.take_action(parsed_args) - def test_configure_specified_nodes(self): + def test_configure_specified_nodes(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm argslist = ['node_uuid1', 'node_uuid2'] verifylist = [('node_uuids', ['node_uuid1', 'node_uuid2'])] parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) - def test_configure_no_node_or_flag_specified(self): + def test_configure_no_node_or_flag_specified(self, mock_conn, + mock_connect, mock_conf, + mock_bm): self.assertRaises(test_utils.ParserException, self.check_parser, self.cmd, [], []) - def test_configure_uuids_and_all_both_specified(self): + def test_configure_uuids_and_all_both_specified(self, mock_conn, + mock_connect, mock_conf, + mock_bm): argslist = ['node_id1', 'node_id2', '--all-manageable'] verifylist = [('node_uuids', ['node_id1', 'node_id2']), ('all_manageable', True)] @@ -741,7 +786,23 @@ class TestConfigureNode(fakes.TestOvercloudNode): self.check_parser, self.cmd, argslist, verifylist) - def test_configure_kernel_and_ram(self): + def test_configure_kernel_and_ram(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal_introspection = mock_bm + + introspector_client = mock_bm.baremetal_introspection + introspector_client.get_introspection_data = mock_bm + introspector_client.get_introspection_data.return_value = { + 'inventory': {'disks': self.disks} + } + + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node])] + argslist = ['--all-manageable', '--deploy-ramdisk', 'test_ramdisk', '--deploy-kernel', 'test_kernel'] verifylist = [('deploy_kernel', 'test_kernel'), @@ -750,14 +811,35 @@ class TestConfigureNode(fakes.TestOvercloudNode): parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) - def test_configure_instance_boot_option(self): + def test_configure_instance_boot_option(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node])] argslist = ['--all-manageable', '--instance-boot-option', 'netboot'] verifylist = [('instance_boot_option', 'netboot')] parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) - def test_configure_root_device(self): + def test_configure_root_device(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal_introspection = mock_bm + + introspector_client = mock_bm.baremetal_introspection + introspector_client.get_introspection_data = mock_bm + introspector_client.get_introspection_data.return_value = { + 'inventory': {'disks': self.disks} + } + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node])] argslist = ['--all-manageable', '--root-device', 'smallest', '--root-device-minimum-size', '2', @@ -772,7 +854,23 @@ class TestConfigureNode(fakes.TestOvercloudNode): @mock.patch('tripleoclient.workflows.baremetal.' '_apply_root_device_strategy') def test_configure_specified_node_with_all_arguments( - self, mock_root_device): + self, mock_root_device, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal_introspection = mock_bm + + introspector_client = mock_bm.baremetal_introspection + introspector_client.get_introspection_data = mock_bm + introspector_client.get_introspection_data.return_value = { + 'inventory': {'disks': self.disks} + } + + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node])] + argslist = ['node_id', '--deploy-kernel', 'test_kernel', '--deploy-ramdisk', 'test_ramdisk', diff --git a/tripleoclient/tests/workflows/test_baremetal.py b/tripleoclient/tests/workflows/test_baremetal.py index 31b38c164..9e858ab12 100644 --- a/tripleoclient/tests/workflows/test_baremetal.py +++ b/tripleoclient/tests/workflows/test_baremetal.py @@ -108,12 +108,6 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution): node_timeout=1200, max_retries=1, retry_timeout=120, ) - def test_configure_success(self): - baremetal.configure(self.app.client_manager, node_uuids=[]) - - def test_configure_manageable_nodes_success(self): - baremetal.configure_manageable_nodes(self.app.client_manager) - def test_run_instance_boot_option(self): result = baremetal._configure_boot( self.app.client_manager, diff --git a/tripleoclient/v1/overcloud_node.py b/tripleoclient/v1/overcloud_node.py index b797d8793..f34536378 100644 --- a/tripleoclient/v1/overcloud_node.py +++ b/tripleoclient/v1/overcloud_node.py @@ -333,36 +333,31 @@ class ConfigureNode(command.Command): action='store_true', help=_('Whether to overwrite existing root device ' 'hints when --root-device is used.')) + parser.add_argument("--verbosity", + type=int, + default=1, + help=_("Print debug output during execution")) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) + conf = tb.TripleoConfigure( + kernel_name=parsed_args.deploy_kernel, + ramdisk_name=parsed_args.deploy_ramdisk, + instance_boot_option=parsed_args.instance_boot_option, + boot_mode=parsed_args.boot_mode, + root_device=parsed_args.root_device, + root_device_minimum_size=parsed_args.root_device_minimum_size, + overwrite_root_device_hints=( + parsed_args.overwrite_root_device_hints) + ) + if parsed_args.node_uuids: - baremetal.configure( - self.app.client_manager, - node_uuids=parsed_args.node_uuids, - kernel_name=parsed_args.deploy_kernel, - ramdisk_name=parsed_args.deploy_ramdisk, - instance_boot_option=parsed_args.instance_boot_option, - boot_mode=parsed_args.boot_mode, - root_device=parsed_args.root_device, - root_device_minimum_size=parsed_args.root_device_minimum_size, - overwrite_root_device_hints=( - parsed_args.overwrite_root_device_hints) - ) + conf.configure( + node_uuids=parsed_args.node_uuids) else: - baremetal.configure_manageable_nodes( - self.app.client_manager, - kernel_name=parsed_args.deploy_kernel, - ramdisk_name=parsed_args.deploy_ramdisk, - instance_boot_option=parsed_args.instance_boot_option, - boot_mode=parsed_args.boot_mode, - root_device=parsed_args.root_device, - root_device_minimum_size=parsed_args.root_device_minimum_size, - overwrite_root_device_hints=( - parsed_args.overwrite_root_device_hints) - ) + conf.configure_manageable_nodes() class DiscoverNode(command.Command): diff --git a/tripleoclient/workflows/baremetal.py b/tripleoclient/workflows/baremetal.py index 022f1f3ff..cf18c8773 100644 --- a/tripleoclient/workflows/baremetal.py +++ b/tripleoclient/workflows/baremetal.py @@ -351,105 +351,6 @@ def _apply_root_device_strategy(clients, node_uuid, strategy, {'node': node.uuid, 'dev': root_device, 'local_gb': new_size}) -def configure(clients, node_uuids, kernel_name=None, - ramdisk_name=None, instance_boot_option=None, - boot_mode=None, root_device=None, root_device_minimum_size=4, - overwrite_root_device_hints=False): - """Configure Node boot options. - - :param node_uuids: List of instance UUID(s). - :type node_uuids: List - - :param kernel_name: Kernel to use - :type kernel_name: String - - :param ramdisk_name: RAMDISK to use - :type ramdisk_name: String - - :param instance_boot_option: Boot options to use - :type instance_boot_option: String - - :param boot_mode: Boot mode to use - :type instance_boot_option: String - - :param root_device: Path (name) of the root device. - :type root_device: String - - :param root_device_minimum_size: Size of the given root device. - :type root_device_minimum_size: Integer - - :param overwrite_root_device_hints: Whether to overwrite existing root - device hints when `root_device` is - used. - :type overwrite_root_device_hints: Boolean - """ - - for node_uuid in node_uuids: - _configure_boot(clients, node_uuid, kernel_name, - ramdisk_name, instance_boot_option, boot_mode) - if root_device: - _apply_root_device_strategy( - clients, node_uuid, - strategy=root_device, - minimum_size=root_device_minimum_size, - overwrite=overwrite_root_device_hints) - print('Successfully configured the nodes.') - - -def configure_manageable_nodes(clients, kernel_name='bm-deploy-kernel', - ramdisk_name='bm-deploy-ramdisk', - instance_boot_option=None, boot_mode=None, - root_device=None, root_device_minimum_size=4, - overwrite_root_device_hints=False): - """Configure all manageable Nodes. - - kernel_name=parsed_args.deploy_kernel, - ramdisk_name=parsed_args.deploy_ramdisk, - instance_boot_option=parsed_args.instance_boot_option, - root_device=parsed_args.root_device, - root_device_minimum_size=parsed_args.root_device_minimum_size, - overwrite_root_device_hints=(parsed_args.overwrite_root_device_hints) - - :param kernel_name: Kernel to use - :type kernel_name: String - - :param ramdisk_name: RAMDISK to use - :type ramdisk_name: String - - :param instance_boot_option: Boot options to use - :type instance_boot_option: String - - :param boot_mode: Boot mode to use - :type instance_boot_option: String - - :param root_device: Path (name) of the root device. - :type root_device: String - - :param root_device_minimum_size: Size of the given root device. - :type root_device_minimum_size: Integer - - :param overwrite_root_device_hints: Whether to overwrite existing root - device hints when `root_device` is - used. - :type overwrite_root_device_hints: Boolean - """ - - configure( - clients=clients, - node_uuids=[ - i.uuid for i in clients.baremetal.node.list() - if i.provision_state == "manageable" and not i.maintenance - ], - kernel_name=kernel_name, - ramdisk_name=ramdisk_name, - instance_boot_option=instance_boot_option, - boot_mode=boot_mode, - root_device=root_device, - root_device_minimum_size=root_device_minimum_size, - overwrite_root_device_hints=overwrite_root_device_hints - ) - - def create_raid_configuration(clients, node_uuids, configuration, verbosity=0): """Create RAID configuration on nodes. diff --git a/tripleoclient/workflows/tripleo_baremetal.py b/tripleoclient/workflows/tripleo_baremetal.py index 313ff92b7..d85f686dc 100644 --- a/tripleoclient/workflows/tripleo_baremetal.py +++ b/tripleoclient/workflows/tripleo_baremetal.py @@ -19,7 +19,9 @@ from concurrent import futures from openstack import connect as sdkclient from openstack import exceptions from openstack.utils import iterate_timeout +from oslo_utils import units from tripleoclient import exceptions as ooo_exceptions +from tripleo_common.utils import nodes as node_utils class TripleoBaremetal(object): @@ -309,3 +311,216 @@ class TripleoClean(TripleoBaremetal): self.log.info(msg) except exceptions.OpenStackCloudException as err: self.log.error(str(err)) + + +class TripleoConfigure(TripleoBaremetal): + + """TripleoConfigure handles properties for the ironic nodes. + + We use this class to set the properties for each node such as the + kernel, ramdisk, boot device, root_device. + + :param kernel_name: The name of the kernel image we will deploy + :type kernel_name: String + + :param ramdisk_name: The name of the ramdisk image we will deploy + :type ramdisk_name: String + + :param instance_boot: Should the node boot from local disks or something + else + :type instance_boot: String + + :param boot_mode: Is this node using BIOS or UEFI + :type boot_mode: String + + :param: root_device: What is the root device for this node. eg /dev/sda + :type root_device: String + + :param root_device_minimum_size: What is the smallest disk we should + consider acceptable for deployment + :type root_device: Integer + + :param overwrite_root_device_hints: Should we overwrite existing root + device hints when root_device is used. + :type overwrite_root_device_hints: Boolean + """ + + log = logging.getLogger(__name__) + + def __init__(self, kernel_name: str = None, ramdisk_name: str = None, + instance_boot_option: str = None, boot_mode: str = None, + root_device: str = None, verbosity: int = 0, + root_device_minimum_size: int = 4, + overwrite_root_device_hints: bool = False): + + super().__init__(verbosity=verbosity) + self.kernel_name = kernel_name + self.ramdisk_name = ramdisk_name + self.instance_boot_option = instance_boot_option + self.boot_mode = boot_mode + self.root_device = root_device + self.root_device_minimum_size = root_device_minimum_size + self.overwrite_root_device_hints = overwrite_root_device_hints + + def _apply_root_device_strategy(self, node_uuid: List, + strategy: str, minimum_size: int = 4, + overwrite: bool = False): + clients = self.conn + node = clients.baremetal.find_node(node_uuid) + + if node.properties.get('root_device') and not overwrite: + # This is a correct situation, we still want to allow people to + # fine-tune the root device setting for a subset of nodes. + # However, issue a warning, so that they know which nodes were not + # updated during this run. + self.log.warning('Root device hints are already set for node ' + '{} and overwriting is not requested,' + ' skipping'.format(node.id)) + self.log.warning('You may unset them by running $ ironic ' + 'node-update {} remove ' + 'properties/root_device'.format(node.id)) + return + + inspector_client = self.conn.baremetal_introspection + baremetal_client = self.conn.baremetal + + try: + data = inspector_client.get_introspection_data(node.id) + except Exception: + raise exceptions.RootDeviceDetectionError( + f'No introspection data found for node {node.id}, ' + 'root device cannot be detected') + try: + disks = data['inventory']['disks'] + except KeyError: + raise exceptions.RootDeviceDetectionError( + f'Malformed introspection data for node {node.id}: ' + 'disks list is missing') + + minimum_size *= units.Gi + disks = [d for d in disks if d.get('size', 0) >= minimum_size] + + if not disks: + raise exceptions.RootDeviceDetectionError( + f'No suitable disks found for node {node.id}') + + if strategy == 'smallest': + disks.sort(key=lambda d: d['size']) + root_device = disks[0] + elif strategy == 'largest': + disks.sort(key=lambda d: d['size'], reverse=True) + root_device = disks[0] + else: + disk_names = [x.strip() for x in strategy.split(',')] + disks = {d['name']: d for d in disks} + for candidate in disk_names: + try: + root_device = disks['/dev/%s' % candidate] + except KeyError: + continue + else: + break + else: + raise exceptions.RootDeviceDetectionError( + f'Cannot find a disk with any of names {strategy} ' + f'for node {node.id}') + + hint = None + + for hint_name in ('wwn_with_extension', 'wwn', 'serial'): + if root_device.get(hint_name): + hint = {hint_name: root_device[hint_name]} + break + + if hint is None: + # I don't think it might actually happen, but just in case + raise exceptions.RootDeviceDetectionError( + f"Neither WWN nor serial number are known for device " + f"{root_device['name']} " + f"on node {node.id}; root device hints cannot be used") + + # During the introspection process we got local_gb assigned according + # to the default strategy. Now we need to update it. + new_size = root_device['size'] / units.Gi + # This -1 is what we always do to account for partitioning + new_size -= 1 + + baremetal_client.update_node( + node.id, + [{'op': 'add', 'path': '/properties/root_device', 'value': hint}, + {'op': 'add', 'path': '/properties/local_gb', 'value': new_size}]) + self.log.info('Updated root device for node %s, new device ' + 'is %s, new local_gb is %s', + node.id, root_device, new_size + ) + + def _configure_boot(self, node_uuid: List, + kernel_name: str = None, + ramdisk_name: str = None, + instance_boot_option: str = None, + boot_mode: str = None): + + baremetal_client = self.conn.baremetal + + image_ids = {'kernel': kernel_name, 'ramdisk': ramdisk_name} + node = baremetal_client.find_node(node_uuid) + capabilities = node.properties.get('capabilities', {}) + capabilities = node_utils.capabilities_to_dict(capabilities) + + if instance_boot_option is not None: + capabilities['boot_option'] = instance_boot_option + if boot_mode is not None: + capabilities['boot_mode'] = boot_mode + + capabilities = node_utils.dict_to_capabilities(capabilities) + baremetal_client.update_node(node.id, [ + { + 'op': 'add', + 'path': '/properties/capabilities', + 'value': capabilities, + }, + { + 'op': 'add', + 'path': '/driver_info/deploy_ramdisk', + 'value': image_ids['ramdisk'], + }, + { + 'op': 'add', + 'path': '/driver_info/deploy_kernel', + 'value': image_ids['kernel'], + }, + { + 'op': 'add', + 'path': '/driver_info/rescue_ramdisk', + 'value': image_ids['ramdisk'], + }, + { + 'op': 'add', + 'path': '/driver_info/rescue_kernel', + 'value': image_ids['kernel'], + }, + ]) + + def configure(self, node_uuids: List): + + """Configure Node boot options. + + :param node_uuids: List of instance UUID(s). + :type node_uuids: List + + """ + for node_uuid in node_uuids: + self._configure_boot(node_uuid, self.kernel_name, + self.ramdisk_name, self.instance_boot_option, + self.boot_mode) + if self.root_device: + self._apply_root_device_strategy( + node_uuid, + strategy=self.root_device, + minimum_size=self.root_device_minimum_size, + overwrite=self.overwrite_root_device_hints) + + self.log.info('Successfully configured the nodes.') + + def configure_manageable_nodes(self): + self.configure(node_uuids=self.all_manageable_nodes())