Support multiple IPv6 prefixes on internal router ports

(Patch set #3 for the multiple-ipv6-prefixes blueprint)

Provides support for adding multiple IPv6 subnets to an internal router
port. The limitation of one IPv4 subnet per internal router port
remains, though a port may contain one IPv4 subnet with any number of
IPv6 subnets.

This changes the behavior of both the router-interface-add and
router-interface-delete APIs. When router-interface-add is called with
an IPv6 subnet, the subnet will be added to an existing internal port
on the router with the same network ID if the existing port already has
one or more IPv6 subnets. Otherwise, a new port will be created on the
router for that subnet. When calling the router-interface-add with a
port (one that has already been created using the port-create command),
that port will be added to the router if it meets the following
conditions:

        1. The port has no more than one IPv4 subnet.
        2. If the port has any IPv6 subnets, it must not have the same
           network ID as an existing port on the router if the existing
           port has any IPv6 subnets.

If the router-interface-delete command is called with a subnet, that
subnet will be removed from the router port to which it belongs. If the
subnet is the last subnet on a port, the port itself will be deleted
from the router. If the router-interface-delete command is called with
a port, that port will be deleted from the router.

This change also allows the RADVD configuration to support advertising
multiple prefixes on a single router interface.

DocImpact

Change-Id: I7d4e8194815e626f1cfa267f77a3f2475fdfa3d1
Closes-Bug: #1439824
Partially-implements: blueprint multiple-ipv6-prefixes
This commit is contained in:
Andrew Boik 2015-03-23 11:21:11 -04:00
parent 22a938d78a
commit 54c05b500a
9 changed files with 597 additions and 108 deletions

View File

@ -323,6 +323,25 @@ class RouterInfo(object):
ip_devs = ip_wrapper.get_devices(exclude_loopback=True)
return [ip_dev.name for ip_dev in ip_devs]
@staticmethod
def _get_updated_ports(existing_ports, current_ports):
updated_ports = dict()
current_ports_dict = {p['id']: p for p in current_ports}
for existing_port in existing_ports:
current_port = current_ports_dict.get(existing_port['id'])
if current_port:
if sorted(existing_port['fixed_ips']) != (
sorted(current_port['fixed_ips'])):
updated_ports[current_port['id']] = current_port
return updated_ports
@staticmethod
def _port_has_ipv6_subnet(port):
if 'subnets' in port:
for subnet in port['subnets']:
if netaddr.IPNetwork(subnet['cidr']).version == 6:
return True
def _process_internal_ports(self):
existing_port_ids = set(p['id'] for p in self.internal_ports)
@ -334,29 +353,33 @@ class RouterInfo(object):
new_ports = [p for p in internal_ports if p['id'] in new_port_ids]
old_ports = [p for p in self.internal_ports
if p['id'] not in current_port_ids]
updated_ports = self._get_updated_ports(self.internal_ports,
internal_ports)
new_ipv6_port = False
old_ipv6_port = False
enable_ra = False
for p in new_ports:
self.internal_network_added(p)
self.internal_ports.append(p)
if not new_ipv6_port:
for subnet in p['subnets']:
if netaddr.IPNetwork(subnet['cidr']).version == 6:
new_ipv6_port = True
break
enable_ra = enable_ra or self._port_has_ipv6_subnet(p)
for p in old_ports:
self.internal_network_removed(p)
self.internal_ports.remove(p)
if not old_ipv6_port:
for subnet in p['subnets']:
if netaddr.IPNetwork(subnet['cidr']).version == 6:
old_ipv6_port = True
break
enable_ra = enable_ra or self._port_has_ipv6_subnet(p)
if updated_ports:
for index, p in enumerate(internal_ports):
if not updated_ports.get(p['id']):
continue
self.internal_ports[index] = updated_ports[p['id']]
interface_name = self.get_internal_device_name(p['id'])
ip_cidrs = common_utils.fixed_ip_cidrs(p['fixed_ips'])
self.driver.init_l3(interface_name, ip_cidrs=ip_cidrs,
namespace=self.ns_name)
enable_ra = enable_ra or self._port_has_ipv6_subnet(p)
# Enable RA
if new_ipv6_port or old_ipv6_port:
if enable_ra:
self.radvd.enable(internal_ports)
existing_devices = self._get_existing_devices()

View File

@ -43,21 +43,21 @@ CONFIG_TEMPLATE = jinja2.Template("""interface {{ interface_name }}
MinRtrAdvInterval 3;
MaxRtrAdvInterval 10;
{% if ra_mode == constants.DHCPV6_STATELESS %}
{% if constants.DHCPV6_STATELESS in ra_modes %}
AdvOtherConfigFlag on;
{% endif %}
{% if ra_mode == constants.DHCPV6_STATEFUL %}
{% if constants.DHCPV6_STATEFUL in ra_modes %}
AdvManagedFlag on;
{% endif %}
{% if ra_mode in (constants.IPV6_SLAAC, constants.DHCPV6_STATELESS) %}
{% for prefix in prefixes %}
prefix {{ prefix }}
{
AdvOnLink on;
AdvAutonomous on;
};
{% endif %}
{% endfor %}
};
""")
@ -79,16 +79,20 @@ class DaemonMonitor(object):
buf = six.StringIO()
for p in router_ports:
subnets = p.get('subnets', [])
for subnet in subnets:
prefix = subnet['cidr']
if netaddr.IPNetwork(prefix).version == 6:
interface_name = self._dev_name_helper(p['id'])
ra_mode = subnet['ipv6_ra_mode']
buf.write('%s' % CONFIG_TEMPLATE.render(
ra_mode=ra_mode,
interface_name=interface_name,
prefix=prefix,
constants=constants))
v6_subnets = [subnet for subnet in subnets if
netaddr.IPNetwork(subnet['cidr']).version == 6]
if not v6_subnets:
continue
ra_modes = {subnet['ipv6_ra_mode'] for subnet in v6_subnets}
auto_config_prefixes = [subnet['cidr'] for subnet in v6_subnets if
subnet['ipv6_ra_mode'] == constants.IPV6_SLAAC or
subnet['ipv6_ra_mode'] == constants.DHCPV6_STATELESS]
interface_name = self._dev_name_helper(p['id'])
buf.write('%s' % CONFIG_TEMPLATE.render(
ra_modes=list(ra_modes),
interface_name=interface_name,
prefixes=auto_config_prefixes,
constants=constants))
utils.replace_file(radvd_conf, buf.getvalue())
return radvd_conf

View File

@ -34,7 +34,7 @@ from neutron.db import model_base
from neutron.db import models_v2
from neutron.extensions import external_net
from neutron.extensions import l3
from neutron.i18n import _LI
from neutron.i18n import _LI, _LE
from neutron import manager
from neutron.openstack.common import uuidutils
from neutron.plugins.common import constants
@ -509,18 +509,50 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
raise n_exc.PortInUse(net_id=port['network_id'],
port_id=port['id'],
device_id=port['device_id'])
# Only allow one router port with IPv6 subnets per network id
if self._port_has_ipv6_address(port):
for existing_port in (rp.port for rp in router.attached_ports):
if (existing_port['network_id'] == port['network_id'] and
self._port_has_ipv6_address(existing_port)):
msg = _("Cannot have multiple router ports with the "
"same network id if both contain IPv6 "
"subnets. Existing port %(p)s has IPv6 "
"subnet(s) and network id %(nid)s")
raise n_exc.BadRequest(resource='router', msg=msg % {
'p': existing_port['id'],
'nid': existing_port['network_id']})
fixed_ips = [ip for ip in port['fixed_ips']]
if len(fixed_ips) != 1:
msg = _('Router port must have exactly one fixed IP')
subnets = []
for fixed_ip in fixed_ips:
subnet = self._core_plugin._get_subnet(context,
fixed_ip['subnet_id'])
subnets.append(subnet)
self._check_for_dup_router_subnet(context, router,
port['network_id'],
subnet['id'],
subnet['cidr'])
# Keep the restriction against multiple IPv4 subnets
if len([s for s in subnets if s['ip_version'] == 4]) > 1:
msg = _LE("Cannot have multiple "
"IPv4 subnets on router port")
raise n_exc.BadRequest(resource='router', msg=msg)
subnet_id = fixed_ips[0]['subnet_id']
subnet = self._core_plugin._get_subnet(context, subnet_id)
self._check_for_dup_router_subnet(context, router,
port['network_id'],
subnet['id'],
subnet['cidr'])
port.update({'device_id': router.id, 'device_owner': owner})
return port
return port, subnets
def _port_has_ipv6_address(self, port):
for fixed_ip in port['fixed_ips']:
if netaddr.IPNetwork(fixed_ip['ip_address']).version == 6:
return True
def _find_ipv6_router_port_by_network(self, router, net_id):
for port in router.attached_ports:
p = port['port']
if p['network_id'] == net_id and self._port_has_ipv6_address(p):
return port
def _add_interface_by_subnet(self, context, router, subnet_id, owner):
subnet = self._core_plugin._get_subnet(context, subnet_id)
@ -540,6 +572,18 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
fixed_ip = {'ip_address': subnet['gateway_ip'],
'subnet_id': subnet['id']}
if subnet['ip_version'] == 6:
# Add new prefix to an existing ipv6 port with the same network id
# if one exists
port = self._find_ipv6_router_port_by_network(router,
subnet['network_id'])
if port:
fixed_ips = list(port['port']['fixed_ips'])
fixed_ips.append(fixed_ip)
return self._core_plugin.update_port(context,
port['port_id'], {'port':
{'fixed_ips': fixed_ips}}), [subnet], False
return self._core_plugin.create_port(context, {
'port':
{'tenant_id': subnet['tenant_id'],
@ -549,16 +593,17 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
'admin_state_up': True,
'device_id': router.id,
'device_owner': owner,
'name': ''}})
'name': ''}}), [subnet], True
@staticmethod
def _make_router_interface_info(
router_id, tenant_id, port_id, subnet_id):
router_id, tenant_id, port_id, subnet_id, subnet_ids):
return {
'id': router_id,
'tenant_id': tenant_id,
'port_id': port_id,
'subnet_id': subnet_id
'subnet_id': subnet_id, # deprecated by IPv6 multi-prefix
'subnet_ids': subnet_ids
}
def add_router_interface(self, context, router_id, interface_info):
@ -566,26 +611,30 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
add_by_port, add_by_sub = self._validate_interface_info(interface_info)
device_owner = self._get_device_owner(context, router_id)
# This should be True unless adding an IPv6 prefix to an existing port
new_port = True
if add_by_port:
port = self._add_interface_by_port(
context, router, interface_info['port_id'], device_owner)
port, subnets = self._add_interface_by_port(
context, router, interface_info['port_id'], device_owner)
# add_by_subnet is not used here, because the validation logic of
# _validate_interface_info ensures that either of add_by_* is True.
else:
port = self._add_interface_by_subnet(
context, router, interface_info['subnet_id'], device_owner)
port, subnets, new_port = self._add_interface_by_subnet(
context, router, interface_info['subnet_id'], device_owner)
with context.session.begin(subtransactions=True):
router_port = RouterPort(
port_id=port['id'],
router_id=router.id,
port_type=device_owner
)
context.session.add(router_port)
if new_port:
with context.session.begin(subtransactions=True):
router_port = RouterPort(
port_id=port['id'],
router_id=router.id,
port_type=device_owner
)
context.session.add(router_port)
return self._make_router_interface_info(
router.id, port['tenant_id'], port['id'],
port['fixed_ips'][0]['subnet_id'])
router.id, port['tenant_id'], port['id'], subnets[-1]['id'],
[subnet['id'] for subnet in subnets])
def _confirm_router_interface_not_in_use(self, context, router_id,
subnet_id):
@ -621,16 +670,19 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
except exc.NoResultFound:
raise l3.RouterInterfaceNotFound(router_id=router_id,
port_id=port_id)
port_subnet_id = port_db['fixed_ips'][0]['subnet_id']
if subnet_id and port_subnet_id != subnet_id:
port_subnet_ids = [fixed_ip['subnet_id']
for fixed_ip in port_db['fixed_ips']]
if subnet_id and subnet_id not in port_subnet_ids:
raise n_exc.SubnetMismatchForPort(
port_id=port_id, subnet_id=subnet_id)
subnet = self._core_plugin._get_subnet(context, port_subnet_id)
self._confirm_router_interface_not_in_use(
context, router_id, port_subnet_id)
subnets = [self._core_plugin._get_subnet(context, port_subnet_id)
for port_subnet_id in port_subnet_ids]
for port_subnet_id in port_subnet_ids:
self._confirm_router_interface_not_in_use(
context, router_id, port_subnet_id)
self._core_plugin.delete_port(context, port_db['id'],
l3_port_check=False)
return (port_db, subnet)
return (port_db, subnets)
def _remove_interface_by_subnet(self, context,
router_id, subnet_id, owner):
@ -647,10 +699,20 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
)
for p in ports:
if p['fixed_ips'][0]['subnet_id'] == subnet_id:
port_subnets = [fip['subnet_id'] for fip in p['fixed_ips']]
if subnet_id in port_subnets and len(port_subnets) > 1:
# multiple prefix port - delete prefix from port
fixed_ips = [fip for fip in p['fixed_ips'] if
fip['subnet_id'] != subnet_id]
self._core_plugin.update_port(context, p['id'],
{'port':
{'fixed_ips': fixed_ips}})
return (p, [subnet])
elif subnet_id in port_subnets:
# only one subnet on port - delete the port
self._core_plugin.delete_port(context, p['id'],
l3_port_check=False)
return (p, subnet)
return (p, [subnet])
except exc.NoResultFound:
pass
raise l3.RouterInterfaceNotFoundForSubnet(router_id=router_id,
@ -664,18 +726,20 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
subnet_id = interface_info.get('subnet_id')
device_owner = self._get_device_owner(context, router_id)
if remove_by_port:
port, subnet = self._remove_interface_by_port(context, router_id,
port_id, subnet_id,
device_owner)
port, subnets = self._remove_interface_by_port(context, router_id,
port_id, subnet_id,
device_owner)
# remove_by_subnet is not used here, because the validation logic of
# _validate_interface_info ensures that at least one of remote_by_*
# is True.
else:
port, subnet = self._remove_interface_by_subnet(
context, router_id, subnet_id, device_owner)
port, subnets = self._remove_interface_by_subnet(
context, router_id, subnet_id, device_owner)
return self._make_router_interface_info(router_id, port['tenant_id'],
port['id'], subnet['id'])
port['id'], subnets[0]['id'],
[subnet['id'] for subnet in
subnets])
def _get_floatingip(self, context, id):
try:

