]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Support multiple IPv6 prefixes on internal router ports
authorAndrew Boik <dboik@cisco.com>
Mon, 23 Mar 2015 15:21:11 +0000 (11:21 -0400)
committerAndrew Boik <dboik@cisco.com>
Fri, 3 Apr 2015 03:19:51 +0000 (03:19 +0000)
(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
neutron/agent/linux/ra.py
neutron/db/l3_db.py
neutron/db/l3_dvr_db.py
neutron/plugins/ibm/sdnve_neutron_plugin.py
neutron/tests/functional/agent/test_l3_agent.py
neutron/tests/unit/db/test_l3_dvr_db.py
neutron/tests/unit/test_l3_agent.py
neutron/tests/unit/test_l3_plugin.py

index f7211765b08d783a5f129616815fe2035703ff38..5569fb77d33d4b53316bfc2df6f26aad660d6eb4 100644 (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()
index f7233e71a07e7c61d68b43770ba8d7053f1b01e4..7f800c269615c0f5f0d6842957a59d95865b7dce 100644 (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
index 190aaa64108d67452f4691f447b8303511f672e0..bc549937b4ea4a8764655ce2ce3afa5d265e096e 100644 (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)
-
-        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)
+            port, subnets, new_port = self._add_interface_by_subnet(
+                    context, router, interface_info['subnet_id'], device_owner)
+
+        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:
index 4cb504d1709774aed06e7d96694fb09b3fa0877c..8c1d94f231b105a48e0f0f287aea169bcd2e1191 100644 (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)
-
-        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'])
+            port, subnets, new_port = self._add_interface_by_subnet(
+                    context, router, interface_info['subnet_id'], device_owner)
+
+        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'][-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
index 87cd5d5d10750e0e2e3e08d79940937412e1f142..2c272250e912854e5abf8c5b88d6ed4ca7ab3154 100644 (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:
index 719ab605af62cbe543777f32b06ccc8184a5548a..ecc7e503292d89f5af5e1f4802cf97948fcd2258 100755 (executable)
@@ -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'])
index ddc6eeb3b1e3aceb337f930cfd27ae69b55e3af3..7e1f369276319be0d831cab53e9a2f7861c9f9ee 100644 (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
index 67b5d174cdde4fd34c369a3259823c2453c3bf39..4c6682bd8aacec7c92f8f7b1dfe038862017f4e5 100644 (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)}
+
+        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 ra_mode, flags_set in expected.iteritems():
+        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])
index 6844eb74132ef3e7d4a7b3331defd7d862af2922..cdc883055daf8420b262be081d8d3ff0e80f3587 100644 (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']