[ovn] Add support for IPv6 metadata

This patch adds support for IPv6 metadata service in ML2/OVN.
The changes include:

- Add the 'fe80::a9fe:a9fe/128' address to the interface of the
  ovnmeta- namespace so that it's reachable from the guests

- Identify the port of the VM by looking up the source MAC address
  of the metadata request

- Restarts the haproxy instances to honor the configuration changes
  upon start of the metadata agent. In particular, haproxy now also
  binds on the 'fe80::a9fe:a9fe' address

When the VM requests metadata from its LLA, the traffic will reach
the ovnmeta namespace associated to its network.

The IPv6 metadata tests are passing and enabled in Tempest by
this patch:
https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/894027

Besides, this patch ensures that the link-local address of the
metadata interface is present so that the metadata IPv6 endpoint
is reachable. It also fixes a bug that was causing the wrong LLA
to be present as the interface was set `up` first prior to changing
the MAC address. Now this order is inverted so that the proper LLA
is configured.

Change-Id: Idcef6de33ed2a73cb3c426db1c55fa9cd06de63f
Signed-off-by: Daniel Alvarez Sanchez <dalvarez@redhat.com>
This commit is contained in:
Daniel Alvarez Sanchez 2023-09-07 09:01:42 +00:00 committed by Brian Haley
parent bfe05ec58f
commit d9c8731af3
9 changed files with 213 additions and 95 deletions

View File

@ -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

View File

@ -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
# ["<port_mac> <ip1> <ip2> ... <ipN>"]
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)
# 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(n_const.METADATA_CIDR)
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])

View File

@ -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:

View File

@ -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

View File

@ -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)]

View File

@ -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:

View File

@ -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):

View File

@ -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'):

View File

@ -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.