View File

@ -278,29 +278,33 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
router = self._get_router(context, router_id)
device_owner = self._get_device_owner(context, router)
# This should be True unless adding an IPv6 prefix to an existing port
new_port = True
if add_by_port:
port = self._add_interface_by_port(
context, router, interface_info['port_id'], device_owner)
port, subnets = self._add_interface_by_port(
context, router, interface_info['port_id'], device_owner)
elif add_by_sub:
port = self._add_interface_by_subnet(
context, router, interface_info['subnet_id'], device_owner)
port, subnets, new_port = self._add_interface_by_subnet(
context, router, interface_info['subnet_id'], device_owner)
with context.session.begin(subtransactions=True):
router_port = l3_db.RouterPort(
port_id=port['id'],
router_id=router.id,
port_type=device_owner
)
context.session.add(router_port)
if new_port:
with context.session.begin(subtransactions=True):
router_port = l3_db.RouterPort(
port_id=port['id'],
router_id=router.id,
port_type=device_owner
)
context.session.add(router_port)
if router.extra_attributes.distributed and router.gw_port:
self.add_csnat_router_interface_port(
context.elevated(), router, port['network_id'],
port['fixed_ips'][0]['subnet_id'])
if router.extra_attributes.distributed and router.gw_port:
self.add_csnat_router_interface_port(
context.elevated(), router, port['network_id'],
port['fixed_ips'][-1]['subnet_id'])
router_interface_info = self._make_router_interface_info(
router_id, port['tenant_id'], port['id'],
port['fixed_ips'][0]['subnet_id'])
router_id, port['tenant_id'], port['id'], subnets[-1]['id'],
[subnet['id'] for subnet in subnets])
self.notify_router_interface_action(
context, router_interface_info, 'add')
return router_interface_info
@ -315,14 +319,14 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
device_owner = self._get_device_owner(context, router)
if remove_by_port:
port, subnet = self._remove_interface_by_port(
context, router_id, port_id, subnet_id, device_owner)
port, subnets = self._remove_interface_by_port(
context, router_id, port_id, subnet_id, device_owner)
# remove_by_subnet is not used here, because the validation logic of
# _validate_interface_info ensures that at least one of remote_by_*
# is True.
else:
port, subnet = self._remove_interface_by_subnet(
context, router_id, subnet_id, device_owner)
port, subnets = self._remove_interface_by_subnet(
context, router_id, subnet_id, device_owner)
if router.extra_attributes.distributed:
if router.gw_port:
@ -339,8 +343,8 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
context, l3_agent['id'], router_id)
router_interface_info = self._make_router_interface_info(
router_id, port['tenant_id'], port['id'],
port['fixed_ips'][0]['subnet_id'])
router_id, port['tenant_id'], port['id'], subnets[0]['id'],
[subnet['id'] for subnet in subnets])
self.notify_router_interface_action(
context, router_interface_info, 'remove')
return router_interface_info

