Properly support neutron subnet `host_routes` on the router appliance.
This commit is contained in:
parent
dc537d739a
commit
9fdc413eec
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'],
|
||||
|
|
1
setup.py
1
setup.py
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue