Properly support neutron subnet `host_routes` on the router appliance.

This commit is contained in:
Ryan Petrello 2014-06-13 12:03:41 -04:00
parent dc537d739a
commit 9fdc413eec
7 changed files with 195 additions and 5 deletions

View File

@ -18,6 +18,7 @@
"""Set up the API server application instance
"""
import flask
from flask.ext import shelve
from akanda.router.api import v1
from akanda.router.debug import handle_traceback
@ -30,6 +31,9 @@ app.register_blueprint(v1.firewall.blueprint)
app.register_blueprint(v1.status.blueprint)
app.register_error_handler(500, handle_traceback)
app.config['SHELVE_FILENAME'] = '/tmp/akanda-state'
shelve.init_app(app)
@app.before_request
def attach_config():

View File

@ -20,6 +20,7 @@ Blueprint for the "system" portion of the version 1 of the API.
"""
from flask import Response
from flask import abort, request
from flask.ext import shelve
from akanda.router import models
from akanda.router import utils
@ -73,5 +74,5 @@ def put_configuration():
'The config failed to validate.\n' + '\n'.join(errors),
status=422)
manager.update_config(config_candidate)
manager.update_config(config_candidate, shelve)
return dict(configuration=manager.config)

View File

@ -61,6 +61,48 @@ class RouteManager(base.Manager):
self._set_default_gateway(subnet.gateway_ip)
gw_set[subnet.gateway_ip.version] = True
def update_host_routes(self, config, db):
for net in config.networks:
# For each subnet...
for subnet in net.subnets:
cidr = str(subnet.cidr)
# ...if there are no host_routes configured, unset all of the
# previous host_routes for this subnet, and move on to the next
# subnet
if not subnet.host_routes:
self._empty_host_routes(cidr, db)
continue
# determine the set of previously written routes for this cidr
if cidr not in db:
db[cidr] = set()
current = db[cidr]
# build a set of new routes for this cidr
latest = set()
for r in subnet.host_routes:
latest.add((r.destination, r.next_hop))
# If the set of previously written routes contains routes that
# aren't defined in the new config, run commands to delete them
for x in current - latest:
if self._alter_route('delete', *x):
current.remove(x)
# If the new config contains routes that aren't defined in the
# set of previously written routes, run commands to add them
for x in latest - current:
if self._alter_route('add', *x):
current.add(x)
# This assignment *is* necessary - Python's `shelve`
# implementation isn't smart enough to capture the changes to
# the reference above, so this setitem call triggers a DB sync
db[cidr] = current
def _get_default_gateway(self, version):
current = None
try:
@ -93,3 +135,22 @@ class RouteManager(base.Manager):
return self.sudo('change', version, 'default', desired)
# Nothing to do
return ''
def _empty_host_routes(self, cidr, db):
if cidr in db:
for x in db[cidr]:
self._alter_route('delete', *x)
del db[cidr]
def _alter_route(self, action, destination, next_hop):
version = '-inet'
if destination.version == 6:
version += '6'
try:
return self.sudo(action, version, str(destination), str(next_hop))
except RuntimeError as e:
# Since these are user-supplied custom routes, it's very possible
# that adding/removing them will fail. A failure to apply one of
# these custom rules, however, should *not* cause an overall router
# failure.
LOG.warn('Route could not be %sed: %s' % (action, unicode(e)))

View File

@ -43,7 +43,7 @@ class Manager(object):
return self._config
def update_config(self, config):
def update_config(self, config, db):
self._config = config
self.update_interfaces()
@ -51,7 +51,7 @@ class Manager(object):
self.update_metadata()
self.update_bgp_and_radv()
self.update_pf()
self.update_routes()
self.update_routes(db)
self.update_arp()
# TODO(mark): update_vpn
@ -83,9 +83,10 @@ class Manager(object):
mgr = pf.PFManager()
mgr.update_conf(rule_data)
def update_routes(self):
def update_routes(self, db):
mgr = route.RouteManager()
mgr.update_default(self.config)
mgr.update_host_routes(self.config, db.get_shelve('c'))
def update_arp(self):
mgr = arp.ARPManager()

View File

@ -428,7 +428,7 @@ class Subnet(ModelBase):
@classmethod
def from_dict(cls, d):
host_routes = [StaticRoute(r['destination'], r['next_hop'])
host_routes = [StaticRoute(r['destination'], r['nexthop'])
for r in d.get('host_routes', [])]
return cls(
d['cidr'],

View File

@ -28,6 +28,7 @@ setup(
license='BSD',
install_requires=[
'flask>=0.9',
'flask-shelve>=0.1.1',
'gunicorn>=0.14.6,<19',
'netaddr>=0.7.7',
'eventlet>=0.9.17',

View File

@ -18,6 +18,8 @@
import mock
import unittest2
import netaddr
from akanda.router import models
from akanda.router.drivers import route
@ -260,3 +262,123 @@ sockaddrs: <DST,GATEWAY,NETMASK,IFP,IFA,LABEL>
net = c.networks[0]
snet = net.subnets[0]
set.assert_called_once_with(snet.gateway_ip)
@mock.patch.object(route.RouteManager, '_set_default_gateway',
lambda *a, **kw: None)
def test_custom_host_routes(self):
subnet = dict(
cidr='192.168.89.0/24',
gateway_ip='192.168.89.1',
dhcp_enabled=True,
dns_nameservers=[],
host_routes=[{
'destination': '192.240.128.0/20',
'nexthop': '192.168.89.2'
}]
)
network = dict(
network_id='netid',
interface=dict(ifname='ge0', addresses=['fe80::2']),
subnets=[subnet]
)
c = models.Configuration({'networks': [network]})
db = {}
with mock.patch.object(self.mgr, 'sudo') as sudo:
# ...so let's add one!
self.mgr.update_host_routes(c, db)
sudo.assert_called_once_with(
'add', '-inet', '192.240.128.0/20', '192.168.89.2'
)
# db[subnet.cidr] should contain the above route
expected = set()
expected.add((
netaddr.IPNetwork('192.240.138.0/20'),
netaddr.IPAddress('192.168.89.2')
))
self.assertEqual(len(db), 1)
self.assertEqual(
db[subnet['cidr']] - expected,
set()
)
# Empty the host_routes list
sudo.reset_mock()
subnet['host_routes'] = []
c = models.Configuration({'networks': [network]})
self.mgr.update_host_routes(c, db)
sudo.assert_called_once_with(
'delete', '-inet', '192.240.128.0/20', '192.168.89.2'
)
self.assertEqual(len(db), 0)
# ...this time, let's add multiple routes and ensure they're added
sudo.reset_mock()
subnet['host_routes'] = [{
'destination': '192.240.128.0/20',
'nexthop': '192.168.89.2'
}, {
'destination': '192.220.128.0/20',
'nexthop': '192.168.89.3'
}]
c = models.Configuration({'networks': [network]})
self.mgr.update_host_routes(c, db)
self.assertEqual(sudo.call_args_list, [
mock.call('add', '-inet', '192.240.128.0/20', '192.168.89.2'),
mock.call('add', '-inet', '192.220.128.0/20', '192.168.89.3'),
])
# ...let's remove one and add another...
sudo.reset_mock()
subnet['host_routes'] = [{
'destination': '192.240.128.0/20',
'nexthop': '192.168.89.2'
}, {
'destination': '192.185.128.0/20',
'nexthop': '192.168.89.4'
}]
c = models.Configuration({'networks': [network]})
self.mgr.update_host_routes(c, db)
self.assertEqual(sudo.call_args_list, [
mock.call('delete', '-inet', '192.220.128.0/20',
'192.168.89.3'),
mock.call('add', '-inet', '192.185.128.0/20', '192.168.89.4')
])
# ...let's add another subnet...
self.assertEqual(len(db), 1)
sudo.reset_mock()
network['subnets'].append(dict(
cidr='192.168.90.0/24',
gateway_ip='192.168.90.1',
dhcp_enabled=True,
dns_nameservers=[],
host_routes=[{
'destination': '192.240.128.0/20',
'nexthop': '192.168.90.1'
}]
))
c = models.Configuration({'networks': [network]})
self.mgr.update_host_routes(c, db)
self.assertEqual(sudo.call_args_list, [
mock.call('add', '-inet', '192.240.128.0/20', '192.168.90.1')
])
self.assertEqual(len(db), 2)
# ...and finally, delete all custom host_routes...
sudo.reset_mock()
network['subnets'][0]['host_routes'] = []
network['subnets'][1]['host_routes'] = []
c = models.Configuration({'networks': [network]})
self.mgr.update_host_routes(c, db)
self.assertEqual(sudo.call_args_list, [
mock.call('delete', '-inet', '192.185.128.0/20',
'192.168.89.4'),
mock.call('delete', '-inet', '192.240.128.0/20',
'192.168.89.2'),
mock.call('delete', '-inet', '192.240.128.0/20',
'192.168.90.1'),
])
self.assertEqual(len(db), 0)