Introduces advanced service drivers to akanda-appliance

This introduces the ability to create service manager drivers to handle
managing advanced services within the akanda-appliance.

It splits some common things into a System manager.  Existing
stuff that is router-specific is moved to a Router manager and we begin
implementing LBAAS drivers using Nginx.

At the moment, configuration for which drivers are loaded by the appliance
code itself is stored in /etc/default/akanda-appliance.  This is setup by
a DIB_* variable and accessed by the appliance via environment variable. We
should improve this later when we need to expose richer configuration to the
appliance.

We could and should work on the API for this.  Currently, our v1
API is entirely router-specific.  This adds to that and allows the
RUG to attach other advanced service configuratino data to the config
object it pushes.  If the corresponding service's driver has been enabled
in the appliance, it will attempt to find that data and configure the
advanced service accordingly.  Ideally, longterm we want a v2 API
that can reference all services the same.  There's a few ugly compat
hacks added here to maintain compatability with where the RUG expects
certain router resources to be.  We can evolve this over time.

Partially-implements: blueprint appliance-provisioning-driver
Depends-on: Ic19a883f56fb6d65a83b1f4d93b581f9e242d97f
Change-Id: I6048789ec15fad1dbc899cbbd82508433cb96d44
This commit is contained in:
Adam Gandelman 2015-09-21 18:16:00 -07:00
parent 35529c9050
commit 433a4c7190
24 changed files with 1209 additions and 115 deletions

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include akanda/router/drivers/loadbalancer/nginx.conf.template

View File