View File

@ -542,6 +542,12 @@ class SdnvePluginV2(db_base_plugin_v2.NeutronDbPluginV2,
"failed to add the interface in the roll back."
" of a remove_router_interface operation"))
def _find_router_port_by_subnet_id(self, ports, subnet_id):
for p in ports:
subnet_ids = [fip['subnet_id'] for fip in p['fixed_ips']]
if subnet_id in subnet_ids:
return p['id']
@_ha
def remove_router_interface(self, context, router_id, interface_info):
LOG.debug("Remove router interface in progress: "
@ -576,7 +582,14 @@ class SdnvePluginV2(db_base_plugin_v2.NeutronDbPluginV2,
'network_id': [subnet['network_id']]}
ports = self.get_ports(context, filters=df)
if ports:
pid = ports[0]['id']
pid = self._find_router_port_by_subnet_id(ports, subnet_id)
if not pid:
raise sdnve_exc.SdnveException(
msg=(_('Update router-remove-interface '
'failed SDN-VE: subnet %(sid) is not '
'associated with any ports on router '
'%(rid)'), {'sid': subnet_id,
'rid': router_id}))
interface_info['port_id'] = pid
msg = ("SdnvePluginV2.remove_router_interface "
"subnet_id: %(sid)s port_id: %(pid)s")
@ -593,6 +606,11 @@ class SdnvePluginV2(db_base_plugin_v2.NeutronDbPluginV2,
session = context.session
with session.begin(subtransactions=True):
try:
if not port_id:
# port_id was not originally given in interface_info,
# so we want to remove the interface by subnet instead
# of port
del interface_info['port_id']
info = super(SdnvePluginV2, self).remove_router_interface(
context, router_id, interface_info)
except Exception:

View File

@ -150,6 +150,13 @@ class L3AgentTestFramework(base.BaseOVSLinuxTestCase):
'host': host}
router.router[l3_constants.FLOATINGIP_KEY].append(fip)
def _add_internal_interface_by_subnet(self, router, count=1,
ip_version=4,
ipv6_subnet_modes=None,
interface_id=None):
return test_l3_agent.router_append_subnet(router, count,
ip_version, ipv6_subnet_modes, interface_id)
def _namespace_exists(self, namespace):
ip = ip_lib.IPWrapper(namespace=namespace)
return ip.netns.exists(namespace)
@ -543,6 +550,14 @@ class L3AgentTestCase(L3AgentTestFramework):
v6_ext_gw_with_sub))
router = self.manage_router(self.agent, router_info)
# Add multiple-IPv6-prefix internal router port
slaac = l3_constants.IPV6_SLAAC
slaac_mode = {'ra_mode': slaac, 'address_mode': slaac}
subnet_modes = [slaac_mode] * 2
self._add_internal_interface_by_subnet(router.router, count=2,
ip_version=6, ipv6_subnet_modes=subnet_modes)
router.process(self.agent)
if enable_ha:
port = router.get_ex_gw_port()
interface_name = router.get_external_device_name(port['id'])

