329 lines
10 KiB
Python
329 lines
10 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 functools
|
|
import netaddr
|
|
import logging
|
|
import random
|
|
|
|
from neutron.api.v2 import attributes
|
|
from neutron.common.config import cfg
|
|
from neutron.common import exceptions as q_exc
|
|
from neutron.db import models_v2 as qmodels
|
|
from neutron.db import l3_db
|
|
from neutron.i18n import _
|
|
from neutron import manager
|
|
|
|
from neutron.plugins.common import constants
|
|
|
|
|
|
IPV6_ASSIGNMENT_ATTEMPTS = 1000
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
akanda_opts = [
|
|
cfg.StrOpt('akanda_ipv6_tenant_range',
|
|
default='fdd6:a1fa:cfa8::/48',
|
|
help='IPv6 address prefix'),
|
|
cfg.IntOpt('akanda_ipv6_prefix_length',
|
|
default=64,
|
|
help='Default length of prefix to pre-assign'),
|
|
cfg.ListOpt(
|
|
'akanda_allowed_cidr_ranges',
|
|
default=['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7'],
|
|
help='List of allowed subnet cidrs for non-admin users'),
|
|
cfg.BoolOpt(
|
|
'astara_auto_add_resources',
|
|
default=True,
|
|
help='Attempt to auto add resources to speed up network construction'
|
|
)
|
|
]
|
|
|
|
cfg.CONF.register_opts(akanda_opts)
|
|
|
|
SUPPORTED_EXTENSIONS = [
|
|
'dhrouterstatus',
|
|
]
|
|
|
|
|
|
def auto_add_ipv6_subnet(f):
|
|
@functools.wraps(f)
|
|
def wrapper(self, context, network):
|
|
LOG.debug('auto_add_ipv6_subnet')
|
|
net = f(self, context, network)
|
|
if cfg.CONF.astara_auto_add_resources:
|
|
_add_ipv6_subnet(context, net)
|
|
return net
|
|
return wrapper
|
|
|
|
|
|
def auto_add_subnet_to_router(f):
|
|
@functools.wraps(f)
|
|
def wrapper(self, context, subnet):
|
|
LOG.debug('auto_add_subnet_to_router')
|
|
check_subnet_cidr_meets_policy(context, subnet)
|
|
subnet = f(self, context, subnet)
|
|
if cfg.CONF.astara_auto_add_resources:
|
|
_add_subnet_to_router(context, subnet)
|
|
return subnet
|
|
return wrapper
|
|
|
|
|
|
# NOTE(mark): in Havana gateway_ip cannot be updated leaving here if this
|
|
# returns in Icehouse.
|
|
def sync_subnet_gateway_port(f):
|
|
@functools.wraps(f)
|
|
def wrapper(self, context, id, subnet):
|
|
LOG.debug('sync_subnet_gateway_port')
|
|
retval = f(self, context, id, subnet)
|
|
_update_internal_gateway_port_ip(context, retval)
|
|
return retval
|
|
return wrapper
|
|
|
|
|
|
def check_subnet_cidr_meets_policy(context, subnet):
|
|
if context.is_admin:
|
|
return
|
|
elif getattr(context, '_akanda_auto_add', None):
|
|
return
|
|
|
|
net = netaddr.IPNetwork(subnet['subnet']['cidr'])
|
|
|
|
for allowed_cidr in cfg.CONF.akanda_allowed_cidr_ranges:
|
|
if net in netaddr.IPNetwork(allowed_cidr):
|
|
return
|
|
|
|
else:
|
|
reason = _('Cannot create a subnet that is not within the '
|
|
'allowed address ranges [%s].' %
|
|
cfg.CONF.akanda_allowed_cidr_ranges)
|
|
raise q_exc.AdminRequired(reason=reason)
|
|
|
|
|
|
def get_special_ipv6_addrs(ips, mac_address):
|
|
current_ips = set(ips)
|
|
special_ips = set([_generate_ipv6_address('fe80::/64', mac_address)])
|
|
|
|
akanda_ipv6_cidr = netaddr.IPNetwork(cfg.CONF.akanda_ipv6_tenant_range)
|
|
|
|
for ip in current_ips:
|
|
if '/' not in ip and netaddr.IPAddress(ip) in akanda_ipv6_cidr:
|
|
# Calculate the cidr here because the caller does not have access
|
|
# to request context, subnet or port_id.
|
|
special_ips.add(
|
|
'%s/%s' % (
|
|
netaddr.IPAddress(
|
|
netaddr.IPNetwork(
|
|
'%s/%d' % (ip, cfg.CONF.akanda_ipv6_prefix_length)
|
|
).first
|
|
),
|
|
cfg.CONF.akanda_ipv6_prefix_length
|
|
)
|
|
)
|
|
return special_ips - current_ips
|
|
|
|
|
|
def _add_subnet_to_router(context, subnet):
|
|
LOG.debug('_add_subnet_to_router')
|
|
if context.is_admin:
|
|
# admins can manually add their own interfaces
|
|
return
|
|
|
|
if not subnet.get('gateway_ip'):
|
|
return
|
|
|
|
service_plugin = manager.NeutronManager.get_service_plugins().get(
|
|
constants.L3_ROUTER_NAT)
|
|
|
|
router_q = context.session.query(l3_db.Router)
|
|
router_q = router_q.filter_by(tenant_id=context.tenant_id)
|
|
|
|
router = router_q.first()
|
|
|
|
if not router:
|
|
router_args = {
|
|
'tenant_id': subnet['tenant_id'],
|
|
'name': 'ak-%s' % subnet['tenant_id'],
|
|
'admin_state_up': True
|
|
}
|
|
router = service_plugin.create_router(context, {'router': router_args})
|
|
if not _update_internal_gateway_port_ip(context, router['id'], subnet):
|
|
service_plugin.add_router_interface(context.elevated(),
|
|
router['id'],
|
|
{'subnet_id': subnet['id']})
|
|
|
|
|
|
def _update_internal_gateway_port_ip(context, router_id, subnet):
|
|
"""Attempt to update internal gateway port if one already exists."""
|
|
LOG.debug(
|
|
'setting gateway port IP for router %s on network %s for subnet %s',
|
|
router_id,
|
|
subnet['network_id'],
|
|
subnet['id'],
|
|
)
|
|
if not subnet.get('gateway_ip'):
|
|
LOG.debug('no gateway set for subnet %s, skipping', subnet['id'])
|
|
return
|
|
|
|
q = context.session.query(l3_db.RouterPort)
|
|
q = q.join(qmodels.Port)
|
|
q = q.filter(
|
|
l3_db.RouterPort.router_id == router_id,
|
|
l3_db.RouterPort.port_type == l3_db.DEVICE_OWNER_ROUTER_INTF,
|
|
qmodels.Port.network_id == subnet['network_id']
|
|
|
|
)
|
|
routerport = q.first()
|
|
|
|
if not routerport:
|
|
LOG.info(
|
|
'Unable to find a %s port for router %s on network %s.'
|
|
% ('DEVICE_OWNER_ROUTER_INTF', router_id, subnet['network_id'])
|
|
)
|
|
return
|
|
|
|
fixed_ips = [
|
|
{'subnet_id': ip["subnet_id"], 'ip_address': ip["ip_address"]}
|
|
for ip in routerport.port["fixed_ips"]
|
|
]
|
|
|
|
plugin = manager.NeutronManager.get_plugin()
|
|
service_plugin = manager.NeutronManager.get_service_plugins().get(
|
|
constants.L3_ROUTER_NAT)
|
|
|
|
for index, ip in enumerate(fixed_ips):
|
|
if ip['subnet_id'] == subnet['id']:
|
|
if not subnet['gateway_ip']:
|
|
del fixed_ips[index]
|
|
elif ip['ip_address'] != subnet['gateway_ip']:
|
|
ip['ip_address'] = subnet['gateway_ip']
|
|
else:
|
|
return True # nothing to update
|
|
break
|
|
else:
|
|
try:
|
|
service_plugin._check_for_dup_router_subnet(
|
|
context,
|
|
routerport.router,
|
|
subnet['network_id'],
|
|
subnet['id'],
|
|
subnet['cidr']
|
|
)
|
|
except:
|
|
LOG.info(
|
|
('Subnet %(id)s will not be auto added to router because '
|
|
'%(gateway_ip)s is already in use by another attached '
|
|
'network attached to this router.'),
|
|
subnet
|
|
)
|
|
return True # nothing to add
|
|
fixed_ips.append(
|
|
{'subnet_id': subnet['id'], 'ip_address': subnet['gateway_ip']}
|
|
)
|
|
|
|
# we call into the plugin vs updating the db directly because of l3 hooks
|
|
# baked into the plugins.
|
|
port_dict = {'fixed_ips': fixed_ips}
|
|
plugin.update_port(
|
|
context.elevated(),
|
|
routerport.port['id'],
|
|
{'port': port_dict}
|
|
)
|
|
return True
|
|
|
|
|
|
def _add_ipv6_subnet(context, network):
|
|
|
|
plugin = manager.NeutronManager.get_plugin()
|
|
|
|
try:
|
|
subnet_generator = _ipv6_subnet_generator(
|
|
cfg.CONF.akanda_ipv6_tenant_range,
|
|
cfg.CONF.akanda_ipv6_prefix_length)
|
|
except:
|
|
LOG.exception('Unable able to add tenant IPv6 subnet.')
|
|
return
|
|
|
|
remaining = IPV6_ASSIGNMENT_ATTEMPTS
|
|
|
|
while remaining:
|
|
remaining -= 1
|
|
|
|
candidate_cidr = subnet_generator.next()
|
|
|
|
sub_q = context.session.query(qmodels.Subnet)
|
|
sub_q = sub_q.filter_by(cidr=str(candidate_cidr))
|
|
existing = sub_q.all()
|
|
|
|
if not existing:
|
|
create_args = {
|
|
'tenant_id': network['tenant_id'],
|
|
'network_id': network['id'],
|
|
'name': '',
|
|
'cidr': str(candidate_cidr),
|
|
'ip_version': candidate_cidr.version,
|
|
'enable_dhcp': True,
|
|
'ipv6_address_mode': 'slaac',
|
|
'ipv6_ra_mode': 'slaac',
|
|
'gateway_ip': attributes.ATTR_NOT_SPECIFIED,
|
|
'dns_nameservers': attributes.ATTR_NOT_SPECIFIED,
|
|
'host_routes': attributes.ATTR_NOT_SPECIFIED,
|
|
'allocation_pools': attributes.ATTR_NOT_SPECIFIED
|
|
}
|
|
context._akanda_auto_add = True
|
|
plugin.create_subnet(context, {'subnet': create_args})
|
|
del context._akanda_auto_add
|
|
break
|
|
else:
|
|
LOG.error('Unable to generate a unique tenant subnet cidr')
|
|
|
|
|
|
def _ipv6_subnet_generator(network_range, prefixlen):
|
|
# coerce prefixlen to stay within bounds
|
|
prefixlen = min(128, prefixlen)
|
|
|
|
net = netaddr.IPNetwork(network_range)
|
|
if net.version != 6:
|
|
raise ValueError('Tenant range %s is not a valid IPv6 cidr' %
|
|
network_range)
|
|
|
|
if prefixlen < net.prefixlen:
|
|
raise ValueError('Prefixlen (/%d) must be larger than the network '
|
|
'range prefixlen (/%s)' % (prefixlen, net.prefixlen))
|
|
|
|
rand = random.SystemRandom()
|
|
max_range = 2 ** (prefixlen - net.prefixlen)
|
|
|
|
while True:
|
|
rand_bits = rand.randint(0, max_range)
|
|
|
|
candidate_cidr = netaddr.IPNetwork(
|
|
netaddr.IPAddress(net.value + (rand_bits << prefixlen)))
|
|
candidate_cidr.prefixlen = prefixlen
|
|
|
|
yield candidate_cidr
|
|
|
|
|
|
# Note(rods): we need to keep this method untill the nsx driver won't
|
|
# be updated to use neutron's native support for slaac
|
|
def _generate_ipv6_address(cidr, mac_address):
|
|
network = netaddr.IPNetwork(cidr)
|
|
tokens = ['%02x' % int(t, 16) for t in mac_address.split(':')]
|
|
eui64 = int(''.join(tokens[0:3] + ['ff', 'fe'] + tokens[3:6]), 16)
|
|
|
|
# the bit inversion is required by the RFC
|
|
return str(netaddr.IPAddress(network.value + (eui64 ^ 0x0200000000000000)))
|