[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:
parent
bfe05ec58f
commit
d9c8731af3
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)]
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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'):
|
||||
|
7
releasenotes/notes/ovn-metadata-v6-fe371854b09c8b56.yaml
Normal file
7
releasenotes/notes/ovn-metadata-v6-fe371854b09c8b56.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user