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']