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.