Merge "[ovn] Add support for IPv6 metadata"
This commit is contained in:
commit
cf1d5ea35c
|
@ -52,10 +52,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
|
||||
|
|
|
@ -344,6 +344,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):
|
||||
|
@ -543,8 +546,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 "
|
||||
|
@ -555,7 +558,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()
|
||||
|
@ -564,7 +568,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:
|
||||
|
@ -577,16 +581,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
|
||||
|
||||
|
@ -597,7 +603,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
|
||||
|
@ -609,15 +615,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
|
||||
|
@ -643,7 +645,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, "
|
||||
|
@ -691,34 +693,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
|
||||
|
@ -749,8 +743,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'):
|
||||
|
|
|
@ -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