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 <router_id> \
        --routes type=dict list=true destination=40.0.1.0/24,nexthop=10.1.0.10

Change-Id: I2a11486709e55d3143373858254febaabb93cfe8
This commit is contained in:
Nachi Ueno 2013-01-16 17:52:47 -08:00
parent e1c0d2a8f7
commit fcac32f04f
14 changed files with 967 additions and 33 deletions

View File

@ -35,6 +35,7 @@ from quantum.agent.linux import utils
from quantum.agent import rpc as agent_rpc from quantum.agent import rpc as agent_rpc
from quantum.common import constants as l3_constants from quantum.common import constants as l3_constants
from quantum.common import topics from quantum.common import topics
from quantum.common import utils as common_utils
from quantum import context from quantum import context
from quantum import manager from quantum import manager
from quantum.openstack.common import importutils from quantum.openstack.common import importutils
@ -105,6 +106,8 @@ class RouterInfo(object):
#FIXME(danwent): use_ipv6=True, #FIXME(danwent): use_ipv6=True,
namespace=self.ns_name()) namespace=self.ns_name())
self.routes = []
def ns_name(self): def ns_name(self):
if self.use_namespaces: if self.use_namespaces:
return NS_PREFIX + self.router_id return NS_PREFIX + self.router_id
@ -319,6 +322,8 @@ class L3NATAgent(manager.Manager):
ri.ex_gw_port = ex_gw_port ri.ex_gw_port = ex_gw_port
self.routes_updated(ri)
def process_router_floating_ips(self, ri, ex_gw_port): def process_router_floating_ips(self, ri, ex_gw_port):
floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, []) floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, [])
existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips]) 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): def after_start(self):
LOG.info(_("L3 agent started")) 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): class L3NATAgentWithStateReport(L3NATAgent):

View File

@ -162,3 +162,24 @@ def compare_elements(a, b):
if b is None: if b is None:
b = [] b = []
return set(a) == set(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]

View File

@ -181,7 +181,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
return [] return []
def _get_route_by_subnet(self, context, subnet_id): 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() return route_qry.filter_by(subnet_id=subnet_id).all()
def _get_subnets_by_network(self, context, network_id): def _get_subnets_by_network(self, context, network_id):
@ -1085,7 +1085,8 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED: if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED:
for rt in s['host_routes']: for rt in s['host_routes']:
route = models_v2.Route(subnet_id=subnet.id, route = models_v2.SubnetRoute(
subnet_id=subnet.id,
destination=rt['destination'], destination=rt['destination'],
nexthop=rt['nexthop']) nexthop=rt['nexthop'])
context.session.add(route) context.session.add(route)
@ -1157,7 +1158,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
if _combine(route) == route_str: if _combine(route) == route_str:
context.session.delete(route) context.session.delete(route)
for route_str in new_route_set - old_route_set: for route_str in new_route_set - old_route_set:
route = models_v2.Route( route = models_v2.SubnetRoute(
destination=route_str.partition("_")[0], destination=route_str.partition("_")[0],
nexthop=route_str.partition("_")[2], nexthop=route_str.partition("_")[2],
subnet_id=id) subnet_id=id)

174
quantum/db/extraroute_db.py Normal file
View File

@ -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)

View File

@ -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')

View File

@ -92,6 +92,19 @@ class IPAllocation(model_base.BASEV2):
expiration = sa.Column(sa.DateTime, nullable=True) 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): class Port(model_base.BASEV2, HasId, HasTenant):
"""Represents a port on a quantum v2 network.""" """Represents a port on a quantum v2 network."""
name = sa.Column(sa.String(255)) name = sa.Column(sa.String(255))
@ -114,16 +127,6 @@ class DNSNameServer(model_base.BASEV2):
primary_key=True) 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): class Subnet(model_base.BASEV2, HasId, HasTenant):
"""Represents a quantum subnet. """Represents a quantum subnet.
@ -143,7 +146,7 @@ class Subnet(model_base.BASEV2, HasId, HasTenant):
dns_nameservers = orm.relationship(DNSNameServer, dns_nameservers = orm.relationship(DNSNameServer,
backref='subnet', backref='subnet',
cascade='delete') cascade='delete')
routes = orm.relationship(Route, routes = orm.relationship(SubnetRoute,
backref='subnet', backref='subnet',
cascade='delete') cascade='delete')
shared = sa.Column(sa.Boolean) shared = sa.Column(sa.Boolean)

View File

@ -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 {}

View File

