]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Routing table configuration support on L3
authorNachi Ueno <nachi@nttmcl.com>
Thu, 17 Jan 2013 01:52:47 +0000 (17:52 -0800)
committerNachi Ueno <nachi@nttmcl.com>
Tue, 19 Feb 2013 23:05:34 +0000 (15:05 -0800)
Implements bp quantum-l3-routes

  -- Adding the extraroute extension
  -- Updating the routing table based on routes attribute on route
  -- Updated OVS plugin, linuxbridge plugin, metaplugin
     NEC plugin, Ryu plugin

  User can configure the routes through quantum client API by
  using the extension feature.
  sample
  quantum router-update <router_id> \
        --routes type=dict list=true destination=40.0.1.0/24,nexthop=10.1.0.10

Change-Id: I2a11486709e55d3143373858254febaabb93cfe8

14 files changed:
quantum/agent/l3_agent.py
quantum/common/utils.py
quantum/db/db_base_plugin_v2.py
quantum/db/extraroute_db.py [new file with mode: 0644]
quantum/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py [new file with mode: 0644]
quantum/db/models_v2.py
quantum/extensions/extraroute.py [new file with mode: 0644]
quantum/plugins/linuxbridge/lb_quantum_plugin.py
quantum/plugins/metaplugin/meta_quantum_plugin.py
quantum/plugins/nec/nec_plugin.py
quantum/plugins/openvswitch/ovs_quantum_plugin.py
quantum/plugins/ryu/ryu_quantum_plugin.py
quantum/tests/unit/test_extension_extraroute.py [new file with mode: 0644]
quantum/tests/unit/test_l3_agent.py

index d8f3eaf2f4050cdc10571f2cb5c698d97c61bc27..e47109caedf4b26aae9e92c7d35f48c0be265079 100644 (file)
@@ -35,6 +35,7 @@ from quantum.agent.linux import utils
 from quantum.agent import rpc as agent_rpc
 from quantum.common import constants as l3_constants
 from quantum.common import topics
+from quantum.common import utils as common_utils
 from quantum import context
 from quantum import manager
 from quantum.openstack.common import importutils
@@ -105,6 +106,8 @@ class RouterInfo(object):
             #FIXME(danwent): use_ipv6=True,
             namespace=self.ns_name())
 
+        self.routes = []
+
     def ns_name(self):
         if self.use_namespaces:
             return NS_PREFIX + self.router_id
@@ -319,6 +322,8 @@ class L3NATAgent(manager.Manager):
 
         ri.ex_gw_port = ex_gw_port
 
+        self.routes_updated(ri)
+
     def process_router_floating_ips(self, ri, ex_gw_port):
         floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, [])
         existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips])
@@ -618,6 +623,36 @@ class L3NATAgent(manager.Manager):
     def after_start(self):
         LOG.info(_("L3 agent started"))
 
+    def _update_routing_table(self, ri, operation, route):
+        cmd = ['ip', 'route', operation, 'to', route['destination'],
+               'via', route['nexthop']]
+        #TODO(nati) move this code to iplib
+        if self.conf.use_namespaces:
+            ip_wrapper = ip_lib.IPWrapper(self.conf.root_helper,
+                                          namespace=ri.ns_name())
+            ip_wrapper.netns.execute(cmd, check_exit_code=False)
+        else:
+            utils.execute(cmd, check_exit_code=False,
+                          root_helper=self.conf.root_helper)
+
+    def routes_updated(self, ri):
+        new_routes = ri.router['routes']
+        old_routes = ri.routes
+        adds, removes = common_utils.diff_list_of_dict(old_routes,
+                                                       new_routes)
+        for route in adds:
+            LOG.debug(_("Added route entry is '%s'"), route)
+            # remove replaced route from deleted route
+            for del_route in removes:
+                if route['destination'] == del_route['destination']:
+                    removes.remove(del_route)
+            #replace success even if there is no existing route
+            self._update_routing_table(ri, 'replace', route)
+        for route in removes:
+            LOG.debug(_("Removed route entry is '%s'"), route)
+            self._update_routing_table(ri, 'delete', route)
+        ri.routes = new_routes
+
 
 class L3NATAgentWithStateReport(L3NATAgent):
 
index da0e019153c4a5a04fa929e66cf43f22b92924df..761c60c1005d67586d86af1949a1855a7354ee11 100644 (file)
@@ -162,3 +162,24 @@ def compare_elements(a, b):
     if b is None:
         b = []
     return set(a) == set(b)
+
+
+def dict2str(dic):
+    return ','.join("%s=%s" % (key, val)
+                    for key, val in sorted(dic.iteritems()))
+
+
+def str2dict(string):
+    res_dict = {}
+    for keyvalue in string.split(',', 1):
+        (key, value) = keyvalue.split('=', 1)
+        res_dict[key] = value
+    return res_dict
+
+
+def diff_list_of_dict(old_list, new_list):
+    new_set = set([dict2str(l) for l in new_list])
+    old_set = set([dict2str(l) for l in old_list])
+    added = new_set - old_set
+    removed = old_set - new_set
+    return [str2dict(a) for a in added], [str2dict(r) for r in removed]
index f129cf0ac4449f3912b81a3382770a2c8a721a60..078924b978776bafcc50fc878ac1ef6cf86dc5fb 100644 (file)
@@ -181,7 +181,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
             return []
 
     def _get_route_by_subnet(self, context, subnet_id):
