From: Nachi Ueno Date: Thu, 17 Jan 2013 01:52:47 +0000 (-0800) Subject: Routing table configuration support on L3 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=7ffc2a2b6fbf3938e92e75374dbf7fdced5be4ea;p=openstack-build%2Fneutron-build.git Routing table configuration support on L3 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 \ --routes type=dict list=true destination=40.0.1.0/24,nexthop=10.1.0.10 Change-Id: I2a11486709e55d3143373858254febaabb93cfe8 --- diff --git a/quantum/agent/l3_agent.py b/quantum/agent/l3_agent.py index d8f3eaf2f..e47109cae 100644 --- a/quantum/agent/l3_agent.py +++ b/quantum/agent/l3_agent.py @@ -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): diff --git a/quantum/common/utils.py b/quantum/common/utils.py index da0e01915..761c60c10 100644 --- a/quantum/common/utils.py +++ b/quantum/common/utils.py @@ -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] diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index f129cf0ac..078924b97 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -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 index 000000000..6282cc211 --- /dev/null +++ b/quantum/db/extraroute_db.py @@ -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 index 000000000..e223b2174 --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/1c33fa3cd1a1_extra_route_config.py @@ -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') diff --git a/quantum/db/models_v2.py b/quantum/db/models_v2.py index def066d99..122828fb9 100644 --- a/quantum/db/models_v2.py +++ b/quantum/db/models_v2.py @@ -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 index 000000000..64588c9e6 --- /dev/null +++ b/quantum/extensions/extraroute.py @@ -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 {} diff --git a/quantum/plugins/linuxbridge/lb_quantum_plugin.py b/quantum/plugins/linuxbridge/lb_quantum_plugin.py index db6948e49..8eff5112d 100644 --- a/quantum/plugins/linuxbridge/lb_quantum_plugin.py +++ b/quantum/plugins/linuxbridge/lb_quantum_plugin.py @@ -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" diff --git a/quantum/plugins/metaplugin/meta_quantum_plugin.py b/quantum/plugins/metaplugin/meta_quantum_plugin.py index ef173eede..7ec3ffe8f 100644 --- a/quantum/plugins/metaplugin/meta_quantum_plugin.py +++ b/quantum/plugins/metaplugin/meta_quantum_plugin.py @@ -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): diff --git a/quantum/plugins/nec/nec_plugin.py b/quantum/plugins/nec/nec_plugin.py index 38dcfbad9..f7ae8da09 100644 --- a/quantum/plugins/nec/nec_plugin.py +++ b/quantum/plugins/nec/nec_plugin.py @@ -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" diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 05a0a8981..e17a56468 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -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" diff --git a/quantum/plugins/ryu/ryu_quantum_plugin.py b/quantum/plugins/ryu/ryu_quantum_plugin.py index 8b09afb76..402a9d353 100644 --- a/quantum/plugins/ryu/ryu_quantum_plugin.py +++ b/quantum/plugins/ryu/ryu_quantum_plugin.py @@ -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 index 000000000..f32310a2c --- /dev/null +++ b/quantum/tests/unit/test_extension_extraroute.py @@ -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) diff --git a/quantum/tests/unit/test_l3_agent.py b/quantum/tests/unit/test_l3_agent.py index fbb3dfad6..7cf8b4307 100644 --- a/quantum/tests/unit/test_l3_agent.py +++ b/quantum/tests/unit/test_l3_agent.py @@ -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)