@ -28,7 +28,7 @@ from quantum.db import agents_db
from quantum.db import api as db_api from quantum.db import api as db_api
from quantum.db import db_base_plugin_v2 from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base 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 l3_rpc_base
# NOTE: quota_db cannot be removed, it is for db model # NOTE: quota_db cannot be removed, it is for db model
from quantum.db import quota_db from quantum.db import quota_db
@ -172,7 +172,7 @@ class AgentNotifierApi(proxy.RpcProxy,
class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2, class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
l3_db.L3_NAT_db_mixin, extraroute_db.ExtraRoute_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin, sg_db_rpc.SecurityGroupServerRpcMixin,
agents_db.AgentDbMixin): agents_db.AgentDbMixin):
"""Implement the Quantum abstractions using Linux bridging. """Implement the Quantum abstractions using Linux bridging.
@ -197,7 +197,7 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
__native_bulk_support = True __native_bulk_support = True
supported_extension_aliases = ["provider", "router", "binding", "quotas", supported_extension_aliases = ["provider", "router", "binding", "quotas",
"security-group", "agent"] "security-group", "agent", "extraroute"]
network_view = "extension:provider_network:view" network_view = "extension:provider_network:view"
network_set = "extension:provider_network:set" network_set = "extension:provider_network:set"

View File

@ -20,6 +20,7 @@ from oslo.config import cfg
from quantum.common import exceptions as exc from quantum.common import exceptions as exc
from quantum.db import api as db from quantum.db import api as db
from quantum.db import db_base_plugin_v2 from quantum.db import db_base_plugin_v2
from quantum.db import extraroute_db
from quantum.db import l3_db from quantum.db import l3_db
from quantum.db import models_v2 from quantum.db import models_v2
from quantum.extensions.flavor import (FLAVOR_NETWORK, FLAVOR_ROUTER) from quantum.extensions.flavor import (FLAVOR_NETWORK, FLAVOR_ROUTER)
@ -45,13 +46,13 @@ class FaildToAddFlavorBinding(exc.QuantumException):
class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2, class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
l3_db.L3_NAT_db_mixin): extraroute_db.ExtraRoute_db_mixin):
def __init__(self, configfile=None): def __init__(self, configfile=None):
LOG.debug(_("Start initializing metaplugin")) LOG.debug(_("Start initializing metaplugin"))
self.supported_extension_aliases = \ self.supported_extension_aliases = \
cfg.CONF.META.supported_extension_aliases.split(',') cfg.CONF.META.supported_extension_aliases.split(',')
self.supported_extension_aliases += ['flavor', 'router'] self.supported_extension_aliases += ['flavor', 'router', 'extraroute']
# Ignore config option overapping # Ignore config option overapping
def _is_opt_registered(opts, opt): def _is_opt_registered(opts, opt):

View File

@ -22,7 +22,7 @@ from quantum.common import rpc as q_rpc
from quantum.common import topics from quantum.common import topics
from quantum import context from quantum import context
from quantum.db import dhcp_rpc_base 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 l3_rpc_base
#NOTE(amotoki): quota_db cannot be removed, it is for db model #NOTE(amotoki): quota_db cannot be removed, it is for db model
from quantum.db import quota_db from quantum.db import quota_db
@ -58,7 +58,7 @@ class OperationalStatus:
class NECPluginV2(nec_plugin_base.NECPluginV2Base, class NECPluginV2(nec_plugin_base.NECPluginV2Base,
l3_db.L3_NAT_db_mixin, extraroute_db.ExtraRoute_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin): sg_db_rpc.SecurityGroupServerRpcMixin):
"""NECPluginV2 controls an OpenFlow Controller. """NECPluginV2 controls an OpenFlow Controller.
@ -74,7 +74,7 @@ class NECPluginV2(nec_plugin_base.NECPluginV2Base,
""" """
supported_extension_aliases = ["router", "quotas", "binding", supported_extension_aliases = ["router", "quotas", "binding",
"security-group"] "security-group", "extraroute"]
binding_view = "extension:port_binding:view" binding_view = "extension:port_binding:view"
binding_set = "extension:port_binding:set" binding_set = "extension:port_binding:set"

View File

@ -33,7 +33,7 @@ from quantum.common import topics
from quantum.db import agents_db from quantum.db import agents_db
from quantum.db import db_base_plugin_v2 from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base 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 l3_rpc_base
# NOTE: quota_db cannot be removed, it is for db model # NOTE: quota_db cannot be removed, it is for db model
from quantum.db import quota_db from quantum.db import quota_db
@ -209,10 +209,9 @@ class AgentNotifierApi(proxy.RpcProxy,
class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2, class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
l3_db.L3_NAT_db_mixin, extraroute_db.ExtraRoute_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin, sg_db_rpc.SecurityGroupServerRpcMixin,
agents_db.AgentDbMixin): agents_db.AgentDbMixin):
"""Implement the Quantum abstractions using Open vSwitch. """Implement the Quantum abstractions using Open vSwitch.
Depending on whether tunneling is enabled, either a GRE tunnel or 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 __native_bulk_support = True
supported_extension_aliases = ["provider", "router", supported_extension_aliases = ["provider", "router",
"binding", "quotas", "security-group", "binding", "quotas", "security-group",
"agent"] "agent",
"extraroute"]
network_view = "extension:provider_network:view" network_view = "extension:provider_network:view"
network_set = "extension:provider_network:set" network_set = "extension:provider_network:set"