View File

@ -507,7 +507,7 @@ class L3DvrTestCase(testlib_api.SqlTestCase):
mkintf, notify):
grtr.return_value = router
gdev.return_value = mock.Mock()
rmintf.return_value = (mock.MagicMock(), mock.Mock())
rmintf.return_value = (mock.MagicMock(), mock.MagicMock())
mkintf.return_value = mock.Mock()
gplugin.return_value = {plugin_const.L3_ROUTER_NAT: plugin}
delintf.return_value = None

View File

@ -17,6 +17,8 @@ import contextlib
import copy
import eventlet
from itertools import chain as iter_chain
from itertools import combinations as iter_combinations
import mock
import netaddr
from oslo_log import log
@ -106,6 +108,75 @@ def router_append_interface(router, count=1, ip_version=4, ra_mode=None,
mac_address.value += 1
def router_append_subnet(router, count=1, ip_version=4,
ipv6_subnet_modes=None, interface_id=None):
if ip_version == 6:
subnet_mode_none = {'ra_mode': None, 'address_mode': None}
if not ipv6_subnet_modes:
ipv6_subnet_modes = [subnet_mode_none] * count
elif len(ipv6_subnet_modes) != count:
ipv6_subnet_modes.extend([subnet_mode_none for i in
xrange(len(ipv6_subnet_modes), count)])
if ip_version == 4:
ip_pool = '35.4.%i.4'
cidr_pool = '35.4.%i.0/24'
prefixlen = 24
gw_pool = '35.4.%i.1'
elif ip_version == 6:
ip_pool = 'fd01:%x::6'
cidr_pool = 'fd01:%x::/64'
prefixlen = 64
gw_pool = 'fd01:%x::1'
else:
raise ValueError("Invalid ip_version: %s" % ip_version)
interfaces = copy.deepcopy(router.get(l3_constants.INTERFACE_KEY, []))
if interface_id:
try:
interface = (i for i in interfaces
if i['id'] == interface_id).next()
except StopIteration:
raise ValueError("interface_id not found")
fixed_ips, subnets = interface['fixed_ips'], interface['subnets']
else:
interface = None
fixed_ips, subnets = [], []
num_existing_subnets = len(subnets)
for i in xrange(count):
subnet_id = _uuid()
fixed_ips.append(
{'ip_address': ip_pool % (i + num_existing_subnets),
'subnet_id': subnet_id,
'prefixlen': prefixlen})
subnets.append(
{'id': subnet_id,
'cidr': cidr_pool % (i + num_existing_subnets),
'gateway_ip': gw_pool % (i + num_existing_subnets),
'ipv6_ra_mode': ipv6_subnet_modes[i]['ra_mode'],
'ipv6_address_mode': ipv6_subnet_modes[i]['address_mode']})
if interface:
# Update old interface
index = interfaces.index(interface)
interfaces[index].update({'fixed_ips': fixed_ips, 'subnets': subnets})
else:
# New interface appended to interfaces list
mac_address = netaddr.EUI('ca:fe:de:ad:be:ef')
mac_address.dialect = netaddr.mac_unix
interfaces.append(
{'id': _uuid(),
'network_id': _uuid(),
'admin_state_up': True,
'mac_address': str(mac_address),
'fixed_ips': fixed_ips,
'subnets': subnets})
router[l3_constants.INTERFACE_KEY] = interfaces
def prepare_router_data(ip_version=4, enable_snat=None, num_internal_ports=1,
enable_floating_ip=False, enable_ha=False,
extra_routes=False, dual_stack=False,
@ -1330,6 +1401,20 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
namespace=ri.ns_name,
conf=mock.ANY)]
def _process_router_ipv6_subnet_added(
self, router, ipv6_subnet_modes=None):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
ri = l3router.RouterInfo(router['id'], router, **self.ri_kwargs)
agent.external_gateway_added = mock.Mock()
self._process_router_instance_for_agent(agent, ri, router)
# Add an IPv6 interface with len(ipv6_subnet_modes) subnets
# and reprocess
router_append_subnet(router, count=len(ipv6_subnet_modes),
ip_version=6, ipv6_subnet_modes=ipv6_subnet_modes)
# Reassign the router object to RouterInfo
self._process_router_instance_for_agent(agent, ri, router)
return ri
def _assert_ri_process_enabled(self, ri, process):
"""Verify that process was enabled for a router instance."""
expected_calls = self._expected_call_lookup_ri_process(
@ -1361,6 +1446,59 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
self.assertIn('prefix',
self.utils_replace_file.call_args[0][1].split())
def test_process_router_ipv6_subnets_added(self):
router = prepare_router_data()
ri = self._process_router_ipv6_subnet_added(router, ipv6_subnet_modes=[
{'ra_mode': l3_constants.IPV6_SLAAC,
'address_mode': l3_constants.IPV6_SLAAC},
{'ra_mode': l3_constants.DHCPV6_STATELESS,
'address_mode': l3_constants.DHCPV6_STATELESS},
{'ra_mode': l3_constants.DHCPV6_STATEFUL,
'address_mode': l3_constants.DHCPV6_STATEFUL}])
self._assert_ri_process_enabled(ri, 'radvd')
radvd_config = self.utils_replace_file.call_args[0][1].split()
# Assert we have a prefix from IPV6_SLAAC and a prefix from
# DHCPV6_STATELESS on one interface
self.assertEqual(2, radvd_config.count("prefix"))
self.assertEqual(1, radvd_config.count("interface"))
def test_process_router_ipv6_subnets_added_to_existing_port(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router = prepare_router_data()
ri = l3router.RouterInfo(router['id'], router, **self.ri_kwargs)
agent.external_gateway_added = mock.Mock()
self._process_router_instance_for_agent(agent, ri, router)
# Add the first subnet on a new interface
router_append_subnet(router, count=1, ip_version=6, ipv6_subnet_modes=[
{'ra_mode': l3_constants.IPV6_SLAAC,
'address_mode': l3_constants.IPV6_SLAAC}])
self._process_router_instance_for_agent(agent, ri, router)
self._assert_ri_process_enabled(ri, 'radvd')
radvd_config = self.utils_replace_file.call_args[0][1].split()
self.assertEqual(1, len(ri.internal_ports[1]['subnets']))
self.assertEqual(1, len(ri.internal_ports[1]['fixed_ips']))
self.assertEqual(1, radvd_config.count("prefix"))
self.assertEqual(1, radvd_config.count("interface"))
# Reset mocks to verify radvd enabled and configured correctly
# after second subnet added to interface
self.external_process.reset_mock()
self.utils_replace_file.reset_mock()
# Add the second subnet on the same interface
interface_id = router[l3_constants.INTERFACE_KEY][1]['id']
router_append_subnet(router, count=1, ip_version=6, ipv6_subnet_modes=[
{'ra_mode': l3_constants.IPV6_SLAAC,
'address_mode': l3_constants.IPV6_SLAAC}],
interface_id=interface_id)
self._process_router_instance_for_agent(agent, ri, router)
# radvd should have been enabled again and the interface
# should have two prefixes
self._assert_ri_process_enabled(ri, 'radvd')
radvd_config = self.utils_replace_file.call_args[0][1].split()
self.assertEqual(2, len(ri.internal_ports[1]['subnets']))
self.assertEqual(2, len(ri.internal_ports[1]['fixed_ips']))
self.assertEqual(2, radvd_config.count("prefix"))
self.assertEqual(1, radvd_config.count("interface"))
def test_process_router_ipv6v4_interface_added(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router = prepare_router_data()
@ -1408,6 +1546,38 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
self._process_router_instance_for_agent(agent, ri, router)
self._assert_ri_process_disabled(ri, 'radvd')
def test_process_router_ipv6_subnet_removed(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router = prepare_router_data()
ri = l3router.RouterInfo(router['id'], router, **self.ri_kwargs)
agent.external_gateway_added = mock.Mock()
self._process_router_instance_for_agent(agent, ri, router)
# Add an IPv6 interface with two subnets and reprocess
router_append_subnet(router, count=2, ip_version=6,
ipv6_subnet_modes=([
{'ra_mode': l3_constants.IPV6_SLAAC,
'address_mode': l3_constants.IPV6_SLAAC}
] * 2))
self._process_router_instance_for_agent(agent, ri, router)
self._assert_ri_process_enabled(ri, 'radvd')
# Reset mocks to check for modified radvd config
self.utils_replace_file.reset_mock()
self.external_process.reset_mock()
# Remove one subnet from the interface and reprocess
interfaces = copy.deepcopy(router[l3_constants.INTERFACE_KEY])
del interfaces[1]['subnets'][0]
del interfaces[1]['fixed_ips'][0]
router[l3_constants.INTERFACE_KEY] = interfaces
self._process_router_instance_for_agent(agent, ri, router)
# Assert radvd was enabled again and that we only have one
# prefix on the interface
self._assert_ri_process_enabled(ri, 'radvd')
radvd_config = self.utils_replace_file.call_args[0][1].split()
self.assertEqual(1, len(ri.internal_ports[1]['subnets']))
self.assertEqual(1, len(ri.internal_ports[1]['fixed_ips']))
self.assertEqual(1, radvd_config.count("interface"))
self.assertEqual(1, radvd_config.count("prefix"))
def test_process_router_internal_network_added_unexpected_error(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
router = prepare_router_data()
@ -2161,30 +2331,33 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework):
self.assertIn(_join('-m', 'syslog'), cmd)
def test_generate_radvd_conf_other_and_managed_flag(self):
_skip_check = object()
skip = lambda flag: True if flag is _skip_check else False
# expected = {ra_mode: (AdvOtherConfigFlag, AdvManagedFlag), ...}
expected = {l3_constants.IPV6_SLAAC: (False, False),
l3_constants.DHCPV6_STATELESS: (True, False),
# we don't check other flag for stateful since it's redundant
# for this mode and can be ignored by clients, as per RFC4861
l3_constants.DHCPV6_STATEFUL: (_skip_check, True)}
l3_constants.DHCPV6_STATEFUL: (False, True)}
for ra_mode, flags_set in expected.iteritems():
modes = [l3_constants.IPV6_SLAAC, l3_constants.DHCPV6_STATELESS,
l3_constants.DHCPV6_STATEFUL]
mode_combos = list(iter_chain(*[[list(combo) for combo in
iter_combinations(modes, i)] for i in range(1, len(modes) + 1)]))
for mode_list in mode_combos:
ipv6_subnet_modes = [{'ra_mode': mode, 'address_mode': mode}
for mode in mode_list]
router = prepare_router_data()
ri = self._process_router_ipv6_interface_added(router,
ra_mode=ra_mode)
ri = self._process_router_ipv6_subnet_added(router,
ipv6_subnet_modes)
ri.radvd._generate_radvd_conf(router[l3_constants.INTERFACE_KEY])
def assertFlag(flag):
return (self.assertIn if flag else self.assertNotIn)
other_flag, managed_flag = flags_set
if not skip(other_flag):
assertFlag(other_flag)('AdvOtherConfigFlag on;',
self.utils_replace_file.call_args[0][1])
other_flag, managed_flag = (
any(expected[mode][0] for mode in mode_list),
any(expected[mode][1] for mode in mode_list))
if not skip(managed_flag):
assertFlag(managed_flag)('AdvManagedFlag on',
self.utils_replace_file.call_args[0][1])
assertFlag(other_flag)('AdvOtherConfigFlag on;',
self.utils_replace_file.call_args[0][1])
assertFlag(managed_flag)('AdvManagedFlag on;',
self.utils_replace_file.call_args[0][1])

View File

@ -967,6 +967,89 @@ class L3NatTestCaseBase(L3NatTestCaseMixin):
ipv6_address_mode=uc['address_mode']) as s:
self._test_router_add_interface_subnet(r, s, uc['msg'])
def test_router_add_interface_multiple_ipv4_subnets(self):
"""Test router-interface-add for multiple ipv4 subnets.
Verify that adding multiple ipv4 subnets from the same network
to a router places them all on different router interfaces.
"""
with self.router() as r, self.network() as n:
with self.subnet(network=n, cidr='10.0.0.0/24') as s1, (
self.subnet(network=n, cidr='10.0.1.0/24')) as s2:
body = self._router_interface_action('add',
r['router']['id'],
s1['subnet']['id'],
None)
pid1 = body['port_id']
body = self._router_interface_action('add',
r['router']['id'],
s2['subnet']['id'],
None)
pid2 = body['port_id']
self.assertNotEqual(pid1, pid2)
self._router_interface_action('remove', r['router']['id'],
s1['subnet']['id'], None)
self._router_interface_action('remove', r['router']['id'],
s2['subnet']['id'], None)
def test_router_add_interface_multiple_ipv6_subnets_same_net(self):
"""Test router-interface-add for multiple ipv6 subnets on a network.
Verify that adding multiple ipv6 subnets from the same network
to a router places them all on the same router interface.
"""
with self.router() as r, self.network() as n:
with (self.subnet(network=n, cidr='fd00::1/64', ip_version=6)
) as s1, self.subnet(network=n, cidr='fd01::1/64',
ip_version=6) as s2:
body = self._router_interface_action('add',
r['router']['id'],
s1['subnet']['id'],
None)
pid1 = body['port_id']
body = self._router_interface_action('add',
r['router']['id'],
s2['subnet']['id'],
None)
pid2 = body['port_id']
self.assertEqual(pid1, pid2)
port = self._show('ports', pid1)
self.assertEqual(2, len(port['port']['fixed_ips']))
port_subnet_ids = [fip['subnet_id'] for fip in
port['port']['fixed_ips']]
self.assertIn(s1['subnet']['id'], port_subnet_ids)
self.assertIn(s2['subnet']['id'], port_subnet_ids)
self._router_interface_action('remove', r['router']['id'],
s1['subnet']['id'], None)
self._router_interface_action('remove', r['router']['id'],
s2['subnet']['id'], None)
def test_router_add_interface_multiple_ipv6_subnets_different_net(self):
"""Test router-interface-add for ipv6 subnets on different networks.
Verify that adding multiple ipv6 subnets from different networks
to a router places them on different router interfaces.
"""
with self.router() as r, self.network() as n1, self.network() as n2:
with (self.subnet(network=n1, cidr='fd00::1/64', ip_version=6)
) as s1, self.subnet(network=n2, cidr='fd01::1/64',
ip_version=6) as s2:
body = self._router_interface_action('add',
r['router']['id'],
s1['subnet']['id'],
None)
pid1 = body['port_id']
body = self._router_interface_action('add',
r['router']['id'],
s2['subnet']['id'],
None)
pid2 = body['port_id']
self.assertNotEqual(pid1, pid2)
self._router_interface_action('remove', r['router']['id'],
s1['subnet']['id'], None)
self._router_interface_action('remove', r['router']['id'],
s2['subnet']['id'], None)
def test_router_add_iface_ipv6_ext_ra_subnet_returns_400(self):
"""Test router-interface-add for in-valid ipv6 subnets.
@ -1077,6 +1160,83 @@ class L3NatTestCaseBase(L3NatTestCaseMixin):
body = self._show('ports', p['port']['id'])
self.assertEqual(body['port']['device_id'], r['router']['id'])
# clean-up
self._router_interface_action('remove',
r['router']['id'],
None,
p['port']['id'])
def test_router_add_interface_multiple_ipv4_subnet_port_returns_400(self):
"""Test adding router port with multiple IPv4 subnets fails.
Multiple IPv4 subnets are not allowed on a single router port.
Ensure that adding a port with multiple IPv4 subnets to a router fails.
"""
with self.network() as n, self.router() as r:
with self.subnet(network=n, cidr='10.0.0.0/24') as s1, (
self.subnet(network=n, cidr='10.0.1.0/24')) as s2:
fixed_ips = [{'subnet_id': s1['subnet']['id']},
{'subnet_id': s2['subnet']['id']}]
with self.port(subnet=s1, fixed_ips=fixed_ips) as p:
exp_code = exc.HTTPBadRequest.code
self._router_interface_action('add',
r['router']['id'],
None,
p['port']['id'],
expected_code=exp_code)
def test_router_add_interface_ipv6_port_existing_network_returns_400(self):
"""Ensure unique IPv6 router ports per network id.
Adding a router port containing one or more IPv6 subnets with the same
network id as an existing router port should fail. This is so
there is no ambiguity regarding on which port to add an IPv6 subnet
when executing router-interface-add with a subnet and no port.
"""
with self.network() as n, self.router() as r:
with self.subnet(network=n, cidr='fd00::/64',
ip_version=6) as s1, (
self.subnet(network=n, cidr='fd01::/64',
ip_version=6)) as s2:
with self.port(subnet=s1) as p:
self._router_interface_action('add',
r['router']['id'],
s2['subnet']['id'],
None)
exp_code = exc.HTTPBadRequest.code
self._router_interface_action('add',
r['router']['id'],
None,
p['port']['id'],
expected_code=exp_code)
self._router_interface_action('remove',
r['router']['id'],
s2['subnet']['id'],
None)
def test_router_add_interface_multiple_ipv6_subnet_port(self):
"""A port with multiple IPv6 subnets can be added to a router
Create a port with multiple associated IPv6 subnets and attach
it to a router. The action should succeed.
"""
with self.network() as n, self.router() as r:
with self.subnet(network=n, cidr='fd00::/64',
ip_version=6) as s1, (
self.subnet(network=n, cidr='fd01::/64',
ip_version=6)) as s2:
fixed_ips = [{'subnet_id': s1['subnet']['id']},
{'subnet_id': s2['subnet']['id']}]
with self.port(subnet=s1, fixed_ips=fixed_ips) as p:
self._router_interface_action('add',
r['router']['id'],
None,
p['port']['id'])
self._router_interface_action('remove',
r['router']['id'],
None,
p['port']['id'])
def test_router_add_interface_empty_port_and_subnet_ids(self):
with self.router() as r:
self._router_interface_action('add', r['router']['id'],
@ -1470,6 +1630,34 @@ class L3NatTestCaseBase(L3NatTestCaseMixin):
p2['port']['id'],
exc.HTTPNotFound.code)
def test_router_remove_ipv6_subnet_from_interface(self):
"""Delete a subnet from a router interface
Verify that deleting a subnet with router-interface-delete removes
that subnet when there are multiple subnets on the interface and
removes the interface when it is the last subnet on the interface.
"""
with self.router() as r, self.network() as n:
with (self.subnet(network=n, cidr='fd00::1/64', ip_version=6)
) as s1, self.subnet(network=n, cidr='fd01::1/64',
ip_version=6) as s2:
body = self._router_interface_action('add', r['router']['id'],
s1['subnet']['id'],
None)
self._router_interface_action('add', r['router']['id'],
s2['subnet']['id'], None)
port = self._show('ports', body['port_id'])
self.assertEqual(2, len(port['port']['fixed_ips']))
self._router_interface_action('remove', r['router']['id'],
s1['subnet']['id'], None)
port = self._show('ports', body['port_id'])
self.assertEqual(1, len(port['port']['fixed_ips']))
self._router_interface_action('remove', r['router']['id'],
s2['subnet']['id'], None)
exp_code = exc.HTTPNotFound.code
port = self._show('ports', body['port_id'],
expected_code=exp_code)
def test_router_delete(self):
with self.router() as router:
router_id = router['router']['id']