@ -24,6 +24,7 @@ from dogpile.cache import make_region
from akanda.router import models
from akanda.router import utils
from akanda.router import settings
from akanda.router.manager import manager
blueprint = utils.blueprint_factory(__name__)
@ -32,6 +33,9 @@ blueprint = utils.blueprint_factory(__name__)
_cache = None
ADVANCED_SERVICES_KEY = 'services'
def _get_cache():
global _cache
if _cache is None:
@ -51,7 +55,7 @@ def get_interface(ifname):
Show interface parameters given an interface name.
For example ge1, ge2 for generic ethernet
'''
return dict(interface=manager.get_interface(ifname))
return dict(interface=manager.router.get_interface(ifname))
@blueprint.route('/interfaces')
@ -60,7 +64,7 @@ def get_interfaces():
'''
Show all interfaces and parameters
'''
return dict(interfaces=manager.get_interfaces())
return dict(interfaces=manager.router.get_interfaces())
@blueprint.route('/config', methods=['GET'])
@ -77,17 +81,75 @@ def put_configuration():
abort(415)
try:
config_candidate = models.Configuration(request.json)
system_config_candidate = models.SystemConfiguration(request.json)
except ValueError, e:
return Response(
'The config failed to deserialize.\n' + str(e),
'The system config failed to deserialize.\n' + str(e),
status=422)
errors = config_candidate.validate()
errors = system_config_candidate.validate()
if errors:
return Response(
'The config failed to validate.\n' + '\n'.join(errors),
status=422)
manager.update_config(config_candidate, _get_cache())
# Config requests to a router appliance will always contain a default ASN,
# so we can key on that for now. Later on we need to move router stuff
# to the extensible list of things the appliance can handle
if request.json.get('asn'):
try:
router_config_candidate = models.RouterConfiguration(request.json)
except ValueError, e:
return Response(
'The router config failed to deserialize.\n' + str(e),
status=422)
errors = router_config_candidate.validate()
if errors:
return Response(
'The config failed to validate.\n' + '\n'.join(errors),
status=422)
else:
router_config_candidate = None
if router_config_candidate:
advanced_service_configs = [router_config_candidate]
else:
advanced_service_configs = []
advanced_services = request.json.get(ADVANCED_SERVICES_KEY, {})
for svc in advanced_services.keys():
if svc not in settings.ENABLED_SERVICES:
return Response(
'This appliance cannot service requested advanced '
'service: %s' % svc, status=400)
for svc in settings.ENABLED_SERVICES:
if not advanced_services.get(svc):
continue
config_model = models.get_config_model(service=svc)
if not config_model:
continue
try:
svc_config_candidate = config_model(advanced_services.get(svc))
except ValueError, e:
return Response(
'The %s config failed to deserialize.\n' + str(e) %
config_model.service_name, status=422)
errors = svc_config_candidate.validate()
if errors:
return Response(
'The %s config failed to validate.\n' + '\n'.join(errors),
config_model.service_name, status=422)
advanced_service_configs.append(svc_config_candidate)
manager.update_config(
system_config=system_config_candidate,
service_configs=advanced_service_configs,
cache=_get_cache())
return dict(configuration=manager.config)

0
akanda/router/drivers/iptables.py Executable file → Normal file
View File

View File

@ -0,0 +1,37 @@
# Copyright (c) 2015 Akanda, 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 akanda.router.drivers.loadbalancer import nginx
# XXX move to config
CONFIGURED_LB_DRIVER = 'nginx'
AVAILABLE_DRIVERS = {
'nginx': nginx.NginxLB,
'nginx+': nginx.NginxPlusLB,
# 'haxproxy': HaProxyLB,
}
class InvalidDriverException(Exception):
pass
def get_loadbalancer_driver(name):
try:
return AVAILABLE_DRIVERS[name]
except KeyError:
raise InvalidDriverException(
'Could not find LB driver by name %s' % name)

View File

@ -0,0 +1,19 @@
{%- for listener in loadbalancer.listeners %}
{%- if listener.default_pool and listener.default_pool.members %}
server {
listen {{ loadbalancer.vip_address }}:{{ listener.protocol_port }};
location / {
proxy_pass {{ listener.protocol.lower() }}://pool_{{ listener.default_pool.id }};
}
}
upstream pool_{{ listener.default_pool.id }} {
{%- for member in listener.default_pool.members: %}
server {{ member.address }}:{{ member.protocol_port }} weight={{ member.weight }};
{%- endfor %}
}
{%- endif %}
{%- endfor %}

View File

@ -0,0 +1,75 @@
# Copyright (c) 2015 Akanda, 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 jinja2
import os
from akanda.router.drivers import base
from akanda.router.utils import execute
class NginxTemplateNotFound(Exception):
# TODO(adam_g): These should return 50x errors and not logged
# exceptions.
pass
class NginxLB(base.Manager):
NAME = 'nginx'
CONFIG_PATH = '/etc/nginx/sites-enabled/'
CONFIG_FILE_TEMPLATE = os.path.join(
os.path.dirname(__file__), 'nginx.conf.template')
INIT = 'nginx'
def __init__(self, root_helper='sudo'):
"""
Initializes DHCPManager class.
:type root_helper: str
:param root_helper: System utility used to gain escalate privileges.
"""
super(NginxLB, self).__init__(root_helper)
self._load_template()
def _load_template(self):
if not os.path.exists(self.CONFIG_FILE_TEMPLATE):
raise NginxTemplateNotFound(
'NGINX Config template not found @ %s' %
self.CONFIG_FILE_TEMPLATE
)
self.config_tmpl = jinja2.Template(
open(self.CONFIG_FILE_TEMPLATE).read())
def _render_config_template(self, path, config):
self._load_template()
with open(path, 'w') as out:
out.write(
self.config_tmpl.render(loadbalancer=config)
)
def restart(self):
execute(['service', self.INIT, 'restart'], self.root_helper)
pass
def update_config(self, config):
path = os.path.join(
self.CONFIG_PATH, 'ak-loadbalancer-%s.conf' % config.id)
self._render_config_template(path=path, config=config)
self.restart()
class NginxPlusLB(NginxLB):
NAME = 'nginxplus'
CONFIG_FILE = '/tmp/nginx_plus.conf'
INIT = 'nginxplus'

View File

@ -19,35 +19,61 @@ import os
import re
from akanda.router import models
from akanda.router import settings
from akanda.router.drivers import (bird, dnsmasq, ip, metadata,
iptables, arp, hostname)
iptables, arp, hostname, loadbalancer)
class Manager(object):
class ServiceManagerBase(object):
def __init__(self, state_path='.'):
self._config = None
self.state_path = os.path.abspath(state_path)
self.ip_mgr = ip.IPManager()
self.ip_mgr.ensure_mapping()
self._config = models.Configuration()
def management_address(self, ensure_configuration=False):
return self.ip_mgr.get_management_address(ensure_configuration)
@property
def config(self):
"""Make config a read-only property.
To update the value, update_config() must called to change the global
state of router.
state of appliance.
"""
return self._config
def update_config(self, config, cache):
self._config = config
pass
class SystemManager(ServiceManagerBase):
def __init__(self, state_path='.'):
super(SystemManager, self).__init__(state_path)
self._config = models.SystemConfiguration()
self.ip_mgr = ip.IPManager()
self.ip_mgr.ensure_mapping()
def update_config(self, config, cache):
self._config = config
self.update_hostname()
self.update_interfaces()
def update_hostname(self):
mgr = hostname.HostnameManager()
mgr.update(self._config)
def update_interfaces(self):
for network in self._config.networks:
self.ip_mgr.disable_duplicate_address_detection(network)
self.ip_mgr.update_interfaces(self._config.interfaces)
class RouterManager(ServiceManagerBase):
def __init__(self, state_path='.'):
super(RouterManager, self).__init__(state_path)
self.ip_mgr = ip.IPManager()
self.ip_mgr.ensure_mapping()
def update_config(self, config, cache):
self._config = config
self.update_interfaces()
self.update_dhcp()
self.update_metadata()
self.update_bgp_and_radv()
@ -55,30 +81,24 @@ class Manager(object):
self.update_routes(cache)
self.update_arp()
# TODO(mark): update_vpn
def update_hostname(self):
mgr = hostname.HostnameManager()
mgr.update(self.config)
def update_interfaces(self):
for network in self.config.networks:
for network in self._config.networks:
self.ip_mgr.disable_duplicate_address_detection(network)
self.ip_mgr.update_interfaces(self.config.interfaces)
self.ip_mgr.update_interfaces(self._config.interfaces)
def update_dhcp(self):
mgr = dnsmasq.DHCPManager()
mgr.delete_all_config()
for network in self.config.networks:
for network in self._config.networks:
real_ifname = self.ip_mgr.generic_to_host(network.interface.ifname)
mgr.update_network_dhcp_config(real_ifname, network)
mgr.restart()
def update_metadata(self):
mgr = metadata.MetadataManager()
should_restart = mgr.networks_have_changed(self.config)
mgr.save_config(self.config)
should_restart = mgr.networks_have_changed(self._config)
mgr.save_config(self._config)
if should_restart:
mgr.restart()
else:
@ -86,26 +106,26 @@ class Manager(object):
def update_bgp_and_radv(self):
mgr = bird.BirdManager()
mgr.save_config(self.config, self.ip_mgr.generic_mapping)
mgr.save_config(self._config, self.ip_mgr.generic_mapping)
mgr.restart()
def update_firewall(self):
mgr = iptables.IPTablesManager()
mgr.save_config(self.config, self.ip_mgr.generic_mapping)
mgr.save_config(self._config, self.ip_mgr.generic_mapping)
mgr.restart()
def update_routes(self, cache):
mgr = ip.IPManager()
mgr.update_default_gateway(self.config)
mgr.update_host_routes(self.config, cache)
mgr.update_default_gateway(self._config)
mgr.update_host_routes(self._config, cache)
def update_arp(self):
mgr = arp.ARPManager()
mgr.send_gratuitous_arp_for_floating_ips(
self.config,
self._config,
self.ip_mgr.generic_to_host
)
mgr.remove_stale_entries(self.config)
mgr.remove_stale_entries(self._config)
def get_interfaces(self):
return self.ip_mgr.get_interfaces()
@ -123,6 +143,133 @@ class Manager(object):
rules.append(re.sub('([\s!])(ge\d+([\s:]|$))', r'\1$\2', virt_data))
return '\n'.join(rules)
def get_config_or_default(self):
# This is a hack to provide compatability with the original API, see
# Manager.config()
if not self._config:
return models.RouterConfiguration()
else:
return self._config
class LoadBalancerManager(ServiceManagerBase):
def __init__(self, state_path='.'):
super(LoadBalancerManager, self).__init__(state_path)
self.lb_manager = loadbalancer.get_loadbalancer_driver(
# xxx pull from cfg
loadbalancer.CONFIGURED_LB_DRIVER)()
def update_config(self, config, cache):
self._config = config
self.lb_manager.update_config(self.config)
SERVICE_MANAGER_MAP = {
'router': RouterManager,
'loadbalancer': LoadBalancerManager,
}
class Manager(object):
def __init__(self, state_path='.'):
self.state_path = os.path.abspath(state_path)
self.ip_mgr = ip.IPManager()
self.ip_mgr.ensure_mapping()
# Holds the common system config
self._system_config = models.SystemConfiguration()
# Holds config models for various services (router, loadbalancer)
self._service_configs = []
self._service_managers = {
'system': SystemManager()
}
self._load_managers()
def _load_managers(self):
for svc in settings.ENABLED_SERVICES:
manager = SERVICE_MANAGER_MAP.get(svc)
if manager:
self._service_managers[svc] = manager()
def get_manager(self, service):
try:
return self._service_managers[service]
except:
raise Exception('No such service manager loaded for appliance '
'service %s' % service)
def management_address(self, ensure_configuration=False):
return self.ip_mgr.get_management_address(ensure_configuration)
@property
def router(self):
"""Returns the router manager.
This is mostly to keep compat with the existing API.
"""
return self.get_manager('router')
@property
def system_config(self):
"""Make config a read-only property.
To update the value, update_config() must called to change the global
state of appliance.
"""
return self._system_config
@property
def service_configs(self):
"""Make config a read-only property.
To update the value, update_config() must called to change the global
state of router.
"""
return self._service_configs
def update_config(self, system_config, service_configs, cache):
self._system_config = system_config
self._service_configs = service_configs
# first update the system config
manager = self.get_manager(self.system_config.service_name)
manager.update_config(self.system_config, cache)
for svc_cfg in self.service_configs:
manager = self.get_manager(svc_cfg.service_name)
manager.update_config(svc_cfg, cache)
@property
def config(self):
out = {}
if 'router' in self._service_managers:
# The original appliance API provides router config
# in the root 'configuration' key. We want to move that
# to the 'services' bucket but provide compat to those who might
# still be expecting it in the root. This seeds the root with the
# default empty values if no router is associated with the
# appliance and allows for
# ['configuration']['services']['router'] to be None at the same
# time.
router_cfg = self.router.get_config_or_default().to_dict()
out = router_cfg
else:
out = {}
out['services'] = {}
for svc in SERVICE_MANAGER_MAP:
try:
manager = self.get_manager(svc)
except:
continue
out['services'][svc] = manager.config
out['system'] = self.system_config
return out
class ManagerProxy(object):
def __init__(self):

View File

@ -365,6 +365,7 @@ class Network(ModelBase):
TYPE_INTERNAL = 'internal'
TYPE_ISOLATED = 'isolated'
TYPE_MANAGEMENT = 'management'
TYPE_LOADBALANCER = 'loadbalancer'
# TODO(mark): add subnet support for Quantum subnet host routes
@ -406,7 +407,8 @@ class Network(ModelBase):
@network_type.setter
def network_type(self, value):
network_types = (self.TYPE_EXTERNAL, self.TYPE_INTERNAL,
self.TYPE_ISOLATED, self.TYPE_MANAGEMENT)
self.TYPE_ISOLATED, self.TYPE_MANAGEMENT,
self.TYPE_LOADBALANCER)
if value not in network_types:
msg = ('network must be one of %s not (%s).' %
('|'.join(network_types), value))
@ -451,6 +453,13 @@ class Network(ModelBase):
@classmethod
def from_dict(cls, d):
missing = []
for k in ['network_id', 'interface']:
if not d.get(k):
missing.append(k)
if missing:
raise ValueError('Missing required data: %s.' % missing)
return cls(
d['network_id'],
interface=Interface.from_dict(d['interface']),
@ -463,15 +472,234 @@ class Network(ModelBase):
subnets=[Subnet.from_dict(s) for s in d.get('subnets', [])])
class Configuration(ModelBase):
class LoadBalancer(ModelBase):
def __init__(self, id_, tenant_id, name, admin_state_up, status,
vip_address, vip_port=None, listeners=()):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.admin_state_up = admin_state_up
self.status = status
self.vip_address = vip_address
self.vip_port = vip_port
self.listeners = listeners
@classmethod
def from_dict(cls, d):
if d.get('listeners'):
d['listeners'] = [
Listener.from_dict(l) for l in d.get('listeners', [])
]
if d.get('vip_port'):
d['vip_port'] = Port.from_dict(d.get('vip_port'))
out = cls(
d['id'],
d['tenant_id'],
d['name'],
d['admin_state_up'],
d['status'],
d['vip_address'],
d['vip_port'],
d['listeners'],
)
return out
class Listener(ModelBase):
def __init__(self, id_, tenant_id, name, admin_state_up, protocol,
protocol_port, default_pool=None):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.admin_state_up = admin_state_up
self.protocol = protocol
self.protocol_port = protocol_port
self.default_pool = default_pool
@classmethod
def from_dict(cls, d):
if d.get('default_pool'):
def_pool = Pool.from_dict(d['default_pool'])
else:
def_pool = None
return cls(
d['id'],
d['tenant_id'],
d['name'],
d['admin_state_up'],
d['protocol'],
d['protocol_port'],
def_pool,
)
def to_dict(self):
fields = ('id', 'tenant_id', 'name', 'admin_state_up', 'protocol',
'protocol_port')
out = dict((f, getattr(self, f)) for f in fields)
if self.default_pool:
out['default_pool'] = self.default_pool.to_dict()
else:
out['default_pool'] = None
return out
class Pool(ModelBase):
def __init__(self, id_, tenant_id, name, admin_state_up, lb_algorithm,
protocol, healthmonitor=None, session_persistence=None,
members=()):
self.id = id_
self.tenant_id = tenant_id
self.name = name
self.admin_state_up = admin_state_up
self.lb_algorithm = lb_algorithm
self.protocol = protocol
self.healthmonitor = healthmonitor
self.session_persistence = session_persistence
self.members = members
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['tenant_id'],
d['name'],
d['admin_state_up'],
d['lb_algorithm'],
d['protocol'],
d.get('healthmonitor'),
d.get('session_persistence'),
[Member.from_dict(m) for m in d.get('members', [])],
)
def to_dict(self):
fields = ('id', 'tenant_id', 'name', 'admin_state_up',
'lb_algorithm', 'protocol', 'healthmonitor',
'session_persistence')
out = dict((f, getattr(self, f)) for f in fields)
out['members'] = [m.to_dict() for m in self.members]
return out
class Member(ModelBase):
def __init__(self, id_, tenant_id, admin_state_up, address, protocol_port,
weight, subnet=None):
self.id = id_
self.tenant_id = tenant_id
self.admin_state_up = admin_state_up
self.address = str(netaddr.IPAddress(address))
self.protocol_port = protocol_port
self.weight = weight
self.subnet = subnet
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['tenant_id'],
d['admin_state_up'],
d['address'],
d['protocol_port'],
d['weight'],
)
def to_dict(self):
fields = ('id', 'tenant_id', 'admin_state_up', 'address',
'protocol_port', 'weight', 'subnet')
return dict((f, getattr(self, f)) for f in fields)
class Port(ModelBase):
def __init__(self, id_, device_id='', fixed_ips=None, mac_address='',
network_id='', device_owner='', name=''):
self.id = id_
self.device_id = device_id
self.fixed_ips = fixed_ips or []
self.mac_address = mac_address
self.network_id = network_id
self.device_owner = device_owner
self.name = name
@classmethod
def from_dict(cls, d):
return cls(
d['id'],
d['device_id'],
fixed_ips=[FixedIp.from_dict(fip) for fip in d['fixed_ips']],
mac_address=d['mac_address'],
network_id=d['network_id'],
device_owner=d['device_owner'],
name=d['name'])
def to_dict(self):
fields = ('id', 'device_id', 'mac_address', 'network_id',
'device_owner', 'name')
out = dict((f, getattr(self, f)) for f in fields)
out['fixed_ips'] = [fip.to_dict() for fip in self.fixed_ips]
return out
class FixedIp(ModelBase):
def __init__(self, subnet_id, ip_address):
self.subnet_id = subnet_id
self.ip_address = netaddr.IPAddress(ip_address)
@classmethod
def from_dict(cls, d):
return cls(d['subnet_id'], d['ip_address'])
def to_dict(self):
fields = ('subnet_id', 'ip_address')
return dict((f, getattr(self, f)) for f in fields)
class SystemConfiguration(ModelBase):
service_name = 'system'
def __init__(self, conf_dict={}):
self.tenant_id = conf_dict.get('tenant_id')
self.hostname = conf_dict.get('hostname')
self.networks = [
Network.from_dict(n) for n in conf_dict.get('networks', [])]
def validate(self):
# TODO: Improve this interface, it currently sucks.
errors = []
for attr in ['tenant_id', 'hostname']:
if not getattr(self, attr):
errors.append((attr, 'Config does not contain a %s' % attr))
return errors
@property
def management_address(self):
addrs = []
for net in self.networks:
if net.is_management_network:
addrs.extend((net.interface.first_v4, net.interface.first_v6))
addrs = sorted(a for a in addrs if a)
if addrs:
return addrs[0]
@property
def interfaces(self):
return [n.interface for n in self.networks if n.interface]
def to_dict(self):
fields = ('tenant_id', 'hostname', 'management_address', 'interfaces')
return dict((f, getattr(self, f)) for f in fields)
class RouterConfiguration(SystemConfiguration):
service_name = 'router'
def __init__(self, conf_dict={}):
super(RouterConfiguration, self).__init__(conf_dict)
gw = conf_dict.get('default_v4_gateway')
self.default_v4_gateway = netaddr.IPAddress(gw) if gw else None
self.asn = conf_dict.get('asn', DEFAULT_AS)
self.neighbor_asn = conf_dict.get('neighbor_asn', self.asn)
self.networks = [
Network.from_dict(n) for n in conf_dict.get('networks', [])]
self.static_routes = [StaticRoute(*r) for r in
conf_dict.get('static_routes', [])]
@ -491,17 +719,13 @@ class Configuration(ModelBase):
FloatingIP.from_dict(fip)
for fip in conf_dict.get('floating_ips', [])
]
self.tenant_id = conf_dict.get('tenant_id')
self.hostname = conf_dict.get('hostname')
self._attach_floating_ips(self.floating_ips)
def validate(self):
"""Validate anchor rules to ensure that ifaces and tables exist."""
errors = []
interfaces = set(n.interface.ifname for n in self.networks)
errors = []
for anchor in self.anchors:
for rule in anchor.rules:
for iface in (rule.interface, rule.destination_interface):
@ -561,18 +785,49 @@ class Configuration(ModelBase):
if addrs:
return addrs[0]
@property
def interfaces(self):
return [n.interface for n in self.networks if n.interface]
@property
def management_address(self):
addrs = []
for net in self.networks:
if net.is_management_network:
addrs.extend((net.interface.first_v4, net.interface.first_v6))
class LoadBalancerConfiguration(SystemConfiguration):
service_name = 'loadbalancer'
addrs = sorted(a for a in addrs if a)
def __init__(self, conf_dict={}):
super(LoadBalancerConfiguration, self).__init__(conf_dict)
self.id = conf_dict.get('id')
self.name = conf_dict.get('name')
if conf_dict:
self._loadbalancer = LoadBalancer.from_dict(conf_dict)
self.vip_port = self._loadbalancer.vip_port
self.vip_address = self._loadbalancer.vip_address
self.listeners = self._loadbalancer.listeners
else:
self.vip_port = None
self.vip_address = None
self.listeners = []
if addrs:
return addrs[0]
def validate(self):
super(LoadBalancerConfiguration, self).validate()
errors = []
if not self.id:
errors.append(['id', 'Missing in config id'])
return errors
def to_dict(self):
if self.vip_port:
vip_port = self.vip_port.to_dict()
else:
vip_port = {}
return {
'id': self.id,
'name': self.name,
'vip_port': vip_port,
'vip_address': self.vip_address,
'listeners': [l.to_dict() for l in self.listeners],
}
SERVICE_MAP = {
RouterConfiguration.service_name: RouterConfiguration,
LoadBalancerConfiguration.service_name: LoadBalancerConfiguration,
}
def get_config_model(service):
return SERVICE_MAP[service]

13
akanda/router/settings.py Normal file
View File

@ -0,0 +1,13 @@
# Configures which advanced service drivers are loaded by this
# instance of the appliance.
ENABLED_SERVICES = ['router']
# If akanda_local_settings.py is located in your python path,
# it can be used to override the defaults. DIB will install this
# into /usr/local/share/akanda and append that path to the gunicorn's
# python path.
try:
from akanda_local_settings import * # noqa
except ImportError:
pass

View File

@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
import json
import os
@ -27,6 +26,9 @@ import netaddr
from akanda.router import models
DEFAULT_ENABLED_SERVICES = ['router']
VALID_SERVICES = ['router', 'loadbalancer']
def execute(args, root_helper=None):
if root_helper:

View File

@ -12,7 +12,7 @@
do_cleanup: True
router_appliance: True
update_kernel: True
enabled_advanced_services: "router"
tasks:
- include: tasks/debian_backports.yml
when: ansible_distribution == "Debian" and ansible_distribution_release == "wheezy"

View File

@ -34,6 +34,15 @@
- metadata
- akanda-router-api-server
- name: create /usr/local/share/akanda/
file: path=/usr/local/share/akanda state=directory
- name: make /usr/local/share/akanda/ importable
copy: dest=/usr/local/share/akanda/__init__.py content=''
- name: install akanda_local_settings.py
copy: dest=/usr/local/share/akanda/akanda_local_settings.py content='ENABLED_SERVICES = {{enabled_advanced_services.split(',')}}\n'
- name: update-rc
command: update-rc.d akanda-router-api-server start

View File

@ -1,3 +1,9 @@
This is the base element for building an Akanda appliance image.
Ansible is required on the local system.
Advanced service drivers may be enabled in the appliance by setting
``DIB_AKANDA_ADVANCED_SERVICES``. This defaults to enabling only the
router driver, but you may enabled other avialable drivers ie:
DIB_AKANDA_ADVANCED_SERVICES=router,loadbalancer

View File

@ -2,8 +2,10 @@
set -eux
set -o pipefail
DIB_AKANDA_ADVANCED_SERVICES=${DIB_AKANDA_ADVANCED_SERVICES:-"router"}
APP_SRC_DIR="/tmp/akanda-appliance"
[ -d "${APP_SRC_DIR}" ] || exit 0
ansible-playbook -i "localhost," -c local $APP_SRC_DIR/ansible/main.yml
ansible-playbook -i "localhost," -c local -e enabled_advanced_services="$DIB_AKANDA_ADVANCED_SERVICES" $APP_SRC_DIR/ansible/main.yml

View File

@ -0,0 +1,16 @@
#!/bin/bash -xe
# Copyright (c) 2015 Akanda, 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.
rm -rf /etc/nginx/sites-enabled/default

View File

@ -13,7 +13,7 @@
PATH=/bin:/usr/bin:/sbin:/usr/sbin
DAEMON="/usr/local/bin/gunicorn"
NAME="akanda-router-api-server"
OPTIONS="-c /etc/akanda_gunicorn_config akanda.router.api.server:app"
OPTIONS="--pythonpath /usr/local/share/akanda -c /etc/akanda_gunicorn_config akanda.router.api.server:app"
PIDFILE=/var/run/gunicorn.pid
test -x $DAEMON || exit 0

View File

@ -42,8 +42,8 @@ all_files = 1
build-dir = doc/build
source-dir = doc/source
[nosetests]
where = test
verbosity = 2
detailed-errors = 1
cover-package = akanda
#[nosetests]
#where = test
#verbosity = 2
#detailed-errors = 1
#cover-package = akanda

View File

@ -26,9 +26,15 @@ import flask
import json
import mock
from akanda.router import manager
from akanda.router.api import v1
SYSTEM_CONFIG = {
'tenant_id': 'foo_tenant_id',
'hostname': 'foohostname',
}
class SystemAPITestCase(unittest.TestCase):
"""
This test case contains the unit tests for the Python server implementation
@ -54,7 +60,7 @@ class SystemAPITestCase(unittest.TestCase):
'unsupported platform'
)
def test_get_interface(self):
with mock.patch.object(v1.system.manager, 'get_interface') as get_if:
with mock.patch.object(v1.system.manager.router, 'get_interface') as get_if:
get_if.return_value = 'ge1'
result = self.test_app.get('/v1/system/interface/ge1')
get_if.assert_called_once_with('ge1')
@ -68,7 +74,7 @@ class SystemAPITestCase(unittest.TestCase):
'unsupported platform'
)
def test_get_interfaces(self):
with mock.patch.object(v1.system.manager, 'get_interfaces') as get_ifs:
with mock.patch.object(v1.system.manager.router, 'get_interfaces') as get_ifs:
get_ifs.return_value = ['ge0', 'ge1']
result = self.test_app.get('/v1/system/interfaces')
get_ifs.assert_called_once_with()
@ -81,14 +87,29 @@ class SystemAPITestCase(unittest.TestCase):
not distutils.spawn.find_executable('ip'),
'unsupported platform'
)
def test_get_configuration(self):
@mock.patch.object(manager, 'settings')
@mock.patch.object(v1.system, 'settings')
def test_get_configuration(self, fake_api_settings, fake_mgr_settings):
fake_api_settings.ENABLED_SERVICES = ['router', 'loadbalancer']
fake_mgr_settings.ENABLED_SERVICES = ['router', 'loadbalancer']
result = self.test_app.get('/v1/system/config')
expected = {
'configuration': {
'address_book': {},
'anchors': [],
'networks': [],
'services': {
'loadbalancer': None,
'router': None
},
'static_routes': [],
'anchors': []
'system': {
'hostname': None,
'interfaces': [],
'management_address': None,
'tenant_id': None
}
}
}
self.assertEqual(json.loads(result.data), expected)
@ -102,7 +123,7 @@ class SystemAPITestCase(unittest.TestCase):
self.assertEqual(result.status_code, 415)
def test_put_configuration_returns_422_for_ValueError(self):
with mock.patch('akanda.router.models.Configuration') as Config:
with mock.patch('akanda.router.models.RouterConfiguration') as Config:
Config.side_effect = ValueError
result = self.test_app.put(
'/v1/system/config',
@ -112,11 +133,11 @@ class SystemAPITestCase(unittest.TestCase):
self.assertEqual(result.status_code, 422)
def test_put_configuration_returns_422_for_errors(self):
with mock.patch('akanda.router.models.Configuration') as Config:
with mock.patch('akanda.router.models.SystemConfiguration') as Config:
Config.return_value.validate.return_value = ['error1']
result = self.test_app.put(
'/v1/system/config',
data=json.dumps({'networks': [{}]}), # malformed dict
data=json.dumps(SYSTEM_CONFIG),
content_type='application/json'
)
self.assertEqual(result.status_code, 422)
@ -129,13 +150,149 @@ class SystemAPITestCase(unittest.TestCase):
not distutils.spawn.find_executable('ip'),
'unsupported platform'
)
def test_put_configuration_returns_200(self):
with mock.patch.object(v1.system.manager, 'update_config') as update:
result = self.test_app.put(
'/v1/system/config',
data=json.dumps({}),
content_type='application/json'
)
self.assertEqual(result.status_code, 200)
self.assertTrue(json.loads(result.data))
@mock.patch('akanda.router.api.v1.system._get_cache')
@mock.patch('akanda.router.models.SystemConfiguration')
@mock.patch.object(v1.system.manager, 'update_config')
def test_put_configuration_returns_200(self, mock_update,
fake_system_config, fake_cache):
fake_cache.return_value = 'fake_cache'
sys_config_obj = mock.Mock()
sys_config_obj.validate = mock.Mock()
sys_config_obj.validate.return_value = []
fake_system_config.return_value = sys_config_obj
result = self.test_app.put(
'/v1/system/config',
data=json.dumps({
'tenant_id': 'foo_tenant_id',
'hostname': 'foo_hostname',
}),
content_type='application/json'
)
self.assertEqual(result.status_code, 200)
self.assertTrue(json.loads(result.data))
mock_update.assert_called_with(
cache='fake_cache', service_configs=[], system_config=sys_config_obj)
@mock.patch('akanda.router.manager.Manager.config',
new_callable=mock.PropertyMock, return_value={})
@mock.patch('akanda.router.api.v1.system._get_cache')
@mock.patch('akanda.router.models.RouterConfiguration')
@mock.patch('akanda.router.models.SystemConfiguration')
@mock.patch.object(v1.system.manager, 'update_config')
def test_put_configuration_with_router(self, mock_update,
fake_system_config, fake_router_config, fake_cache, fake_config):
fake_config.return_value = 'foo'
fake_cache.return_value = 'fake_cache'
sys_config_obj = mock.Mock()
sys_config_obj.validate = mock.Mock()
sys_config_obj.validate.return_value = []
fake_system_config.return_value = sys_config_obj
router_config_obj = mock.Mock()
router_config_obj.validate = mock.Mock()
router_config_obj.validate.return_value = []
fake_router_config.return_value = router_config_obj
result = self.test_app.put(
'/v1/system/config',
data=json.dumps({
'tenant_id': 'foo_tenant_id',
'hostname': 'foo_hostname',
'asn': 'foo_asn',
}),
content_type='application/json'
)
self.assertEqual(result.status_code, 200)
self.assertTrue(json.loads(result.data))
mock_update.assert_called_with(
cache='fake_cache', service_configs=[router_config_obj],
system_config=sys_config_obj)
@mock.patch('akanda.router.models.get_config_model')
@mock.patch.object(manager, 'settings')
@mock.patch.object(v1.system, 'settings')
@mock.patch('akanda.router.manager.Manager.config',
new_callable=mock.PropertyMock, return_value={})
@mock.patch('akanda.router.api.v1.system._get_cache')
@mock.patch('akanda.router.models.LoadBalancerConfiguration')
@mock.patch('akanda.router.models.SystemConfiguration')
@mock.patch.object(v1.system.manager, 'update_config')
def test_put_configuration_with_adv_services(self, mock_update,
fake_system_config, fake_lb_config, fake_cache, fake_config,
fake_api_settings, fake_mgr_settings, fake_get_config_model):
fake_api_settings.ENABLED_SERVICES = ['loadbalancer']
fake_mgr_settings.ENABLED_SERVICES = ['loadbalancer']
fake_config.return_value = 'foo'
fake_cache.return_value = 'fake_cache'
sys_config_obj = mock.Mock()
sys_config_obj.validate = mock.Mock()
sys_config_obj.validate.return_value = []
fake_system_config.return_value = sys_config_obj
lb_config_obj = mock.Mock()
lb_config_obj.validate = mock.Mock()
lb_config_obj.validate.return_value = []
fake_lb_config.return_value = lb_config_obj
fake_get_config_model.return_value = fake_lb_config
result = self.test_app.put(
'/v1/system/config',
data=json.dumps({
'tenant_id': 'foo_tenant_id',
'hostname': 'foo_hostname',
'services': {
'loadbalancer': {'id': 'foo'}
}
}),
content_type='application/json'
)
self.assertEqual(result.status_code, 200)
self.assertTrue(json.loads(result.data))
mock_update.assert_called_with(
cache='fake_cache', service_configs=[lb_config_obj],
system_config=sys_config_obj)
@mock.patch('akanda.router.models.get_config_model')
@mock.patch.object(manager, 'settings')
@mock.patch.object(v1.system, 'settings')
@mock.patch('akanda.router.manager.Manager.config',
new_callable=mock.PropertyMock, return_value={})
@mock.patch('akanda.router.api.v1.system._get_cache')
@mock.patch('akanda.router.models.LoadBalancerConfiguration')
@mock.patch('akanda.router.models.SystemConfiguration')
@mock.patch.object(v1.system.manager, 'update_config')
def test_put_configuration_with_disabled_svc_returns_400(self, mock_update,
fake_system_config, fake_lb_config, fake_cache, fake_config,
fake_api_settings, fake_mgr_settings, fake_get_config_model):
fake_api_settings.ENABLED_SERVICES = ['foo']
fake_mgr_settings.ENABLED_SERVICES = ['foo']
fake_config.return_value = 'foo'
fake_cache.return_value = 'fake_cache'
sys_config_obj = mock.Mock()
sys_config_obj.validate = mock.Mock()
sys_config_obj.validate.return_value = []
fake_system_config.return_value = sys_config_obj
lb_config_obj = mock.Mock()
lb_config_obj.validate = mock.Mock()
lb_config_obj.validate.return_value = []
fake_lb_config.return_value = lb_config_obj
fake_get_config_model.return_value = fake_lb_config
result = self.test_app.put(
'/v1/system/config',
data=json.dumps({
'tenant_id': 'foo_tenant_id',
'hostname': 'foo_hostname',
'services': {
'loadbalancer': {'id': 'foo'}
}
}),
content_type='application/json'
)
self.assertEqual(result.status_code, 400)

View File

@ -94,7 +94,7 @@ class ARPTest(unittest2.TestCase):
])
def test_send_gratuitous_arp_for_config(self):
config = models.Configuration({
config = models.RouterConfiguration({
'networks': [{
'network_id': 'ABC456',
'interface': {

View File

@ -7,7 +7,7 @@ import netaddr
from akanda.router import models
from akanda.router.drivers import iptables
CONFIG = models.Configuration({
CONFIG = models.RouterConfiguration({
'networks': [{
'network_id': 'ABC123',
'interface': {
@ -127,16 +127,16 @@ V6_OUTPUT = [
]
class TestIPTablesConfiguration(TestCase):
class TestIPTablesRouterConfiguration(TestCase):
def setUp(self):
super(TestIPTablesConfiguration, self).setUp()
super(TestIPTablesRouterConfiguration, self).setUp()
self.execute = mock.patch('akanda.router.utils.execute').start()
self.replace = mock.patch('akanda.router.utils.replace_file').start()
self.patches = [self.execute, self.replace]
def tearDown(self):
super(TestIPTablesConfiguration, self).tearDown()
super(TestIPTablesRouterConfiguration, self).tearDown()
for p in self.patches:
p.stop()

View File

@ -161,7 +161,7 @@ class RouteTest(unittest2.TestCase):
)
def test_update_default_no_inputs(self):
c = models.Configuration({})
c = models.RouterConfiguration({})
with mock.patch.object(self.mgr, '_set_default_gateway') as set:
set.side_effect = AssertionError(
'should not try to set default gw'
@ -169,7 +169,7 @@ class RouteTest(unittest2.TestCase):
self.mgr.update_default_gateway(c)
def test_update_default_v4_from_gateway(self):
c = models.Configuration({'default_v4_gateway': '172.16.77.1'})
c = models.RouterConfiguration({'default_v4_gateway': '172.16.77.1'})
with mock.patch.object(self.mgr, '_set_default_gateway') as set:
self.mgr.update_default_gateway(c)
set.assert_called_once_with(c.default_v4_gateway, None)
@ -189,7 +189,7 @@ class RouteTest(unittest2.TestCase):
subnets=[subnet],
network_type='external',
)
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
with mock.patch.object(self.mgr, '_set_default_gateway') as set:
self.mgr.update_default_gateway(c)
net = c.networks[0]
@ -217,7 +217,7 @@ class RouteTest(unittest2.TestCase):
subnets=[subnet, subnet2],
network_type='external',
)
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
with mock.patch.object(self.mgr, '_set_default_gateway') as set:
self.mgr.update_default_gateway(c)
net = c.networks[0]
@ -239,7 +239,7 @@ class RouteTest(unittest2.TestCase):
subnets=[subnet],
network_type='external',
)
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
with mock.patch.object(self.mgr, '_set_default_gateway') as set:
self.mgr.update_default_gateway(c)
net = c.networks[0]
@ -267,7 +267,7 @@ class RouteTest(unittest2.TestCase):
subnets=[subnet, subnet2],
network_type='external',
)
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
with mock.patch.object(self.mgr, '_set_default_gateway') as set:
self.mgr.update_default_gateway(c)
net = c.networks[0]
@ -292,7 +292,7 @@ class RouteTest(unittest2.TestCase):
interface=dict(ifname='ge0', addresses=['fe80::2']),
subnets=[subnet]
)
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
cache = make_region().configure('dogpile.cache.memory')
with mock.patch.object(self.mgr, 'sudo') as sudo:
@ -319,7 +319,7 @@ class RouteTest(unittest2.TestCase):
# Empty the host_routes list
sudo.reset_mock()
subnet['host_routes'] = []
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
self.mgr.update_host_routes(c, cache)
sudo.assert_called_once_with(
'-4', 'route', 'del', '192.240.128.0/20', 'via',
@ -336,7 +336,7 @@ class RouteTest(unittest2.TestCase):
'destination': '192.220.128.0/20',
'nexthop': '192.168.89.3'
}]
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
self.mgr.update_host_routes(c, cache)
self.assertEqual(sudo.call_args_list, [
mock.call('-4', 'route', 'add', '192.240.128.0/20',
@ -354,7 +354,7 @@ class RouteTest(unittest2.TestCase):
'destination': '192.185.128.0/20',
'nexthop': '192.168.89.4'
}]
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
self.mgr.update_host_routes(c, cache)
self.assertEqual(sudo.call_args_list, [
mock.call('-4', 'route', 'del', '192.220.128.0/20',
@ -376,7 +376,7 @@ class RouteTest(unittest2.TestCase):
'nexthop': '192.168.90.1'
}]
))
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
self.mgr.update_host_routes(c, cache)
self.assertEqual(sudo.call_args_list, [
mock.call('-4', 'route', 'add', '192.240.128.0/20',
@ -388,7 +388,7 @@ class RouteTest(unittest2.TestCase):
sudo.reset_mock()
network['subnets'][0]['host_routes'] = []
network['subnets'][1]['host_routes'] = []
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
self.mgr.update_host_routes(c, cache)
self.assertEqual(sudo.call_args_list, [
mock.call('-4', 'route', 'del', '192.185.128.0/20',
@ -416,7 +416,7 @@ class RouteTest(unittest2.TestCase):
interface=dict(ifname='ge0', addresses=['fe80::2']),
subnets=[subnet]
)
c = models.Configuration({'networks': [network]})
c = models.RouterConfiguration({'networks': [network]})
cache = make_region().configure('dogpile.cache.memory')
with mock.patch.object(self.mgr, 'sudo') as sudo:

138
test/unit/fakes.py Normal file
View File

@ -0,0 +1,138 @@
from copy import copy
FAKE_SYSTEM_DICT = {
"tenant_id": "d22b149cee9b4eac8349c517eda00b89",
"hostname": "ak-loadbalancer-d22b149cee9b4eac8349c517eda00b89",
"networks": [
{
"v4_conf_service": "static",
"network_type": "loadbalancer",
"v6_conf_service": "static",
"network_id": "b7fc9b39-401c-47cc-a07d-9f8cde75ccbf",
"allocations": [],
"subnets": [
{
"host_routes": [],
"cidr": "192.168.0.0/24",
"gateway_ip": "192.168.0.1",
"dns_nameservers": [],
"dhcp_enabled": True
},
{
"host_routes": [],
"cidr": "fdd6:a1fa:cfa8:6af6::/64",
"gateway_ip": "fdd6:a1fa:cfa8:6af6::1",
"dns_nameservers": [],
"dhcp_enabled": False
}],
"interface": {
"ifname": "ge1",
"addresses": [
"192.168.0.137/24", "fdd6:a1fa:cfa8:6af6:f816:3eff:fea0:8082/64"
]
},
},
{
"v4_conf_service": "static",
"network_type": "management",
"v6_conf_service": "static",
"network_id": "43dc2fad-f6f9-4668-9695-fed50f7768aa",
"allocations": [],
"subnets": [
{
"host_routes": [],
"cidr": "fdca:3ba5:a17a:acda::/64",
"gateway_ip": "fdca:3ba5:a17a:acda::1",
"dns_nameservers": [],
"dhcp_enabled": True}
],
"interface": {
"ifname": "ge0",
"addresses": ["fdca:3ba5:a17a:acda:f816:3eff:fee0:e1b0/64"]
},
}]
}
FAKE_LOADBALANCER_DICT = {
"id": "8ac54799-b143-48e5-94d4-e5e989592229",
"status": "ACTIVE",
"name": "balancer1",
"admin_state_up": True,
"tenant_id": "d22b149cee9b4eac8349c517eda00b89",
"vip_port": {
"name": "loadbalancer-8ac54799-b143-48e5-94d4-e5e989592229",
"network_id": "b7fc9b39-401c-47cc-a07d-9f8cde75ccbf",
"device_owner": "neutron:LOADBALANCERV2",
"mac_address": "fa:16:3e:a0:80:82",
"fixed_ips": [
{
"subnet_id": "8c58b558-be54-45de-9873-169fe845bb80",
"ip_address": "192.168.0.137"
},
{
"subnet_id": "89fe7a9d-be92-469c-9a1e-503a39462ed1",
"ip_address": "fdd6:a1fa:cfa8:6af6:f816:3eff:fea0:8082"}
],
"id": "352e2867-06c6-4ced-8e81-1c016991fb38",
"device_id": "8ac54799-b143-48e5-94d4-e5e989592229"},
"vip_address": "192.168.0.137",
"id": "8ac54799-b143-48e5-94d4-e5e989592229",
"listeners": [],
}
FAKE_LISTENER_DICT = {
'admin_state_up': True,
'default_pool': None,
'id': '8dca64a2-beaa-484e-a3c8-59c9b63913e0',
'name': 'listener1',
'protocol': 'HTTP',
'protocol_port': 80,
'tenant_id': 'd22b149cee9b4eac8349c517eda00b89'
}
FAKE_POOL_DICT = {
'admin_state_up': True,
'healthmonitor': None,
'id': u'255c4d63-6199-4afc-abec-48c5ab46ac2e',
'lb_algorithm': u'ROUND_ROBIN',
'members': [],
'name': u'pool1',
'protocol': u'HTTP',
'session_persistence': None,
'tenant_id': u'd22b149cee9b4eac8349c517eda00b89'
}
FAKE_MEMBER_DICT = {
'address': u'192.168.0.194',
'admin_state_up': True,
'id': u'30fc9549-7804-4196-bb86-8ebabc3a79e2',
'protocol_port': 80,
'subnet': None,
'tenant_id': u'd22b149cee9b4eac8349c517eda00b89',
'weight': 1
}
def fake_loadbalancer_dict(listener=False, pool=False, members=False):
lb_dict = copy(FAKE_LOADBALANCER_DICT)
if listener:
lb_dict['listeners'] = [copy(FAKE_LISTENER_DICT)]
if pool:
if not listener:
raise Exception("Cannot create pool without a listener")
lb_dict['listeners'][0]['default_pool'] = \
copy(FAKE_POOL_DICT)
if members:
if not pool:
raise Exception("Cannot create member without a pool")
lb_dict['listeners'][0]['default_pool']['members'] = \
[copy(FAKE_MEMBER_DICT)]
return lb_dict

View File

@ -17,11 +17,14 @@
import textwrap
import copy
import mock
import netaddr
from unittest2 import TestCase
from akanda.router import models
from test.unit import fakes
class InterfaceModelTestCase(TestCase):
@ -360,7 +363,7 @@ class NetworkTestCase(TestCase):
n = models.Network('id', None, v6_conf_service='invalid')
class ConfigurationTestCase(TestCase):
class RouterConfigurationTestCase(TestCase):
def test_init_only_networks(self):
subnet = dict(
cidr='192.168.1.0/24',
@ -375,28 +378,28 @@ class ConfigurationTestCase(TestCase):
allocations=[],
subnets=[subnet])
c = models.Configuration(dict(networks=[network]))
c = models.RouterConfiguration(dict(networks=[network]))
self.assertEqual(len(c.networks), 1)
self.assertEqual(c.networks[0],
models.Network.from_dict(network))
def test_init_tenant_id(self):
c = models.Configuration({'tenant_id': 'abc123'})
c = models.RouterConfiguration({'tenant_id': 'abc123'})
self.assertEqual(c.tenant_id, 'abc123')
def test_no_default_v4_gateway(self):
c = models.Configuration({})
c = models.RouterConfiguration({})
self.assertIsNone(c.default_v4_gateway)
def test_valid_default_v4_gateway(self):
c = models.Configuration({'default_v4_gateway': '172.16.77.1'})
c = models.RouterConfiguration({'default_v4_gateway': '172.16.77.1'})
self.assertEqual(c.default_v4_gateway.version, 4)
self.assertEqual(str(c.default_v4_gateway), '172.16.77.1')
def test_init_only_static_routes(self):
routes = [('0.0.0.0/0', '192.168.1.1'),
('172.16.77.0/16', '192.168.1.254')]
c = models.Configuration(dict(networks=[], static_routes=routes))
c = models.RouterConfiguration(dict(networks=[], static_routes=routes))
self.assertEqual(len(c.static_routes), 2)
self.assertEqual(
@ -406,7 +409,7 @@ class ConfigurationTestCase(TestCase):
def test_init_address_book(self):
ab = {"webservers": ["192.168.57.101/32", "192.168.57.230/32"]}
c = models.Configuration(dict(networks=[], address_book=ab))
c = models.RouterConfiguration(dict(networks=[], address_book=ab))
self.assertEqual(
c.address_book.get('webservers'),
models.AddressBookEntry('webservers', ab['webservers']))
@ -414,7 +417,7 @@ class ConfigurationTestCase(TestCase):
def test_init_label(self):
labels = {"external": ["192.168.57.0/24"]}
c = models.Configuration(dict(networks=[], labels=labels))
c = models.RouterConfiguration(dict(networks=[], labels=labels))
self.assertEqual(
c.labels[0],
models.Label('external', ['192.168.57.0/24']))
@ -424,30 +427,30 @@ class ConfigurationTestCase(TestCase):
name='theanchor',
rules=[])
c = models.Configuration(dict(networks=[], anchors=[anchor_dict]))
c = models.RouterConfiguration(dict(networks=[], anchors=[anchor_dict]))
self.assertEqual(len(c.anchors), 1)
def test_init_anchor(self):
test_rule = dict(action='block', source='192.168.1.1/32')
anchor_dict = dict(name='theanchor', rules=[test_rule])
c = models.Configuration(dict(networks=[], anchors=[anchor_dict]))
c = models.RouterConfiguration(dict(networks=[], anchors=[anchor_dict]))
self.assertEqual(len(c.anchors), 1)
self.assertEqual(len(c.anchors[0].rules), 1)
self.assertEqual(c.anchors[0].rules[0].action, 'block')
def test_asn_default(self):
c = models.Configuration({'networks': []})
c = models.RouterConfiguration({'networks': []})
self.assertEqual(c.asn, 64512)
self.assertEqual(c.neighbor_asn, 64512)
def test_asn_provided_with_neighbor_fallback(self):
c = models.Configuration({'networks': [], 'asn': 12345})
c = models.RouterConfiguration({'networks': [], 'asn': 12345})
self.assertEqual(c.asn, 12345)
self.assertEqual(c.neighbor_asn, 12345)
def test_asn_provided_with_neighbor_different(self):
c = models.Configuration(
c = models.RouterConfiguration(
{'networks': [], 'asn': 12, 'neighbor_asn': 34}
)
self.assertEqual(c.asn, 12)
@ -463,7 +466,7 @@ class ConfigurationTestCase(TestCase):
ab = {"webservers": ["192.168.57.101/32", "192.168.57.230/32"]}
anchor_dict = dict(name='theanchor', rules=[rule_dict])
c = models.Configuration(
c = models.RouterConfiguration(
dict(networks=[network], anchors=[anchor_dict], address_book=ab))
errors = c.validate()
@ -517,10 +520,163 @@ class ConfigurationTestCase(TestCase):
self.assertEqual(len(errors), 1)
def test_to_dict(self):
c = models.Configuration({'networks': []})
c = models.RouterConfiguration({'networks': []})
expected = dict(networks=[],
address_book={},
static_routes=[],
anchors=[])
self.assertEqual(c.to_dict(), expected)
class LBListenerTest(TestCase):
def test_from_dict(self):
ldict = copy.copy(fakes.FAKE_LISTENER_DICT)
listener = models.Listener.from_dict(ldict)
for k in ldict.keys():
self.assertEqual(getattr(listener, k), ldict[k])
def test_from_dict_with_pool(self):
ldict = copy.copy(fakes.FAKE_LISTENER_DICT)
pdict = copy.copy(fakes.FAKE_POOL_DICT)
ldict['default_pool'] = pdict
listener = models.Listener.from_dict(ldict)
keys = ldict.keys()
keys.remove('default_pool')
for k in keys:
self.assertEqual(getattr(listener, k), ldict[k])
self.assertTrue(isinstance(listener.default_pool, models.Pool))
def test_to_dict(self):
ldict = copy.copy(fakes.FAKE_LISTENER_DICT)
listener = models.Listener.from_dict(ldict)
l_to_dict = listener.to_dict()
for k in ldict.keys():
self.assertEqual(l_to_dict[k], ldict[k])
def test_to_dict_with_pool(self):
ldict = copy.copy(fakes.FAKE_LISTENER_DICT)
pdict = copy.copy(fakes.FAKE_POOL_DICT)
ldict['default_pool'] = pdict
listener = models.Listener.from_dict(ldict).to_dict()
self.assertEqual(listener['default_pool']['id'], pdict['id'])
class LBPoolTest(TestCase):
def test_from_dict(self):
pdict = copy.copy(fakes.FAKE_POOL_DICT)
pool = models.Pool.from_dict(pdict)
for k in pdict.keys():
self.assertEqual(getattr(pool, k), pdict[k])
def test_from_dict_with_member(self):
pdict = copy.copy(fakes.FAKE_POOL_DICT)
mdict = copy.copy(fakes.FAKE_MEMBER_DICT)
pdict['members'] = [mdict]
pool = models.Pool.from_dict(pdict)
keys = pdict.keys()
keys.remove('members')
for k in keys:
self.assertEqual(getattr(pool, k), pdict[k])
self.assertTrue(isinstance(pool.members[0], models.Member))
def test_to_dict(self):
pdict = copy.copy(fakes.FAKE_POOL_DICT)
pool = models.Pool.from_dict(pdict)
p_to_dict = pool.to_dict()
for k in pdict.keys():
self.assertEqual(p_to_dict[k], pdict[k])
def test_to_dict_with_member(self):
pdict = copy.copy(fakes.FAKE_POOL_DICT)
mdict = copy.copy(fakes.FAKE_MEMBER_DICT)
pdict['members'] = [mdict]
pool = models.Pool.from_dict(pdict)
pool_to_dict = pool.to_dict()
self.assertEqual(pool_to_dict['members'][0]['id'], mdict['id'])
class LBMemberTest(TestCase):
def test_from_dict(self):
mdict = copy.copy(fakes.FAKE_MEMBER_DICT)
member = models.Member.from_dict(mdict)
for k in mdict.keys():
self.assertEqual(getattr(member, k), mdict[k])
def test_to_dict(self):
mdict = copy.copy(fakes.FAKE_MEMBER_DICT)
member = models.Member.from_dict(mdict)
m_to_dict = member.to_dict()
for k in mdict.keys():
self.assertEqual(m_to_dict[k], mdict[k])
class LoadBalancerTest(TestCase):
def test_from_dict_lb(self):
lb_dict = fakes.fake_loadbalancer_dict()
lb = models.LoadBalancer.from_dict(lb_dict)
for k in lb_dict.keys():
self.assertEqual(getattr(lb, k), lb_dict[k])
def test_from_dict_lb_listener(self):
lb_dict = fakes.fake_loadbalancer_dict(listener=True)
expected_listener_id = lb_dict['listeners'][0]['id']
lb = models.LoadBalancer.from_dict(lb_dict)
for k in lb_dict.keys():
self.assertEqual(getattr(lb, k), lb_dict[k])
self.assertTrue(isinstance(lb.listeners[0], models.Listener))
self.assertEqual(lb.listeners[0].id, expected_listener_id)
def test_from_dict_lb_listener_pool(self):
lb_dict = fakes.fake_loadbalancer_dict(listener=True, pool=True)
expected_listener_id = lb_dict['listeners'][0]['id']
expected_pool_id = lb_dict['listeners'][0]['default_pool']['id']
lb = models.LoadBalancer.from_dict(lb_dict)
for k in lb_dict.keys():
self.assertEqual(getattr(lb, k), lb_dict[k])
self.assertTrue(isinstance(lb.listeners[0], models.Listener))
self.assertTrue(isinstance(lb.listeners[0].default_pool,
models.Pool))
self.assertEqual(lb.listeners[0].id, expected_listener_id)
self.assertEqual(lb.listeners[0].default_pool.id, expected_pool_id)
def test_from_dict_lb_listener_pool_members(self):
lb_dict = fakes.fake_loadbalancer_dict(listener=True, pool=True,
members=True)
expected_listener_id = lb_dict['listeners'][0]['id']
expected_pool_id = lb_dict['listeners'][0]['default_pool']['id']
expected_member = lb_dict['listeners'][0]['default_pool']['members'][0]
lb = models.LoadBalancer.from_dict(lb_dict)
for k in lb_dict.keys():
self.assertEqual(getattr(lb, k), lb_dict[k])
self.assertTrue(isinstance(lb.listeners[0], models.Listener))
self.assertTrue(isinstance(lb.listeners[0].default_pool,
models.Pool))
self.assertTrue(isinstance(lb.listeners[0].default_pool.members[0],
models.Member))
self.assertEqual(lb.listeners[0].id, expected_listener_id)
self.assertEqual(lb.listeners[0].default_pool.id, expected_pool_id)
self.assertEqual(lb.listeners[0].default_pool.members[0].id,
expected_member['id'])
class LoadBalancerConfigurationTest(TestCase):
def setUp(self):
super(LoadBalancerConfigurationTest, self).setUp()
self.conf_dict = fakes.fake_loadbalancer_dict(
listener=True, pool=True, members=True
)
def test_loadbalancer_config(self):
lb_conf = models.LoadBalancerConfiguration(self.conf_dict)
errors = lb_conf.validate()
lb_conf.to_dict()
self.assertEqual(errors, [])
def test_loadbalancer_config_validation_failed(self):
self.conf_dict.pop('id')
lb_conf = models.LoadBalancerConfiguration({})
errors = lb_conf.validate()
# id is required
self.assertEqual(len(errors), 1)

View File

@ -129,4 +129,3 @@ class ExecuteTest(TestCase):
utils.execute(['/bin/ls', '/no-such-directory'])
except RuntimeError as e:
self.assertIn('cannot access', str(e))