-        route_qry = context.session.query(models_v2.Route)
+        route_qry = context.session.query(models_v2.SubnetRoute)
         return route_qry.filter_by(subnet_id=subnet_id).all()
 
     def _get_subnets_by_network(self, context, network_id):
@@ -1085,9 +1085,10 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
 
             if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED:
                 for rt in s['host_routes']:
-                    route = models_v2.Route(subnet_id=subnet.id,
-                                            destination=rt['destination'],
-                                            nexthop=rt['nexthop'])
+                    route = models_v2.SubnetRoute(
+                        subnet_id=subnet.id,
+                        destination=rt['destination'],
+                        nexthop=rt['nexthop'])
                     context.session.add(route)
 
             for pool in s['allocation_pools']:
@@ -1157,7 +1158,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
                         if _combine(route) == route_str:
                             context.session.delete(route)
                 for route_str in new_route_set - old_route_set:
-                    route = models_v2.Route(
+                    route = models_v2.SubnetRoute(
                         destination=route_str.partition("_")[0],
                         nexthop=route_str.partition("_")[2],
                         subnet_id=id)
diff --git a/quantum/db/extraroute_db.py b/quantum/db/extraroute_db.py
new file mode 100644 (file)
index 0000000..6282cc2
--- /dev/null
@@ -0,0 +1,174 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT MCL, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import netaddr
+from oslo.config import cfg
+import sqlalchemy as sa
+
+from quantum.common import utils
+from quantum.db import l3_db
+from quantum.db import model_base
+from quantum.db import models_v2
+from quantum.extensions import extraroute
+from quantum.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
+
+extra_route_opts = [
+    #TODO(nati): use quota framework when it support quota for attributes
+    cfg.IntOpt('max_routes', default=30,
+               help=_("Maximum number of routes")),
+]
+
+cfg.CONF.register_opts(extra_route_opts)
+
+
+class RouterRoute(model_base.BASEV2, models_v2.Route):
+    router_id = sa.Column(sa.String(36),
+                          sa.ForeignKey('routers.id',
+                                        ondelete="CASCADE"),
+                          primary_key=True)
+
+
+class ExtraRoute_db_mixin(l3_db.L3_NAT_db_mixin):
+    """ Mixin class to support extra route configuration on router"""
+    def update_router(self, context, id, router):
+        r = router['router']
+        with context.session.begin(subtransactions=True):
+            #check if route exists and have permission to access
+            router_db = self._get_router(context, id)
+            if 'routes' in r:
+                self._update_extra_routes(context,
+                                          router_db,
+                                          r['routes'])
+            router_updated = super(ExtraRoute_db_mixin, self).update_router(
+                context, id, router)
+            router_updated['routes'] = self._get_extra_routes_by_router_id(
+                context, id)
+
+        return router_updated
+
+    def _get_subnets_by_cidr(self, context, cidr):
+        query_subnets = context.session.query(models_v2.Subnet)
+        return query_subnets.filter_by(cidr=cidr).all()
+
+    def _validate_routes_nexthop(self, context, ports, routes, nexthop):
+        #Note(nati): Nexthop should be connected,
+        # so we need to check
+        # nexthop belongs to one of cidrs of the router ports
+        cidrs = []
+        for port in ports:
+            cidrs += [self._get_subnet(context,
+                                       ip['subnet_id'])['cidr']
+                      for ip in port['fixed_ips']]
+        if not netaddr.all_matching_cidrs(nexthop, cidrs):
+            raise extraroute.InvalidRoutes(
+                routes=routes,
+                reason=_('the nexthop is not connected with router'))
+        #Note(nati) nexthop should not be same as fixed_ips
+        for port in ports:
+            for ip in port['fixed_ips']:
+                if nexthop == ip['ip_address']:
+                    raise extraroute.InvalidRoutes(
+                        routes=routes,
+                        reason=_('the nexthop is used by router'))
+
+    def _validate_routes(self, context,
+                         router_id, routes):
+        if len(routes) > cfg.CONF.max_routes:
+            raise extraroute.RoutesExhausted(
+                router_id=router_id,
+                quota=cfg.CONF.max_routes)
+
+        filters = {'device_id': [router_id]}
+        ports = self.get_ports(context, filters)
+        for route in routes:
+            self._validate_routes_nexthop(
+                context, ports, routes, route['nexthop'])
+
+    def _update_extra_routes(self, context, router, routes):
+        self._validate_routes(context, router['id'],
+                              routes)
+        old_routes = self._get_extra_routes_by_router_id(
+            context, router['id'])
+        added, removed = utils.diff_list_of_dict(old_routes,
+                                                 routes)
+        LOG.debug('Added routes are %s' % added)
+        for route in added:
+            router_routes = RouterRoute(
+                router_id=router['id'],
+                destination=route['destination'],
+                nexthop=route['nexthop'])
+            context.session.add(router_routes)
+
+        LOG.debug('Removed routes are %s' % removed)
+        for route in removed:
+            del_context = context.session.query(RouterRoute)
+            del_context.filter_by(router_id=router['id'],
+                                  destination=route['destination'],
+                                  nexthop=route['nexthop']).delete()
+
+    def _make_extra_route_list(self, extra_routes):
+        return [{'destination': route['destination'],
+                 'nexthop': route['nexthop']}
+                for route in extra_routes]
+
+    def _get_extra_routes_by_router_id(self, context, id):
+        query = context.session.query(RouterRoute)
+        query.filter(RouterRoute.router_id == id)
+        extra_routes = query.all()
+        return self._make_extra_route_list(extra_routes)
+
+    def get_router(self, context, id, fields=None):
+        with context.session.begin(subtransactions=True):
+            router = super(ExtraRoute_db_mixin, self).get_router(
+                context, id, fields)
+            router['routes'] = self._get_extra_routes_by_router_id(
+                context, id)
+            return router
+
+    def get_routers(self, context, filters=None, fields=None):
+        with context.session.begin(subtransactions=True):
+            routers = super(ExtraRoute_db_mixin, self).get_routers(
+                context, filters, fields)
+            for router in routers:
+                router['routes'] = self._get_extra_routes_by_router_id(
+                    context, router['id'])
+            return routers
+
+    def get_sync_data(self, context, router_ids=None):
+        """Query routers and their related floating_ips, interfaces."""
+        with context.session.begin(subtransactions=True):
+            routers = super(ExtraRoute_db_mixin,
+                            self).get_sync_data(context, router_ids)
+            for router in routers:
+                router['routes'] = self._get_extra_routes_by_router_id(
+                    context, router['id'])
+        return routers
+
+    def _confirm_router_interface_not_in_use(self, context, router_id,
+                                             subnet_id):
+        super(ExtraRoute_db_mixin, self)._confirm_router_interface_not_in_use(
+            context, router_id, subnet_id)
+        subnet_db = self._get_subnet(context, subnet_id)
+        subnet_cidr = netaddr.IPNetwork(subnet_db['cidr'])
+        extra_routes = self._get_extra_routes_by_router_id(context, router_id)
+        for route in extra_routes:
+            if netaddr.all_matching_cidrs(route['nexthop'], [subnet_cidr]):
+                raise extraroute.RouterInterfaceInUseByRoute(
+                    router_id=router_id, subnet_id=subnet_id)
diff --git a/quantum/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py b/quantum/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py
new file mode 100644 (file)
index 0000000..e223b21
--- /dev/null
@@ -0,0 +1,74 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 OpenStack LLC
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+#
+
+"""Support routring table configration on Router
+
+Revision ID: 1c33fa3cd1a1
+Revises: 1d76643bcec4
+Create Date: 2013-01-17 14:35:09.386975
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '1c33fa3cd1a1'
+down_revision = '1d76643bcec4'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+    'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2',
+    'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2',
+    'quantum.plugins.nec.nec_plugin.NECPluginV2',
+    'quantum.plugins.ryu.ryu_quantum_plugin.RyuQuantumPluginV2',
+    'quantum.plugins.metaplugin.meta_quantum_plugin.MetaPluginV2'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+from quantum.db import migration
+
+
+def upgrade(active_plugin=None, options=None):
+    if not migration.should_run(active_plugin, migration_for_plugins):
+        return
+
+    op.rename_table(
+        'routes',
+        'subnetroutes',
+    )
+    op.create_table(
+        'routerroutes',
+        sa.Column('destination', sa.String(length=64), nullable=False),
+        sa.Column(
+            'nexthop', sa.String(length=64), nullable=False),
+        sa.Column('router_id', sa.String(length=36), nullable=False),
+        sa.ForeignKeyConstraint(
+            ['router_id'], ['routers.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('destination', 'nexthop', 'router_id')
+    )
+
+
+def downgrade(active_plugin=None, options=None):
+    if not migration.should_run(active_plugin, migration_for_plugins):
+        return
+
+    op.rename_table(
+        'subnetroutes',
+        'routes',
+    )
+    op.drop_table('routerroutes')
index def066d99aa87a6bda8c10907b77dc6d97cedfb7..122828fb95e20a706579ed955596ac85b91596ed 100644 (file)
@@ -92,6 +92,19 @@ class IPAllocation(model_base.BASEV2):
     expiration = sa.Column(sa.DateTime, nullable=True)
 
 
+class Route(object):
+    """mixin of a route."""
+    destination = sa.Column(sa.String(64), nullable=False, primary_key=True)
+    nexthop = sa.Column(sa.String(64), nullable=False, primary_key=True)
+
+
+class SubnetRoute(model_base.BASEV2, Route):
+    subnet_id = sa.Column(sa.String(36),
+                          sa.ForeignKey('subnets.id',
+                                        ondelete="CASCADE"),
+                          primary_key=True)
+
+
 class Port(model_base.BASEV2, HasId, HasTenant):
     """Represents a port on a quantum v2 network."""
     name = sa.Column(sa.String(255))
@@ -114,16 +127,6 @@ class DNSNameServer(model_base.BASEV2):
                           primary_key=True)
 
 
-class Route(model_base.BASEV2):
-    """Represents a route for a subnet or port."""
-    destination = sa.Column(sa.String(64), nullable=False, primary_key=True)
-    nexthop = sa.Column(sa.String(64), nullable=False, primary_key=True)
-    subnet_id = sa.Column(sa.String(36),
-                          sa.ForeignKey('subnets.id',
-                                        ondelete="CASCADE"),
-                          primary_key=True)
-
-
 class Subnet(model_base.BASEV2, HasId, HasTenant):
     """Represents a quantum subnet.
 
@@ -143,7 +146,7 @@ class Subnet(model_base.BASEV2, HasId, HasTenant):
     dns_nameservers = orm.relationship(DNSNameServer,
                                        backref='subnet',
                                        cascade='delete')
-    routes = orm.relationship(Route,
+    routes = orm.relationship(SubnetRoute,
                               backref='subnet',
                               cascade='delete')
     shared = sa.Column(sa.Boolean)
diff --git a/quantum/extensions/extraroute.py b/quantum/extensions/extraroute.py
new file mode 100644 (file)
index 0000000..64588c9
--- /dev/null
@@ -0,0 +1,74 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT MCL, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+from quantum.api.v2 import attributes as attr
+from quantum.common import exceptions as qexception
+
+
+# Extra Routes Exceptions
+class InvalidRoutes(qexception.InvalidInput):
+    message = _("Invalid format for routes: %(routes)s, %(reason)s")
+
+
+class RouterInterfaceInUseByRoute(qexception.InUse):
+    message = _("Router interface for subnet %(subnet_id)s on router "
+                "%(router_id)s cannot be deleted, as it is required "
+                "by one or more routes.")
+
+
+class RoutesExhausted(qexception.BadRequest):
+    message = _("Unable to complete operation for %(router_id)s. "
+                "The number of routes exceeds the maximum %(quota)s.")
+
+# Attribute Map
+EXTENDED_ATTRIBUTES_2_0 = {
+    'routers': {
+        'routes': {'allow_post': False, 'allow_put': True,
+                   'validate': {'type:hostroutes': None},
+                   'is_visible': True, 'default': attr.ATTR_NOT_SPECIFIED},
+    }
+}
+
+
+class Extraroute():
+
+    @classmethod
+    def get_name(cls):
+        return "Quantum Extra Route"
+
+    @classmethod
+    def get_alias(cls):
+        return "extraroute"
+
+    @classmethod
+    def get_description(cls):
+        return "Extra routes configuration for L3 router"
+
+    @classmethod
+    def get_namespace(cls):
+        return "http://docs.openstack.org/ext/quantum/extraroutes/api/v1.0"
+
+    @classmethod
+    def get_updated(cls):
+        return "2013-02-01T10:00:00-00:00"
+
+    def get_extended_resources(self, version):
+        if version == "2.0":
+            return EXTENDED_ATTRIBUTES_2_0
+        else:
+            return {}
index db6948e4906839eb6c369c7999196224a46de4cd..8eff5112debedb65c0b81c9f0a32ee45fbdb46ad 100644 (file)
@@ -28,7 +28,7 @@ from quantum.db import agents_db
 from quantum.db import api as db_api
 from quantum.db import db_base_plugin_v2
 from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
 from quantum.db import l3_rpc_base
 # NOTE: quota_db cannot be removed, it is for db model
 from quantum.db import quota_db
@@ -172,7 +172,7 @@ class AgentNotifierApi(proxy.RpcProxy,
 
 
 class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
-                          l3_db.L3_NAT_db_mixin,
+                          extraroute_db.ExtraRoute_db_mixin,
                           sg_db_rpc.SecurityGroupServerRpcMixin,
                           agents_db.AgentDbMixin):
     """Implement the Quantum abstractions using Linux bridging.
@@ -197,7 +197,7 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
     __native_bulk_support = True
 
     supported_extension_aliases = ["provider", "router", "binding", "quotas",
-                                   "security-group", "agent"]
+                                   "security-group", "agent", "extraroute"]
 
     network_view = "extension:provider_network:view"
     network_set = "extension:provider_network:set"
index ef173eede98a9ea78f38cb52eb891b3c15b603d0..7ec3ffe8f29ffec26f692eb5a30da8720bb3fc9d 100644 (file)
@@ -20,6 +20,7 @@ from oslo.config import cfg
 from quantum.common import exceptions as exc
 from quantum.db import api as db
 from quantum.db import db_base_plugin_v2
+from quantum.db import extraroute_db
 from quantum.db import l3_db
 from quantum.db import models_v2
 from quantum.extensions.flavor import (FLAVOR_NETWORK, FLAVOR_ROUTER)
@@ -45,13 +46,13 @@ class FaildToAddFlavorBinding(exc.QuantumException):
 
 
 class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
-                   l3_db.L3_NAT_db_mixin):
+                   extraroute_db.ExtraRoute_db_mixin):
 
     def __init__(self, configfile=None):
         LOG.debug(_("Start initializing metaplugin"))
         self.supported_extension_aliases = \
             cfg.CONF.META.supported_extension_aliases.split(',')
-        self.supported_extension_aliases += ['flavor', 'router']
+        self.supported_extension_aliases += ['flavor', 'router', 'extraroute']
 
         # Ignore config option overapping
         def _is_opt_registered(opts, opt):
index 38dcfbad917e3456dbfa465e3e3d69366c260565..f7ae8da0945620b6a39b04ab3d1d36e680dbc948 100644 (file)
@@ -22,7 +22,7 @@ from quantum.common import rpc as q_rpc
 from quantum.common import topics
 from quantum import context
 from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
 from quantum.db import l3_rpc_base
 #NOTE(amotoki): quota_db cannot be removed, it is for db model
 from quantum.db import quota_db
@@ -58,7 +58,7 @@ class OperationalStatus:
 
 
 class NECPluginV2(nec_plugin_base.NECPluginV2Base,
-                  l3_db.L3_NAT_db_mixin,
+                  extraroute_db.ExtraRoute_db_mixin,
                   sg_db_rpc.SecurityGroupServerRpcMixin):
     """NECPluginV2 controls an OpenFlow Controller.
 
@@ -74,7 +74,7 @@ class NECPluginV2(nec_plugin_base.NECPluginV2Base,
     """
 
     supported_extension_aliases = ["router", "quotas", "binding",
-                                   "security-group"]
+                                   "security-group", "extraroute"]
 
     binding_view = "extension:port_binding:view"
     binding_set = "extension:port_binding:set"
index 05a0a898154388699e712d0a521b59c537ab406f..e17a56468db7b21154382041efec55c87f538c55 100644 (file)
@@ -33,7 +33,7 @@ from quantum.common import topics
 from quantum.db import agents_db
 from quantum.db import db_base_plugin_v2
 from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
 from quantum.db import l3_rpc_base
 # NOTE: quota_db cannot be removed, it is for db model
 from quantum.db import quota_db
@@ -209,10 +209,9 @@ class AgentNotifierApi(proxy.RpcProxy,
 
 
 class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
-                         l3_db.L3_NAT_db_mixin,
+                         extraroute_db.ExtraRoute_db_mixin,
                          sg_db_rpc.SecurityGroupServerRpcMixin,
                          agents_db.AgentDbMixin):
-
     """Implement the Quantum abstractions using Open vSwitch.
 
     Depending on whether tunneling is enabled, either a GRE tunnel or
@@ -236,7 +235,8 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
     __native_bulk_support = True
     supported_extension_aliases = ["provider", "router",
                                    "binding", "quotas", "security-group",
-                                   "agent"]
+                                   "agent",
+                                   "extraroute"]
 
     network_view = "extension:provider_network:view"
     network_set = "extension:provider_network:set"
index 8b09afb76bdc07a00ccbd535cca63133a8c89d5c..402a9d3534a0ecbeb0a934f121070757e08cb23a 100644 (file)
@@ -27,7 +27,7 @@ from quantum.common import topics
 from quantum.db import api as db
 from quantum.db import db_base_plugin_v2
 from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
 from quantum.db import l3_rpc_base
 from quantum.db import models_v2
 from quantum.openstack.common import log as logging
@@ -56,9 +56,9 @@ class RyuRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin,
 
 
 class RyuQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
-                         l3_db.L3_NAT_db_mixin):
+                         extraroute_db.ExtraRoute_db_mixin):
 
