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:
parent
e1c0d2a8f7
commit
fcac32f04f
@ -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):
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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
174
quantum/db/extraroute_db.py
Normal 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)
|
@ -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')
|
@ -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)
|
||||||
|
74
quantum/extensions/extraroute.py
Normal file
74
quantum/extensions/extraroute.py
Normal 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 {}
|
@ -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"
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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()
|
||||||
|
449
quantum/tests/unit/test_extension_extraroute.py
Normal file
449
quantum/tests/unit/test_extension_extraroute.py
Normal 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)
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user