from quantum.agent import rpc as agent_rpc
from quantum.common import constants as l3_constants
from quantum.common import topics
+from quantum.common import utils as common_utils
from quantum import context
from quantum import manager
from quantum.openstack.common import importutils
#FIXME(danwent): use_ipv6=True,
namespace=self.ns_name())
+ self.routes = []
+
def ns_name(self):
if self.use_namespaces:
return NS_PREFIX + self.router_id
ri.ex_gw_port = ex_gw_port
+ self.routes_updated(ri)
+
def process_router_floating_ips(self, ri, ex_gw_port):
floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, [])
existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips])
def after_start(self):
LOG.info(_("L3 agent started"))
+ def _update_routing_table(self, ri, operation, route):
+ cmd = ['ip', 'route', operation, 'to', route['destination'],
+ 'via', route['nexthop']]
+ #TODO(nati) move this code to iplib
+ if self.conf.use_namespaces:
+ ip_wrapper = ip_lib.IPWrapper(self.conf.root_helper,
+ namespace=ri.ns_name())
+ ip_wrapper.netns.execute(cmd, check_exit_code=False)
+ else:
+ utils.execute(cmd, check_exit_code=False,
+ root_helper=self.conf.root_helper)
+
+ def routes_updated(self, ri):
+ new_routes = ri.router['routes']
+ old_routes = ri.routes
+ adds, removes = common_utils.diff_list_of_dict(old_routes,
+ new_routes)
+ for route in adds:
+ LOG.debug(_("Added route entry is '%s'"), route)
+ # remove replaced route from deleted route
+ for del_route in removes:
+ if route['destination'] == del_route['destination']:
+ removes.remove(del_route)
+ #replace success even if there is no existing route
+ self._update_routing_table(ri, 'replace', route)
+ for route in removes:
+ LOG.debug(_("Removed route entry is '%s'"), route)
+ self._update_routing_table(ri, 'delete', route)
+ ri.routes = new_routes
+
class L3NATAgentWithStateReport(L3NATAgent):
if b is None:
b = []
return set(a) == set(b)
+
+
+def dict2str(dic):
+ return ','.join("%s=%s" % (key, val)
+ for key, val in sorted(dic.iteritems()))
+
+
+def str2dict(string):
+ res_dict = {}
+ for keyvalue in string.split(',', 1):
+ (key, value) = keyvalue.split('=', 1)
+ res_dict[key] = value
+ return res_dict
+
+
+def diff_list_of_dict(old_list, new_list):
+ new_set = set([dict2str(l) for l in new_list])
+ old_set = set([dict2str(l) for l in old_list])
+ added = new_set - old_set
+ removed = old_set - new_set
+ return [str2dict(a) for a in added], [str2dict(r) for r in removed]
return []
def _get_route_by_subnet(self, context, subnet_id):
- route_qry = context.session.query(models_v2.Route)
+ route_qry = context.session.query(models_v2.SubnetRoute)
return route_qry.filter_by(subnet_id=subnet_id).all()
def _get_subnets_by_network(self, context, network_id):
if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED:
for rt in s['host_routes']:
- route = models_v2.Route(subnet_id=subnet.id,
- destination=rt['destination'],
- nexthop=rt['nexthop'])
+ route = models_v2.SubnetRoute(
+ subnet_id=subnet.id,
+ destination=rt['destination'],
+ nexthop=rt['nexthop'])
context.session.add(route)
for pool in s['allocation_pools']:
if _combine(route) == route_str:
context.session.delete(route)
for route_str in new_route_set - old_route_set:
- route = models_v2.Route(
+ route = models_v2.SubnetRoute(
destination=route_str.partition("_")[0],
nexthop=route_str.partition("_")[2],
subnet_id=id)
--- /dev/null
+# 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)
--- /dev/null
+# 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')
expiration = sa.Column(sa.DateTime, nullable=True)
+class Route(object):
+ """mixin of a route."""
+ destination = sa.Column(sa.String(64), nullable=False, primary_key=True)
+ nexthop = sa.Column(sa.String(64), nullable=False, primary_key=True)
+
+
+class SubnetRoute(model_base.BASEV2, Route):
+ subnet_id = sa.Column(sa.String(36),
+ sa.ForeignKey('subnets.id',
+ ondelete="CASCADE"),
+ primary_key=True)
+
+
class Port(model_base.BASEV2, HasId, HasTenant):
"""Represents a port on a quantum v2 network."""
name = sa.Column(sa.String(255))
primary_key=True)
-class Route(model_base.BASEV2):
- """Represents a route for a subnet or port."""
- destination = sa.Column(sa.String(64), nullable=False, primary_key=True)
- nexthop = sa.Column(sa.String(64), nullable=False, primary_key=True)
- subnet_id = sa.Column(sa.String(36),
- sa.ForeignKey('subnets.id',
- ondelete="CASCADE"),
- primary_key=True)
-
-
class Subnet(model_base.BASEV2, HasId, HasTenant):
"""Represents a quantum subnet.
dns_nameservers = orm.relationship(DNSNameServer,
backref='subnet',
cascade='delete')
- routes = orm.relationship(Route,
+ routes = orm.relationship(SubnetRoute,
backref='subnet',
cascade='delete')
shared = sa.Column(sa.Boolean)
--- /dev/null
+# 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 {}
from quantum.db import api as db_api
from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
from quantum.db import l3_rpc_base
# NOTE: quota_db cannot be removed, it is for db model
from quantum.db import quota_db
class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
- l3_db.L3_NAT_db_mixin,
+ extraroute_db.ExtraRoute_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin,
agents_db.AgentDbMixin):
"""Implement the Quantum abstractions using Linux bridging.
__native_bulk_support = True
supported_extension_aliases = ["provider", "router", "binding", "quotas",
- "security-group", "agent"]
+ "security-group", "agent", "extraroute"]
network_view = "extension:provider_network:view"
network_set = "extension:provider_network:set"
from quantum.common import exceptions as exc
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
+from quantum.db import extraroute_db
from quantum.db import l3_db
from quantum.db import models_v2
from quantum.extensions.flavor import (FLAVOR_NETWORK, FLAVOR_ROUTER)
class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
- l3_db.L3_NAT_db_mixin):
+ extraroute_db.ExtraRoute_db_mixin):
def __init__(self, configfile=None):
LOG.debug(_("Start initializing metaplugin"))
self.supported_extension_aliases = \
cfg.CONF.META.supported_extension_aliases.split(',')
- self.supported_extension_aliases += ['flavor', 'router']
+ self.supported_extension_aliases += ['flavor', 'router', 'extraroute']
# Ignore config option overapping
def _is_opt_registered(opts, opt):
from quantum.common import topics
from quantum import context
from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
from quantum.db import l3_rpc_base
#NOTE(amotoki): quota_db cannot be removed, it is for db model
from quantum.db import quota_db
class NECPluginV2(nec_plugin_base.NECPluginV2Base,
- l3_db.L3_NAT_db_mixin,
+ extraroute_db.ExtraRoute_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin):
"""NECPluginV2 controls an OpenFlow Controller.
"""
supported_extension_aliases = ["router", "quotas", "binding",
- "security-group"]
+ "security-group", "extraroute"]
binding_view = "extension:port_binding:view"
binding_set = "extension:port_binding:set"
from quantum.db import agents_db
from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
from quantum.db import l3_rpc_base
# NOTE: quota_db cannot be removed, it is for db model
from quantum.db import quota_db
class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
- l3_db.L3_NAT_db_mixin,
+ extraroute_db.ExtraRoute_db_mixin,
sg_db_rpc.SecurityGroupServerRpcMixin,
agents_db.AgentDbMixin):
-
"""Implement the Quantum abstractions using Open vSwitch.
Depending on whether tunneling is enabled, either a GRE tunnel or
__native_bulk_support = True
supported_extension_aliases = ["provider", "router",
"binding", "quotas", "security-group",
- "agent"]
+ "agent",
+ "extraroute"]
network_view = "extension:provider_network:view"
network_set = "extension:provider_network:set"
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base
-from quantum.db import l3_db
+from quantum.db import extraroute_db
from quantum.db import l3_rpc_base
from quantum.db import models_v2
from quantum.openstack.common import log as logging
class RyuQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
- l3_db.L3_NAT_db_mixin):
+ extraroute_db.ExtraRoute_db_mixin):
- supported_extension_aliases = ["router"]
+ supported_extension_aliases = ["router", "extraroute"]
def __init__(self, configfile=None):
db.configure_db()
--- /dev/null
+# 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)
self.assertTrue(ri.ns_name().endswith(id))
def testAgentCreate(self):
- agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
+ l3_agent.L3NATAgent(HOSTNAME, self.conf)
def _test_internal_network_action(self, action):
port_id = _uuid()
ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
self.conf.use_namespaces)
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
- interface_name = agent.get_internal_device_name(port_id)
cidr = '99.0.1.9/24'
mac = 'ca:fe:de:ad:be:ef'
ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30'}]}
def testAgentRemoveFloatingIP(self):
self._test_floating_ip_action('remove')
+ def _check_agent_method_called(self, agent, calls, namespace):
+ if namespace:
+ self.mock_ip.netns.execute.assert_has_calls(
+ [mock.call(call, check_exit_code=False) for call in calls],
+ any_order=True)
+ else:
+ self.utils_exec.assert_has_calls([
+ mock.call(call, root_helper='sudo',
+ check_exit_code=False) for call in calls],
+ any_order=True)
+
+ def _test_routing_table_update(self, namespace):
+ if not namespace:
+ self.conf.set_override('use_namespaces', False)
+
+ router_id = _uuid()
+ ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+ self.conf.use_namespaces)
+ agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
+
+ fake_route1 = {'destination': '135.207.0.0/16',
+ 'nexthop': '1.2.3.4'}
+ fake_route2 = {'destination': '135.207.111.111/32',
+ 'nexthop': '1.2.3.4'}
+
+ agent._update_routing_table(ri, 'replace', fake_route1)
+ expected = [['ip', 'route', 'replace', 'to', '135.207.0.0/16',
+ 'via', '1.2.3.4']]
+ self._check_agent_method_called(agent, expected, namespace)
+
+ agent._update_routing_table(ri, 'delete', fake_route1)
+ expected = [['ip', 'route', 'delete', 'to', '135.207.0.0/16',
+ 'via', '1.2.3.4']]
+ self._check_agent_method_called(agent, expected, namespace)
+
+ agent._update_routing_table(ri, 'replace', fake_route2)
+ expected = [['ip', 'route', 'replace', 'to', '135.207.111.111/32',
+ 'via', '1.2.3.4']]
+ self._check_agent_method_called(agent, expected, namespace)
+
+ agent._update_routing_table(ri, 'delete', fake_route2)
+ expected = [['ip', 'route', 'delete', 'to', '135.207.111.111/32',
+ 'via', '1.2.3.4']]
+ self._check_agent_method_called(agent, expected, namespace)
+
+ def testAgentRoutingTableUpdated(self):
+ self._test_routing_table_update(namespace=True)
+
+ def testAgentRoutingTableUpdatedNoNameSpace(self):
+ self._test_routing_table_update(namespace=False)
+
+ def testRoutesUpdated(self):
+ self._test_routes_updated(namespace=True)
+
+ def testRoutesUpdatedNoNamespace(self):
+ self._test_routes_updated(namespace=False)
+
+ def _test_routes_updated(self, namespace=True):
+ if not namespace:
+ self.conf.set_override('use_namespaces', False)
+ agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
+ router_id = _uuid()
+
+ ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
+ self.conf.use_namespaces)
+ ri.router = {}
+
+ fake_old_routes = []
+ fake_new_routes = [{'destination': "110.100.31.0/24",
+ 'nexthop': "10.100.10.30"},
+ {'destination': "110.100.30.0/24",
+ 'nexthop': "10.100.10.30"}]
+ ri.routes = fake_old_routes
+ ri.router['routes'] = fake_new_routes
+ agent.routes_updated(ri)
+
+ expected = [['ip', 'route', 'replace', 'to', '110.100.30.0/24',
+ 'via', '10.100.10.30'],
+ ['ip', 'route', 'replace', 'to', '110.100.31.0/24',
+ 'via', '10.100.10.30']]
+
+ self._check_agent_method_called(agent, expected, namespace)
+
+ fake_new_routes = [{'destination': "110.100.30.0/24",
+ 'nexthop': "10.100.10.30"}]
+ ri.router['routes'] = fake_new_routes
+ agent.routes_updated(ri)
+ expected = [['ip', 'route', 'delete', 'to', '110.100.31.0/24',
+ 'via', '10.100.10.30']]
+
+ self._check_agent_method_called(agent, expected, namespace)
+ fake_new_routes = []
+ ri.router['routes'] = fake_new_routes
+ agent.routes_updated(ri)
+
+ expected = [['ip', 'route', 'delete', 'to', '110.100.30.0/24',
+ 'via', '10.100.10.30']]
+ self._check_agent_method_called(agent, expected, namespace)
+
def testProcessRouter(self):
agent = l3_agent.L3NATAgent(HOSTNAME, self.conf)
'floating_ip_address': '8.8.8.8',
'fixed_ip_address': '7.7.7.7',
'port_id': _uuid()}]}
+
router = {
'id': router_id,
l3_constants.FLOATINGIP_KEY: fake_floatingips1['floatingips'],
l3_constants.INTERFACE_KEY: [internal_port],
+ 'routes': [],
'gw_port': ex_gw_port}
ri = l3_agent.RouterInfo(router_id, self.conf.root_helper,
self.conf.use_namespaces, router=router)
# remap floating IP to a new fixed ip
fake_floatingips2 = copy.deepcopy(fake_floatingips1)
fake_floatingips2['floatingips'][0]['fixed_ip_address'] = '7.7.7.8'
+
router[l3_constants.FLOATINGIP_KEY] = fake_floatingips2['floatingips']
agent.process_router(ri)
routers = [
{'id': _uuid(),
'admin_state_up': True,
+ 'routes': [],
'external_gateway_info': {}}]
agent._process_routers(routers)