-    supported_extension_aliases = ["router"]
+    supported_extension_aliases = ["router", "extraroute"]
 
     def __init__(self, configfile=None):
         db.configure_db()
diff --git a/quantum/tests/unit/test_extension_extraroute.py b/quantum/tests/unit/test_extension_extraroute.py
new file mode 100644 (file)
index 0000000..f32310a
--- /dev/null
@@ -0,0 +1,449 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013, Nachi Ueno, NTT MCL, Inc.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo.config import cfg
+from webob import exc
+
+from quantum.common.test_lib import test_config
+from quantum.db import extraroute_db
+from quantum.extensions import extraroute
+from quantum.extensions import l3
+from quantum.openstack.common import log as logging
+from quantum.openstack.common.notifier import api as notifier_api
+from quantum.openstack.common.notifier import test_notifier
+from quantum.openstack.common import uuidutils
+from quantum.tests.unit import test_api_v2
+from quantum.tests.unit import test_l3_plugin as test_l3
+
+
+LOG = logging.getLogger(__name__)
+
+_uuid = uuidutils.generate_uuid
+_get_path = test_api_v2._get_path
+
+
+class ExtraRouteTestExtensionManager(object):
+
+    def get_resources(self):
+        l3.RESOURCE_ATTRIBUTE_MAP['routers'].update(
+            extraroute.EXTENDED_ATTRIBUTES_2_0['routers'])
+        return l3.L3.get_resources()
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
+
+
+# This plugin class is just for testing
+class TestExtraRoutePlugin(test_l3.TestL3NatPlugin,
+                           extraroute_db.ExtraRoute_db_mixin):
+    supported_extension_aliases = ["router", "extraroute"]
+
+
+class ExtraRouteDBTestCase(test_l3.L3NatDBTestCase):
+
+    def setUp(self):
+        test_config['plugin_name_v2'] = (
+            'quantum.tests.unit.'
+            'test_extension_extraroute.TestExtraRoutePlugin')
+        # for these tests we need to enable overlapping ips
+        cfg.CONF.set_default('allow_overlapping_ips', True)
+        cfg.CONF.set_default('max_routes', 3)
+        ext_mgr = ExtraRouteTestExtensionManager()
+        test_config['extension_manager'] = ext_mgr
+        #L3NatDBTestCase will overwrite plugin_name_v2,
+        #so we don't need to setUp on the class here
+        super(test_l3.L3NatTestCaseBase, self).setUp()
+
+        # Set to None to reload the drivers
+        notifier_api._drivers = None
+        cfg.CONF.set_override("notification_driver", [test_notifier.__name__])
+
+    def test_route_update_with_one_route(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    body = self._show('routers', r['router']['id'])
+                    body = self._router_interface_action('add',
+                                                         r['router']['id'],
+                                                         None,
+                                                         p['port']['id'])
+
+                    routes = [{'destination': '135.207.0.0/16',
+                               'nexthop': '10.0.1.3'}]
+
+                    body = self._update('routers', r['router']['id'],
+                                        {'router': {'routes': routes}})
+
+                    body = self._show('routers', r['router']['id'])
+                    self.assertEquals(body['router']['routes'],
+                                      routes)
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes': []}})
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_interface_in_use_by_route(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    body = self._router_interface_action('add',
+                                                         r['router']['id'],
+                                                         None,
+                                                         p['port']['id'])
+
+                    routes = [{'destination': '135.207.0.0/16',
+                               'nexthop': '10.0.1.3'}]
+
+                    body = self._update('routers', r['router']['id'],
+                                        {'router': {'routes': routes}})
+
+                    body = self._show('routers', r['router']['id'])
+                    self.assertEquals(body['router']['routes'],
+                                      routes)
+
+                    self._router_interface_action(
+                        'remove',
+                        r['router']['id'],
+                        None,
+                        p['port']['id'],
+                        expected_code=exc.HTTPConflict.code)
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes': []}})
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_route_update_with_multi_routes(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    body = self._router_interface_action('add',
+                                                         r['router']['id'],
+                                                         None,
+                                                         p['port']['id'])
+
+                    routes = [{'destination': '135.207.0.0/16',
+                               'nexthop': '10.0.1.3'},
+                              {'destination': '12.0.0.0/8',
+                               'nexthop': '10.0.1.4'},
+                              {'destination': '141.212.0.0/16',
+                               'nexthop': '10.0.1.5'}]
+
+                    body = self._update('routers', r['router']['id'],
+                                        {'router': {'routes': routes}})
+
+                    body = self._show('routers', r['router']['id'])
+                    self.assertItemsEqual(body['router']['routes'],
+                                          routes)
+
+                    # clean-up
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes': []}})
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_delete_routes(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    body = self._router_interface_action('add',
+                                                         r['router']['id'],
+                                                         None,
+                                                         p['port']['id'])
+
+                    routes_orig = [{'destination': '135.207.0.0/16',
+                                    'nexthop': '10.0.1.3'},
+                                   {'destination': '12.0.0.0/8',
+                                    'nexthop': '10.0.1.4'},
+                                   {'destination': '141.212.0.0/16',
+                                    'nexthop': '10.0.1.5'}]
+
+                    body = self._update('routers', r['router']['id'],
+                                        {'router': {'routes':
+                                                    routes_orig}})
+
+                    body = self._show('routers', r['router']['id'])
+                    self.assertItemsEqual(body['router']['routes'],
+                                          routes_orig)
+
+                    routes_left = [{'destination': '135.207.0.0/16',
+                                    'nexthop': '10.0.1.3'},
+                                   {'destination': '141.212.0.0/16',
+                                    'nexthop': '10.0.1.5'}]
+
+                    body = self._update('routers', r['router']['id'],
+                                        {'router': {'routes':
+                                                    routes_left}})
+
+                    body = self._show('routers', r['router']['id'])
+                    self.assertItemsEqual(body['router']['routes'],
+                                          routes_left)
+
+                    body = self._update('routers', r['router']['id'],
+                                        {'router': {'routes': []}})
+
+                    body = self._show('routers', r['router']['id'])
+                    self.assertEqual(body['router']['routes'], [])
+
+                    # clean-up
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes': []}})
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def _test_malformed_route(self, routes):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes': routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_no_destination_route(self):
+        self._test_malformed_route([{'nexthop': '10.0.1.6'}])
+
+    def test_no_nexthop_route(self):
+        self._test_malformed_route({'destination': '135.207.0.0/16'})
+
+    def test_none_destination(self):
+        self._test_malformed_route([{'destination': None,
+                                     'nexthop': '10.0.1.3'}])
+
+    def test_none_nexthop(self):
+        self._test_malformed_route([{'destination': '135.207.0.0/16',
+                                     'nexthop': None}])
+
+    def test_nexthop_is_port_ip(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+                    port_ip = p['port']['fixed_ips'][0]['ip_address']
+                    routes = [{'destination': '135.207.0.0/16',
+                               'nexthop': port_ip}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_with_too_many_routes(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+                    routes = [{'destination': '135.207.0.0/16',
+                               'nexthop': '10.0.1.3'},
+                              {'destination': '12.0.0.0/8',
+                               'nexthop': '10.0.1.4'},
+                              {'destination': '141.212.0.0/16',
+                               'nexthop': '10.0.1.5'},
+                              {'destination': '192.168.0.0/16',
+                               'nexthop': '10.0.1.6'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_with_dup_address(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+                    routes = [{'destination': '135.207.0.0/16',
+                               'nexthop': '10.0.1.3'},
+                              {'destination': '135.207.0.0/16',
+                               'nexthop': '10.0.1.3'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_with_invalid_ip_address(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+                    routes = [{'destination': '512.207.0.0/16',
+                               'nexthop': '10.0.1.3'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    routes = [{'destination': '127.207.0.0/48',
+                               'nexthop': '10.0.1.3'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    routes = [{'destination': 'invalid_ip_address',
+                               'nexthop': '10.0.1.3'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_with_invalid_nexthop_ip(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+                    routes = [{'destination': '127.207.0.0/16',
+                               'nexthop': ' 300.10.10.4'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_with_nexthop_is_outside_port_subnet(self):
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                with self.port(subnet=s, no_delete=True) as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+                    routes = [{'destination': '127.207.0.0/16',
+                               'nexthop': ' 20.10.10.4'}]
+
+                    self._update('routers', r['router']['id'],
+                                 {'router': {'routes':
+                                             routes}},
+                                 expected_code=exc.HTTPBadRequest.code)
+
+                    # clean-up
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+
+    def test_router_update_on_external_port(self):
+        DEVICE_OWNER_ROUTER_GW = "network:router_gateway"
+        with self.router() as r:
+            with self.subnet(cidr='10.0.1.0/24') as s:
+                self._set_net_external(s['subnet']['network_id'])
+                self._add_external_gateway_to_router(
+                    r['router']['id'],
+                    s['subnet']['network_id'])
+                body = self._show('routers', r['router']['id'])
+                net_id = body['router']['external_gateway_info']['network_id']
+                self.assertEquals(net_id, s['subnet']['network_id'])
+                port_res = self._list_ports('json',
+                                            200,
+                                            s['subnet']['network_id'],
+                                            tenant_id=r['router']['tenant_id'],
+                                            device_own=DEVICE_OWNER_ROUTER_GW)
+                port_list = self.deserialize('json', port_res)
+                self.assertEqual(len(port_list['ports']), 1)
+
+                routes = [{'destination': '135.207.0.0/16',
+                           'nexthop': '10.0.1.3'}]
+
+                body = self._update('routers', r['router']['id'],
+                                    {'router': {'routes':
+                                                routes}})
+
+                body = self._show('routers', r['router']['id'])
+                self.assertEquals(body['router']['routes'],
+                                  routes)
+
+                self._remove_external_gateway_from_router(
+                    r['router']['id'],
+                    s['subnet']['network_id'])
+                body = self._show('routers', r['router']['id'])
+                gw_info = body['router']['external_gateway_info']
+                self.assertEquals(gw_info, None)
index fbb3dfad6dd19e4bc3c92a36189afa52ec6a7f64..7cf8b430710202cdbaec99a75bbf4a258e7ff2a5 100644 (file)
@@ -91,7 +91,7 @@ class TestBasicRouterOperations(unittest2.TestCase):
         self.assertTrue(ri.ns_name().endswith(id))
 
     def testAgentCreate(self):
-        agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
+        l3_agent.L3NATAgent(HOSTNAME, self.conf)
 
     def _test_internal_network_action(self, action):
         port_id = _uuid()
@@ -100,7 +100,6 @@ class TestBasicRouterOperations(unittest2.TestCase):
         ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
                                  self.conf.use_namespaces)
         agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
-        interface_name = agent.get_internal_device_name(port_id)
         cidr = '99.0.1.9/24'
         mac = 'ca:fe:de:ad:be:ef'
         ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30'}]}
@@ -209,6 +208,105 @@ class TestBasicRouterOperations(unittest2.TestCase):
     def testAgentRemoveFloatingIP(self):
         self._test_floating_ip_action('remove')
 
+    def _check_agent_method_called(self, agent, calls, namespace):
+        if namespace:
+            self.mock_ip.netns.execute.assert_has_calls(
+                [mock.call(call, check_exit_code=False) for call in calls],
+                any_order=True)
+        else:
+            self.utils_exec.assert_has_calls([
+                mock.call(call, root_helper='sudo',
+                          check_exit_code=False) for call in calls],
+                any_order=True)
+
+    def _test_routing_table_update(self, namespace):
+        if not namespace:
+            self.conf.set_override('use_namespaces', False)
+
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces)
+        agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
+
+        fake_route1 = {'destination': '135.207.0.0/16',
+                       'nexthop': '1.2.3.4'}
+        fake_route2 = {'destination': '135.207.111.111/32',
+                       'nexthop': '1.2.3.4'}
+
+        agent._update_routing_table(ri, 'replace', fake_route1)
+        expected = [['ip', 'route', 'replace', 'to', '135.207.0.0/16',
+                     'via', '1.2.3.4']]
+        self._check_agent_method_called(agent, expected, namespace)
+
+        agent._update_routing_table(ri, 'delete', fake_route1)
+        expected = [['ip', 'route', 'delete', 'to', '135.207.0.0/16',
+                     'via', '1.2.3.4']]
+        self._check_agent_method_called(agent, expected, namespace)
+
+        agent._update_routing_table(ri, 'replace', fake_route2)
+        expected = [['ip', 'route', 'replace', 'to', '135.207.111.111/32',
+                     'via', '1.2.3.4']]
+        self._check_agent_method_called(agent, expected, namespace)
+
+        agent._update_routing_table(ri, 'delete', fake_route2)
+        expected = [['ip', 'route', 'delete', 'to', '135.207.111.111/32',
+                     'via', '1.2.3.4']]
+        self._check_agent_method_called(agent, expected, namespace)
+
+    def testAgentRoutingTableUpdated(self):
+        self._test_routing_table_update(namespace=True)
+
+    def testAgentRoutingTableUpdatedNoNameSpace(self):
+        self._test_routing_table_update(namespace=False)
+
+    def testRoutesUpdated(self):
+        self._test_routes_updated(namespace=True)
+
+    def testRoutesUpdatedNoNamespace(self):
+        self._test_routes_updated(namespace=False)
+
+    def _test_routes_updated(self, namespace=True):
+        if not namespace:
+            self.conf.set_override('use_namespaces', False)
+        agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
+        router_id = _uuid()
+
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+                                 self.conf.use_namespaces)
+        ri.router = {}
+
+        fake_old_routes = []
+        fake_new_routes = [{'destination': "110.100.31.0/24",
+                            'nexthop': "10.100.10.30"},
+                           {'destination': "110.100.30.0/24",
+                            'nexthop': "10.100.10.30"}]
+        ri.routes = fake_old_routes
+        ri.router['routes'] = fake_new_routes
+        agent.routes_updated(ri)
+
+        expected = [['ip', 'route', 'replace', 'to', '110.100.30.0/24',
+                    'via', '10.100.10.30'],
+                    ['ip', 'route', 'replace', 'to', '110.100.31.0/24',
+                    'via', '10.100.10.30']]
+
+        self._check_agent_method_called(agent, expected, namespace)
+
+        fake_new_routes = [{'destination': "110.100.30.0/24",
+                            'nexthop': "10.100.10.30"}]
+        ri.router['routes'] = fake_new_routes
+        agent.routes_updated(ri)
+        expected = [['ip', 'route', 'delete', 'to', '110.100.31.0/24',
+                    'via', '10.100.10.30']]
+
+        self._check_agent_method_called(agent, expected, namespace)
+        fake_new_routes = []
+        ri.router['routes'] = fake_new_routes
+        agent.routes_updated(ri)
+
+        expected = [['ip', 'route', 'delete', 'to', '110.100.30.0/24',
+                    'via', '10.100.10.30']]
+        self._check_agent_method_called(agent, expected, namespace)
+
     def testProcessRouter(self):
 
         agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
@@ -233,10 +331,12 @@ class TestBasicRouterOperations(unittest2.TestCase):
              'floating_ip_address': '8.8.8.8',
              'fixed_ip_address': '7.7.7.7',
              'port_id': _uuid()}]}
+
         router = {
             'id': router_id,
             l3_constants.FLOATINGIP_KEY: fake_floatingips1['floatingips'],
             l3_constants.INTERFACE_KEY: [internal_port],
+            'routes': [],
             'gw_port': ex_gw_port}
         ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
                                  self.conf.use_namespaces, router=router)
@@ -245,6 +345,7 @@ class TestBasicRouterOperations(unittest2.TestCase):
         # remap floating IP to a new fixed ip
         fake_floatingips2 = copy.deepcopy(fake_floatingips1)
         fake_floatingips2['floatingips'][0]['fixed_ip_address'] = '7.7.7.8'
+
         router[l3_constants.FLOATINGIP_KEY] = fake_floatingips2['floatingips']
         agent.process_router(ri)
 
@@ -274,6 +375,7 @@ class TestBasicRouterOperations(unittest2.TestCase):
         routers = [
             {'id': _uuid(),
              'admin_state_up': True,
+             'routes': [],
              'external_gateway_info': {}}]
         agent._process_routers(routers)