diff --git a/doc/source/ovn/gaps.rst b/doc/source/ovn/gaps.rst index 9c7d479fab6..7edd3a4abe8 100644 --- a/doc/source/ovn/gaps.rst +++ b/doc/source/ovn/gaps.rst @@ -61,10 +61,6 @@ at [1]_. The NDP proxy functionality for IPv6 addresses is not supported by OVN. -* Metadata via IPv6 - - The OVN metadata agent currently does not allow access via IPv6. - * East/West Fragmentation The core OVN implementation does not support fragmentation of East/West diff --git a/neutron/agent/ovn/metadata/agent.py b/neutron/agent/ovn/metadata/agent.py index 5e5faad0d54..944b37409b3 100644 --- a/neutron/agent/ovn/metadata/agent.py +++ b/neutron/agent/ovn/metadata/agent.py @@ -346,6 +346,9 @@ class MetadataAgent(object): resource_type='metadata') self._sb_idl = None self._post_fork_event = threading.Event() + # We'll restart all haproxy instances upon start so that they honor + # any potential changes in their configuration. + self.restarted_metadata_proxy_set = set() @property def sb_idl(self): @@ -557,8 +560,8 @@ class MetadataAgent(object): iptables_mgr.ipv4['mangle'].add_rule('POSTROUTING', rule, wrap=False) iptables_mgr.apply() - def _get_port_ips(self, port): - # Retrieve IPs from the port mac column which is in form + def _get_port_ip4_ips(self, port): + # Retrieve IPv4 addresses from the port mac column which is in form # [" ... "] if not port.mac: LOG.warning("Port %s MAC column is empty, cannot retrieve IP " @@ -569,7 +572,8 @@ class MetadataAgent(object): if not ips: LOG.debug("Port %s IP addresses were not retrieved from the " "Port_Binding MAC column %s", port.uuid, mac_field_attrs) - return ips + return [ip for ip in ips if ( + utils.get_ip_version(ip) == n_const.IP_VERSION_4)] def _active_subnets_cidrs(self, datapath_ports_ips, metadata_port_cidrs): active_subnets_cidrs = set() @@ -578,7 +582,7 @@ class MetadataAgent(object): # reconstruct IPNetwork objects repeatedly in the for loop metadata_cidrs_to_network_objects = { metadata_port_cidr: netaddr.IPNetwork(metadata_port_cidr) - for metadata_port_cidr in metadata_port_cidrs + for metadata_port_cidr in metadata_port_cidrs if metadata_port_cidr } for datapath_port_ip in datapath_ports_ips: @@ -591,16 +595,18 @@ class MetadataAgent(object): return active_subnets_cidrs def _process_cidrs(self, current_namespace_cidrs, - datapath_ports_ips, metadata_port_subnet_cidrs): + datapath_ports_ips, metadata_port_subnet_cidrs, lla): active_subnets_cidrs = self._active_subnets_cidrs( datapath_ports_ips, metadata_port_subnet_cidrs) cidrs_to_add = active_subnets_cidrs - current_namespace_cidrs - if n_const.METADATA_CIDR not in current_namespace_cidrs: - cidrs_to_add.add(n_const.METADATA_CIDR) - else: - active_subnets_cidrs.add(n_const.METADATA_CIDR) + # Make sure that all addresses, including the LLA, are present + for addr in (n_const.METADATA_CIDR, n_const.METADATA_V6_CIDR, lla): + if addr not in current_namespace_cidrs: + cidrs_to_add.add(addr) + else: + active_subnets_cidrs.add(addr) cidrs_to_delete = current_namespace_cidrs - active_subnets_cidrs @@ -611,7 +617,7 @@ class MetadataAgent(object): needed to provision namespace. Function will confirm that: - 1. Datapath metadata port has valid MAC and subnet CIDRs + 1. Datapath metadata port has valid MAC 2. There are datapath port IPs If any of those rules are not valid the nemaspace for the @@ -623,15 +629,11 @@ class MetadataAgent(object): datapath_uuid = str(datapath.uuid) metadata_port = self.sb_idl.get_metadata_port_network(datapath_uuid) - # If there's no metadata port or it doesn't have a MAC or IP - # addresses, then tear the namespace down if needed. This might happen - # when there are no subnets yet created so metadata port doesn't have - # an IP address. - if not (metadata_port and metadata_port.mac and - metadata_port.external_ids.get( - ovn_const.OVN_CIDRS_EXT_ID_KEY, None)): + # If there's no metadata port or it doesn't have a MAC address, then + # tear the namespace down if needed. + if not (metadata_port and metadata_port.mac): LOG.debug("There is no metadata port for network %s or it has no " - "MAC or IP addresses configured, tearing the namespace " + "MAC address configured, tearing the namespace " "down if needed", net_name) self.teardown_datapath(net_name) return @@ -657,7 +659,7 @@ class MetadataAgent(object): datapath_ports_ips = [] for chassis_port in self._vif_ports(chassis_ports): if str(chassis_port.datapath.uuid) == datapath_uuid: - datapath_ports_ips.extend(self._get_port_ips(chassis_port)) + datapath_ports_ips.extend(self._get_port_ip4_ips(chassis_port)) if not datapath_ports_ips: LOG.debug("No valid VIF ports were found for network %s, " @@ -705,34 +707,26 @@ class MetadataAgent(object): ip1, ip2 = ip_lib.IPWrapper().add_veth( veth_name[0], veth_name[1], namespace) + # Configure the MAC address. + ip2.link.set_address(metadata_port_info.mac) + # Make sure both ends of the VETH are up ip1.link.set_up() ip2.link.set_up() - # Configure the MAC address. - ip2.link.set_address(metadata_port_info.mac) - cidrs_to_add, cidrs_to_delete = self._process_cidrs( {dev['cidr'] for dev in ip2.addr.list()}, datapath_ports_ips, - metadata_port_info.ip_addresses + metadata_port_info.ip_addresses, + ip_lib.get_ipv6_lladdr(metadata_port_info.mac) ) + # Delete any non active addresses from the network namespace if cidrs_to_delete: ip2.addr.delete_multiple(list(cidrs_to_delete)) - # NOTE(dalvarez): metadata only works on IPv4. We're doing this - # extra check here because it could be that the metadata port has - # an IPv6 address if there's an IPv6 subnet with SLAAC in its - # network. Neutron IPAM will autoallocate an IPv6 address for every - # port in the network. - ipv4_cidrs_to_add = [ - cidr - for cidr in cidrs_to_add - if utils.get_ip_version(cidr) == n_const.IP_VERSION_4] - - if ipv4_cidrs_to_add: - ip2.addr.add_multiple(ipv4_cidrs_to_add) + if cidrs_to_add: + ip2.addr.add_multiple(list(cidrs_to_add)) # Check that this port is not attached to any other OVS bridge. This # can happen when the OVN bridge changes (for example, during a @@ -763,8 +757,14 @@ class MetadataAgent(object): # Ensure the correct checksum in the metadata traffic. self._ensure_datapath_checksum(namespace) + if net_name not in self.restarted_metadata_proxy_set: + metadata_driver.MetadataDriver.destroy_monitored_metadata_proxy( + self._process_monitor, net_name, self.conf, namespace) + self.restarted_metadata_proxy_set.add(net_name) + # Spawn metadata proxy if it's not already running. metadata_driver.MetadataDriver.spawn_monitored_metadata_proxy( self._process_monitor, namespace, n_const.METADATA_PORT, self.conf, bind_address=n_const.METADATA_V4_IP, - network_id=net_name) + network_id=net_name, bind_address_v6=n_const.METADATA_V6_IP, + bind_interface=veth_name[1]) diff --git a/neutron/agent/ovn/metadata/driver.py b/neutron/agent/ovn/metadata/driver.py index 6553a622d11..af82390e1da 100644 --- a/neutron/agent/ovn/metadata/driver.py +++ b/neutron/agent/ovn/metadata/driver.py @@ -19,6 +19,7 @@ import os import pwd from neutron.agent.linux import external_process +from neutron.agent.linux import ip_lib from neutron_lib import exceptions from oslo_config import cfg from oslo_log import log as logging @@ -40,6 +41,7 @@ _HEADER_CONFIG_TEMPLATE = """ _UNLIMITED_CONFIG_TEMPLATE = """ listen listener bind %(host)s:%(port)s + %(bind_v6_line)s server metadata %(unix_socket_path)s """ @@ -47,13 +49,16 @@ listen listener class HaproxyConfigurator(object): def __init__(self, network_id, router_id, unix_socket_path, host, port, user, group, state_path, pid_file, - rate_limiting_config): + rate_limiting_config, host_v6=None, + bind_interface=None): self.network_id = network_id self.router_id = router_id if network_id is None and router_id is None: raise exceptions.NetworkIdOrRouterIdRequiredError() self.host = host + self.host_v6 = host_v6 + self.bind_interface = bind_interface self.port = port self.user = user self.group = group @@ -102,6 +107,11 @@ class HaproxyConfigurator(object): 'log_tag': self.log_tag, 'bind_v6_line': '', } + if self.host_v6 and self.bind_interface: + cfg_info['bind_v6_line'] = ( + 'bind %s:%s interface %s' % ( + self.host_v6, self.port, self.bind_interface) + ) if self.network_id: cfg_info['res_type'] = 'Network' cfg_info['res_id'] = self.network_id @@ -158,7 +168,9 @@ class MetadataDriver(object): @classmethod def _get_metadata_proxy_callback(cls, bind_address, port, conf, - network_id=None, router_id=None): + network_id=None, router_id=None, + bind_address_v6=None, + bind_interface=None): def callback(pid_file): metadata_proxy_socket = conf.metadata_proxy_socket user, group = ( @@ -172,7 +184,9 @@ class MetadataDriver(object): group, conf.state_path, pid_file, - conf.metadata_rate_limiting) + conf.metadata_rate_limiting, + bind_address_v6, + bind_interface) haproxy.create_config_file() proxy_cmd = [HAPROXY_SERVICE, '-f', haproxy.cfg_path] @@ -183,14 +197,23 @@ class MetadataDriver(object): @classmethod def spawn_monitored_metadata_proxy(cls, monitor, ns_name, port, conf, bind_address="0.0.0.0", network_id=None, - router_id=None): + router_id=None, bind_address_v6=None, + bind_interface=None): uuid = network_id or router_id callback = cls._get_metadata_proxy_callback( bind_address, port, conf, network_id=network_id, - router_id=router_id) + router_id=router_id, bind_address_v6=bind_address_v6, + bind_interface=bind_interface) pm = cls._get_metadata_proxy_process_manager(uuid, conf, ns_name=ns_name, callback=callback) + if bind_interface is not None and bind_address_v6 is not None: + # HAProxy cannot bind() until IPv6 Duplicate Address Detection + # completes. We must wait until the address leaves its 'tentative' + # state. + ip_lib.IpAddrCommand( + parent=ip_lib.IPDevice(name=bind_interface, namespace=ns_name) + ).wait_until_address_ready(address=bind_address_v6) try: pm.enable() except exceptions.ProcessExecutionError as exec_err: diff --git a/neutron/agent/ovn/metadata/server.py b/neutron/agent/ovn/metadata/server.py index 75ee4c1e17a..466b70681a9 100644 --- a/neutron/agent/ovn/metadata/server.py +++ b/neutron/agent/ovn/metadata/server.py @@ -15,6 +15,8 @@ import threading import urllib +import netaddr + from neutron._i18n import _ from neutron.agent.linux import utils as agent_utils from neutron.agent.ovn.metadata import ovsdb @@ -25,8 +27,10 @@ from neutron.conf.agent.metadata import config from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources +from neutron_lib import constants from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import netutils import requests import webob @@ -95,11 +99,26 @@ class MetadataProxyHandler(object): return webob.exc.HTTPInternalServerError(explanation=explanation) def _get_instance_and_project_id(self, req): - remote_address = req.headers.get('X-Forwarded-For') + forwarded_for = req.headers.get('X-Forwarded-For') network_id = req.headers.get('X-OVN-Network-ID') + remote_mac = None + remote_ip = netaddr.IPAddress(forwarded_for) + if remote_ip.version == constants.IP_VERSION_6: + if remote_ip.is_ipv4_mapped(): + # When haproxy listens on v4 AND v6 then it inserts ipv4 + # addresses as ipv4-mapped v6 addresses into X-Forwarded-For. + forwarded_for = str(remote_ip.ipv4()) + if remote_ip.is_link_local(): + # When haproxy sees an ipv6 link-local client address + # (and sends that to us in X-Forwarded-For) we must rely + # on the EUI encoded in it, because that's all we can + # recognize. + remote_mac = str(netutils.get_mac_addr_by_ipv6(remote_ip)) + ports = self.sb_idl.get_network_port_bindings_by_ip(network_id, - remote_address) + forwarded_for, + mac=remote_mac) num_ports = len(ports) if num_ports == 1: external_ids = ports[0].external_ids @@ -107,14 +126,14 @@ class MetadataProxyHandler(object): external_ids[ovn_const.OVN_PROJID_EXT_ID_KEY]) elif num_ports == 0: LOG.error("No port found in network %s with IP address %s", - network_id, remote_address) + network_id, forwarded_for) elif num_ports > 1: port_uuids = ', '.join([str(port.uuid) for port in ports]) LOG.error("More than one port found in network %s with IP address " "%s. Please run the neutron-ovn-db-sync-util script as " "there seems to be inconsistent data between Neutron " "and OVN databases. OVN Port uuids: %s", network_id, - remote_address, port_uuids) + forwarded_for, port_uuids) return None, None diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py index c5cef98c011..c6565d4c2ed 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py @@ -926,7 +926,7 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): return cmd.UpdateChassisExtIdsCommand( self, chassis, {desc_key: description}, if_exists=False) - def get_network_port_bindings_by_ip(self, network, ip_address): + def get_network_port_bindings_by_ip(self, network, ip_address, mac=None): rows = self.db_list_rows('Port_Binding').execute(check_error=True) # TODO(twilson) It would be useful to have a db_find that takes a # comparison function @@ -935,12 +935,23 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): # If the port is not bound to any chassis it is not relevant if not port.chassis: return False + if not port.mac: + return False + # The MAC and IP address(es) are both present in port.mac as + # ["MAC IP {IP2...IPN}"]. If either one is present that is a + # match, since for link-local clients we can only match the MAC. + mac_ip = port.mac[0].split(' ') + address_match = False + if mac and mac in mac_ip: + address_match = True + elif ip_address in mac_ip: + address_match = True + if not address_match: + return False is_in_network = utils.get_network_name_from_datapath( port.datapath) == network - return (port.mac and - is_in_network and - (ip_address in port.mac[0].split(' '))) + return is_in_network return [r for r in rows if check_net_and_ip(r)] diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py index 833247237b4..d0b0e37b506 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_impl_idl.py @@ -16,7 +16,9 @@ import copy from unittest import mock import uuid +import netaddr from neutron_lib import constants +from oslo_utils import netutils from oslo_utils import uuidutils from ovsdbapp.backend.ovs_idl import connection from ovsdbapp import constants as const @@ -159,12 +161,10 @@ class TestSbApi(BaseOvnIdlTest): val = str(uuid.uuid4()) self.assertIsNone(self.api.get_metadata_port_network(val)) - def _create_bound_port_with_ip(self): + def _create_bound_port_with_ip(self, mac, ipaddr): chassis, switch = self._add_switch( self.data['chassis'][0]['name']) port, binding = self._add_port_to_switch(switch) - mac = 'de:ad:be:ef:4d:ad' - ipaddr = '192.0.2.1' mac_ip = '%s %s' % (mac, ipaddr) pb_update_event = events.WaitForUpdatePortBindingEvent( port.name, mac=[mac_ip]) @@ -174,16 +174,29 @@ class TestSbApi(BaseOvnIdlTest): self.assertTrue(pb_update_event.wait()) self.api.lsp_bind(port.name, chassis.name).execute(check_error=True) - return binding, ipaddr, switch + return binding, switch def test_get_network_port_bindings_by_ip(self): - binding, ipaddr, switch = self._create_bound_port_with_ip() + mac = 'de:ad:be:ef:4d:ad' + ipaddr = '192.0.2.1' + binding, switch = self._create_bound_port_with_ip(mac, ipaddr) + # binding, ipaddr, switch = self._create_bound_port_with_ip() + network_id = switch.name.replace('neutron-', '') + result = self.api.get_network_port_bindings_by_ip(network_id, ipaddr) + self.assertIn(binding, result) + + def test_get_network_port_bindings_by_ip_ipv6_ll(self): + ipaddr = 'fe80::99' + mac = str(netutils.get_mac_addr_by_ipv6(netaddr.IPAddress(ipaddr))) + binding, switch = self._create_bound_port_with_ip(mac, ipaddr) network_id = switch.name.replace('neutron-', '') result = self.api.get_network_port_bindings_by_ip(network_id, ipaddr) self.assertIn(binding, result) def test_get_network_port_bindings_by_ip_with_unbound_port(self): - binding, ipaddr, switch = self._create_bound_port_with_ip() + mac = 'de:ad:be:ef:4d:ad' + ipaddr = '192.0.2.1' + binding, switch = self._create_bound_port_with_ip(mac, ipaddr) unbound_port_name = utils.get_rand_device_name(prefix="port") mac_ip = "de:ad:be:ef:4d:ab %s" % ipaddr with self.nbapi.transaction(check_error=True) as txn: diff --git a/neutron/tests/unit/agent/ovn/metadata/test_agent.py b/neutron/tests/unit/agent/ovn/metadata/test_agent.py index 49ab6f1207e..73487e1b130 100644 --- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py +++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py @@ -208,14 +208,18 @@ class TestMetadataAgent(base.BaseTestCase): current_namespace_cidrs = set() datapath_port_ips = ['10.0.0.2', '10.0.0.3', '10.0.1.5'] metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] + lla = 'fe80::f816:3eff:fe63:8dc5/64' expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28', - n_const.METADATA_CIDR]) + n_const.METADATA_CIDR, + n_const.METADATA_V6_CIDR, + lla]) expected_cidrs_to_delete = set() actual_result = self.agent._process_cidrs(current_namespace_cidrs, datapath_port_ips, - metadaport_subnet_cidrs) + metadaport_subnet_cidrs, + lla) actual_cidrs_to_add, actual_cidrs_to_delete = actual_result self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) @@ -226,29 +230,36 @@ class TestMetadataAgent(base.BaseTestCase): current_namespace_cidrs = set([n_const.METADATA_CIDR]) datapath_port_ips = ['10.0.0.2', '10.0.0.3', '10.0.1.5'] metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] + lla = 'fe80::f816:3eff:fe63:8dc5/64' - expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28']) + expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28', + n_const.METADATA_V6_CIDR, lla]) expected_cidrs_to_delete = set() actual_result = self.agent._process_cidrs(current_namespace_cidrs, datapath_port_ips, - metadaport_subnet_cidrs) + metadaport_subnet_cidrs, + lla) actual_cidrs_to_add, actual_cidrs_to_delete = actual_result self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) self.assertSetEqual(actual_cidrs_to_delete, expected_cidrs_to_delete) def test__process_cidrs_when_current_namespace_contains_stale_cidr(self): - current_namespace_cidrs = set([n_const.METADATA_CIDR, '10.0.1.0/31']) + lla = 'fe80::f816:3eff:fe63:8dc5/64' + current_namespace_cidrs = set([n_const.METADATA_CIDR, '10.0.1.0/31', + lla]) datapath_port_ips = ['10.0.0.2', '10.0.0.3', '10.0.1.5'] metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] - expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28']) + expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28', + n_const.METADATA_V6_CIDR]) expected_cidrs_to_delete = set(['10.0.1.0/31']) actual_result = self.agent._process_cidrs(current_namespace_cidrs, datapath_port_ips, - metadaport_subnet_cidrs) + metadaport_subnet_cidrs, + lla) actual_cidrs_to_add, actual_cidrs_to_delete = actual_result self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) @@ -258,18 +269,22 @@ class TestMetadataAgent(base.BaseTestCase): """Current namespace cidrs contains stale cidrs and it is missing new required cidrs. """ + lla = 'fe80::f816:3eff:fe63:8dc5/64' current_namespace_cidrs = set([n_const.METADATA_CIDR, '10.0.1.0/31', - '10.0.1.0/28']) + '10.0.1.0/28', + 'fe77::/64', + lla]) datapath_port_ips = ['10.0.0.2', '10.0.1.5'] metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] - expected_cidrs_to_add = set(['10.0.0.0/30']) - expected_cidrs_to_delete = set(['10.0.1.0/31']) + expected_cidrs_to_add = set(['10.0.0.0/30', n_const.METADATA_V6_CIDR]) + expected_cidrs_to_delete = set(['10.0.1.0/31', 'fe77::/64']) actual_result = self.agent._process_cidrs(current_namespace_cidrs, datapath_port_ips, - metadaport_subnet_cidrs) + metadaport_subnet_cidrs, + lla) actual_cidrs_to_add, actual_cidrs_to_delete = actual_result self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) @@ -440,13 +455,17 @@ class TestMetadataAgent(base.BaseTestCase): ('external_ids', {'iface-id': metadaport_logical_port})) # Check that the metadata port has the IP addresses properly # configured and that IPv6 address has been skipped. - expected_call = [n_const.METADATA_CIDR, '10.0.0.1/23'] + expected_call = [n_const.METADATA_CIDR, n_const.METADATA_V6_CIDR, + '10.0.0.1/23', + ip_lib.get_ipv6_lladdr('aa:bb:cc:dd:ee:ff')] self.assertCountEqual(expected_call, ip_addr_add_multiple.call_args.args[0]) # Check that metadata proxy has been spawned spawn_mdp.assert_called_once_with( mock.ANY, nemaspace_name, 80, mock.ANY, - bind_address=n_const.METADATA_V4_IP, network_id=net_name) + bind_address=n_const.METADATA_V4_IP, network_id=net_name, + bind_address_v6=n_const.METADATA_V6_IP, + bind_interface='veth_1') mock_checksum.assert_called_once_with(nemaspace_name) def test__load_config(self): diff --git a/neutron/tests/unit/agent/ovn/metadata/test_server.py b/neutron/tests/unit/agent/ovn/metadata/test_server.py index 3f2b2cea5a0..c8ede299357 100644 --- a/neutron/tests/unit/agent/ovn/metadata/test_server.py +++ b/neutron/tests/unit/agent/ovn/metadata/test_server.py @@ -29,7 +29,7 @@ from neutron.conf.agent.ovn.metadata import config as ovn_meta_conf from neutron.tests import base OvnPortInfo = collections.namedtuple( - 'OvnPortInfo', ['external_ids', 'chassis']) + 'OvnPortInfo', ['external_ids', 'chassis', 'mac']) class ConfFixture(config_fixture.Config): @@ -88,14 +88,18 @@ class TestMetadataProxyHandler(base.BaseTestCase): self.assertIsInstance(retval, webob.exc.HTTPInternalServerError) self.assertEqual(len(self.log.mock_calls), 2) - def _get_instance_and_project_id_helper(self, headers, list_ports_retval, - network=None): - remote_address = '192.168.1.1' - headers['X-Forwarded-For'] = remote_address + def _get_instance_and_project_id_helper(self, forwarded_for, ports, + mac=None): + network_id = 'the_id' + headers = { + 'X-Forwarded-For': forwarded_for, + 'X-OVN-Network-ID': network_id + } + req = mock.Mock(headers=headers) def mock_get_network_port_bindings_by_ip(*args, **kwargs): - return list_ports_retval.pop(0) + return ports.pop(0) self.handler.sb_idl.get_network_port_bindings_by_ip.side_effect = ( mock_get_network_port_bindings_by_ip) @@ -103,40 +107,66 @@ class TestMetadataProxyHandler(base.BaseTestCase): instance_id, project_id = ( self.handler._get_instance_and_project_id(req)) - expected = [mock.call(network, '192.168.1.1')] + expected = [mock.call(network_id, forwarded_for, mac=mac)] self.handler.sb_idl.get_network_port_bindings_by_ip.assert_has_calls( expected) return (instance_id, project_id) - def test_get_instance_id_network_id(self): - network_id = 'the_id' - headers = { - 'X-OVN-Network-ID': network_id - } - + def test_get_instance_id_network_id_ipv4(self): + forwarded_for = '192.168.1.1' + mac = 'fa:16:3e:12:34:56' ovn_port = OvnPortInfo( external_ids={'neutron:device_id': 'device_id', 'neutron:project_id': 'project_id'}, - chassis=['chassis1']) + chassis=['chassis1'], + mac=mac) ports = [[ovn_port]] self.assertEqual( - self._get_instance_and_project_id_helper(headers, ports, - network='the_id'), + self._get_instance_and_project_id_helper(forwarded_for, ports), + ('device_id', 'project_id') + ) + + def test_get_instance_id_network_id_ipv6(self): + forwarded_for = '2001:db8::1' + mac = 'fa:16:3e:12:34:56' + ovn_port = OvnPortInfo( + external_ids={'neutron:device_id': 'device_id', + 'neutron:project_id': 'project_id'}, + chassis=['chassis1'], + mac=mac) + ports = [[ovn_port]] + + self.assertEqual( + self._get_instance_and_project_id_helper(forwarded_for, ports), + ('device_id', 'project_id') + ) + + def test_get_instance_id_network_id_ipv6_ll(self): + forwarded_for = 'fe80::99' + # This is the EUI encoded MAC based on the IPv6 address + forwarded_mac = '02:00:00:00:00:99' + ovn_port = OvnPortInfo( + external_ids={'neutron:device_id': 'device_id', + 'neutron:project_id': 'project_id'}, + chassis=['chassis1'], + mac=forwarded_mac) + ports = [[ovn_port]] + + # IPv6 and link-local, the MAC will be passed + self.assertEqual( + self._get_instance_and_project_id_helper(forwarded_for, ports, + mac=forwarded_mac), ('device_id', 'project_id') ) def test_get_instance_id_network_id_no_match(self): - network_id = 'the_id' - headers = { - 'X-OVN-Network-ID': network_id - } - + forwarded_for = '192.168.1.1' ports = [[]] expected = (None, None) - observed = self._get_instance_and_project_id_helper(headers, ports, - network='the_id') + observed = self._get_instance_and_project_id_helper(forwarded_for, + ports) self.assertEqual(expected, observed) def _proxy_request_test_helper(self, response_code=200, method='GET'): diff --git a/releasenotes/notes/ovn-metadata-v6-fe371854b09c8b56.yaml b/releasenotes/notes/ovn-metadata-v6-fe371854b09c8b56.yaml new file mode 100644 index 00000000000..4c1b263a44f --- /dev/null +++ b/releasenotes/notes/ovn-metadata-v6-fe371854b09c8b56.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + IPv6 Metadata support was added to the ML2/OVN driver. The agent now + provisions the ``fe80::a9fe:a9fe/128`` address to the OVN metadata + namespace and makes haproxy listen on it to serve metadata requests + to instances over IPv6.