9f9b7d0fde
Our pep8 is now checking E731 and failing. This stops passing the lambda in questino around and instead just does the work in-line. Change-Id: I47c44a559f5e912386a004bf7655732e13e844d3
547 lines
19 KiB
Python
547 lines
19 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 logging
|
|
import re
|
|
|
|
import netaddr
|
|
|
|
from astara_router import models
|
|
from astara_router.drivers import base
|
|
from astara_router import utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
GENERIC_IFNAME = 'ge'
|
|
PHYSICAL_INTERFACES = ['lo', 'eth', 'em', 're', 'en', 'vio', 'vtnet']
|
|
ULA_PREFIX = 'fdca:3ba5:a17a:acda::/64'
|
|
|
|
|
|
class IPManager(base.Manager):
|
|
"""
|
|
A class that provides a pythonic interface to unix system network
|
|
configuration information.
|
|
"""
|
|
|
|
EXECUTABLE = '/sbin/ip'
|
|
|
|
def __init__(self, root_helper='sudo'):
|
|
"""Initializes resources for the IPManager class"""
|
|
super(IPManager, self).__init__(root_helper)
|
|
self.next_generic_index = 0
|
|
self.host_mapping = {}
|
|
self.generic_mapping = {}
|
|
|
|
def ensure_mapping(self):
|
|
"""
|
|
Creates a mapping of generic interface names (e.g., ge0, ge1) to
|
|
physical interface names (eth1, eth2) if one does not already exist.
|
|
"""
|
|
if not self.host_mapping:
|
|
self.get_interfaces()
|
|
|
|
def get_interfaces(self):
|
|
"""
|
|
Returns a list of the available network interfaces. This information
|
|
is obtained through the `ip addr show` system command.
|
|
"""
|
|
interfaces = _parse_interfaces(self.do('addr', 'show'),
|
|
filters=PHYSICAL_INTERFACES)
|
|
|
|
interfaces.sort(key=lambda x: x.ifname)
|
|
for i in interfaces:
|
|
if i.ifname not in self.host_mapping:
|
|
generic_name = 'ge%d' % self.next_generic_index
|
|
self.host_mapping[i.ifname] = generic_name
|
|
self.next_generic_index += 1
|
|
|
|
# change ifname to generic version
|
|
i.ifname = self.host_mapping[i.ifname]
|
|
self.generic_mapping = dict((v, k) for k, v in
|
|
self.host_mapping.iteritems())
|
|
|
|
return interfaces
|
|
|
|
def get_interface(self, ifname):
|
|
"""
|
|
Returns network configuration information for the requested network
|
|
interface. This information is obtained through the system command `ip
|
|
addr show <ifname>`.
|
|
|
|
:param ifname: the name of the interface to retrieve, e.g., `eth1`
|
|
:type ifname: str
|
|
:rtype: astara_router.model.Interface
|
|
"""
|
|
real_ifname = self.generic_to_host(ifname)
|
|
retval = _parse_interface(self.do('addr', 'show', real_ifname))
|
|
retval.ifname = ifname
|
|
return retval
|
|
|
|
def is_valid(self, ifname):
|
|
"""
|
|
Validates if the supplied interface is a valid system network
|
|
interface. Returns `True` if <ifname> is a valid interface. Returns
|
|
`False` if <ifname> is not a valid interface.
|
|
|
|
:param ifname: the name of the interface to retrieve, e.g., `eth1`
|
|
:type ifname: str
|
|
"""
|
|
self.ensure_mapping()
|
|
return ifname in self.generic_mapping
|
|
|
|
def generic_to_host(self, generic_name):
|
|
"""
|
|
Translates a generic interface name into the physical network interface
|
|
name.
|
|
:param ifname: the generic name to translate, e.g., `ge0`
|
|
:type ifname: str
|
|
:rtype: str
|
|
"""
|
|
self.ensure_mapping()
|
|
return self.generic_mapping.get(generic_name)
|
|
|
|
def host_to_generic(self, real_name):
|
|
"""
|
|
Translates a physical interface name into the generic network interface
|
|
name.
|
|
:param ifname: the physical name to translate, e.g., `eth0`
|
|
:type ifname: str
|
|
:rtype: str
|
|
"""
|
|
self.ensure_mapping()
|
|
return self.host_mapping.get(real_name)
|
|
|
|
def update_interfaces(self, interfaces):
|
|
"""
|
|
Wrapper function that accepts a list of interfaces and iterates over
|
|
them, calling update_interface(<interface>) in order to update
|
|
their configuration.
|
|
"""
|
|
for i in interfaces:
|
|
self.update_interface(i)
|
|
|
|
def up(self, interface):
|
|
"""
|
|
Sets the administrative mode for the network link on interface
|
|
<interface> to "up".
|
|
:param interface: the interface to mark up
|
|
:type interface: astara_router.models.Interface
|
|
"""
|
|
real_ifname = self.generic_to_host(interface.ifname)
|
|
self.sudo('link', 'set', real_ifname, 'up')
|
|
return self.get_interface(interface.ifname)
|
|
|
|
def down(self, interface):
|
|
"""
|
|
Sets the administrative mode for the network link on interface
|
|
<interface> to "down".
|
|
:param interface: the interface to mark down
|
|
:type interface: astara_router.models.Interface
|
|
"""
|
|
real_ifname = self.generic_to_host(interface.ifname)
|
|
self.sudo('link', 'set', real_ifname, 'down')
|
|
|
|
def update_interface(self, interface, ignore_link_local=True):
|
|
"""
|
|
Updates a network interface, particularly its addresses
|
|
:param interface: the interface to update
|
|
:type interface: astara_router.models.Interface
|
|
:param ignore_link_local: When True, link local addresses will not be
|
|
added/removed
|
|
:type ignore_link_local: bool
|
|
"""
|
|
real_ifname = self.generic_to_host(interface.ifname)
|
|
old_interface = self.get_interface(interface.ifname)
|
|
|
|
if ignore_link_local:
|
|
interface.addresses = [a for a in interface.addresses
|
|
if not a.is_link_local()]
|
|
old_interface.addresses = [a for a in old_interface.addresses
|
|
if not a.is_link_local()]
|
|
# Must update primary before aliases otherwise will lose address
|
|
# in case where primary and alias are swapped.
|
|
self._update_addresses(real_ifname, interface, old_interface)
|
|
|
|
def _update_addresses(self, real_ifname, interface, old_interface):
|
|
"""
|
|
Compare the state of an interface, and add/remove address that have
|
|
changed.
|
|
:param real_ifname: the name of the interface to modify
|
|
:param real_ifname: str
|
|
:param interface: the new interface reference
|
|
:type interface: astara_router.models.Interface
|
|
:param old_interface: the reference to the current network interface
|
|
:type old_interface: astara_router.models.Interface
|
|
"""
|
|
|
|
def _gen_cmd(cmd, address):
|
|
"""
|
|
Generates an `ip addr (add|del) <cidr> dev <ifname>` command.
|
|
"""
|
|
family = {4: 'inet', 6: 'inet6'}[address[0].version]
|
|
args = ['addr', cmd, '%s/%s' % (address[0], address[1])]
|
|
if family == 'inet' and cmd == 'add':
|
|
args += ['brd', '+']
|
|
args += ['dev', real_ifname]
|
|
if family == 'inet6':
|
|
args = ['-6'] + args
|
|
return args
|
|
|
|
add = functools.partial(_gen_cmd, 'add')
|
|
delete = functools.partial(_gen_cmd, 'del')
|
|
|
|
self._update_set(real_ifname, interface, old_interface,
|
|
'all_addresses', add, delete)
|
|
|
|
def _update_set(self, real_ifname, interface, old_interface, attribute,
|
|
fmt_args_add, fmt_args_delete):
|
|
"""
|
|
Compare the set of addresses (the current set and the desired set)
|
|
for an interface and generate a series of `ip addr add` and `ip addr
|
|
del` commands.
|
|
"""
|
|
next_set = set((i.ip, i.prefixlen)
|
|
for i in getattr(interface, attribute))
|
|
prev_set = set((i.ip, i.prefixlen)
|
|
for i in getattr(old_interface, attribute))
|
|
|
|
if next_set == prev_set:
|
|
return
|
|
|
|
for item in (next_set - prev_set):
|
|
self.sudo(*fmt_args_add(item))
|
|
self.up(interface)
|
|
|
|
for item in (prev_set - next_set):
|
|
self.sudo(*fmt_args_delete(item))
|
|
ip, prefix = item
|
|
if ip.version == 4:
|
|
self._delete_conntrack_state(ip)
|
|
|
|
def update_default_gateway(self, config):
|
|
"""
|
|
Sets the default gateway for v4 and v6 via the use of `ip route add`.
|
|
|
|
:type config: astara_router.models.Configuration
|
|
"""
|
|
# Track whether we have set the default gateways, by IP
|
|
# version.
|
|
gw_set = {
|
|
4: False,
|
|
6: False,
|
|
}
|
|
|
|
ifname = None
|
|
for net in config.networks:
|
|
if not net.is_external_network:
|
|
continue
|
|
ifname = net.interface.ifname
|
|
|
|
# The default v4 gateway is pulled out as a special case
|
|
# because we only want one but we might have multiple v4
|
|
# subnets on the external network. However, sometimes the RUG
|
|
# can't figure out what that value is, because it thinks we
|
|
# don't have any external IP addresses, yet. In that case, it
|
|
# doesn't give us a default.
|
|
if config.default_v4_gateway:
|
|
self._set_default_gateway(config.default_v4_gateway, ifname)
|
|
gw_set[4] = True
|
|
|
|
# Look through our networks and make sure we have a default
|
|
# gateway set for each IP version, if we have an IP for that
|
|
# version on the external net. If we haven't already set the
|
|
# v4 gateway, this picks the gateway for the first subnet we
|
|
# find, which might be wrong.
|
|
for net in config.networks:
|
|
if not net.is_external_network:
|
|
continue
|
|
|
|
for subnet in net.subnets:
|
|
if subnet.gateway_ip and not gw_set[subnet.gateway_ip.version]:
|
|
self._set_default_gateway(
|
|
subnet.gateway_ip,
|
|
net.interface.ifname
|
|
)
|
|
gw_set[subnet.gateway_ip.version] = True
|
|
|
|
def update_host_routes(self, config, cache):
|
|
"""
|
|
Update the network routes. This is primarily used to support static
|
|
routes that users provide to neutron.
|
|
|
|
:type config: astara_router.models.Configuration
|
|
:param cache: a dbm cache for storing the "last applied routes".
|
|
Because Linux does not differentiate user-provided routes
|
|
from, for example, the default gateway, this is necessary
|
|
so that subsequent calls to this method can determine
|
|
"what changed" for the user-provided routes.
|
|
:type cache: dogpile.cache.region.CacheRegion
|
|
"""
|
|
db = cache.get_or_create('host_routes', lambda: {})
|
|
for net in config.networks:
|
|
|
|
# For each subnet...
|
|
for subnet in net.subnets:
|
|
cidr = str(subnet.cidr)
|
|
|
|
# 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(net.interface.ifname, 'del', *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(net.interface.ifname, 'add', *x):
|
|
current.add(x)
|
|
|
|
if not current:
|
|
del db[cidr]
|
|
|
|
cache.set('host_routes', db)
|
|
|
|
def _get_default_gateway(self, version):
|
|
"""
|
|
Gets the default gateway.
|
|
|
|
:param version: the IP version, 4 or 6
|
|
:type version: int
|
|
:rtype: str
|
|
"""
|
|
try:
|
|
cmd_out = self.sudo('-%s' % version, 'route', 'show')
|
|
except:
|
|
# assume the route is missing and use defaults
|
|
pass
|
|
else:
|
|
for l in cmd_out.splitlines():
|
|
l = l.strip()
|
|
if l.startswith('default'):
|
|
match = re.search('via (?P<gateway>[^ ]+)', l)
|
|
if match:
|
|
return match.group('gateway')
|
|
|
|
def _set_default_gateway(self, gateway_ip, ifname):
|
|
"""
|
|
Sets the default gateway.
|
|
|
|
:param gateway_ip: the IP address to set as the default gateway_ip
|
|
:type gateway_ip: netaddr.IPAddress
|
|
:param ifname: the interface name (in our case, of the external
|
|
network)
|
|
:type ifname: str
|
|
"""
|
|
version = 4
|
|
if gateway_ip.version == 6:
|
|
version = 6
|
|
current = self._get_default_gateway(version)
|
|
desired = str(gateway_ip)
|
|
ifname = self.generic_to_host(ifname)
|
|
|
|
if current and current != desired:
|
|
# Remove the current gateway and add the desired one
|
|
self.sudo(
|
|
'-%s' % version, 'route', 'del', 'default', 'via', current,
|
|
'dev', ifname
|
|
)
|
|
return self.sudo(
|
|
'-%s' % version, 'route', 'add', 'default', 'via', desired,
|
|
'dev', ifname
|
|
)
|
|
if not current:
|
|
# Add the desired gateway
|
|
return self.sudo(
|
|
'-%s' % version, 'route', 'add', 'default', 'via', desired,
|
|
'dev', ifname
|
|
)
|
|
|
|
def _alter_route(self, ifname, action, destination, next_hop):
|
|
"""
|
|
Apply/remove a custom (generally, user-supplied) route using the `ip
|
|
route add/delete` command.
|
|
|
|
:param ifname: The name of the interface on which to alter the route
|
|
:type ifname: str
|
|
:param action: The action, 'add' or 'del'
|
|
:type action: str
|
|
:param destination: The destination CIDR
|
|
:type destination: netaddr.IPNetwork
|
|
:param next_hop: The next hop IP addressj
|
|
:type next_hop: netaddr.IPAddress
|
|
"""
|
|
version = destination.version
|
|
ifname = self.generic_to_host(ifname)
|
|
try:
|
|
LOG.debug(self.sudo(
|
|
'-%s' % version, 'route', action, str(destination), 'via',
|
|
str(next_hop), 'dev', ifname
|
|
))
|
|
return True
|
|
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.warning('Route could not be %sed: %s' % (action, unicode(e)))
|
|
return False
|
|
|
|
def disable_duplicate_address_detection(self, network):
|
|
"""
|
|
Disabled duplicate address detection for a specific interface.
|
|
|
|
:type network: astara.models.Network
|
|
"""
|
|
# For non-external networks, duplicate address detection isn't
|
|
# necessary (and it sometimes results in race conditions for services
|
|
# that attempt to bind to addresses before they're ready).
|
|
|
|
if network.network_type != network.TYPE_EXTERNAL:
|
|
real_ifname = self.generic_to_host(network.interface.ifname)
|
|
try:
|
|
utils.execute([
|
|
'sysctl', '-w', 'net.ipv6.conf.%s.accept_dad=0'
|
|
% real_ifname
|
|
], self.root_helper)
|
|
except RuntimeError:
|
|
LOG.debug(
|
|
'Failed to disable v6 dad on %s' % real_ifname
|
|
)
|
|
|
|
def _delete_conntrack_state(self, ip):
|
|
"""
|
|
Explicitly remove an IP from in-kernel connection tracking.
|
|
|
|
:param ip: The IP address to remove
|
|
:type ip: netaddr.IPAddress
|
|
"""
|
|
|
|
# If no flow entries are deleted, `conntrack -D` will return 1
|
|
try:
|
|
utils.execute(['conntrack', '-D', '-d', str(ip)], self.root_helper)
|
|
except RuntimeError:
|
|
LOG.debug(
|
|
'Failed deleting ingress connection state of %s' % ip
|
|
)
|
|
try:
|
|
utils.execute(['conntrack', '-D', '-q', str(ip)], self.root_helper)
|
|
except RuntimeError:
|
|
LOG.debug(
|
|
'Failed deleting egress connection state of %s' % ip
|
|
)
|
|
|
|
|
|
def get_rug_address():
|
|
""" Return the RUG address """
|
|
net = netaddr.IPNetwork(ULA_PREFIX)
|
|
return str(netaddr.IPAddress(net.first + 1))
|
|
|
|
|
|
def _parse_interfaces(data, filters=None):
|
|
"""
|
|
Parse the output of `ip addr show`.
|
|
|
|
:param data: the output of `ip addr show`
|
|
:type data: str
|
|
:param filter: a list of valid interface names to match on
|
|
:type data: list of str
|
|
:rtype: list of astara_router.models.Interface
|
|
"""
|
|
retval = []
|
|
for iface_data in re.split('(^|\n)(?=[0-9]+: \w+\d{0,3}:)', data):
|
|
if not iface_data.strip():
|
|
continue
|
|
number, interface = iface_data.split(': ', 1)
|
|
|
|
# FIXME (mark): the logic works, but should be more readable
|
|
for f in filters or ['']:
|
|
if f == '':
|
|
break
|
|
elif interface.startswith(f) and interface[len(f)].isdigit():
|
|
break
|
|
else:
|
|
continue
|
|
|
|
retval.append(_parse_interface(iface_data))
|
|
return retval
|
|
|
|
|
|
def _parse_interface(data):
|
|
"""
|
|
Parse details for an interface, given its data from `ip addr show <ifname>`
|
|
|
|
:rtype: astara_router.models.Interface
|
|
"""
|
|
retval = dict(addresses=[])
|
|
for line in data.split('\n'):
|
|
if line.startswith(' '):
|
|
line = line.strip()
|
|
if line.startswith('inet'):
|
|
retval['addresses'].append(_parse_inet(line))
|
|
elif 'link/ether' in line:
|
|
retval['lladdr'] = _parse_lladdr(line)
|
|
else:
|
|
retval.update(_parse_head(line))
|
|
|
|
return models.Interface.from_dict(retval)
|
|
|
|
|
|
def _parse_head(line):
|
|
"""
|
|
Parse the line of `ip addr show` that contains the interface name, MTU, and
|
|
flags.
|
|
"""
|
|
retval = {}
|
|
m = re.match(
|
|
'[0-9]+: (?P<if>\w+\d{1,3}): <(?P<flags>[^>]+)> mtu (?P<mtu>[0-9]+)',
|
|
line
|
|
)
|
|
if m:
|
|
retval['ifname'] = m.group('if')
|
|
retval['mtu'] = int(m.group('mtu'))
|
|
retval['flags'] = m.group('flags').split(',')
|
|
return retval
|
|
|
|
|
|
def _parse_inet(line):
|
|
"""
|
|
Parse a line of `ip addr show` that contains an address.
|
|
"""
|
|
tokens = line.split()
|
|
return netaddr.IPNetwork(tokens[1])
|
|
|
|
|
|
def _parse_lladdr(line):
|
|
"""
|
|
Parse the line of `ip addr show` that contains the hardware address.
|
|
"""
|
|
tokens = line.split()
|
|
return tokens[1]
|