From 54c05b500ac3ffad98cb480dc5bfd04bdcf91229 Mon Sep 17 00:00:00 2001 From: Andrew Boik Date: Mon, 23 Mar 2015 11:21:11 -0400 Subject: [PATCH] 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 --- neutron/agent/l3/router_info.py | 49 +++-- neutron/agent/linux/ra.py | 32 +-- neutron/db/l3_db.py | 144 ++++++++---- neutron/db/l3_dvr_db.py | 50 +++-- neutron/plugins/ibm/sdnve_neutron_plugin.py | 20 +- .../tests/functional/agent/test_l3_agent.py | 15 ++ neutron/tests/unit/db/test_l3_dvr_db.py | 2 +- neutron/tests/unit/test_l3_agent.py | 205 ++++++++++++++++-- neutron/tests/unit/test_l3_plugin.py | 188 ++++++++++++++++ 9 files changed, 597 insertions(+), 108 deletions(-) diff --git a/neutron/agent/l3/router_info.py b/neutron/agent/l3/router_info.py index f7211765b08..5569fb77d33 100644 --- a/neutron/agent/l3/router_info.py +++ b/neutron/agent/l3/router_info.py @@ -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() diff --git a/neutron/agent/linux/ra.py b/neutron/agent/linux/ra.py index f7233e71a07..7f800c26961 100644 --- a/neutron/agent/linux/ra.py +++ b/neutron/agent/linux/ra.py @@ -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 diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 190aaa64108..bc549937b4e 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -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: diff --git a/neutron/db/l3_dvr_db.py b/neutron/db/l3_dvr_db.py index 4cb504d1709..8c1d94f231b 100644 --- a/neutron/db/l3_dvr_db.py +++ b/neutron/db/l3_dvr_db.py @@ -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 diff --git a/neutron/plugins/ibm/sdnve_neutron_plugin.py b/neutron/plugins/ibm/sdnve_neutron_plugin.py index 87cd5d5d107..2c272250e91 100644 --- a/neutron/plugins/ibm/sdnve_neutron_plugin.py +++ b/neutron/plugins/ibm/sdnve_neutron_plugin.py @@ -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: diff --git a/neutron/tests/functional/agent/test_l3_agent.py b/neutron/tests/functional/agent/test_l3_agent.py index 719ab605af6..ecc7e503292 100755 --- a/neutron/tests/functional/agent/test_l3_agent.py +++ b/neutron/tests/functional/agent/test_l3_agent.py @@ -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']) diff --git a/neutron/tests/unit/db/test_l3_dvr_db.py b/neutron/tests/unit/db/test_l3_dvr_db.py index ddc6eeb3b1e..7e1f3692763 100644 --- a/neutron/tests/unit/db/test_l3_dvr_db.py +++ b/neutron/tests/unit/db/test_l3_dvr_db.py @@ -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 diff --git a/neutron/tests/unit/test_l3_agent.py b/neutron/tests/unit/test_l3_agent.py index 67b5d174cdd..4c6682bd8aa 100644 --- a/neutron/tests/unit/test_l3_agent.py +++ b/neutron/tests/unit/test_l3_agent.py @@ -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]) diff --git a/neutron/tests/unit/test_l3_plugin.py b/neutron/tests/unit/test_l3_plugin.py index 6844eb74132..cdc883055da 100644 --- a/neutron/tests/unit/test_l3_plugin.py +++ b/neutron/tests/unit/test_l3_plugin.py @@ -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']