From 23d935b3a60741ddb52f076ffeacde9c37f17c8c Mon Sep 17 00:00:00 2001 From: Hironori Shiina Date: Fri, 26 May 2017 19:30:59 +0900 Subject: [PATCH] Ironic: Get IP address for volume connector When storage network for booting an instance from iSCSI volume is managed by neutron, an IP address for volume connector cannot be registered by an operator in advance. A MAC address can be registered as a volume connector for an Ironic node. The IP address may be required depending on cinder backend drivers. This patch gets an IP address for a volume connector based on a MAC address assigned to a volume connector in ironic. To bind VIFs to ironic ports before connecting a volume, VIFs are attached earlier with a new virt driver API. Nova can get an IP address assigned to a VIF attached to an ironic port by retrieving the port with the MAC address. Co-Authored-By: Satoru Moriya Implements: blueprint ironic-volume-connector-ip Change-Id: I999bbfc0e28ec43390298deb59e2b6f6e10bf8ea --- nova/compute/manager.py | 9 ++ nova/tests/unit/compute/test_compute_mgr.py | 28 ++++- nova/tests/unit/virt/ironic/test_driver.py | 116 +++++++++++++++--- nova/tests/unit/virt/ironic/utils.py | 3 + nova/virt/driver.py | 27 ++++ nova/virt/ironic/driver.py | 109 +++++++++++++++- ...-volume-connector-ip-467396a516dc668a.yaml | 11 ++ 7 files changed, 275 insertions(+), 28 deletions(-) create mode 100644 releasenotes/notes/bp-ironic-volume-connector-ip-467396a516dc668a.yaml diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 04b95f164f5..2451a04df88 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -2189,6 +2189,11 @@ def _build_resources(self, context, instance, requested_networks, reason=msg) try: + # Depending on a virt driver, some network configuration is + # necessary before preparing block devices. + self.driver.prepare_networks_before_block_device_mapping( + instance, network_info) + # Verify that all the BDMs have a device_name set and assign a # default to the ones missing it with the help of the driver. self._default_block_device_names(instance, image_meta, @@ -2209,11 +2214,14 @@ def _build_resources(self, context, instance, requested_networks, # Make sure the async call finishes if network_info is not None: network_info.wait(do_raise=False) + self.driver.clean_networks_preparation(instance, + network_info) except (exception.UnexpectedTaskStateError, exception.OverQuota, exception.InvalidBDM) as e: # Make sure the async call finishes if network_info is not None: network_info.wait(do_raise=False) + self.driver.clean_networks_preparation(instance, network_info) raise exception.BuildAbortException(instance_uuid=instance.uuid, reason=e.format_message()) except Exception: @@ -2222,6 +2230,7 @@ def _build_resources(self, context, instance, requested_networks, # Make sure the async call finishes if network_info is not None: network_info.wait(do_raise=False) + self.driver.clean_networks_preparation(instance, network_info) msg = _('Failure prepping block device.') raise exception.BuildAbortException(instance_uuid=instance.uuid, reason=msg) diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index ee17f54f595..1934c7543b8 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -5221,8 +5221,12 @@ def test_build_resources_buildabort_reraise(self, mock_notify, mock_save, @mock.patch.object(objects.Instance, 'save') @mock.patch.object(manager.ComputeManager, '_build_networks_for_instance') @mock.patch.object(manager.ComputeManager, '_prep_block_device') - def test_build_resources_reraises_on_failed_bdm_prep(self, mock_prep, - mock_build, mock_save): + @mock.patch.object(virt_driver.ComputeDriver, + 'prepare_networks_before_block_device_mapping') + @mock.patch.object(virt_driver.ComputeDriver, + 'clean_networks_preparation') + def test_build_resources_reraises_on_failed_bdm_prep( + self, mock_clean, mock_prepnet, mock_prep, mock_build, mock_save): mock_save.return_value = self.instance mock_build.return_value = self.network_info mock_prep.side_effect = test.TestingException @@ -5240,6 +5244,8 @@ def test_build_resources_reraises_on_failed_bdm_prep(self, mock_prep, self.requested_networks, self.security_groups) mock_prep.assert_called_once_with(self.context, self.instance, self.block_device_mapping) + mock_prepnet.assert_called_once_with(self.instance, self.network_info) + mock_clean.assert_called_once_with(self.instance, self.network_info) @mock.patch('nova.virt.block_device.attach_block_devices', side_effect=exception.VolumeNotCreated('oops!')) @@ -5293,7 +5299,12 @@ def test_validate_instance_group_policy_handles_hint_list(self, mock_get): instance, hints) mock_get.assert_called_once_with(self.context, uuids.group_hint) - def test_failed_bdm_prep_from_delete_raises_unexpected(self): + @mock.patch.object(virt_driver.ComputeDriver, + 'prepare_networks_before_block_device_mapping') + @mock.patch.object(virt_driver.ComputeDriver, + 'clean_networks_preparation') + def test_failed_bdm_prep_from_delete_raises_unexpected(self, mock_clean, + mock_prepnet): with test.nested( mock.patch.object(self.compute, '_build_networks_for_instance', @@ -5319,6 +5330,8 @@ def test_failed_bdm_prep_from_delete_raises_unexpected(self): self.requested_networks, self.security_groups)]) save.assert_has_calls([mock.call()]) + mock_prepnet.assert_called_once_with(self.instance, self.network_info) + mock_clean.assert_called_once_with(self.instance, self.network_info) @mock.patch.object(manager.ComputeManager, '_build_networks_for_instance') def test_build_resources_aborts_on_failed_network_alloc(self, mock_build): @@ -5430,8 +5443,13 @@ def test_build_resources_instance_not_found_before_yield( @mock.patch( 'nova.compute.manager.ComputeManager._build_networks_for_instance') @mock.patch('nova.objects.Instance.save') + @mock.patch.object(virt_driver.ComputeDriver, + 'prepare_networks_before_block_device_mapping') + @mock.patch.object(virt_driver.ComputeDriver, + 'clean_networks_preparation') def test_build_resources_unexpected_task_error_before_yield( - self, mock_save, mock_build_network, mock_info_wait): + self, mock_clean, mock_prepnet, mock_save, mock_build_network, + mock_info_wait): mock_build_network.return_value = self.network_info mock_save.side_effect = exception.UnexpectedTaskStateError( instance_uuid=uuids.instance, expected={}, actual={}) @@ -5445,6 +5463,8 @@ def test_build_resources_unexpected_task_error_before_yield( mock_build_network.assert_called_once_with(self.context, self.instance, self.requested_networks, self.security_groups) mock_info_wait.assert_called_once_with(do_raise=False) + mock_prepnet.assert_called_once_with(self.instance, self.network_info) + mock_clean.assert_called_once_with(self.instance, self.network_info) @mock.patch('nova.network.model.NetworkInfoAsyncWrapper.wait') @mock.patch( diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py index 351f0bb1d5d..cbaff6ed011 100644 --- a/nova/tests/unit/virt/ironic/test_driver.py +++ b/nova/tests/unit/virt/ironic/test_driver.py @@ -32,6 +32,7 @@ from nova.console import type as console_type from nova import context as nova_context from nova import exception +from nova.network import model as network_model from nova import objects from nova.objects import fields from nova import servicegroup @@ -1030,9 +1031,8 @@ def test_get_info_http_not_found(self, mock_gbiu): @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') @mock.patch.object(ironic_driver.IronicDriver, '_add_instance_info_to_node') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - def _test_spawn(self, mock_sf, mock_pvifs, mock_aiitn, mock_wait_active, + def _test_spawn(self, mock_sf, mock_aiitn, mock_wait_active, mock_avti, mock_node, mock_looping, mock_save): node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' node = ironic_utils.get_test_node(driver='fake', uuid=node_uuid) @@ -1059,7 +1059,6 @@ def _test_spawn(self, mock_sf, mock_pvifs, mock_aiitn, mock_wait_active, test.MatchType(objects.ImageMeta), fake_flavor, block_device_info=None) mock_avti.assert_called_once_with(self.ctx, instance, None) - mock_pvifs.assert_called_once_with(node, instance, None) mock_sf.assert_called_once_with(instance, None) mock_node.set_provision_state.assert_called_once_with(node_uuid, 'active', configdrive=mock.ANY) @@ -1099,11 +1098,10 @@ def test_spawn_with_configdrive(self, mock_required_by, mock_configdrive): @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') @mock.patch.object(ironic_driver.IronicDriver, '_add_instance_info_to_node') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - def test_spawn_destroyed_after_failure(self, mock_sf, mock_pvifs, - mock_aiitn, mock_wait_active, - mock_avti, mock_destroy, mock_node, + def test_spawn_destroyed_after_failure(self, mock_sf, mock_aiitn, + mock_wait_active, mock_avti, + mock_destroy, mock_node, mock_looping, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1356,10 +1354,9 @@ def test_spawn_node_driver_validation_fail(self, mock_avti, mock_node, @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy') def test_spawn_node_prepare_for_deploy_fail(self, mock_cleanup_deploy, - mock_pvifs, mock_sf, mock_avti, + mock_sf, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1389,9 +1386,8 @@ class TestException(Exception): @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_generate_configdrive') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') def test_spawn_node_configdrive_fail(self, - mock_pvifs, mock_sf, mock_configdrive, + mock_sf, mock_configdrive, mock_avti, mock_node, mock_save, mock_required_by): mock_required_by.return_value = True @@ -1422,10 +1418,9 @@ class TestException(Exception): @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy') def test_spawn_node_trigger_deploy_fail(self, mock_cleanup_deploy, - mock_pvifs, mock_sf, mock_avti, + mock_sf, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1451,10 +1446,9 @@ def test_spawn_node_trigger_deploy_fail(self, mock_cleanup_deploy, @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_cleanup_deploy') def test_spawn_node_trigger_deploy_fail2(self, mock_cleanup_deploy, - mock_pvifs, mock_sf, mock_avti, + mock_sf, mock_avti, mock_node, mock_required_by): mock_required_by.return_value = False node_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' @@ -1481,10 +1475,9 @@ def test_spawn_node_trigger_deploy_fail2(self, mock_cleanup_deploy, @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, 'destroy') def test_spawn_node_trigger_deploy_fail3(self, mock_destroy, - mock_pvifs, mock_sf, mock_avti, + mock_sf, mock_avti, mock_node, mock_looping, mock_required_by): mock_required_by.return_value = False @@ -1514,9 +1507,8 @@ def test_spawn_node_trigger_deploy_fail3(self, mock_destroy, @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.IronicDriver, '_add_volume_target_info') @mock.patch.object(ironic_driver.IronicDriver, '_wait_for_active') - @mock.patch.object(ironic_driver.IronicDriver, '_plug_vifs') @mock.patch.object(ironic_driver.IronicDriver, '_start_firewall') - def test_spawn_sets_default_ephemeral_device(self, mock_sf, mock_pvifs, + def test_spawn_sets_default_ephemeral_device(self, mock_sf, mock_wait, mock_avti, mock_node, mock_save, mock_looping, @@ -2192,8 +2184,13 @@ def test_get_volume_connector(self, mock_node): mock_node.list_volume_connectors.assert_called_once_with( node_uuid, detail=True) + @mock.patch.object(objects.instance.Instance, 'get_network_info') @mock.patch.object(FAKE_CLIENT, 'node') - def test_get_volume_connector_no_ip(self, mock_node): + @mock.patch.object(FAKE_CLIENT.port, 'list') + @mock.patch.object(FAKE_CLIENT.portgroup, 'list') + def _test_get_volume_connector_no_ip( + self, mac_specified, mock_pgroup, mock_port, mock_node, + mock_nw_info, portgroup_exist=False): node_uuid = uuids.node_uuid node_props = {'cpu_arch': 'x86_64'} node = ironic_utils.get_test_node(uuid=node_uuid, @@ -2201,20 +2198,99 @@ def test_get_volume_connector_no_ip(self, mock_node): connectors = [ironic_utils.get_test_volume_connector( node_uuid=node_uuid, type='iqn', connector_id='iqn.test')] + if mac_specified: + connectors.append(ironic_utils.get_test_volume_connector( + node_uuid=node_uuid, type='mac', + connector_id='11:22:33:44:55:66')) + fixed_ip = network_model.FixedIP(address='1.2.3.4', version=4) + subnet = network_model.Subnet(ips=[fixed_ip]) + network = network_model.Network(subnets=[subnet]) + vif = network_model.VIF( + id='aaaaaaaa-vv11-cccc-dddd-eeeeeeeeeeee', network=network) + expected_props = {'initiator': 'iqn.test', + 'ip': '1.2.3.4', + 'host': '1.2.3.4', 'multipath': False, 'os_type': 'baremetal', 'platform': 'x86_64'} mock_node.get.return_value = node mock_node.list_volume_connectors.return_value = connectors + mock_nw_info.return_value = [vif] instance = fake_instance.fake_instance_obj(self.ctx, node=node_uuid) + port = ironic_utils.get_test_port( + node_uuid=node_uuid, address='11:22:33:44:55:66', + internal_info={'tenant_vif_port_id': vif['id']}) + mock_port.return_value = [port] + if portgroup_exist: + portgroup = ironic_utils.get_test_portgroup( + node_uuid=node_uuid, address='11:22:33:44:55:66', + extra={'vif_port_id': vif['id']}) + mock_pgroup.return_value = [portgroup] + else: + mock_pgroup.return_value = [] props = self.driver.get_volume_connector(instance) self.assertEqual(expected_props, props) mock_node.get.assert_called_once_with(node_uuid) mock_node.list_volume_connectors.assert_called_once_with( node_uuid, detail=True) + if mac_specified: + mock_pgroup.assert_called_once_with( + node=node_uuid, address='11:22:33:44:55:66', detail=True) + if not portgroup_exist: + mock_port.assert_called_once_with( + node=node_uuid, address='11:22:33:44:55:66', detail=True) + else: + mock_port.assert_not_called() + else: + mock_pgroup.assert_not_called() + mock_port.assert_not_called() + + def test_get_volume_connector_no_ip_with_mac(self): + self._test_get_volume_connector_no_ip(True) + + def test_get_volume_connector_no_ip_with_mac_with_portgroup(self): + self._test_get_volume_connector_no_ip(True, portgroup_exist=True) + + def test_get_volume_connector_no_ip_without_mac(self): + self._test_get_volume_connector_no_ip(False) + + @mock.patch.object(ironic_driver.IronicDriver, 'plug_vifs') + def test_prepare_networks_before_block_device_mapping(self, mock_pvifs): + instance = fake_instance.fake_instance_obj(self.ctx) + network_info = utils.get_test_network_info() + self.driver.prepare_networks_before_block_device_mapping(instance, + network_info) + mock_pvifs.assert_called_once_with(instance, network_info) + + @mock.patch.object(ironic_driver.IronicDriver, 'plug_vifs') + def test_prepare_networks_before_block_device_mapping_error(self, + mock_pvifs): + instance = fake_instance.fake_instance_obj(self.ctx) + network_info = utils.get_test_network_info() + mock_pvifs.side_effect = ironic_exception.BadRequest('fake error') + self.assertRaises( + ironic_exception.BadRequest, + self.driver.prepare_networks_before_block_device_mapping, + instance, network_info) + mock_pvifs.assert_called_once_with(instance, network_info) + + @mock.patch.object(ironic_driver.IronicDriver, 'unplug_vifs') + def test_clean_networks_preparation(self, mock_upvifs): + instance = fake_instance.fake_instance_obj(self.ctx) + network_info = utils.get_test_network_info() + self.driver.clean_networks_preparation(instance, network_info) + mock_upvifs.assert_called_once_with(instance, network_info) + + @mock.patch.object(ironic_driver.IronicDriver, 'unplug_vifs') + def test_clean_networks_preparation_error(self, mock_upvifs): + instance = fake_instance.fake_instance_obj(self.ctx) + network_info = utils.get_test_network_info() + mock_upvifs.side_effect = ironic_exception.BadRequest('fake error') + self.driver.clean_networks_preparation(instance, network_info) + mock_upvifs.assert_called_once_with(instance, network_info) @mock.patch.object(FAKE_CLIENT, 'node') @mock.patch.object(ironic_driver.LOG, 'error') diff --git a/nova/tests/unit/virt/ironic/utils.py b/nova/tests/unit/virt/ironic/utils.py index 3066ec2d66c..733dc43568a 100644 --- a/nova/tests/unit/virt/ironic/utils.py +++ b/nova/tests/unit/virt/ironic/utils.py @@ -156,6 +156,9 @@ def delete(self, volume_target_id): class FakePortClient(object): + def list(self, address=None, node=None): + pass + def get(self, port_uuid): pass diff --git a/nova/virt/driver.py b/nova/virt/driver.py index c7f96eabde1..9f3219c2b52 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -505,6 +505,33 @@ def extend_volume(self, connection_info, instance): """ raise NotImplementedError() + def prepare_networks_before_block_device_mapping(self, instance, + network_info): + """Prepare networks before the block devices are mapped to instance. + + Drivers who need network information for block device preparation can + do some network preparation necessary for block device preparation. + + :param nova.objects.instance.Instance instance: + The instance whose networks are prepared. + :param nova.network.model.NetworkInfoAsyncWrapper network_info: + The network information of the given `instance`. + """ + pass + + def clean_networks_preparation(self, instance, network_info): + """Clean networks preparation when block device mapping is failed. + + Drivers who need network information for block device preparaion should + clean the preparation when block device mapping is failed. + + :param nova.objects.instance.Instance instance: + The instance whose networks are prepared. + :param nova.network.model.NetworkInfoAsyncWrapper network_info: + The network information of the given `instance`. + """ + pass + def attach_interface(self, context, instance, image_meta, vif): """Use hotplug to add a network interface to a running instance. diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py index 8d20740682a..004a33f0bab 100644 --- a/nova/virt/ironic/driver.py +++ b/nova/virt/ironic/driver.py @@ -1062,7 +1062,6 @@ def spawn(self, context, instance, image_meta, injected_files, # prepare for the deploy try: - self._plug_vifs(node, instance, network_info) self._start_firewall(instance, network_info) except Exception: with excutils.save_and_reraise_exception(): @@ -1782,6 +1781,44 @@ def get_serial_console(self, context, instance): def need_legacy_block_device_info(self): return False + def prepare_networks_before_block_device_mapping(self, instance, + network_info): + """Prepare networks before the block devices are mapped to instance. + + Plug VIFs before block device preparation. In case where storage + network is managed by neutron and a MAC address is specified as a + volume connector to a node, we can get the IP address assigned to + the connector. An IP address of volume connector may be required by + some volume backend drivers. For getting the IP address, VIFs need to + be plugged before block device preparation so that a VIF is assigned to + a MAC address. + """ + + try: + self.plug_vifs(instance, network_info) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Error preparing deploy for instance " + "%(instance)s on baremetal node %(node)s.", + {'instance': instance.uuid, + 'node': instance.node}, + instance=instance) + + def clean_networks_preparation(self, instance, network_info): + """Clean networks preparation when block device mapping is failed. + + Unplug VIFs when block device preparation is failed. + """ + + try: + self.unplug_vifs(instance, network_info) + except Exception as e: + LOG.warning('Error detaching VIF from node %(node)s ' + 'after deploy failed; %(reason)s', + {'node': instance.node, + 'reason': six.text_type(e)}, + instance=instance) + def get_volume_connector(self, instance): """Get connector information for the instance for attaching to volumes. @@ -1797,8 +1834,14 @@ def get_volume_connector(self, instance): 'host': hostname } + An IP address is set if a volume connector with type ip is assigned to + a node. An IP address is also set if a node has a volume connector with + type mac. An IP address is got from a VIF attached to an ironic port + or portgroup with the MAC address. Otherwise, an IP address of one + of VIFs is used. + :param instance: nova instance - :returns: A connector information dictionary + :return: A connector information dictionary """ node = self.ironicclient.call("node.get", instance.node) properties = self._parse_node_properties(node) @@ -1809,8 +1852,13 @@ def get_volume_connector(self, instance): values.setdefault(conn.type, []).append(conn.connector_id) props = {} - if values.get('ip'): - props['ip'] = props['host'] = values['ip'][0] + ip = self._get_volume_connector_ip(instance, node, values) + if ip: + LOG.debug('Volume connector IP address for node %(node)s is ' + '%(ip)s.', + {'node': node.uuid, 'ip': ip}, + instance=instance) + props['ip'] = props['host'] = ip if values.get('iqn'): props['initiator'] = values['iqn'][0] if values.get('wwpn'): @@ -1824,3 +1872,56 @@ def get_volume_connector(self, instance): # we should at least set the value to False. props['multipath'] = False return props + + def _get_volume_connector_ip(self, instance, node, values): + if values.get('ip'): + LOG.debug('Node %s has an IP address for volume connector', + node.uuid, instance=instance) + return values['ip'][0] + + vif_id = self._get_vif_from_macs(node, values.get('mac', []), instance) + + # retrieve VIF and get the IP address + nw_info = instance.get_network_info() + if vif_id: + fixed_ips = [ip for vif in nw_info if vif['id'] == vif_id + for ip in vif.fixed_ips()] + else: + fixed_ips = [ip for vif in nw_info for ip in vif.fixed_ips()] + fixed_ips_v4 = [ip for ip in fixed_ips if ip['version'] == 4] + if fixed_ips_v4: + return fixed_ips_v4[0]['address'] + elif fixed_ips: + return fixed_ips[0]['address'] + return None + + def _get_vif_from_macs(self, node, macs, instance): + """Get a VIF from specified MACs. + + Retrieve ports and portgroups which have specified MAC addresses and + return a UUID of a VIF attached to a port or a portgroup found first. + + :param node: The node object. + :param mac: A list of MAC addresses of volume connectors. + :param instance: nova instance, used for logging. + :return: A UUID of a VIF assigned to one of the MAC addresses. + """ + for mac in macs: + for method in ['portgroup.list', 'port.list']: + ports = self.ironicclient.call(method, + node=node.uuid, + address=mac, + detail=True) + for p in ports: + vif_id = (p.internal_info.get('tenant_vif_port_id') or + p.extra.get('vif_port_id')) + if vif_id: + LOG.debug('VIF %(vif)s for volume connector is ' + 'retrieved with MAC %(mac)s of node ' + '%(node)s', + {'vif': vif_id, + 'mac': mac, + 'node': node.uuid}, + instance=instance) + return vif_id + return None diff --git a/releasenotes/notes/bp-ironic-volume-connector-ip-467396a516dc668a.yaml b/releasenotes/notes/bp-ironic-volume-connector-ip-467396a516dc668a.yaml new file mode 100644 index 00000000000..34ce36a78bd --- /dev/null +++ b/releasenotes/notes/bp-ironic-volume-connector-ip-467396a516dc668a.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + When creating a baremetal instance with volumes, the ironic driver will + now pass an IP address of an iSCSI initiator to the block storage service + because the volume backend may require the IP address for access control. + If an IP address is set to an ironic node as a volume connector resource, + the address is used. If a node has MAC addresses as volume connector + resources, an IP address is retrieved from VIFs associated with the MAC + addresses. IPv4 addresses are given priority over IPv6 addresses if both + are available.