View File

@ -27,7 +27,7 @@ from quantum.common import topics
from quantum.db import api as db from quantum.db import api as db
from quantum.db import db_base_plugin_v2 from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base 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 l3_rpc_base
from quantum.db import models_v2 from quantum.db import models_v2
from quantum.openstack.common import log as logging 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, 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): def __init__(self, configfile=None):
db.configure_db() db.configure_db()

View File

@ -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)

View File

@ -91,7 +91,7 @@ class TestBasicRouterOperations(unittest2.TestCase):
self.assertTrue(ri.ns_name().endswith(id)) self.assertTrue(ri.ns_name().endswith(id))
def testAgentCreate(self): def testAgentCreate(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) l3_agent.L3NATAgent(HOSTNAME, self.conf)
def _test_internal_network_action(self, action): def _test_internal_network_action(self, action):
port_id = _uuid() port_id = _uuid()
@ -100,7 +100,6 @@ class TestBasicRouterOperations(unittest2.TestCase):
ri = l3_agent.RouterInfo(router_id, self.conf.root_helper, ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
self.conf.use_namespaces) self.conf.use_namespaces)
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
interface_name = agent.get_internal_device_name(port_id)
cidr = '99.0.1.9/24' cidr = '99.0.1.9/24'
mac = 'ca:fe:de:ad:be:ef' mac = 'ca:fe:de:ad:be:ef'
ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30'}]} ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30'}]}
@ -209,6 +208,105 @@ class TestBasicRouterOperations(unittest2.TestCase):
def testAgentRemoveFloatingIP(self): def testAgentRemoveFloatingIP(self):
self._test_floating_ip_action('remove') 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): def testProcessRouter(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
@ -233,10 +331,12 @@ class TestBasicRouterOperations(unittest2.TestCase):
'floating_ip_address': '8.8.8.8', 'floating_ip_address': '8.8.8.8',
'fixed_ip_address': '7.7.7.7', 'fixed_ip_address': '7.7.7.7',
'port_id': _uuid()}]} 'port_id': _uuid()}]}
router = { router = {
'id': router_id, 'id': router_id,
l3_constants.FLOATINGIP_KEY: fake_floatingips1['floatingips'], l3_constants.FLOATINGIP_KEY: fake_floatingips1['floatingips'],
l3_constants.INTERFACE_KEY: [internal_port], l3_constants.INTERFACE_KEY: [internal_port],
'routes': [],
'gw_port': ex_gw_port} 'gw_port': ex_gw_port}
ri = l3_agent.RouterInfo(router_id, self.conf.root_helper, ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
self.conf.use_namespaces, router=router) self.conf.use_namespaces, router=router)
@ -245,6 +345,7 @@ class TestBasicRouterOperations(unittest2.TestCase):
# remap floating IP to a new fixed ip # remap floating IP to a new fixed ip
fake_floatingips2 = copy.deepcopy(fake_floatingips1) fake_floatingips2 = copy.deepcopy(fake_floatingips1)
fake_floatingips2['floatingips'][0]['fixed_ip_address'] = '7.7.7.8' fake_floatingips2['floatingips'][0]['fixed_ip_address'] = '7.7.7.8'
router[l3_constants.FLOATINGIP_KEY] = fake_floatingips2['floatingips'] router[l3_constants.FLOATINGIP_KEY] = fake_floatingips2['floatingips']
agent.process_router(ri) agent.process_router(ri)
@ -274,6 +375,7 @@ class TestBasicRouterOperations(unittest2.TestCase):
routers = [ routers = [
{'id': _uuid(), {'id': _uuid(),
'admin_state_up': True, 'admin_state_up': True,
'routes': [],
'external_gateway_info': {}}] 'external_gateway_info': {}}]
agent._process_routers(routers) agent._process_routers(routers)