443 lines
17 KiB
Python
443 lines
17 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
# 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 abc
|
|
|
|
import netaddr
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import six
|
|
|
|
from neutron.agent.common import ovs_lib
|
|
from neutron.agent.linux import ip_lib
|
|
from neutron.agent.linux import utils
|
|
from neutron.common import constants as n_const
|
|
from neutron.common import exceptions
|
|
from neutron.common import ipv6_utils
|
|
from neutron.i18n import _LE, _LI
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
OPTS = [
|
|
cfg.StrOpt('ovs_integration_bridge',
|
|
default='br-int',
|
|
help=_('Name of Open vSwitch bridge to use')),
|
|
cfg.BoolOpt('ovs_use_veth',
|
|
default=False,
|
|
help=_('Uses veth for an interface or not')),
|
|
cfg.IntOpt('network_device_mtu',
|
|
help=_('MTU setting for device.')),
|
|
]
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class LinuxInterfaceDriver(object):
|
|
|
|
# from linux IF_NAMESIZE
|
|
DEV_NAME_LEN = 14
|
|
DEV_NAME_PREFIX = n_const.TAP_DEVICE_PREFIX
|
|
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
if self.conf.network_device_mtu:
|
|
self._validate_network_device_mtu()
|
|
|
|
def _validate_network_device_mtu(self):
|
|
if (ipv6_utils.is_enabled() and
|
|
self.conf.network_device_mtu < n_const.IPV6_MIN_MTU):
|
|
LOG.error(_LE("IPv6 protocol requires a minimum MTU of "
|
|
"%(min_mtu)s, while the configured value is "
|
|
"%(current_mtu)s"), {'min_mtu': n_const.IPV6_MIN_MTU,
|
|
'current_mtu': self.conf.network_device_mtu})
|
|
raise SystemExit(1)
|
|
|
|
def init_l3(self, device_name, ip_cidrs, namespace=None,
|
|
preserve_ips=[], gateway_ips=None,
|
|
clean_connections=False):
|
|
"""Set the L3 settings for the interface using data from the port.
|
|
|
|
ip_cidrs: list of 'X.X.X.X/YY' strings
|
|
preserve_ips: list of ip cidrs that should not be removed from device
|
|
gateway_ips: For gateway ports, list of external gateway ip addresses
|
|
clean_connections: Boolean to indicate if we should cleanup connections
|
|
associated to removed ips
|
|
"""
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
|
|
# The LLA generated by the operating system is not known to
|
|
# Neutron, so it would be deleted if we added it to the 'previous'
|
|
# list here
|
|
default_ipv6_lla = ip_lib.get_ipv6_lladdr(device.link.address)
|
|
previous = {addr['cidr'] for addr in device.addr.list(
|
|
filters=['permanent'])} - {default_ipv6_lla}
|
|
|
|
# add new addresses
|
|
for ip_cidr in ip_cidrs:
|
|
|
|
net = netaddr.IPNetwork(ip_cidr)
|
|
# Convert to compact IPv6 address because the return values of
|
|
# "ip addr list" are compact.
|
|
if net.version == 6:
|
|
ip_cidr = str(net)
|
|
if ip_cidr in previous:
|
|
previous.remove(ip_cidr)
|
|
continue
|
|
|
|
device.addr.add(ip_cidr)
|
|
|
|
# clean up any old addresses
|
|
for ip_cidr in previous:
|
|
if ip_cidr not in preserve_ips:
|
|
if clean_connections:
|
|
device.delete_addr_and_conntrack_state(ip_cidr)
|
|
else:
|
|
device.addr.delete(ip_cidr)
|
|
|
|
for gateway_ip in gateway_ips or []:
|
|
device.route.add_gateway(gateway_ip)
|
|
|
|
def init_router_port(self,
|
|
device_name,
|
|
ip_cidrs,
|
|
namespace,
|
|
preserve_ips=None,
|
|
gateway_ips=None,
|
|
extra_subnets=None,
|
|
enable_ra_on_gw=False,
|
|
clean_connections=False):
|
|
"""Set the L3 settings for a router interface using data from the port.
|
|
|
|
ip_cidrs: list of 'X.X.X.X/YY' strings
|
|
preserve_ips: list of ip cidrs that should not be removed from device
|
|
gateway_ips: For gateway ports, list of external gateway ip addresses
|
|
enable_ra_on_gw: Boolean to indicate configuring acceptance of IPv6 RA
|
|
clean_connections: Boolean to indicate if we should cleanup connections
|
|
associated to removed ips
|
|
extra_subnets: An iterable of cidrs to add as routes without address
|
|
"""
|
|
LOG.debug("init_router_port: device_name(%s), namespace(%s)",
|
|
device_name, namespace)
|
|
self.init_l3(device_name=device_name,
|
|
ip_cidrs=ip_cidrs,
|
|
namespace=namespace,
|
|
preserve_ips=preserve_ips or [],
|
|
gateway_ips=gateway_ips,
|
|
clean_connections=clean_connections)
|
|
|
|
if enable_ra_on_gw:
|
|
self.configure_ipv6_ra(namespace, device_name)
|
|
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
|
|
# Manage on-link routes (routes without an associated address)
|
|
new_onlink_routes = set(s['cidr'] for s in extra_subnets or [])
|
|
existing_onlink_routes = set(
|
|
device.route.list_onlink_routes(n_const.IP_VERSION_4) +
|
|
device.route.list_onlink_routes(n_const.IP_VERSION_6))
|
|
for route in new_onlink_routes - existing_onlink_routes:
|
|
LOG.debug("adding onlink route(%s)", route)
|
|
device.route.add_onlink_route(route)
|
|
for route in existing_onlink_routes - new_onlink_routes:
|
|
LOG.debug("deleting onlink route(%s)", route)
|
|
device.route.delete_onlink_route(route)
|
|
|
|
def add_ipv6_addr(self, device_name, v6addr, namespace, scope='global'):
|
|
device = ip_lib.IPDevice(device_name,
|
|
namespace=namespace)
|
|
net = netaddr.IPNetwork(v6addr)
|
|
device.addr.add(str(net), scope)
|
|
|
|
def delete_ipv6_addr(self, device_name, v6addr, namespace):
|
|
device = ip_lib.IPDevice(device_name,
|
|
namespace=namespace)
|
|
device.delete_addr_and_conntrack_state(v6addr)
|
|
|
|
def delete_ipv6_addr_with_prefix(self, device_name, prefix, namespace):
|
|
"""Delete the first listed IPv6 address that falls within a given
|
|
prefix.
|
|
"""
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
net = netaddr.IPNetwork(prefix)
|
|
for address in device.addr.list(scope='global', filters=['permanent']):
|
|
ip_address = netaddr.IPNetwork(address['cidr'])
|
|
if ip_address in net:
|
|
device.delete_addr_and_conntrack_state(address['cidr'])
|
|
break
|
|
|
|
def get_ipv6_llas(self, device_name, namespace):
|
|
device = ip_lib.IPDevice(device_name,
|
|
namespace=namespace)
|
|
|
|
return device.addr.list(scope='link', ip_version=6)
|
|
|
|
def check_bridge_exists(self, bridge):
|
|
if not ip_lib.device_exists(bridge):
|
|
raise exceptions.BridgeDoesNotExist(bridge=bridge)
|
|
|
|
def get_device_name(self, port):
|
|
return (self.DEV_NAME_PREFIX + port.id)[:self.DEV_NAME_LEN]
|
|
|
|
@staticmethod
|
|
def configure_ipv6_ra(namespace, dev_name):
|
|
"""Configure acceptance of IPv6 route advertisements on an intf."""
|
|
# Learn the default router's IP address via RAs
|
|
ip_lib.IPWrapper(namespace=namespace).netns.execute(
|
|
['sysctl', '-w', 'net.ipv6.conf.%s.accept_ra=2' % dev_name])
|
|
|
|
@abc.abstractmethod
|
|
def plug_new(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
"""Plug in the interface only for new devices that don't exist yet."""
|
|
|
|
def plug(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
if not ip_lib.device_exists(device_name,
|
|
namespace=namespace):
|
|
self.plug_new(network_id, port_id, device_name, mac_address,
|
|
bridge, namespace, prefix)
|
|
else:
|
|
LOG.info(_LI("Device %s already exists"), device_name)
|
|
|
|
@abc.abstractmethod
|
|
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
|
|
"""Unplug the interface."""
|
|
|
|
|
|
class NullDriver(LinuxInterfaceDriver):
|
|
def plug_new(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
pass
|
|
|
|
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
|
|
pass
|
|
|
|
|
|
class OVSInterfaceDriver(LinuxInterfaceDriver):
|
|
"""Driver for creating an internal interface on an OVS bridge."""
|
|
|
|
DEV_NAME_PREFIX = n_const.TAP_DEVICE_PREFIX
|
|
|
|
def __init__(self, conf):
|
|
super(OVSInterfaceDriver, self).__init__(conf)
|
|
if self.conf.ovs_use_veth:
|
|
self.DEV_NAME_PREFIX = 'ns-'
|
|
|
|
def _get_tap_name(self, dev_name, prefix=None):
|
|
if self.conf.ovs_use_veth:
|
|
dev_name = dev_name.replace(prefix or self.DEV_NAME_PREFIX,
|
|
n_const.TAP_DEVICE_PREFIX)
|
|
return dev_name
|
|
|
|
def _ovs_add_port(self, bridge, device_name, port_id, mac_address,
|
|
internal=True):
|
|
attrs = [('external_ids', {'iface-id': port_id,
|
|
'iface-status': 'active',
|
|
'attached-mac': mac_address})]
|
|
if internal:
|
|
attrs.insert(0, ('type', 'internal'))
|
|
|
|
ovs = ovs_lib.OVSBridge(bridge)
|
|
ovs.replace_port(device_name, *attrs)
|
|
|
|
def plug_new(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
"""Plug in the interface."""
|
|
if not bridge:
|
|
bridge = self.conf.ovs_integration_bridge
|
|
|
|
self.check_bridge_exists(bridge)
|
|
|
|
ip = ip_lib.IPWrapper()
|
|
tap_name = self._get_tap_name(device_name, prefix)
|
|
|
|
if self.conf.ovs_use_veth:
|
|
# Create ns_dev in a namespace if one is configured.
|
|
root_dev, ns_dev = ip.add_veth(tap_name,
|
|
device_name,
|
|
namespace2=namespace)
|
|
else:
|
|
ns_dev = ip.device(device_name)
|
|
|
|
internal = not self.conf.ovs_use_veth
|
|
self._ovs_add_port(bridge, tap_name, port_id, mac_address,
|
|
internal=internal)
|
|
|
|
ns_dev.link.set_address(mac_address)
|
|
|
|
if self.conf.network_device_mtu:
|
|
ns_dev.link.set_mtu(self.conf.network_device_mtu)
|
|
if self.conf.ovs_use_veth:
|
|
root_dev.link.set_mtu(self.conf.network_device_mtu)
|
|
|
|
# Add an interface created by ovs to the namespace.
|
|
if not self.conf.ovs_use_veth and namespace:
|
|
namespace_obj = ip.ensure_namespace(namespace)
|
|
namespace_obj.add_device_to_namespace(ns_dev)
|
|
|
|
ns_dev.link.set_up()
|
|
if self.conf.ovs_use_veth:
|
|
root_dev.link.set_up()
|
|
|
|
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
|
|
"""Unplug the interface."""
|
|
if not bridge:
|
|
bridge = self.conf.ovs_integration_bridge
|
|
|
|
tap_name = self._get_tap_name(device_name, prefix)
|
|
self.check_bridge_exists(bridge)
|
|
ovs = ovs_lib.OVSBridge(bridge)
|
|
|
|
try:
|
|
ovs.delete_port(tap_name)
|
|
if self.conf.ovs_use_veth:
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
device.link.delete()
|
|
LOG.debug("Unplugged interface '%s'", device_name)
|
|
except RuntimeError:
|
|
LOG.error(_LE("Failed unplugging interface '%s'"),
|
|
device_name)
|
|
|
|
|
|
class MidonetInterfaceDriver(LinuxInterfaceDriver):
|
|
|
|
def plug_new(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
"""This method is called by the Dhcp agent or by the L3 agent
|
|
when a new network is created
|
|
"""
|
|
ip = ip_lib.IPWrapper()
|
|
tap_name = device_name.replace(prefix or n_const.TAP_DEVICE_PREFIX,
|
|
n_const.TAP_DEVICE_PREFIX)
|
|
|
|
# Create ns_dev in a namespace if one is configured.
|
|
root_dev, ns_dev = ip.add_veth(tap_name, device_name,
|
|
namespace2=namespace)
|
|
|
|
ns_dev.link.set_address(mac_address)
|
|
|
|
# Add an interface created by ovs to the namespace.
|
|
namespace_obj = ip.ensure_namespace(namespace)
|
|
namespace_obj.add_device_to_namespace(ns_dev)
|
|
|
|
ns_dev.link.set_up()
|
|
root_dev.link.set_up()
|
|
|
|
cmd = ['mm-ctl', '--bind-port', port_id, device_name]
|
|
utils.execute(cmd, run_as_root=True)
|
|
|
|
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
|
|
# the port will be deleted by the dhcp agent that will call the plugin
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
try:
|
|
device.link.delete()
|
|
except RuntimeError:
|
|
LOG.error(_LE("Failed unplugging interface '%s'"), device_name)
|
|
LOG.debug("Unplugged interface '%s'", device_name)
|
|
|
|
ip_lib.IPWrapper(namespace=namespace).garbage_collect_namespace()
|
|
|
|
|
|
class IVSInterfaceDriver(LinuxInterfaceDriver):
|
|
"""Driver for creating an internal interface on an IVS bridge."""
|
|
|
|
DEV_NAME_PREFIX = n_const.TAP_DEVICE_PREFIX
|
|
|
|
def __init__(self, conf):
|
|
super(IVSInterfaceDriver, self).__init__(conf)
|
|
self.DEV_NAME_PREFIX = 'ns-'
|
|
|
|
def _get_tap_name(self, dev_name, prefix=None):
|
|
dev_name = dev_name.replace(prefix or self.DEV_NAME_PREFIX,
|
|
n_const.TAP_DEVICE_PREFIX)
|
|
return dev_name
|
|
|
|
def _ivs_add_port(self, device_name, port_id, mac_address):
|
|
cmd = ['ivs-ctl', 'add-port', device_name]
|
|
utils.execute(cmd, run_as_root=True)
|
|
|
|
def plug_new(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
"""Plug in the interface."""
|
|
ip = ip_lib.IPWrapper()
|
|
tap_name = self._get_tap_name(device_name, prefix)
|
|
|
|
root_dev, ns_dev = ip.add_veth(tap_name, device_name)
|
|
|
|
self._ivs_add_port(tap_name, port_id, mac_address)
|
|
|
|
ns_dev = ip.device(device_name)
|
|
ns_dev.link.set_address(mac_address)
|
|
|
|
if self.conf.network_device_mtu:
|
|
ns_dev.link.set_mtu(self.conf.network_device_mtu)
|
|
root_dev.link.set_mtu(self.conf.network_device_mtu)
|
|
|
|
if namespace:
|
|
namespace_obj = ip.ensure_namespace(namespace)
|
|
namespace_obj.add_device_to_namespace(ns_dev)
|
|
|
|
ns_dev.link.set_up()
|
|
root_dev.link.set_up()
|
|
|
|
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
|
|
"""Unplug the interface."""
|
|
tap_name = self._get_tap_name(device_name, prefix)
|
|
try:
|
|
cmd = ['ivs-ctl', 'del-port', tap_name]
|
|
utils.execute(cmd, run_as_root=True)
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
device.link.delete()
|
|
LOG.debug("Unplugged interface '%s'", device_name)
|
|
except RuntimeError:
|
|
LOG.error(_LE("Failed unplugging interface '%s'"),
|
|
device_name)
|
|
|
|
|
|
class BridgeInterfaceDriver(LinuxInterfaceDriver):
|
|
"""Driver for creating bridge interfaces."""
|
|
|
|
DEV_NAME_PREFIX = 'ns-'
|
|
|
|
def plug_new(self, network_id, port_id, device_name, mac_address,
|
|
bridge=None, namespace=None, prefix=None):
|
|
"""Plugin the interface."""
|
|
ip = ip_lib.IPWrapper()
|
|
|
|
# Enable agent to define the prefix
|
|
tap_name = device_name.replace(prefix or self.DEV_NAME_PREFIX,
|
|
n_const.TAP_DEVICE_PREFIX)
|
|
# Create ns_veth in a namespace if one is configured.
|
|
root_veth, ns_veth = ip.add_veth(tap_name, device_name,
|
|
namespace2=namespace)
|
|
ns_veth.link.set_address(mac_address)
|
|
|
|
if self.conf.network_device_mtu:
|
|
root_veth.link.set_mtu(self.conf.network_device_mtu)
|
|
ns_veth.link.set_mtu(self.conf.network_device_mtu)
|
|
|
|
root_veth.link.set_up()
|
|
ns_veth.link.set_up()
|
|
|
|
def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
|
|
"""Unplug the interface."""
|
|
device = ip_lib.IPDevice(device_name, namespace=namespace)
|
|
try:
|
|
device.link.delete()
|
|
LOG.debug("Unplugged interface '%s'", device_name)
|
|
except RuntimeError:
|
|
LOG.error(_LE("Failed unplugging interface '%s'"),
|
|
device_name)
|