astara-appliance/akanda/router/models.py

834 lines
25 KiB
Python

# Copyright 2014 DreamHost, LLC
#
# Author: DreamHost, 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.
import abc
import re
import netaddr
GROUP_NAME_LENGTH = 15
DEFAULT_AS = 64512
class ModelBase(object):
__metaclass__ = abc.ABCMeta
def __eq__(self, other):
return type(self) == type(other) and vars(self) == vars(other)
class Interface(ModelBase):
"""
"""
def __init__(self, ifname=None, addresses=[], groups=None, flags=None,
lladdr=None, mtu=1500, media=None,
description=None, **extra_params):
self.ifname = ifname
self.description = description
self.addresses = addresses
self.groups = [g[:GROUP_NAME_LENGTH] for g in (groups or [])]
self.flags = flags or []
self.lladdr = lladdr
self.mtu = mtu
self.media = media
self.extra_params = extra_params
self._aliases = []
def __repr__(self):
return '<Interface: %s %s>' % (self.ifname,
[str(a) for a in self.addresses])
def __eq__(self, other):
"""Check model equality only on limit fields."""
return (type(self) == type(other) and
self.ifname == other.ifname and
self.all_addresses == other.all_addresses and
self.description == other.description and
self.mtu == other.mtu and
self.groups == other.groups)
@property
def description(self):
return self._description
@description.setter
def description(self, value):
if not value:
self._description = ''
elif re.match('\w*$', value):
self._description = value
else:
raise ValueError('Description must be chars from [a-zA-Z0-9_]')
@property
def addresses(self):
return self._addresses
@addresses.setter
def addresses(self, value):
self._addresses = [netaddr.IPNetwork(a) for a in value]
@property
def aliases(self):
return self._aliases
@aliases.setter
def aliases(self, value):
self._aliases = [netaddr.IPNetwork(a) for a in value]
@property
def all_addresses(self):
return self._addresses + self._aliases
@property
def is_up(self):
if self.extra_params.get('state', '').lower() == 'up':
return 'UP'
return 'UP' in self.flags
@property
def first_v4(self):
return self._first_addr_for_version(4)
@property
def first_v6(self):
return self._first_addr_for_version(6)
def _first_addr_for_version(self, version):
addrs = sorted(a.ip for a in self._addresses if a.version == version)
if addrs:
return addrs[0]
@classmethod
def from_dict(cls, d):
return cls(**d)
def to_dict(self, extended=False):
include = ['ifname', 'groups', 'mtu', 'lladdr', 'media']
if extended:
include.extend(['flags', 'extra_params'])
retval = dict(
[(k, v) for k, v in vars(self).iteritems() if k in include])
retval['description'] = self.description
retval['addresses'] = self.addresses
retval['state'] = (self.is_up and 'up') or 'down'
return retval
class FilterRule(ModelBase):
"""
"""
def __init__(self, action=None, direction=None, interface=None,
family=None, protocol=None, source=None, source_port=None,
destination_interface=None,
destination=None, destination_port=None,
redirect=None, redirect_port=None):
self.action = action
self.direction = direction
self.interface = interface
self.family = family
self.protocol = protocol
self.source = source
self.source_port = source_port
self.destination_interface = destination_interface
self.destination = destination
self.destination_port = destination_port
self.redirect = redirect
self.redirect_port = redirect_port
def __setattr__(self, name, value):
if name != 'action' and not value:
pass
elif name == 'action':
if value not in ('pass', 'block'):
raise ValueError("Action must be 'pass' or 'block' not '%s'" %
value)
elif name in ('source', 'destination'):
if '/' in value:
value = netaddr.IPNetwork(value)
elif value.lower() == 'any':
value = None # any is the default so conver to None
elif name == 'direction':
if value not in ('in', 'out'):
raise ValueError(
"Direction must be 'in' or 'out' not '%s'" % value
)
elif name == 'redirect':
value = netaddr.IPAddress(value)
elif name.endswith('_port'):
value = int(value)
elif name == 'family':
if value not in ('inet', 'inet6'):
raise ValueError("Family must be 'inet', 'inet6', None and not"
" %s" % value)
elif name == 'protocol':
if value not in ('tcp', 'udp', 'imcp'):
raise ValueError("Protocol must be tcp|udp|imcp not '%s'." %
value)
super(FilterRule, self).__setattr__(name, value)
@classmethod
def from_dict(cls, d):
return FilterRule(**d)
@staticmethod
def _format_ip_or_table(obj):
if isinstance(obj, netaddr.IPNetwork):
return str(obj)
else: # must be table name
return '<%s>' % obj
class Anchor(ModelBase):
def __init__(self, name, rules=[]):
self.name = name
self.rules = rules
class AddressBookEntry(ModelBase):
def __init__(self, name, cidrs=[]):
self.name = name
self.cidrs = cidrs
@property
def cidrs(self):
return self._cidrs
@cidrs.setter
def cidrs(self, values):
self._cidrs = [netaddr.IPNetwork(a) for a in values]
def external_table_data(self):
return '\n'.join(map(str, self.cidrs))
class Allocation(ModelBase):
def __init__(self, mac_address, ip_addresses, hostname, device_id):
self.mac_address = mac_address
self.ip_addresses = ip_addresses or {}
self.hostname = hostname
self.device_id = device_id
@property
def dhcp_addresses(self):
return [ip for ip, dhcp in self.ip_addresses.items() if dhcp]
@classmethod
def from_dict(cls, d):
return cls(
d['mac_address'],
d['ip_addresses'],
d['hostname'],
d['device_id'],
)
class FloatingIP(ModelBase):
def __init__(self, floating_ip, fixed_ip):
self.floating_ip = floating_ip
self.fixed_ip = fixed_ip
self.network = None
@property
def floating_ip(self):
return self._floating_ip
@floating_ip.setter
def floating_ip(self, value):
self._floating_ip = netaddr.IPAddress(value)
@property
def fixed_ip(self):
return self._fixed_ip
@fixed_ip.setter
def fixed_ip(self, value):
self._fixed_ip = netaddr.IPAddress(value)
@classmethod
def from_dict(cls, d):
return cls(
d['floating_ip'],
d['fixed_ip']
)
class StaticRoute(ModelBase):
def __init__(self, destination, next_hop):
self.destination = destination
self.next_hop = next_hop
@property
def destination(self):
return self._destination
@destination.setter
def destination(self, value):
self._destination = netaddr.IPNetwork(value)
@property
def next_hop(self):
return self._next_hop
@next_hop.setter
def next_hop(self, value):
self._next_hop = netaddr.IPAddress(value)
def to_dict(self):
return dict(destination=self.destination, next_hop=self.next_hop)
class Label(ModelBase):
def __init__(self, name, cidrs=[]):
self.name = name
self.cidrs = cidrs
@property
def cidrs(self):
return self._cidrs
@cidrs.setter
def cidrs(self, values):
self._cidrs = [netaddr.IPNetwork(a) for a in values]
class Subnet(ModelBase):
def __init__(self, cidr, gateway_ip, dhcp_enabled=True,
dns_nameservers=None, host_routes=None):
self.cidr = cidr
self.gateway_ip = gateway_ip
self.dhcp_enabled = bool(dhcp_enabled)
self.dns_nameservers = dns_nameservers
self.host_routes = host_routes
@property
def cidr(self):
return self._cidr
@cidr.setter
def cidr(self, value):
self._cidr = netaddr.IPNetwork(value)
@property
def gateway_ip(self):
return self._gateway_ip
@gateway_ip.setter
def gateway_ip(self, value):
if value:
self._gateway_ip = netaddr.IPAddress(value)
else:
self._gateway_ip = None
@property
def dns_nameservers(self):
return self._dns_nameservers
@dns_nameservers.setter
def dns_nameservers(self, value):
self._dns_nameservers = [netaddr.IPAddress(a) for a in value]
@classmethod
def from_dict(cls, d):
host_routes = [StaticRoute(r['destination'], r['nexthop'])
for r in d.get('host_routes', [])]
return cls(
d['cidr'],
d['gateway_ip'],
d['dhcp_enabled'],
d['dns_nameservers'],
host_routes)
class Network(ModelBase):
SERVICE_STATIC = 'static'
SERVICE_RA = 'ra'
SERVICE_DHCP = 'dhcp'
TYPE_EXTERNAL = 'external'
TYPE_INTERNAL = 'internal'
TYPE_ISOLATED = 'isolated'
TYPE_MANAGEMENT = 'management'
TYPE_LOADBALANCER = 'loadbalancer'
# TODO(mark): add subnet support for Quantum subnet host routes
def __init__(self, id_, interface, name='', network_type=TYPE_ISOLATED,
v4_conf_service=SERVICE_STATIC,
v6_conf_service=SERVICE_STATIC,
address_allocations=None,
subnets=None):
self.id = id_
self.interface = interface
self.name = name
self.network_type = network_type
self.v4_conf_service = v4_conf_service
self.v6_conf_service = v6_conf_service
self.address_allocations = address_allocations or []
self.subnets = subnets or []
self.floating_ips = []
@property
def is_tenant_network(self):
return self._network_type in (self.TYPE_INTERNAL, self.TYPE_ISOLATED)
@property
def is_internal_network(self):
return self._network_type == self.TYPE_INTERNAL
@property
def is_external_network(self):
return self._network_type == self.TYPE_EXTERNAL
@property
def is_management_network(self):
return self._network_type == self.TYPE_MANAGEMENT
@property
def network_type(self):
return self._network_type
@network_type.setter
def network_type(self, value):
network_types = (self.TYPE_EXTERNAL, self.TYPE_INTERNAL,
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))
raise ValueError(msg)
self._network_type = value
@property
def v4_conf_service(self):
return self._v4_conf_service
@v4_conf_service.setter
def v4_conf_service(self, value):
if value not in (self.SERVICE_DHCP, self.SERVICE_STATIC):
msg = ('v4_conf_service must be one of dhcp|static not (%s).' %
value)
raise ValueError(msg)
self._v4_conf_service = value
@property
def v6_conf_service(self):
return self._v6_conf_service
@v6_conf_service.setter
def v6_conf_service(self, value):
if value not in (self.SERVICE_DHCP, self.SERVICE_RA,
self.SERVICE_STATIC):
msg = ('v6_conf_service must be one of dhcp|ra|static not (%s).' %
value)
raise ValueError(msg)
self._v6_conf_service = value
def to_dict(self):
return dict(
network_id=self.id,
interface=self.interface,
name=self.name,
network_type=self.network_type,
v4_conf_service=self.v4_conf_service,
v6_conf_service=self.v6_conf_service,
address_allocations=self.address_allocations
)
@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']),
name=d.get('name', ''),
network_type=d.get('network_type', cls.TYPE_ISOLATED),
v6_conf_service=d.get('v6_conf_service', cls.SERVICE_STATIC),
v4_conf_service=d.get('v4_conf_service', cls.SERVICE_STATIC),
address_allocations=[
Allocation.from_dict(a) for a in d.get('allocations', [])],
subnets=[Subnet.from_dict(s) for s in d.get('subnets', [])])
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.static_routes = [StaticRoute(*r) for r in
conf_dict.get('static_routes', [])]
self.address_book = dict(
(name, AddressBookEntry(name, cidrs)) for name, cidrs in
conf_dict.get('address_book', {}).iteritems())
self.anchors = [
Anchor(a['name'], [FilterRule.from_dict(r) for r in a['rules']])
for a in conf_dict.get('anchors', [])]
self.labels = [
Label(name, cidr) for name, cidr in
conf_dict.get('labels', {}).iteritems()]
self.floating_ips = [
FloatingIP.from_dict(fip)
for fip in conf_dict.get('floating_ips', [])
]
self._attach_floating_ips(self.floating_ips)
def validate(self):
"""Validate anchor rules to ensure that ifaces and tables exist."""
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):
if iface and iface not in interfaces:
errors.append((rule, '%s does not exist' % iface))
for address in (rule.source, rule.destination):
if not address or isinstance(address, netaddr.IPNetwork):
pass
elif address in self.address_book:
pass
else:
reason = '%s is not in the address book' % address
errors.append((rule, reason))
return ["'%s' %s" % e for e in errors]
def _attach_floating_ips(self, floating_ips):
ext_cidr_map = {}
int_cidr_map = {}
for network in self.networks:
if network.is_external_network:
m = ext_cidr_map
elif network.is_internal_network:
m = int_cidr_map
else:
continue
m.update((s.cidr, network) for s in network.subnets)
for fip in floating_ips:
# add address to external interface
for ext_cidr, net in ext_cidr_map.items():
if fip.floating_ip in ext_cidr:
addr = '%s/%s' % (fip.floating_ip, ext_cidr.prefixlen)
net.interface.aliases += [netaddr.IPNetwork(addr)]
net.floating_ips.append(fip)
break
# add to internal
for int_cidr, net in int_cidr_map.items():
if fip.fixed_ip in int_cidr:
fip.network = net
def to_dict(self):
fields = ('networks', 'address_book', 'anchors', 'static_routes')
return dict((f, getattr(self, f)) for f in fields)
@property
def external_v4_id(self):
addrs = [n.interface.first_v4
for n in self.networks if n.is_external_network]
# remove any none
addrs = sorted(a for a in addrs if a)
if addrs:
return addrs[0]
class LoadBalancerConfiguration(SystemConfiguration):
service_name = 'loadbalancer'
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 = []
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]